Repository: doccano/doccano-client (language: python)

Issue:
get_document_list doesn't work with offset>9
when calling method get_document_list using an offset parameter, the results are wrong when offset is greater than 9. There is an error in the build_url_parameter method, that split offset=15 to ?offset=1&offset=5
Add ability to create a new project via api
Feature description
---------
Often I need to create multiple, very similar projects. Currently the workflow is to go into the UI and manually create these - it's very repetitive and time consuming. It would be great to have the ability to create a new project using the API, including the ability to upload a dataset, labels, and assign members.

Add test cases for usecases
# Description

- comment
- data export
- data import
- example
- label type
- label
- member
- project
- role


Discussion / hints:
I have found the same thing, I built a simple work around for myself.
Changed the build_url_parameter to look like this:
```python
    def build_url_parameter(
            self,
            url_parameter: dict
            ) -> str:
        """
        Format url_parameters.

        Args:
            url_parameter (dict): Every value must be a list.

        Returns:
            A URL parameter string. Ex: `?key1=u1&key1=u2&key2=v1&...`
        """
        params="".join([f"&{tup[0]}={tup[1]}" for tup in url_parameter.items()])
        return "?"+params[1:]
```


> I have found the same thing, I built a simple work around for myself.
> Changed the build_url_parameter to look like this:
> 
> ```python
>     def build_url_parameter(
>             self,
>             url_parameter: dict
>             ) -> str:
>         """
>         Format url_parameters.
> 
>         Args:
>             url_parameter (dict): Every value must be a list.
> 
>         Returns:
>             A URL parameter string. Ex: `?key1=u1&key1=u2&key2=v1&...`
>         """
>         params="".join([f"&{tup[0]}={tup[1]}" for tup in url_parameter.items()])
>         return "?"+params[1:]
> ```

This works! thanks!

Why not open a PR?
@LizMcQuillan This is currently possible using a series of different functions in the base object, the DoccanoClient has a function create_project for initializing a project. To Attach members to the project the post_members function can be used. Uploading a dataset uses the post_doc_upload function. Adding in the labels will use functions based on the project type, for example a SequenceLabeling project uses the post_span_type_upload .  The general flow is

1.  Initialize a DoccanoClient object
2.  Pick a project_type and a resource_type (SequenceLabeling & SequenceLabelingProject etc.)
3.  Get the Username/s and Role Name/s that should be attached to the project
4.  Set a upload Document Path
5.  Set a label upload path

```
...
project = doccano_client.create_project(name=...., description=...., project_type=...., resourcetype=..., collaborative_annoation=...)
project_id = project["id"]
users = [user]
roles = [role]
_result = doccano_client.post_members(project_id=project_id, usernames=users, roles=roles)

doccano_client.post_span_type_upload(project_id=project_id, file_name=..., file_path=...)
doccano_client.post_doc_upload(project_id=project_id, file_name=..., file_path=...)

```

Hope this helps!


Fix patch:
diff --git a/.github/workflows/mkdocs-deployment.yml b/.github/workflows/mkdocs-deployment.yml
index 38fe823..f5348ed 100644
--- a/.github/workflows/mkdocs-deployment.yml
+++ b/.github/workflows/mkdocs-deployment.yml
@@ -3,6 +3,7 @@ on:
   push:
     branches:
       - master
+      - develop
 
 jobs:
   build:
@@ -12,7 +13,7 @@ jobs:
       - name: Checkout main
         uses: actions/checkout@v2
       - name: Deploy docs
-        uses: mhausenblas/mkdocs-deploy-gh-pages@1.16
+        uses: mhausenblas/mkdocs-deploy-gh-pages@1.25
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
           CONFIG_FILE: docs/mkdocs.yml
diff --git a/Makefile b/Makefile
index 7ab76df..9ce428a 100644
--- a/Makefile
+++ b/Makefile
@@ -2,6 +2,7 @@ lint:
 	poetry run task flake8
 	poetry run task black
 	poetry run task isort
+	poetry run task mypy
 
 test:
 	poetry run task test
diff --git a/README.md b/README.md
index a0759b3..64866ee 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,6 @@
-# Doccano API Client
+# doccano client
 
-A simple client wrapper for the doccano API.
-
-- [Doccano API Client](#doccano-api-client)
-  - [Installation](#installation)
-  - [Usage](#usage)
-  - [Completion](#completion)
-  - [To-Do](#to-do)
-- [Doccano API BETA Client](#doccano-api-beta-client)
+A simple client for the doccano API.
 
 ## Installation
 
@@ -19,77 +12,23 @@ pip install doccano-client
 
 ## Usage
 
-- Object instantiation takes care of session authorization.
-- All methods return a `requests.models.Response` object.
-
 ```python
 from doccano_client import DoccanoClient
 
 # instantiate a client and log in to a Doccano instance
-doccano_client = DoccanoClient(
-  'http://doccano.example.com',
-  'username',
-  'password'
-)
+client = DoccanoClient('http://doccano.example.com')
+client.login(username='username', password='password')
 
 # get basic information about the authorized user
-r_me = doccano_client.get_me()
-
-# print the details from the above query
-print(r_me)
+user = doccano_client.get_profile()
 
-# get the label text from project 1, label 3
-label_text = doccano_client.get_label_detail(1, 3)['text']
-
-# upload a json file to project 1. If file is in current directory, file_path is omittable
-r_json_upload = doccano_client.post_doc_upload(1, 'json', 'file.json', '/path/to/file/without/filename/')
+# list all projects
+projects = doccano_client.list_projects()
 ```
 
-Info: Uploading documents has been reported as broken, but it works with the beta-client (see below)
-- [#16](https://github.com/doccano/doccano-client/issues/16)
-- [#13](https://github.com/doccano/doccano-client/issues/13)
-- [#50](https://github.com/doccano/doccano-client/issues/50)
-
-## Completion
-
-This wrapper's methods are based on doccano url [paths](https://github.com/chakki-works/doccano/blob/master/app/api/urls.py).
-
-Key:
-
-- ✔️ implemented
-- ❌ not implemented
-- ⚠️ currently broken or improperly implemented
-
-Endpoint Names:
-
-- ✔️ `auth-token`
-- ✔️ `me`
-- ✔️ `user_list`
-- ✔️ `roles`
-- ✔️ `features`
-- ✔️ `project_list`
-- ✔️ `project_detail`
-- ✔️ `statistics`
-- ✔️ `label_list`
-- ✔️ `label_detail`
-- ❌ `label_upload`
-- ✔️ `doc_list`
-- ✔️ `doc_detail`
-- ✔️ `doc_uploader`
-- ❌ `cloud_uploader`
-- ✔️ `approve_labels`
-- ✔️ `annotation_list`
-- ⚠️ `annotation_detail`
-- ✔️ `doc_downloader`
-- ✔️ `rolemapping_list`
-- ⚠️ `rolemapping_detail`
-
-## To-Do
-
-- investigate more secure alternatives to plaintext login
-- improve docstrings
+Please see the [documentation](https://doccano.github.io/doccano-client/) for further details.
 
-# Doccano API BETA Client
+## Doccano API BETA Client
 
 We're introducing a newly revamped Doccano API Client that features more Pythonic interaction as well as more testing and documentation. It also adds more regulated compatibility with specific Doccano release versions.
 
diff --git a/doccano_client/__init__.py b/doccano_client/__init__.py
index 602c0b2..b2d6930 100644
--- a/doccano_client/__init__.py
+++ b/doccano_client/__init__.py
@@ -1,1040 +1,3 @@
-import os
-import typing
-from urllib.parse import urljoin
+from doccano_client.client import DoccanoClient
 
-import requests
-
-
-class _Router:
-    """
-    Provides generic `get` and `post` methods. Implemented by DoccanoClient.
-    """
-
-    def get(
-        self,
-        endpoint: str,
-        params: dict = {},
-    ) -> requests.models.Response:
-        """
-        Args:
-            endpoint (str): An API endpoint to query.
-            params: (optional) Dictionary or bytes to be sent in the query string.
-
-        Returns:
-            requests.models.Response: The request response (JSON).
-        """
-        request_url = urljoin(self.baseurl, endpoint)
-        return self._get(request_url, params=params).json()
-
-    def get_file(
-        self,
-        endpoint: str,
-        params: dict = {},
-        headers: dict = {},
-    ) -> requests.models.Response:
-        """Gets a file.
-
-        Args:
-            endpoint (str): An API endpoint to query.
-            params: (optional) Dictionary or bytes to be sent in the query string.
-            headers: (optional) Dictionary of HTTP Headers to send with the `Request`.
-
-        Returns:
-            requests.models.Response: The request response (JSON).
-        """
-        request_url = urljoin(self.baseurl, endpoint)
-        return self._get(request_url, params=params, headers=headers)
-
-    def _get(
-        self,
-        url: str,
-        params: dict = {},
-        headers: dict = {},
-    ) -> requests.models.Response:
-        return self.session.get(url, params=params, headers=headers)
-
-    def post(
-        self,
-        endpoint: str,
-        data: dict = {},
-        json: dict = {},
-        files: dict = {},
-        headers: typing.Optional[dict] = None,
-        as_json=True,
-    ) -> requests.models.Response:
-        """Used to POST arbitrary (form) data or explicit JSON.
-        Both will have the correct Content-Type header set.
-
-        Args:
-            endpoint (str): An API endpoint to query.
-            data: (optional) Dictionary, list of tuples, bytes, or file-like object to send in the body of the Request`.
-            json: (optional) json to send in the body of the `Request`.
-            files: (optional) Dictionary of ``'filename': file-like-objects`` for multipart encoding upload.
-            headers: (optional) Dictionary of HTTP Headers to send with the `Request`.
-            as_json: (optional) If True, return the response as json.
-
-        Returns:
-            requests.models.Response: The request response (JSON).
-        """
-        if json and data:
-            return "Error: cannot have both data and json"
-
-        request_url = urljoin(self.baseurl, endpoint)
-        result = self.session.post(request_url, data=data, files=files, json=json, headers=headers)
-        # return json if requested
-        if as_json:
-            return result.json()
-        return result
-
-    def delete(
-        self,
-        endpoint: str,
-        data: typing.Optional[dict] = None,
-        files: typing.Optional[dict] = None,
-        headers: typing.Optional[dict] = None,
-    ) -> requests.models.Response:
-        """Deletes something at the given endpoint.
-
-        Args:
-            endpoint (str): An API endpoint to query.
-            data: (optional) Dictionary, list of tuples, bytes, or file-like object to send in the body of the Request`.
-            files: (optional) Dictionary of ``'filename': file-like-objects`` for multipart encoding upload.
-            headers: (optional) Dictionary of HTTP Headers to send with the `Request`.
-
-        Returns:
-            requests.models.Response: The request response (JSON).
-        """
-        request_url = urljoin(self.baseurl, endpoint)
-        return self.session.delete(request_url, data=data, files=files, headers=headers)
-
-    def update(self, endpoint: str, data: dict = {}) -> requests.models.Response:
-        """Updates a content specified by the endpoint with the data.
-
-        Args:
-            endpoint (str): An API endpoint to query.
-            data: (optional) Dictionary, list of tuples, bytes, or file-like object to send in the body of the Request`.
-
-        Returns:
-            requests.models.Response: The request response (JSON).
-        """
-        request_url = urljoin(self.baseurl, endpoint)
-        return self.session.patch(request_url, data=data)
-
-    def build_url_parameter(self, url_parameter: dict) -> str:
-        """Format url_parameters.
-
-        Args:
-            url_parameter (dict): Every value must be a list.
-
-        Example:
-            client.build_url_parameter(2, {'limit': [10], 'offset': [20]})
-
-        Returns:
-            A URL parameter string. Ex: `?key1=u1&key1=u2&key2=v1&...`
-        """
-        return "".join(
-            [
-                "?",
-                "&".join(
-                    ["&".join(["=".join([tup[0], str(value)]) for value in tup[1]]) for tup in url_parameter.items()]
-                ),
-            ]
-        )
-
-
-class DoccanoClient(_Router):
-    """
-    TODO: investigate alternatives to plaintext login
-
-    Args:
-        baseurl (str): The baseurl of a Doccano instance, eg. http://localhost:8000
-        username (str): The Doccano username to use for the client session.
-        password (str): The respective username's password.
-
-    Returns:
-        An authorized client instance.
-    """
-
-    def __init__(self, baseurl: str, username: str, password: str):
-        self.baseurl = baseurl if baseurl[-1] == "/" else baseurl + "/"
-        self.session = requests.Session()
-        self.session.headers = {"referer": self.baseurl}
-        self._login(username, password)
-
-    def _login(self, username: str, password: str) -> requests.models.Response:
-        """Authorizes the DoccanoClient instance.
-
-        Args:
-            username (str): The Doccano username to use for the client session.
-            password (str): The respective username's password.
-
-        Returns:
-            requests.models.Response: The authorization request response.
-        """
-        url = "v1/auth/login/"
-        auth = {"username": username, "password": password}
-        response = self.post(url, auth)
-        self._set_csrf_header()
-        return response
-
-    def _set_csrf_header(self):
-        """Sets the CSRF token required for the POST requests.
-
-        NB: this function has to be called
-        after the login endpoint.
-        Even if it's the post endpoint too it doesn't require
-        CSRF verification, but the token can be received from the cookies
-        """
-        csrf = self.session.cookies.get("csrftoken")
-        self.session.headers["X-CSRFToken"] = csrf
-
-    def get_me(self) -> requests.models.Response:
-        """Gets this account information.
-
-        Returns:
-            requests.models.Response: The request response.
-        """
-        return self.get("v1/me")
-
-    def get_features(self) -> requests.models.Response:
-        """Gets features.
-
-        Returns:
-            requests.models.Response: The request response.
-        """
-        return self.get("v1/features")
-
-    def get_project_list(self, url_parameters: dict = {}) -> requests.models.Response:
-        """Gets projects list.
-
-        Args:
-            url_parameters (dict): `limit` and `offset`
-
-        Returns:
-            requests.models.Response: The request response.
-        """
-        return self.get("v1/projects{url_parameters}".format(url_parameters=self.build_url_parameter(url_parameters)))
-
-    def create_project(
-        self,
-        name: str,
-        description: str = "",
-        project_type: str = "DocumentClassification",
-        guideline: str = "",
-        resourcetype: str = "TextClassificationProject",
-        randomize_document_order: bool = False,
-        collaborative_annotation: bool = False,
-    ) -> requests.models.Response:
-        """Creates a new project.
-
-        Args:
-            name (str): The project name.
-            description (str): The project description.
-            project_type (str): The type of the project.
-            guideline (str): The annotation guideline for the project.
-            resourcetype (str): This is determined by the project type.
-            randomize_document_order (bool): Shuffle the uploaded data.
-            collaborative_annotation (bool): If True, a data can be annotated by multiple users.
-
-        Returns:
-            requests.models.Response: The request response.
-        """
-        payload = {
-            "name": name,
-            "description": description,
-            "project_type": project_type,
-            "guideline": guideline,
-            "resourcetype": resourcetype,
-            "randomize_document_order": randomize_document_order,
-            "collaborative_annotation": collaborative_annotation,
-        }
-        return self.post("v1/projects", data=payload)
-
-    def delete_project(self, project_id: int) -> requests.models.Response:
-        """Deletes a project.
-
-        Args:
-            project_id (int): A project identifier.
-
-        Returns:
-            requests.models.Response: The request response.
-        """
-        url = "v1/projects/{}".format(project_id)
-        return self.delete(url)
-
-    def update_project(
-        self,
-        project_id: int,
-        name: str,
-        description: str = "",
-        project_type: str = "DocumentClassification",
-        guideline: str = "",
-        resourcetype: str = "TextClassificationProject",
-        randomize_document_order: bool = False,
-        collaborative_annotation: bool = False,
-    ) -> requests.models.Response:
-        """Updates a project.
-
-        Args:
-            project_id (int): The project id.
-            name (str): The project name.
-            description (str): The project description.
-            project_type (str): The type of the project.
-            guideline (str): The annotation guideline for the project.
-            resourcetype (str): This is determined by the project type.
-            randomize_document_order (bool): Shuffle the uploaded data.
-            collaborative_annotation (bool): If True, a data can be annotated by multiple users.
-
-        Returns:
-            requests.models.Response: The request response.
-        """
-        url = "v1/projects/{}".format(project_id)
-        payload = {
-            "name": name,
-            "description": description,
-            "project_type": project_type,
-            "guideline": guideline,
-            "resourcetype": resourcetype,
-            "randomize_document_order": randomize_document_order,
-            "collaborative_annotation": collaborative_annotation,
-        }
-        return self.update(url, data=payload)
-
-    def create_example(
-        self,
-        project_id: int,
-        text: str,
-        annotations: typing.Optional[typing.List] = None,
-        annotation_approver: str = None,
-    ) -> requests.models.Response:
-        """Creates a example.
-
-        Args:
-            project_id (int): The project id.
-            text (str): your text
-            annotations (list): annotations
-            annotation_approver (str): account that approved
-
-        Returns:
-            requests.models.Response: The request response
-        """
-
-        if annotations is None:
-            annotations = []
-
-        url = "v1/projects/{}/examples".format(project_id)
-        data = {
-            "text": text,
-            "annotations": annotations,
-            "annotation_approver": annotation_approver,
-        }
-        return self.post(url, data=data)
-
-    def delete_example(
-        self,
-        project_id: int,
-        example_id: int,
-    ) -> requests.models.Response:
-        url = "v1/projects/{}/examples/{}".format(project_id, example_id)
-        return self.delete(url)
-
-    def delete_span(
-        self,
-        project_id: int,
-        example_id: int,
-        span_id: int,
-    ) -> requests.models.Response:
-        url = "v1/projects/{project_id}/examples/{example_id}/spans/{span_id}".format(
-            project_id=project_id, example_id=example_id, span_id=span_id
-        )
-        return self.delete(url)
-
-    def create_span_type(
-        self,
-        project_id: int,
-        text: str,
-        text_color: str = "#ffffff",
-        background_color: str = "#cdcdcd",
-        prefix_key: str = None,
-        suffix_key: str = None,
-    ) -> requests.models.Response:
-        """Creates a span_type to be used for annotating a document.
-
-        Args:
-            project_id (int): The project id.
-            text (str): The label text.
-            text_color (str): The text color of the label.
-            background_color (str): The background color of the label.
-            prefix_key (str): The prefix key for shortcut.
-            suffix_key (str): The suffix key for shortcut.
-
-        Returns:
-            requests.models.Response: The request response.
-        """
-        url = "v1/projects/{}/span-types".format(project_id)
-        label_payload = {
-            "projectId": project_id,
-            "text": text,
-            "prefix_key": prefix_key,
-            "suffix_key": suffix_key,
-            "background_color": background_color,
-            "text_color": text_color,
-        }
-
-        try:
-            return self.post(url, data=label_payload)
-        except Exception as e:
-            return "Failed (duplicate?): {}".format(e)
-
-    def update_span_type(
-        self,
-        project_id: int,
-        span_type_id: int,
-        text: str,
-        text_color: str = "#ffffff",
-        background_color: str = "#cdcdcd",
-        prefix_key: str = None,
-        suffix_key: str = None,
-    ) -> requests.models.Response:
-        """Updates a span_type.
-
-        Args:
-            project_id (int): The project id.
-            span_type_id (int): A span_type ID to update.
-            text (str): The label text.
-            text_color (str): The text color of the label.
-            background_color (str): The background color of the label.
-            prefix_key (str): The prefix key for shortcut.
-            suffix_key (str): The suffix key for shortcut.
-
-
-        Returns:
-            requests.models.Response: The request response.
-        """
-        url = "v1/projects/{project_id}/span-types/{span_type_id}".format(
-            project_id=project_id, span_type_id=span_type_id
-        )
-        label_payload = {
-            "projectId": project_id,
-            "text": text,
-            "prefix_key": prefix_key,
-            "suffix_key": suffix_key,
-            "background_color": background_color,
-            "text_color": text_color,
-        }
-        return self.update(url, data=label_payload)
-
-    def create_span(self, project_id: int, example_id: int, label_id: int, **kwargs) -> requests.models.Response:
-        """Creates a span to a given example.
-
-        Variable keyword arguments kwargs give support to doccano
-        annotations for different project types.
-
-        For example, for SequenceLabeling one should call using start_offset
-        and end_offset keyword arguments.
-
-        Args:
-            project_id (int): The project id.
-            label_id (int): Label identifier.
-            example_id (int): Example identifier.
-            **kwargs: Arbitrary keyword arguments.
-
-        Returns:
-            requests.models.Response: The request response.
-        """
-        url = "/v1/projects/{project_id}/examples/{example_id}/spans".format(
-            project_id=project_id, example_id=example_id
-        )
-        payload = {"label": label_id, **kwargs}
-        return self.post(url, json=payload)
-
-    def get_user_list(self) -> requests.models.Response:
-        """Gets user list.
-
-        Returns:
-            requests.models.Response: The request response.
-        """
-        return self.get("v1/users")
-
-    def get_roles(self) -> requests.models.Response:
-        """Gets available Doccano user roles.
-
-        Returns:
-            requests.models.Response: The request response.
-        """
-        return self.get("v1/roles")
-
-    def get_project_detail(self, project_id: int) -> requests.models.Response:
-        """Gets details of a specific project.
-
-        Args:
-            project_id (int): The project id.
-
-        Returns:
-            requests.models.Response: The request response.
-        """
-        return self.get("v1/projects/{project_id}".format(project_id=project_id))
-
-    def get_metrics_member_progress(self, project_id: int) -> requests.models.Response:
-        """Gets project member progress metrics.
-
-        Args:
-            project_id (int): The project id.
-
-        Returns:
-            requests.models.Response: The request response.
-        """
-        return self.get("v1/projects/{project_id}/metrics/member-progress".format(project_id=project_id))
-
-    def get_metrics_span_distribution(self, project_id: int) -> requests.models.Response:
-        """Gets project span_distribution metrics.
-
-        Args:
-            project_id (int): The project id.
-
-        Returns:
-            requests.models.Response: The request response.
-        """
-        return self.get("v1/projects/{project_id}/metrics/span-distribution".format(project_id=project_id))
-
-    def get_span_type_list(self, project_id: int) -> requests.models.Response:
-        """Gets a list of span_types in a given project.
-
-        Args:
-            project_id (int): The project id.
-
-        Returns:
-            requests.models.Response: The request response.
-        """
-        return self.get("v1/projects/{project_id}/span-types".format(project_id=project_id))
-
-    def get_span_type_detail(self, project_id: int, span_type_id: int) -> requests.models.Response:
-        """Gets details of a specific span type.
-
-        Args:
-            project_id (int): The project id.
-            span_type_id (int): A span_type ID to query.
-
-        Returns:
-            requests.models.Response: The request response.
-        """
-        return self.get(
-            "v1/projects/{project_id}/span-types/{span_type_id}".format(
-                project_id=project_id, span_type_id=span_type_id
-            )
-        )
-
-    def get_examples(self, project_id: int, url_parameters: dict = {}) -> requests.models.Response:
-        """Gets a list of examples in a project.
-
-        Args:
-            project_id (int): The project id.
-            url_parameters (dict): `limit` and `offset`
-
-        Example:
-            client.get_examples(2, {'limit': [10], 'offset': [20]})
-
-        Returns:
-            requests.models.Response: The request response.
-        """
-        return self.get(
-            "v1/projects/{project_id}/examples{url_parameters}".format(
-                project_id=project_id, url_parameters=self.build_url_parameter(url_parameters)
-            )
-        )
-
-    def get_example_detail(self, project_id: int, example_id: int) -> requests.models.Response:
-        """Gets details of a given example.
-
-        Args:
-            project_id (int): The project id.
-            example_id (int): A example ID to query.
-
-        Returns:
-            requests.models.Response: The request response.
-        """
-        return self.get(
-            "v1/projects/{project_id}/examples/{example_id}".format(project_id=project_id, example_id=example_id)
-        )
-
-    def get_spans(self, project_id: int, example_id: int) -> requests.models.Response:
-        """Gets a list of spans in a given project and example.
-
-        Args:
-            project_id (int): The project id.
-            example_id (int): A example ID to query.
-
-        Returns:
-            requests.models.Response: The request response.
-        """
-        return self.get(
-            "v1/projects/{project_id}/examples/{example_id}/spans".format(project_id=project_id, example_id=example_id)
-        )
-
-    def get_span_detail(self, project_id: int, example_id: int, span_id: int) -> requests.models.Response:
-        """Gets a span.
-
-        Args:
-            project_id (int): The project id.
-            example_id (int): A example ID to query.
-            span_id (int): The span id.
-
-        Returns:
-            requests.models.Response: The request response.
-        """
-        return self.get(
-            "v1/projects/{project_id}/examples/{example_id}/spans/{span_id}".format(
-                project_id=project_id, example_id=example_id, span_id=span_id
-            )
-        )
-
-    def get_category_type_list(self, project_id: int) -> requests.models.Response:
-        """Gets a list of category_types in a given project.
-
-        Args:
-            project_id (int): The project id.
-
-        Returns:
-            requests.models.Response: The request response.
-        """
-        return self.get("v1/projects/{project_id}/category-types".format(project_id=project_id))
-
-    def get_category_type_detail(self, project_id: int, category_type_id: int) -> requests.models.Response:
-        """Gets details of a specific category type.
-
-        Args:
-            project_id (int): The project id.
-            category_type_id (int): A category_type ID to query.
-
-        Returns:
-            requests.models.Response: The request response.
-        """
-        return self.get(
-            "v1/projects/{project_id}/category-types/{category_type_id}".format(
-                project_id=project_id, category_type_id=category_type_id
-            )
-        )
-
-    def create_category_type(
-        self,
-        project_id: int,
-        text: str,
-        text_color: str = "#ffffff",
-        background_color: str = "#cdcdcd",
-        prefix_key: str = None,
-        suffix_key: str = None,
-    ) -> requests.models.Response:
-        """Creates a category_type to be used for annotating a document.
-
-        Args:
-            project_id (int): The project id.
-            text (str): The label text.
-            text_color (str): The text color of the label.
-            background_color (str): The background color of the label.
-            prefix_key (str): The prefix key for shortcut.
-            suffix_key (str): The suffix key for shortcut.
-
-        Returns:
-            requests.models.Response: The request response.
-        """
-        url = "v1/projects/{}/category-types".format(project_id)
-        label_payload = {
-            "projectId": project_id,
-            "text": text,
-            "prefix_key": prefix_key,
-            "suffix_key": suffix_key,
-            "background_color": background_color,
-            "text_color": text_color,
-        }
-        try:
-            return self.post(url, data=label_payload)
-        except Exception as e:
-            return f"Failed (duplicate?): {e}"
-
-    def update_category_type(
-        self,
-        project_id: int,
-        category_type_id: int,
-        text: str,
-        text_color: str = "#ffffff",
-        background_color: str = "#cdcdcd",
-        prefix_key: str = None,
-        suffix_key: str = None,
-    ) -> requests.models.Response:
-        """Updates a category_type.
-
-        Args:
-            project_id (int): The project id.
-            category_type_id (int): A category_type ID to update.
-            text (str): The label text.
-            text_color (str): The text color of the label.
-            background_color (str): The background color of the label.
-            prefix_key (str): The prefix key for shortcut.
-            suffix_key (str): The suffix key for shortcut.
-
-
-        Returns:
-            requests.models.Response: The request response.
-        """
-        url = "v1/projects/{project_id}/category-types/{category_type_id}".format(
-            project_id=project_id, category_type_id=category_type_id
-        )
-        label_payload = {
-            "projectId": project_id,
-            "text": text,
-            "prefix_key": prefix_key,
-            "suffix_key": suffix_key,
-            "background_color": background_color,
-            "text_color": text_color,
-        }
-        return self.update(url, data=label_payload)
-
-    def get_relation_type_list(self, project_id: int) -> requests.models.Response:
-        """Gets a list of relation_types in a given project.
-
-        Args:
-            project_id (int): The project id.
-
-        Returns:
-            requests.models.Response: The request response.
-        """
-        return self.get("v1/projects/{project_id}/relation-types".format(project_id=project_id))
-
-    def get_relation_type_detail(self, project_id: int, relation_type_id: int) -> requests.models.Response:
-        """Gets details of a specific relation type.
-
-        Args:
-            project_id (int): The project id.
-            relation_type_id (int): A relation_type ID to query.
-
-        Returns:
-            requests.models.Response: The request response.
-        """
-        return self.get(
-            "v1/projects/{project_id}/relation-types/{relation_type_id}".format(
-                project_id=project_id, relation_type_id=relation_type_id
-            )
-        )
-
-    def create_relation_type(
-        self,
-        project_id: int,
-        text: str,
-        text_color: str = "#ffffff",
-        background_color: str = "#cdcdcd",
-        prefix_key: str = None,
-        suffix_key: str = None,
-    ) -> requests.models.Response:
-        """Creates a relation_type to be used for annotating a document.
-
-        Args:
-            project_id (int): The project id.
-            text (str): The label text.
-            text_color (str): The text color of the label.
-            background_color (str): The background color of the label.
-            prefix_key (str): The prefix key for shortcut.
-            suffix_key (str): The suffix key for shortcut.
-
-        Returns:
-            requests.models.Response: The request response.
-        """
-        url = "v1/projects/{}/relation-types".format(project_id)
-        label_payload = {
-            "projectId": project_id,
-            "text": text,
-            "prefix_key": prefix_key,
-            "suffix_key": suffix_key,
-            "background_color": background_color,
-            "text_color": text_color,
-        }
-        try:
-            return self.post(url, data=label_payload)
-        except Exception as e:
-            return f"Failed (duplicate?): {e}"
-
-    def update_relation_type(
-        self,
-        project_id: int,
-        relation_type_id: int,
-        text: str,
-        text_color: str = "#ffffff",
-        background_color: str = "#cdcdcd",
-        prefix_key: str = None,
-        suffix_key: str = None,
-    ) -> requests.models.Response:
-        """Updates a relation_type.
-
-        Args:
-            project_id (int): The project id.
-            relation_type_id (int): A relation_type ID to update.
-            text (str): The label text.
-            text_color (str): The text color of the label.
-            background_color (str): The background color of the label.
-            prefix_key (str): The prefix key for shortcut.
-            suffix_key (str): The suffix key for shortcut.
-
-
-        Returns:
-            requests.models.Response: The request response.
-        """
-        url = "v1/projects/{project_id}/relation-types/{relation_type_id}".format(
-            project_id=project_id, relation_type_id=relation_type_id
-        )
-        label_payload = {
-            "projectId": project_id,
-            "text": text,
-            "prefix_key": prefix_key,
-            "suffix_key": suffix_key,
-            "background_color": background_color,
-            "text_color": text_color,
-        }
-        return self.update(url, data=label_payload)
-
-    def get_example_states(self, project_id: int, example_id: int) -> requests.models.Response:
-        """Gets example states of a given example.
-
-        Args:
-            project_id (int): The project id.
-            example_id (int): A example ID to query.
-
-        Returns:
-            requests.models.Response: The request response.
-        """
-        return self.get(
-            "v1/projects/{project_id}/examples/{example_id}/states".format(project_id=project_id, example_id=example_id)
-        )
-
-    def get_doc_download(
-        self, project_id: int, file_format: str = "json", only_approved: bool = False
-    ) -> requests.models.Response:
-        """Downloads the dataset in specified format.
-
-        Args:
-            project_id (int): The project id.
-            file_format (str): The download file format.
-            only_approved (bool): If True, download the approved data only.
-
-        Returns:
-            requests.models.Response: The request response.
-        """
-        accept_headers = {"json": "application/json", "csv": "text/csv"}
-        headers = {"accept": accept_headers[file_format]}
-
-        return self.get_file(
-            "v1/projects/{project_id}/docs/download".format(project_id=project_id),
-            params={"q": file_format, "onlyApproved": str(only_approved).lower()},
-            headers=headers,
-        )
-
-    def get_rolemapping_list(
-        self,
-        project_id: int,
-    ) -> requests.models.Response:
-        return self.get("v1/projects/{project_id}/roles".format(project_id=project_id))
-
-    def get_rolemapping_detail(
-        self,
-        project_id: int,
-        rolemapping_id: int,
-    ) -> requests.models.Response:
-        return self.get(
-            "v1/projets/{project_id}/roles/{rolemapping_id}".format(
-                project_id=project_id, rolemapping_id=rolemapping_id
-            )
-        )
-
-    def post_doc_upload_binary(
-        self,
-        project_id: int,
-        files: typing.List[typing.IO],
-        column_data: str = "text",
-        column_label: str = "label",
-        delimiter: str = "",
-        encoding: str = "utf_8",
-        format: str = "JSONL",
-        task: str = "SequenceLabeling",
-    ) -> requests.models.Response:
-        """Upload documents to doccano
-
-        Args:
-            project_id (int): The project id.
-            files (typing.List[typing.IO]): List of files to be uploaded
-            column_data (str): Name of the column with data (text for annotation)
-            column_label (str): Name of the column with labels (labels for annotation)
-            delimiter (str): Delimeter for the current dataset
-            encoding (str): Current file encoding
-            format (str): The file format, ex: `plain`, `json`, or `conll`.
-            task (str): task type, ex. `SequenceLabeling`, `TextClassification`
-
-        Returns:
-            requests.models.Response: The request response.
-
-        Raises:
-            TypeError: If files is not a list of files.
-            Exception: If upload failed.
-        """
-
-        # upload files with filepond
-        if not isinstance(files, (list, tuple)):
-            # this check is very important
-            # as file object is iterable and this function will
-            # try to upload file line by line
-            raise TypeError("Please provide a list with files")
-
-        upload_ids = []
-        for file_ in files:
-            try:
-                fp_resp = self.post("v1/fp/process/", files={"filepond": file_}, as_json=False)
-                fp_resp.raise_for_status()
-                upload_ids.append(fp_resp.text)
-            except Exception as e:
-                # revert previous uploads if we have a problem
-                for upload_id in upload_ids:
-                    self.delete("v1/fp/revert/", data=upload_id, headers={"Content-Type": "text/plain"})
-                raise e
-
-        task = self.get_project_detail(project_id=project_id).get("project_type")
-
-        # confirm uploads and run processing
-        upload_data = {
-            "column_data": column_data,
-            "column_label": column_label,
-            "delimiter": delimiter,
-            "encoding": encoding,
-            "format": format,
-            "uploadIds": upload_ids,
-            "task": task,
-        }
-        return self.post(f"v1/projects/{project_id}/upload", json=upload_data)
-
-    def post_doc_upload(
-        self,
-        project_id: int,
-        file_name: str,
-        file_path: str = "./",
-        column_data: str = "text",
-        column_label: str = "label",
-        delimiter: str = "",
-        encoding: str = "utf_8",
-        format: str = "JSONL",
-        task: str = "SequenceLabeling",
-    ) -> requests.models.Response:
-        """Uploads a file to a Doccano project.
-
-        Args:
-            project_id (int): The project id.
-            file_name (str): The name of the file.
-            file_path (str): The parent path of the file. Defaults to `./`.
-            column_data (str): Name of the column with data (text for annotation)
-            column_label (str): Name of the column with labels (labels for annotation)
-            delimiter (str): Delimeter for the current dataset
-            encoding (str): Current file encoding
-            format (str): The file format, ex: `plain`, `json`, or `conll`.
-            task (str): task type, ex. `SequenceLabeling`, `TextClassification`
-
-        Returns:
-            requests.models.Response: The request response.
-        """
-        return self.post_doc_upload_binary(
-            project_id=project_id,
-            files=[open(os.path.join(file_path, file_name), "rb")],
-            column_data=column_data,
-            column_label=column_label,
-            delimiter=delimiter,
-            encoding=encoding,
-            format=format,
-            task=task,
-        )
-
-    def post_members(
-        self, project_id: int, usernames: typing.List[str], roles: typing.List[str]
-    ) -> typing.List[requests.models.Response]:
-        """Add members to a Doccano project.
-
-        Args:
-            project_id (int): The project id.
-            usernames (typing.List[str]): Names to be added to the project.
-            roles (typing.List[str]): Roles to be assigned to the users respectively.
-
-        Returns:
-            typing.List[requests.models.Response]: The request responses.
-        """
-        res_roles = self.get_roles()
-        res_users = self.get_user_list()
-
-        arr_response = []
-        for username, rolename in zip(usernames, roles):
-            user = list(filter(lambda user_info: user_info["username"] == username, res_users))
-            assert len(user) >= 1, "username {username} not found".format(username=username)
-            user = user[0]
-
-            role = list(filter(lambda role_info: role_info["name"] == rolename, res_roles))
-            assert len(role) >= 1, "rolename {rolename} not found".format(rolename=rolename)
-            role = role[0]
-
-            arr_response.append(
-                self.post(
-                    "v1/projects/{project_id}/members".format(project_id=project_id),
-                    data={"role": role["id"], "user": user["id"]},
-                )
-            )
-
-        return arr_response
-
-    def post_span_type_upload(self, project_id: int, file_name: str, file_path: str = "./") -> requests.models.Response:
-        """Uploads a span_type file to a Doccano project.
-
-        Args:
-            project_id (int): The project id.
-            file_name (str): The name of the file.
-            file_path (str): The parent path of the file. Defaults to `./`.
-
-        Returns:
-            requests.models.Response: The request response.
-        """
-        return self.post(
-            "v1/projects/{project_id}/span-type-upload".format(project_id=project_id),
-            files={"file": open(os.path.join(file_path, file_name), "rb")},
-            as_json=False,
-        )
-
-    def post_category_type_upload(
-        self, project_id: int, file_name: str, file_path: str = "./"
-    ) -> requests.models.Response:
-        """Uploads a category_type file to a Doccano project.
-
-        Args:
-            project_id (int): The project id.
-            file_name (str): The name of the file.
-            file_path (str): The parent path of the file. Defaults to `./`.
-
-        Returns:
-            requests.models.Response: The request response.
-        """
-        return self.post(
-            "v1/projects/{project_id}/category-type-upload".format(project_id=project_id),
-            files={"file": open(os.path.join(file_path, file_name), "rb")},
-            as_json=False,
-        )
-
-    def post_relation_type_upload(
-        self, project_id: int, file_name: str, file_path: str = "./"
-    ) -> requests.models.Response:
-        """Uploads a relation_type file to a Doccano project.
-
-        Args:
-            project_id (int): The project id.
-            file_name (str): The name of the file.
-            file_path (str): The parent path of the file. Defaults to `./`.
-
-        Returns:
-            requests.models.Response: The request response.
-        """
-        return self.post(
-            "v1/projects/{project_id}/relation-type-upload".format(project_id=project_id),
-            files={"file": open(os.path.join(file_path, file_name), "rb")},
-            as_json=False,
-        )
-
-    def _get_any_endpoint(self, endpoint: str) -> requests.models.Response:
-        return self.get(endpoint)
+__all__ = ["DoccanoClient"]
diff --git a/doccano_client/client.py b/doccano_client/client.py
new file mode 100644
index 0000000..fb5ab8e
--- /dev/null
+++ b/doccano_client/client.py
@@ -0,0 +1,1380 @@
+from __future__ import annotations
+
+import pathlib
+from typing import Any, Dict, Iterator, List, Literal, Optional
+
+from doccano_client.models.comment import Comment
+from doccano_client.models.data_download import Option as DataExportOption
+from doccano_client.models.data_upload import AvailableTask
+from doccano_client.models.data_upload import Option as DataImportOption
+from doccano_client.models.example import Example
+from doccano_client.models.label import (
+    BoundingBox,
+    Category,
+    Relation,
+    Segment,
+    Span,
+    Text,
+)
+from doccano_client.models.label_type import PREFIX_KEY, SUFFIX_KEY, LabelType
+from doccano_client.models.member import Member
+from doccano_client.models.metrics import LabelDistribution, MemberProgress, Progress
+from doccano_client.models.project import Project
+from doccano_client.models.role import Role
+from doccano_client.models.task_status import TaskStatus
+from doccano_client.models.user import User
+from doccano_client.repositories.base import BaseRepository
+from doccano_client.repositories.comment import CommentRepository
+from doccano_client.repositories.data_download import DataDownloadRepository
+from doccano_client.repositories.data_upload import DataUploadRepository
+from doccano_client.repositories.example import ExampleRepository
+from doccano_client.repositories.label import (
+    BoundingBoxRepository,
+    CategoryRepository,
+    RelationRepository,
+    SegmentRepository,
+    SpanRepository,
+    TextRepository,
+)
+from doccano_client.repositories.label_type import (
+    CategoryTypeRepository,
+    RelationTypeRepository,
+    SpanTypeRepository,
+)
+from doccano_client.repositories.member import MemberRepository
+from doccano_client.repositories.metrics import MetricsRepository
+from doccano_client.repositories.project import ProjectRepository
+from doccano_client.repositories.role import RoleRepository
+from doccano_client.repositories.task_status import TaskStatusRepository
+from doccano_client.repositories.user import UserRepository
+from doccano_client.services.label_type import LabelTypeService
+from doccano_client.usecase.comment import CommentUseCase
+from doccano_client.usecase.data_download import DataDownloadUseCase
+from doccano_client.usecase.data_upload import DataUploadUseCase
+from doccano_client.usecase.example import ExampleUseCase
+from doccano_client.usecase.label import (
+    BoundingBoxUseCase,
+    CategoryUseCase,
+    RelationUseCase,
+    SegmentUseCase,
+    SpanUseCase,
+    TextUseCase,
+)
+from doccano_client.usecase.label_type import LabelTypeUseCase
+from doccano_client.usecase.member import MemberUseCase
+from doccano_client.usecase.project import ProjectType, ProjectUseCase
+
+
+class DoccanoClient:
+    def __init__(self, base_url: str):
+        self._base_repository = BaseRepository(base_url)
+        self._user_repository = UserRepository(self._base_repository)
+        self._role_repository = RoleRepository(self._base_repository)
+        self._project_repository = ProjectRepository(self._base_repository)
+        self._metrics_repository = MetricsRepository(self._base_repository)
+        self._example_repository = ExampleRepository(self._base_repository)
+        self._comment_repository = CommentRepository(self._base_repository)
+        self._member_repository = MemberRepository(self._base_repository)
+
+        # label type repositories
+        self._category_type_repository = CategoryTypeRepository(self._base_repository)
+        self._span_type_repository = SpanTypeRepository(self._base_repository)
+        self._relation_type_repository = RelationTypeRepository(self._base_repository)
+
+        # label repositories
+        self._category_repository = CategoryRepository(self._base_repository)
+        self._span_repository = SpanRepository(self._base_repository)
+        self._relation_repository = RelationRepository(self._base_repository)
+        self._segment_repository = SegmentRepository(self._base_repository)
+        self._bounding_box_repository = BoundingBoxRepository(self._base_repository)
+        self._text_repository = TextRepository(self._base_repository)
+
+        self._task_status_repository = TaskStatusRepository(self._base_repository)
+        self._data_import_repository = DataUploadRepository(self._base_repository)
+        self._data_export_repository = DataDownloadRepository(self._base_repository)
+
+    def login(self, username: str, password: str) -> None:
+        """Login to a session with the Doccano instance related to the base url.
+
+        Args:
+            username (str): The username of the user.
+            password (str): The password of the user.
+        """
+        self._base_repository.login(username, password)
+
+    @property
+    def project(self) -> ProjectUseCase:
+        return ProjectUseCase(self._project_repository)
+
+    @property
+    def example(self) -> ExampleUseCase:
+        return ExampleUseCase(self._example_repository)
+
+    @property
+    def comment(self) -> CommentUseCase:
+        return CommentUseCase(self._comment_repository)
+
+    @property
+    def category_type(self) -> LabelTypeUseCase:
+        service = LabelTypeService(self._category_type_repository)
+        return LabelTypeUseCase(self._category_type_repository, service)
+
+    @property
+    def span_type(self) -> LabelTypeUseCase:
+        service = LabelTypeService(self._span_type_repository)
+        return LabelTypeUseCase(self._span_type_repository, service)
+
+    @property
+    def relation_type(self) -> LabelTypeUseCase:
+        service = LabelTypeService(self._relation_type_repository)
+        return LabelTypeUseCase(self._relation_type_repository, service)
+
+    @property
+    def data_import(self) -> DataUploadUseCase:
+        return DataUploadUseCase(self._data_import_repository, self._task_status_repository)
+
+    @property
+    def data_export(self) -> DataDownloadUseCase:
+        return DataDownloadUseCase(self._data_export_repository, self._task_status_repository)
+
+    @property
+    def member(self) -> MemberUseCase:
+        return MemberUseCase(self._member_repository, self._user_repository, self._role_repository)
+
+    @property
+    def category(self) -> CategoryUseCase:
+        return CategoryUseCase(self._category_repository, self._category_type_repository)
+
+    @property
+    def span(self) -> SpanUseCase:
+        return SpanUseCase(self._span_repository, self._span_type_repository)
+
+    @property
+    def relation(self) -> RelationUseCase:
+        return RelationUseCase(self._relation_repository, self._relation_type_repository)
+
+    @property
+    def segment(self) -> SegmentUseCase:
+        return SegmentUseCase(self._segment_repository, self._category_type_repository)
+
+    @property
+    def bounding_box(self) -> BoundingBoxUseCase:
+        return BoundingBoxUseCase(self._bounding_box_repository, self._category_type_repository)
+
+    @property
+    def text(self) -> TextUseCase:
+        return TextUseCase(self._text_repository)
+
+    def _get_label_type_usecase(self, type: Literal["category", "span", "relation"]) -> LabelTypeUseCase:
+        if type == "category":
+            return self.category_type
+        elif type == "span":
+            return self.span_type
+        elif type == "relation":
+            return self.relation_type
+        else:
+            raise ValueError(f"Invalid type: {type}")
+
+    def list_roles(self) -> List[Role]:
+        """Return all roles.
+
+        Returns:
+            List[Role]: The list of roles.
+        """
+        return self._role_repository.list()
+
+    def get_profile(self) -> User:
+        """Return the profile of the logged in user.
+
+        Returns:
+            User: The profile of the logged in user.
+        """
+        return self._user_repository.get_profile()
+
+    def search_users(self, name: str = "") -> List[User]:
+        """Search users by name.
+
+        Args:
+            name (str): The name of the user to search for.
+
+        Returns:
+            List[User]: The list of the users.
+        """
+        return self._user_repository.list(name=name)
+
+    def find_user_by_name(self, name: str) -> User:
+        """Find a user by name.
+
+        Args:
+            name (str): The name of the user.
+
+        Returns:
+            User: The found user.
+        """
+        return self._user_repository.find_by_name(name)
+
+    def list_projects(self) -> Iterator[Project]:
+        """Return all projects in which you are a member.
+
+        Yields:
+            Project: The next project.
+        """
+        yield from self.project.list()
+
+    def find_project_by_id(self, project_id: int) -> Project:
+        """Find a project by id.
+
+        Args:
+            project_id (int): The id of the project to find.
+
+        Returns:
+            Project: The found project.
+        """
+        return self.project.find_by_id(project_id)
+
+    def get_progress(self, project_id: int) -> Progress:
+        """Get the authenticated user's progress.
+
+        Args:
+            project_id (int): The id of the project.
+
+        Returns:
+            Progress: The user's progress.
+        """
+        return self._metrics_repository.get_progress(project_id)
+
+    def get_members_progress(self, project_id: int) -> List[MemberProgress]:
+        """Return all metricss in which you are a member.
+
+        Args:
+            project_id (int): The id of the project.
+
+        Returns:
+            List[MemberProgress]: The list of the member progress.
+        """
+        return self._metrics_repository.get_members_progress(project_id)
+
+    def get_label_distribution(
+        self, project_id: int, type: Literal["category", "span", "relation"]
+    ) -> List[LabelDistribution]:
+        """Return label distribution.
+
+        Args:
+            project_id (int): The id of the project.
+            type (Literal["category", "span", "relation"]): The type of the label.
+
+        Returns:
+            LabelDistribution: The label distribution.
+
+        Raises:
+            ValueError: If the type is invalid.
+        """
+        if type == "category":
+            return self._metrics_repository.get_category_distribution(project_id)
+        elif type == "span":
+            return self._metrics_repository.get_span_distribution(project_id)
+        elif type == "relation":
+            return self._metrics_repository.get_relation_distribution(project_id)
+        else:
+            raise ValueError(f"Invalid type: {type}")
+
+    def create_project(
+        self,
+        name: str,
+        project_type: ProjectType,
+        description: str,
+        guideline: str = "",
+        random_order: bool = False,
+        collaborative_annotation: bool = False,
+        single_class_classification: bool = False,
+        allow_overlapping: bool = False,
+        grapheme_mode: bool = False,
+        use_relation: bool = False,
+        tags: Optional[List[str]] = None,
+    ) -> Project:
+        """Create a new project. `ProjectType` is one of the
+        `DocumentClassification`, `SequenceLabeling`, `Seq2seq`, `Speech2text`,
+        `ImageClassification`, `BoundingBox`, `Segmentation`, `ImageCaptioning`,
+        and `IntentDetectionAndSlotFilling`.
+
+        Args:
+            name (str): The name of the project.
+            project_type (ProjectType): The type of the project.
+            description (str): The description of the project.
+            guideline (str): The annotation guideline. Defaults to "".
+            random_order (bool): Whether to shuffle the uploaded data. Defaults to False.
+            collaborative_annotation (bool): If True, a data can be annotated by multiple users. Defaults to False.
+            single_class_classification (bool): If True, only one label can apply a data. Defaults to False.
+            allow_overlapping (bool): If True, span overlapping is allowed. Defaults to False.
+            grapheme_mode (bool): If True, count multi-byte characters as one character. Defaults to False.
+            use_relation (bool): If True, relation labeling is allowed. Defaults to False.
+            tags (Optional[List[str]], optional): The tags of the project. Defaults to None.
+
+        Returns:
+            Project: The created project.
+        """
+        return self.project.create(
+            name=name,
+            project_type=project_type,
+            description=description,
+            guideline=guideline,
+            random_order=random_order,
+            collaborative_annotation=collaborative_annotation,
+            single_class_classification=single_class_classification,
+            allow_overlapping=allow_overlapping,
+            grapheme_mode=grapheme_mode,
+            use_relation=use_relation,
+            tags=tags,
+        )
+
+    def update_project(
+        self,
+        project_id: int,
+        name: str = None,
+        project_type: ProjectType = None,
+        description: str = None,
+        guideline: str = None,
+        random_order: bool = None,
+        collaborative_annotation: bool = None,
+        single_class_classification: bool = None,
+        allow_overlapping: bool = None,
+        grapheme_mode: bool = None,
+        use_relation: bool = None,
+        tags: Optional[List[str]] = None,
+    ) -> Project:
+        """Update a project. `ProjectType` is one of the
+        `DocumentClassification`, `SequenceLabeling`, `Seq2seq`, `Speech2text`,
+        `ImageClassification`, `BoundingBox`, `Segmentation`, `ImageCaptioning`,
+        and `IntentDetectionAndSlotFilling`.
+
+        Args:
+            project_id (int): The project id.
+            name (str): The name of the project.
+            project_type (ProjectType): The type of the project.
+            description (str): The description of the project. Defaults to None.
+            guideline (str): The annotation guideline. Defaults to None.
+            random_order (bool): Whether to shuffle the uploaded data. Defaults to None.
+            collaborative_annotation (bool): If True, a data can be annotated by multiple users. Defaults to None.
+            single_class_classification (bool): If True, only one label can apply a data. Defaults to None.
+            allow_overlapping (bool): If True, span overlapping is allowed. Defaults to None.
+            grapheme_mode (bool): If True, count multi-byte characters as one character. Defaults to None.
+            use_relation (bool): If True, relation labeling is allowed. Defaults to None.
+            tags (Optional[List[str]], optional): The tags of the project. Defaults to None.
+
+        Returns:
+            Project: The updated project.
+        """
+        return self.project.update(
+            project_id=project_id,
+            name=name,
+            project_type=project_type,
+            description=description,
+            guideline=guideline,
+            random_order=random_order,
+            collaborative_annotation=collaborative_annotation,
+            single_class_classification=single_class_classification,
+            allow_overlapping=allow_overlapping,
+            grapheme_mode=grapheme_mode,
+            use_relation=use_relation,
+            tags=tags,
+        )
+
+    def delete_project(self, project_id: int):
+        """Delete a project.
+
+        Args:
+            project_id (int): The project id.
+        """
+        self.project.delete(project_id)
+
+    def list_label_types(self, project_id: int, type: Literal["category", "span", "relation"]) -> List[LabelType]:
+        """Return all label types in a project.
+
+        Args:
+            project_id (int): The project id.
+            type (Literal["category", "span", "relation"]): The type of the label type.
+
+        Returns:
+            List[LabelType]: The list of label types.
+        """
+        return self._get_label_type_usecase(type).list(project_id)
+
+    def find_label_type_by_id(
+        self, project_id: int, label_type_id: int, type: Literal["category", "span", "relation"]
+    ) -> LabelType:
+        """Find a label type by id.
+
+        Args:
+            project_id (int): The project id.
+            label_type_id (int): The label type id.
+            type (Literal["category", "span", "relation"]): The type of the label type.
+
+        Returns:
+            LabelType: The found label type.
+        """
+        return self._get_label_type_usecase(type).find_by_id(project_id, label_type_id)
+
+    def create_label_type(
+        self,
+        project_id: int,
+        type: Literal["category", "span", "relation"],
+        text: str,
+        prefix_key: PREFIX_KEY = None,
+        suffix_key: SUFFIX_KEY = None,
+        color: Optional[str] = None,
+    ) -> LabelType:
+        """Create a new label type.
+
+        Args:
+            project_id (int): The project id.
+            type (Literal["category", "span", "relation"]): The type of the label type.
+            text (str): The name of the label type.
+            prefix_key (PREFIX_KEY): The prefix key of the label type.
+            suffix_key (SUFFIX_KEY): The suffix key of the label type.
+            color (str): The color of the label type. Defaults to None.
+
+        Returns:
+            LabelType: The created label type.
+        """
+        return self._get_label_type_usecase(type).create(
+            project_id=project_id,
+            text=text,
+            prefix_key=prefix_key,
+            suffix_key=suffix_key,
+            color=color,
+        )
+
+    def update_label_type(
+        self,
+        project_id: int,
+        label_type_id: int,
+        type: Literal["category", "span", "relation"],
+        text: str = None,
+        prefix_key: PREFIX_KEY | int = -1,
+        suffix_key: SUFFIX_KEY | int = -1,
+        color: str = None,
+    ) -> LabelType:
+        """Update a label type.
+
+        Args:
+            project_id (int): The project id.
+            label_type_id (int): The label type id.
+            type (Literal["category", "span", "relation"]): The type of the label type.
+            text (str): The name of the label type.
+            prefix_key (PREFIX_KEY): The prefix key of the label type.
+            suffix_key (SUFFIX_KEY): The suffix key of the label type.
+            color (str): The color of the label type. Defaults to None.
+
+        Returns:
+            LabelType: The updated label type.
+        """
+        return self._get_label_type_usecase(type).update(
+            project_id=project_id,
+            label_type_id=label_type_id,
+            text=text,
+            prefix_key=prefix_key,
+            suffix_key=suffix_key,
+            color=color,
+        )
+
+    def delete_label_type(self, project_id: int, label_type_id: int, type: Literal["category", "span", "relation"]):
+        """Delete a label type.
+
+        Args:
+            project_id (int): The project id.
+            label_type_id (int): The label type id.
+            type (Literal["category", "span", "relation"]): The type of the label type.
+        """
+        self._get_label_type_usecase(type).delete(project_id, label_type_id)
+
+    def bulk_delete_label_types(
+        self, project_id: int, label_type_ids: List[int], type: Literal["category", "span", "relation"]
+    ):
+        """Delete multiple label types.
+
+        Args:
+            project_id (int): The project id.
+            label_type_ids (List[int]): The label type ids.
+            type (Literal["category", "span", "relation"]): The type of the label type.
+        """
+        self._get_label_type_usecase(type).bulk_delete(project_id, label_type_ids)
+
+    def upload_label_type(self, project_id: int, file_path: str, type: Literal["category", "span", "relation"]):
+        """Upload a label type.
+
+        Args:
+            project_id (int): The id of the project.
+            file_path (str): The path to the file to upload.
+            type (Literal["category", "span", "relation"]): The type of the label type.
+        """
+        self._get_label_type_usecase(type).upload(project_id, file_path)
+
+    def list_examples(self, project_id: int) -> Iterator[Example]:
+        """Return all examples.
+
+        Args:
+            project_id (int): The id of the project.
+
+        Yields:
+            Example: The examples in the project.
+        """
+        yield from self.example.list(project_id)
+
+    def find_example_by_id(self, project_id: int, example_id: int) -> Example:
+        """Find an example by id.
+
+        Args:
+            project_id (int): The id of the project.
+            example_id (int): The id of the example.
+
+        Returns:
+            Example: The found example.
+        """
+        return self.example.find_by_id(project_id, example_id)
+
+    def create_example(self, project_id: int, text: str, meta: Dict[str, Any] = None) -> Example:
+        """Create a new example.
+
+        Args:
+            project_id (int): The id of the project.
+            text (str): The text of the example.
+            meta (Dict[str, Any]): The meta data of the example.
+
+        Returns:
+            Example: The created example.
+        """
+        return self.example.create(project_id, text, meta)
+
+    def update_example(
+        self, project_id: int, example_id: int, text: str = None, meta: Dict[str, Any] = None
+    ) -> Example:
+        """Update an example.
+
+        Args:
+            project_id (int): The id of the project.
+            example_id (int): The id of the example.
+            text (str): The text of the example.
+            meta (Dict[str, Any]): The meta data of the example.
+
+        Returns:
+            Example: The updated example.
+        """
+        return self.example.update(project_id, example_id, text, meta)
+
+    def delete_example(self, project_id: int, example_id: int):
+        """Delete an example.
+
+        Args:
+            project_id (int): The id of the project.
+            example_id (int): The id of the example.
+        """
+        self.example.delete(project_id, example_id)
+
+    def bulk_delete_examples(self, project_id: int, example_ids: List[int]):
+        """Delete multiple examples.
+
+        Args:
+            project_id (int): The id of the project.
+            example_ids (List[int]): The ids of the examples.
+        """
+        self.example.bulk_delete(project_id, example_ids)
+
+    def delete_all_examples(self, project_id: int):
+        """Delete all examples.
+
+        Args:
+            project_id (int): The id of the project.
+        """
+        self.example.delete_all(project_id)
+
+    def update_example_state(self, project_id: int, example_id: int):
+        """Update the state of an example.
+
+        Args:
+            project_id (int): The id of the project.
+            example_id (int): The id of the example.
+        """
+        self.example.update_state(project_id, example_id)
+
+    def find_comment_by_id(self, project_id: int, comment_id: int) -> Comment:
+        """Find a comment by id.
+
+        Args:
+            project_id (int): The id of the project.
+            comment_id (int): The id of the comment.
+
+        Returns:
+            Comment: The found comment.
+        """
+        return self.comment.find_by_id(project_id, comment_id)
+
+    def list_comments(self, project_id: int, example_id: int, query: str = "") -> Iterator[Comment]:
+        """Return all comments.
+
+        Args:
+            project_id (int): The id of the project.
+            example_id (int): The id of the example.
+            query (str): The query string to filter comments.
+
+        Yields:
+            Comment: The comments in the project.
+        """
+        yield from self.comment.list(project_id, example_id, query)
+
+    def create_comment(self, project_id: int, example_id: int, text: str) -> Comment:
+        """Create a new comment.
+
+        Args:
+            project_id (int): The id of the project.
+            example_id (int): The id of the example.
+            text (str): The text of the comment.
+
+        Returns:
+            Comment: The created comment.
+        """
+        return self.comment.create(project_id, example_id, text)
+
+    def update_comment(self, project_id: int, comment_id: int, text: str) -> Comment:
+        """Update a comment.
+
+        Args:
+            project_id (int): The id of the project.
+            comment_id (int): The id of the comment.
+            text (str): The text of the comment.
+
+        Returns:
+            Comment: The updated comment.
+        """
+        return self.comment.update(project_id, comment_id, text)
+
+    def delete_comment(self, project_id: int, comment_id: int):
+        """Delete a comment.
+
+        Args:
+            project_id (int): The id of the project.
+            comment_id (int): The id of the comment.
+        """
+        self.comment.delete(project_id, comment_id)
+
+    def bulk_delete_comments(self, project_id: int, comment_ids: List[int]):
+        """Delete multiple comments.
+
+        Args:
+            project_id (int): The id of the project.
+            comment_ids (List[int]): The ids of the comments.
+        """
+        self.comment.bulk_delete(project_id, comment_ids)
+
+    def list_upload_options(self, project_id: int) -> List[DataImportOption]:
+        """Return all upload options.
+
+        Args:
+            project_id (int): The id of the project.
+
+        Returns:
+            List[Option]: The list of the upload options.
+        """
+        return self.data_import.list_options(project_id)
+
+    def list_download_options(self, project_id: int) -> List[DataExportOption]:
+        """Return all download options.
+
+        Args:
+            project_id (int): The id of the project.
+
+        Returns:
+            List[Option]: The list of the download options.
+        """
+        return self.data_export.list_options(project_id)
+
+    def upload(
+        self,
+        project_id: int,
+        file_paths: List[str],
+        task: AvailableTask,
+        format: str,
+        column_data: str = "text",
+        column_label: str = "label",
+    ) -> TaskStatus:
+        """Upload a file.
+
+        Args:
+            project_id (int): The id of the project.
+            file_paths (List[str]): The list of the file paths.
+            task (AvailableTask): The task of the upload.
+            format (str): The format of the upload.
+            column_data (str): The column name of the data.
+            column_label (str): The column name of the label.
+
+        Returns:
+            TaskStatus: The status of the upload task.
+        """
+        return self.data_import.upload(project_id, file_paths, task, format, column_data, column_label)
+
+    def download(self, project_id: int, format: str, only_approved=False, dir_name=".") -> pathlib.Path:
+        """Download a file.
+
+        Args:
+            project_id (int): The id of the project.
+            format (str): The format of the download.
+            only_approved (bool): Whether to export approved data only.
+            dir_name (str): The directory to save the file.
+
+        Returns:
+            pathlib.Path: The path to the downloaded file.
+        """
+        return self.data_export.download(project_id, format, only_approved, dir_name)
+
+    def find_member_by_id(self, project_id: int, member_id: int) -> Member:
+        """Find a member by id.
+
+        Args:
+            project_id (int): The id of the project to find.
+            member_id (int): The id of the member to find.
+
+        Returns:
+            Member: The found member.
+        """
+        return self.member.find_by_id(project_id, member_id)
+
+    def list_members(self, project_id: int) -> List[Member]:
+        """Return all members.
+
+        Args:
+            project_id (int): The id of the project.
+
+        Returns:
+            List[Member]: The members in the project.
+        """
+        return self.member.list(project_id)
+
+    def add_member(
+        self,
+        project_id: int,
+        username: str,
+        role_name: str,
+    ) -> Member:
+        """Create a new member.
+
+        Args:
+            project_id (int): The id of the project.
+            username (str): The username of the future member.
+            role_name (str): The role of the future member.
+
+        Returns:
+            Member: The created member.
+        """
+        return self.member.add(project_id, username, role_name)
+
+    def update_member(
+        self,
+        project_id: int,
+        member_id: int,
+        role_name: str,
+    ) -> Member:
+        """Update a member role.
+
+        Args:
+            project_id (int): The id of the project.
+            member_id (int): The id of the member.
+            role_name (str): The role of the member.
+
+        Returns:
+            Member: The updated member.
+        """
+        return self.member.update(project_id, member_id, role_name)
+
+    def delete_member(self, project_id: int, member_id: int):
+        """Delete a member.
+
+        Args:
+            project_id (int): The id of the project.
+            member_id (int): The id of the member.
+        """
+        self.member.delete(project_id, member_id)
+
+    def bulk_delete_members(self, project_id: int, member_ids: List[int]):
+        """Delete multiple members.
+
+        Args:
+            project_id (int): The id of the project.
+            member_ids (List[int]): The ids of the members.
+        """
+        self.member.bulk_delete(project_id, member_ids)
+
+    def find_category_by_id(self, project_id: int, example_id: int, label_id: int) -> Category:
+        """Find a category by id.
+
+        Args:
+            project_id (int): The id of the project to find.
+            example_id (int): The id of the example.
+            label_id (int): The id of the label to find.
+
+        Returns:
+            Category: The found category.
+        """
+        return self.category.find_by_id(project_id, example_id, label_id)
+
+    def find_span_by_id(self, project_id: int, example_id: int, label_id: int) -> Span:
+        """Find a span by id.
+
+        Args:
+            project_id (int): The id of the project to find.
+            example_id (int): The id of the example.
+            label_id (int): The id of the label.
+
+        Returns:
+            Span: The found span.
+        """
+        return self.span.find_by_id(project_id, example_id, label_id)
+
+    def find_relation_by_id(self, project_id: int, example_id: int, label_id: int) -> Relation:
+        """Find a relation by id.
+
+        Args:
+            project_id (int): The id of the project to find.
+            example_id (int): The id of the example.
+            label_id (int): The id of the label.
+
+        Returns:
+            Relation: The found relation.
+        """
+        return self.relation.find_by_id(project_id, example_id, label_id)
+
+    def find_text_by_id(self, project_id: int, example_id: int, label_id: int) -> Text:
+        """Find a text by id.
+
+        Args:
+            project_id (int): The id of the project to find.
+            example_id (int): The id of the example.
+            label_id (int): The id of the label.
+
+        Returns:
+            Text: The found text.
+        """
+        return self.text.find_by_id(project_id, example_id, label_id)
+
+    def find_segment_by_id(self, project_id: int, example_id: int, label_id: int) -> Segment:
+        """Find a segment by id.
+
+        Args:
+            project_id (int): The id of the project to find.
+            example_id (int): The id of the example.
+            label_id (int): The id of the label.
+
+        Returns:
+            Segment: The found segment.
+        """
+        return self.segment.find_by_id(project_id, example_id, label_id)
+
+    def find_bounding_box_by_id(self, project_id: int, example_id: int, label_id: int) -> BoundingBox:
+        """Find a bounding box by id.
+
+        Args:
+            project_id (int): The id of the project to find.
+            example_id (int): The id of the example.
+            label_id (int): The id of the label.
+
+        Returns:
+            BoundingBox: The found bounding box.
+        """
+        return self.bounding_box.find_by_id(project_id, example_id, label_id)
+
+    def list_categories(self, project_id: int, example_id: int) -> List[Category]:
+        """Return all categories.
+
+        Args:
+            project_id (int): The id of the project.
+            example_id (int): The id of the example.
+
+        Returns:
+            List[Category]: The categories in the project.
+        """
+        return self.category.list(project_id, example_id)
+
+    def list_spans(self, project_id: int, example_id: int) -> List[Span]:
+        """Return all spans.
+
+        Args:
+            project_id (int): The id of the project.
+            example_id (int): The id of the example.
+
+        Returns:
+            List[Span]: The spans in the project.
+        """
+        return self.span.list(project_id, example_id)
+
+    def list_relations(self, project_id: int, example_id: int) -> List[Relation]:
+        """Return all relations.
+
+        Args:
+            project_id (int): The id of the project.
+            example_id (int): The id of the example.
+
+        Returns:
+            List[Relation]: The relations in the project.
+        """
+        return self.relation.list(project_id, example_id)
+
+    def list_texts(self, project_id: int, example_id: int) -> List[Text]:
+        """Return all texts.
+
+        Args:
+            project_id (int): The id of the project.
+            example_id (int): The id of the example.
+
+        Returns:
+            List[Text]: The texts in the project.
+        """
+        return self.text.list(project_id, example_id)
+
+    def list_segments(self, project_id: int, example_id: int) -> List[Segment]:
+        """Return all segments.
+
+        Args:
+            project_id (int): The id of the project.
+            example_id (int): The id of the example.
+
+        Returns:
+            List[Segment]: The segments in the project.
+        """
+        return self.segment.list(project_id, example_id)
+
+    def list_bounding_boxes(self, project_id: int, example_id: int) -> List[BoundingBox]:
+        """Return all bounding boxes.
+
+        Args:
+            project_id (int): The id of the project.
+            example_id (int): The id of the example.
+
+        Returns:
+            List[BoundingBox]: The bounding boxes in the project.
+        """
+        return self.bounding_box.list(project_id, example_id)
+
+    def delete_category(self, project_id: int, example_id: int, label_id: int):
+        """Delete a category.
+
+        Args:
+            project_id (int): The project id.
+            example_id (int): The id of the example.
+            label_id (int): The label id.
+        """
+        self.category.delete(project_id, example_id, label_id)
+
+    def delete_span(self, project_id: int, example_id: int, label_id: int):
+        """Delete a span.
+
+        Args:
+            project_id (int): The project id.
+            example_id (int): The id of the example.
+            label_id (int): The label id.
+        """
+        self.span.delete(project_id, example_id, label_id)
+
+    def delete_relation(self, project_id: int, example_id: int, label_id: int):
+        """Delete a relation.
+
+        Args:
+            project_id (int): The project id.
+            example_id (int): The id of the example.
+            label_id (int): The label id.
+        """
+        self.relation.delete(project_id, example_id, label_id)
+
+    def delete_text(self, project_id: int, example_id: int, label_id: int):
+        """Delete a text.
+
+        Args:
+            project_id (int): The project id.
+            example_id (int): The id of the example.
+            label_id (int): The label id.
+        """
+        self.text.delete(project_id, example_id, label_id)
+
+    def delete_segment(self, project_id: int, example_id: int, label_id: int):
+        """Delete a segment.
+
+        Args:
+            project_id (int): The project id.
+            example_id (int): The id of the example.
+            label_id (int): The label id.
+        """
+        self.segment.delete(project_id, example_id, label_id)
+
+    def delete_bounding_box(self, project_id: int, example_id: int, label_id: int):
+        """Delete a bounding box.
+
+        Args:
+            project_id (int): The project id.
+            example_id (int): The id of the example.
+            label_id (int): The label id.
+        """
+        self.bounding_box.delete(project_id, example_id, label_id)
+
+    def delete_all_categories(self, project_id: int, example_id: int):
+        """Delete all categories.
+
+        Args:
+            project_id (int): The id of the project.
+            example_id (int): The id of the example.
+        """
+        self.category.delete_all(project_id, example_id)
+
+    def delete_all_spans(self, project_id: int, example_id: int):
+        """Delete all spans.
+
+        Args:
+            project_id (int): The id of the project.
+            example_id (int): The id of the example.
+        """
+        self.span.delete_all(project_id, example_id)
+
+    def delete_all_relations(self, project_id: int, example_id: int):
+        """Delete all relations.
+
+        Args:
+            project_id (int): The id of the project.
+            example_id (int): The id of the example.
+        """
+        self.relation.delete_all(project_id, example_id)
+
+    def delete_all_texts(self, project_id: int, example_id: int):
+        """Delete all texts.
+
+        Args:
+            project_id (int): The id of the project.
+            example_id (int): The id of the example.
+        """
+        self.text.delete_all(project_id, example_id)
+
+    def delete_all_segments(self, project_id: int, example_id: int):
+        """Delete all segments.
+
+        Args:
+            project_id (int): The id of the project.
+            example_id (int): The id of the example.
+        """
+        self.segment.delete_all(project_id, example_id)
+
+    def delete_all_bounding_boxes(self, project_id: int, example_id: int):
+        """Delete all bounding boxes.
+
+        Args:
+            project_id (int): The id of the project.
+            example_id (int): The id of the example.
+        """
+        self.bounding_box.delete_all(project_id, example_id)
+
+    def create_category(
+        self, project_id: int, example_id: int, label: int | str, human_annotated=False, confidence=0.0
+    ) -> Category:
+        """Create a new category label.
+
+        Args:
+            project_id (int): The id of the project.
+            example_id (int): The id of the example.
+            label (int | str): The label to create.
+            human_annotated (bool): Whether the label is human annotated. Defaults to False.
+            confidence (float): The confidence of the label. Defaults to 0.0.
+
+        Returns:
+            Category: The created category label.
+        """
+        return self.category.create(project_id, example_id, label, human_annotated, confidence)
+
+    def create_span(
+        self,
+        project_id: int,
+        example_id: int,
+        start_offset: int,
+        end_offset: int,
+        label: int | str,
+        human_annotated: bool = False,
+        confidence: float = 0.0,
+    ) -> Span:
+        """Create a new span label.
+
+        Args:
+            project_id (int): The id of the project.
+            example_id (int): The id of the example.
+            start_offset (int): The start offset of the span.
+            end_offset (int): The end offset of the span.
+            label (int | str): The label to create.
+            human_annotated (bool): Whether the label is human annotated. Defaults to False.
+            confidence (float): The confidence of the label. Defaults to 0.0.
+
+        Returns:
+            Span: The created span label.
+        """
+        return self.span.create(project_id, example_id, start_offset, end_offset, label, human_annotated, confidence)
+
+    def create_relation(
+        self,
+        project_id: int,
+        example_id: int,
+        from_id: int,
+        to_id: int,
+        label: int | str,
+        human_annotated: bool = False,
+        confidence: float = 0.0,
+    ) -> Relation:
+        """Create a new relation label.
+
+        Args:
+            project_id (int): The id of the project.
+            example_id (int): The id of the example.
+            from_id (int): The id of the from span.
+            to_id (int): The id of the to span.
+            label (int | str): The label to create.
+            human_annotated (bool): Whether the label is human annotated. Defaults to False.
+            confidence (float): The confidence of the label. Defaults to 0.0.
+
+        Returns:
+            Relation: The created relation label.
+        """
+        return self.relation.create(project_id, example_id, from_id, to_id, label, human_annotated, confidence)
+
+    def create_text(
+        self,
+        project_id: int,
+        example_id: int,
+        text: str,
+        human_annotated: bool = False,
+        confidence: float = 0.0,
+    ) -> Text:
+        """Create a new text label.
+
+        Args:
+            project_id (int): The id of the project.
+            example_id (int): The id of the example.
+            text (str): The text to create.
+            human_annotated (bool): Whether the label is human annotated. Defaults to False.
+            confidence (float): The confidence of the label. Defaults to 0.0.
+
+        Returns:
+            Text: The created text label.
+        """
+        return self.text.create(project_id, example_id, text, human_annotated, confidence)
+
+    def create_bounding_box(
+        self,
+        project_id: int,
+        example_id: int,
+        x: float,
+        y: float,
+        width: float,
+        height: float,
+        label: int | str,
+        human_annotated: bool = False,
+        confidence: float = 0.0,
+    ) -> BoundingBox:
+        """Create a new bounding box label.
+
+        Args:
+            project_id (int): The id of the project.
+            example_id (int): The id of the example.
+            x (float): The x coordinate of the bounding box.
+            y (float): The y coordinate of the bounding box.
+            width (float): The width of the bounding box.
+            height (float): The height of the bounding box.
+            label (int | str): The label to create.
+            human_annotated (bool): Whether the label is human annotated. Defaults to False.
+            confidence (float): The confidence of the label. Defaults to 0.0.
+
+        Returns:
+            BoundingBox: The created bounding box label.
+        """
+        return self.bounding_box.create(project_id, example_id, x, y, width, height, label, human_annotated, confidence)
+
+    def create_segment(
+        self,
+        project_id: int,
+        example_id: int,
+        points: List[float],
+        label: int | str,
+        human_annotated: bool = False,
+        confidence: float = 0.0,
+    ) -> Segment:
+        """Create a new segment label.
+
+        Args:
+            project_id (int): The id of the project.
+            example_id (int): The id of the example.
+            points (List[float]): The points of the segment.
+            label (int | str): The label to create.
+            human_annotated (bool): Whether the label is human annotated. Defaults to False.
+            confidence (float): The confidence of the label. Defaults to 0.0.
+
+        Returns:
+            Segment: The created segment label.
+        """
+        return self.segment.create(project_id, example_id, points, label, human_annotated, confidence)
+
+    def update_category(
+        self,
+        project_id: int,
+        example_id: int,
+        label_id: int,
+        label: Optional[int | str] = None,
+        human_annotated: bool = None,
+        confidence: float = None,
+    ) -> Category:
+        """Update a category label.
+
+        Args:
+            project_id (int): The id of the project.
+            example_id (int): The id of the example.
+            label_id (int): The id of the label.
+            label (int | str): The label to create.
+            human_annotated (bool): Whether the label is human annotated. Defaults to None.
+            confidence (float): The confidence of the label. Defaults to None.
+
+        Returns:
+            Category: The updated category label.
+        """
+        return self.category.update(project_id, example_id, label_id, label, human_annotated, confidence)
+
+    def update_span(
+        self,
+        project_id: int,
+        example_id: int,
+        label_id: int,
+        start_offset: Optional[int] = None,
+        end_offset: Optional[int] = None,
+        label: Optional[int | str] = None,
+        human_annotated: bool = None,
+        confidence: float = None,
+    ) -> Span:
+        """Update a span label.
+
+        Args:
+            project_id (int): The id of the project.
+            example_id (int): The id of the example.
+            label_id (int): The id of the label.
+            start_offset (int): The start offset of the span.
+            end_offset (int): The end offset of the span.
+            label (int | str): The label to create.
+            human_annotated (bool): Whether the label is human annotated. Defaults to None.
+            confidence (float): The confidence of the label. Defaults to None.
+
+        Returns:
+            Span: The updated span label.
+        """
+        return self.span.update(
+            project_id, example_id, label_id, start_offset, end_offset, label, human_annotated, confidence
+        )
+
+    def update_relation(
+        self,
+        project_id: int,
+        example_id: int,
+        label_id: int,
+        from_id: Optional[int] = None,
+        to_id: Optional[int] = None,
+        label: Optional[int | str] = None,
+        human_annotated: bool = None,
+        confidence: float = None,
+    ) -> Relation:
+        """Update a relation label.
+
+        Args:
+            project_id (int): The id of the project.
+            example_id (int): The id of the example.
+            label_id (int): The id of the label.
+            from_id (int): The id of the from span.
+            to_id (int): The id of the to span.
+            label (int | str): The label to create.
+            human_annotated (bool): Whether the label is human annotated. Defaults to None.
+            confidence (float): The confidence of the label. Defaults to None.
+
+        Returns:
+            Relation: The updated relation label.
+        """
+        return self.relation.update(
+            project_id, example_id, label_id, from_id, to_id, label, human_annotated, confidence
+        )
+
+    def update_text(
+        self,
+        project_id: int,
+        example_id: int,
+        label_id: int,
+        text: Optional[str] = None,
+        human_annotated: bool = None,
+        confidence: float = None,
+    ) -> Text:
+        """Update a text label.
+
+        Args:
+            project_id (int): The id of the project.
+            example_id (int): The id of the example.
+            label_id (int): The id of the label.
+            text (str): The text to update.
+            human_annotated (bool): Whether the label is human annotated. Defaults to None.
+            confidence (float): The confidence of the label. Defaults to None.
+
+        Returns:
+            Text: The updated text label.
+        """
+        return self.text.update(project_id, example_id, label_id, text, human_annotated, confidence)
+
+    def update_bounding_box(
+        self,
+        project_id: int,
+        example_id: int,
+        label_id: int,
+        x: Optional[float] = None,
+        y: Optional[float] = None,
+        width: Optional[float] = None,
+        height: Optional[float] = None,
+        label: Optional[int | str] = None,
+        human_annotated: bool = None,
+        confidence: float = None,
+    ) -> BoundingBox:
+        """Update a bounding box label.
+
+        Args:
+            project_id (int): The id of the project.
+            example_id (int): The id of the example.
+            label_id (int): The id of the label.
+            x (float): The x coordinate of the bounding box.
+            y (float): The y coordinate of the bounding box.
+            width (float): The width of the bounding box.
+            height (float): The height of the bounding box.
+            label (int | str): The label to create.
+            human_annotated (bool): Whether the label is human annotated. Defaults to None.
+            confidence (float): The confidence of the label. Defaults to None.
+
+        Returns:
+            BoundingBox: The updated bounding box label.
+        """
+        return self.bounding_box.update(
+            project_id, example_id, label_id, x, y, width, height, label, human_annotated, confidence
+        )
+
+    def update_segment(
+        self,
+        project_id: int,
+        example_id: int,
+        label_id: int,
+        points: Optional[List[float]] = None,
+        label: Optional[int | str] = None,
+        human_annotated: bool = None,
+        confidence: float = None,
+    ) -> Segment:
+        """Update a segment label.
+
+        Args:
+            project_id (int): The id of the project.
+            example_id (int): The id of the example.
+            label_id (int): The id of the label.
+            points (List[float]): The points of the segment.
+            label (int | str): The label to create.
+            human_annotated (bool): Whether the label is human annotated. Defaults to None.
+            confidence (float): The confidence of the label. Defaults to None.
+
+        Returns:
+            Segment: The updated segment label.
+        """
+        return self.segment.update(project_id, example_id, label_id, points, label, human_annotated, confidence)
diff --git a/doccano_client/models/__init__.py b/doccano_client/models/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/doccano_client/models/comment.py b/doccano_client/models/comment.py
new file mode 100644
index 0000000..627474b
--- /dev/null
+++ b/doccano_client/models/comment.py
@@ -0,0 +1,14 @@
+from typing import Optional
+
+from pydantic import BaseModel
+
+
+class Comment(BaseModel):
+    """Contains the data relevant to a comment on a Doccano project"""
+
+    id: Optional[int]
+    text: str = ""
+    example: int
+    user: Optional[int]
+    username: Optional[str]
+    created_at: Optional[str]
diff --git a/doccano_client/models/data_download.py b/doccano_client/models/data_download.py
new file mode 100644
index 0000000..0eb40ec
--- /dev/null
+++ b/doccano_client/models/data_download.py
@@ -0,0 +1,6 @@
+from pydantic import BaseModel
+
+
+class Option(BaseModel):
+    name: str
+    example: str = ""
diff --git a/doccano_client/models/data_upload.py b/doccano_client/models/data_upload.py
new file mode 100644
index 0000000..2c73d63
--- /dev/null
+++ b/doccano_client/models/data_upload.py
@@ -0,0 +1,26 @@
+from enum import Enum
+from typing import Any, Dict
+
+from pydantic import BaseModel
+
+
+class Option(BaseModel):
+    task_id: str
+    name: str
+    display_name: str
+    example: str
+    accept_types: str
+    properties: Dict[str, Any]
+
+
+class AvailableTask(str, Enum):
+    DOCUMENT_CLASSIFICATION = "DocumentClassification"
+    SEQUENCE_LABELING = "SequenceLabeling"
+    SEQ2SEQ = "Seq2seq"
+    SPEECH2TEXT = "Speech2text"
+    IMAGE_CLASSIFICATION = "ImageClassification"
+    BOUNDING_BOX = "BoundingBox"
+    SEGMENTATION = "Segmentation"
+    IMAGE_CAPTIONING = "ImageCaptioning"
+    INTENT_DETECTION_AND_SLOT_FILLING = "IntentDetectionAndSlotFilling"
+    RELATION_EXTRACTION = "RelationExtraction"
diff --git a/doccano_client/models/example.py b/doccano_client/models/example.py
new file mode 100644
index 0000000..610e19a
--- /dev/null
+++ b/doccano_client/models/example.py
@@ -0,0 +1,16 @@
+from typing import Any, Dict, Optional
+
+from pydantic import BaseModel, Field
+
+
+class Example(BaseModel):
+    """Contains the data relevant to a example on a Doccano project"""
+
+    id: Optional[int]
+    text: str = ""
+    meta: Dict[str, Any] = Field(default_factory=dict)
+    annotation_approver: Optional[str] = None
+    comment_count: int = 0
+    is_confirmed: bool = False
+    filename: str = ""
+    upload_name: str = ""
diff --git a/doccano_client/models/label.py b/doccano_client/models/label.py
new file mode 100644
index 0000000..cfa3822
--- /dev/null
+++ b/doccano_client/models/label.py
@@ -0,0 +1,64 @@
+from typing import List, Optional
+
+from pydantic import (
+    BaseModel,
+    Field,
+    NonNegativeFloat,
+    NonNegativeInt,
+    root_validator,
+    validator,
+)
+
+
+class Label(BaseModel):
+    id: Optional[int]
+    example: int
+    prob: float = 0.0
+    manual: bool = False
+    user: Optional[int]
+
+
+class Category(Label):
+    label: int
+
+
+class Span(Label):
+    label: int
+    start_offset: NonNegativeInt
+    end_offset: NonNegativeInt
+
+    @root_validator
+    def check_start_offset_is_less_than_end_offset(cls, values):
+        start_offset, end_offset = values.get("start_offset"), values.get("end_offset")
+        if start_offset >= end_offset:
+            raise ValueError("start_offset must be less than end_offset.")
+        return values
+
+
+class Relation(Label):
+    from_id: int
+    to_id: int
+    type: int
+
+
+class BoundingBox(Label):
+    x: NonNegativeFloat
+    y: NonNegativeFloat
+    width: NonNegativeFloat
+    height: NonNegativeFloat
+    label: int
+
+
+class Segment(Label):
+    points: List[NonNegativeFloat] = Field(default_factory=list)
+    label: int
+
+    @validator("points")
+    def check_points_length_is_even(cls, points):
+        if len(points) % 2 != 0:
+            raise ValueError("The length of points must be even.")
+        return points
+
+
+class Text(Label):
+    text: str
diff --git a/doccano_client/models/label_type.py b/doccano_client/models/label_type.py
new file mode 100644
index 0000000..350738c
--- /dev/null
+++ b/doccano_client/models/label_type.py
@@ -0,0 +1,98 @@
+import random
+import re
+from typing import Literal, Optional
+
+from pydantic import BaseModel, Field, root_validator
+from pydantic.types import ConstrainedStr
+
+PREFIX_KEY = Literal["ctrl", "shift", "ctrl shift"]
+SUFFIX_KEY = Literal[
+    "0",
+    "1",
+    "2",
+    "3",
+    "4",
+    "5",
+    "6",
+    "7",
+    "8",
+    "9",
+    "a",
+    "b",
+    "c",
+    "d",
+    "e",
+    "f",
+    "g",
+    "h",
+    "i",
+    "j",
+    "k",
+    "l",
+    "m",
+    "n",
+    "o",
+    "p",
+    "q",
+    "r",
+    "s",
+    "t",
+    "u",
+    "v",
+    "w",
+    "x",
+    "y",
+    "z",
+]
+
+
+def generate_random_hex_color():
+    return f"#{random.randint(0, 0xFFFFFF):06x}"
+
+
+class Text(ConstrainedStr):
+    min_length = 1
+    max_length = 100
+    strip_whitespace = True
+
+
+class Color(ConstrainedStr):
+    regex = re.compile(r"#[a-fA-F0-9]{6}")
+
+
+class LabelType(BaseModel):
+    """Contains the data relevant to a LabelType on a Doccano project"""
+
+    id: Optional[int]
+    text: Text
+    prefix_key: Optional[PREFIX_KEY] = None
+    suffix_key: Optional[SUFFIX_KEY] = None
+    background_color: Color = Field(default_factory=generate_random_hex_color)
+    text_color: Color = Field(default="#ffffff")
+
+    @root_validator
+    def deny_only_prefix_key(cls, values):
+        prefix_key = values.get("prefix_key")
+        suffix_key = values.get("suffix_key")
+        if prefix_key and suffix_key is None:
+            raise ValueError("You must specify a suffix_key if you specify a prefix_key.")
+        return values
+
+    @classmethod
+    def create(
+        cls,
+        text: str,
+        prefix_key: PREFIX_KEY = None,
+        suffix_key: SUFFIX_KEY = None,
+        color: Optional[str] = None,
+        id: Optional[int] = None,
+    ):
+        if color is None:
+            return cls(id=id, text=text, prefix_key=prefix_key, suffix_key=suffix_key)
+        else:
+            return cls(id=id, text=text, prefix_key=prefix_key, suffix_key=suffix_key, background_color=color)
+
+
+CategoryType = LabelType
+SpanType = LabelType
+RelationType = LabelType
diff --git a/doccano_client/models/member.py b/doccano_client/models/member.py
new file mode 100644
index 0000000..aac8e37
--- /dev/null
+++ b/doccano_client/models/member.py
@@ -0,0 +1,15 @@
+from typing import Literal, Optional
+
+from pydantic import BaseModel
+
+ROLE_NAME = Literal["project_admin", "annotator", "annotation_approver"]
+
+
+class Member(BaseModel):
+    """Contains the data relevant to a member on a Doccano project"""
+
+    id: Optional[int]
+    user: int
+    role: int
+    username: str = ""
+    rolename: str = ""
diff --git a/doccano_client/models/metrics.py b/doccano_client/models/metrics.py
new file mode 100644
index 0000000..3261455
--- /dev/null
+++ b/doccano_client/models/metrics.py
@@ -0,0 +1,28 @@
+from typing import List
+
+from pydantic import BaseModel
+
+
+class LabelCount(BaseModel):
+    """Contains the data relevant to a label frequency on a Doccano project"""
+
+    label: str
+    count: int
+
+
+class LabelDistribution(BaseModel):
+    """Contains the data relevant to a role on a Doccano project"""
+
+    username: str
+    counts: List[LabelCount]
+
+
+class Progress(BaseModel):
+    total: int
+    remaining: int
+    completed: int
+
+
+class MemberProgress(BaseModel):
+    username: str
+    progress: Progress
diff --git a/doccano_client/models/project.py b/doccano_client/models/project.py
new file mode 100644
index 0000000..f297588
--- /dev/null
+++ b/doccano_client/models/project.py
@@ -0,0 +1,91 @@
+from __future__ import annotations
+
+from enum import Enum
+from typing import AbstractSet, Any, Dict, List, Mapping, Optional, Union
+
+from pydantic import BaseModel
+from pydantic.types import ConstrainedStr
+
+IntStr = Union[int, str]
+AbstractSetIntStr = AbstractSet[IntStr]
+DictStrAny = Dict[str, Any]
+MappingIntStrAny = Mapping[IntStr, Any]
+
+
+class ProjectType(str, Enum):
+    """Constants pointing to the types of project resource types"""
+
+    DOCUMENT_CLASSIFICATION = "DocumentClassification"
+    SEQUENCE_LABELING = "SequenceLabeling"
+    SEQ2SEQ = "Seq2seq"
+    SPEECH2TEXT = "Speech2text"
+    IMAGE_CLASSIFICATION = "ImageClassification"
+    BOUNDING_BOX = "BoundingBox"
+    SEGMENTATION = "Segmentation"
+    IMAGE_CAPTIONING = "ImageCaptioning"
+    INTENT_DETECTION_AND_SLOT_FILLING = "IntentDetectionAndSlotFilling"
+
+
+class Name(ConstrainedStr):
+    min_length = 1
+    max_length = 100
+    strip_whitespace = True
+
+
+class Description(ConstrainedStr):
+    min_length = 1
+    strip_whitespace = True
+
+
+class Project(BaseModel):
+    id: Optional[int]
+    name: Name
+    description: Description
+    guideline: str = "Please write annotation guideline."
+    project_type: ProjectType
+    random_order: bool = False
+    collaborative_annotation: bool = False
+    single_class_classification: bool = False
+    allow_overlapping: bool = False
+    grapheme_mode: bool = False
+    use_relation: bool = False
+    tags: List[str] = []
+
+    def dict(
+        self,
+        *,
+        include: Union["AbstractSetIntStr", "MappingIntStrAny"] = None,
+        exclude: Union["AbstractSetIntStr", "MappingIntStrAny"] = None,
+        by_alias: bool = False,
+        skip_defaults: bool = None,
+        exclude_unset: bool = False,
+        exclude_defaults: bool = False,
+        exclude_none: bool = False,
+    ) -> "DictStrAny":
+        attrs = super().dict(
+            include=include,
+            exclude=exclude,
+            by_alias=by_alias,
+            skip_defaults=skip_defaults,
+            exclude_unset=exclude_unset,
+            exclude_defaults=exclude_defaults,
+            exclude_none=exclude_none,
+        )
+        attrs["resourcetype"] = self.resource_type
+        attrs["project_type"] = self.project_type.value
+        return attrs
+
+    @property
+    def resource_type(self) -> str:
+        PROJECT_TO_RESOURCE_TYPE = {
+            ProjectType.DOCUMENT_CLASSIFICATION: "TextClassificationProject",
+            ProjectType.SEQUENCE_LABELING: "SequenceLabelingProject",
+            ProjectType.SEQ2SEQ: "Seq2seqProject",
+            ProjectType.SPEECH2TEXT: "Speech2textProject",
+            ProjectType.IMAGE_CLASSIFICATION: "ImageClassificationProject",
+            ProjectType.BOUNDING_BOX: "BoundingBoxProject",
+            ProjectType.SEGMENTATION: "SegmentationProject",
+            ProjectType.IMAGE_CAPTIONING: "ImageCaptioningProject",
+            ProjectType.INTENT_DETECTION_AND_SLOT_FILLING: "IntentDetectionAndSlotFillingProject",
+        }
+        return PROJECT_TO_RESOURCE_TYPE[self.project_type]
diff --git a/doccano_client/models/role.py b/doccano_client/models/role.py
new file mode 100644
index 0000000..a0236f6
--- /dev/null
+++ b/doccano_client/models/role.py
@@ -0,0 +1,8 @@
+from pydantic import BaseModel
+
+
+class Role(BaseModel):
+    """Contains the data relevant to a role on a Doccano project"""
+
+    id: int
+    name: str
diff --git a/doccano_client/models/task_status.py b/doccano_client/models/task_status.py
new file mode 100644
index 0000000..d7e7bfc
--- /dev/null
+++ b/doccano_client/models/task_status.py
@@ -0,0 +1,11 @@
+from typing import Any, Optional
+
+from pydantic import BaseModel
+
+
+class TaskStatus(BaseModel):
+    """Contains the data relevant to a task status on a Doccano project"""
+
+    ready: bool = False
+    result: Optional[Any] = None
+    error: Optional[Any] = None
diff --git a/doccano_client/models/user.py b/doccano_client/models/user.py
new file mode 100644
index 0000000..43c4add
--- /dev/null
+++ b/doccano_client/models/user.py
@@ -0,0 +1,10 @@
+from pydantic import BaseModel
+
+
+class User(BaseModel):
+    """Contains the data relevant to a user on a Doccano project"""
+
+    id: int
+    username: str
+    is_superuser: bool
+    is_staff: bool
diff --git a/doccano_client/repositories/__init__.py b/doccano_client/repositories/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/doccano_client/repositories/base.py b/doccano_client/repositories/base.py
new file mode 100644
index 0000000..f743563
--- /dev/null
+++ b/doccano_client/repositories/base.py
@@ -0,0 +1,137 @@
+import requests
+from requests import Response, exceptions
+
+
+class DoccanoAPIError(exceptions.HTTPError):
+    def __init__(self, response: Response):
+        """Initialize the exception with the response.
+
+        Args:
+            response (Response): The response to initialize the exception
+        """
+        super().__init__(str(response.json()), response=response)
+
+
+def verbose_raise_for_status(response: Response) -> Response:
+    """Output a bad response's text before raising for verbosity, return response otherwise.
+
+    Args:
+        response (Response): The response to raise for status
+
+    Returns:
+        Response: The response
+
+    Raises:
+        DoccanoAPIError: if request raises HTTPError.
+    """
+    try:
+        response.raise_for_status()
+    except exceptions.HTTPError as err:
+        raise DoccanoAPIError(err.response)
+    return response
+
+
+class BaseRepository:
+    """Base repository for interacting with the Doccano API"""
+
+    def __init__(self, base_url: str) -> None:
+        self._base_url = base_url
+        self._session = requests.Session()
+        headers = {
+            "content-type": "application/json",
+            "accept": "application/json",
+            "referer": base_url,
+        }
+        self._session.headers.update(headers)
+
+    @property
+    def login_url(self) -> str:
+        """Retrieve an API login url based on the base_url
+
+        Returns:
+            str: The login url
+        """
+        return f"{self._base_url}/v1/auth/login/"
+
+    @property
+    def api_url(self) -> str:
+        """Retrieve an API url based on the base_url
+
+        Returns:
+            str: The API url
+        """
+        return f"{self._base_url}/v1"
+
+    def login(self, username: str, password: str) -> None:
+        """Login to a session with the Doccano instance related to the base url
+
+        Args:
+            username (str): The username of the user
+            password (str): The password of the user
+        """
+        response = self._session.post(self.login_url, json={"username": username, "password": password})
+        # TODO: do we want to do anything with the return value token in the future?
+        verbose_raise_for_status(response)
+        self._session.headers.update({"X-CSRFToken": self._session.cookies.get("csrftoken")})
+
+    def get(self, resource: str, **kwargs) -> requests.Response:
+        """Make a get request to the Doccano API
+
+        Args:
+            resource (str): The resource to get
+            kwargs: Additional arguments to pass to the request
+
+        Returns:
+            requests.Response: The response from the API
+        """
+        if resource.startswith(self.api_url):
+            resource = resource[len(self.api_url) + 1 :]
+        url = f"{self.api_url}/{resource}"
+        response = self._session.get(url, **kwargs)
+        verbose_raise_for_status(response)
+        return response
+
+    def post(self, resource: str, **kwargs) -> requests.Response:
+        """Make a post request to the Doccano API
+
+        Args:
+            resource (str): The resource to post
+            kwargs: Additional arguments to pass to the request
+
+        Returns:
+            requests.Response: The response from the API
+        """
+        url = f"{self.api_url}/{resource}"
+        response = self._session.post(url, **kwargs)
+        verbose_raise_for_status(response)
+        return response
+
+    def put(self, resource: str, **kwargs) -> requests.Response:
+        """Make a put request to the Doccano API
+
+        Args:
+            resource (str): The resource to put
+            kwargs: Additional arguments to pass to the request
+
+        Returns:
+            requests.Response: The response from the API
+        """
+        url = f"{self.api_url}/{resource}"
+        response = self._session.put(url, **kwargs)
+        verbose_raise_for_status(response)
+        return response
+
+    def delete(self, resource: str, **kwargs) -> requests.Response:
+        """Make a delete request to the Doccano API
+
+        Args:
+            resource (str): The resource to delete
+            kwargs: Additional arguments to pass to the request
+
+        Returns:
+            requests.Response: The response from the API
+        """
+        url = f"{self.api_url}/{resource}"
+        response = self._session.delete(url, **kwargs)
+        verbose_raise_for_status(response)
+        return response
diff --git a/doccano_client/repositories/comment.py b/doccano_client/repositories/comment.py
new file mode 100644
index 0000000..cd931c4
--- /dev/null
+++ b/doccano_client/repositories/comment.py
@@ -0,0 +1,103 @@
+from __future__ import annotations
+
+from typing import Iterator, List, Optional
+
+from doccano_client.models.comment import Comment
+from doccano_client.repositories.base import BaseRepository
+
+
+class CommentRepository:
+    """Repository for interacting with the Doccano comment API"""
+
+    resource_type = "comments"
+
+    def __init__(self, client: BaseRepository):
+        self._client = client
+
+    def find_by_id(self, project_id: int, comment_id: int) -> Comment:
+        """Find a comment by id
+
+        Args:
+            project_id (int): The id of the project
+            comment_id (int): The id of the comment to find
+
+        Returns:
+            Comment: The found comment
+        """
+        response = self._client.get(f"projects/{project_id}/{self.resource_type}/{comment_id}")
+        return Comment.parse_obj(response.json())
+
+    def list(self, project_id: int, example_id: Optional[int] = None, query: str = "") -> Iterator[Comment]:
+        """Return all comments in which you are a member
+
+        Args:
+            project_id (int): The id of the project
+            example_id (Optional[int], optional): The id of the example. Defaults to None.
+            query (str): The query to search. Defaults to None.
+
+        Yields:
+            Comment: The list of the comments.
+        """
+        params = {"example": example_id, "q": query}
+        if not example_id:
+            params.pop("example")
+
+        response = self._client.get(f"projects/{project_id}/{self.resource_type}", params=params)
+        while True:
+            comments = response.json()
+            for comment in comments["results"]:
+                yield Comment.parse_obj(comment)
+
+            if comments["next"] is None:
+                break
+            else:
+                response = self._client.get(comments["next"])
+
+    def create(self, project_id: int, comment: Comment) -> Comment:
+        """Create a new comment
+
+        Args:
+            project_id (int): The id of the project
+            comment (Comment): The comment to create
+
+        Returns:
+            Comment: The created comment
+        """
+        resource = f"projects/{project_id}/{self.resource_type}?example={comment.example}"
+        response = self._client.post(resource, json=comment.dict(exclude={"id", "example"}))
+        return Comment.parse_obj(response.json())
+
+    def update(self, project_id: int, comment: Comment) -> Comment:
+        """Update a comment
+
+        Args:
+            project_id (int): The id of the project
+            comment (Comment): The comment to update
+
+        Returns:
+            Comment: The updated comment
+        """
+        resource = f"projects/{project_id}/{self.resource_type}/{comment.id}"
+        response = self._client.put(resource, json=comment.dict())
+        return Comment.parse_obj(response.json())
+
+    def delete(self, project_id: int, comment: Comment | int):
+        """Delete a comment
+
+        Args:
+            project_id (int): The id of the project
+            comment (Comment | int): The comment to delete
+        """
+        comment_id = comment if isinstance(comment, int) else comment.id
+        resource = f"projects/{project_id}/{self.resource_type}/{comment_id}"
+        self._client.delete(resource)
+
+    def bulk_delete(self, project_id: int, comments: List[int] | List[Comment]):
+        """Bulk delete comments
+
+        Args:
+            project_id (int): The id of the project
+            comments (List[int] | List[Comment]): The list of comment ids to delete
+        """
+        ids = [comment if isinstance(comment, int) else comment.id for comment in comments]
+        self._client.delete(f"projects/{project_id}/{self.resource_type}", json={"ids": ids})
diff --git a/doccano_client/repositories/data_download.py b/doccano_client/repositories/data_download.py
new file mode 100644
index 0000000..c4b20da
--- /dev/null
+++ b/doccano_client/repositories/data_download.py
@@ -0,0 +1,89 @@
+from __future__ import annotations
+
+import pathlib
+from typing import List
+
+from doccano_client.models.data_download import Option
+from doccano_client.repositories.base import BaseRepository
+
+
+class DataDownloadRepository:
+    """Repository for interacting with the Doccano data download API"""
+
+    def __init__(self, client: BaseRepository):
+        self._client = client
+
+    def list_options(self, project_id: int) -> List[Option]:
+        """Return all download options
+
+        Args:
+            project_id (int): The id of the project
+
+        Returns:
+            List[Option]: The list of the download options.
+        """
+        resource = f"projects/{project_id}/download-format"
+        response = self._client.get(resource)
+        options = [Option.parse_obj(label) for label in response.json()]
+        return options
+
+    def find_option_by_name(self, project_id: int, name: str) -> Option:
+        """Find a download option by name
+
+        Args:
+            project_id (int): The id of the project
+            name (str): The name of the download option to find
+
+        Returns:
+            Option: The found download option
+
+        Raises:
+            ValueError: If the download option is not found
+        """
+        options = self.list_options(project_id)
+        for option in options:
+            if option.name == name:
+                return option
+        raise ValueError(f"Download option '{name}' not found")
+
+    def schedule_download(self, project_id: int, option: Option, only_approved=False) -> str:
+        """Schedule a download
+
+        Args:
+            project_id (int): The id of the project
+            option (Option): The download option
+            only_approved (bool): Whether to download approved data only
+
+        Returns:
+            str: The celery task id
+        """
+        resource = f"projects/{project_id}/download"
+        data = {"format": option.name, "exportApproved": only_approved}
+        response = self._client.post(resource, json=data)
+        task_id = response.json()["task_id"]
+        return task_id
+
+    def download(self, project_id: int, task_id: str, dir_name=".") -> pathlib.Path:
+        """Download a file from the server
+
+        Args:
+            project_id (int): The id of the project
+            task_id (str): The celery task id
+            dir_name (str): The directory to save the file
+
+        Returns:
+            pathlib.Path: The path to the downloaded file
+        """
+        resource = f"projects/{project_id}/download"
+        params = {"taskId": task_id}
+        response = self._client.get(resource, params=params, stream=True)
+        content_disposition = response.headers["Content-Disposition"]
+        ATTRIBUTE = "filename="
+        file_name = content_disposition[content_disposition.find(ATTRIBUTE) + len(ATTRIBUTE) + 1 : -1]
+        dir_path = pathlib.Path(dir_name)
+        dir_path.mkdir(parents=True, exist_ok=True)
+        file_path = dir_path / file_name
+        with file_path.open("wb") as f:
+            for chunk in response.iter_content(chunk_size=8192):
+                f.write(chunk)
+        return file_path
diff --git a/doccano_client/repositories/data_upload.py b/doccano_client/repositories/data_upload.py
new file mode 100644
index 0000000..01d4553
--- /dev/null
+++ b/doccano_client/repositories/data_upload.py
@@ -0,0 +1,75 @@
+from __future__ import annotations
+
+import pathlib
+from typing import List
+
+from requests_toolbelt import MultipartEncoder
+
+from doccano_client.models.data_upload import AvailableTask, Option
+from doccano_client.repositories.base import BaseRepository
+
+
+class DataUploadRepository:
+    """Repository for interacting with the Doccano data upload API"""
+
+    def __init__(self, client: BaseRepository):
+        self._client = client
+
+    def list_options(self, project_id: int) -> List[Option]:
+        """Return all upload options
+
+        Args:
+            project_id (int): The id of the project
+
+        Returns:
+            List[Option]: The list of the upload options.
+        """
+        resource = f"projects/{project_id}/catalog"
+        response = self._client.get(resource)
+        options = [Option.parse_obj(label) for label in response.json()]
+        return options
+
+    def upload(self, file_path: str) -> str:
+        """Upload a file to the server
+
+        Args:
+            file_path (str): The path to the file to upload
+
+        Returns:
+            str: The id of the uploaded file
+        """
+        resource = "fp/process/"
+        path = pathlib.Path(file_path)
+        with path.open("rb") as f:
+            m = MultipartEncoder(fields={"filepond": (path.name, f)})
+            headers = {"Content-Type": m.content_type, "Accept": "*/*"}
+            response = self._client.post(resource, data=m, headers=headers)
+            return response.content.decode()
+
+    def delete(self, upload_id: str):
+        """Delete the uploaded file from the server
+
+        Args:
+            upload_id (str): The id of the uploaded file
+        """
+        resource = "fp/revert/"
+        headers = {"Content-Type": "text/plain", "Accept": "*/*"}
+        self._client.delete(resource, data=upload_id, headers=headers)
+
+    def ingest(self, project_id: int, upload_ids: List[str], task: AvailableTask, format: str, **kwargs) -> str:
+        """Ingest the uploaded files into the project
+
+        Args:
+            project_id (int): The id of the project
+            upload_ids (List[str]): The ids of the uploaded files
+            task (AvailableTask): The project's task name
+            format (str): The format of the uploaded files
+            **kwargs: Additional keyword arguments like column_data and column_label
+
+        Returns:
+            str: The celery task id
+        """
+        resource = f"projects/{project_id}/upload"
+        data = {"uploadIds": upload_ids, "task": task, "format": format, **kwargs}
+        response = self._client.post(resource, json=data)
+        return response.json()["task_id"]
diff --git a/doccano_client/repositories/example.py b/doccano_client/repositories/example.py
new file mode 100644
index 0000000..e77437d
--- /dev/null
+++ b/doccano_client/repositories/example.py
@@ -0,0 +1,114 @@
+from __future__ import annotations
+
+from typing import Iterator, List
+
+from doccano_client.models.example import Example
+from doccano_client.repositories.base import BaseRepository
+
+
+class ExampleRepository:
+    """Repository for interacting with the Doccano example API"""
+
+    def __init__(self, client: BaseRepository):
+        self._client = client
+
+    def find_by_id(self, project_id: int, example_id: int) -> Example:
+        """Find a example by id
+
+        Args:
+            project_id (int): The id of the project
+            example_id (int): The id of the example to find
+
+        Returns:
+            Example: The found example
+        """
+        response = self._client.get(f"projects/{project_id}/examples/{example_id}")
+        return Example.parse_obj(response.json())
+
+    def list(self, project_id: int) -> Iterator[Example]:
+        """Return all examples in which you are a member
+
+        Args:
+            project_id (int): The id of the project
+
+        Yields:
+            Example: The next example.
+        """
+        response = self._client.get(f"projects/{project_id}/examples")
+
+        while True:
+            examples = response.json()
+            for example in examples["results"]:
+                yield Example.parse_obj(example)
+
+            if examples["next"] is None:
+                break
+            else:
+                response = self._client.get(examples["next"])
+
+    def create(self, project_id: int, example: Example) -> Example:
+        """Create a new example
+
+        Args:
+            project_id (int): The id of the project
+            example (Example): The example to create
+
+        Returns:
+            Example: The created example
+        """
+        response = self._client.post(f"projects/{project_id}/examples", json=example.dict(exclude={"id"}))
+        return Example.parse_obj(response.json())
+
+    def update(self, project_id: int, example: Example) -> Example:
+        """Update a example
+
+        Args:
+            project_id (int): The id of the project
+            example (Example): The example to update
+
+        Returns:
+            Example: The updated example
+        """
+        resource = f"projects/{project_id}/examples/{example.id}"
+        response = self._client.put(resource, json=example.dict())
+        return Example.parse_obj(response.json())
+
+    def delete(self, project_id: int, example: Example | int):
+        """Delete a example
+
+        Args:
+            project_id (int): The id of the project
+            example (Example | int): The example to delete
+        """
+        resource = f"projects/{project_id}/examples/{example if isinstance(example, int) else example.id}"
+        self._client.delete(resource)
+
+    def delete_all(self, project_id: int):
+        """Delete all examples
+
+        Args:
+            project_id (int): The id of the project
+        """
+        examples: List[int] = []
+        self.bulk_delete(project_id, examples)
+
+    def bulk_delete(self, project_id: int, examples: List[int] | List[Example]):
+        """Bulk delete examples
+
+        Args:
+            project_id (int): The id of the project
+            examples (List[int] | List[Example]): The list of example ids to delete
+        """
+        ids = [example if isinstance(example, int) else example.id for example in examples]
+        self._client.delete(f"projects/{project_id}/examples", json={"ids": ids})
+
+    def update_state(self, project_id: int, example: Example | int):
+        """Update completed state of example
+
+        Args:
+            project_id (int): The id of the project
+            example (Example | int): The example to confirm
+        """
+        example_id = example if isinstance(example, int) else example.id
+        resource = f"projects/{project_id}/examples/{example_id}/states"
+        self._client.post(resource)
diff --git a/doccano_client/repositories/label.py b/doccano_client/repositories/label.py
new file mode 100644
index 0000000..4552f07
--- /dev/null
+++ b/doccano_client/repositories/label.py
@@ -0,0 +1,122 @@
+from __future__ import annotations
+
+import functools
+from typing import Generic, List, TypeVar
+
+from doccano_client.models.label import (
+    BoundingBox,
+    Category,
+    Label,
+    Relation,
+    Segment,
+    Span,
+    Text,
+)
+from doccano_client.repositories.base import BaseRepository
+
+T = TypeVar("T", bound=Label)
+
+
+class LabelRepository(Generic[T]):
+    """Repository for interacting with the Doccano label API"""
+
+    def __init__(self, client: BaseRepository, label_class: T, resource_type: str):
+        self._client = client
+        self._label_class = label_class
+        self._resource_type = resource_type
+
+    def find_by_id(self, project_id: int, example_id: int, label_id: int) -> T:
+        """Find a label by id
+
+        Args:
+            project_id (int): The id of the project
+            example_id (int): The id of the example
+            label_id (int): The id of the label to find
+
+        Returns:
+            T: The found label
+        """
+        resource = f"projects/{project_id}/examples/{example_id}/{self._resource_type}/{label_id}"
+        response = self._client.get(resource)
+        return self._label_class.parse_obj(response.json())
+
+    def list(self, project_id: int, example_id: int) -> List[T]:
+        """Return all label in which you are a member
+
+        Args:
+            project_id (int): The id of the project
+            example_id (int): The id of the example
+
+        Returns:
+            T: The list of the label.
+        """
+        resource = f"projects/{project_id}/examples/{example_id}/{self._resource_type}"
+        response = self._client.get(resource)
+        labels = [self._label_class.parse_obj(label) for label in response.json()]
+        return labels
+
+    def create(self, project_id: int, label: T) -> T:
+        """Create a new label
+
+        Args:
+            project_id (int): The id of the project
+            label (T): The label to create
+
+        Returns:
+            T: The created label
+        """
+        resource = f"projects/{project_id}/examples/{label.example}/{self._resource_type}"
+        response = self._client.post(resource, json=label.dict(exclude={"id"}))
+        return self._label_class.parse_obj(response.json())
+
+    def update(self, project_id: int, label: T) -> T:
+        """Update a label
+
+        Args:
+            project_id (int): The id of the project
+            label (T): The label to update
+
+        Returns:
+            T: The updated label
+
+        Raises:
+            ValueError: If the label id is not set
+        """
+        if label.id is None:
+            raise ValueError("Label id is required")
+        resource = f"projects/{project_id}/examples/{label.example}/{self._resource_type}/{label.id}"
+        response = self._client.put(resource, json=label.dict())
+        return self._label_class.parse_obj(response.json())
+
+    def delete(self, project_id: int, label: T):
+        """Delete a label
+
+        Args:
+            project_id (int): The id of the project
+            label (T): The label to delete
+
+        Raises:
+            ValueError: If the label id is not set
+        """
+        if label.id is None:
+            raise ValueError("Label id is required")
+        resource = f"projects/{project_id}/examples/{label.example}/{self._resource_type}/{label.id}"
+        self._client.delete(resource)
+
+    def delete_all(self, project_id: int, example_id: int):
+        """Delete all labels
+
+        Args:
+            project_id (int): The id of the project
+            example_id (int): The id of the example
+        """
+        resource = f"projects/{project_id}/examples/{example_id}/{self._resource_type}"
+        self._client.delete(resource)
+
+
+CategoryRepository = functools.partial(LabelRepository[Category], label_class=Category, resource_type="categories")
+SpanRepository = functools.partial(LabelRepository[Span], label_class=Span, resource_type="spans")
+RelationRepository = functools.partial(LabelRepository[Relation], label_class=Relation, resource_type="relations")
+SegmentRepository = functools.partial(LabelRepository[Segment], label_class=Segment, resource_type="segments")
+TextRepository = functools.partial(LabelRepository[Text], label_class=Text, resource_type="texts")
+BoundingBoxRepository = functools.partial(LabelRepository[BoundingBox], label_class=BoundingBox, resource_type="bboxes")
diff --git a/doccano_client/repositories/label_type.py b/doccano_client/repositories/label_type.py
new file mode 100644
index 0000000..972d47e
--- /dev/null
+++ b/doccano_client/repositories/label_type.py
@@ -0,0 +1,142 @@
+from __future__ import annotations
+
+import functools
+import pathlib
+from typing import List
+
+from requests_toolbelt import MultipartEncoder
+
+from doccano_client.models.label_type import LabelType
+from doccano_client.repositories.base import BaseRepository
+
+
+class LabelTypeRepository:
+    """Repository for interacting with the Doccano label type API"""
+
+    def __init__(self, client: BaseRepository, resource_type="label-type"):
+        self._client = client
+        self._resource_type = resource_type
+
+    def find_by_name(self, project_id: int, name: str) -> LabelType:
+        """Find a label type by name
+
+        Args:
+            project_id (int): The id of the project
+            name (str): The name of the label type to find
+
+        Returns:
+            LabelType: The found label type
+
+        Raises:
+            ValueError: If the label type is not found
+        """
+        label_types = self.list(project_id)
+        for label_type in label_types:
+            if label_type.text == name:
+                return label_type
+        raise ValueError(f"Label type with name {name} not found")
+
+    def find_by_id(self, project_id: int, label_type_id: int) -> LabelType:
+        """Find a label type by id
+
+        Args:
+            project_id (int): The id of the project
+            label_type_id (int): The id of the label type to find
+
+        Returns:
+            LabelType: The found label type
+        """
+        response = self._client.get(f"projects/{project_id}/{self._resource_type}s/{label_type_id}")
+        return LabelType.parse_obj(response.json())
+
+    def list(self, project_id: int) -> List[LabelType]:
+        """Return all label types in which you are a member
+
+        Args:
+            project_id (int): The id of the project
+
+        Returns:
+            LabelType: The list of the label types.
+        """
+        response = self._client.get(f"projects/{project_id}/{self._resource_type}s")
+        label_types = [LabelType.parse_obj(label_type) for label_type in response.json()]
+        return label_types
+
+    def create(self, project_id: int, label_type: LabelType) -> LabelType:
+        """Create a new label type
+
+        Args:
+            project_id (int): The id of the project
+            label_type (LabelType): The label type to create
+
+        Returns:
+            LabelType: The created label type
+        """
+        response = self._client.post(
+            f"projects/{project_id}/{self._resource_type}s", json=label_type.dict(exclude={"id"})
+        )
+        return LabelType.parse_obj(response.json())
+
+    def update(self, project_id: int, label_type: LabelType) -> LabelType:
+        """Update a label type
+
+        Args:
+            project_id (int): The id of the project
+            label_type (LabelType): The label type to update
+
+        Returns:
+            LabelType: The updated label type
+
+        Raises:
+            ValueError: If the label_type id is not set
+        """
+        if label_type.id is None:
+            raise ValueError("label_type id must be set")
+        resource = f"projects/{project_id}/{self._resource_type}s/{label_type.id}"
+        response = self._client.put(resource, json=label_type.dict())
+        return LabelType.parse_obj(response.json())
+
+    def delete(self, project_id: int, label_type: LabelType | int):
+        """Delete a label type
+
+        Args:
+            project_id (int): The id of the project
+            label_type (LabelType | int): The label type to delete
+
+        Raises:
+            ValueError: If the label_type id is not set
+        """
+        label_type_id = label_type if isinstance(label_type, int) else label_type.id
+        if label_type_id is None:
+            raise ValueError("label_type id must be set")
+        resource = f"projects/{project_id}/{self._resource_type}s/{label_type_id}"
+        self._client.delete(resource)
+
+    def bulk_delete(self, project_id: int, label_types: List[int] | List[LabelType]):
+        """Bulk delete label types
+
+        Args:
+            project_id (int): The id of the project
+            label_types (List[int | LabelType]): The list of label type ids to delete
+        """
+        ids = [label_type if isinstance(label_type, int) else label_type.id for label_type in label_types]
+        self._client.delete(f"projects/{project_id}/{self._resource_type}s", json={"ids": ids})
+
+    def upload(self, project_id: int, file_path: str):
+        """Upload a label type
+
+        Args:
+            project_id (int): The id of the project
+            file_path (str): The path to the file to upload
+        """
+        resource = f"projects/{project_id}/{self._resource_type}-upload"
+        path = pathlib.Path(file_path)
+        with path.open("rb") as f:
+            m = MultipartEncoder(fields={"file": (path.name, f, "application/json")})
+            headers = {"Content-Type": m.content_type}
+            self._client.post(resource, data=m, headers=headers)
+
+
+CategoryTypeRepository = functools.partial(LabelTypeRepository, resource_type="category-type")
+SpanTypeRepository = functools.partial(LabelTypeRepository, resource_type="span-type")
+RelationTypeRepository = functools.partial(LabelTypeRepository, resource_type="relation-type")
diff --git a/doccano_client/repositories/member.py b/doccano_client/repositories/member.py
new file mode 100644
index 0000000..68e1340
--- /dev/null
+++ b/doccano_client/repositories/member.py
@@ -0,0 +1,99 @@
+from __future__ import annotations
+
+from typing import List
+
+from doccano_client.models.member import Member
+from doccano_client.repositories.base import BaseRepository
+
+
+class MemberRepository:
+    """Repository for interacting with the Doccano member API"""
+
+    def __init__(self, client: BaseRepository):
+        self._client = client
+
+    def find_by_id(self, project_id: int, member_id: int) -> Member:
+        """Find a member by id
+
+        Args:
+            project_id (int): The id of the project
+            member_id (int): The id of the member to find
+
+        Returns:
+            Member: The found member
+        """
+        resource = f"projects/{project_id}/members/{member_id}"
+        response = self._client.get(resource)
+        return Member.parse_obj(response.json())
+
+    def list(self, project_id: int) -> List[Member]:
+        """Return all member in which you are a member
+
+        Args:
+            project_id (int): The id of the project
+
+        Returns:
+            Member: The list of the member.
+        """
+        resource = f"projects/{project_id}/members"
+        response = self._client.get(resource)
+        members = [Member.parse_obj(member) for member in response.json()]
+        return members
+
+    def create(self, project_id: int, member: Member) -> Member:
+        """Create a new member
+
+        Args:
+            project_id (int): The id of the project
+            member (Member): The member to create
+
+        Returns:
+            Member: The created member
+        """
+        resource = f"projects/{project_id}/members"
+        response = self._client.post(resource, json=member.dict(exclude={"id", "username", "rolename"}))
+        return Member.parse_obj(response.json())
+
+    def update(self, project_id: int, member: Member) -> Member:
+        """Update a member
+
+        Args:
+            project_id (int): The id of the project
+            member (Member): The member to update
+
+        Returns:
+            Member: The updated member
+
+        Raises:
+            ValueError: If the member id is not set
+        """
+        if member.id is None:
+            raise ValueError("Member id is required")
+        resource = f"projects/{project_id}/members/{member.id}"
+        response = self._client.put(resource, json=member.dict(exclude={"username", "rolename"}))
+        return Member.parse_obj(response.json())
+
+    def delete(self, project_id: int, member: Member | int):
+        """Delete a member
+
+        Args:
+            project_id (int): The id of the project
+            member (Member | int): The member to delete
+
+        Raises:
+            ValueError: If the member id is not set
+        """
+        if isinstance(member, Member) and member.id is None:
+            raise ValueError("Member id is required")
+        member_id = member if isinstance(member, int) else member.id
+        self.bulk_delete(project_id, [member_id])  # type: ignore
+
+    def bulk_delete(self, project_id: int, members: List[int] | List[Member]):
+        """Bulk delete members
+
+        Args:
+            project_id (int): The id of the project
+            members (List[int] | List[Member]): The list of member ids to delete
+        """
+        ids = [member if isinstance(member, int) else member.id for member in members]
+        self._client.delete(f"projects/{project_id}/members", json={"ids": ids})
diff --git a/doccano_client/repositories/metrics.py b/doccano_client/repositories/metrics.py
new file mode 100644
index 0000000..e5811c8
--- /dev/null
+++ b/doccano_client/repositories/metrics.py
@@ -0,0 +1,102 @@
+from __future__ import annotations
+
+from typing import Iterator, List
+
+from doccano_client.models.metrics import (
+    LabelCount,
+    LabelDistribution,
+    MemberProgress,
+    Progress,
+)
+from doccano_client.repositories.base import BaseRepository
+
+
+class MetricsRepository:
+    """Repository for interacting with the Doccano metrics API"""
+
+    def __init__(self, client: BaseRepository):
+        self._client = client
+
+    def get_progress(self, project_id: int) -> Progress:
+        """Get my progress
+
+        Args:
+            project_id (int): The id of the project
+
+        Returns:
+            Progress: Your progress
+        """
+        resource = f"projects/{project_id}/metrics/progress"
+        response = self._client.get(resource).json()
+        return Progress(total=response["total"], completed=response["complete"], remaining=response["remaining"])
+
+    def get_members_progress(self, project_id: int) -> List[MemberProgress]:
+        """Return all metricss in which you are a member
+
+        Args:
+            project_id (int): The id of the project
+
+        Returns:
+            List[MemberProgress]: The list of the member progress.
+        """
+        resource = f"projects/{project_id}/metrics/member-progress"
+        response = self._client.get(resource).json()
+        return [
+            MemberProgress(
+                username=progress["user"],
+                progress=Progress(
+                    total=response["total"], completed=progress["done"], remaining=response["total"] - progress["done"]
+                ),
+            )
+            for progress in response["progress"]
+        ]
+
+    def _get_label_distribution(self, resource: str) -> Iterator[LabelDistribution]:
+        """Yield label distribution
+
+        Args:
+            resource (str): The resource to get
+
+        Yields:
+            LabelDistribution: The category distribution.
+        """
+        response = self._client.get(resource).json()
+        for username, counts in response.items():
+            label_counts = [LabelCount(label=label, count=count) for label, count in counts.items()]
+            yield LabelDistribution(username=username, counts=label_counts)
+
+    def get_category_distribution(self, project_id: int) -> List[LabelDistribution]:
+        """Return category distribution
+
+        Args:
+            project_id (int): The id of the project
+
+        Returns:
+            LabelDistribution: The category distribution.
+        """
+        resource = f"projects/{project_id}/metrics/category-distribution"
+        return list(self._get_label_distribution(resource))
+
+    def get_span_distribution(self, project_id: int) -> List[LabelDistribution]:
+        """Return span distribution
+
+        Args:
+            project_id (int): The id of the project
+
+        Returns:
+            LabelDistribution: The span distribution.
+        """
+        resource = f"projects/{project_id}/metrics/span-distribution"
+        return list(self._get_label_distribution(resource))
+
+    def get_relation_distribution(self, project_id: int) -> List[LabelDistribution]:
+        """Return relation distribution
+
+        Args:
+            project_id (int): The id of the project
+
+        Returns:
+            LabelDistribution: The relation distribution.
+        """
+        resource = f"projects/{project_id}/metrics/relation-distribution"
+        return list(self._get_label_distribution(resource))
diff --git a/doccano_client/repositories/project.py b/doccano_client/repositories/project.py
new file mode 100644
index 0000000..e1bcf20
--- /dev/null
+++ b/doccano_client/repositories/project.py
@@ -0,0 +1,77 @@
+from __future__ import annotations
+
+from typing import Iterator
+
+from doccano_client.models.project import Project
+from doccano_client.repositories.base import BaseRepository
+
+
+class ProjectRepository:
+    """Repository for interacting with the Doccano project API"""
+
+    def __init__(self, client: BaseRepository):
+        self._client = client
+
+    def find_by_id(self, project_id: int) -> Project:
+        """Find a project by id
+
+        Args:
+            project_id (int): The id of the project to find
+
+        Returns:
+            Project: The found project
+        """
+        response = self._client.get(f"projects/{project_id}")
+        return Project.parse_obj(response.json())
+
+    def list(self) -> Iterator[Project]:
+        """Return all projects in which you are a member
+
+        Yields:
+            Project: The next project.
+        """
+        response = self._client.get("projects")
+
+        while True:
+            projects = response.json()
+            for project in projects["results"]:
+                yield Project.parse_obj(project)
+
+            if projects["next"] is None:
+                break
+            else:
+                response = self._client.get(projects["next"])
+
+    def create(self, project: Project) -> Project:
+        """Create a new project
+
+        Args:
+            project (Project): The project to create
+
+        Returns:
+            Project: The created project
+        """
+        response = self._client.post("projects", json=project.dict(exclude={"id"}))
+        return Project.parse_obj(response.json())
+
+    def update(self, project: Project) -> Project:
+        """Update a project
+
+        Args:
+            project (Project): The project to update
+
+        Returns:
+            Project: The updated project
+        """
+        resource = f"projects/{project.id}"
+        response = self._client.put(resource, json=project.dict())
+        return Project.parse_obj(response.json())
+
+    def delete(self, project: Project | int):
+        """Delete a project
+
+        Args:
+            project (Project | int): The project to delete
+        """
+        resource = f"projects/{project if isinstance(project, int) else project.id}"
+        self._client.delete(resource)
diff --git a/doccano_client/repositories/role.py b/doccano_client/repositories/role.py
new file mode 100644
index 0000000..79c5f90
--- /dev/null
+++ b/doccano_client/repositories/role.py
@@ -0,0 +1,41 @@
+from __future__ import annotations
+
+from typing import List
+
+from doccano_client.models.role import Role
+from doccano_client.repositories.base import BaseRepository
+
+
+class RoleRepository:
+    """Repository for interacting with the Doccano role API"""
+
+    def __init__(self, client: BaseRepository):
+        self._client = client
+
+    def list(self) -> List[Role]:
+        """Return all roles
+
+        Returns:
+            Role: The list of the roles.
+        """
+        response = self._client.get("roles")
+        roles = [Role.parse_obj(role) for role in response.json()]
+        return roles
+
+    def find_by_name(self, name: str) -> Role:
+        """Find a role by name
+
+        Args:
+            name (str): The name of the role to find
+
+        Returns:
+            Role: The found role
+
+        Raises:
+            ValueError: If the role is not found
+        """
+        roles = self.list()
+        for role in roles:
+            if role.name == name:
+                return role
+        raise ValueError(f"Role '{name}' not found")
diff --git a/doccano_client/repositories/task_status.py b/doccano_client/repositories/task_status.py
new file mode 100644
index 0000000..0b8ab59
--- /dev/null
+++ b/doccano_client/repositories/task_status.py
@@ -0,0 +1,45 @@
+from __future__ import annotations
+
+import time
+
+from doccano_client.models.task_status import TaskStatus
+from doccano_client.repositories.base import BaseRepository
+
+
+class TaskStatusRepository:
+    """Repository for interacting with the Doccano task status API"""
+
+    def __init__(self, client: BaseRepository):
+        self._client = client
+
+    def get(self, task_id: str) -> TaskStatus:
+        """Return the specified task_status
+
+        Args:
+            task_id (str): The celery task id
+
+        Returns:
+            TaskStatus: The task_status.
+        """
+        response = self._client.get(f"tasks/status/{task_id}")
+        return TaskStatus.parse_obj(response.json())
+
+    def wait(self, task_id: str, timeout: int = 3600) -> TaskStatus:
+        """Wait for the specified task id
+
+        Args:
+            task_id (str): The celery task id
+            timeout (int): The timeout in seconds
+
+        Returns:
+            TaskStatus: The task_status.
+
+        Raises:
+            TimeoutError: If the task does not complete within the timeout
+        """
+        for _ in range(timeout):
+            status = self.get(task_id)
+            if status.ready:
+                return status
+            time.sleep(1)
+        raise TimeoutError(f"Timeout waiting for task {task_id}")
diff --git a/doccano_client/repositories/user.py b/doccano_client/repositories/user.py
new file mode 100644
index 0000000..7464345
--- /dev/null
+++ b/doccano_client/repositories/user.py
@@ -0,0 +1,53 @@
+from __future__ import annotations
+
+from typing import List
+
+from doccano_client.models.user import User
+from doccano_client.repositories.base import BaseRepository
+
+
+class UserRepository:
+    """Repository for interacting with the Doccano user API"""
+
+    def __init__(self, client: BaseRepository):
+        self._client = client
+
+    def get_profile(self) -> User:
+        """Get a profile
+
+        Returns:
+            User: The user.
+        """
+        response = self._client.get("me")
+        return User.parse_obj(response.json())
+
+    def list(self, name: str = "") -> List[User]:
+        """Return users
+
+        Args:
+            name (str): The name of the user to search for
+
+        Returns:
+            User: The list of the users.
+        """
+        response = self._client.get(f"users?q={name}")
+        users = [User.parse_obj(user) for user in response.json()]
+        return users
+
+    def find_by_name(self, name: str) -> User:
+        """Find a user by name
+
+        Args:
+            name (str): The name of the user to find
+
+        Returns:
+            User: The found user
+
+        Raises:
+            ValueError: If the user is not found
+        """
+        users = self.list(name)
+        for user in users:
+            if user.username == name:
+                return user
+        raise ValueError(f"User '{name}' not found")
diff --git a/doccano_client/services/label_type.py b/doccano_client/services/label_type.py
new file mode 100644
index 0000000..b489722
--- /dev/null
+++ b/doccano_client/services/label_type.py
@@ -0,0 +1,28 @@
+from doccano_client.models.label_type import LabelType
+from doccano_client.repositories.label_type import LabelTypeRepository
+
+
+class LabelTypeService:
+    def __init__(self, repository: LabelTypeRepository):
+        self._repository = repository
+
+    def exists(self, project_id: int, label_type: LabelType) -> bool:
+        """Check if the label type exists
+
+        Args:
+            project_id (int): The id of the project
+            label_type (LabelType): The label type to check
+
+        Returns:
+            bool: True if the label type exists, False otherwise
+        """
+        label_types = self._repository.list(project_id)
+        for label_type_ in label_types:
+            if label_type_.text == label_type.text and label_type_.id != label_type.id:
+                return True
+            if label_type.prefix_key or label_type.suffix_key:
+                if label_type_.id == label_type.id:
+                    continue
+                if label_type_.prefix_key == label_type.prefix_key and label_type_.suffix_key == label_type.suffix_key:
+                    return True
+        return False
diff --git a/doccano_client/usecase/__init__.py b/doccano_client/usecase/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/doccano_client/usecase/comment.py b/doccano_client/usecase/comment.py
new file mode 100644
index 0000000..8734325
--- /dev/null
+++ b/doccano_client/usecase/comment.py
@@ -0,0 +1,91 @@
+from typing import Iterator, List
+
+from doccano_client.models.comment import Comment
+from doccano_client.repositories.comment import CommentRepository
+
+
+class CommentUseCase:
+    def __init__(self, repository: CommentRepository):
+        self._repository = repository
+
+    def find_by_id(self, project_id: int, comment_id: int) -> Comment:
+        """Find a comment by id
+
+        Args:
+            project_id (int): The id of the project to find
+            comment_id (int): The id of the comment to find
+
+        Returns:
+            Comment: The found comment
+        """
+        return self._repository.find_by_id(project_id, comment_id)
+
+    def list(self, project_id: int, example_id: int, query: str = "") -> Iterator[Comment]:
+        """Return all comments
+
+        Args:
+            project_id (int): The id of the project
+            example_id (int): The id of the example
+            query (str): The query string to filter comments
+
+        Yields:
+            Comment: The comments in the project.
+        """
+        yield from self._repository.list(project_id, example_id, query)
+
+    def create(
+        self,
+        project_id: int,
+        example_id: int,
+        text: str,
+    ) -> Comment:
+        """Create a new comment
+
+        Args:
+            project_id (int): The id of the project
+            example_id (int): The id of the example
+            text (str): The text of the comment
+
+        Returns:
+            Comment: The created comment
+        """
+        comment = Comment(text=text, example=example_id)
+        return self._repository.create(project_id, comment)
+
+    def update(
+        self,
+        project_id: int,
+        comment_id: int,
+        text: str,
+    ) -> Comment:
+        """Update a comment
+
+        Args:
+            project_id (int): The id of the project
+            comment_id (int): The id of the comment
+            text (str): The text of the comment
+
+        Returns:
+            Comment: The updated comment
+        """
+        comment = self.find_by_id(project_id, comment_id)
+        comment = Comment(id=comment.id, text=text, example=comment.example)
+        return self._repository.update(project_id, comment)
+
+    def delete(self, project_id: int, comment_id: int):
+        """Delete a comment.
+
+        Args:
+            project_id (int): The project id.
+            comment_id (int): The comment id.
+        """
+        self._repository.delete(project_id, comment_id)
+
+    def bulk_delete(self, project_id: int, comment_ids: List[int]):
+        """Bulk delete comments
+
+        Args:
+            project_id (int): The id of the project
+            comment_ids (List[int]): The list of comment ids to delete
+        """
+        self._repository.bulk_delete(project_id, comment_ids)
diff --git a/doccano_client/usecase/data_download.py b/doccano_client/usecase/data_download.py
new file mode 100644
index 0000000..4fe7bb0
--- /dev/null
+++ b/doccano_client/usecase/data_download.py
@@ -0,0 +1,41 @@
+import pathlib
+from typing import List
+
+from doccano_client.models.data_download import Option
+from doccano_client.repositories.data_download import DataDownloadRepository
+from doccano_client.repositories.task_status import TaskStatusRepository
+
+
+class DataDownloadUseCase:
+    def __init__(self, data_download_repository: DataDownloadRepository, task_status_repository: TaskStatusRepository):
+        self._data_download_repository = data_download_repository
+        self._task_status_repository = task_status_repository
+
+    def list_options(self, project_id: int) -> List[Option]:
+        """Return all download options
+
+        Args:
+            project_id (int): The id of the project
+
+        Returns:
+            List[Option]: The list of the download options.
+        """
+        return self._data_download_repository.list_options(project_id)
+
+    def download(self, project_id: int, format: str, only_approved=False, dir_name=".") -> pathlib.Path:
+        """Download a file
+
+        Args:
+            project_id (int): The id of the project
+            format (str): The format of the download
+            only_approved (bool): Whether to download approved data only
+            dir_name (str): The directory to save the file
+
+        Returns:
+            pathlib.Path: The path to the downloaded file
+        """
+        option = self._data_download_repository.find_option_by_name(project_id, format)
+        task_id = self._data_download_repository.schedule_download(project_id, option, only_approved)
+        self._task_status_repository.wait(task_id)
+        file_path = self._data_download_repository.download(project_id, task_id, dir_name)
+        return file_path
diff --git a/doccano_client/usecase/data_upload.py b/doccano_client/usecase/data_upload.py
new file mode 100644
index 0000000..391b54d
--- /dev/null
+++ b/doccano_client/usecase/data_upload.py
@@ -0,0 +1,51 @@
+from typing import List
+
+from doccano_client.models.data_upload import AvailableTask, Option
+from doccano_client.models.task_status import TaskStatus
+from doccano_client.repositories.data_upload import DataUploadRepository
+from doccano_client.repositories.task_status import TaskStatusRepository
+
+
+class DataUploadUseCase:
+    def __init__(self, data_upload_repository: DataUploadRepository, task_status_repository: TaskStatusRepository):
+        self._data_upload_repository = data_upload_repository
+        self._task_status_repository = task_status_repository
+
+    def list_options(self, project_id: int) -> List[Option]:
+        """Return all upload options
+
+        Args:
+            project_id (int): The id of the project
+
+        Returns:
+            List[Option]: The list of the upload options.
+        """
+        return self._data_upload_repository.list_options(project_id)
+
+    def upload(
+        self,
+        project_id: int,
+        file_paths: List[str],
+        task: AvailableTask,
+        format: str,
+        column_data: str = "text",
+        column_label: str = "label",
+    ) -> TaskStatus:
+        """Upload a file
+
+        Args:
+            project_id (int): The id of the project
+            file_paths (List[str]): The list of the file paths
+            task (AvailableTask): The task of the upload
+            format (str): The format of the upload
+            column_data (str): The column name of the data
+            column_label (str): The column name of the label
+
+        Returns:
+            TaskStatus: The status of the upload task.
+        """
+        upload_ids = [self._data_upload_repository.upload(file_path) for file_path in file_paths]
+        task_id = self._data_upload_repository.ingest(
+            project_id, upload_ids, task, format, column_data=column_data, column_label=column_label
+        )
+        return self._task_status_repository.wait(task_id)
diff --git a/doccano_client/usecase/example.py b/doccano_client/usecase/example.py
new file mode 100644
index 0000000..bdc382e
--- /dev/null
+++ b/doccano_client/usecase/example.py
@@ -0,0 +1,114 @@
+from typing import Any, Dict, Iterator, List
+
+from doccano_client.models.example import Example
+from doccano_client.repositories.example import ExampleRepository
+
+
+class ExampleUseCase:
+    def __init__(self, repository: ExampleRepository):
+        self._repository = repository
+
+    def find_by_id(self, project_id: int, example_id: int) -> Example:
+        """Find a example by id
+
+        Args:
+            project_id (int): The id of the project to find
+            example_id (int): The id of the example to find
+
+        Returns:
+            Example: The found example
+        """
+        return self._repository.find_by_id(project_id, example_id)
+
+    def list(self, project_id: int) -> Iterator[Example]:
+        """Return all examples
+
+        Args:
+            project_id (int): The id of the project
+
+        Yields:
+            Example: The examples in the project.
+        """
+        yield from self._repository.list(project_id)
+
+    def create(
+        self,
+        project_id: int,
+        text: str,
+        meta: Dict[str, Any] = None,
+    ) -> Example:
+        """Create a new example
+
+        Args:
+            project_id (int): The id of the project
+            text (str): The text of the example
+            meta (Dict[str, Any]): The meta data of the example
+
+        Returns:
+            Example: The created example
+        """
+        if meta is None:
+            meta = {}
+        example = Example(text=text, meta=meta)
+        return self._repository.create(project_id, example)
+
+    def update(
+        self,
+        project_id: int,
+        example_id: int,
+        text: str = None,
+        meta: Dict[str, Any] = None,
+    ) -> Example:
+        """Update a example
+
+        Args:
+            project_id (int): The id of the project
+            example_id (int): The id of the example
+            text (str): The text of the example
+            meta (Dict[str, Any]): The meta data of the example
+
+        Returns:
+            Example: The updated example
+        """
+        example = self.find_by_id(project_id, example_id)
+        example = Example(
+            id=example_id,
+            text=text if text is not None else example.text,
+            meta=meta if meta is not None else example.meta,
+        )
+        return self._repository.update(project_id, example)
+
+    def delete(self, project_id: int, example_id: int):
+        """Delete a example.
+
+        Args:
+            project_id (int): The project id.
+            example_id (int): The example id.
+        """
+        self._repository.delete(project_id, example_id)
+
+    def bulk_delete(self, project_id: int, example_ids: List[int]):
+        """Bulk delete examples
+
+        Args:
+            project_id (int): The id of the project
+            example_ids (List[int]): The list of example ids to delete
+        """
+        self._repository.bulk_delete(project_id, example_ids)
+
+    def delete_all(self, project_id: int):
+        """Delete all examples
+
+        Args:
+            project_id (int): The id of the project
+        """
+        self._repository.delete_all(project_id)
+
+    def update_state(self, project_id: int, example_id: int):
+        """Update completed state of example
+
+        Args:
+            project_id (int): The id of the project
+            example_id (int): The example id
+        """
+        self._repository.update_state(project_id, example_id)
diff --git a/doccano_client/usecase/label.py b/doccano_client/usecase/label.py
new file mode 100644
index 0000000..7f67492
--- /dev/null
+++ b/doccano_client/usecase/label.py
@@ -0,0 +1,589 @@
+from __future__ import annotations
+
+from typing import Generic, List, Optional, TypeVar
+
+from doccano_client.models.label import (
+    BoundingBox,
+    Category,
+    Label,
+    Relation,
+    Segment,
+    Span,
+    Text,
+)
+from doccano_client.repositories.label import LabelRepository
+from doccano_client.repositories.label_type import LabelTypeRepository
+
+T = TypeVar("T", bound=Label)
+
+
+class LabelUseCase(Generic[T]):
+    def __init__(self, repository: LabelRepository, label_type_repository: LabelTypeRepository = None):
+        self._repository = repository
+        self._label_type_repository = label_type_repository
+
+    def find_by_id(self, project_id: int, example_id: int, label_id: int) -> T:
+        """Find a label by id
+
+        Args:
+            project_id (int): The id of the project to find
+            example_id (int): The id of the example
+            label_id (int): The id of the label to find
+
+        Returns:
+            T: The found label
+        """
+        return self._repository.find_by_id(project_id, example_id, label_id)
+
+    def list(self, project_id: int, example_id: int) -> List[T]:
+        """Return all labels
+
+        Args:
+            project_id (int): The id of the project
+            example_id (int): The id of the example
+
+        Returns:
+            T: The labels in the project.
+        """
+        return self._repository.list(project_id, example_id)
+
+    def delete(self, project_id: int, example_id: int, label_id: int):
+        """Delete a label.
+
+        Args:
+            project_id (int): The project id
+            example_id (int): The id of the example
+            label_id (int): The label id
+        """
+        label = self.find_by_id(project_id, example_id, label_id)
+        self._repository.delete(project_id, label)
+
+    def delete_all(self, project_id: int, example_id: int):
+        """Delete all labels
+
+        Args:
+            project_id (int): The id of the project
+            example_id (int): The id of the example
+        """
+        self._repository.delete_all(project_id, example_id)
+
+
+class CategoryUseCase(LabelUseCase[Category]):
+    def create(
+        self, project_id: int, example_id: int, label: int | str, human_annotated=False, confidence=0.0
+    ) -> Category:
+        """Create a new category label
+
+        Args:
+            project_id (int): The id of the project
+            example_id (int): The id of the example
+            label (int | str): The label to create
+            human_annotated (bool): Whether the label is human annotated. Defaults to False.
+            confidence (float): The confidence of the label. Defaults to 0.0.
+
+        Returns:
+            Category: The created category label
+
+        Raises:
+            ValueError: If the label type repository is not set
+        """
+        if self._label_type_repository is None:
+            raise ValueError("LabelTypeRepository is not set")
+
+        if isinstance(label, str):
+            label_type = self._label_type_repository.find_by_name(project_id, label)
+            label = label_type.id  # type: ignore
+
+        category = Category(example=example_id, label=label, manual=human_annotated, prob=confidence)
+        return self._repository.create(project_id, category)
+
+    def update(
+        self,
+        project_id: int,
+        example_id: int,
+        label_id: int,
+        label: Optional[int | str] = None,
+        human_annotated: bool = None,
+        confidence: float = None,
+    ) -> Category:
+        """Update a category label
+
+        Args:
+            project_id (int): The id of the project
+            example_id (int): The id of the example
+            label_id (int): The id of the label
+            label (int | str): The label to create
+            human_annotated (bool): Whether the label is human annotated. Defaults to False.
+            confidence (float): The confidence of the label. Defaults to 0.0.
+
+        Returns:
+            Category: The updated category label
+
+        Raises:
+            ValueError: If the label type repository is not set
+        """
+        category = self.find_by_id(project_id, example_id, label_id)
+
+        if self._label_type_repository is None:
+            raise ValueError("LabelTypeRepository is not set")
+
+        if isinstance(label, str):
+            label_type = self._label_type_repository.find_by_name(project_id, label)
+            label = label_type.id  # type: ignore
+
+        category = Category(
+            id=category.id,
+            example=example_id,
+            label=label or category.label,
+            manual=human_annotated or category.manual,
+            prob=confidence or category.prob,
+        )
+        return self._repository.update(project_id, category)
+
+
+class SpanUseCase(LabelUseCase[Span]):
+    def create(
+        self,
+        project_id: int,
+        example_id: int,
+        start_offset: int,
+        end_offset: int,
+        label: int | str,
+        human_annotated: bool = False,
+        confidence: float = 0.0,
+    ) -> Span:
+        """Create a new span label
+
+        Args:
+            project_id (int): The id of the project
+            example_id (int): The id of the example
+            start_offset (int): The start offset of the span
+            end_offset (int): The end offset of the span
+            label (int | str): The label to create
+            human_annotated (bool): Whether the label is human annotated. Defaults to False.
+            confidence (float): The confidence of the label. Defaults to 0.0.
+
+        Returns:
+            Span: The created span label
+
+        Raises:
+            ValueError: If the label type repository is not set
+        """
+        if self._label_type_repository is None:
+            raise ValueError("LabelTypeRepository is not set")
+
+        if isinstance(label, str):
+            label_type = self._label_type_repository.find_by_name(project_id, label)
+            label = label_type.id  # type: ignore
+
+        span = Span(
+            example=example_id,
+            start_offset=start_offset,
+            end_offset=end_offset,
+            label=label,
+            manual=human_annotated,
+            prob=confidence,
+        )
+        return self._repository.create(project_id, span)
+
+    def update(
+        self,
+        project_id: int,
+        example_id: int,
+        label_id: int,
+        start_offset: Optional[int] = None,
+        end_offset: Optional[int] = None,
+        label: Optional[int | str] = None,
+        human_annotated: bool = None,
+        confidence: float = None,
+    ) -> Span:
+        """Update a span label
+
+        Args:
+            project_id (int): The id of the project
+            example_id (int): The id of the example
+            label_id (int): The id of the label
+            start_offset (int): The start offset of the span
+            end_offset (int): The end offset of the span
+            label (int | str): The label to create
+            human_annotated (bool): Whether the label is human annotated. Defaults to False.
+            confidence (float): The confidence of the label. Defaults to 0.0.
+
+        Returns:
+            Span: The updated span label
+
+        Raises:
+            ValueError: If the label type repository is not set
+        """
+        span = self.find_by_id(project_id, example_id, label_id)
+
+        if self._label_type_repository is None:
+            raise ValueError("LabelTypeRepository is not set")
+
+        if isinstance(label, str):
+            label_type = self._label_type_repository.find_by_name(project_id, label)
+            label = label_type.id
+
+        span = Span(
+            id=span.id,
+            example=example_id,
+            start_offset=start_offset or span.start_offset,
+            end_offset=end_offset or span.end_offset,
+            label=label or span.label,
+            manual=human_annotated or span.manual,
+            prob=confidence or span.prob,
+        )
+        return self._repository.update(project_id, span)
+
+
+class RelationUseCase(LabelUseCase[Relation]):
+    def create(
+        self,
+        project_id: int,
+        example_id: int,
+        from_id: int,
+        to_id: int,
+        label: int | str,
+        human_annotated: bool = False,
+        confidence: float = 0.0,
+    ) -> Relation:
+        """Create a new relation label
+
+        Args:
+            project_id (int): The id of the project
+            example_id (int): The id of the example
+            from_id (int): The id of the from span
+            to_id (int): The id of the to span
+            label (int | str): The label to create
+            human_annotated (bool): Whether the label is human annotated. Defaults to False.
+            confidence (float): The confidence of the label. Defaults to 0.0.
+
+        Returns:
+            Relation: The created relation label
+
+        Raises:
+            ValueError: If the label type repository is not set
+        """
+        if self._label_type_repository is None:
+            raise ValueError("LabelTypeRepository is not set")
+
+        if isinstance(label, str):
+            label_type = self._label_type_repository.find_by_name(project_id, label)
+            label = label_type.id  # type: ignore
+
+        relation = Relation(
+            example=example_id,
+            from_id=from_id,
+            to_id=to_id,
+            type=label,
+            manual=human_annotated,
+            prob=confidence,
+        )
+        return self._repository.create(project_id, relation)
+
+    def update(
+        self,
+        project_id: int,
+        example_id: int,
+        label_id: int,
+        from_id: Optional[int] = None,
+        to_id: Optional[int] = None,
+        label: Optional[int | str] = None,
+        human_annotated: bool = None,
+        confidence: float = None,
+    ) -> Relation:
+        """Update a relation label
+
+        Args:
+            project_id (int): The id of the project
+            example_id (int): The id of the example
+            label_id (int): The id of the label
+            from_id (int): The id of the from span
+            to_id (int): The id of the to span
+            label (int | str): The label to create
+            human_annotated (bool): Whether the label is human annotated. Defaults to False.
+            confidence (float): The confidence of the label. Defaults to 0.0.
+
+        Returns:
+            Relation: The updated relation label
+
+        Raises:
+            ValueError: If the label type repository is not set
+        """
+        relation = self.find_by_id(project_id, example_id, label_id)
+
+        if self._label_type_repository is None:
+            raise ValueError("LabelTypeRepository is not set")
+
+        if isinstance(label, str):
+            label_type = self._label_type_repository.find_by_name(project_id, label)
+            label = label_type.id
+
+        relation = Relation(
+            id=relation.id,
+            example=example_id,
+            from_id=from_id or relation.from_id,
+            to_id=to_id or relation.to_id,
+            type=label or relation.type,
+            manual=human_annotated or relation.manual,
+            prob=confidence or relation.prob,
+        )
+        return self._repository.update(project_id, relation)
+
+
+class TextUseCase(LabelUseCase[Text]):
+    def create(
+        self,
+        project_id: int,
+        example_id: int,
+        text: str,
+        human_annotated: bool = False,
+        confidence: float = 0.0,
+    ) -> Text:
+        """Create a new text label
+
+        Args:
+            project_id (int): The id of the project
+            example_id (int): The id of the example
+            text (str): The text to create
+            human_annotated (bool): Whether the label is human annotated. Defaults to False.
+            confidence (float): The confidence of the label. Defaults to 0.0.
+
+        Returns:
+            Text: The created text label
+        """
+        text_label = Text(
+            example=example_id,
+            text=text,
+            manual=human_annotated,
+            prob=confidence,
+        )
+        return self._repository.create(project_id, text_label)
+
+    def update(
+        self,
+        project_id: int,
+        example_id: int,
+        label_id: int,
+        text: Optional[str] = None,
+        human_annotated: bool = None,
+        confidence: float = None,
+    ) -> Text:
+        """Update a text label
+
+        Args:
+            project_id (int): The id of the project
+            example_id (int): The id of the example
+            label_id (int): The id of the label
+            text (str): The text to update
+            human_annotated (bool): Whether the label is human annotated. Defaults to False.
+            confidence (float): The confidence of the label. Defaults to 0.0.
+
+        Returns:
+            Text: The updated text label
+        """
+        text_label = self.find_by_id(project_id, example_id, label_id)
+        text_label = Text(
+            id=text_label.id,
+            example=example_id,
+            text=text or text_label.text,
+            manual=human_annotated or text_label.manual,
+            prob=confidence or text_label.prob,
+        )
+        return self._repository.update(project_id, text_label)
+
+
+class BoundingBoxUseCase(LabelUseCase[BoundingBox]):
+    def create(
+        self,
+        project_id: int,
+        example_id: int,
+        x: float,
+        y: float,
+        width: float,
+        height: float,
+        label: int | str,
+        human_annotated: bool = False,
+        confidence: float = 0.0,
+    ) -> BoundingBox:
+        """Create a new bounding box label
+
+        Args:
+            project_id (int): The id of the project
+            example_id (int): The id of the example
+            x (float): The x coordinate of the bounding box
+            y (float): The y coordinate of the bounding box
+            width (float): The width of the bounding box
+            height (float): The height of the bounding box
+            label (int | str): The label to create
+            human_annotated (bool): Whether the label is human annotated. Defaults to False.
+            confidence (float): The confidence of the label. Defaults to 0.0.
+
+        Returns:
+            BoundingBox: The created bounding box label
+
+        Raises:
+            ValueError: If the label type repository is not set
+        """
+        if self._label_type_repository is None:
+            raise ValueError("LabelTypeRepository is not set")
+
+        if isinstance(label, str):
+            label_type = self._label_type_repository.find_by_name(project_id, label)
+            label = label_type.id  # type: ignore
+
+        bounding_box = BoundingBox(
+            example=example_id,
+            x=x,
+            y=y,
+            width=width,
+            height=height,
+            label=label,
+            manual=human_annotated,
+            prob=confidence,
+        )
+        return self._repository.create(project_id, bounding_box)
+
+    def update(
+        self,
+        project_id: int,
+        example_id: int,
+        label_id: int,
+        x: Optional[float] = None,
+        y: Optional[float] = None,
+        width: Optional[float] = None,
+        height: Optional[float] = None,
+        label: Optional[int | str] = None,
+        human_annotated: bool = None,
+        confidence: float = None,
+    ) -> BoundingBox:
+        """Update a bounding box label
+
+        Args:
+            project_id (int): The id of the project
+            example_id (int): The id of the example
+            label_id (int): The id of the label
+            x (float): The x coordinate of the bounding box
+            y (float): The y coordinate of the bounding box
+            width (float): The width of the bounding box
+            height (float): The height of the bounding box
+            label (int | str): The label to create
+            human_annotated (bool): Whether the label is human annotated. Defaults to False.
+            confidence (float): The confidence of the label. Defaults to 0.0.
+
+        Returns:
+            BoundingBox: The updated bounding box label
+
+        Raises:
+            ValueError: If the label type repository is not set
+        """
+        if self._label_type_repository is None:
+            raise ValueError("LabelTypeRepository is not set")
+
+        bounding_box = self.find_by_id(project_id, example_id, label_id)
+
+        if isinstance(label, str):
+            label_type = self._label_type_repository.find_by_name(project_id, label)
+            label = label_type.id
+
+        bounding_box = BoundingBox(
+            id=bounding_box.id,
+            example=example_id,
+            x=x or bounding_box.x,
+            y=y or bounding_box.y,
+            width=width or bounding_box.width,
+            height=height or bounding_box.height,
+            label=label or bounding_box.label,
+            manual=human_annotated or bounding_box.manual,
+            prob=confidence or bounding_box.prob,
+        )
+        return self._repository.update(project_id, bounding_box)
+
+
+class SegmentUseCase(LabelUseCase[Segment]):
+    def create(
+        self,
+        project_id: int,
+        example_id: int,
+        points: List[float],
+        label: int | str,
+        human_annotated: bool = False,
+        confidence: float = 0.0,
+    ) -> Segment:
+        """Create a new segment label
+
+        Args:
+            project_id (int): The id of the project
+            example_id (int): The id of the example
+            points (List[float]): The points of the segment
+            label (int | str): The label to create
+            human_annotated (bool): Whether the label is human annotated. Defaults to False.
+            confidence (float): The confidence of the label. Defaults to 0.0.
+
+        Returns:
+            Segment: The created segment label
+
+        Raises:
+            ValueError: If the label type repository is not set
+        """
+        if self._label_type_repository is None:
+            raise ValueError("LabelTypeRepository is not set")
+
+        if isinstance(label, str):
+            label_type = self._label_type_repository.find_by_name(project_id, label)
+            label = label_type.id  # type: ignore
+
+        segment = Segment(
+            example=example_id,
+            points=points,
+            label=label,
+            manual=human_annotated,
+            prob=confidence,
+        )
+        return self._repository.create(project_id, segment)
+
+    def update(
+        self,
+        project_id: int,
+        example_id: int,
+        label_id: int,
+        points: Optional[List[float]] = None,
+        label: Optional[int | str] = None,
+        human_annotated: bool = None,
+        confidence: float = None,
+    ) -> Segment:
+        """Update a segment label
+
+        Args:
+            project_id (int): The id of the project
+            example_id (int): The id of the example
+            label_id (int): The id of the label
+            points (List[float]): The points of the segment
+            label (int | str): The label to create
+            human_annotated (bool): Whether the label is human annotated. Defaults to False.
+            confidence (float): The confidence of the label. Defaults to 0.0.
+
+        Returns:
+            Segment: The updated segment label
+
+        Raises:
+            ValueError: If the label type repository is not set
+        """
+        if self._label_type_repository is None:
+            raise ValueError("LabelTypeRepository is not set")
+
+        segment = self.find_by_id(project_id, example_id, label_id)
+
+        if isinstance(label, str):
+            label_type = self._label_type_repository.find_by_name(project_id, label)
+            label = label_type.id
+
+        segment = Segment(
+            id=segment.id,
+            example=example_id,
+            points=points or segment.points,
+            label=label or segment.label,
+            manual=human_annotated or segment.manual,
+            prob=confidence or segment.prob,
+        )
+        return self._repository.update(project_id, segment)
diff --git a/doccano_client/usecase/label_type.py b/doccano_client/usecase/label_type.py
new file mode 100644
index 0000000..b70509f
--- /dev/null
+++ b/doccano_client/usecase/label_type.py
@@ -0,0 +1,128 @@
+from __future__ import annotations
+
+from typing import List, Optional
+
+from doccano_client.models.label_type import PREFIX_KEY, SUFFIX_KEY, LabelType
+from doccano_client.repositories.label_type import LabelTypeRepository
+from doccano_client.services.label_type import LabelTypeService
+
+
+class LabelTypeUseCase:
+    def __init__(self, repository: LabelTypeRepository, service: LabelTypeService):
+        self._repository = repository
+        self._service = service
+
+    def find_by_id(self, project_id: int, label_type_id: int) -> LabelType:
+        """Find a label type by id
+
+        Args:
+            project_id (int): The id of the project to find
+            label_type_id (int): The id of the label type to find
+
+        Returns:
+            LabelType: The found label type
+        """
+        return self._repository.find_by_id(project_id, label_type_id)
+
+    def list(self, project_id: int) -> List[LabelType]:
+        """Return all label types
+
+        Args:
+            project_id (int): The id of the project
+
+        Returns:
+            List[LabelType]: The label types in the project.
+        """
+        return self._repository.list(project_id)
+
+    def create(
+        self,
+        project_id: int,
+        text: str,
+        prefix_key: PREFIX_KEY = None,
+        suffix_key: SUFFIX_KEY = None,
+        color: Optional[str] = None,
+    ) -> LabelType:
+        """Create a new label type
+
+        Args:
+            project_id (int): The id of the project
+            text (str): The text of the label type
+            prefix_key (PREFIX_KEY): The prefix key of the label type
+            suffix_key (SUFFIX_KEY): The suffix key of the label type
+            color (str): The color of the label type
+
+        Returns:
+            LabelType: The created label type
+
+        Raises:
+            ValueError: If the label type already exists
+        """
+        label_type = LabelType.create(text=text, prefix_key=prefix_key, suffix_key=suffix_key, color=color)
+        if self._service.exists(project_id, label_type):
+            raise ValueError("The label type already exists")
+        return self._repository.create(project_id, label_type)
+
+    def update(
+        self,
+        project_id: int,
+        label_type_id: int,
+        text: str = None,
+        prefix_key: PREFIX_KEY | int = -1,
+        suffix_key: SUFFIX_KEY | int = -1,
+        color: str = None,
+    ) -> LabelType:
+        """Update a label type
+
+        Args:
+            project_id (int): The id of the project
+            label_type_id (int): The id of the label type
+            text (str): The text of the label type
+            prefix_key (PREFIX_KEY): The prefix key of the label type
+            suffix_key (SUFFIX_KEY): The suffix key of the label type
+            color (str): The color of the label type
+
+        Returns:
+            LabelType: The updated label type
+
+        Raises:
+            ValueError: If the label type already exists
+        """
+        label_type = self._repository.find_by_id(project_id, label_type_id)
+        label_type = LabelType(
+            id=label_type.id,
+            text=text or label_type.text,
+            prefix_key=prefix_key if prefix_key != -1 else label_type.prefix_key,
+            suffix_key=suffix_key if suffix_key != -1 else label_type.suffix_key,
+            background_color=color or label_type.background_color,
+        )
+        if self._service.exists(project_id, label_type):
+            raise ValueError("The label type already exists")
+        return self._repository.update(project_id, label_type)
+
+    def delete(self, project_id: int, label_type_id: int):
+        """Delete a label type.
+
+        Args:
+            project_id (int): The project id.
+            label_type_id (int): The label type id.
+        """
+        self._repository.delete(project_id, label_type_id)
+
+    def bulk_delete(self, project_id: int, label_type_ids: List[int]):
+        """Bulk delete label types
+
+        Args:
+            project_id (int): The id of the project
+            label_type_ids (List[int]): The list of label type ids to delete
+        """
+        self._repository.bulk_delete(project_id, label_type_ids)
+
+    def upload(self, project_id: int, file_path: str):
+        """Upload a label type
+
+        Args:
+            project_id (int): The id of the project
+            file_path (str): The path to the file to upload
+        """
+        self._repository.upload(project_id, file_path)
diff --git a/doccano_client/usecase/member.py b/doccano_client/usecase/member.py
new file mode 100644
index 0000000..b5efc14
--- /dev/null
+++ b/doccano_client/usecase/member.py
@@ -0,0 +1,98 @@
+from typing import List
+
+from doccano_client.models.member import Member
+from doccano_client.repositories.member import MemberRepository
+from doccano_client.repositories.role import RoleRepository
+from doccano_client.repositories.user import UserRepository
+
+
+class MemberUseCase:
+    def __init__(
+        self, member_repository: MemberRepository, user_repository: UserRepository, role_repository: RoleRepository
+    ):
+        self._member_repository = member_repository
+        self._user_repository = user_repository
+        self._role_repository = role_repository
+
+    def find_by_id(self, project_id: int, member_id: int) -> Member:
+        """Find a member by id
+
+        Args:
+            project_id (int): The id of the project to find
+            member_id (int): The id of the member to find
+
+        Returns:
+            Member: The found member
+        """
+        return self._member_repository.find_by_id(project_id, member_id)
+
+    def list(self, project_id: int) -> List[Member]:
+        """Return all members
+
+        Args:
+            project_id (int): The id of the project
+
+        Returns:
+            List[Member]: The members in the project.
+        """
+        return self._member_repository.list(project_id)
+
+    def add(
+        self,
+        project_id: int,
+        username: str,
+        role_name: str,
+    ) -> Member:
+        """Create a new member
+
+        Args:
+            project_id (int): The id of the project
+            username (str): The username of the future member
+            role_name (str): The role of the future member
+
+        Returns:
+            Member: The created member
+        """
+        user = self._user_repository.find_by_name(username)
+        role = self._role_repository.find_by_name(role_name)
+        member = Member(user=user.id, role=role.id)
+        return self._member_repository.create(project_id, member)
+
+    def update(
+        self,
+        project_id: int,
+        member_id: int,
+        role_name: str,
+    ) -> Member:
+        """Update a member role
+
+        Args:
+            project_id (int): The id of the project
+            member_id (int): The id of the member
+            role_name (str): The role of the member
+
+        Returns:
+            Member: The updated member
+        """
+        member = self.find_by_id(project_id, member_id)
+        role = self._role_repository.find_by_name(role_name)
+        member.role = role.id
+        return self._member_repository.update(project_id, member)
+
+    def delete(self, project_id: int, member_id: int):
+        """Delete a member.
+
+        Args:
+            project_id (int): The project id.
+            member_id (int): The member id.
+        """
+        self._member_repository.delete(project_id, member_id)
+
+    def bulk_delete(self, project_id: int, member_ids: List[int]):
+        """Bulk delete members
+
+        Args:
+            project_id (int): The id of the project
+            member_ids (List[int]): The list of member ids to delete
+        """
+        self._member_repository.bulk_delete(project_id, member_ids)
diff --git a/doccano_client/usecase/project.py b/doccano_client/usecase/project.py
new file mode 100644
index 0000000..4eead7a
--- /dev/null
+++ b/doccano_client/usecase/project.py
@@ -0,0 +1,150 @@
+from typing import Iterator, List, Literal, Optional
+
+from doccano_client.models.project import Project
+from doccano_client.repositories.project import ProjectRepository
+
+ProjectType = Literal[
+    "DocumentClassification",
+    "SequenceLabeling",
+    "Seq2seq",
+    "Speech2text",
+    "ImageClassification",
+    "BoundingBox",
+    "Segmentation",
+    "ImageCaptioning",
+    "IntentDetectionAndSlotFilling",
+]
+
+
+class ProjectUseCase:
+    def __init__(self, repository: ProjectRepository):
+        self._repository = repository
+
+    def find_by_id(self, project_id: int) -> Project:
+        """Find a project by id
+
+        Args:
+            project_id (int): The id of the project to find
+
+        Returns:
+            Project: The found project
+        """
+        return self._repository.find_by_id(project_id)
+
+    def list(self) -> Iterator[Project]:
+        """Return all projects in which you are a member
+
+        Yields:
+            Project: The next project.
+        """
+        yield from self._repository.list()
+
+    def create(
+        self,
+        name: str,
+        project_type: ProjectType,
+        description: str,
+        guideline: str = "",
+        random_order: bool = False,
+        collaborative_annotation: bool = False,
+        single_class_classification: bool = False,
+        allow_overlapping: bool = False,
+        grapheme_mode: bool = False,
+        use_relation: bool = False,
+        tags: Optional[List[str]] = None,
+    ) -> Project:
+        """Create a new project
+
+        Args:
+            name (str): The name of the project
+            project_type (ProjectType): The type of the project
+            description (str): The description of the project.
+            guideline (str): The annotation guideline. Defaults to "".
+            random_order (bool): Whether to shuffle the uploaded data. Defaults to False.
+            collaborative_annotation (bool): If True, a data can be annotated by multiple users. Defaults to False.
+            single_class_classification (bool): If True, only one label can apply a data. Defaults to False.
+            allow_overlapping (bool): If True, span overlapping is allowed. Defaults to False.
+            grapheme_mode (bool): If True, count multi-byte characters as one character. Defaults to False.
+            use_relation (bool): If True, relation labeling is allowed. Defaults to False.
+            tags (Optional[List[str]], optional): The tags of the project. Defaults to None.
+
+        Returns:
+            Project: The created project
+        """
+        project = Project(
+            name=name,
+            description=description,
+            guideline=guideline,
+            project_type=project_type,
+            random_order=random_order,
+            collaborative_annotation=collaborative_annotation,
+            single_class_classification=single_class_classification,
+            allow_overlapping=allow_overlapping,
+            grapheme_mode=grapheme_mode,
+            use_relation=use_relation,
+            tags=tags or [],
+        )
+        return self._repository.create(project)
+
+    def update(
+        self,
+        project_id: int,
+        name: str = None,
+        project_type: ProjectType = None,
+        description: str = None,
+        guideline: str = None,
+        random_order: bool = None,
+        collaborative_annotation: bool = None,
+        single_class_classification: bool = None,
+        allow_overlapping: bool = None,
+        grapheme_mode: bool = None,
+        use_relation: bool = None,
+        tags: Optional[List[str]] = None,
+    ) -> Project:
+        """Update a project
+
+        Args:
+            project_id (int): The project id.
+            name (str): The name of the project
+            project_type (ProjectType): The type of the project
+            description (str): The description of the project. Defaults to None.
+            guideline (str): The annotation guideline. Defaults to None.
+            random_order (bool): Whether to shuffle the uploaded data. Defaults to None.
+            collaborative_annotation (bool): If True, a data can be annotated by multiple users. Defaults to None.
+            single_class_classification (bool): If True, only one label can apply a data. Defaults to None.
+            allow_overlapping (bool): If True, span overlapping is allowed. Defaults to None.
+            grapheme_mode (bool): If True, count multi-byte characters as one character. Defaults to None.
+            use_relation (bool): If True, relation labeling is allowed. Defaults to None.
+            tags (Optional[List[str]], optional): The tags of the project. Defaults to None.
+
+        Returns:
+            Project: The updated project
+        """
+        project = self.find_by_id(project_id)
+        project = Project(
+            id=project_id,
+            name=name or project.name,
+            description=description or project.description,
+            guideline=guideline if guideline is not None else project.guideline,
+            project_type=project_type or project.project_type,
+            random_order=random_order if random_order is not None else project.random_order,
+            collaborative_annotation=collaborative_annotation
+            if collaborative_annotation is not None
+            else project.collaborative_annotation,
+            single_class_classification=single_class_classification
+            if single_class_classification is not None
+            else project.single_class_classification,
+            allow_overlapping=allow_overlapping if allow_overlapping is not None else project.allow_overlapping,
+            grapheme_mode=grapheme_mode if grapheme_mode is not None else project.grapheme_mode,
+            use_relation=use_relation if use_relation is not None else project.use_relation,
+            tags=tags or [],
+        )
+        return self._repository.update(project)
+
+    def delete(self, project_id: int):
+        """Delete a project.
+
+        Args:
+            project_id (int): The project id.
+        """
+        self._repository.delete(project_id)
diff --git a/doccano_client/usecase/role.py b/doccano_client/usecase/role.py
new file mode 100644
index 0000000..aa00acf
--- /dev/null
+++ b/doccano_client/usecase/role.py
@@ -0,0 +1,17 @@
+from typing import List
+
+from doccano_client.models.role import Role
+from doccano_client.repositories.role import RoleRepository
+
+
+class RoleUseCase:
+    def __init__(self, repository: RoleRepository):
+        self._repository = repository
+
+    def list(self) -> List[Role]:
+        """Return all roles
+
+        Returns:
+            List[Role]: The list of the roles.
+        """
+        return self._repository.list()
diff --git a/docs/cli.md b/docs/cli.md
new file mode 100644
index 0000000..37e70f3
--- /dev/null
+++ b/docs/cli.md
@@ -0,0 +1,51 @@
+# CLI
+
+By using the CLI, you can pre-annotate texts with the spaCy model. Currently, `login` and `predict` commands are supported. `login` is the command to login to doccano and must be executed first. `predict` is the command to annotate data with the model. The usage is as follows:
+
+```bash
+# On GPU machine
+$ python -m spacy train config.cfg \
+    --output outputs \
+    --paths.train /path/to/train.spacy \
+    --paths.dev /path/to/dev.spacy \
+    --gpu-id 0
+
+# On another machine
+$ docli login \
+    --host http://127.0.0.1:8000 \
+    --username admin \
+    --password password
+$ docli predict <task> \
+    --project <project_id> \
+    --model <en_core_web_sm> \
+    --mapping [mapping.json] \
+    --framework [spacy]
+```
+
+Currently, only `ner` is supported as a task and `spacy` as a framework. Also, the `framework` and `mapping` options can be omitted.
+
+```bash
+$ docli predict ner \
+    --project <project_id> \
+    --model <en_core_web_sm>
+```
+
+Mapping can be a path to a JSON file. This JSON file consists of keys and values as shown below. The key is the label names output by the model, and the value is the label names you defined in doccano. In this way, the label names output by the model is converted to the defined label names so that they can be labeled.
+
+```bash
+{
+   "LOCATION":"LOC",
+   "ORGANIZATION":"ORG",
+   "GPE":"LOC"
+}
+```
+
+Note that if the label names output by the model are not defined in doccano and are not converted using mapping, the labels will be discarded.
+
+This feature is integrated with [spacy-partial-tagger](https://github.com/doccano/spacy-partial-tagger), which allows you to train models with just a dictionary and some text. Please see [the repository](https://github.com/doccano/spacy-partial-tagger) for more details.
+
+## Installation
+
+```bash
+pip install doccano-client[spacy]
+```
diff --git a/docs/index.md b/docs/index.md
index 07dd0c5..8981bcc 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,1 +1,27 @@
 # Overview
+
+doccano-client is a simple client wrapper for the doccano API.
+
+## Installation
+
+To install `doccano-client`, simply run:
+
+```bash
+pip install doccano-client
+```
+
+## Usage
+
+```python
+from doccano_client import DoccanoClient
+
+# instantiate a client and log in to a Doccano instance
+client = DoccanoClient('http://doccano.example.com')
+client.login(username='username', password='password')
+
+# get basic information about the authorized user
+user = doccano_client.get_profile()
+
+# list all projects
+projects = doccano_client.list_projects()
+```
diff --git a/docs/install.md b/docs/install.md
index 55af1c5..c3d1cd2 100644
--- a/docs/install.md
+++ b/docs/install.md
@@ -1,1 +1,7 @@
 # Install
+
+To install `doccano-client`, simply run:
+
+```bash
+pip install doccano-client
+```
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
index e9755d6..4267620 100644
--- a/docs/mkdocs.yml
+++ b/docs/mkdocs.yml
@@ -28,8 +28,25 @@ extra:
 
 plugins:
   - same-dir
+  - mkdocstrings:
+      handlers:
+        python:
+          options:
+            show_root_heading: true
+            show_signature: false
+            show_root_full_path: false
+            heading_level: 3
+
+markdown_extensions:
+  - pymdownx.highlight:
+      anchor_linenums: true
+  - pymdownx.inlinehilite
+  - pymdownx.snippets
+  - pymdownx.superfences
 
 # Page tree
 nav:
   - Overview: index.md
   - Install: install.md
+  - Usage: usage.md
+  - CLI: cli.md
diff --git a/docs/requirements.txt b/docs/requirements.txt
index fbee236..99edd04 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -1,1 +1,2 @@
 mkdocs-same-dir
+mkdocstrings[python]
diff --git a/docs/usage.md b/docs/usage.md
new file mode 100644
index 0000000..8e56259
--- /dev/null
+++ b/docs/usage.md
@@ -0,0 +1,132 @@
+# Usage
+
+## Authentication
+
+::: doccano_client.DoccanoClient.login
+
+## User
+
+::: doccano_client.DoccanoClient.get_profile
+::: doccano_client.DoccanoClient.search_users
+::: doccano_client.DoccanoClient.find_user_by_name
+
+## Role
+
+::: doccano_client.DoccanoClient.list_roles
+
+## Project
+
+::: doccano_client.DoccanoClient.find_project_by_id
+::: doccano_client.DoccanoClient.list_projects
+::: doccano_client.DoccanoClient.create_project
+::: doccano_client.DoccanoClient.update_project
+::: doccano_client.DoccanoClient.delete_project
+
+## Member
+
+::: doccano_client.DoccanoClient.find_member_by_id
+::: doccano_client.DoccanoClient.list_members
+::: doccano_client.DoccanoClient.add_member
+::: doccano_client.DoccanoClient.update_member
+::: doccano_client.DoccanoClient.delete_member
+::: doccano_client.DoccanoClient.bulk_delete_members
+
+## Label Type
+
+::: doccano_client.DoccanoClient.find_label_type_by_id
+::: doccano_client.DoccanoClient.list_label_types
+::: doccano_client.DoccanoClient.create_label_type
+::: doccano_client.DoccanoClient.update_label_type
+::: doccano_client.DoccanoClient.delete_label_type
+::: doccano_client.DoccanoClient.bulk_delete_label_types
+::: doccano_client.DoccanoClient.upload_label_type
+
+## Example
+
+::: doccano_client.DoccanoClient.find_example_by_id
+::: doccano_client.DoccanoClient.list_examples
+::: doccano_client.DoccanoClient.create_example
+::: doccano_client.DoccanoClient.update_example
+::: doccano_client.DoccanoClient.delete_example
+::: doccano_client.DoccanoClient.bulk_delete_examples
+::: doccano_client.DoccanoClient.delete_all_examples
+::: doccano_client.DoccanoClient.update_example_state
+
+## Comment
+
+::: doccano_client.DoccanoClient.find_comment_by_id
+::: doccano_client.DoccanoClient.list_comments
+::: doccano_client.DoccanoClient.create_comment
+::: doccano_client.DoccanoClient.update_comment
+::: doccano_client.DoccanoClient.delete_comment
+::: doccano_client.DoccanoClient.bulk_delete_comments
+
+## Data Upload
+
+::: doccano_client.DoccanoClient.list_upload_options
+::: doccano_client.DoccanoClient.upload
+
+## Data Download
+
+::: doccano_client.DoccanoClient.list_download_options
+::: doccano_client.DoccanoClient.download
+
+## Metrics
+
+::: doccano_client.DoccanoClient.get_progress
+::: doccano_client.DoccanoClient.get_members_progress
+::: doccano_client.DoccanoClient.get_label_distribution
+
+## Category
+
+::: doccano_client.DoccanoClient.find_category_by_id
+::: doccano_client.DoccanoClient.list_categories
+::: doccano_client.DoccanoClient.create_category
+::: doccano_client.DoccanoClient.update_category
+::: doccano_client.DoccanoClient.delete_category
+::: doccano_client.DoccanoClient.delete_all_categories
+
+## Span
+
+::: doccano_client.DoccanoClient.find_span_by_id
+::: doccano_client.DoccanoClient.list_spans
+::: doccano_client.DoccanoClient.create_span
+::: doccano_client.DoccanoClient.update_span
+::: doccano_client.DoccanoClient.delete_span
+::: doccano_client.DoccanoClient.delete_all_spans
+
+## Relation
+
+::: doccano_client.DoccanoClient.find_relation_by_id
+::: doccano_client.DoccanoClient.list_relations
+::: doccano_client.DoccanoClient.create_relation
+::: doccano_client.DoccanoClient.update_relation
+::: doccano_client.DoccanoClient.delete_relation
+::: doccano_client.DoccanoClient.delete_all_relations
+
+## Text
+
+::: doccano_client.DoccanoClient.find_text_by_id
+::: doccano_client.DoccanoClient.list_texts
+::: doccano_client.DoccanoClient.create_text
+::: doccano_client.DoccanoClient.update_text
+::: doccano_client.DoccanoClient.delete_text
+::: doccano_client.DoccanoClient.delete_all_texts
+
+## Bounding Box
+
+::: doccano_client.DoccanoClient.find_bounding_box_by_id
+::: doccano_client.DoccanoClient.list_bounding_boxes
+::: doccano_client.DoccanoClient.create_bounding_box
+::: doccano_client.DoccanoClient.update_bounding_box
+::: doccano_client.DoccanoClient.delete_bounding_box
+::: doccano_client.DoccanoClient.delete_all_bounding_boxes
+
+## Segment
+
+::: doccano_client.DoccanoClient.find_segment_by_id
+::: doccano_client.DoccanoClient.list_segments
+::: doccano_client.DoccanoClient.create_segment
+::: doccano_client.DoccanoClient.update_segment
+::: doccano_client.DoccanoClient.delete_segment
+::: doccano_client.DoccanoClient.delete_all_segments
diff --git a/poetry.lock b/poetry.lock
index 80bad04..2fe5ae0 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -45,6 +45,14 @@ python-versions = "*"
 [package.dependencies]
 numpy = ">=1.15.0"
 
+[[package]]
+name = "cached-property"
+version = "1.5.2"
+description = "A decorator for caching properties in classes."
+category = "dev"
+optional = false
+python-versions = "*"
+
 [[package]]
 name = "catalogue"
 version = "2.0.8"
@@ -174,6 +182,17 @@ python-dateutil = ">=2.8.1"
 [package.extras]
 dev = ["wheel", "flake8", "markdown", "twine"]
 
+[[package]]
+name = "griffe"
+version = "0.22.2"
+description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+async = ["aiofiles (>=0.7,<1.0)"]
+
 [[package]]
 name = "huggingface-hub"
 version = "0.9.1"
@@ -284,6 +303,17 @@ importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""}
 [package.extras]
 testing = ["coverage", "pyyaml"]
 
+[[package]]
+name = "markdown-callouts"
+version = "0.3.0"
+description = "Markdown extension: a classier syntax for admonitions"
+category = "dev"
+optional = false
+python-versions = ">=3.6,<4.0"
+
+[package.dependencies]
+Markdown = ">=3.3.3,<4.0.0"
+
 [[package]]
 name = "markupsafe"
 version = "2.1.1"
@@ -359,6 +389,18 @@ watchdog = ">=2.0"
 [package.extras]
 i18n = ["babel (>=2.9.0)"]
 
+[[package]]
+name = "mkdocs-autorefs"
+version = "0.4.1"
+description = "Automatically link across pages in MkDocs."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+Markdown = ">=3.3"
+mkdocs = ">=1.1"
+
 [[package]]
 name = "mkdocs-material"
 version = "8.5.3"
@@ -395,6 +437,57 @@ python-versions = ">=3.6,<4.0"
 [package.dependencies]
 mkdocs = ">=1.0.3,<2.0.0"
 
+[[package]]
+name = "mkdocstrings"
+version = "0.19.0"
+description = "Automatic documentation from sources, for MkDocs."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+Jinja2 = ">=2.11.1"
+Markdown = ">=3.3"
+MarkupSafe = ">=1.1"
+mkdocs = ">=1.2"
+mkdocs-autorefs = ">=0.3.1"
+mkdocstrings-crystal = {version = ">=0.3.4", optional = true, markers = "extra == \"crystal\""}
+mkdocstrings-python = {version = ">=0.5.2", optional = true, markers = "extra == \"python\""}
+pymdown-extensions = ">=6.3"
+
+[package.extras]
+crystal = ["mkdocstrings-crystal (>=0.3.4)"]
+python = ["mkdocstrings-python (>=0.5.2)"]
+python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"]
+
+[[package]]
+name = "mkdocstrings-crystal"
+version = "0.3.5"
+description = "Crystal language doc generator for mkdocstrings"
+category = "dev"
+optional = false
+python-versions = ">=3.7,<4.0"
+
+[package.dependencies]
+cached-property = "*"
+Jinja2 = ">=2.11.2,<4.0"
+markdown-callouts = ">=0.1.0"
+markupsafe = ">=1.1.1,<3.0"
+mkdocs-autorefs = ">=0.2.0"
+mkdocstrings = ">=0.15.1"
+
+[[package]]
+name = "mkdocstrings-python"
+version = "0.7.1"
+description = "A Python handler for mkdocstrings."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+griffe = ">=0.11.1"
+mkdocstrings = ">=0.19"
+
 [[package]]
 name = "mslex"
 version = "0.3.0"
@@ -403,6 +496,14 @@ category = "dev"
 optional = false
 python-versions = ">=3.5"
 
+[[package]]
+name = "multidict"
+version = "6.0.2"
+description = "multidict implementation"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
 [[package]]
 name = "murmurhash"
 version = "1.0.8"
@@ -561,7 +662,7 @@ name = "pydantic"
 version = "1.9.2"
 description = "Data validation and settings management using python type hints"
 category = "main"
-optional = true
+optional = false
 python-versions = ">=3.6.1"
 
 [package.dependencies]
@@ -708,6 +809,17 @@ urllib3 = ">=1.21.1,<1.27"
 socks = ["PySocks (>=1.5.6,!=1.5.7)"]
 use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"]
 
+[[package]]
+name = "requests-toolbelt"
+version = "0.9.1"
+description = "A utility belt for advanced users of python-requests"
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+requests = ">=2.0.1,<3.0.0"
+
 [[package]]
 name = "responses"
 version = "0.21.0"
@@ -1116,6 +1228,20 @@ brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"]
 secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "urllib3-secure-extra", "ipaddress"]
 socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
 
+[[package]]
+name = "vcrpy"
+version = "4.2.1"
+description = "Automatically mock your HTTP interactions to simplify and speed up testing"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+PyYAML = "*"
+six = ">=1.5"
+wrapt = "*"
+yarl = "*"
+
 [[package]]
 name = "wasabi"
 version = "0.10.1"
@@ -1135,6 +1261,26 @@ python-versions = ">=3.6"
 [package.extras]
 watchmedo = ["PyYAML (>=3.10)"]
 
+[[package]]
+name = "wrapt"
+version = "1.14.1"
+description = "Module for decorators, wrappers and monkey patching."
+category = "dev"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+
+[[package]]
+name = "yarl"
+version = "1.8.1"
+description = "Yet another URL library"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+idna = ">=2.0"
+multidict = ">=4.0"
+
 [[package]]
 name = "zipp"
 version = "3.8.1"
@@ -1153,12 +1299,13 @@ spacy = ["spacy", "spacy-partial-tagger", "tqdm"]
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.8"
-content-hash = "38b339e312e4c779bea6aadb237fa0f9f35a9dbadbd77776c6415a63a6479640"
+content-hash = "1801a25aee7d4687eb7e3516d5bfcb7d4cabd264e8b7e8d39e8d935c8762e6ab"
 
 [metadata.files]
 attrs = []
 black = []
 blis = []
+cached-property = []
 catalogue = []
 certifi = []
 charset-normalizer = []
@@ -1194,6 +1341,7 @@ dataclasses-json = []
 filelock = []
 flake8 = []
 ghp-import = []
+griffe = []
 huggingface-hub = []
 idna = []
 importlib-metadata = []
@@ -1214,6 +1362,7 @@ langcodes = [
     {file = "langcodes-3.3.0.tar.gz", hash = "sha256:794d07d5a28781231ac335a1561b8442f8648ca07cd518310aeb45d6f0807ef6"},
 ]
 markdown = []
+markdown-callouts = []
 markupsafe = [
     {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"},
     {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"},
@@ -1261,10 +1410,15 @@ marshmallow-enum = []
 mccabe = []
 mergedeep = []
 mkdocs = []
+mkdocs-autorefs = []
 mkdocs-material = []
 mkdocs-material-extensions = []
 mkdocs-same-dir = []
+mkdocstrings = []
+mkdocstrings-crystal = []
+mkdocstrings-python = []
 mslex = []
+multidict = []
 murmurhash = []
 mypy = []
 mypy-extensions = [
@@ -1313,6 +1467,7 @@ pyyaml = []
 pyyaml-env-tag = []
 regex = []
 requests = []
+requests-toolbelt = []
 responses = []
 six = [
     {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
@@ -1345,6 +1500,9 @@ types-urllib3 = []
 typing-extensions = []
 typing-inspect = []
 urllib3 = []
+vcrpy = []
 wasabi = []
 watchdog = []
+wrapt = []
+yarl = []
 zipp = []
diff --git a/pyproject.toml b/pyproject.toml
index 91ad117..36e84f1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -24,6 +24,8 @@ dataclasses-json = "^0.5.7"
 spacy = { version = "^3.4.1", optional = true }
 spacy-partial-tagger = { version = "^0.9.1", optional = true }
 tqdm = { version = "^4.64.1", optional = true }
+pydantic = "^1.9.2"
+requests-toolbelt = "^0.9.1"
 
 [tool.poetry.dev-dependencies]
 flake8 = "^5.0.4"
@@ -39,6 +41,8 @@ taskipy = "^1.10.3"
 mkdocs = "^1.3.1"
 mkdocs-material = "^8.5.3"
 mkdocs-same-dir = "^0.1.1"
+vcrpy = "^4.2.1"
+mkdocstrings = {extras = ["python", "crystal"], version = "^0.19.0"}
 
 [tool.poetry.extras]
 spacy = ["spacy", "spacy-partial-tagger", "tqdm"]
@@ -58,8 +62,9 @@ include = '\.pyi?$'
 [tool.flake8]
 max-line-length = 120
 max-complexity = 18
-ignore = "E203,E266,W503,DAR101,DAR201" # Todo: remove DAR*
+ignore = "E203,E266,W503"
 filename = "*.py"
+exclude = ".git,__pycache__,docs,build,dist,doccano_client/beta"
 
 [tool.isort]
 profile = "black"
@@ -70,6 +75,9 @@ multi_line_output = 3
 python_version = "3.8"
 ignore_missing_imports = true
 show_error_codes = true
+exclude = [
+    'doccano_client/beta',
+]
 
 [tool.taskipy.tasks]
 isort = "isort . -c"


Test patch:
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..779867a
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,5 @@
+import pathlib
+
+cassettes_path = pathlib.Path(__file__).parent / "fixtures/cassettes"
+repository_fixtures = pathlib.Path(__file__).parent / "integration/repositories/fixtures"
+usecase_fixtures = pathlib.Path(__file__).parent / "integration/usecase/fixtures"
diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/integration/data/classification.jsonl b/tests/integration/data/classification.jsonl
new file mode 100644
index 0000000..910d396
--- /dev/null
+++ b/tests/integration/data/classification.jsonl
@@ -0,0 +1,3 @@
+{"text": "exampleA", "labels": ["positive"], "meta": {"wikiPageID": 1}}
+{"text": "exampleB", "labels": ["positive", "negative"], "meta": {"wikiPageID": 2}}
+{"text": "exampleC", "labels": [], "meta": {"wikiPageID": 3}}
diff --git a/tests/integration/data/labels.json b/tests/integration/data/labels.json
new file mode 100644
index 0000000..73b5f90
--- /dev/null
+++ b/tests/integration/data/labels.json
@@ -0,0 +1,20 @@
+[
+  {
+    "id": 44,
+    "text": "Dog",
+    "prefix_key": null,
+    "suffix_key": "a",
+    "background_color": "#FF0000",
+    "text_color": "#ffffff",
+    "task_type": "Category"
+  },
+  {
+    "id": 45,
+    "text": "Cat",
+    "prefix_key": null,
+    "suffix_key": "c",
+    "background_color": "#FF0000",
+    "text_color": "#ffffff",
+    "task_type": "Span"
+  }
+]
diff --git a/tests/integration/repositories/__init__.py b/tests/integration/repositories/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/integration/repositories/fixtures/comment/create.yaml b/tests/integration/repositories/fixtures/comment/create.yaml
new file mode 100644
index 0000000..e0de538
--- /dev/null
+++ b/tests/integration/repositories/fixtures/comment/create.yaml
@@ -0,0 +1,57 @@
+interactions:
+- request:
+    body: '{"text": "Test comment", "user": null, "username": null, "created_at":
+      null}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '76'
+      Cookie:
+      - csrftoken=GKXRT0ynEqZ6LKVh8KIm7J1RpsPzP8yYkDPTrUm3At4GOxlkHMMmRn6GQIBEbi9v;
+        sessionid=8czutj4pl50odndek7rwb0t68rxiwnsw
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - GKXRT0ynEqZ6LKVh8KIm7J1RpsPzP8yYkDPTrUm3At4GOxlkHMMmRn6GQIBEbi9v
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects/16/comments?example=14069
+  response:
+    body:
+      string: '{"id":2,"user":1,"username":"admin","example":"14069","text":"Test
+        comment","created_at":"2022-09-29T12:46:39.166721Z"}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '119'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Thu, 29 Sep 2022 12:46:39 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+version: 1
diff --git a/tests/integration/repositories/fixtures/comment/delete.yaml b/tests/integration/repositories/fixtures/comment/delete.yaml
new file mode 100644
index 0000000..207435f
--- /dev/null
+++ b/tests/integration/repositories/fixtures/comment/delete.yaml
@@ -0,0 +1,53 @@
+interactions:
+- request:
+    body: '{}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '2'
+      Cookie:
+      - csrftoken=GKXRT0ynEqZ6LKVh8KIm7J1RpsPzP8yYkDPTrUm3At4GOxlkHMMmRn6GQIBEbi9v;
+        sessionid=8czutj4pl50odndek7rwb0t68rxiwnsw
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - GKXRT0ynEqZ6LKVh8KIm7J1RpsPzP8yYkDPTrUm3At4GOxlkHMMmRn6GQIBEbi9v
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: DELETE
+    uri: http://localhost:8000/v1/projects/16/comments/2
+  response:
+    body:
+      string: ''
+    headers:
+      Allow:
+      - GET, PUT, PATCH, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '0'
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Thu, 29 Sep 2022 12:46:39 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 204
+      message: No Content
+version: 1
diff --git a/tests/integration/repositories/fixtures/comment/login.yaml b/tests/integration/repositories/fixtures/comment/login.yaml
new file mode 100644
index 0000000..55d75a6
--- /dev/null
+++ b/tests/integration/repositories/fixtures/comment/login.yaml
@@ -0,0 +1,55 @@
+interactions:
+- request:
+    body: '{"username": "admin", "password": "password"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '45'
+      User-Agent:
+      - python-requests/2.28.1
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/auth/login/
+  response:
+    body:
+      string: '{"key":"b37c59f6e0db6bf8a829858ce925b9c2b4d7b03d"}'
+    headers:
+      Allow:
+      - POST, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '50'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Thu, 29 Sep 2022 12:46:39 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Set-Cookie:
+      - csrftoken=GKXRT0ynEqZ6LKVh8KIm7J1RpsPzP8yYkDPTrUm3At4GOxlkHMMmRn6GQIBEbi9v;
+        expires=Thu, 28 Sep 2023 12:46:39 GMT; Max-Age=31449600; Path=/; SameSite=Lax
+      - sessionid=8czutj4pl50odndek7rwb0t68rxiwnsw; expires=Thu, 13 Oct 2022 12:46:39
+        GMT; HttpOnly; Max-Age=1209600; Path=/; SameSite=Lax
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/repositories/fixtures/comment/update.yaml b/tests/integration/repositories/fixtures/comment/update.yaml
new file mode 100644
index 0000000..d452e8c
--- /dev/null
+++ b/tests/integration/repositories/fixtures/comment/update.yaml
@@ -0,0 +1,57 @@
+interactions:
+- request:
+    body: '{"id": 2, "text": "Updated Type", "example": 14069, "user": 1, "username":
+      "admin", "created_at": "2022-09-29T12:46:39.166721Z"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '128'
+      Cookie:
+      - csrftoken=GKXRT0ynEqZ6LKVh8KIm7J1RpsPzP8yYkDPTrUm3At4GOxlkHMMmRn6GQIBEbi9v;
+        sessionid=8czutj4pl50odndek7rwb0t68rxiwnsw
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - GKXRT0ynEqZ6LKVh8KIm7J1RpsPzP8yYkDPTrUm3At4GOxlkHMMmRn6GQIBEbi9v
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: PUT
+    uri: http://localhost:8000/v1/projects/16/comments/2
+  response:
+    body:
+      string: '{"id":2,"user":1,"username":"admin","example":14069,"text":"Updated
+        Type","created_at":"2022-09-29T12:46:39.166721Z"}'
+    headers:
+      Allow:
+      - GET, PUT, PATCH, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '117'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Thu, 29 Sep 2022 12:46:39 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/repositories/fixtures/data_download/download.yaml b/tests/integration/repositories/fixtures/data_download/download.yaml
new file mode 100644
index 0000000..314ad98
--- /dev/null
+++ b/tests/integration/repositories/fixtures/data_download/download.yaml
@@ -0,0 +1,113 @@
+interactions:
+- request:
+    body: '{"format": "JSONL", "exportApproved": false}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '44'
+      Cookie:
+      - csrftoken=FEvkld0o1MszakkYc27mcoDrcTxNvA0J9jjIzKW6OMzkxS0Zar9aK0aeOk3iALkQ;
+        sessionid=rmbx2nr20xbrk8ofdi9u4ocgai6bze9g
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - FEvkld0o1MszakkYc27mcoDrcTxNvA0J9jjIzKW6OMzkxS0Zar9aK0aeOk3iALkQ
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects/16/download
+  response:
+    body:
+      string: '{"task_id":"775c0bde-3ee5-4e19-b9cd-6f4d53c3c120"}'
+    headers:
+      Allow:
+      - GET, POST, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '50'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Sun, 02 Oct 2022 07:57:46 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=FEvkld0o1MszakkYc27mcoDrcTxNvA0J9jjIzKW6OMzkxS0Zar9aK0aeOk3iALkQ;
+        sessionid=rmbx2nr20xbrk8ofdi9u4ocgai6bze9g
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - FEvkld0o1MszakkYc27mcoDrcTxNvA0J9jjIzKW6OMzkxS0Zar9aK0aeOk3iALkQ
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/16/download?taskId=775c0bde-3ee5-4e19-b9cd-6f4d53c3c120
+  response:
+    body:
+      string: !!binary |
+        UEsDBBQAAAAIADc/QlW3l8FGmgAAAGsCAAALAAAAYWRtaW4uanNvbmytzTELwjAQhuHdn/HNGdRW
+        q9lsXdzcS4eIRwkmaSFRC6X/3WCxaEGpkOnuOHjeFvIMvojnScTgqHHgflgHBiVOpMDzgiGrtCbj
+        bH89H343V6UYNDnR792sHbQ4qLYKqq2DasmgUSN0rWg3RcxRV1Y6eSMUL7nFXV7kUZR02Hu7e69s
+        xpX0zwqDoVL8Ci4/gttxMJsU/GJH3n4AUEsBAhQDFAAAAAgANz9CVbeXwUaaAAAAawIAAAsAAAAA
+        AAAAAAAAAKSBAAAAAGFkbWluLmpzb25sUEsFBgAAAAABAAEAOQAAAMMAAAAAAA==
+    headers:
+      Allow:
+      - GET, POST, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Disposition:
+      - attachment; filename="169be049-d9b1-4882-a974-cd8625adcd67.zip"
+      Content-Length:
+      - '274'
+      Content-Type:
+      - application/zip
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Sun, 02 Oct 2022 07:57:56 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/repositories/fixtures/data_download/login.yaml b/tests/integration/repositories/fixtures/data_download/login.yaml
new file mode 100644
index 0000000..1701bcb
--- /dev/null
+++ b/tests/integration/repositories/fixtures/data_download/login.yaml
@@ -0,0 +1,55 @@
+interactions:
+- request:
+    body: '{"username": "admin", "password": "password"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '45'
+      User-Agent:
+      - python-requests/2.28.1
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/auth/login/
+  response:
+    body:
+      string: '{"key":"b37c59f6e0db6bf8a829858ce925b9c2b4d7b03d"}'
+    headers:
+      Allow:
+      - POST, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '50'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Sun, 02 Oct 2022 07:53:26 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Set-Cookie:
+      - csrftoken=FEvkld0o1MszakkYc27mcoDrcTxNvA0J9jjIzKW6OMzkxS0Zar9aK0aeOk3iALkQ;
+        expires=Sun, 01 Oct 2023 07:53:26 GMT; Max-Age=31449600; Path=/; SameSite=Lax
+      - sessionid=rmbx2nr20xbrk8ofdi9u4ocgai6bze9g; expires=Sun, 16 Oct 2022 07:53:26
+        GMT; HttpOnly; Max-Age=1209600; Path=/; SameSite=Lax
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/repositories/fixtures/data_download/options.yaml b/tests/integration/repositories/fixtures/data_download/options.yaml
new file mode 100644
index 0000000..62ba533
--- /dev/null
+++ b/tests/integration/repositories/fixtures/data_download/options.yaml
@@ -0,0 +1,62 @@
+interactions:
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=FEvkld0o1MszakkYc27mcoDrcTxNvA0J9jjIzKW6OMzkxS0Zar9aK0aeOk3iALkQ;
+        sessionid=rmbx2nr20xbrk8ofdi9u4ocgai6bze9g
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - FEvkld0o1MszakkYc27mcoDrcTxNvA0J9jjIzKW6OMzkxS0Zar9aK0aeOk3iALkQ
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/16/download-format
+  response:
+    body:
+      string: '[{"name":"CSV","example":"text,label\n\"Terrible customer service.\",\"negative\"\n\"Really
+        great transaction.\",\"positive\"\n\"Great price.\",\"positive\"\n"},{"name":"fastText","example":"__label__negative
+        Terrible customer service.\n__label__positive Really great transaction.\n__label__positive
+        Great price.\n"},{"name":"JSON","example":"[\n    {\n        \"text\": \"Terrible
+        customer service.\",\n        \"label\": [\"negative\"]\n    },\n    {\n        \"text\":
+        \"Really great transaction.\",\n        \"label\": [\"positive\"]\n    },\n    {\n        \"text\":
+        \"Great price.\",\n        \"label\": [\"positive\"]\n    }\n]\n"},{"name":"JSONL","example":"{\"text\":
+        \"Terrible customer service.\", \"label\": [\"negative\"]}\n{\"text\": \"Really
+        great transaction.\", \"label\": [\"positive\"]}\n{\"text\": \"Great price.\",
+        \"label\": [\"positive\"]}\n"}]'
+    headers:
+      Allow:
+      - GET, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '867'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Sun, 02 Oct 2022 07:53:26 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/repositories/fixtures/data_download/schedule_download.yaml b/tests/integration/repositories/fixtures/data_download/schedule_download.yaml
new file mode 100644
index 0000000..695f9dd
--- /dev/null
+++ b/tests/integration/repositories/fixtures/data_download/schedule_download.yaml
@@ -0,0 +1,55 @@
+interactions:
+- request:
+    body: '{"format": "JSONL", "exportApproved": false}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '44'
+      Cookie:
+      - csrftoken=FEvkld0o1MszakkYc27mcoDrcTxNvA0J9jjIzKW6OMzkxS0Zar9aK0aeOk3iALkQ;
+        sessionid=rmbx2nr20xbrk8ofdi9u4ocgai6bze9g
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - FEvkld0o1MszakkYc27mcoDrcTxNvA0J9jjIzKW6OMzkxS0Zar9aK0aeOk3iALkQ
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects/16/download
+  response:
+    body:
+      string: '{"task_id":"3a17236c-1e34-4027-adee-8a1be6fe5e02"}'
+    headers:
+      Allow:
+      - GET, POST, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '50'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Sun, 02 Oct 2022 07:57:46 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/repositories/fixtures/data_upload/delete.yaml b/tests/integration/repositories/fixtures/data_upload/delete.yaml
new file mode 100644
index 0000000..0e96007
--- /dev/null
+++ b/tests/integration/repositories/fixtures/data_upload/delete.yaml
@@ -0,0 +1,117 @@
+interactions:
+- request:
+    body: !!python/object/new:_io.BytesIO
+      state: !!python/tuple
+      - !!binary |
+        LS0wYzAzNjM2OGU0Yzc0OGI0YmM4ZTMyZjdhNzBlYmU1OQ0KQ29udGVudC1EaXNwb3NpdGlvbjog
+        Zm9ybS1kYXRhOyBuYW1lPSJmaWxlcG9uZCI7IGZpbGVuYW1lPSJjbGFzc2lmaWNhdGlvbi5qc29u
+        Ig0KDQp7InRleHQiOiAiZXhhbXBsZUEiLCAibGFiZWxzIjogWyJwb3NpdGl2ZSJdLCAibWV0YSI6
+        IHsid2lraVBhZ2VJRCI6IDF9fQp7InRleHQiOiAiZXhhbXBsZUIiLCAibGFiZWxzIjogWyJwb3Np
+        dGl2ZSIsICJuZWdhdGl2ZSJdLCAibWV0YSI6IHsid2lraVBhZ2VJRCI6IDJ9fQp7InRleHQiOiAi
+        ZXhhbXBsZUMiLCAibGFiZWxzIjogW10sICJtZXRhIjogeyJ3aWtpUGFnZUlEIjogM319Cg0KLS0w
+        YzAzNjM2OGU0Yzc0OGI0YmM4ZTMyZjdhNzBlYmU1OS0tDQo=
+      - 0
+      - null
+    headers:
+      Accept:
+      - '*/*'
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '377'
+      Content-Type:
+      - multipart/form-data; boundary=0c036368e4c748b4bc8e32f7a70ebe59
+      Cookie:
+      - csrftoken=dt7iPmKOH48xSezmmypld4izNQtiMVcEA64Qf4a7sSVlTIToGyXjnsQHJRYHuy2b;
+        sessionid=fcjn46n6aqmya549fego0q7igo89cxks
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - dt7iPmKOH48xSezmmypld4izNQtiMVcEA64Qf4a7sSVlTIToGyXjnsQHJRYHuy2b
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/fp/process/
+  response:
+    body:
+      string: HmoLcxgwo9QLFHN27DBKgx
+    headers:
+      Allow:
+      - POST, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '22'
+      Content-Type:
+      - text/plain
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Sun, 02 Oct 2022 04:44:37 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: HmoLcxgwo9QLFHN27DBKgx
+    headers:
+      Accept:
+      - '*/*'
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '22'
+      Content-Type:
+      - text/plain
+      Cookie:
+      - csrftoken=dt7iPmKOH48xSezmmypld4izNQtiMVcEA64Qf4a7sSVlTIToGyXjnsQHJRYHuy2b;
+        sessionid=fcjn46n6aqmya549fego0q7igo89cxks
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - dt7iPmKOH48xSezmmypld4izNQtiMVcEA64Qf4a7sSVlTIToGyXjnsQHJRYHuy2b
+      referer:
+      - http://localhost:8000
+    method: DELETE
+    uri: http://localhost:8000/v1/fp/revert/
+  response:
+    body:
+      string: ''
+    headers:
+      Allow:
+      - DELETE, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '4'
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Sun, 02 Oct 2022 04:44:37 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 204
+      message: No Content
+version: 1
diff --git a/tests/integration/repositories/fixtures/data_upload/ingest.yaml b/tests/integration/repositories/fixtures/data_upload/ingest.yaml
new file mode 100644
index 0000000..684b349
--- /dev/null
+++ b/tests/integration/repositories/fixtures/data_upload/ingest.yaml
@@ -0,0 +1,120 @@
+interactions:
+- request:
+    body: !!python/object/new:_io.BytesIO
+      state: !!python/tuple
+      - !!binary |
+        LS0wOGE2MGVjNzQzYzE0NjM0OTQ4ZjJhZWFmYTJiZWY3Yg0KQ29udGVudC1EaXNwb3NpdGlvbjog
+        Zm9ybS1kYXRhOyBuYW1lPSJmaWxlcG9uZCI7IGZpbGVuYW1lPSJjbGFzc2lmaWNhdGlvbi5qc29u
+        Ig0KDQp7InRleHQiOiAiZXhhbXBsZUEiLCAibGFiZWxzIjogWyJwb3NpdGl2ZSJdLCAibWV0YSI6
+        IHsid2lraVBhZ2VJRCI6IDF9fQp7InRleHQiOiAiZXhhbXBsZUIiLCAibGFiZWxzIjogWyJwb3Np
+        dGl2ZSIsICJuZWdhdGl2ZSJdLCAibWV0YSI6IHsid2lraVBhZ2VJRCI6IDJ9fQp7InRleHQiOiAi
+        ZXhhbXBsZUMiLCAibGFiZWxzIjogW10sICJtZXRhIjogeyJ3aWtpUGFnZUlEIjogM319Cg0KLS0w
+        OGE2MGVjNzQzYzE0NjM0OTQ4ZjJhZWFmYTJiZWY3Yi0tDQo=
+      - 0
+      - null
+    headers:
+      Accept:
+      - '*/*'
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '377'
+      Content-Type:
+      - multipart/form-data; boundary=08a60ec743c14634948f2aeafa2bef7b
+      Cookie:
+      - csrftoken=dt7iPmKOH48xSezmmypld4izNQtiMVcEA64Qf4a7sSVlTIToGyXjnsQHJRYHuy2b;
+        sessionid=fcjn46n6aqmya549fego0q7igo89cxks
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - dt7iPmKOH48xSezmmypld4izNQtiMVcEA64Qf4a7sSVlTIToGyXjnsQHJRYHuy2b
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/fp/process/
+  response:
+    body:
+      string: dntzCMUgfA3sFzXXmSjGe6
+    headers:
+      Allow:
+      - POST, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '22'
+      Content-Type:
+      - text/plain
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Sun, 02 Oct 2022 04:42:25 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: '{"uploadIds": ["dntzCMUgfA3sFzXXmSjGe6"], "task": "DocumentClassification",
+      "format": "JSONL"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '94'
+      Cookie:
+      - csrftoken=dt7iPmKOH48xSezmmypld4izNQtiMVcEA64Qf4a7sSVlTIToGyXjnsQHJRYHuy2b;
+        sessionid=fcjn46n6aqmya549fego0q7igo89cxks
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - dt7iPmKOH48xSezmmypld4izNQtiMVcEA64Qf4a7sSVlTIToGyXjnsQHJRYHuy2b
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects/16/upload
+  response:
+    body:
+      string: '{"task_id":"713c6904-8a07-44bc-8411-333672fb77d5"}'
+    headers:
+      Allow:
+      - POST, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '50'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Sun, 02 Oct 2022 04:42:26 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/repositories/fixtures/data_upload/login.yaml b/tests/integration/repositories/fixtures/data_upload/login.yaml
new file mode 100644
index 0000000..79a82f6
--- /dev/null
+++ b/tests/integration/repositories/fixtures/data_upload/login.yaml
@@ -0,0 +1,55 @@
+interactions:
+- request:
+    body: '{"username": "admin", "password": "password"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '45'
+      User-Agent:
+      - python-requests/2.28.1
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/auth/login/
+  response:
+    body:
+      string: '{"key":"b37c59f6e0db6bf8a829858ce925b9c2b4d7b03d"}'
+    headers:
+      Allow:
+      - POST, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '50'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Sun, 02 Oct 2022 04:31:52 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Set-Cookie:
+      - csrftoken=dt7iPmKOH48xSezmmypld4izNQtiMVcEA64Qf4a7sSVlTIToGyXjnsQHJRYHuy2b;
+        expires=Sun, 01 Oct 2023 04:31:52 GMT; Max-Age=31449600; Path=/; SameSite=Lax
+      - sessionid=fcjn46n6aqmya549fego0q7igo89cxks; expires=Sun, 16 Oct 2022 04:31:52
+        GMT; HttpOnly; Max-Age=1209600; Path=/; SameSite=Lax
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/repositories/fixtures/data_upload/options.yaml b/tests/integration/repositories/fixtures/data_upload/options.yaml
new file mode 100644
index 0000000..8e305c0
--- /dev/null
+++ b/tests/integration/repositories/fixtures/data_upload/options.yaml
@@ -0,0 +1,80 @@
+interactions:
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=dt7iPmKOH48xSezmmypld4izNQtiMVcEA64Qf4a7sSVlTIToGyXjnsQHJRYHuy2b;
+        sessionid=fcjn46n6aqmya549fego0q7igo89cxks
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - dt7iPmKOH48xSezmmypld4izNQtiMVcEA64Qf4a7sSVlTIToGyXjnsQHJRYHuy2b
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/16/catalog
+  response:
+    body:
+      string: "[{\"name\":\"TextFile\",\"accept_types\":\"text/*\",\"title\":\"ArgEncoding\",\"type\":\"object\",\"properties\":{\"encoding\":{\"title\":\"Encoding\",\"default\":\"utf_8\",\"enum\":[\"Auto\",\"ascii\",\"big5\",\"big5hkscs\",\"cp037\",\"cp273\",\"cp424\",\"cp437\",\"cp500\",\"cp720\",\"cp737\",\"cp775\",\"cp850\",\"cp852\",\"cp855\",\"cp856\",\"cp857\",\"cp858\",\"cp860\",\"cp861\",\"cp862\",\"cp863\",\"cp864\",\"cp865\",\"cp866\",\"cp869\",\"cp874\",\"cp875\",\"cp932\",\"cp949\",\"cp950\",\"cp1006\",\"cp1026\",\"cp1125\",\"cp1140\",\"cp1250\",\"cp1251\",\"cp1252\",\"cp1253\",\"cp1254\",\"cp1255\",\"cp1256\",\"cp1257\",\"cp1258\",\"cp65001\",\"euc_jp\",\"euc_jis_2004\",\"euc_jisx0213\",\"euc_kr\",\"gb2312\",\"gbk\",\"gb18030\",\"hz\",\"iso2022_jp\",\"iso2022_jp_1\",\"iso2022_jp_2\",\"iso2022_jp_2004\",\"iso2022_jp_3\",\"iso2022_jp_ext\",\"iso2022_kr\",\"latin_1\",\"iso8859_2\",\"iso8859_3\",\"iso8859_4\",\"iso8859_5\",\"iso8859_6\",\"iso8859_7\",\"iso8859_8\",\"iso8859_9\",\"iso8859_10\",\"iso8859_11\",\"iso8859_13\",\"iso8859_14\",\"iso8859_15\",\"iso8859_16\",\"johab\",\"koi8_r\",\"koi8_t\",\"koi8_u\",\"kz1048\",\"mac_cyrillic\",\"mac_greek\",\"mac_iceland\",\"mac_latin2\",\"mac_roman\",\"mac_turkish\",\"ptcp154\",\"shift_jis\",\"shift_jis_2004\",\"shift_jisx0213\",\"utf_32\",\"utf_32_be\",\"utf_32_le\",\"utf_16\",\"utf_16_be\",\"utf_16_le\",\"utf_7\",\"utf_8\",\"utf_8_sig\"],\"type\":\"string\"}},\"example\":\"\u251C\u2500\u2500
+        202104210943.txt\\n\u251C\u2500\u2500 202104210944.txt\\n\u2514\u2500\u2500
+        202104210945.txt\\n\",\"task_id\":\"DocumentClassification\",\"display_name\":\"TextFile\"},{\"name\":\"TextLine\",\"accept_types\":\"text/*\",\"title\":\"ArgEncoding\",\"type\":\"object\",\"properties\":{\"encoding\":{\"title\":\"Encoding\",\"default\":\"utf_8\",\"enum\":[\"Auto\",\"ascii\",\"big5\",\"big5hkscs\",\"cp037\",\"cp273\",\"cp424\",\"cp437\",\"cp500\",\"cp720\",\"cp737\",\"cp775\",\"cp850\",\"cp852\",\"cp855\",\"cp856\",\"cp857\",\"cp858\",\"cp860\",\"cp861\",\"cp862\",\"cp863\",\"cp864\",\"cp865\",\"cp866\",\"cp869\",\"cp874\",\"cp875\",\"cp932\",\"cp949\",\"cp950\",\"cp1006\",\"cp1026\",\"cp1125\",\"cp1140\",\"cp1250\",\"cp1251\",\"cp1252\",\"cp1253\",\"cp1254\",\"cp1255\",\"cp1256\",\"cp1257\",\"cp1258\",\"cp65001\",\"euc_jp\",\"euc_jis_2004\",\"euc_jisx0213\",\"euc_kr\",\"gb2312\",\"gbk\",\"gb18030\",\"hz\",\"iso2022_jp\",\"iso2022_jp_1\",\"iso2022_jp_2\",\"iso2022_jp_2004\",\"iso2022_jp_3\",\"iso2022_jp_ext\",\"iso2022_kr\",\"latin_1\",\"iso8859_2\",\"iso8859_3\",\"iso8859_4\",\"iso8859_5\",\"iso8859_6\",\"iso8859_7\",\"iso8859_8\",\"iso8859_9\",\"iso8859_10\",\"iso8859_11\",\"iso8859_13\",\"iso8859_14\",\"iso8859_15\",\"iso8859_16\",\"johab\",\"koi8_r\",\"koi8_t\",\"koi8_u\",\"kz1048\",\"mac_cyrillic\",\"mac_greek\",\"mac_iceland\",\"mac_latin2\",\"mac_roman\",\"mac_turkish\",\"ptcp154\",\"shift_jis\",\"shift_jis_2004\",\"shift_jisx0213\",\"utf_32\",\"utf_32_be\",\"utf_32_le\",\"utf_16\",\"utf_16_be\",\"utf_16_le\",\"utf_7\",\"utf_8\",\"utf_8_sig\"],\"type\":\"string\"}},\"example\":\"Terrible
+        customer service.\\nReally great transaction.\\nGreat price.\\n\",\"task_id\":\"DocumentClassification\",\"display_name\":\"TextLine\"},{\"name\":\"CSV\",\"accept_types\":\"text/csv\",\"title\":\"ArgDelimiter\",\"type\":\"object\",\"properties\":{\"encoding\":{\"title\":\"Encoding\",\"default\":\"utf_8\",\"enum\":[\"Auto\",\"ascii\",\"big5\",\"big5hkscs\",\"cp037\",\"cp273\",\"cp424\",\"cp437\",\"cp500\",\"cp720\",\"cp737\",\"cp775\",\"cp850\",\"cp852\",\"cp855\",\"cp856\",\"cp857\",\"cp858\",\"cp860\",\"cp861\",\"cp862\",\"cp863\",\"cp864\",\"cp865\",\"cp866\",\"cp869\",\"cp874\",\"cp875\",\"cp932\",\"cp949\",\"cp950\",\"cp1006\",\"cp1026\",\"cp1125\",\"cp1140\",\"cp1250\",\"cp1251\",\"cp1252\",\"cp1253\",\"cp1254\",\"cp1255\",\"cp1256\",\"cp1257\",\"cp1258\",\"cp65001\",\"euc_jp\",\"euc_jis_2004\",\"euc_jisx0213\",\"euc_kr\",\"gb2312\",\"gbk\",\"gb18030\",\"hz\",\"iso2022_jp\",\"iso2022_jp_1\",\"iso2022_jp_2\",\"iso2022_jp_2004\",\"iso2022_jp_3\",\"iso2022_jp_ext\",\"iso2022_kr\",\"latin_1\",\"iso8859_2\",\"iso8859_3\",\"iso8859_4\",\"iso8859_5\",\"iso8859_6\",\"iso8859_7\",\"iso8859_8\",\"iso8859_9\",\"iso8859_10\",\"iso8859_11\",\"iso8859_13\",\"iso8859_14\",\"iso8859_15\",\"iso8859_16\",\"johab\",\"koi8_r\",\"koi8_t\",\"koi8_u\",\"kz1048\",\"mac_cyrillic\",\"mac_greek\",\"mac_iceland\",\"mac_latin2\",\"mac_roman\",\"mac_turkish\",\"ptcp154\",\"shift_jis\",\"shift_jis_2004\",\"shift_jisx0213\",\"utf_32\",\"utf_32_be\",\"utf_32_le\",\"utf_16\",\"utf_16_be\",\"utf_16_le\",\"utf_7\",\"utf_8\",\"utf_8_sig\"],\"type\":\"string\"},\"column_data\":{\"title\":\"Column
+        Data\",\"default\":\"text\",\"type\":\"string\"},\"column_label\":{\"title\":\"Column
+        Label\",\"default\":\"label\",\"type\":\"string\"},\"delimiter\":{\"title\":\"Delimiter\",\"default\":\",\",\"enum\":[\",\",\"\\t\",\";\",\"|\",\"
+        \"],\"type\":\"string\"}},\"example\":\"column_data,column_label\\n\\\"Terrible
+        customer service.\\\",\\\"negative\\\"\\n\\\"Really great transaction.\\\",\\\"positive\\\"\\n\\\"Great
+        price.\\\",\\\"positive\\\"\\n\",\"task_id\":\"DocumentClassification\",\"display_name\":\"CSV\"},{\"name\":\"fastText\",\"accept_types\":\"text/plain\",\"title\":\"ArgEncoding\",\"type\":\"object\",\"properties\":{\"encoding\":{\"title\":\"Encoding\",\"default\":\"utf_8\",\"enum\":[\"Auto\",\"ascii\",\"big5\",\"big5hkscs\",\"cp037\",\"cp273\",\"cp424\",\"cp437\",\"cp500\",\"cp720\",\"cp737\",\"cp775\",\"cp850\",\"cp852\",\"cp855\",\"cp856\",\"cp857\",\"cp858\",\"cp860\",\"cp861\",\"cp862\",\"cp863\",\"cp864\",\"cp865\",\"cp866\",\"cp869\",\"cp874\",\"cp875\",\"cp932\",\"cp949\",\"cp950\",\"cp1006\",\"cp1026\",\"cp1125\",\"cp1140\",\"cp1250\",\"cp1251\",\"cp1252\",\"cp1253\",\"cp1254\",\"cp1255\",\"cp1256\",\"cp1257\",\"cp1258\",\"cp65001\",\"euc_jp\",\"euc_jis_2004\",\"euc_jisx0213\",\"euc_kr\",\"gb2312\",\"gbk\",\"gb18030\",\"hz\",\"iso2022_jp\",\"iso2022_jp_1\",\"iso2022_jp_2\",\"iso2022_jp_2004\",\"iso2022_jp_3\",\"iso2022_jp_ext\",\"iso2022_kr\",\"latin_1\",\"iso8859_2\",\"iso8859_3\",\"iso8859_4\",\"iso8859_5\",\"iso8859_6\",\"iso8859_7\",\"iso8859_8\",\"iso8859_9\",\"iso8859_10\",\"iso8859_11\",\"iso8859_13\",\"iso8859_14\",\"iso8859_15\",\"iso8859_16\",\"johab\",\"koi8_r\",\"koi8_t\",\"koi8_u\",\"kz1048\",\"mac_cyrillic\",\"mac_greek\",\"mac_iceland\",\"mac_latin2\",\"mac_roman\",\"mac_turkish\",\"ptcp154\",\"shift_jis\",\"shift_jis_2004\",\"shift_jisx0213\",\"utf_32\",\"utf_32_be\",\"utf_32_le\",\"utf_16\",\"utf_16_be\",\"utf_16_le\",\"utf_7\",\"utf_8\",\"utf_8_sig\"],\"type\":\"string\"}},\"example\":\"__label__negative
+        Terrible customer service.\\n__label__positive Really great transaction.\\n__label__positive
+        Great price.\\n\",\"task_id\":\"DocumentClassification\",\"display_name\":\"fastText\"},{\"name\":\"JSON\",\"accept_types\":\"application/json\",\"title\":\"ArgColumn\",\"type\":\"object\",\"properties\":{\"encoding\":{\"title\":\"Encoding\",\"default\":\"utf_8\",\"enum\":[\"Auto\",\"ascii\",\"big5\",\"big5hkscs\",\"cp037\",\"cp273\",\"cp424\",\"cp437\",\"cp500\",\"cp720\",\"cp737\",\"cp775\",\"cp850\",\"cp852\",\"cp855\",\"cp856\",\"cp857\",\"cp858\",\"cp860\",\"cp861\",\"cp862\",\"cp863\",\"cp864\",\"cp865\",\"cp866\",\"cp869\",\"cp874\",\"cp875\",\"cp932\",\"cp949\",\"cp950\",\"cp1006\",\"cp1026\",\"cp1125\",\"cp1140\",\"cp1250\",\"cp1251\",\"cp1252\",\"cp1253\",\"cp1254\",\"cp1255\",\"cp1256\",\"cp1257\",\"cp1258\",\"cp65001\",\"euc_jp\",\"euc_jis_2004\",\"euc_jisx0213\",\"euc_kr\",\"gb2312\",\"gbk\",\"gb18030\",\"hz\",\"iso2022_jp\",\"iso2022_jp_1\",\"iso2022_jp_2\",\"iso2022_jp_2004\",\"iso2022_jp_3\",\"iso2022_jp_ext\",\"iso2022_kr\",\"latin_1\",\"iso8859_2\",\"iso8859_3\",\"iso8859_4\",\"iso8859_5\",\"iso8859_6\",\"iso8859_7\",\"iso8859_8\",\"iso8859_9\",\"iso8859_10\",\"iso8859_11\",\"iso8859_13\",\"iso8859_14\",\"iso8859_15\",\"iso8859_16\",\"johab\",\"koi8_r\",\"koi8_t\",\"koi8_u\",\"kz1048\",\"mac_cyrillic\",\"mac_greek\",\"mac_iceland\",\"mac_latin2\",\"mac_roman\",\"mac_turkish\",\"ptcp154\",\"shift_jis\",\"shift_jis_2004\",\"shift_jisx0213\",\"utf_32\",\"utf_32_be\",\"utf_32_le\",\"utf_16\",\"utf_16_be\",\"utf_16_le\",\"utf_7\",\"utf_8\",\"utf_8_sig\"],\"type\":\"string\"},\"column_data\":{\"title\":\"Column
+        Data\",\"default\":\"text\",\"type\":\"string\"},\"column_label\":{\"title\":\"Column
+        Label\",\"default\":\"label\",\"type\":\"string\"}},\"example\":\"[\\n    {\\n
+        \       \\\"column_data\\\": \\\"Terrible customer service.\\\",\\n        \\\"column_label\\\":
+        [\\\"negative\\\"]\\n    },\\n    {\\n        \\\"column_data\\\": \\\"Really
+        great transaction.\\\",\\n        \\\"column_label\\\": [\\\"positive\\\"]\\n
+        \   },\\n    {\\n        \\\"column_data\\\": \\\"Great price.\\\",\\n        \\\"column_label\\\":
+        [\\\"positive\\\"]\\n    }\\n]\\n\",\"task_id\":\"DocumentClassification\",\"display_name\":\"JSON\"},{\"name\":\"JSONL\",\"accept_types\":\"*\",\"title\":\"ArgColumn\",\"type\":\"object\",\"properties\":{\"encoding\":{\"title\":\"Encoding\",\"default\":\"utf_8\",\"enum\":[\"Auto\",\"ascii\",\"big5\",\"big5hkscs\",\"cp037\",\"cp273\",\"cp424\",\"cp437\",\"cp500\",\"cp720\",\"cp737\",\"cp775\",\"cp850\",\"cp852\",\"cp855\",\"cp856\",\"cp857\",\"cp858\",\"cp860\",\"cp861\",\"cp862\",\"cp863\",\"cp864\",\"cp865\",\"cp866\",\"cp869\",\"cp874\",\"cp875\",\"cp932\",\"cp949\",\"cp950\",\"cp1006\",\"cp1026\",\"cp1125\",\"cp1140\",\"cp1250\",\"cp1251\",\"cp1252\",\"cp1253\",\"cp1254\",\"cp1255\",\"cp1256\",\"cp1257\",\"cp1258\",\"cp65001\",\"euc_jp\",\"euc_jis_2004\",\"euc_jisx0213\",\"euc_kr\",\"gb2312\",\"gbk\",\"gb18030\",\"hz\",\"iso2022_jp\",\"iso2022_jp_1\",\"iso2022_jp_2\",\"iso2022_jp_2004\",\"iso2022_jp_3\",\"iso2022_jp_ext\",\"iso2022_kr\",\"latin_1\",\"iso8859_2\",\"iso8859_3\",\"iso8859_4\",\"iso8859_5\",\"iso8859_6\",\"iso8859_7\",\"iso8859_8\",\"iso8859_9\",\"iso8859_10\",\"iso8859_11\",\"iso8859_13\",\"iso8859_14\",\"iso8859_15\",\"iso8859_16\",\"johab\",\"koi8_r\",\"koi8_t\",\"koi8_u\",\"kz1048\",\"mac_cyrillic\",\"mac_greek\",\"mac_iceland\",\"mac_latin2\",\"mac_roman\",\"mac_turkish\",\"ptcp154\",\"shift_jis\",\"shift_jis_2004\",\"shift_jisx0213\",\"utf_32\",\"utf_32_be\",\"utf_32_le\",\"utf_16\",\"utf_16_be\",\"utf_16_le\",\"utf_7\",\"utf_8\",\"utf_8_sig\"],\"type\":\"string\"},\"column_data\":{\"title\":\"Column
+        Data\",\"default\":\"text\",\"type\":\"string\"},\"column_label\":{\"title\":\"Column
+        Label\",\"default\":\"label\",\"type\":\"string\"}},\"example\":\"{\\\"column_data\\\":
+        \\\"Terrible customer service.\\\", \\\"column_label\\\": [\\\"negative\\\"]}\\n{\\\"column_data\\\":
+        \\\"Really great transaction.\\\", \\\"column_label\\\": [\\\"positive\\\"]}\\n{\\\"column_data\\\":
+        \\\"Great price.\\\", \\\"column_label\\\": [\\\"positive\\\"]}\\n\",\"task_id\":\"DocumentClassification\",\"display_name\":\"JSONL\"},{\"name\":\"Excel\",\"accept_types\":\"application/vnd.ms-excel,
+        application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\"title\":\"ArgColumn\",\"type\":\"object\",\"properties\":{\"encoding\":{\"title\":\"Encoding\",\"default\":\"utf_8\",\"enum\":[\"Auto\",\"ascii\",\"big5\",\"big5hkscs\",\"cp037\",\"cp273\",\"cp424\",\"cp437\",\"cp500\",\"cp720\",\"cp737\",\"cp775\",\"cp850\",\"cp852\",\"cp855\",\"cp856\",\"cp857\",\"cp858\",\"cp860\",\"cp861\",\"cp862\",\"cp863\",\"cp864\",\"cp865\",\"cp866\",\"cp869\",\"cp874\",\"cp875\",\"cp932\",\"cp949\",\"cp950\",\"cp1006\",\"cp1026\",\"cp1125\",\"cp1140\",\"cp1250\",\"cp1251\",\"cp1252\",\"cp1253\",\"cp1254\",\"cp1255\",\"cp1256\",\"cp1257\",\"cp1258\",\"cp65001\",\"euc_jp\",\"euc_jis_2004\",\"euc_jisx0213\",\"euc_kr\",\"gb2312\",\"gbk\",\"gb18030\",\"hz\",\"iso2022_jp\",\"iso2022_jp_1\",\"iso2022_jp_2\",\"iso2022_jp_2004\",\"iso2022_jp_3\",\"iso2022_jp_ext\",\"iso2022_kr\",\"latin_1\",\"iso8859_2\",\"iso8859_3\",\"iso8859_4\",\"iso8859_5\",\"iso8859_6\",\"iso8859_7\",\"iso8859_8\",\"iso8859_9\",\"iso8859_10\",\"iso8859_11\",\"iso8859_13\",\"iso8859_14\",\"iso8859_15\",\"iso8859_16\",\"johab\",\"koi8_r\",\"koi8_t\",\"koi8_u\",\"kz1048\",\"mac_cyrillic\",\"mac_greek\",\"mac_iceland\",\"mac_latin2\",\"mac_roman\",\"mac_turkish\",\"ptcp154\",\"shift_jis\",\"shift_jis_2004\",\"shift_jisx0213\",\"utf_32\",\"utf_32_be\",\"utf_32_le\",\"utf_16\",\"utf_16_be\",\"utf_16_le\",\"utf_7\",\"utf_8\",\"utf_8_sig\"],\"type\":\"string\"},\"column_data\":{\"title\":\"Column
+        Data\",\"default\":\"text\",\"type\":\"string\"},\"column_label\":{\"title\":\"Column
+        Label\",\"default\":\"label\",\"type\":\"string\"}},\"example\":\"column_data,column_label\\n\\\"Terrible
+        customer service.\\\",\\\"negative\\\"\\n\\\"Really great transaction.\\\",\\\"positive\\\"\\n\\\"Great
+        price.\\\",\\\"positive\\\"\\n\",\"task_id\":\"DocumentClassification\",\"display_name\":\"Excel\"}]"
+    headers:
+      Allow:
+      - GET, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '10734'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Sun, 02 Oct 2022 04:31:52 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/repositories/fixtures/data_upload/upload.yaml b/tests/integration/repositories/fixtures/data_upload/upload.yaml
new file mode 100644
index 0000000..40b4092
--- /dev/null
+++ b/tests/integration/repositories/fixtures/data_upload/upload.yaml
@@ -0,0 +1,66 @@
+interactions:
+- request:
+    body: !!python/object/new:_io.BytesIO
+      state: !!python/tuple
+      - !!binary |
+        LS0xNDYxODMzMmI1Mzg0YWE4OTcyYWYyZjBmZWNkODA1Yg0KQ29udGVudC1EaXNwb3NpdGlvbjog
+        Zm9ybS1kYXRhOyBuYW1lPSJmaWxlcG9uZCI7IGZpbGVuYW1lPSJjbGFzc2lmaWNhdGlvbi5qc29u
+        Ig0KDQp7InRleHQiOiAiZXhhbXBsZUEiLCAibGFiZWxzIjogWyJwb3NpdGl2ZSJdLCAibWV0YSI6
+        IHsid2lraVBhZ2VJRCI6IDF9fQp7InRleHQiOiAiZXhhbXBsZUIiLCAibGFiZWxzIjogWyJwb3Np
+        dGl2ZSIsICJuZWdhdGl2ZSJdLCAibWV0YSI6IHsid2lraVBhZ2VJRCI6IDJ9fQp7InRleHQiOiAi
+        ZXhhbXBsZUMiLCAibGFiZWxzIjogW10sICJtZXRhIjogeyJ3aWtpUGFnZUlEIjogM319Cg0KLS0x
+        NDYxODMzMmI1Mzg0YWE4OTcyYWYyZjBmZWNkODA1Yi0tDQo=
+      - 0
+      - null
+    headers:
+      Accept:
+      - '*/*'
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '377'
+      Content-Type:
+      - multipart/form-data; boundary=14618332b5384aa8972af2f0fecd805b
+      Cookie:
+      - csrftoken=dt7iPmKOH48xSezmmypld4izNQtiMVcEA64Qf4a7sSVlTIToGyXjnsQHJRYHuy2b;
+        sessionid=fcjn46n6aqmya549fego0q7igo89cxks
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - dt7iPmKOH48xSezmmypld4izNQtiMVcEA64Qf4a7sSVlTIToGyXjnsQHJRYHuy2b
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/fp/process/
+  response:
+    body:
+      string: 5qT568LS6QvL33z3Ej4jEo
+    headers:
+      Allow:
+      - POST, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '22'
+      Content-Type:
+      - text/plain
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Sun, 02 Oct 2022 04:36:51 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/repositories/fixtures/example/create.yaml b/tests/integration/repositories/fixtures/example/create.yaml
new file mode 100644
index 0000000..bd8ed70
--- /dev/null
+++ b/tests/integration/repositories/fixtures/example/create.yaml
@@ -0,0 +1,57 @@
+interactions:
+- request:
+    body: '{"text": "Test Example", "meta": {}, "annotation_approver": null, "comment_count":
+      0, "is_confirmed": false, "filename": "", "upload_name": ""}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '143'
+      Cookie:
+      - csrftoken=gJvGpE4U6m8bIfWDrit6SWSykfRZZEpM6NAl3u3Rxzw2pXDPALlLzdNGbiHydN08;
+        sessionid=hscb9julcs80kxu2dc8ey7xu91akns9k
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - gJvGpE4U6m8bIfWDrit6SWSykfRZZEpM6NAl3u3Rxzw2pXDPALlLzdNGbiHydN08
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects/16/examples
+  response:
+    body:
+      string: '{"id":14068,"filename":"http://localhost:8000/media/","meta":{},"annotation_approver":null,"comment_count":0,"text":"Test
+        Example","is_confirmed":false,"upload_name":""}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '169'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Wed, 28 Sep 2022 05:40:12 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+version: 1
diff --git a/tests/integration/repositories/fixtures/example/delete.yaml b/tests/integration/repositories/fixtures/example/delete.yaml
new file mode 100644
index 0000000..b867e54
--- /dev/null
+++ b/tests/integration/repositories/fixtures/example/delete.yaml
@@ -0,0 +1,53 @@
+interactions:
+- request:
+    body: '{}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '2'
+      Cookie:
+      - csrftoken=gJvGpE4U6m8bIfWDrit6SWSykfRZZEpM6NAl3u3Rxzw2pXDPALlLzdNGbiHydN08;
+        sessionid=hscb9julcs80kxu2dc8ey7xu91akns9k
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - gJvGpE4U6m8bIfWDrit6SWSykfRZZEpM6NAl3u3Rxzw2pXDPALlLzdNGbiHydN08
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: DELETE
+    uri: http://localhost:8000/v1/projects/16/examples/14068
+  response:
+    body:
+      string: ''
+    headers:
+      Allow:
+      - GET, PUT, PATCH, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '0'
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Wed, 28 Sep 2022 05:40:12 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 204
+      message: No Content
+version: 1
diff --git a/tests/integration/repositories/fixtures/example/login.yaml b/tests/integration/repositories/fixtures/example/login.yaml
new file mode 100644
index 0000000..ae35d8b
--- /dev/null
+++ b/tests/integration/repositories/fixtures/example/login.yaml
@@ -0,0 +1,55 @@
+interactions:
+- request:
+    body: '{"username": "admin", "password": "password"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '45'
+      User-Agent:
+      - python-requests/2.28.1
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/auth/login/
+  response:
+    body:
+      string: '{"key":"b37c59f6e0db6bf8a829858ce925b9c2b4d7b03d"}'
+    headers:
+      Allow:
+      - POST, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '50'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Wed, 28 Sep 2022 05:55:33 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Set-Cookie:
+      - csrftoken=yoN2kKGzjGfHGx0JnFL6gEb1MA9h9c1l6VtsVGPYXKntJVLERzzIOMkMJiGo7XEs;
+        expires=Wed, 27 Sep 2023 05:55:33 GMT; Max-Age=31449600; Path=/; SameSite=Lax
+      - sessionid=4p07d3rvde0nt8v1sus455pdlq7q3dwj; expires=Wed, 12 Oct 2022 05:55:33
+        GMT; HttpOnly; Max-Age=1209600; Path=/; SameSite=Lax
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/repositories/fixtures/example/update.yaml b/tests/integration/repositories/fixtures/example/update.yaml
new file mode 100644
index 0000000..ec709cd
--- /dev/null
+++ b/tests/integration/repositories/fixtures/example/update.yaml
@@ -0,0 +1,58 @@
+interactions:
+- request:
+    body: '{"id": 14068, "text": "Updated Example", "meta": {}, "annotation_approver":
+      null, "comment_count": 0, "is_confirmed": false, "filename": "http://localhost:8000/media/",
+      "upload_name": ""}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '187'
+      Cookie:
+      - csrftoken=gJvGpE4U6m8bIfWDrit6SWSykfRZZEpM6NAl3u3Rxzw2pXDPALlLzdNGbiHydN08;
+        sessionid=hscb9julcs80kxu2dc8ey7xu91akns9k
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - gJvGpE4U6m8bIfWDrit6SWSykfRZZEpM6NAl3u3Rxzw2pXDPALlLzdNGbiHydN08
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: PUT
+    uri: http://localhost:8000/v1/projects/16/examples/14068
+  response:
+    body:
+      string: '{"id":14068,"filename":"http://localhost:8000/media/","meta":{},"annotation_approver":null,"comment_count":0,"text":"Updated
+        Example","is_confirmed":false,"upload_name":""}'
+    headers:
+      Allow:
+      - GET, PUT, PATCH, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '172'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Wed, 28 Sep 2022 05:40:12 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/repositories/fixtures/label/create.yaml b/tests/integration/repositories/fixtures/label/create.yaml
new file mode 100644
index 0000000..1ddd884
--- /dev/null
+++ b/tests/integration/repositories/fixtures/label/create.yaml
@@ -0,0 +1,56 @@
+interactions:
+- request:
+    body: '{"example": 14073, "prob": 0.0, "manual": false, "user": null, "label":
+      23}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '75'
+      Cookie:
+      - csrftoken=2l7fOzqiWuaMJnhgKOMfFFHnD5LiS4rrykhDCpq7yx6q9MsErDcSiyXI3VrKD9Xk;
+        sessionid=37zofuqk5a2vc105fcf4if0n0sjdzlay
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 2l7fOzqiWuaMJnhgKOMfFFHnD5LiS4rrykhDCpq7yx6q9MsErDcSiyXI3VrKD9Xk
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects/16/examples/14073/categories
+  response:
+    body:
+      string: '{"id":28,"prob":0.0,"user":1,"example":14073,"created_at":"2022-10-01T02:17:25.153190Z","updated_at":"2022-10-01T02:17:25.153211Z","label":23}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '142'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Sat, 01 Oct 2022 02:17:25 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+version: 1
diff --git a/tests/integration/repositories/fixtures/label/delete.yaml b/tests/integration/repositories/fixtures/label/delete.yaml
new file mode 100644
index 0000000..4abe1be
--- /dev/null
+++ b/tests/integration/repositories/fixtures/label/delete.yaml
@@ -0,0 +1,53 @@
+interactions:
+- request:
+    body: '{}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '2'
+      Cookie:
+      - csrftoken=2l7fOzqiWuaMJnhgKOMfFFHnD5LiS4rrykhDCpq7yx6q9MsErDcSiyXI3VrKD9Xk;
+        sessionid=37zofuqk5a2vc105fcf4if0n0sjdzlay
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 2l7fOzqiWuaMJnhgKOMfFFHnD5LiS4rrykhDCpq7yx6q9MsErDcSiyXI3VrKD9Xk
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: DELETE
+    uri: http://localhost:8000/v1/projects/16/examples/14073/categories/28
+  response:
+    body:
+      string: ''
+    headers:
+      Allow:
+      - GET, PUT, PATCH, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '0'
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Sat, 01 Oct 2022 02:17:25 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 204
+      message: No Content
+version: 1
diff --git a/tests/integration/repositories/fixtures/label/login.yaml b/tests/integration/repositories/fixtures/label/login.yaml
new file mode 100644
index 0000000..702da85
--- /dev/null
+++ b/tests/integration/repositories/fixtures/label/login.yaml
@@ -0,0 +1,55 @@
+interactions:
+- request:
+    body: '{"username": "admin", "password": "password"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '45'
+      User-Agent:
+      - python-requests/2.28.1
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/auth/login/
+  response:
+    body:
+      string: '{"key":"b37c59f6e0db6bf8a829858ce925b9c2b4d7b03d"}'
+    headers:
+      Allow:
+      - POST, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '50'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Sat, 01 Oct 2022 02:17:25 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Set-Cookie:
+      - csrftoken=2l7fOzqiWuaMJnhgKOMfFFHnD5LiS4rrykhDCpq7yx6q9MsErDcSiyXI3VrKD9Xk;
+        expires=Sat, 30 Sep 2023 02:17:25 GMT; Max-Age=31449600; Path=/; SameSite=Lax
+      - sessionid=37zofuqk5a2vc105fcf4if0n0sjdzlay; expires=Sat, 15 Oct 2022 02:17:25
+        GMT; HttpOnly; Max-Age=1209600; Path=/; SameSite=Lax
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/repositories/fixtures/label/update.yaml b/tests/integration/repositories/fixtures/label/update.yaml
new file mode 100644
index 0000000..6b0aa9c
--- /dev/null
+++ b/tests/integration/repositories/fixtures/label/update.yaml
@@ -0,0 +1,56 @@
+interactions:
+- request:
+    body: '{"id": 28, "example": 14073, "prob": 0.0, "manual": false, "user": 1, "label":
+      24}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '82'
+      Cookie:
+      - csrftoken=2l7fOzqiWuaMJnhgKOMfFFHnD5LiS4rrykhDCpq7yx6q9MsErDcSiyXI3VrKD9Xk;
+        sessionid=37zofuqk5a2vc105fcf4if0n0sjdzlay
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 2l7fOzqiWuaMJnhgKOMfFFHnD5LiS4rrykhDCpq7yx6q9MsErDcSiyXI3VrKD9Xk
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: PUT
+    uri: http://localhost:8000/v1/projects/16/examples/14073/categories/28
+  response:
+    body:
+      string: '{"id":28,"prob":0.0,"user":1,"example":14073,"created_at":"2022-10-01T02:17:25.153190Z","updated_at":"2022-10-01T02:17:25.225819Z","label":24}'
+    headers:
+      Allow:
+      - GET, PUT, PATCH, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '142'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Sat, 01 Oct 2022 02:17:25 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/repositories/fixtures/label_type/create.yaml b/tests/integration/repositories/fixtures/label_type/create.yaml
new file mode 100644
index 0000000..804471d
--- /dev/null
+++ b/tests/integration/repositories/fixtures/label_type/create.yaml
@@ -0,0 +1,56 @@
+interactions:
+- request:
+    body: '{"text": "Test Label Type", "prefix_key": null, "suffix_key": null, "background_color":
+      "#8f47de", "text_color": "#ffffff"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '123'
+      Cookie:
+      - csrftoken=z8s2Hr9yz2zf9fTCrkbykGBasMYCz0IxjGKJxBBFBJmpzvDKL23ECcNq25cuqzco;
+        sessionid=canl1n7tgn5jzhge9qmammz6x9wx4yxu
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - z8s2Hr9yz2zf9fTCrkbykGBasMYCz0IxjGKJxBBFBJmpzvDKL23ECcNq25cuqzco
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects/16/category-types
+  response:
+    body:
+      string: '{"id":22,"text":"Test Label Type","prefix_key":null,"suffix_key":null,"background_color":"#8f47de","text_color":"#ffffff"}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '122'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Thu, 29 Sep 2022 05:12:26 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+version: 1
diff --git a/tests/integration/repositories/fixtures/label_type/delete.yaml b/tests/integration/repositories/fixtures/label_type/delete.yaml
new file mode 100644
index 0000000..1a8b4c4
--- /dev/null
+++ b/tests/integration/repositories/fixtures/label_type/delete.yaml
@@ -0,0 +1,53 @@
+interactions:
+- request:
+    body: '{}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '2'
+      Cookie:
+      - csrftoken=z8s2Hr9yz2zf9fTCrkbykGBasMYCz0IxjGKJxBBFBJmpzvDKL23ECcNq25cuqzco;
+        sessionid=canl1n7tgn5jzhge9qmammz6x9wx4yxu
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - z8s2Hr9yz2zf9fTCrkbykGBasMYCz0IxjGKJxBBFBJmpzvDKL23ECcNq25cuqzco
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: DELETE
+    uri: http://localhost:8000/v1/projects/16/category-types/22
+  response:
+    body:
+      string: ''
+    headers:
+      Allow:
+      - GET, PUT, PATCH, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '0'
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Thu, 29 Sep 2022 05:12:29 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 204
+      message: No Content
+version: 1
diff --git a/tests/integration/repositories/fixtures/label_type/list.yaml b/tests/integration/repositories/fixtures/label_type/list.yaml
new file mode 100644
index 0000000..ee83c88
--- /dev/null
+++ b/tests/integration/repositories/fixtures/label_type/list.yaml
@@ -0,0 +1,53 @@
+interactions:
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=z8s2Hr9yz2zf9fTCrkbykGBasMYCz0IxjGKJxBBFBJmpzvDKL23ECcNq25cuqzco;
+        sessionid=canl1n7tgn5jzhge9qmammz6x9wx4yxu
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - z8s2Hr9yz2zf9fTCrkbykGBasMYCz0IxjGKJxBBFBJmpzvDKL23ECcNq25cuqzco
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/16/category-types
+  response:
+    body:
+      string: '[{"id":23,"text":"Dog","prefix_key":null,"suffix_key":"a","background_color":"#FF0000","text_color":"#ffffff"},{"id":24,"text":"Cat","prefix_key":null,"suffix_key":"c","background_color":"#FF0000","text_color":"#ffffff"}]'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '221'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Thu, 29 Sep 2022 06:36:54 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/repositories/fixtures/label_type/login.yaml b/tests/integration/repositories/fixtures/label_type/login.yaml
new file mode 100644
index 0000000..65f4413
--- /dev/null
+++ b/tests/integration/repositories/fixtures/label_type/login.yaml
@@ -0,0 +1,55 @@
+interactions:
+- request:
+    body: '{"username": "admin", "password": "password"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '45'
+      User-Agent:
+      - python-requests/2.28.1
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/auth/login/
+  response:
+    body:
+      string: '{"key":"b37c59f6e0db6bf8a829858ce925b9c2b4d7b03d"}'
+    headers:
+      Allow:
+      - POST, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '50'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Thu, 29 Sep 2022 05:12:26 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Set-Cookie:
+      - csrftoken=z8s2Hr9yz2zf9fTCrkbykGBasMYCz0IxjGKJxBBFBJmpzvDKL23ECcNq25cuqzco;
+        expires=Thu, 28 Sep 2023 05:12:26 GMT; Max-Age=31449600; Path=/; SameSite=Lax
+      - sessionid=canl1n7tgn5jzhge9qmammz6x9wx4yxu; expires=Thu, 13 Oct 2022 05:12:26
+        GMT; HttpOnly; Max-Age=1209600; Path=/; SameSite=Lax
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/repositories/fixtures/label_type/update.yaml b/tests/integration/repositories/fixtures/label_type/update.yaml
new file mode 100644
index 0000000..3f7aef7
--- /dev/null
+++ b/tests/integration/repositories/fixtures/label_type/update.yaml
@@ -0,0 +1,56 @@
+interactions:
+- request:
+    body: '{"id": 22, "text": "Updated Type", "prefix_key": null, "suffix_key": null,
+      "background_color": "#8f47de", "text_color": "#ffffff"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '130'
+      Cookie:
+      - csrftoken=z8s2Hr9yz2zf9fTCrkbykGBasMYCz0IxjGKJxBBFBJmpzvDKL23ECcNq25cuqzco;
+        sessionid=canl1n7tgn5jzhge9qmammz6x9wx4yxu
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - z8s2Hr9yz2zf9fTCrkbykGBasMYCz0IxjGKJxBBFBJmpzvDKL23ECcNq25cuqzco
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: PUT
+    uri: http://localhost:8000/v1/projects/16/category-types/22
+  response:
+    body:
+      string: '{"id":22,"text":"Updated Type","prefix_key":null,"suffix_key":null,"background_color":"#8f47de","text_color":"#ffffff"}'
+    headers:
+      Allow:
+      - GET, PUT, PATCH, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '119'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Thu, 29 Sep 2022 05:12:29 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/repositories/fixtures/label_type/upload.yaml b/tests/integration/repositories/fixtures/label_type/upload.yaml
new file mode 100644
index 0000000..aebd539
--- /dev/null
+++ b/tests/integration/repositories/fixtures/label_type/upload.yaml
@@ -0,0 +1,67 @@
+interactions:
+- request:
+    body: !!python/object/new:_io.BytesIO
+      state: !!python/tuple
+      - !!binary |
+        LS1iOWY5MTNhNjViYzM0N2EzYmFjMjYxNmFkYzJiYzg2ZQ0KQ29udGVudC1EaXNwb3NpdGlvbjog
+        Zm9ybS1kYXRhOyBuYW1lPSJmaWxlIjsgZmlsZW5hbWU9ImxhYmVscy5qc29uIg0KQ29udGVudC1U
+        eXBlOiBhcHBsaWNhdGlvbi9qc29uDQoNClsKICB7CiAgICAiaWQiOiA0NCwKICAgICJ0ZXh0Ijog
+        IkRvZyIsCiAgICAicHJlZml4X2tleSI6IG51bGwsCiAgICAic3VmZml4X2tleSI6ICJhIiwKICAg
+        ICJiYWNrZ3JvdW5kX2NvbG9yIjogIiNGRjAwMDAiLAogICAgInRleHRfY29sb3IiOiAiI2ZmZmZm
+        ZiIsCiAgICAidGFza190eXBlIjogIkNhdGVnb3J5IgogIH0sCiAgewogICAgImlkIjogNDUsCiAg
+        ICAidGV4dCI6ICJDYXQiLAogICAgInByZWZpeF9rZXkiOiBudWxsLAogICAgInN1ZmZpeF9rZXki
+        OiAiYyIsCiAgICAiYmFja2dyb3VuZF9jb2xvciI6ICIjRkYwMDAwIiwKICAgICJ0ZXh0X2NvbG9y
+        IjogIiNmZmZmZmYiLAogICAgInRhc2tfdHlwZSI6ICJTcGFuIgogIH0KXQoNCi0tYjlmOTEzYTY1
+        YmMzNDdhM2JhYzI2MTZhZGMyYmM4NmUtLQ0K
+      - 0
+      - null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '540'
+      Content-Type:
+      - multipart/form-data; boundary=b9f913a65bc347a3bac2616adc2bc86e
+      Cookie:
+      - csrftoken=z8s2Hr9yz2zf9fTCrkbykGBasMYCz0IxjGKJxBBFBJmpzvDKL23ECcNq25cuqzco;
+        sessionid=canl1n7tgn5jzhge9qmammz6x9wx4yxu
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - z8s2Hr9yz2zf9fTCrkbykGBasMYCz0IxjGKJxBBFBJmpzvDKL23ECcNq25cuqzco
+      accept:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects/16/category-type-upload
+  response:
+    body:
+      string: ''
+    headers:
+      Allow:
+      - POST, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '0'
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Thu, 29 Sep 2022 06:36:53 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+version: 1
diff --git a/tests/integration/repositories/fixtures/member/create.yaml b/tests/integration/repositories/fixtures/member/create.yaml
new file mode 100644
index 0000000..d0abca0
--- /dev/null
+++ b/tests/integration/repositories/fixtures/member/create.yaml
@@ -0,0 +1,106 @@
+interactions:
+- request:
+    body: '{"user": 3, "role": 1}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '22'
+      Cookie:
+      - csrftoken=aN3vvSOwjjR64tJzDgjAo8dj2M3H0A44WGpTgktwjAEQloAEOGweXlUeQT1qU6td;
+        sessionid=5cjypm6avynu40oob98ybbw5mb1d7xq3
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - aN3vvSOwjjR64tJzDgjAo8dj2M3H0A44WGpTgktwjAEQloAEOGweXlUeQT1qU6td
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects/16/members
+  response:
+    body:
+      string: '{"id":21,"user":3,"role":1,"username":"hironsan","rolename":"project_admin"}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '76'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Mon, 03 Oct 2022 08:25:05 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: '{"ids": [21]}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '13'
+      Cookie:
+      - csrftoken=aN3vvSOwjjR64tJzDgjAo8dj2M3H0A44WGpTgktwjAEQloAEOGweXlUeQT1qU6td;
+        sessionid=5cjypm6avynu40oob98ybbw5mb1d7xq3
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - aN3vvSOwjjR64tJzDgjAo8dj2M3H0A44WGpTgktwjAEQloAEOGweXlUeQT1qU6td
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: DELETE
+    uri: http://localhost:8000/v1/projects/16/members
+  response:
+    body:
+      string: ''
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '0'
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Mon, 03 Oct 2022 08:25:05 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 204
+      message: No Content
+version: 1
diff --git a/tests/integration/repositories/fixtures/member/login.yaml b/tests/integration/repositories/fixtures/member/login.yaml
new file mode 100644
index 0000000..ec208fc
--- /dev/null
+++ b/tests/integration/repositories/fixtures/member/login.yaml
@@ -0,0 +1,55 @@
+interactions:
+- request:
+    body: '{"username": "admin", "password": "password"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '45'
+      User-Agent:
+      - python-requests/2.28.1
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/auth/login/
+  response:
+    body:
+      string: '{"key":"b37c59f6e0db6bf8a829858ce925b9c2b4d7b03d"}'
+    headers:
+      Allow:
+      - POST, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '50'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Mon, 03 Oct 2022 08:25:05 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Set-Cookie:
+      - csrftoken=aN3vvSOwjjR64tJzDgjAo8dj2M3H0A44WGpTgktwjAEQloAEOGweXlUeQT1qU6td;
+        expires=Mon, 02 Oct 2023 08:25:05 GMT; Max-Age=31449600; Path=/; SameSite=Lax
+      - sessionid=5cjypm6avynu40oob98ybbw5mb1d7xq3; expires=Mon, 17 Oct 2022 08:25:05
+        GMT; HttpOnly; Max-Age=1209600; Path=/; SameSite=Lax
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/repositories/fixtures/member/update.yaml b/tests/integration/repositories/fixtures/member/update.yaml
new file mode 100644
index 0000000..e23901a
--- /dev/null
+++ b/tests/integration/repositories/fixtures/member/update.yaml
@@ -0,0 +1,159 @@
+interactions:
+- request:
+    body: '{"user": 3, "role": 1}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '22'
+      Cookie:
+      - csrftoken=aN3vvSOwjjR64tJzDgjAo8dj2M3H0A44WGpTgktwjAEQloAEOGweXlUeQT1qU6td;
+        sessionid=5cjypm6avynu40oob98ybbw5mb1d7xq3
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - aN3vvSOwjjR64tJzDgjAo8dj2M3H0A44WGpTgktwjAEQloAEOGweXlUeQT1qU6td
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects/16/members
+  response:
+    body:
+      string: '{"id":22,"user":3,"role":1,"username":"hironsan","rolename":"project_admin"}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '76'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Mon, 03 Oct 2022 08:25:05 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: '{"id": 22, "user": 3, "role": 2}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '32'
+      Cookie:
+      - csrftoken=aN3vvSOwjjR64tJzDgjAo8dj2M3H0A44WGpTgktwjAEQloAEOGweXlUeQT1qU6td;
+        sessionid=5cjypm6avynu40oob98ybbw5mb1d7xq3
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - aN3vvSOwjjR64tJzDgjAo8dj2M3H0A44WGpTgktwjAEQloAEOGweXlUeQT1qU6td
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: PUT
+    uri: http://localhost:8000/v1/projects/16/members/22
+  response:
+    body:
+      string: '{"id":22,"user":3,"role":2,"username":"hironsan","rolename":"annotator"}'
+    headers:
+      Allow:
+      - GET, PUT, PATCH, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '72'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Mon, 03 Oct 2022 08:25:05 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: '{"ids": [22]}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '13'
+      Cookie:
+      - csrftoken=aN3vvSOwjjR64tJzDgjAo8dj2M3H0A44WGpTgktwjAEQloAEOGweXlUeQT1qU6td;
+        sessionid=5cjypm6avynu40oob98ybbw5mb1d7xq3
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - aN3vvSOwjjR64tJzDgjAo8dj2M3H0A44WGpTgktwjAEQloAEOGweXlUeQT1qU6td
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: DELETE
+    uri: http://localhost:8000/v1/projects/16/members
+  response:
+    body:
+      string: ''
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '0'
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Mon, 03 Oct 2022 08:25:05 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 204
+      message: No Content
+version: 1
diff --git a/tests/integration/repositories/fixtures/metrics/category_distribution.yaml b/tests/integration/repositories/fixtures/metrics/category_distribution.yaml
new file mode 100644
index 0000000..42ee428
--- /dev/null
+++ b/tests/integration/repositories/fixtures/metrics/category_distribution.yaml
@@ -0,0 +1,53 @@
+interactions:
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8rof30s5feEJ46wjawl015jqli9KnihIKgCdxn7EwFgJCMwjHSuUg0Y97C4jgb9H;
+        sessionid=6v6atgbiq12zp4ohi20jjhp8cpcijgkk
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8rof30s5feEJ46wjawl015jqli9KnihIKgCdxn7EwFgJCMwjHSuUg0Y97C4jgb9H
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/16/metrics/category-distribution
+  response:
+    body:
+      string: '{"admin":{"Dog":0,"Cat":0}}'
+    headers:
+      Allow:
+      - GET, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '27'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 30 Sep 2022 02:56:25 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/repositories/fixtures/metrics/login.yaml b/tests/integration/repositories/fixtures/metrics/login.yaml
new file mode 100644
index 0000000..f9cba96
--- /dev/null
+++ b/tests/integration/repositories/fixtures/metrics/login.yaml
@@ -0,0 +1,55 @@
+interactions:
+- request:
+    body: '{"username": "admin", "password": "password"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '45'
+      User-Agent:
+      - python-requests/2.28.1
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/auth/login/
+  response:
+    body:
+      string: '{"key":"b37c59f6e0db6bf8a829858ce925b9c2b4d7b03d"}'
+    headers:
+      Allow:
+      - POST, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '50'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 30 Sep 2022 02:56:25 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Set-Cookie:
+      - csrftoken=8rof30s5feEJ46wjawl015jqli9KnihIKgCdxn7EwFgJCMwjHSuUg0Y97C4jgb9H;
+        expires=Fri, 29 Sep 2023 02:56:25 GMT; Max-Age=31449600; Path=/; SameSite=Lax
+      - sessionid=6v6atgbiq12zp4ohi20jjhp8cpcijgkk; expires=Fri, 14 Oct 2022 02:56:25
+        GMT; HttpOnly; Max-Age=1209600; Path=/; SameSite=Lax
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/repositories/fixtures/metrics/member_progress.yaml b/tests/integration/repositories/fixtures/metrics/member_progress.yaml
new file mode 100644
index 0000000..569a4cb
--- /dev/null
+++ b/tests/integration/repositories/fixtures/metrics/member_progress.yaml
@@ -0,0 +1,53 @@
+interactions:
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8rof30s5feEJ46wjawl015jqli9KnihIKgCdxn7EwFgJCMwjHSuUg0Y97C4jgb9H;
+        sessionid=6v6atgbiq12zp4ohi20jjhp8cpcijgkk
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8rof30s5feEJ46wjawl015jqli9KnihIKgCdxn7EwFgJCMwjHSuUg0Y97C4jgb9H
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/16/metrics/member-progress
+  response:
+    body:
+      string: '{"total":1,"progress":[{"user":"admin","done":0}]}'
+    headers:
+      Allow:
+      - GET, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '50'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 30 Sep 2022 02:56:25 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/repositories/fixtures/metrics/progress.yaml b/tests/integration/repositories/fixtures/metrics/progress.yaml
new file mode 100644
index 0000000..e2fb29f
--- /dev/null
+++ b/tests/integration/repositories/fixtures/metrics/progress.yaml
@@ -0,0 +1,53 @@
+interactions:
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8rof30s5feEJ46wjawl015jqli9KnihIKgCdxn7EwFgJCMwjHSuUg0Y97C4jgb9H;
+        sessionid=6v6atgbiq12zp4ohi20jjhp8cpcijgkk
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8rof30s5feEJ46wjawl015jqli9KnihIKgCdxn7EwFgJCMwjHSuUg0Y97C4jgb9H
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/16/metrics/progress
+  response:
+    body:
+      string: '{"total":1,"remaining":1,"complete":0}'
+    headers:
+      Allow:
+      - GET, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '38'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 30 Sep 2022 02:56:25 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/repositories/fixtures/role/list.yaml b/tests/integration/repositories/fixtures/role/list.yaml
new file mode 100644
index 0000000..e3c46eb
--- /dev/null
+++ b/tests/integration/repositories/fixtures/role/list.yaml
@@ -0,0 +1,53 @@
+interactions:
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=vh5XGrjfK7pD4OA5tzFEqOuJT0snaPZsVzmeOJWfugZLP9DBfPynANfxscFlEb85;
+        sessionid=n75d5v6c8a6txsvwa47j22nlspgmr91j
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - vh5XGrjfK7pD4OA5tzFEqOuJT0snaPZsVzmeOJWfugZLP9DBfPynANfxscFlEb85
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/roles
+  response:
+    body:
+      string: '[{"id":1,"name":"project_admin"},{"id":2,"name":"annotator"},{"id":3,"name":"annotation_approver"}]'
+    headers:
+      Allow:
+      - GET, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '99'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Thu, 29 Sep 2022 23:00:18 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/repositories/fixtures/role/login.yaml b/tests/integration/repositories/fixtures/role/login.yaml
new file mode 100644
index 0000000..5e85903
--- /dev/null
+++ b/tests/integration/repositories/fixtures/role/login.yaml
@@ -0,0 +1,55 @@
+interactions:
+- request:
+    body: '{"username": "admin", "password": "password"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '45'
+      User-Agent:
+      - python-requests/2.28.1
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/auth/login/
+  response:
+    body:
+      string: '{"key":"b37c59f6e0db6bf8a829858ce925b9c2b4d7b03d"}'
+    headers:
+      Allow:
+      - POST, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '50'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Thu, 29 Sep 2022 23:00:10 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Set-Cookie:
+      - csrftoken=vh5XGrjfK7pD4OA5tzFEqOuJT0snaPZsVzmeOJWfugZLP9DBfPynANfxscFlEb85;
+        expires=Thu, 28 Sep 2023 23:00:10 GMT; Max-Age=31449600; Path=/; SameSite=Lax
+      - sessionid=n75d5v6c8a6txsvwa47j22nlspgmr91j; expires=Thu, 13 Oct 2022 23:00:10
+        GMT; HttpOnly; Max-Age=1209600; Path=/; SameSite=Lax
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/repositories/fixtures/task_status/list.yaml b/tests/integration/repositories/fixtures/task_status/list.yaml
new file mode 100644
index 0000000..58afa48
--- /dev/null
+++ b/tests/integration/repositories/fixtures/task_status/list.yaml
@@ -0,0 +1,53 @@
+interactions:
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=KZHBtmhJrSDkeSD6fZpcIYWikEbUb1sfCl1PkABJQ1exDnsPKkJnqQozYd7EQdYx;
+        sessionid=7n5q9r3zlv6v18e7jwetnk9yb2ihhbfj
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - KZHBtmhJrSDkeSD6fZpcIYWikEbUb1sfCl1PkABJQ1exDnsPKkJnqQozYd7EQdYx
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/tasks/status/2a49f2cf-5508-45d3-b75e-d1dd2489aaf8
+  response:
+    body:
+      string: '{"ready":true,"result":{"error":[]},"error":null}'
+    headers:
+      Allow:
+      - GET, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '49'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Thu, 29 Sep 2022 23:35:54 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/repositories/fixtures/task_status/login.yaml b/tests/integration/repositories/fixtures/task_status/login.yaml
new file mode 100644
index 0000000..61e0836
--- /dev/null
+++ b/tests/integration/repositories/fixtures/task_status/login.yaml
@@ -0,0 +1,55 @@
+interactions:
+- request:
+    body: '{"username": "admin", "password": "password"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '45'
+      User-Agent:
+      - python-requests/2.28.1
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/auth/login/
+  response:
+    body:
+      string: '{"key":"b37c59f6e0db6bf8a829858ce925b9c2b4d7b03d"}'
+    headers:
+      Allow:
+      - POST, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '50'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Thu, 29 Sep 2022 23:35:54 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Set-Cookie:
+      - csrftoken=KZHBtmhJrSDkeSD6fZpcIYWikEbUb1sfCl1PkABJQ1exDnsPKkJnqQozYd7EQdYx;
+        expires=Thu, 28 Sep 2023 23:35:54 GMT; Max-Age=31449600; Path=/; SameSite=Lax
+      - sessionid=7n5q9r3zlv6v18e7jwetnk9yb2ihhbfj; expires=Thu, 13 Oct 2022 23:35:54
+        GMT; HttpOnly; Max-Age=1209600; Path=/; SameSite=Lax
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/repositories/fixtures/user/get_profile.yaml b/tests/integration/repositories/fixtures/user/get_profile.yaml
new file mode 100644
index 0000000..be5d8b5
--- /dev/null
+++ b/tests/integration/repositories/fixtures/user/get_profile.yaml
@@ -0,0 +1,53 @@
+interactions:
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=ZFXoFhJy3Kg5cXvleNqF3asZyqemiNoTrtQ8xFvngNrfQ8GVSc8rAoGVYAP9qHI8;
+        sessionid=1tcqome0opyrqamkcagwibe630ok9cb3
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - ZFXoFhJy3Kg5cXvleNqF3asZyqemiNoTrtQ8xFvngNrfQ8GVSc8rAoGVYAP9qHI8
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/me
+  response:
+    body:
+      string: '{"id":1,"username":"admin","is_superuser":true,"is_staff":true}'
+    headers:
+      Allow:
+      - GET, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '63'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 30 Sep 2022 00:10:34 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/repositories/fixtures/user/list.yaml b/tests/integration/repositories/fixtures/user/list.yaml
new file mode 100644
index 0000000..d9caea6
--- /dev/null
+++ b/tests/integration/repositories/fixtures/user/list.yaml
@@ -0,0 +1,53 @@
+interactions:
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=ZFXoFhJy3Kg5cXvleNqF3asZyqemiNoTrtQ8xFvngNrfQ8GVSc8rAoGVYAP9qHI8;
+        sessionid=1tcqome0opyrqamkcagwibe630ok9cb3
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - ZFXoFhJy3Kg5cXvleNqF3asZyqemiNoTrtQ8xFvngNrfQ8GVSc8rAoGVYAP9qHI8
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/users?q=
+  response:
+    body:
+      string: '[{"id":1,"username":"admin","is_superuser":true,"is_staff":true}]'
+    headers:
+      Allow:
+      - GET, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '65'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 30 Sep 2022 00:10:34 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/repositories/fixtures/user/login.yaml b/tests/integration/repositories/fixtures/user/login.yaml
new file mode 100644
index 0000000..8d78519
--- /dev/null
+++ b/tests/integration/repositories/fixtures/user/login.yaml
@@ -0,0 +1,55 @@
+interactions:
+- request:
+    body: '{"username": "admin", "password": "password"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '45'
+      User-Agent:
+      - python-requests/2.28.1
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/auth/login/
+  response:
+    body:
+      string: '{"key":"b37c59f6e0db6bf8a829858ce925b9c2b4d7b03d"}'
+    headers:
+      Allow:
+      - POST, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '50'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 30 Sep 2022 00:10:33 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Set-Cookie:
+      - csrftoken=ZFXoFhJy3Kg5cXvleNqF3asZyqemiNoTrtQ8xFvngNrfQ8GVSc8rAoGVYAP9qHI8;
+        expires=Fri, 29 Sep 2023 00:10:33 GMT; Max-Age=31449600; Path=/; SameSite=Lax
+      - sessionid=1tcqome0opyrqamkcagwibe630ok9cb3; expires=Fri, 14 Oct 2022 00:10:33
+        GMT; HttpOnly; Max-Age=1209600; Path=/; SameSite=Lax
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/repositories/test_comment.py b/tests/integration/repositories/test_comment.py
new file mode 100644
index 0000000..a78821a
--- /dev/null
+++ b/tests/integration/repositories/test_comment.py
@@ -0,0 +1,44 @@
+import pathlib
+
+import pytest
+import vcr
+
+from doccano_client.models.comment import Comment
+from doccano_client.repositories.base import BaseRepository
+from doccano_client.repositories.comment import CommentRepository
+from tests.conftest import repository_fixtures
+
+
+@pytest.fixture
+def comment():
+    return Comment(text="Test comment", example=14069)
+
+
+class TestCommentRepository:
+    @classmethod
+    def setup_class(cls):
+        with vcr.use_cassette(str(repository_fixtures / "comment/login.yaml"), mode="once"):
+            client = BaseRepository("http://localhost:8000")
+            client.login(username="admin", password="password")
+        cls.client = CommentRepository(client)
+        cls.project_id = 16
+
+    def test_create(self, comment):
+        with vcr.use_cassette(str(repository_fixtures / "comment/create.yaml"), mode="once"):
+            response = self.client.create(self.project_id, comment)
+        assert response.text == comment.text
+
+    def test_update(self, comment):
+        with vcr.use_cassette(str(repository_fixtures / "comment/create.yaml"), mode="once"):
+            response = self.client.create(self.project_id, comment)
+        with vcr.use_cassette(str(repository_fixtures / "comment/update.yaml"), mode="once"):
+            response.text = "Updated Type"
+            updated = self.client.update(self.project_id, response)
+        assert updated.text == "Updated Type"
+        assert updated.id == response.id
+
+    def test_delete(self, comment):
+        with vcr.use_cassette(str(repository_fixtures / "comment/create.yaml"), mode="once"):
+            response = self.client.create(self.project_id, comment)
+        with vcr.use_cassette(str(repository_fixtures / "comment/delete.yaml"), mode="once"):
+            self.client.delete(self.project_id, response.id)
diff --git a/tests/integration/repositories/test_data_download.py b/tests/integration/repositories/test_data_download.py
new file mode 100644
index 0000000..3f327d5
--- /dev/null
+++ b/tests/integration/repositories/test_data_download.py
@@ -0,0 +1,41 @@
+import time
+
+import vcr
+
+from doccano_client.models.data_download import Option
+from doccano_client.repositories.base import BaseRepository
+from doccano_client.repositories.data_download import DataDownloadRepository
+from tests.conftest import repository_fixtures
+
+
+class TestDataDownloadRepository:
+    @classmethod
+    def setup_class(cls):
+        with vcr.use_cassette(str(repository_fixtures / "data_download/login.yaml"), mode="once"):
+            client = BaseRepository("http://localhost:8000")
+            client.login(username="admin", password="password")
+        cls.client = DataDownloadRepository(client)
+        cls.project_id = 16
+
+    def test_list_options(self):
+        with vcr.use_cassette(str(repository_fixtures / "data_download/options.yaml"), mode="once"):
+            response = self.client.list_options(self.project_id)
+        assert len(response) > 0
+        assert all(isinstance(option, Option) for option in response)
+
+    def test_schedule_download(self):
+        with vcr.use_cassette(str(repository_fixtures / "data_download/schedule_download.yaml"), mode="once"):
+            option = Option(name="JSONL")
+            task_id = self.client.schedule_download(self.project_id, option)
+        assert task_id is not None
+        assert isinstance(task_id, str)
+
+    def test_download(self):
+        with vcr.use_cassette(str(repository_fixtures / "data_download/download.yaml"), mode="once"):
+            option = Option(name="JSONL")
+            task_id = self.client.schedule_download(self.project_id, option)
+            time.sleep(1)  # lazy work
+            file = self.client.download(self.project_id, task_id)
+            assert file.exists()
+            assert file.stat().st_size > 0
+            file.unlink()
diff --git a/tests/integration/repositories/test_data_upload.py b/tests/integration/repositories/test_data_upload.py
new file mode 100644
index 0000000..1ce88cc
--- /dev/null
+++ b/tests/integration/repositories/test_data_upload.py
@@ -0,0 +1,43 @@
+import pathlib
+
+import vcr
+
+from doccano_client.models.data_upload import Option
+from doccano_client.repositories.base import BaseRepository
+from doccano_client.repositories.data_upload import DataUploadRepository
+from tests.conftest import repository_fixtures
+
+
+class TestDataUploadRepository:
+    @classmethod
+    def setup_class(cls):
+        with vcr.use_cassette(str(repository_fixtures / "data_upload/login.yaml"), mode="once"):
+            client = BaseRepository("http://localhost:8000")
+            client.login(username="admin", password="password")
+        cls.client = DataUploadRepository(client)
+        cls.project_id = 16
+        cls.file_path = pathlib.Path(__file__).parent.parent / "data/classification.jsonl"
+
+    def test_list_options(self):
+        with vcr.use_cassette(str(repository_fixtures / "data_upload/options.yaml"), mode="once"):
+            response = self.client.list_options(self.project_id)
+        assert len(response) > 0
+        assert all(isinstance(option, Option) for option in response)
+
+    def test_upload(self):
+        with vcr.use_cassette(str(repository_fixtures / "data_upload/upload.yaml"), mode="once"):
+            upload_id = self.client.upload(self.file_path)
+        assert upload_id is not None
+        assert isinstance(upload_id, str)
+
+    def test_delete(self):
+        with vcr.use_cassette(str(repository_fixtures / "data_upload/delete.yaml"), mode="once"):
+            upload_id = self.client.upload(self.file_path)
+            self.client.delete(upload_id)
+
+    def test_ingest(self):
+        with vcr.use_cassette(str(repository_fixtures / "data_upload/ingest.yaml"), mode="once"):
+            upload_id = self.client.upload(self.file_path)
+            task_id = self.client.ingest(self.project_id, [upload_id], task="DocumentClassification", format="JSONL")
+        assert task_id is not None
+        assert isinstance(task_id, str)
diff --git a/tests/integration/repositories/test_example.py b/tests/integration/repositories/test_example.py
new file mode 100644
index 0000000..4073553
--- /dev/null
+++ b/tests/integration/repositories/test_example.py
@@ -0,0 +1,42 @@
+import pytest
+import vcr
+
+from doccano_client.models.example import Example
+from doccano_client.repositories.base import BaseRepository
+from doccano_client.repositories.example import ExampleRepository
+from tests.conftest import repository_fixtures
+
+
+@pytest.fixture
+def example():
+    return Example(text="Test Example")
+
+
+class TestExampleRepository:
+    @classmethod
+    def setup_class(cls):
+        with vcr.use_cassette(str(repository_fixtures / "example/login.yaml"), mode="once"):
+            client = BaseRepository("http://localhost:8000")
+            client.login(username="admin", password="password")
+        cls.client = ExampleRepository(client)
+        cls.project_id = 16
+
+    def test_create(self, example):
+        with vcr.use_cassette(str(repository_fixtures / "example/create.yaml"), mode="once"):
+            response = self.client.create(self.project_id, example)
+        assert response.text == example.text
+
+    def test_update(self, example):
+        with vcr.use_cassette(str(repository_fixtures / "example/create.yaml"), mode="once"):
+            response = self.client.create(self.project_id, example)
+        with vcr.use_cassette(str(repository_fixtures / "example/update.yaml"), mode="once"):
+            response.text = "Updated Example"
+            updated = self.client.update(self.project_id, response)
+        assert updated.text == "Updated Example"
+        assert updated.id == response.id
+
+    def test_delete(self, example):
+        with vcr.use_cassette(str(repository_fixtures / "example/create.yaml"), mode="once"):
+            response = self.client.create(self.project_id, example)
+        with vcr.use_cassette(str(repository_fixtures / "example/delete.yaml"), mode="once"):
+            self.client.delete(self.project_id, response.id)
diff --git a/tests/integration/repositories/test_label.py b/tests/integration/repositories/test_label.py
new file mode 100644
index 0000000..43b7ce6
--- /dev/null
+++ b/tests/integration/repositories/test_label.py
@@ -0,0 +1,42 @@
+import pytest
+import vcr
+
+from doccano_client.models.label import Category
+from doccano_client.repositories.base import BaseRepository
+from doccano_client.repositories.label import CategoryRepository
+from tests.conftest import repository_fixtures
+
+
+@pytest.fixture
+def category():
+    return Category(example=14073, label=23)
+
+
+class TestCategoryRepository:
+    @classmethod
+    def setup_class(cls):
+        with vcr.use_cassette(str(repository_fixtures / "label/login.yaml"), mode="once"):
+            client = BaseRepository("http://localhost:8000")
+            client.login(username="admin", password="password")
+        cls.client = CategoryRepository(client)
+        cls.project_id = 16
+
+    def test_create(self, category):
+        with vcr.use_cassette(str(repository_fixtures / "label/create.yaml"), mode="once"):
+            response = self.client.create(self.project_id, category)
+        assert response.label == category.label
+
+    def test_update(self, category):
+        with vcr.use_cassette(str(repository_fixtures / "label/create.yaml"), mode="once"):
+            response = self.client.create(self.project_id, category)
+        with vcr.use_cassette(str(repository_fixtures / "label/update.yaml"), mode="once"):
+            response.label = 24
+            updated = self.client.update(self.project_id, response)
+        assert updated.label == response.label
+        assert updated.id == response.id
+
+    def test_delete(self, category):
+        with vcr.use_cassette(str(repository_fixtures / "label/create.yaml"), mode="once"):
+            response = self.client.create(self.project_id, category)
+        with vcr.use_cassette(str(repository_fixtures / "label/delete.yaml"), mode="once"):
+            self.client.delete(self.project_id, response)
diff --git a/tests/integration/repositories/test_label_types.py b/tests/integration/repositories/test_label_types.py
new file mode 100644
index 0000000..fe55860
--- /dev/null
+++ b/tests/integration/repositories/test_label_types.py
@@ -0,0 +1,52 @@
+import pathlib
+
+import pytest
+import vcr
+
+from doccano_client.models.label_type import LabelType
+from doccano_client.repositories.base import BaseRepository
+from doccano_client.repositories.label_type import CategoryTypeRepository
+from tests.conftest import repository_fixtures
+
+
+@pytest.fixture
+def label_type():
+    return LabelType(text="Test Label Type")
+
+
+class TestCategoryTypeRepository:
+    @classmethod
+    def setup_class(cls):
+        with vcr.use_cassette(str(repository_fixtures / "label_type/login.yaml"), mode="once"):
+            client = BaseRepository("http://localhost:8000")
+            client.login(username="admin", password="password")
+        cls.client = CategoryTypeRepository(client)
+        cls.project_id = 16
+
+    def test_create(self, label_type):
+        with vcr.use_cassette(str(repository_fixtures / "label_type/create.yaml"), mode="once"):
+            response = self.client.create(self.project_id, label_type)
+        assert response.text == label_type.text
+
+    def test_update(self, label_type):
+        with vcr.use_cassette(str(repository_fixtures / "label_type/create.yaml"), mode="once"):
+            response = self.client.create(self.project_id, label_type)
+        with vcr.use_cassette(str(repository_fixtures / "label_type/update.yaml"), mode="once"):
+            response.text = "Updated Type"
+            updated = self.client.update(self.project_id, response)
+        assert updated.text == "Updated Type"
+        assert updated.id == response.id
+
+    def test_delete(self, label_type):
+        with vcr.use_cassette(str(repository_fixtures / "label_type/create.yaml"), mode="once"):
+            response = self.client.create(self.project_id, label_type)
+        with vcr.use_cassette(str(repository_fixtures / "label_type/delete.yaml"), mode="once"):
+            self.client.delete(self.project_id, response.id)
+
+    def test_upload(self):
+        file_path = pathlib.Path(__file__).parent.parent / "data/labels.json"
+        with vcr.use_cassette(str(repository_fixtures / "label_type/upload.yaml"), mode="once"):
+            self.client.upload(self.project_id, file_path)
+        with vcr.use_cassette(str(repository_fixtures / "label_type/list.yaml"), mode="once"):
+            label_types = self.client.list(self.project_id)
+            assert len(label_types) == 2
diff --git a/tests/integration/repositories/test_member.py b/tests/integration/repositories/test_member.py
new file mode 100644
index 0000000..476607d
--- /dev/null
+++ b/tests/integration/repositories/test_member.py
@@ -0,0 +1,39 @@
+import pathlib
+
+import pytest
+import vcr
+
+from doccano_client.models.member import Member
+from doccano_client.repositories.base import BaseRepository
+from doccano_client.repositories.member import MemberRepository
+from tests.conftest import repository_fixtures
+
+
+@pytest.fixture
+def member():
+    return Member(user=3, role=1)
+
+
+class TestMemberRepository:
+    @classmethod
+    def setup_class(cls):
+        with vcr.use_cassette(str(repository_fixtures / "member/login.yaml"), mode="once"):
+            client = BaseRepository("http://localhost:8000")
+            client.login(username="admin", password="password")
+        cls.client = MemberRepository(client)
+        cls.project_id = 16
+
+    def test_create(self, member):
+        with vcr.use_cassette(str(repository_fixtures / "member/create.yaml"), mode="once"):
+            response = self.client.create(self.project_id, member)
+            self.client.delete(self.project_id, response)
+        assert response.role == member.role
+
+    def test_update(self, member):
+        with vcr.use_cassette(str(repository_fixtures / "member/update.yaml"), mode="once"):
+            response = self.client.create(self.project_id, member)
+            response.role = 2
+            updated = self.client.update(self.project_id, response)
+            self.client.delete(self.project_id, response)
+        assert updated.role == 2
+        assert updated.id == response.id
diff --git a/tests/integration/repositories/test_metrics.py b/tests/integration/repositories/test_metrics.py
new file mode 100644
index 0000000..12bac80
--- /dev/null
+++ b/tests/integration/repositories/test_metrics.py
@@ -0,0 +1,31 @@
+import vcr
+
+from doccano_client.models.metrics import LabelDistribution, MemberProgress, Progress
+from doccano_client.repositories.base import BaseRepository
+from doccano_client.repositories.metrics import MetricsRepository
+from tests.conftest import repository_fixtures
+
+
+class TestMetricsRepository:
+    @classmethod
+    def setup_class(cls):
+        with vcr.use_cassette(str(repository_fixtures / "metrics/login.yaml"), mode="once"):
+            client = BaseRepository("http://localhost:8000")
+            client.login(username="admin", password="password")
+        cls.client = MetricsRepository(client)
+        cls.project_id = 16
+
+    def test_get_progress(self):
+        with vcr.use_cassette(str(repository_fixtures / "metrics/progress.yaml"), mode="once"):
+            response = self.client.get_progress(self.project_id)
+        assert isinstance(response, Progress)
+
+    def test_get_member_progress(self):
+        with vcr.use_cassette(str(repository_fixtures / "metrics/member_progress.yaml"), mode="once"):
+            response = self.client.get_members_progress(self.project_id)
+        assert all(isinstance(progress, MemberProgress) for progress in response)
+
+    def test_get_category_distribution(self):
+        with vcr.use_cassette(str(repository_fixtures / "metrics/category_distribution.yaml"), mode="once"):
+            response = self.client.get_category_distribution(self.project_id)
+        assert all(isinstance(distribution, LabelDistribution) for distribution in response)
diff --git a/tests/integration/repositories/test_project.py b/tests/integration/repositories/test_project.py
new file mode 100644
index 0000000..683760d
--- /dev/null
+++ b/tests/integration/repositories/test_project.py
@@ -0,0 +1,48 @@
+from unittest.mock import MagicMock
+
+import pytest
+
+from doccano_client.models.project import Project
+from doccano_client.repositories.project import ProjectRepository
+
+
+@pytest.fixture
+def response():
+    return {
+        "id": 1,
+        "name": "Test Project",
+        "description": "Test project",
+        "project_type": "SequenceLabeling",
+    }
+
+
+class TestProjectRepository:
+    def test_find_by_id(self, response):
+        client = MagicMock()
+        client.get.return_value.json.return_value = response
+        project_client = ProjectRepository(client)
+        project = project_client.find_by_id(1)
+        assert project.id == 1
+
+    def test_create(self, response):
+        client = MagicMock()
+        client.post.return_value.json.return_value = response
+        project_client = ProjectRepository(client)
+        project = project_client.create(Project.parse_obj(response))
+        assert project.id == 1
+
+    def test_update(self, response):
+        client = MagicMock()
+        client.put.return_value.json.return_value = response.copy()
+        project_client = ProjectRepository(client)
+        response["name"] = "Updated Project"
+        project = Project.parse_obj(response)
+        project = project_client.update(project)
+        assert project.id == 1
+        assert project.name == "Test Project"
+
+    def test_delete(self):
+        client = MagicMock()
+        project_client = ProjectRepository(client)
+        project_client.delete(1)
+        client.delete.assert_called_once_with("projects/1")
diff --git a/tests/integration/repositories/test_role.py b/tests/integration/repositories/test_role.py
new file mode 100644
index 0000000..463f1fc
--- /dev/null
+++ b/tests/integration/repositories/test_role.py
@@ -0,0 +1,19 @@
+import vcr
+
+from doccano_client.repositories.base import BaseRepository
+from doccano_client.repositories.role import RoleRepository
+from tests.conftest import repository_fixtures
+
+
+class TestRoleRepository:
+    @classmethod
+    def setup_class(cls):
+        with vcr.use_cassette(str(repository_fixtures / "role/login.yaml"), mode="once"):
+            client = BaseRepository("http://localhost:8000")
+            client.login(username="admin", password="password")
+        cls.client = RoleRepository(client)
+
+    def test_list(self):
+        with vcr.use_cassette(str(repository_fixtures / "role/list.yaml"), mode="once"):
+            roles = self.client.list()
+            assert len(roles) == 3
diff --git a/tests/integration/repositories/test_task_status.py b/tests/integration/repositories/test_task_status.py
new file mode 100644
index 0000000..53bc1a5
--- /dev/null
+++ b/tests/integration/repositories/test_task_status.py
@@ -0,0 +1,19 @@
+import vcr
+
+from doccano_client.repositories.base import BaseRepository
+from doccano_client.repositories.task_status import TaskStatusRepository
+from tests.conftest import repository_fixtures
+
+
+class TestTaskStatusRepository:
+    @classmethod
+    def setup_class(cls):
+        with vcr.use_cassette(str(repository_fixtures / "task_status/login.yaml"), mode="once"):
+            client = BaseRepository("http://localhost:8000")
+            client.login(username="admin", password="password")
+        cls.client = TaskStatusRepository(client)
+
+    def test_get(self):
+        with vcr.use_cassette(str(repository_fixtures / "task_status/list.yaml"), mode="once"):
+            task_id = "2a49f2cf-5508-45d3-b75e-d1dd2489aaf8"
+            self.client.get(task_id)
diff --git a/tests/integration/repositories/test_user.py b/tests/integration/repositories/test_user.py
new file mode 100644
index 0000000..dbcb920
--- /dev/null
+++ b/tests/integration/repositories/test_user.py
@@ -0,0 +1,25 @@
+import vcr
+
+from doccano_client.models.user import User
+from doccano_client.repositories.base import BaseRepository
+from doccano_client.repositories.user import UserRepository
+from tests.conftest import repository_fixtures
+
+
+class TestUserRepository:
+    @classmethod
+    def setup_class(cls):
+        with vcr.use_cassette(str(repository_fixtures / "user/login.yaml"), mode="once"):
+            client = BaseRepository("http://localhost:8000")
+            client.login(username="admin", password="password")
+        cls.client = UserRepository(client)
+
+    def test_get_profile(self):
+        with vcr.use_cassette(str(repository_fixtures / "user/get_profile.yaml"), mode="once"):
+            user = self.client.get_profile()
+            assert user.username == "admin"
+
+    def test_list(self):
+        with vcr.use_cassette(str(repository_fixtures / "user/list.yaml"), mode="once"):
+            users = self.client.list()
+            assert all(isinstance(user, User) for user in users)
diff --git a/tests/integration/usecase/__init__.py b/tests/integration/usecase/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/integration/usecase/fixtures/comment/create.yaml b/tests/integration/usecase/fixtures/comment/create.yaml
new file mode 100644
index 0000000..78f6357
--- /dev/null
+++ b/tests/integration/usecase/fixtures/comment/create.yaml
@@ -0,0 +1,165 @@
+interactions:
+- request:
+    body: '{"name": "test", "description": "test", "guideline": "", "project_type":
+      "DocumentClassification", "random_order": false, "collaborative_annotation":
+      false, "single_class_classification": false, "allow_overlapping": false, "grapheme_mode":
+      false, "use_relation": false, "tags": [], "resourcetype": "TextClassificationProject"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '326'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects
+  response:
+    body:
+      string: '{"id":113,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T04:13:52.087574Z","updated_at":"2022-10-07T04:13:52.087604Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '472'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 04:13:52 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: '{"text": "test", "meta": {}, "annotation_approver": null, "comment_count":
+      0, "is_confirmed": false, "filename": "", "upload_name": ""}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '135'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects/113/examples
+  response:
+    body:
+      string: '{"id":14123,"filename":"http://localhost:8000/media/","meta":{},"annotation_approver":null,"comment_count":0,"text":"test","is_confirmed":false,"upload_name":""}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '161'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 04:13:52 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: '{"text": "test", "user": null, "username": null, "created_at": null}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '68'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects/113/comments?example=14123
+  response:
+    body:
+      string: '{"id":3,"user":1,"username":"admin","example":"14123","text":"test","created_at":"2022-10-07T04:13:52.291128Z"}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '111'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 04:13:52 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+version: 1
diff --git a/tests/integration/usecase/fixtures/comment/delete.yaml b/tests/integration/usecase/fixtures/comment/delete.yaml
new file mode 100644
index 0000000..6f280c1
--- /dev/null
+++ b/tests/integration/usecase/fixtures/comment/delete.yaml
@@ -0,0 +1,267 @@
+interactions:
+- request:
+    body: '{"name": "test", "description": "test", "guideline": "", "project_type":
+      "DocumentClassification", "random_order": false, "collaborative_annotation":
+      false, "single_class_classification": false, "allow_overlapping": false, "grapheme_mode":
+      false, "use_relation": false, "tags": [], "resourcetype": "TextClassificationProject"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '326'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects
+  response:
+    body:
+      string: '{"id":115,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T04:13:52.483137Z","updated_at":"2022-10-07T04:13:52.483169Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '472'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 04:13:52 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: '{"text": "test", "meta": {}, "annotation_approver": null, "comment_count":
+      0, "is_confirmed": false, "filename": "", "upload_name": ""}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '135'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects/115/examples
+  response:
+    body:
+      string: '{"id":14125,"filename":"http://localhost:8000/media/","meta":{},"annotation_approver":null,"comment_count":0,"text":"test","is_confirmed":false,"upload_name":""}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '161'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 04:13:52 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: '{"text": "test", "user": null, "username": null, "created_at": null}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '68'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects/115/comments?example=14125
+  response:
+    body:
+      string: '{"id":5,"user":1,"username":"admin","example":"14125","text":"test","created_at":"2022-10-07T04:13:52.538141Z"}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '111'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 04:13:52 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '0'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: DELETE
+    uri: http://localhost:8000/v1/projects/115/comments/5
+  response:
+    body:
+      string: ''
+    headers:
+      Allow:
+      - GET, PUT, PATCH, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '0'
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 04:13:52 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 204
+      message: No Content
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/115/comments/5
+  response:
+    body:
+      string: '{"detail":"Not found."}'
+    headers:
+      Allow:
+      - GET, PUT, PATCH, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '23'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 04:13:52 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 404
+      message: Not Found
+version: 1
diff --git a/tests/integration/usecase/fixtures/comment/update.yaml b/tests/integration/usecase/fixtures/comment/update.yaml
new file mode 100644
index 0000000..fa399a1
--- /dev/null
+++ b/tests/integration/usecase/fixtures/comment/update.yaml
@@ -0,0 +1,270 @@
+interactions:
+- request:
+    body: '{"name": "test", "description": "test", "guideline": "", "project_type":
+      "DocumentClassification", "random_order": false, "collaborative_annotation":
+      false, "single_class_classification": false, "allow_overlapping": false, "grapheme_mode":
+      false, "use_relation": false, "tags": [], "resourcetype": "TextClassificationProject"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '326'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects
+  response:
+    body:
+      string: '{"id":114,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T04:13:52.383597Z","updated_at":"2022-10-07T04:13:52.383621Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '472'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 04:13:52 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: '{"text": "test", "meta": {}, "annotation_approver": null, "comment_count":
+      0, "is_confirmed": false, "filename": "", "upload_name": ""}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '135'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects/114/examples
+  response:
+    body:
+      string: '{"id":14124,"filename":"http://localhost:8000/media/","meta":{},"annotation_approver":null,"comment_count":0,"text":"test","is_confirmed":false,"upload_name":""}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '161'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 04:13:52 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: '{"text": "test", "user": null, "username": null, "created_at": null}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '68'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects/114/comments?example=14124
+  response:
+    body:
+      string: '{"id":4,"user":1,"username":"admin","example":"14124","text":"test","created_at":"2022-10-07T04:13:52.442240Z"}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '111'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 04:13:52 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/114/comments/4
+  response:
+    body:
+      string: '{"id":4,"user":1,"username":"admin","example":14124,"text":"test","created_at":"2022-10-07T04:13:52.442240Z"}'
+    headers:
+      Allow:
+      - GET, PUT, PATCH, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '109'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 04:13:52 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: '{"id": 4, "text": "test2", "example": 14124, "user": null, "username":
+      null, "created_at": null}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '96'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: PUT
+    uri: http://localhost:8000/v1/projects/114/comments/4
+  response:
+    body:
+      string: '{"id":4,"user":1,"username":"admin","example":14124,"text":"test2","created_at":"2022-10-07T04:13:52.442240Z"}'
+    headers:
+      Allow:
+      - GET, PUT, PATCH, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '110'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 04:13:52 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/usecase/fixtures/data_download/download.yaml b/tests/integration/usecase/fixtures/data_download/download.yaml
new file mode 100644
index 0000000..11beab6
--- /dev/null
+++ b/tests/integration/usecase/fixtures/data_download/download.yaml
@@ -0,0 +1,602 @@
+interactions:
+- request:
+    body: '{"name": "test", "description": "test", "guideline": "", "project_type":
+      "DocumentClassification", "random_order": false, "collaborative_annotation":
+      false, "single_class_classification": false, "allow_overlapping": false, "grapheme_mode":
+      false, "use_relation": false, "tags": [], "resourcetype": "TextClassificationProject"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '326'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects
+  response:
+    body:
+      string: '{"id":103,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T03:27:04.551024Z","updated_at":"2022-10-07T03:27:04.551048Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '472'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:27:04 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: !!python/object/new:_io.BytesIO
+      state: !!python/tuple
+      - !!binary |
+        LS1jYTJlMDc2MmE5NTI0ZDZjYTMyYzcyOGRjMTQ2MzQ0OQ0KQ29udGVudC1EaXNwb3NpdGlvbjog
+        Zm9ybS1kYXRhOyBuYW1lPSJmaWxlcG9uZCI7IGZpbGVuYW1lPSJjbGFzc2lmaWNhdGlvbi5qc29u
+        bCINCg0KeyJ0ZXh0IjogImV4YW1wbGVBIiwgImxhYmVscyI6IFsicG9zaXRpdmUiXSwgIm1ldGEi
+        OiB7Indpa2lQYWdlSUQiOiAxfX0KeyJ0ZXh0IjogImV4YW1wbGVCIiwgImxhYmVscyI6IFsicG9z
+        aXRpdmUiLCAibmVnYXRpdmUiXSwgIm1ldGEiOiB7Indpa2lQYWdlSUQiOiAyfX0KeyJ0ZXh0Ijog
+        ImV4YW1wbGVDIiwgImxhYmVscyI6IFtdLCAibWV0YSI6IHsid2lraVBhZ2VJRCI6IDN9fQoNCi0t
+        Y2EyZTA3NjJhOTUyNGQ2Y2EzMmM3MjhkYzE0NjM0NDktLQ0K
+      - 0
+      - null
+    headers:
+      Accept:
+      - '*/*'
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '378'
+      Content-Type:
+      - multipart/form-data; boundary=ca2e0762a9524d6ca32c728dc1463449
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/fp/process/
+  response:
+    body:
+      string: jcQmqxLobd5bfd3vNGxfbv
+    headers:
+      Allow:
+      - POST, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '22'
+      Content-Type:
+      - text/plain
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:27:04 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: '{"uploadIds": ["jcQmqxLobd5bfd3vNGxfbv"], "task": "DocumentClassification",
+      "format": "JSONL", "column_data": "text", "column_label": "labels"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '143'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects/103/upload
+  response:
+    body:
+      string: '{"task_id":"a959d3e8-fa3b-4f98-a6e4-ef9e222bfebe"}'
+    headers:
+      Allow:
+      - POST, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '50'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:27:04 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/tasks/status/a959d3e8-fa3b-4f98-a6e4-ef9e222bfebe
+  response:
+    body:
+      string: '{"ready":false,"result":null,"error":null}'
+    headers:
+      Allow:
+      - GET, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '42'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:27:04 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/tasks/status/a959d3e8-fa3b-4f98-a6e4-ef9e222bfebe
+  response:
+    body:
+      string: '{"ready":false,"result":null,"error":null}'
+    headers:
+      Allow:
+      - GET, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '42'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:27:05 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/tasks/status/a959d3e8-fa3b-4f98-a6e4-ef9e222bfebe
+  response:
+    body:
+      string: '{"ready":true,"result":{"error":[]},"error":null}'
+    headers:
+      Allow:
+      - GET, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '49'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:27:06 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/103/download-format
+  response:
+    body:
+      string: '[{"name":"CSV","example":"text,label\n\"Terrible customer service.\",\"negative\"\n\"Really
+        great transaction.\",\"positive\"\n\"Great price.\",\"positive\"\n"},{"name":"fastText","example":"__label__negative
+        Terrible customer service.\n__label__positive Really great transaction.\n__label__positive
+        Great price.\n"},{"name":"JSON","example":"[\n    {\n        \"text\": \"Terrible
+        customer service.\",\n        \"label\": [\"negative\"]\n    },\n    {\n        \"text\":
+        \"Really great transaction.\",\n        \"label\": [\"positive\"]\n    },\n    {\n        \"text\":
+        \"Great price.\",\n        \"label\": [\"positive\"]\n    }\n]\n"},{"name":"JSONL","example":"{\"text\":
+        \"Terrible customer service.\", \"label\": [\"negative\"]}\n{\"text\": \"Really
+        great transaction.\", \"label\": [\"positive\"]}\n{\"text\": \"Great price.\",
+        \"label\": [\"positive\"]}\n"}]'
+    headers:
+      Allow:
+      - GET, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '867'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:27:06 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: '{"format": "JSONL", "exportApproved": false}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '44'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects/103/download
+  response:
+    body:
+      string: '{"task_id":"5fc8906a-00d1-4fbf-8605-dcea3ac91b43"}'
+    headers:
+      Allow:
+      - GET, POST, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '50'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:27:06 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/tasks/status/5fc8906a-00d1-4fbf-8605-dcea3ac91b43
+  response:
+    body:
+      string: '{"ready":false,"result":null,"error":null}'
+    headers:
+      Allow:
+      - GET, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '42'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:27:06 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/tasks/status/5fc8906a-00d1-4fbf-8605-dcea3ac91b43
+  response:
+    body:
+      string: '{"ready":true,"result":"/doccano/backend/media/f8982f4f-1d8b-43c1-861c-fa3895806035.zip","error":null}'
+    headers:
+      Allow:
+      - GET, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '102'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:27:08 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/103/download?taskId=5fc8906a-00d1-4fbf-8605-dcea3ac91b43
+  response:
+    body:
+      string: !!binary |
+        UEsDBBQAAAAIAGMbR1VSh36/fAAAAA8BAAALAAAAYWRtaW4uanNvbmyrVspMUbIyNDE0NNdRKkmt
+        KFGyUkqtSMwtyEl1VNJRyk0tSVSyqlYqz8zODEhMT/V0ASqu1VHKSUxKzVGyilYqyC/OLMksS1WK
+        1VFyzs/NTc0rKQaKx9ZyVcNNtkA32QmHyUbIJuelpieCTdYhyhJLdEuccVhijGQJhnkAUEsBAhQD
+        FAAAAAgAYxtHVVKHfr98AAAADwEAAAsAAAAAAAAAAAAAAKSBAAAAAGFkbWluLmpzb25sUEsFBgAA
+        AAABAAEAOQAAAKUAAAAAAA==
+    headers:
+      Allow:
+      - GET, POST, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Disposition:
+      - attachment; filename="f8982f4f-1d8b-43c1-861c-fa3895806035.zip"
+      Content-Length:
+      - '244'
+      Content-Type:
+      - application/zip
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:27:08 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/usecase/fixtures/data_upload/upload.yaml b/tests/integration/usecase/fixtures/data_upload/upload.yaml
new file mode 100644
index 0000000..c30bd2d
--- /dev/null
+++ b/tests/integration/usecase/fixtures/data_upload/upload.yaml
@@ -0,0 +1,329 @@
+interactions:
+- request:
+    body: '{"name": "test", "description": "test", "guideline": "", "project_type":
+      "DocumentClassification", "random_order": false, "collaborative_annotation":
+      false, "single_class_classification": false, "allow_overlapping": false, "grapheme_mode":
+      false, "use_relation": false, "tags": [], "resourcetype": "TextClassificationProject"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '326'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects
+  response:
+    body:
+      string: '{"id":104,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T03:27:08.116596Z","updated_at":"2022-10-07T03:27:08.116618Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '472'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:27:08 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: !!python/object/new:_io.BytesIO
+      state: !!python/tuple
+      - !!binary |
+        LS1lODMxMTZkMTI3ZDU0YWI5OGY5NWFiNjJmZTExNWQzYg0KQ29udGVudC1EaXNwb3NpdGlvbjog
+        Zm9ybS1kYXRhOyBuYW1lPSJmaWxlcG9uZCI7IGZpbGVuYW1lPSJjbGFzc2lmaWNhdGlvbi5qc29u
+        bCINCg0KeyJ0ZXh0IjogImV4YW1wbGVBIiwgImxhYmVscyI6IFsicG9zaXRpdmUiXSwgIm1ldGEi
+        OiB7Indpa2lQYWdlSUQiOiAxfX0KeyJ0ZXh0IjogImV4YW1wbGVCIiwgImxhYmVscyI6IFsicG9z
+        aXRpdmUiLCAibmVnYXRpdmUiXSwgIm1ldGEiOiB7Indpa2lQYWdlSUQiOiAyfX0KeyJ0ZXh0Ijog
+        ImV4YW1wbGVDIiwgImxhYmVscyI6IFtdLCAibWV0YSI6IHsid2lraVBhZ2VJRCI6IDN9fQoNCi0t
+        ZTgzMTE2ZDEyN2Q1NGFiOThmOTVhYjYyZmUxMTVkM2ItLQ0K
+      - 0
+      - null
+    headers:
+      Accept:
+      - '*/*'
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '378'
+      Content-Type:
+      - multipart/form-data; boundary=e83116d127d54ab98f95ab62fe115d3b
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/fp/process/
+  response:
+    body:
+      string: QNcXFXM2yiHfJCkJNm9HRG
+    headers:
+      Allow:
+      - POST, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '22'
+      Content-Type:
+      - text/plain
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:27:08 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: '{"uploadIds": ["QNcXFXM2yiHfJCkJNm9HRG"], "task": "DocumentClassification",
+      "format": "JSONL", "column_data": "text", "column_label": "labels"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '143'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects/104/upload
+  response:
+    body:
+      string: '{"task_id":"24f2936b-573d-4710-97fb-227403e78095"}'
+    headers:
+      Allow:
+      - POST, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '50'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:27:08 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/tasks/status/24f2936b-573d-4710-97fb-227403e78095
+  response:
+    body:
+      string: '{"ready":false,"result":null,"error":null}'
+    headers:
+      Allow:
+      - GET, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '42'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:27:08 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/tasks/status/24f2936b-573d-4710-97fb-227403e78095
+  response:
+    body:
+      string: '{"ready":true,"result":{"error":[]},"error":null}'
+    headers:
+      Allow:
+      - GET, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '49'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:27:09 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/104/examples
+  response:
+    body:
+      string: '{"count":3,"next":null,"previous":null,"results":[{"id":14120,"filename":"http://localhost:8000/media/QNcXFXM2yiHfJCkJNm9HRG/HwAJALbRTAHGKDZMBnx97K","meta":{"meta":{"wikiPageID":1}},"annotation_approver":null,"comment_count":0,"text":"exampleA","is_confirmed":false,"upload_name":"classification.jsonl"},{"id":14121,"filename":"http://localhost:8000/media/QNcXFXM2yiHfJCkJNm9HRG/HwAJALbRTAHGKDZMBnx97K","meta":{"meta":{"wikiPageID":2}},"annotation_approver":null,"comment_count":0,"text":"exampleB","is_confirmed":false,"upload_name":"classification.jsonl"},{"id":14122,"filename":"http://localhost:8000/media/QNcXFXM2yiHfJCkJNm9HRG/HwAJALbRTAHGKDZMBnx97K","meta":{"meta":{"wikiPageID":3}},"annotation_approver":null,"comment_count":0,"text":"exampleC","is_confirmed":false,"upload_name":"classification.jsonl"}]}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '813'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:27:09 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/usecase/fixtures/example/create.yaml b/tests/integration/usecase/fixtures/example/create.yaml
new file mode 100644
index 0000000..e6caf26
--- /dev/null
+++ b/tests/integration/usecase/fixtures/example/create.yaml
@@ -0,0 +1,112 @@
+interactions:
+- request:
+    body: '{"name": "test", "description": "test", "guideline": "", "project_type":
+      "DocumentClassification", "random_order": false, "collaborative_annotation":
+      false, "single_class_classification": false, "allow_overlapping": false, "grapheme_mode":
+      false, "use_relation": false, "tags": [], "resourcetype": "TextClassificationProject"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '326'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects
+  response:
+    body:
+      string: '{"id":89,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T03:25:50.119934Z","updated_at":"2022-10-07T03:25:50.119946Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '471'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:50 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: '{"text": "test", "meta": {}, "annotation_approver": null, "comment_count":
+      0, "is_confirmed": false, "filename": "", "upload_name": ""}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '135'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects/89/examples
+  response:
+    body:
+      string: '{"id":14113,"filename":"http://localhost:8000/media/","meta":{},"annotation_approver":null,"comment_count":0,"text":"test","is_confirmed":false,"upload_name":""}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '161'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:50 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+version: 1
diff --git a/tests/integration/usecase/fixtures/example/delete.yaml b/tests/integration/usecase/fixtures/example/delete.yaml
new file mode 100644
index 0000000..97e2b8f
--- /dev/null
+++ b/tests/integration/usecase/fixtures/example/delete.yaml
@@ -0,0 +1,214 @@
+interactions:
+- request:
+    body: '{"name": "test", "description": "test", "guideline": "", "project_type":
+      "DocumentClassification", "random_order": false, "collaborative_annotation":
+      false, "single_class_classification": false, "allow_overlapping": false, "grapheme_mode":
+      false, "use_relation": false, "tags": [], "resourcetype": "TextClassificationProject"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '326'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects
+  response:
+    body:
+      string: '{"id":91,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T03:25:50.343214Z","updated_at":"2022-10-07T03:25:50.343238Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '471'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:50 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: '{"text": "test", "meta": {}, "annotation_approver": null, "comment_count":
+      0, "is_confirmed": false, "filename": "", "upload_name": ""}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '135'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects/91/examples
+  response:
+    body:
+      string: '{"id":14115,"filename":"http://localhost:8000/media/","meta":{},"annotation_approver":null,"comment_count":0,"text":"test","is_confirmed":false,"upload_name":""}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '161'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:50 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '0'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: DELETE
+    uri: http://localhost:8000/v1/projects/91/examples/14115
+  response:
+    body:
+      string: ''
+    headers:
+      Allow:
+      - GET, PUT, PATCH, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '0'
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:50 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 204
+      message: No Content
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/91/examples/14115
+  response:
+    body:
+      string: '{"detail":"Not found."}'
+    headers:
+      Allow:
+      - GET, PUT, PATCH, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '23'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:50 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 404
+      message: Not Found
+version: 1
diff --git a/tests/integration/usecase/fixtures/example/delete_all.yaml b/tests/integration/usecase/fixtures/example/delete_all.yaml
new file mode 100644
index 0000000..f118b6c
--- /dev/null
+++ b/tests/integration/usecase/fixtures/example/delete_all.yaml
@@ -0,0 +1,214 @@
+interactions:
+- request:
+    body: '{"name": "test", "description": "test", "guideline": "", "project_type":
+      "DocumentClassification", "random_order": false, "collaborative_annotation":
+      false, "single_class_classification": false, "allow_overlapping": false, "grapheme_mode":
+      false, "use_relation": false, "tags": [], "resourcetype": "TextClassificationProject"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '326'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects
+  response:
+    body:
+      string: '{"id":92,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T03:25:50.423638Z","updated_at":"2022-10-07T03:25:50.423656Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '471'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:50 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: '{"text": "test", "meta": {}, "annotation_approver": null, "comment_count":
+      0, "is_confirmed": false, "filename": "", "upload_name": ""}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '135'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects/92/examples
+  response:
+    body:
+      string: '{"id":14116,"filename":"http://localhost:8000/media/","meta":{},"annotation_approver":null,"comment_count":0,"text":"test","is_confirmed":false,"upload_name":""}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '161'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:50 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: '{"ids": []}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '11'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: DELETE
+    uri: http://localhost:8000/v1/projects/92/examples
+  response:
+    body:
+      string: ''
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '0'
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:50 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 204
+      message: No Content
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/92/examples
+  response:
+    body:
+      string: '{"count":0,"next":null,"previous":null,"results":[]}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '52'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:50 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/usecase/fixtures/example/update.yaml b/tests/integration/usecase/fixtures/example/update.yaml
new file mode 100644
index 0000000..246da7c
--- /dev/null
+++ b/tests/integration/usecase/fixtures/example/update.yaml
@@ -0,0 +1,217 @@
+interactions:
+- request:
+    body: '{"name": "test", "description": "test", "guideline": "", "project_type":
+      "DocumentClassification", "random_order": false, "collaborative_annotation":
+      false, "single_class_classification": false, "allow_overlapping": false, "grapheme_mode":
+      false, "use_relation": false, "tags": [], "resourcetype": "TextClassificationProject"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '326'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects
+  response:
+    body:
+      string: '{"id":90,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T03:25:50.182562Z","updated_at":"2022-10-07T03:25:50.182581Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '471'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:50 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: '{"text": "test", "meta": {}, "annotation_approver": null, "comment_count":
+      0, "is_confirmed": false, "filename": "", "upload_name": ""}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '135'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects/90/examples
+  response:
+    body:
+      string: '{"id":14114,"filename":"http://localhost:8000/media/","meta":{},"annotation_approver":null,"comment_count":0,"text":"test","is_confirmed":false,"upload_name":""}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '161'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:50 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/90/examples/14114
+  response:
+    body:
+      string: '{"id":14114,"filename":"http://localhost:8000/media/","meta":{},"annotation_approver":null,"comment_count":0,"text":"test","is_confirmed":false,"upload_name":""}'
+    headers:
+      Allow:
+      - GET, PUT, PATCH, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '161'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:50 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: '{"id": 14114, "text": "test2", "meta": {}, "annotation_approver": null,
+      "comment_count": 0, "is_confirmed": false, "filename": "", "upload_name": ""}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '149'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: PUT
+    uri: http://localhost:8000/v1/projects/90/examples/14114
+  response:
+    body:
+      string: '{"id":14114,"filename":"http://localhost:8000/media/","meta":{},"annotation_approver":null,"comment_count":0,"text":"test2","is_confirmed":false,"upload_name":""}'
+    headers:
+      Allow:
+      - GET, PUT, PATCH, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '162'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:50 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/usecase/fixtures/label/create.yaml b/tests/integration/usecase/fixtures/label/create.yaml
new file mode 100644
index 0000000..e4f5b9b
--- /dev/null
+++ b/tests/integration/usecase/fixtures/label/create.yaml
@@ -0,0 +1,322 @@
+interactions:
+- request:
+    body: '{"name": "test", "description": "test", "guideline": "", "project_type":
+      "DocumentClassification", "random_order": false, "collaborative_annotation":
+      false, "single_class_classification": false, "allow_overlapping": false, "grapheme_mode":
+      false, "use_relation": false, "tags": [], "resourcetype": "TextClassificationProject"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '326'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects
+  response:
+    body:
+      string: '{"id":119,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T04:23:40.353762Z","updated_at":"2022-10-07T04:23:40.353785Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '472'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 04:23:40 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: '{"text": "test", "meta": {}, "annotation_approver": null, "comment_count":
+      0, "is_confirmed": false, "filename": "", "upload_name": ""}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '135'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects/119/examples
+  response:
+    body:
+      string: '{"id":14129,"filename":"http://localhost:8000/media/","meta":{},"annotation_approver":null,"comment_count":0,"text":"test","is_confirmed":false,"upload_name":""}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '161'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 04:23:40 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/119/category-types
+  response:
+    body:
+      string: '[]'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '2'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 04:23:40 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: '{"text": "text", "prefix_key": null, "suffix_key": null, "background_color":
+      "#dc8c0d", "text_color": "#ffffff"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '112'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects/119/category-types
+  response:
+    body:
+      string: '{"id":87,"text":"text","prefix_key":null,"suffix_key":null,"background_color":"#dc8c0d","text_color":"#ffffff"}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '111'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 04:23:40 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/119/category-types
+  response:
+    body:
+      string: '[{"id":87,"text":"text","prefix_key":null,"suffix_key":null,"background_color":"#dc8c0d","text_color":"#ffffff"}]'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '113'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 04:23:40 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: '{"example": 14129, "prob": 0.0, "manual": false, "user": null, "label":
+      87}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '75'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects/119/examples/14129/categories
+  response:
+    body:
+      string: '{"id":56,"prob":0.0,"user":1,"example":14129,"created_at":"2022-10-07T04:23:40.479383Z","updated_at":"2022-10-07T04:23:40.479394Z","label":87}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '142'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 04:23:40 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+version: 1
diff --git a/tests/integration/usecase/fixtures/label/delete.yaml b/tests/integration/usecase/fixtures/label/delete.yaml
new file mode 100644
index 0000000..2450b13
--- /dev/null
+++ b/tests/integration/usecase/fixtures/label/delete.yaml
@@ -0,0 +1,475 @@
+interactions:
+- request:
+    body: '{"name": "test", "description": "test", "guideline": "", "project_type":
+      "DocumentClassification", "random_order": false, "collaborative_annotation":
+      false, "single_class_classification": false, "allow_overlapping": false, "grapheme_mode":
+      false, "use_relation": false, "tags": [], "resourcetype": "TextClassificationProject"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '326'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects
+  response:
+    body:
+      string: '{"id":121,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T04:23:40.641322Z","updated_at":"2022-10-07T04:23:40.641337Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '472'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 04:23:40 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: '{"text": "test", "meta": {}, "annotation_approver": null, "comment_count":
+      0, "is_confirmed": false, "filename": "", "upload_name": ""}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '135'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects/121/examples
+  response:
+    body:
+      string: '{"id":14131,"filename":"http://localhost:8000/media/","meta":{},"annotation_approver":null,"comment_count":0,"text":"test","is_confirmed":false,"upload_name":""}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '161'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 04:23:40 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/121/category-types
+  response:
+    body:
+      string: '[]'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '2'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 04:23:40 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: '{"text": "text", "prefix_key": null, "suffix_key": null, "background_color":
+      "#ea33f7", "text_color": "#ffffff"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '112'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects/121/category-types
+  response:
+    body:
+      string: '{"id":90,"text":"text","prefix_key":null,"suffix_key":null,"background_color":"#ea33f7","text_color":"#ffffff"}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '111'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 04:23:40 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/121/category-types
+  response:
+    body:
+      string: '[{"id":90,"text":"text","prefix_key":null,"suffix_key":null,"background_color":"#ea33f7","text_color":"#ffffff"}]'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '113'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 04:23:40 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: '{"example": 14131, "prob": 0.0, "manual": false, "user": null, "label":
+      90}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '75'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects/121/examples/14131/categories
+  response:
+    body:
+      string: '{"id":58,"prob":0.0,"user":1,"example":14131,"created_at":"2022-10-07T04:23:40.712300Z","updated_at":"2022-10-07T04:23:40.712314Z","label":90}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '142'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 04:23:40 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/121/examples/14131/categories/58
+  response:
+    body:
+      string: '{"id":58,"prob":0.0,"user":1,"example":14131,"created_at":"2022-10-07T04:23:40.712300Z","updated_at":"2022-10-07T04:23:40.712314Z","label":90}'
+    headers:
+      Allow:
+      - GET, PUT, PATCH, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '142'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 04:23:40 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '0'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: DELETE
+    uri: http://localhost:8000/v1/projects/121/examples/14131/categories/58
+  response:
+    body:
+      string: ''
+    headers:
+      Allow:
+      - GET, PUT, PATCH, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '0'
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 04:23:40 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 204
+      message: No Content
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/121/examples/14131/categories/58
+  response:
+    body:
+      string: '{"detail":"Not found."}'
+    headers:
+      Allow:
+      - GET, PUT, PATCH, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '23'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 04:23:40 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 404
+      message: Not Found
+version: 1
diff --git a/tests/integration/usecase/fixtures/label/update.yaml b/tests/integration/usecase/fixtures/label/update.yaml
new file mode 100644
index 0000000..f1720a3
--- /dev/null
+++ b/tests/integration/usecase/fixtures/label/update.yaml
@@ -0,0 +1,583 @@
+interactions:
+- request:
+    body: '{"name": "test", "description": "test", "guideline": "", "project_type":
+      "DocumentClassification", "random_order": false, "collaborative_annotation":
+      false, "single_class_classification": false, "allow_overlapping": false, "grapheme_mode":
+      false, "use_relation": false, "tags": [], "resourcetype": "TextClassificationProject"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '326'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects
+  response:
+    body:
+      string: '{"id":120,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T04:23:40.499940Z","updated_at":"2022-10-07T04:23:40.499955Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '472'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 04:23:40 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: '{"text": "test", "meta": {}, "annotation_approver": null, "comment_count":
+      0, "is_confirmed": false, "filename": "", "upload_name": ""}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '135'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects/120/examples
+  response:
+    body:
+      string: '{"id":14130,"filename":"http://localhost:8000/media/","meta":{},"annotation_approver":null,"comment_count":0,"text":"test","is_confirmed":false,"upload_name":""}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '161'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 04:23:40 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/120/category-types
+  response:
+    body:
+      string: '[]'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '2'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 04:23:40 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: '{"text": "text", "prefix_key": null, "suffix_key": null, "background_color":
+      "#3267cd", "text_color": "#ffffff"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '112'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects/120/category-types
+  response:
+    body:
+      string: '{"id":88,"text":"text","prefix_key":null,"suffix_key":null,"background_color":"#3267cd","text_color":"#ffffff"}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '111'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 04:23:40 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/120/category-types
+  response:
+    body:
+      string: '[{"id":88,"text":"text","prefix_key":null,"suffix_key":null,"background_color":"#3267cd","text_color":"#ffffff"}]'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '113'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 04:23:40 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: '{"text": "text2", "prefix_key": null, "suffix_key": null, "background_color":
+      "#0437db", "text_color": "#ffffff"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '113'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects/120/category-types
+  response:
+    body:
+      string: '{"id":89,"text":"text2","prefix_key":null,"suffix_key":null,"background_color":"#0437db","text_color":"#ffffff"}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '112'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 04:23:40 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/120/category-types
+  response:
+    body:
+      string: '[{"id":88,"text":"text","prefix_key":null,"suffix_key":null,"background_color":"#3267cd","text_color":"#ffffff"},{"id":89,"text":"text2","prefix_key":null,"suffix_key":null,"background_color":"#0437db","text_color":"#ffffff"}]'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '226'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 04:23:40 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: '{"example": 14130, "prob": 0.0, "manual": false, "user": null, "label":
+      88}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '75'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects/120/examples/14130/categories
+  response:
+    body:
+      string: '{"id":57,"prob":0.0,"user":1,"example":14130,"created_at":"2022-10-07T04:23:40.575593Z","updated_at":"2022-10-07T04:23:40.575602Z","label":88}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '142'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 04:23:40 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/120/examples/14130/categories/57
+  response:
+    body:
+      string: '{"id":57,"prob":0.0,"user":1,"example":14130,"created_at":"2022-10-07T04:23:40.575593Z","updated_at":"2022-10-07T04:23:40.575602Z","label":88}'
+    headers:
+      Allow:
+      - GET, PUT, PATCH, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '142'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 04:23:40 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/120/category-types
+  response:
+    body:
+      string: '[{"id":88,"text":"text","prefix_key":null,"suffix_key":null,"background_color":"#3267cd","text_color":"#ffffff"},{"id":89,"text":"text2","prefix_key":null,"suffix_key":null,"background_color":"#0437db","text_color":"#ffffff"}]'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '226'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 04:23:40 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: '{"id": 57, "example": 14130, "prob": 0.0, "manual": false, "user": null,
+      "label": 89}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '85'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: PUT
+    uri: http://localhost:8000/v1/projects/120/examples/14130/categories/57
+  response:
+    body:
+      string: '{"id":57,"prob":0.0,"user":1,"example":14130,"created_at":"2022-10-07T04:23:40.575593Z","updated_at":"2022-10-07T04:23:40.606667Z","label":89}'
+    headers:
+      Allow:
+      - GET, PUT, PATCH, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '142'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 04:23:40 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/usecase/fixtures/label_type/create.yaml b/tests/integration/usecase/fixtures/label_type/create.yaml
new file mode 100644
index 0000000..eccea18
--- /dev/null
+++ b/tests/integration/usecase/fixtures/label_type/create.yaml
@@ -0,0 +1,163 @@
+interactions:
+- request:
+    body: '{"name": "test", "description": "test", "guideline": "", "project_type":
+      "DocumentClassification", "random_order": false, "collaborative_annotation":
+      false, "single_class_classification": false, "allow_overlapping": false, "grapheme_mode":
+      false, "use_relation": false, "tags": [], "resourcetype": "TextClassificationProject"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '326'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects
+  response:
+    body:
+      string: '{"id":109,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T03:31:26.015039Z","updated_at":"2022-10-07T03:31:26.015075Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '472'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:31:26 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/109/category-types
+  response:
+    body:
+      string: '[]'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '2'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:31:26 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: '{"text": "text", "prefix_key": null, "suffix_key": null, "background_color":
+      "#889f18", "text_color": "#ffffff"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '112'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects/109/category-types
+  response:
+    body:
+      string: '{"id":78,"text":"text","prefix_key":null,"suffix_key":null,"background_color":"#889f18","text_color":"#ffffff"}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '111'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:31:26 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+version: 1
diff --git a/tests/integration/usecase/fixtures/label_type/delete.yaml b/tests/integration/usecase/fixtures/label_type/delete.yaml
new file mode 100644
index 0000000..6e1005e
--- /dev/null
+++ b/tests/integration/usecase/fixtures/label_type/delete.yaml
@@ -0,0 +1,265 @@
+interactions:
+- request:
+    body: '{"name": "test", "description": "test", "guideline": "", "project_type":
+      "DocumentClassification", "random_order": false, "collaborative_annotation":
+      false, "single_class_classification": false, "allow_overlapping": false, "grapheme_mode":
+      false, "use_relation": false, "tags": [], "resourcetype": "TextClassificationProject"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '326'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects
+  response:
+    body:
+      string: '{"id":111,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T03:31:26.244430Z","updated_at":"2022-10-07T03:31:26.244455Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '472'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:31:26 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/111/category-types
+  response:
+    body:
+      string: '[]'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '2'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:31:26 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: '{"text": "text", "prefix_key": null, "suffix_key": null, "background_color":
+      "#8af7a4", "text_color": "#ffffff"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '112'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects/111/category-types
+  response:
+    body:
+      string: '{"id":80,"text":"text","prefix_key":null,"suffix_key":null,"background_color":"#8af7a4","text_color":"#ffffff"}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '111'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:31:26 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '0'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: DELETE
+    uri: http://localhost:8000/v1/projects/111/category-types/80
+  response:
+    body:
+      string: ''
+    headers:
+      Allow:
+      - GET, PUT, PATCH, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '0'
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:31:26 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 204
+      message: No Content
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/111/category-types/80
+  response:
+    body:
+      string: '{"detail":"Not found."}'
+    headers:
+      Allow:
+      - GET, PUT, PATCH, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '23'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:31:26 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 404
+      message: Not Found
+version: 1
diff --git a/tests/integration/usecase/fixtures/label_type/update.yaml b/tests/integration/usecase/fixtures/label_type/update.yaml
new file mode 100644
index 0000000..eff6cc0
--- /dev/null
+++ b/tests/integration/usecase/fixtures/label_type/update.yaml
@@ -0,0 +1,319 @@
+interactions:
+- request:
+    body: '{"name": "test", "description": "test", "guideline": "", "project_type":
+      "DocumentClassification", "random_order": false, "collaborative_annotation":
+      false, "single_class_classification": false, "allow_overlapping": false, "grapheme_mode":
+      false, "use_relation": false, "tags": [], "resourcetype": "TextClassificationProject"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '326'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects
+  response:
+    body:
+      string: '{"id":110,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T03:31:26.161800Z","updated_at":"2022-10-07T03:31:26.161822Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '472'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:31:26 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/110/category-types
+  response:
+    body:
+      string: '[]'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '2'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:31:26 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: '{"text": "text", "prefix_key": null, "suffix_key": null, "background_color":
+      "#aede4f", "text_color": "#ffffff"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '112'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects/110/category-types
+  response:
+    body:
+      string: '{"id":79,"text":"text","prefix_key":null,"suffix_key":null,"background_color":"#aede4f","text_color":"#ffffff"}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '111'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:31:26 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/110/category-types/79
+  response:
+    body:
+      string: '{"id":79,"text":"text","prefix_key":null,"suffix_key":null,"background_color":"#aede4f","text_color":"#ffffff"}'
+    headers:
+      Allow:
+      - GET, PUT, PATCH, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '111'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:31:26 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/110/category-types
+  response:
+    body:
+      string: '[{"id":79,"text":"text","prefix_key":null,"suffix_key":null,"background_color":"#aede4f","text_color":"#ffffff"}]'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '113'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:31:26 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: '{"id": 79, "text": "text2", "prefix_key": null, "suffix_key": null, "background_color":
+      "#aede4f", "text_color": "#ffffff"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '123'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: PUT
+    uri: http://localhost:8000/v1/projects/110/category-types/79
+  response:
+    body:
+      string: '{"id":79,"text":"text2","prefix_key":null,"suffix_key":null,"background_color":"#aede4f","text_color":"#ffffff"}'
+    headers:
+      Allow:
+      - GET, PUT, PATCH, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '112'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:31:26 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/usecase/fixtures/label_type/upload.yaml b/tests/integration/usecase/fixtures/label_type/upload.yaml
new file mode 100644
index 0000000..e9263b5
--- /dev/null
+++ b/tests/integration/usecase/fixtures/label_type/upload.yaml
@@ -0,0 +1,174 @@
+interactions:
+- request:
+    body: '{"name": "test", "description": "test", "guideline": "", "project_type":
+      "DocumentClassification", "random_order": false, "collaborative_annotation":
+      false, "single_class_classification": false, "allow_overlapping": false, "grapheme_mode":
+      false, "use_relation": false, "tags": [], "resourcetype": "TextClassificationProject"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '326'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects
+  response:
+    body:
+      string: '{"id":112,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T03:31:26.312401Z","updated_at":"2022-10-07T03:31:26.312420Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '472'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:31:26 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: !!python/object/new:_io.BytesIO
+      state: !!python/tuple
+      - !!binary |
+        LS1lMjhmMWFhNzVhOTg0NjRiOTdkYzdlZjcyZDk2YTYyMg0KQ29udGVudC1EaXNwb3NpdGlvbjog
+        Zm9ybS1kYXRhOyBuYW1lPSJmaWxlIjsgZmlsZW5hbWU9ImxhYmVscy5qc29uIg0KQ29udGVudC1U
+        eXBlOiBhcHBsaWNhdGlvbi9qc29uDQoNClsKICB7CiAgICAiaWQiOiA0NCwKICAgICJ0ZXh0Ijog
+        IkRvZyIsCiAgICAicHJlZml4X2tleSI6IG51bGwsCiAgICAic3VmZml4X2tleSI6ICJhIiwKICAg
+        ICJiYWNrZ3JvdW5kX2NvbG9yIjogIiNGRjAwMDAiLAogICAgInRleHRfY29sb3IiOiAiI2ZmZmZm
+        ZiIsCiAgICAidGFza190eXBlIjogIkNhdGVnb3J5IgogIH0sCiAgewogICAgImlkIjogNDUsCiAg
+        ICAidGV4dCI6ICJDYXQiLAogICAgInByZWZpeF9rZXkiOiBudWxsLAogICAgInN1ZmZpeF9rZXki
+        OiAiYyIsCiAgICAiYmFja2dyb3VuZF9jb2xvciI6ICIjRkYwMDAwIiwKICAgICJ0ZXh0X2NvbG9y
+        IjogIiNmZmZmZmYiLAogICAgInRhc2tfdHlwZSI6ICJTcGFuIgogIH0KXQoNCi0tZTI4ZjFhYTc1
+        YTk4NDY0Yjk3ZGM3ZWY3MmQ5NmE2MjItLQ0K
+      - 0
+      - null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '540'
+      Content-Type:
+      - multipart/form-data; boundary=e28f1aa75a98464b97dc7ef72d96a622
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects/112/category-type-upload
+  response:
+    body:
+      string: ''
+    headers:
+      Allow:
+      - POST, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '0'
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:31:26 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/112/category-types
+  response:
+    body:
+      string: '[{"id":81,"text":"Dog","prefix_key":null,"suffix_key":"a","background_color":"#FF0000","text_color":"#ffffff"},{"id":82,"text":"Cat","prefix_key":null,"suffix_key":"c","background_color":"#FF0000","text_color":"#ffffff"}]'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '221'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:31:26 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/usecase/fixtures/login.yaml b/tests/integration/usecase/fixtures/login.yaml
new file mode 100644
index 0000000..37c9be8
--- /dev/null
+++ b/tests/integration/usecase/fixtures/login.yaml
@@ -0,0 +1,55 @@
+interactions:
+- request:
+    body: '{"username": "admin", "password": "password"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '45'
+      User-Agent:
+      - python-requests/2.28.1
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/auth/login/
+  response:
+    body:
+      string: '{"key":"b37c59f6e0db6bf8a829858ce925b9c2b4d7b03d"}'
+    headers:
+      Allow:
+      - POST, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '50'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:49 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Set-Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        expires=Fri, 06 Oct 2023 03:25:49 GMT; Max-Age=31449600; Path=/; SameSite=Lax
+      - sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr; expires=Fri, 21 Oct 2022 03:25:49
+        GMT; HttpOnly; Max-Age=1209600; Path=/; SameSite=Lax
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/usecase/fixtures/member/add.yaml b/tests/integration/usecase/fixtures/member/add.yaml
new file mode 100644
index 0000000..235f029
--- /dev/null
+++ b/tests/integration/usecase/fixtures/member/add.yaml
@@ -0,0 +1,264 @@
+interactions:
+- request:
+    body: '{"name": "test", "description": "test", "guideline": "", "project_type":
+      "DocumentClassification", "random_order": false, "collaborative_annotation":
+      false, "single_class_classification": false, "allow_overlapping": false, "grapheme_mode":
+      false, "use_relation": false, "tags": [], "resourcetype": "TextClassificationProject"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '326'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects
+  response:
+    body:
+      string: '{"id":97,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T03:25:50.818363Z","updated_at":"2022-10-07T03:25:50.818379Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '471'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:50 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/users?q=hironsan
+  response:
+    body:
+      string: '[{"id":3,"username":"hironsan","is_superuser":false,"is_staff":false}]'
+    headers:
+      Allow:
+      - GET, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '70'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:50 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/roles
+  response:
+    body:
+      string: '[{"id":1,"name":"project_admin"},{"id":2,"name":"annotator"},{"id":3,"name":"annotation_approver"}]'
+    headers:
+      Allow:
+      - GET, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '99'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:50 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: '{"user": 3, "role": 2}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '22'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects/97/members
+  response:
+    body:
+      string: '{"id":113,"user":3,"role":2,"username":"hironsan","rolename":"annotator"}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '73'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:50 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/97/members
+  response:
+    body:
+      string: '[{"id":112,"user":1,"role":1,"username":"admin","rolename":"project_admin"},{"id":113,"user":3,"role":2,"username":"hironsan","rolename":"annotator"}]'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '150'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:50 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/usecase/fixtures/member/delete.yaml b/tests/integration/usecase/fixtures/member/delete.yaml
new file mode 100644
index 0000000..d002df8
--- /dev/null
+++ b/tests/integration/usecase/fixtures/member/delete.yaml
@@ -0,0 +1,366 @@
+interactions:
+- request:
+    body: '{"name": "test", "description": "test", "guideline": "", "project_type":
+      "DocumentClassification", "random_order": false, "collaborative_annotation":
+      false, "single_class_classification": false, "allow_overlapping": false, "grapheme_mode":
+      false, "use_relation": false, "tags": [], "resourcetype": "TextClassificationProject"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '326'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects
+  response:
+    body:
+      string: '{"id":98,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T03:25:50.888658Z","updated_at":"2022-10-07T03:25:50.888669Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '471'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:50 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/users?q=hironsan
+  response:
+    body:
+      string: '[{"id":3,"username":"hironsan","is_superuser":false,"is_staff":false}]'
+    headers:
+      Allow:
+      - GET, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '70'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:50 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/roles
+  response:
+    body:
+      string: '[{"id":1,"name":"project_admin"},{"id":2,"name":"annotator"},{"id":3,"name":"annotation_approver"}]'
+    headers:
+      Allow:
+      - GET, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '99'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:50 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: '{"user": 3, "role": 2}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '22'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects/98/members
+  response:
+    body:
+      string: '{"id":115,"user":3,"role":2,"username":"hironsan","rolename":"annotator"}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '73'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:50 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/98/members
+  response:
+    body:
+      string: '[{"id":114,"user":1,"role":1,"username":"admin","rolename":"project_admin"},{"id":115,"user":3,"role":2,"username":"hironsan","rolename":"annotator"}]'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '150'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:50 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: '{"ids": [115]}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '14'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: DELETE
+    uri: http://localhost:8000/v1/projects/98/members
+  response:
+    body:
+      string: ''
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '0'
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:50 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 204
+      message: No Content
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/98/members
+  response:
+    body:
+      string: '[{"id":114,"user":1,"role":1,"username":"admin","rolename":"project_admin"}]'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '76'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:50 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/usecase/fixtures/member/update.yaml b/tests/integration/usecase/fixtures/member/update.yaml
new file mode 100644
index 0000000..4708280
--- /dev/null
+++ b/tests/integration/usecase/fixtures/member/update.yaml
@@ -0,0 +1,470 @@
+interactions:
+- request:
+    body: '{"name": "test", "description": "test", "guideline": "", "project_type":
+      "DocumentClassification", "random_order": false, "collaborative_annotation":
+      false, "single_class_classification": false, "allow_overlapping": false, "grapheme_mode":
+      false, "use_relation": false, "tags": [], "resourcetype": "TextClassificationProject"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '326'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects
+  response:
+    body:
+      string: '{"id":99,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T03:25:50.973666Z","updated_at":"2022-10-07T03:25:50.973683Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '471'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:50 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/users?q=hironsan
+  response:
+    body:
+      string: '[{"id":3,"username":"hironsan","is_superuser":false,"is_staff":false}]'
+    headers:
+      Allow:
+      - GET, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '70'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:50 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/roles
+  response:
+    body:
+      string: '[{"id":1,"name":"project_admin"},{"id":2,"name":"annotator"},{"id":3,"name":"annotation_approver"}]'
+    headers:
+      Allow:
+      - GET, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '99'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:50 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: '{"user": 3, "role": 2}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '22'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects/99/members
+  response:
+    body:
+      string: '{"id":117,"user":3,"role":2,"username":"hironsan","rolename":"annotator"}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '73'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:51 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/99/members
+  response:
+    body:
+      string: '[{"id":116,"user":1,"role":1,"username":"admin","rolename":"project_admin"},{"id":117,"user":3,"role":2,"username":"hironsan","rolename":"annotator"}]'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '150'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:51 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/99/members/117
+  response:
+    body:
+      string: '{"id":117,"user":3,"role":2,"username":"hironsan","rolename":"annotator"}'
+    headers:
+      Allow:
+      - GET, PUT, PATCH, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '73'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:51 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/roles
+  response:
+    body:
+      string: '[{"id":1,"name":"project_admin"},{"id":2,"name":"annotator"},{"id":3,"name":"annotation_approver"}]'
+    headers:
+      Allow:
+      - GET, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '99'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:51 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: '{"id": 117, "user": 3, "role": 1}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '33'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: PUT
+    uri: http://localhost:8000/v1/projects/99/members/117
+  response:
+    body:
+      string: '{"id":117,"user":3,"role":1,"username":"hironsan","rolename":"project_admin"}'
+    headers:
+      Allow:
+      - GET, PUT, PATCH, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '77'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:51 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/99/members
+  response:
+    body:
+      string: '[{"id":116,"user":1,"role":1,"username":"admin","rolename":"project_admin"},{"id":117,"user":3,"role":1,"username":"hironsan","rolename":"project_admin"}]'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '154'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:51 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/usecase/fixtures/project/delete.yaml b/tests/integration/usecase/fixtures/project/delete.yaml
new file mode 100644
index 0000000..42d2f45
--- /dev/null
+++ b/tests/integration/usecase/fixtures/project/delete.yaml
@@ -0,0 +1,160 @@
+interactions:
+- request:
+    body: '{"name": "test", "description": "test", "guideline": "", "project_type":
+      "DocumentClassification", "random_order": false, "collaborative_annotation":
+      false, "single_class_classification": false, "allow_overlapping": false, "grapheme_mode":
+      false, "use_relation": false, "tags": [], "resourcetype": "TextClassificationProject"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '326'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects
+  response:
+    body:
+      string: '{"id":102,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T03:25:51.500464Z","updated_at":"2022-10-07T03:25:51.500477Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '472'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:51 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '0'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: DELETE
+    uri: http://localhost:8000/v1/projects/102
+  response:
+    body:
+      string: ''
+    headers:
+      Allow:
+      - GET, PUT, PATCH, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '0'
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:51 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 204
+      message: No Content
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/102
+  response:
+    body:
+      string: '{"detail":"Not found."}'
+    headers:
+      Allow:
+      - GET, PUT, PATCH, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '23'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:51 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 404
+      message: Not Found
+version: 1
diff --git a/tests/integration/usecase/fixtures/project/find_by_id.yaml b/tests/integration/usecase/fixtures/project/find_by_id.yaml
new file mode 100644
index 0000000..97b9c47
--- /dev/null
+++ b/tests/integration/usecase/fixtures/project/find_by_id.yaml
@@ -0,0 +1,109 @@
+interactions:
+- request:
+    body: '{"name": "test", "description": "test", "guideline": "", "project_type":
+      "DocumentClassification", "random_order": false, "collaborative_annotation":
+      false, "single_class_classification": false, "allow_overlapping": false, "grapheme_mode":
+      false, "use_relation": false, "tags": [], "resourcetype": "TextClassificationProject"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '326'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects
+  response:
+    body:
+      string: '{"id":100,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T03:25:51.354609Z","updated_at":"2022-10-07T03:25:51.354622Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '472'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:51 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/100
+  response:
+    body:
+      string: '{"id":100,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T03:25:51.354609Z","updated_at":"2022-10-07T03:25:51.354622Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}'
+    headers:
+      Allow:
+      - GET, PUT, PATCH, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '472'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:51 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/usecase/fixtures/project/list.yaml b/tests/integration/usecase/fixtures/project/list.yaml
new file mode 100644
index 0000000..4b7ccdd
--- /dev/null
+++ b/tests/integration/usecase/fixtures/project/list.yaml
@@ -0,0 +1,823 @@
+interactions:
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects
+  response:
+    body:
+      string: '{"count":80,"next":"http://localhost:8000/v1/projects?limit=5&offset=5","previous":null,"results":[{"id":13,"name":"text
+        classification","description":"class","guideline":"","project_type":"DocumentClassification","created_at":"2022-08-19T05:49:18.989295Z","updated_at":"2022-08-19T05:49:18.989306Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":15,"name":"My
+        Project","description":"My Project Description","guideline":"Please write
+        annotation guideline.","project_type":"DocumentClassification","created_at":"2022-09-27T05:02:26.928569Z","updated_at":"2022-09-27T05:02:26.928593Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":16,"name":"hoge","description":"fuga","guideline":"Please
+        write annotation guideline.","project_type":"DocumentClassification","created_at":"2022-09-27T10:14:14.302115Z","updated_at":"2022-09-27T10:14:14.302128Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":18,"name":"NER","description":"test
+        project","guideline":"","project_type":"SequenceLabeling","created_at":"2022-09-27T23:39:38.927284Z","updated_at":"2022-09-27T23:39:38.927308Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":false,"can_define_span":true,"tags":[],"allow_overlapping":false,"grapheme_mode":false,"use_relation":false,"resourcetype":"SequenceLabelingProject"},{"id":20,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-06T23:45:21.788202Z","updated_at":"2022-10-06T23:45:21.788253Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}]}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '2636'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:51 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects?limit=5&offset=5
+  response:
+    body:
+      string: '{"count":80,"next":"http://localhost:8000/v1/projects?limit=5&offset=10","previous":"http://localhost:8000/v1/projects?limit=5","results":[{"id":21,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-06T23:45:22.003286Z","updated_at":"2022-10-06T23:45:22.003301Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":23,"name":"test2","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-06T23:47:17.843438Z","updated_at":"2022-10-06T23:47:18.114101Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":24,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-06T23:59:25.023017Z","updated_at":"2022-10-06T23:59:25.023045Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":25,"name":"test2","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-06T23:59:25.100692Z","updated_at":"2022-10-06T23:59:25.138262Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":27,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T00:04:01.615361Z","updated_at":"2022-10-07T00:04:01.615385Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}]}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '2502'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:51 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects?limit=5&offset=10
+  response:
+    body:
+      string: '{"count":80,"next":"http://localhost:8000/v1/projects?limit=5&offset=15","previous":"http://localhost:8000/v1/projects?limit=5&offset=5","results":[{"id":28,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T00:04:01.895548Z","updated_at":"2022-10-07T00:04:01.895577Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":29,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T00:04:02.081541Z","updated_at":"2022-10-07T00:04:02.081556Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":30,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T00:08:04.706644Z","updated_at":"2022-10-07T00:08:04.706670Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":31,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T00:08:04.916851Z","updated_at":"2022-10-07T00:08:04.916868Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":32,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T00:19:03.370767Z","updated_at":"2022-10-07T00:19:03.370791Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}]}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '2509'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:51 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects?limit=5&offset=15
+  response:
+    body:
+      string: '{"count":80,"next":"http://localhost:8000/v1/projects?limit=5&offset=20","previous":"http://localhost:8000/v1/projects?limit=5&offset=10","results":[{"id":33,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T00:19:03.585853Z","updated_at":"2022-10-07T00:19:03.585892Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":34,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T00:19:03.743982Z","updated_at":"2022-10-07T00:19:03.744001Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":35,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T00:19:03.859430Z","updated_at":"2022-10-07T00:19:03.859450Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":36,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T00:21:07.620790Z","updated_at":"2022-10-07T00:21:07.620853Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":37,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T00:21:07.804738Z","updated_at":"2022-10-07T00:21:07.804755Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}]}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '2510'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:51 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects?limit=5&offset=20
+  response:
+    body:
+      string: '{"count":80,"next":"http://localhost:8000/v1/projects?limit=5&offset=25","previous":"http://localhost:8000/v1/projects?limit=5&offset=15","results":[{"id":38,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T00:21:08.077801Z","updated_at":"2022-10-07T00:21:08.077813Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":39,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T00:21:08.250450Z","updated_at":"2022-10-07T00:21:08.250547Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":40,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T00:33:57.626032Z","updated_at":"2022-10-07T00:33:57.626056Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":41,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T00:33:57.868979Z","updated_at":"2022-10-07T00:33:57.868996Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":42,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T00:33:57.993381Z","updated_at":"2022-10-07T00:33:57.993405Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}]}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '2510'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:51 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects?limit=5&offset=25
+  response:
+    body:
+      string: '{"count":80,"next":"http://localhost:8000/v1/projects?limit=5&offset=30","previous":"http://localhost:8000/v1/projects?limit=5&offset=20","results":[{"id":43,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T00:33:58.079325Z","updated_at":"2022-10-07T00:33:58.079341Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":44,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T00:37:58.879165Z","updated_at":"2022-10-07T00:37:58.879192Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":45,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T00:38:46.823318Z","updated_at":"2022-10-07T00:38:46.823345Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":46,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T00:42:00.966987Z","updated_at":"2022-10-07T00:42:00.967008Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":47,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T01:44:38.265246Z","updated_at":"2022-10-07T01:44:38.265272Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}]}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '2510'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:51 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects?limit=5&offset=30
+  response:
+    body:
+      string: '{"count":80,"next":"http://localhost:8000/v1/projects?limit=5&offset=35","previous":"http://localhost:8000/v1/projects?limit=5&offset=25","results":[{"id":48,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T01:45:12.279877Z","updated_at":"2022-10-07T01:45:12.279912Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":49,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T01:45:31.726949Z","updated_at":"2022-10-07T01:45:31.726971Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":50,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T01:50:22.397427Z","updated_at":"2022-10-07T01:50:22.397479Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":51,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T01:52:21.130742Z","updated_at":"2022-10-07T01:52:21.130768Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":52,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T01:55:13.375198Z","updated_at":"2022-10-07T01:55:13.375240Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}]}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '2510'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:51 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects?limit=5&offset=35
+  response:
+    body:
+      string: '{"count":80,"next":"http://localhost:8000/v1/projects?limit=5&offset=40","previous":"http://localhost:8000/v1/projects?limit=5&offset=30","results":[{"id":53,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T01:57:38.657343Z","updated_at":"2022-10-07T01:57:38.657374Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":54,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T01:58:34.070243Z","updated_at":"2022-10-07T01:58:34.070271Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":55,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T02:43:17.474659Z","updated_at":"2022-10-07T02:43:17.474675Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":56,"name":"test2","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T02:43:17.610568Z","updated_at":"2022-10-07T02:43:17.709865Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":58,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T02:43:17.843409Z","updated_at":"2022-10-07T02:43:17.843422Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}]}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '2511'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:51 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects?limit=5&offset=40
+  response:
+    body:
+      string: '{"count":80,"next":"http://localhost:8000/v1/projects?limit=5&offset=45","previous":"http://localhost:8000/v1/projects?limit=5&offset=35","results":[{"id":59,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T02:43:17.958197Z","updated_at":"2022-10-07T02:43:17.958219Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":60,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T02:43:18.103052Z","updated_at":"2022-10-07T02:43:18.103076Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":61,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T02:43:18.292942Z","updated_at":"2022-10-07T02:43:18.292986Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":62,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T02:43:18.457335Z","updated_at":"2022-10-07T02:43:18.457358Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":63,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T02:43:18.558308Z","updated_at":"2022-10-07T02:43:18.558327Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}]}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '2510'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:51 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects?limit=5&offset=45
+  response:
+    body:
+      string: '{"count":80,"next":"http://localhost:8000/v1/projects?limit=5&offset=50","previous":"http://localhost:8000/v1/projects?limit=5&offset=40","results":[{"id":64,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T02:43:18.655880Z","updated_at":"2022-10-07T02:43:18.655892Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":65,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T02:43:18.733655Z","updated_at":"2022-10-07T02:43:18.733678Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":66,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T02:43:18.786311Z","updated_at":"2022-10-07T02:43:18.786325Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":67,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T02:43:18.873830Z","updated_at":"2022-10-07T02:43:18.873846Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":68,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T02:43:18.942774Z","updated_at":"2022-10-07T02:43:18.942790Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}]}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '2510'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:51 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects?limit=5&offset=50
+  response:
+    body:
+      string: '{"count":80,"next":"http://localhost:8000/v1/projects?limit=5&offset=55","previous":"http://localhost:8000/v1/projects?limit=5&offset=45","results":[{"id":69,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T02:43:19.055336Z","updated_at":"2022-10-07T02:43:19.055365Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":70,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T02:43:19.099537Z","updated_at":"2022-10-07T02:43:19.099547Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":71,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T02:55:52.467255Z","updated_at":"2022-10-07T02:55:52.467274Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":72,"name":"test2","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T02:55:52.514849Z","updated_at":"2022-10-07T02:55:52.548743Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":74,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T02:55:52.629876Z","updated_at":"2022-10-07T02:55:52.629896Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}]}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '2511'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:51 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects?limit=5&offset=55
+  response:
+    body:
+      string: '{"count":80,"next":"http://localhost:8000/v1/projects?limit=5&offset=60","previous":"http://localhost:8000/v1/projects?limit=5&offset=50","results":[{"id":75,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T02:55:52.700943Z","updated_at":"2022-10-07T02:55:52.700960Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":76,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T02:55:52.811079Z","updated_at":"2022-10-07T02:55:52.811090Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":77,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T02:55:52.943692Z","updated_at":"2022-10-07T02:55:52.943701Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":78,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T02:55:52.987130Z","updated_at":"2022-10-07T02:55:52.987149Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":79,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T02:55:53.053653Z","updated_at":"2022-10-07T02:55:53.053672Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}]}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '2510'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:51 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects?limit=5&offset=60
+  response:
+    body:
+      string: '{"count":80,"next":"http://localhost:8000/v1/projects?limit=5&offset=65","previous":"http://localhost:8000/v1/projects?limit=5&offset=55","results":[{"id":80,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T02:55:53.121517Z","updated_at":"2022-10-07T02:55:53.121540Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":81,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T02:55:53.189303Z","updated_at":"2022-10-07T02:55:53.189316Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":82,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T02:55:53.221852Z","updated_at":"2022-10-07T02:55:53.221869Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":83,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T02:55:53.288540Z","updated_at":"2022-10-07T02:55:53.288558Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":84,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T02:55:53.343090Z","updated_at":"2022-10-07T02:55:53.343101Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}]}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '2510'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:51 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects?limit=5&offset=65
+  response:
+    body:
+      string: '{"count":80,"next":"http://localhost:8000/v1/projects?limit=5&offset=70","previous":"http://localhost:8000/v1/projects?limit=5&offset=60","results":[{"id":85,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T02:55:53.400006Z","updated_at":"2022-10-07T02:55:53.400023Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":86,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T02:55:53.447416Z","updated_at":"2022-10-07T02:55:53.447493Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":87,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T03:25:49.966391Z","updated_at":"2022-10-07T03:25:49.966405Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":88,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T03:25:50.063969Z","updated_at":"2022-10-07T03:25:50.063983Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":89,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T03:25:50.119934Z","updated_at":"2022-10-07T03:25:50.119946Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}]}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '2510'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:51 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects?limit=5&offset=70
+  response:
+    body:
+      string: '{"count":80,"next":"http://localhost:8000/v1/projects?limit=5&offset=75","previous":"http://localhost:8000/v1/projects?limit=5&offset=65","results":[{"id":90,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T03:25:50.182562Z","updated_at":"2022-10-07T03:25:50.182581Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":91,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T03:25:50.343214Z","updated_at":"2022-10-07T03:25:50.343238Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":92,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T03:25:50.423638Z","updated_at":"2022-10-07T03:25:50.423656Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":93,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T03:25:50.579588Z","updated_at":"2022-10-07T03:25:50.579598Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":94,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T03:25:50.627164Z","updated_at":"2022-10-07T03:25:50.627177Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}]}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '2510'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:51 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects?limit=5&offset=75
+  response:
+    body:
+      string: '{"count":80,"next":null,"previous":"http://localhost:8000/v1/projects?limit=5&offset=70","results":[{"id":95,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T03:25:50.700509Z","updated_at":"2022-10-07T03:25:50.700526Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":96,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T03:25:50.771388Z","updated_at":"2022-10-07T03:25:50.771402Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":97,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T03:25:50.818363Z","updated_at":"2022-10-07T03:25:50.818379Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":98,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T03:25:50.888658Z","updated_at":"2022-10-07T03:25:50.888669Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"},{"id":99,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T03:25:50.973666Z","updated_at":"2022-10-07T03:25:50.973683Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}]}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '2461'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:51 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/usecase/fixtures/project/update.yaml b/tests/integration/usecase/fixtures/project/update.yaml
new file mode 100644
index 0000000..fd245e6
--- /dev/null
+++ b/tests/integration/usecase/fixtures/project/update.yaml
@@ -0,0 +1,165 @@
+interactions:
+- request:
+    body: '{"name": "test", "description": "test", "guideline": "", "project_type":
+      "DocumentClassification", "random_order": false, "collaborative_annotation":
+      false, "single_class_classification": false, "allow_overlapping": false, "grapheme_mode":
+      false, "use_relation": false, "tags": [], "resourcetype": "TextClassificationProject"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '326'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: POST
+    uri: http://localhost:8000/v1/projects
+  response:
+    body:
+      string: '{"id":101,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T03:25:51.397530Z","updated_at":"2022-10-07T03:25:51.397545Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}'
+    headers:
+      Allow:
+      - GET, POST, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '472'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:51 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 201
+      message: Created
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/projects/101
+  response:
+    body:
+      string: '{"id":101,"name":"test","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T03:25:51.397530Z","updated_at":"2022-10-07T03:25:51.397545Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}'
+    headers:
+      Allow:
+      - GET, PUT, PATCH, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '472'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:51 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+- request:
+    body: '{"id": 101, "name": "test2", "description": "test", "guideline": "", "project_type":
+      "DocumentClassification", "random_order": false, "collaborative_annotation":
+      false, "single_class_classification": false, "allow_overlapping": false, "grapheme_mode":
+      false, "use_relation": false, "tags": [], "resourcetype": "TextClassificationProject"}'
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '338'
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: PUT
+    uri: http://localhost:8000/v1/projects/101
+  response:
+    body:
+      string: '{"id":101,"name":"test2","description":"test","guideline":"","project_type":"DocumentClassification","created_at":"2022-10-07T03:25:51.397530Z","updated_at":"2022-10-07T03:25:51.429057Z","random_order":false,"author":"admin","collaborative_annotation":false,"single_class_classification":false,"is_text_project":true,"can_define_label":true,"can_define_relation":false,"can_define_category":true,"can_define_span":false,"tags":[],"resourcetype":"TextClassificationProject"}'
+    headers:
+      Allow:
+      - GET, PUT, PATCH, DELETE, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '473'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:51 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/usecase/fixtures/role/list.yaml b/tests/integration/usecase/fixtures/role/list.yaml
new file mode 100644
index 0000000..9d82b02
--- /dev/null
+++ b/tests/integration/usecase/fixtures/role/list.yaml
@@ -0,0 +1,53 @@
+interactions:
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/roles
+  response:
+    body:
+      string: '[{"id":1,"name":"project_admin"},{"id":2,"name":"annotator"},{"id":3,"name":"annotation_approver"}]'
+    headers:
+      Allow:
+      - GET, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '99'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:51 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/usecase/fixtures/user/find_by_name.yaml b/tests/integration/usecase/fixtures/user/find_by_name.yaml
new file mode 100644
index 0000000..185f017
--- /dev/null
+++ b/tests/integration/usecase/fixtures/user/find_by_name.yaml
@@ -0,0 +1,53 @@
+interactions:
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/users?q=admin
+  response:
+    body:
+      string: '[{"id":1,"username":"admin","is_superuser":true,"is_staff":true}]'
+    headers:
+      Allow:
+      - GET, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '65'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:51 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/usecase/fixtures/user/get_profile.yaml b/tests/integration/usecase/fixtures/user/get_profile.yaml
new file mode 100644
index 0000000..8983192
--- /dev/null
+++ b/tests/integration/usecase/fixtures/user/get_profile.yaml
@@ -0,0 +1,53 @@
+interactions:
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/me
+  response:
+    body:
+      string: '{"id":1,"username":"admin","is_superuser":true,"is_staff":true}'
+    headers:
+      Allow:
+      - GET, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '63'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:51 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/usecase/fixtures/user/search.yaml b/tests/integration/usecase/fixtures/user/search.yaml
new file mode 100644
index 0000000..185f017
--- /dev/null
+++ b/tests/integration/usecase/fixtures/user/search.yaml
@@ -0,0 +1,53 @@
+interactions:
+- request:
+    body: null
+    headers:
+      Accept-Encoding:
+      - gzip, deflate
+      Connection:
+      - keep-alive
+      Cookie:
+      - csrftoken=8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7;
+        sessionid=05mdqwzdqazpupw2xq3w5m7p21gfrxzr
+      User-Agent:
+      - python-requests/2.28.1
+      X-CSRFToken:
+      - 8WsdoYOHMb9Cxy5nKV3Uj4FSWuYND2pnkTh6cognZUKF9sXFA0gBujvymKW6I5A7
+      accept:
+      - application/json
+      content-type:
+      - application/json
+      referer:
+      - http://localhost:8000
+    method: GET
+    uri: http://localhost:8000/v1/users?q=admin
+  response:
+    body:
+      string: '[{"id":1,"username":"admin","is_superuser":true,"is_staff":true}]'
+    headers:
+      Allow:
+      - GET, HEAD, OPTIONS
+      Connection:
+      - close
+      Content-Length:
+      - '65'
+      Content-Type:
+      - application/json
+      Cross-Origin-Opener-Policy:
+      - same-origin
+      Date:
+      - Fri, 07 Oct 2022 03:25:51 GMT
+      Referrer-Policy:
+      - same-origin
+      Server:
+      - gunicorn
+      Vary:
+      - Accept, Origin, Cookie
+      X-Content-Type-Options:
+      - nosniff
+      X-Frame-Options:
+      - DENY
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/tests/integration/usecase/test_comment.py b/tests/integration/usecase/test_comment.py
new file mode 100644
index 0000000..aa66746
--- /dev/null
+++ b/tests/integration/usecase/test_comment.py
@@ -0,0 +1,47 @@
+import pytest
+import vcr
+
+from doccano_client.repositories.base import BaseRepository
+from doccano_client.repositories.comment import CommentRepository
+from doccano_client.repositories.example import ExampleRepository
+from doccano_client.repositories.project import ProjectRepository
+from doccano_client.usecase.comment import CommentUseCase
+from doccano_client.usecase.example import ExampleUseCase
+from doccano_client.usecase.project import ProjectUseCase
+from tests.conftest import usecase_fixtures
+
+
+class TestCommentUseCase:
+    @classmethod
+    def setup_class(cls):
+        with vcr.use_cassette(str(usecase_fixtures / "login.yaml"), mode="once"):
+            base = BaseRepository("http://localhost:8000")
+            base.login(username="admin", password="password")
+            cls.project = ProjectUseCase(ProjectRepository(base))
+            cls.example = ExampleUseCase(ExampleRepository(base))
+            cls.comment = CommentUseCase(CommentRepository(base))
+
+    def test_create_comment(self):
+        with vcr.use_cassette(str(usecase_fixtures / "comment/create.yaml"), mode="once"):
+            project = self.project.create(name="test", project_type="DocumentClassification", description="test")
+            example = self.example.create(project.id, text="test")
+            comment = self.comment.create(project.id, example.id, text="test")
+            assert comment.text == "test"
+
+    def test_update_comment(self):
+        with vcr.use_cassette(str(usecase_fixtures / "comment/update.yaml"), mode="once"):
+            project = self.project.create(name="test", project_type="DocumentClassification", description="test")
+            example = self.example.create(project.id, text="test")
+            comment = self.comment.create(project.id, example.id, text="test")
+            comment.text = "test2"
+            updated_comment = self.comment.update(project.id, comment.id, text=comment.text)
+            assert comment == updated_comment
+
+    def test_delete_comment(self):
+        with vcr.use_cassette(str(usecase_fixtures / "comment/delete.yaml"), mode="once"):
+            project = self.project.create(name="test", project_type="DocumentClassification", description="test")
+            example = self.example.create(project.id, text="test")
+            comment = self.comment.create(project.id, example.id, text="test")
+            self.comment.delete(project.id, comment.id)
+            with pytest.raises(Exception):
+                self.comment.find_by_id(project.id, comment.id)
diff --git a/tests/integration/usecase/test_data_download.py b/tests/integration/usecase/test_data_download.py
new file mode 100644
index 0000000..c073673
--- /dev/null
+++ b/tests/integration/usecase/test_data_download.py
@@ -0,0 +1,49 @@
+import pathlib
+
+import vcr
+
+from doccano_client.repositories.base import BaseRepository
+from doccano_client.repositories.data_download import DataDownloadRepository
+from doccano_client.repositories.data_upload import DataUploadRepository
+from doccano_client.repositories.project import ProjectRepository
+from doccano_client.repositories.task_status import TaskStatusRepository
+from doccano_client.usecase.data_download import DataDownloadUseCase
+from doccano_client.usecase.data_upload import DataUploadUseCase
+from doccano_client.usecase.project import ProjectUseCase
+from tests.conftest import usecase_fixtures
+
+DATA_DIR = pathlib.Path(__file__).parent.parent / "data"
+
+
+class TestDataDownloadUseCase:
+    @classmethod
+    def setup_class(cls):
+        with vcr.use_cassette(str(usecase_fixtures / "login.yaml"), mode="once"):
+            base = BaseRepository("http://localhost:8000")
+            base.login(username="admin", password="password")
+            cls.project = ProjectUseCase(ProjectRepository(base))
+            cls.data_download = DataDownloadUseCase(
+                DataDownloadRepository(base),
+                TaskStatusRepository(base),
+            )
+            cls.data_import = DataUploadUseCase(
+                DataUploadRepository(base),
+                TaskStatusRepository(base),
+            )
+
+    def test_data_download(self):
+        with vcr.use_cassette(str(usecase_fixtures / "data_download/download.yaml"), mode="once"):
+            project = self.project.create(name="test", project_type="DocumentClassification", description="test")
+            file_path = DATA_DIR / "classification.jsonl"
+            self.data_import.upload(
+                project.id,
+                file_paths=[file_path],
+                task="DocumentClassification",
+                format="JSONL",
+                column_data="text",
+                column_label="labels",
+            )
+            path = self.data_download.download(project.id, format="JSONL")
+            assert path.exists()
+            assert path.stat().st_size > 0
+            path.unlink()
diff --git a/tests/integration/usecase/test_data_upload.py b/tests/integration/usecase/test_data_upload.py
new file mode 100644
index 0000000..626b536
--- /dev/null
+++ b/tests/integration/usecase/test_data_upload.py
@@ -0,0 +1,44 @@
+import pathlib
+
+import vcr
+
+from doccano_client.repositories.base import BaseRepository
+from doccano_client.repositories.data_upload import DataUploadRepository
+from doccano_client.repositories.example import ExampleRepository
+from doccano_client.repositories.project import ProjectRepository
+from doccano_client.repositories.task_status import TaskStatusRepository
+from doccano_client.usecase.data_upload import DataUploadUseCase
+from doccano_client.usecase.example import ExampleUseCase
+from doccano_client.usecase.project import ProjectUseCase
+from tests.conftest import usecase_fixtures
+
+DATA_DIR = pathlib.Path(__file__).parent.parent / "data"
+
+
+class TestDataUploadUseCase:
+    @classmethod
+    def setup_class(cls):
+        with vcr.use_cassette(str(usecase_fixtures / "login.yaml"), mode="once"):
+            base = BaseRepository("http://localhost:8000")
+            base.login(username="admin", password="password")
+            cls.project = ProjectUseCase(ProjectRepository(base))
+            cls.example = ExampleUseCase(ExampleRepository(base))
+            cls.data_upload = DataUploadUseCase(
+                DataUploadRepository(base),
+                TaskStatusRepository(base),
+            )
+
+    def test_data_upload(self):
+        with vcr.use_cassette(str(usecase_fixtures / "data_upload/upload.yaml"), mode="once"):
+            project = self.project.create(name="test", project_type="DocumentClassification", description="test")
+            file_path = DATA_DIR / "classification.jsonl"
+            self.data_upload.upload(
+                project.id,
+                file_paths=[file_path],
+                task="DocumentClassification",
+                format="JSONL",
+                column_data="text",
+                column_label="labels",
+            )
+            examples = self.example.list(project.id)
+            assert len(list(examples)) == 3
diff --git a/tests/integration/usecase/test_example.py b/tests/integration/usecase/test_example.py
new file mode 100644
index 0000000..8578333
--- /dev/null
+++ b/tests/integration/usecase/test_example.py
@@ -0,0 +1,49 @@
+import pytest
+import vcr
+
+from doccano_client.repositories.base import BaseRepository
+from doccano_client.repositories.example import ExampleRepository
+from doccano_client.repositories.project import ProjectRepository
+from doccano_client.usecase.example import ExampleUseCase
+from doccano_client.usecase.project import ProjectUseCase
+from tests.conftest import usecase_fixtures
+
+
+class TestExampleUseCase:
+    @classmethod
+    def setup_class(cls):
+        with vcr.use_cassette(str(usecase_fixtures / "login.yaml"), mode="once"):
+            base = BaseRepository("http://localhost:8000")
+            base.login(username="admin", password="password")
+            cls.project = ProjectUseCase(ProjectRepository(base))
+            cls.example = ExampleUseCase(ExampleRepository(base))
+
+    def test_create_example(self):
+        with vcr.use_cassette(str(usecase_fixtures / "example/create.yaml"), mode="once"):
+            project = self.project.create(name="test", project_type="DocumentClassification", description="test")
+            example = self.example.create(project.id, text="test")
+            assert example.text == "test"
+
+    def test_update_example(self):
+        with vcr.use_cassette(str(usecase_fixtures / "example/update.yaml"), mode="once"):
+            project = self.project.create(name="test", project_type="DocumentClassification", description="test")
+            example = self.example.create(project.id, text="test")
+            example.text = "test2"
+            updated_example = self.example.update(project.id, example.id, text="test2")
+            assert example == updated_example
+
+    def test_delete_example(self):
+        with vcr.use_cassette(str(usecase_fixtures / "example/delete.yaml"), mode="once"):
+            project = self.project.create(name="test", project_type="DocumentClassification", description="test")
+            example = self.example.create(project.id, text="test")
+            self.example.delete(project.id, example.id)
+            with pytest.raises(Exception):
+                self.example.find_by_id(project.id, example.id)
+
+    def test_delete_all_examples(self):
+        with vcr.use_cassette(str(usecase_fixtures / "example/delete_all.yaml"), mode="once"):
+            project = self.project.create(name="test", project_type="DocumentClassification", description="test")
+            self.example.create(project.id, text="test")
+            self.example.delete_all(project.id)
+            examples = self.example.list(project.id)
+            assert len(list(examples)) == 0
diff --git a/tests/integration/usecase/test_label.py b/tests/integration/usecase/test_label.py
new file mode 100644
index 0000000..26e3489
--- /dev/null
+++ b/tests/integration/usecase/test_label.py
@@ -0,0 +1,60 @@
+import pathlib
+
+import pytest
+import vcr
+
+from doccano_client.repositories.base import BaseRepository
+from doccano_client.repositories.example import ExampleRepository
+from doccano_client.repositories.label import CategoryRepository
+from doccano_client.repositories.label_type import CategoryTypeRepository
+from doccano_client.repositories.project import ProjectRepository
+from doccano_client.services.label_type import LabelTypeService
+from doccano_client.usecase.example import ExampleUseCase
+from doccano_client.usecase.label import CategoryUseCase
+from doccano_client.usecase.label_type import LabelTypeUseCase
+from doccano_client.usecase.project import ProjectUseCase
+from tests.conftest import usecase_fixtures
+
+DATA_DIR = pathlib.Path(__file__).parent.parent / "data"
+
+
+class TestCategoryUseCase:
+    @classmethod
+    def setup_class(cls):
+        with vcr.use_cassette(str(usecase_fixtures / "login.yaml"), mode="once"):
+            base = BaseRepository("http://localhost:8000")
+            base.login(username="admin", password="password")
+            cls.project = ProjectUseCase(ProjectRepository(base))
+            cls.example = ExampleUseCase(ExampleRepository(base))
+            cls.category_type = LabelTypeUseCase(
+                CategoryTypeRepository(base), LabelTypeService(CategoryTypeRepository(base))
+            )
+            cls.category = CategoryUseCase(CategoryRepository(base), CategoryTypeRepository(base))
+
+    def test_create_label(self):
+        with vcr.use_cassette(str(usecase_fixtures / "label/create.yaml"), mode="once"):
+            project = self.project.create(name="test", project_type="DocumentClassification", description="test")
+            example = self.example.create(project.id, text="test")
+            category_type = self.category_type.create(project.id, text="text")
+            category = self.category.create(project.id, example.id, label="text")
+            assert category.label == category_type.id
+
+    def test_update_label(self):
+        with vcr.use_cassette(str(usecase_fixtures / "label/update.yaml"), mode="once"):
+            project = self.project.create(name="test", project_type="DocumentClassification", description="test")
+            example = self.example.create(project.id, text="test")
+            self.category_type.create(project.id, text="text")
+            category_type = self.category_type.create(project.id, text="text2")
+            category = self.category.create(project.id, example.id, label="text")
+            category = self.category.update(project.id, example.id, category.id, label="text2")
+            assert category.label == category_type.id
+
+    def test_delete_label(self):
+        with vcr.use_cassette(str(usecase_fixtures / "label/delete.yaml"), mode="once"):
+            project = self.project.create(name="test", project_type="DocumentClassification", description="test")
+            example = self.example.create(project.id, text="test")
+            self.category_type.create(project.id, text="text")
+            category = self.category.create(project.id, example.id, label="text")
+            self.category.delete(project.id, example.id, category.id)
+            with pytest.raises(Exception):
+                self.category.find_by_id(project.id, example.id, category.id)
diff --git a/tests/integration/usecase/test_label_type.py b/tests/integration/usecase/test_label_type.py
new file mode 100644
index 0000000..85967dc
--- /dev/null
+++ b/tests/integration/usecase/test_label_type.py
@@ -0,0 +1,56 @@
+import pathlib
+
+import pytest
+import vcr
+
+from doccano_client.repositories.base import BaseRepository
+from doccano_client.repositories.label_type import CategoryTypeRepository
+from doccano_client.repositories.project import ProjectRepository
+from doccano_client.services.label_type import LabelTypeService
+from doccano_client.usecase.label_type import LabelTypeUseCase
+from doccano_client.usecase.project import ProjectUseCase
+from tests.conftest import usecase_fixtures
+
+DATA_DIR = pathlib.Path(__file__).parent.parent / "data"
+
+
+class TestLabelTypeUseCase:
+    @classmethod
+    def setup_class(cls):
+        with vcr.use_cassette(str(usecase_fixtures / "login.yaml"), mode="once"):
+            base = BaseRepository("http://localhost:8000")
+            base.login(username="admin", password="password")
+            cls.project = ProjectUseCase(ProjectRepository(base))
+            cls.label_type = LabelTypeUseCase(
+                CategoryTypeRepository(base), LabelTypeService(CategoryTypeRepository(base))
+            )
+
+    def test_create_label_type(self):
+        with vcr.use_cassette(str(usecase_fixtures / "label_type/create.yaml"), mode="once"):
+            project = self.project.create(name="test", project_type="DocumentClassification", description="test")
+            label_type = self.label_type.create(project.id, text="text")
+            assert label_type.text == "text"
+
+    def test_update_label_type(self):
+        with vcr.use_cassette(str(usecase_fixtures / "label_type/update.yaml"), mode="once"):
+            project = self.project.create(name="test", project_type="DocumentClassification", description="test")
+            label_type = self.label_type.create(project.id, text="text")
+            label_type.text = "text2"
+            updated_label_type = self.label_type.update(project.id, label_type_id=label_type.id, text="text2")
+            assert label_type == updated_label_type
+
+    def test_delete_label_type(self):
+        with vcr.use_cassette(str(usecase_fixtures / "label_type/delete.yaml"), mode="once"):
+            project = self.project.create(name="test", project_type="DocumentClassification", description="test")
+            label_type = self.label_type.create(project.id, text="text")
+            self.label_type.delete(project.id, label_type.id)
+            with pytest.raises(Exception):
+                self.label_type.find_by_id(project.id, label_type.id)
+
+    def test_upload_label_type(self):
+        with vcr.use_cassette(str(usecase_fixtures / "label_type/upload.yaml"), mode="once"):
+            project = self.project.create(name="test", project_type="DocumentClassification", description="test")
+            file_path = DATA_DIR / "labels.json"
+            self.label_type.upload(project.id, file_path=file_path)
+            label_types = self.label_type.list(project.id)
+            assert len(label_types) == 2
diff --git a/tests/integration/usecase/test_member.py b/tests/integration/usecase/test_member.py
new file mode 100644
index 0000000..4723802
--- /dev/null
+++ b/tests/integration/usecase/test_member.py
@@ -0,0 +1,54 @@
+import vcr
+
+from doccano_client.repositories.base import BaseRepository
+from doccano_client.repositories.member import MemberRepository
+from doccano_client.repositories.project import ProjectRepository
+from doccano_client.repositories.role import RoleRepository
+from doccano_client.repositories.user import UserRepository
+from doccano_client.usecase.member import MemberUseCase
+from doccano_client.usecase.project import ProjectUseCase
+from tests.conftest import usecase_fixtures
+
+
+class TestMemberUseCase:
+    @classmethod
+    def setup_class(cls):
+        with vcr.use_cassette(str(usecase_fixtures / "login.yaml"), mode="once"):
+            base = BaseRepository("http://localhost:8000")
+            base.login(username="admin", password="password")
+            cls.project = ProjectUseCase(ProjectRepository(base))
+            cls.member = MemberUseCase(MemberRepository(base), UserRepository(base), RoleRepository(base))
+
+    def list_members(self):
+        with vcr.use_cassette(str(usecase_fixtures / "member/list.yaml"), mode="once"):
+            project = self.project.create(name="test", project_type="DocumentClassification", description="test")
+            members = self.member.list(project.id)
+            assert len(members) == 1
+
+    def test_add_member(self):
+        with vcr.use_cassette(str(usecase_fixtures / "member/add.yaml"), mode="once"):
+            project = self.project.create(name="test", project_type="DocumentClassification", description="test")
+            self.member.add(project.id, "hironsan", "annotator")
+            members = self.member.list(project.id)
+            assert len(members) == 2
+
+    def test_delete_member(self):
+        with vcr.use_cassette(str(usecase_fixtures / "member/delete.yaml"), mode="once"):
+            project = self.project.create(name="test", project_type="DocumentClassification", description="test")
+            member = self.member.add(project.id, "hironsan", "annotator")
+            members = self.member.list(project.id)
+            assert len(members) == 2
+            self.member.delete(project.id, member.id)
+            members = self.member.list(project.id)
+            assert len(members) == 1
+
+    def test_update_member(self):
+        with vcr.use_cassette(str(usecase_fixtures / "member/update.yaml"), mode="once"):
+            project = self.project.create(name="test", project_type="DocumentClassification", description="test")
+            member = self.member.add(project.id, "hironsan", "annotator")
+            members = self.member.list(project.id)
+            assert len(members) == 2
+            self.member.update(project.id, member.id, "project_admin")
+            members = self.member.list(project.id)
+            assert len(members) == 2
+            assert members[1].rolename == "project_admin"
diff --git a/tests/integration/usecase/test_project.py b/tests/integration/usecase/test_project.py
new file mode 100644
index 0000000..83beecb
--- /dev/null
+++ b/tests/integration/usecase/test_project.py
@@ -0,0 +1,42 @@
+import pytest
+import vcr
+
+from doccano_client.models.project import Project
+from doccano_client.repositories.base import BaseRepository
+from doccano_client.repositories.project import ProjectRepository
+from doccano_client.usecase.project import ProjectUseCase
+from tests.conftest import usecase_fixtures
+
+
+class TestProjectUseCase:
+    @classmethod
+    def setup_class(cls):
+        with vcr.use_cassette(str(usecase_fixtures / "login.yaml"), mode="once"):
+            base = BaseRepository("http://localhost:8000")
+            base.login(username="admin", password="password")
+            cls.usecase = ProjectUseCase(ProjectRepository(base))
+
+    def test_list_projects(self):
+        with vcr.use_cassette(str(usecase_fixtures / "project/list.yaml"), mode="once"):
+            projects = self.usecase.list()
+            assert all(isinstance(project, Project) for project in projects)
+
+    def test_find_project_by_id(self):
+        with vcr.use_cassette(str(usecase_fixtures / "project/find_by_id.yaml"), mode="once"):
+            project = self.usecase.create(name="test", project_type="DocumentClassification", description="test")
+            found_project = self.usecase.find_by_id(project.id)
+            assert project == found_project
+
+    def test_update_project(self):
+        with vcr.use_cassette(str(usecase_fixtures / "project/update.yaml"), mode="once"):
+            project = self.usecase.create(name="test", project_type="DocumentClassification", description="test")
+            project.name = "test2"
+            updated_project = self.usecase.update(project.id, name="test2")
+            assert project == updated_project
+
+    def test_delete_project(self):
+        with vcr.use_cassette(str(usecase_fixtures / "project/delete.yaml"), mode="once"):
+            project = self.usecase.create(name="test", project_type="DocumentClassification", description="test")
+            self.usecase.delete(project)
+            with pytest.raises(Exception):
+                self.usecase.find_by_id(project.id)
diff --git a/tests/integration/usecase/test_role.py b/tests/integration/usecase/test_role.py
new file mode 100644
index 0000000..c793356
--- /dev/null
+++ b/tests/integration/usecase/test_role.py
@@ -0,0 +1,20 @@
+import vcr
+
+from doccano_client.repositories.base import BaseRepository
+from doccano_client.repositories.role import RoleRepository
+from doccano_client.usecase.role import RoleUseCase
+from tests.conftest import usecase_fixtures
+
+
+class TestRoleUseCase:
+    @classmethod
+    def setup_class(cls):
+        with vcr.use_cassette(str(usecase_fixtures / "login.yaml"), mode="once"):
+            base = BaseRepository("http://localhost:8000")
+            base.login(username="admin", password="password")
+            cls.usecase = RoleUseCase(RoleRepository(base))
+
+    def test_list_roles(self):
+        with vcr.use_cassette(str(usecase_fixtures / "role/list.yaml"), mode="once"):
+            roles = self.usecase.list()
+            assert len(roles) == 3
diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/unit/services/__init__.py b/tests/unit/services/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/unit/services/test_label_type.py b/tests/unit/services/test_label_type.py
new file mode 100644
index 0000000..a394adf
--- /dev/null
+++ b/tests/unit/services/test_label_type.py
@@ -0,0 +1,36 @@
+from unittest.mock import MagicMock
+
+from doccano_client.models.label_type import LabelType
+from doccano_client.services.label_type import LabelTypeService
+
+
+class TestLabelTypeService:
+    @classmethod
+    def setup_method(cls):
+        cls.repository = MagicMock()
+        cls.service = LabelTypeService(cls.repository)
+
+    def test_return_false_if_label_type_does_not_exist(self):
+        label_type = LabelType(id=1, text="Test")
+        self.repository.list.return_value = []
+        assert not self.service.exists(0, label_type)
+
+    def test_return_true_if_same_text_exists(self):
+        label_type = LabelType(id=1, text="Test")
+        self.repository.list.return_value = [LabelType(id=2, text="Test")]
+        assert self.service.exists(0, label_type)
+
+    def test_return_true_if_same_key_exists(self):
+        label_type = LabelType(id=1, text="Test", suffix_key="t")
+        self.repository.list.return_value = [LabelType(id=2, text="Test2", suffix_key="t")]
+        assert self.service.exists(0, label_type)
+
+    def test_return_false_if_same_key_exists_but_id_is_same(self):
+        label_type = LabelType(id=1, text="Test", suffix_key="t")
+        self.repository.list.return_value = [LabelType(id=1, text="Test2", suffix_key="t")]
+        assert not self.service.exists(0, label_type)
+
+    def test_return_false_if_same_text_exists_but_id_is_same(self):
+        label_type = LabelType(id=1, text="Test")
+        self.repository.list.return_value = [label_type]
+        assert not self.service.exists(0, label_type)
diff --git a/tests/unit/usecase/__init__.py b/tests/unit/usecase/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/unit/usecase/test_comment.py b/tests/unit/usecase/test_comment.py
new file mode 100644
index 0000000..d68fdcf
--- /dev/null
+++ b/tests/unit/usecase/test_comment.py
@@ -0,0 +1,47 @@
+from unittest.mock import MagicMock
+
+import pytest
+
+from doccano_client.models.comment import Comment
+from doccano_client.usecase.comment import CommentUseCase
+
+
+@pytest.fixture
+def payload():
+    return {
+        "text": "Test",
+    }
+
+
+class TestCommentUseCase:
+    @classmethod
+    def setup_method(cls):
+        cls.repository = MagicMock()
+        cls.usecase = CommentUseCase(cls.repository)
+
+    def test_find_by_id(self):
+        self.usecase.find_by_id(0, 1)
+        self.repository.find_by_id.assert_called_once_with(0, 1)
+
+    def test_list(self):
+        list(self.usecase.list(0, 1, ""))
+        self.repository.list.assert_called_once_with(0, 1, "")
+
+    def test_create(self, payload):
+        project_id = 0
+        example_id = 1
+        self.usecase.create(project_id, example_id, **payload)
+        payload["example"] = example_id
+        self.repository.create.assert_called_once_with(project_id, Comment.parse_obj(payload))
+
+    def test_update(self):
+        project_id = 0
+        comment = Comment(id=2, example=1, text="Test")
+        self.repository.find_by_id.return_value = comment
+        self.usecase.update(project_id, comment.id, text="New Comment")
+        comment.text = "New Comment"
+        self.repository.update.assert_called_once_with(project_id, comment)
+
+    def test_delete(self):
+        self.usecase.delete(0, 1)
+        self.repository.delete.assert_called_once_with(0, 1)
diff --git a/tests/unit/usecase/test_data_download.py b/tests/unit/usecase/test_data_download.py
new file mode 100644
index 0000000..ae710a8
--- /dev/null
+++ b/tests/unit/usecase/test_data_download.py
@@ -0,0 +1,27 @@
+from unittest.mock import MagicMock
+
+from doccano_client.models.data_download import Option
+from doccano_client.usecase.data_download import DataDownloadUseCase
+
+
+class TestDataDownloadUseCase:
+    @classmethod
+    def setup_method(cls):
+        cls.data_download_repository = MagicMock()
+        cls.task_status_repository = MagicMock()
+        cls.usecase = DataDownloadUseCase(cls.data_download_repository, cls.task_status_repository)
+
+    def test_list(self):
+        self.usecase.list_options(0)
+        self.data_download_repository.list_options.assert_called_once_with(0)
+
+    def test_download(self):
+        project_id = 0
+        task_id = "task_id"
+        option = Option(name="test", format="JSONL")
+        self.data_download_repository.schedule_download.return_value = task_id
+        self.data_download_repository.find_option_by_name.return_value = option
+        self.usecase.download(project_id, "JSONL", only_approved=True, dir_name=".")
+        self.data_download_repository.schedule_download.assert_called_once_with(project_id, option, True)
+        self.task_status_repository.wait.assert_called_once_with(task_id)
+        self.data_download_repository.download.assert_called_once_with(project_id, task_id, ".")
diff --git a/tests/unit/usecase/test_data_upload.py b/tests/unit/usecase/test_data_upload.py
new file mode 100644
index 0000000..28f9ac9
--- /dev/null
+++ b/tests/unit/usecase/test_data_upload.py
@@ -0,0 +1,31 @@
+from unittest.mock import MagicMock
+
+from doccano_client.usecase.data_upload import DataUploadUseCase
+
+
+class TestDataUploadUseCase:
+    @classmethod
+    def setup_method(cls):
+        cls.data_upload_repository = MagicMock()
+        cls.task_status_repository = MagicMock()
+        cls.usecase = DataUploadUseCase(cls.data_upload_repository, cls.task_status_repository)
+
+    def test_list(self):
+        self.usecase.list_options(0)
+        self.data_upload_repository.list_options.assert_called_once_with(0)
+
+    def test_upload(self):
+        project_id = 0
+        self.data_upload_repository.upload.return_value = "upload_id"
+        self.usecase.upload(
+            project_id,
+            file_paths=["test.txt"],
+            task="DocumentClassification",
+            format="JSONL",
+            column_data="text",
+            column_label="label",
+        )
+        self.data_upload_repository.upload.assert_called_once_with("test.txt")
+        self.data_upload_repository.ingest.assert_called_once_with(
+            project_id, ["upload_id"], "DocumentClassification", "JSONL", column_data="text", column_label="label"
+        )
diff --git a/tests/unit/usecase/test_example.py b/tests/unit/usecase/test_example.py
new file mode 100644
index 0000000..f670f9f
--- /dev/null
+++ b/tests/unit/usecase/test_example.py
@@ -0,0 +1,46 @@
+from unittest.mock import MagicMock
+
+import pytest
+
+from doccano_client.models.example import Example
+from doccano_client.usecase.example import ExampleUseCase
+
+
+@pytest.fixture
+def payload():
+    return {
+        "text": "Test",
+    }
+
+
+class TestExampleUseCase:
+    @classmethod
+    def setup_method(cls):
+        cls.repository = MagicMock()
+        cls.usecase = ExampleUseCase(cls.repository)
+
+    def test_find_by_id(self):
+        self.usecase.find_by_id(0, 1)
+        self.repository.find_by_id.assert_called_once_with(0, 1)
+
+    def test_list(self):
+        list(self.usecase.list(0))
+        self.repository.list.assert_called_once_with(0)
+
+    def test_create(self, payload):
+        project_id = 0
+        self.usecase.create(project_id, **payload)
+        self.repository.create.assert_called_once_with(project_id, Example.parse_obj(payload))
+
+    def test_update(self, payload):
+        project_id = 0
+        example = Example(id=1, text="Test text")
+        self.repository.find_by_id.return_value = example
+        self.usecase.update(project_id, example.id, **payload)
+        example.text = payload["text"]
+        self.repository.find_by_id.assert_called_once_with(project_id, example.id)
+        self.repository.update.assert_called_once_with(project_id, example)
+
+    def test_delete(self):
+        self.usecase.delete(0, 1)
+        self.repository.delete.assert_called_once_with(0, 1)
diff --git a/tests/unit/usecase/test_label.py b/tests/unit/usecase/test_label.py
new file mode 100644
index 0000000..8952732
--- /dev/null
+++ b/tests/unit/usecase/test_label.py
@@ -0,0 +1,58 @@
+from unittest.mock import MagicMock
+
+from doccano_client.models.label import Category, Label
+from doccano_client.usecase.label import CategoryUseCase, LabelUseCase
+
+
+class TestLabelUseCase:
+    @classmethod
+    def setup_method(cls):
+        cls.label_repository = MagicMock()
+        cls.label_type_repository = MagicMock()
+        cls.usecase = LabelUseCase(cls.label_repository, cls.label_type_repository)
+
+    def test_find_by_id(self):
+        self.usecase.find_by_id(0, 1, 2)
+        self.label_repository.find_by_id.assert_called_once_with(0, 1, 2)
+
+    def test_list(self):
+        self.usecase.list(0, 1)
+        self.label_repository.list.assert_called_once_with(0, 1)
+
+    def test_delete(self):
+        label = Label(id=2, example=1)
+        self.label_repository.find_by_id.return_value = label
+        self.usecase.delete(0, 1, 2)
+        self.label_repository.find_by_id.assert_called_once_with(0, 1, 2)
+        self.label_repository.delete.assert_called_once_with(0, label)
+
+    def test_delete_all(self):
+        self.usecase.delete_all(0, 1)
+        self.label_repository.delete_all.assert_called_once_with(0, 1)
+
+
+class TestCategoryUseCase:
+    @classmethod
+    def setup_method(cls):
+        cls.label_repository = MagicMock()
+        cls.label_type_repository = MagicMock()
+        cls.usecase = CategoryUseCase(cls.label_repository, cls.label_type_repository)
+        cls.name = "Test"
+        cls.project_id = 0
+        cls.example_id = 1
+        cls.label_type_id = 2
+        cls.label_id = 3
+        cls.category = Category(label=cls.label_type_id, example=cls.example_id)
+        cls.label_type_repository.find_by_name.return_value = MagicMock(id=cls.label_type_id)
+
+    def test_create(self):
+        self.usecase.create(self.project_id, self.example_id, self.name)
+        self.label_type_repository.find_by_name.assert_called_once_with(self.project_id, self.name)
+        self.label_repository.create.assert_called_once_with(self.project_id, self.category)
+
+    def test_update(self):
+        self.label_repository.find_by_id.return_value = self.category
+
+        self.usecase.update(self.project_id, self.example_id, self.label_id, self.name)
+        self.label_type_repository.find_by_name.assert_called_once_with(self.project_id, self.name)
+        self.label_repository.update.assert_called_once_with(self.project_id, self.category)
diff --git a/tests/unit/usecase/test_label_type.py b/tests/unit/usecase/test_label_type.py
new file mode 100644
index 0000000..6548c37
--- /dev/null
+++ b/tests/unit/usecase/test_label_type.py
@@ -0,0 +1,62 @@
+from unittest.mock import MagicMock
+
+import pytest
+
+from doccano_client.models.label_type import LabelType
+from doccano_client.usecase.label_type import LabelTypeUseCase
+
+
+@pytest.fixture
+def payload():
+    return {
+        "text": "Test Label Type",
+        "color": "#000000",
+    }
+
+
+class TestLabelTypeUseCase:
+    @classmethod
+    def setup_method(cls):
+        cls.label_type_repository = MagicMock()
+        cls.label_type_service = MagicMock()
+        cls.label_type_service.exists.return_value = False
+        cls.label_type_usecase = LabelTypeUseCase(cls.label_type_repository, cls.label_type_service)
+
+    def test_find_by_id(self):
+        self.label_type_usecase.find_by_id(0, 1)
+        self.label_type_repository.find_by_id.assert_called_once_with(0, 1)
+
+    def test_list(self):
+        self.label_type_usecase.list(0)
+        self.label_type_repository.list.assert_called_once_with(0)
+
+    def test_create(self, payload):
+        project_id = 0
+        self.label_type_usecase.create(project_id, **payload)
+        payload["background_color"] = payload.pop("color")
+        self.label_type_repository.create.assert_called_once_with(project_id, LabelType.parse_obj(payload))
+
+    def test_cannot_create_duplicate_label_type(self, payload):
+        project_id = 0
+        self.label_type_service.exists.return_value = True
+        with pytest.raises(ValueError):
+            self.label_type_usecase.create(project_id, **payload)
+
+    def test_update(self):
+        project_id = 0
+        label_type = LabelType(id=1, text="Test Label Type", background_color="#000000")
+        self.label_type_repository.find_by_id.return_value = label_type
+        self.label_type_usecase.update(project_id, label_type.id, text="New Label Type")
+        label_type.text = "New Label Type"
+        self.label_type_repository.update.assert_called_once_with(project_id, label_type)
+
+    def test_cannot_update_duplicate_label_type(self, payload):
+        project_id = 0
+        label_type_id = 1
+        self.label_type_service.exists.return_value = True
+        with pytest.raises(ValueError):
+            self.label_type_usecase.update(project_id, label_type_id, **payload)
+
+    def test_delete(self):
+        self.label_type_usecase.delete(0, 1)
+        self.label_type_repository.delete.assert_called_once_with(0, 1)
diff --git a/tests/unit/usecase/test_member.py b/tests/unit/usecase/test_member.py
new file mode 100644
index 0000000..24d31e5
--- /dev/null
+++ b/tests/unit/usecase/test_member.py
@@ -0,0 +1,55 @@
+from unittest.mock import MagicMock
+
+import pytest
+
+from doccano_client.models.member import Member
+from doccano_client.models.role import Role
+from doccano_client.models.user import User
+from doccano_client.usecase.member import MemberUseCase
+
+
+@pytest.fixture
+def payload():
+    return {"username": "admin", "role_name": "admin"}
+
+
+class TestMemberUseCase:
+    @classmethod
+    def setup_method(cls):
+        cls.member_repository = MagicMock()
+        cls.user_repository = MagicMock()
+        cls.role_repository = MagicMock()
+        cls.usecase = MemberUseCase(cls.member_repository, cls.user_repository, cls.role_repository)
+
+    def test_find_by_id(self):
+        self.usecase.find_by_id(0, 1)
+        self.member_repository.find_by_id.assert_called_once_with(0, 1)
+
+    def test_list(self):
+        self.usecase.list(0)
+        self.member_repository.list.assert_called_once_with(0)
+
+    def test_create(self, payload):
+        project_id = 0
+        role = Role(id=2, name="annotator")
+        user = User(id=1, username="admin", is_superuser=False, is_staff=False)
+        member = Member(id=None, user=user.id, role=role.id)
+        self.user_repository.find_by_username.return_value = user
+        self.role_repository.find_by_name.return_value = role
+        self.member_repository.create.return_value = member
+        self.usecase.add(project_id, **payload)
+        self.member_repository.create.assert_called_once_with(project_id, member)
+
+    def test_update(self):
+        project_id = 0
+        member = Member(id=0, user=0, role=0)
+        role = Role(id=2, name="annotator")
+        self.member_repository.find_by_id.return_value = member
+        self.role_repository.find_by_name.return_value = role
+        self.usecase.update(project_id, member.id, role_name=role.name)
+        member.role = role.id
+        self.member_repository.update.assert_called_once_with(project_id, member)
+
+    def test_delete(self):
+        self.usecase.delete(0, 1)
+        self.member_repository.delete.assert_called_once_with(0, 1)
diff --git a/tests/unit/usecase/test_project.py b/tests/unit/usecase/test_project.py
new file mode 100644
index 0000000..a4778cc
--- /dev/null
+++ b/tests/unit/usecase/test_project.py
@@ -0,0 +1,49 @@
+from unittest.mock import MagicMock
+
+import pytest
+
+from doccano_client.models.project import Project
+from doccano_client.usecase.project import ProjectUseCase
+
+
+@pytest.fixture
+def payload():
+    return {
+        "id": 1,
+        "name": "Test Project",
+        "description": "Test project",
+        "project_type": "SequenceLabeling",
+        "guideline": "",
+    }
+
+
+class TestProjectUseCase:
+    @classmethod
+    def setup_method(cls):
+        cls.project_repository = MagicMock()
+        cls.project_usecase = ProjectUseCase(cls.project_repository)
+
+    def test_find_by_id(self):
+        self.project_usecase.find_by_id(1)
+        self.project_repository.find_by_id.assert_called_once_with(1)
+
+    def test_list(self):
+        list(self.project_usecase.list())
+        self.project_repository.list.assert_called_once()
+
+    def test_create(self, payload):
+        payload.pop("id")
+        self.project_usecase.create(**payload)
+        self.project_repository.create.assert_called_once_with(Project.parse_obj(payload))
+
+    def test_update(self, payload):
+        project = Project.parse_obj(payload)
+        self.project_repository.find_by_id.return_value = project
+        self.project_usecase.update(project_id=1, name="New Name")
+        project.name = "New Name"
+        self.project_repository.find_by_id.assert_called_once_with(1)
+        self.project_repository.update.assert_called_once_with(project)
+
+    def test_delete(self):
+        self.project_usecase.delete(1)
+        self.project_repository.delete.assert_called_once_with(1)
diff --git a/tests/unit/usecase/test_role.py b/tests/unit/usecase/test_role.py
new file mode 100644
index 0000000..74af599
--- /dev/null
+++ b/tests/unit/usecase/test_role.py
@@ -0,0 +1,14 @@
+from unittest.mock import MagicMock
+
+from doccano_client.usecase.role import RoleUseCase
+
+
+class TestRoleUseCase:
+    @classmethod
+    def setup_class(cls):
+        cls.role_repository = MagicMock()
+        cls.role_usecase = RoleUseCase(cls.role_repository)
+
+    def test_list(self):
+        self.role_usecase.list()
+        self.role_repository.list.assert_called_once()