Repository: snuailab/waffle_utils (language: python)

Issue:
implement data_tools
### Search before asking

- [X] I have searched the [issues](https://github.com///issues) and found no similar feature requests.


### Description

- Implement DataTools (refer. README)

### Use case

_No response_

### Additional

_No response_


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 9869788..ea374e1 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -41,4 +41,4 @@ jobs:
           pip list
       - name: Pytest tests
         shell: bash  # for Windows compatibility
-        run: python -m pytest tests
+        run: python -m pytest tests -v
diff --git a/.gitignore b/.gitignore
index b6e4761..a397f85 100644
--- a/.gitignore
+++ b/.gitignore
@@ -127,3 +127,6 @@ dmypy.json
 
 # Pyre type checker
 .pyre/
+
+# custom
+tmp/
\ No newline at end of file
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index b9edef1..9cc75d1 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -22,6 +22,8 @@ repos:
       - id: check-yaml
       - id: check-toml
       - id: pretty-format-json
+        args:
+          - --autofix
       - id: check-docstring-first
 
   - repo: https://github.com/asottile/pyupgrade
@@ -36,6 +38,10 @@ repos:
     hooks:
       - id: isort
         name: Sort imports
+        args: [
+          --multi-line=3,
+          --split-on-trailing-comma
+          ]
 
   - repo: https://github.com/psf/black
     rev: 22.12.0
@@ -43,15 +49,9 @@ repos:
     - id: black
       args: [--line-length=79]
 
-  - repo: https://github.com/PyCQA/flake8
-    rev: 6.0.0
-    hooks:
-      - id: flake8
-        name: PEP8
-
-  - repo: https://github.com/codespell-project/codespell
-    rev: v2.2.2
-    hooks:
-      - id: codespell
-        args:
-          - --ignore-words-list=crate,nd
\ No newline at end of file
+  # - repo: https://github.com/PyCQA/flake8
+  #   rev: 6.0.0
+  #   hooks:
+  #     - id: flake8
+  #       name: PEP8
+  #       args: ["--ignore=E501,E741"]
\ No newline at end of file
diff --git a/README.md b/README.md
index ada8ce8..5c12e17 100644
--- a/README.md
+++ b/README.md
@@ -5,53 +5,38 @@
 - [Waffle Data Convention](https://snuailab.notion.site/Waffle-Data-Convention-7547fda8c1ca48798d00bd4658ea96bf)
 
 # Usage
-### Generate Dataset
+### Create Dataset from coco format
 ```python
-from waffle_utils.data import DataTools as DT
-from waffle_utils.data.formats import DetAnn
-
-dt = DT()
-
-img_id = dt.add_image(image_path="images/apple.png")
-cat_id = dt.add_category(supercategory=None, name="apple")
-ann_id = dt.add_annotation(
-    img_id,
-    DetAnn(
-        cat_id=cat_id,
-        xyxy=[100, 100, 200, 200]
-    )
+from waffle_utils.dataset import Dataset
+
+url = "https://github.com/snuailab/waffle_utils/raw/main/mnist.zip"
+
+dummy_tmp_dir = "tmp"
+dummy_zip_file = "tmp/mnist.zip"
+dummy_data_root_dir = "tmp/dataset"
+dummy_dataset_name = "mnist"
+
+dummy_extract_dir = "tmp/extract"
+dummy_coco_root_dir = "tmp/extract/raw"
+dummy_coco_file = "tmp/extract/exports/coco.json"
+
+network.get_file_from_url(url, dummy_zip_file, create_directory=True)
+io.unzip(dummy_zip_file, dummy_extract_dir, create_directory=True)
+
+ds = Dataset.from_coco(
+    dummy_dataset_name,
+    dummy_coco_file,
+    dummy_coco_root_dir,
+    root_dir=dummy_data_root_dir,
 )
 
-ann = dt.get_anns(img_id=img_id)[0]
-bbox = ann.bbox
-bbox.xyxy  # [100, 100, 200, 200]
-bbox.xywh  # [100, 100, 100, 100]
-bbox.cxcywh  # [150, 150, 100, 100]
-bbox.aspect_ratio  # 1
-bbox.area  # 10000
-
-dt.export("save_dir", DT.YOLO_DETECTION)
-"""
-save_dir/
-    images/
-        apple.png
-    annotations.json
-"""
-dt.export("save_dir", DT.COCO_DETECTION)
-"""
-save_dir/
-    images/
-        apple.png
-    labels/
-        apple.txt
-"""
+ds = Dataset.from_directory(dummy_dataset_name, dummy_data_root_dir)
+
+ds.split_train_val(train_split_ratio=0.8)
 ```
 ### Format Converting
 ```python
-from waffle_utils.data import DataTools as DT
-
-dt = DT.from_directory("save_dir", DT.YOLO_DETECTION)
-dt.export("save_dir", DT.COCO_DETECTION)
+todo
 ```
 
 ### CLI
diff --git a/mnist.zip b/mnist.zip
new file mode 100644
index 0000000..50db5e3
Binary files /dev/null and b/mnist.zip differ
diff --git a/requirements.txt b/requirements.txt
index fa9cf06..75c0b2f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,1 +1,3 @@
-tqdm
\ No newline at end of file
+tqdm
+wget==3.2
+PyYAML==6.0
\ No newline at end of file
diff --git a/waffle_utils/data/__init__.py b/waffle_utils/data/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/waffle_utils/data/data_tools.py b/waffle_utils/data/data_tools.py
deleted file mode 100644
index e69de29..0000000
diff --git a/waffle_utils/data/formats/__init__.py b/waffle_utils/data/formats/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/waffle_utils/dataset/__init__.py b/waffle_utils/dataset/__init__.py
new file mode 100644
index 0000000..9d5208e
--- /dev/null
+++ b/waffle_utils/dataset/__init__.py
@@ -0,0 +1,4 @@
+from .dataset import Dataset
+from .format import Format
+
+__all__ = ["Dataset", "Format"]
diff --git a/waffle_utils/dataset/dataset.py b/waffle_utils/dataset/dataset.py
new file mode 100644
index 0000000..eb39abd
--- /dev/null
+++ b/waffle_utils/dataset/dataset.py
@@ -0,0 +1,408 @@
+import os
+import random
+from functools import cached_property
+from pathlib import Path
+
+from waffle_utils.file import io
+from waffle_utils.utils import type_validator
+
+from .fields import Annotation, Category, Image
+from .format import Format
+
+
+class Dataset:
+    DEFAULT_DATASET_ROOT_DIR = Path("./datasets")
+    RAW_IMAGE_DIR = Path("raw")
+    IMAGE_DIR = Path("images")
+    ANNOTATION_DIR = Path("labels")
+    CATEGORY_DIR = Path("categories")
+    EXPORT_DIR = Path("exports")
+    SET_DIR = Path("sets")
+
+    TRAIN_SET_NAME = Path("train.json")
+    VAL_SET_NAME = Path("val.json")
+    UNLABELED_SET_NAME = Path("unlabeled.json")
+
+    def __init__(
+        self,
+        name: str,
+        root_dir: str = None,
+    ):
+        self.name = self._name = name
+        self.root_dir = self._root_dir = (
+            Path(root_dir) if root_dir else Dataset.DEFAULT_DATASET_ROOT_DIR
+        )
+
+    # properties
+    @property
+    def name(self):
+        return self._name
+
+    @name.setter
+    @type_validator(str)
+    def name(self, v):
+        self._name = v
+
+    @property
+    def root_dir(self):
+        return self._root_dir
+
+    @root_dir.setter
+    @type_validator(Path)
+    def root_dir(self, v):
+        self._root_dir = v
+
+    # cached properties
+    @cached_property
+    def dataset_dir(self) -> Path:
+        return self.root_dir / self.name
+
+    @cached_property
+    def raw_image_dir(self) -> Path:
+        return self.dataset_dir / Dataset.RAW_IMAGE_DIR
+
+    @cached_property
+    def image_dir(self) -> Path:
+        return self.dataset_dir / Dataset.IMAGE_DIR
+
+    @cached_property
+    def annotation_dir(self) -> Path:
+        return self.dataset_dir / Dataset.ANNOTATION_DIR
+
+    @cached_property
+    def category_dir(self) -> Path:
+        return self.dataset_dir / Dataset.CATEGORY_DIR
+
+    @cached_property
+    def export_dir(self) -> Path:
+        return self.dataset_dir / Dataset.EXPORT_DIR
+
+    @cached_property
+    def set_dir(self) -> Path:
+        return self.dataset_dir / Dataset.SET_DIR
+
+    # factories
+    @classmethod
+    def new(cls, name: str, root_dir: str = None) -> "Dataset":
+        """Create New Dataset
+
+        Args:
+            name (str): Dataset name
+            root_dir (str, optional): Dataset root directory. Defaults to None.
+
+        Raises:
+            FileExistsError: if dataset name already exists
+
+        Returns:
+            Dataset: Dataset Class
+        """
+        ds = cls(name, root_dir)
+        if ds.initialized():
+            raise FileExistsError(
+                f'{ds.dataset_dir} already exists. try another name or Dataset.from_directory("{name}")'
+            )
+        ds.initialize()
+        return ds
+
+    @classmethod
+    def clone(
+        cls,
+        src_name: str,
+        name: str,
+        src_root_dir: str = None,
+        root_dir: str = None,
+    ) -> "Dataset":
+        """Clone Existing Dataset
+
+        Args:
+            src_name (str):
+                Dataset name to clone.
+                It should be Waffle Created Dataset.
+            name (str): New Dataset name
+            src_root_dir (str, optional): Source Dataset root directory. Defaults to None.
+            root_dir (str, optional): New Dataset root directory. Defaults to None.
+
+        Raises:
+            FileNotFoundError: if source dataset does not exist.
+            FileExistsError: if new dataset name already exist.
+
+        Returns:
+            Dataset: Dataset Class
+        """
+        src_ds = cls(src_name, src_root_dir)
+        if not src_ds.initialized():
+            raise FileNotFoundError(
+                f"{src_ds.dataset_dir} has not been created by Waffle."
+            )
+
+        ds = cls(name, root_dir)
+        if ds.initialized():
+            raise FileExistsError(
+                f"{ds.dataset_dir} already exists. try another name."
+            )
+        ds.initialize()
+        io.copy_files_to_directory(
+            src_ds.dataset_dir, ds.dataset_dir, create_directory=True
+        )
+
+    @classmethod
+    def from_directory(cls, name: str, root_dir: str = None) -> "Dataset":
+        """Load Dataset from directory.
+
+        Args:
+            name (str): Dataset name that Waffle Created
+            root_dir (str, optional): Dataset root directory. Defaults to None.
+
+        Raises:
+            FileNotFoundError: if source dataset does not exist.
+
+        Returns:
+            Dataset: Dataset Class
+        """
+        ds = cls(name, root_dir)
+        if not ds.initialized():
+            raise FileNotFoundError(
+                f'{ds.dataset_dir} has not been created. Run Dataset.new("{name}") first.'
+            )
+        return ds
+
+    @classmethod
+    def from_coco(
+        cls,
+        name: str,
+        coco_file: str,
+        coco_root_dir: str,
+        root_dir: str = None,
+    ) -> "Dataset":
+        """Import Dataset from coco format.
+
+        Args:
+            name (str): Dataset name.
+            coco_file (str): Coco json file path.
+            coco_root_dir (str): Coco image root directory.
+            root_dir (str, optional): Dataset root directory. Defaults to None.
+
+        Raises:
+            FileExistsError: if new dataset name already exist.
+
+        Returns:
+            Dataset: Dataset Class
+        """
+        ds = cls(name, root_dir)
+        if ds.initialized():
+            raise FileExistsError(
+                f"{ds.dataset_dir} already exists. try another name."
+            )
+        ds.initialize()
+
+        # parse coco annotation file
+        coco = io.load_json(coco_file)
+        ds.add_imgs([Image.from_dict(image) for image in coco["images"]])
+        ds.add_anns(
+            [
+                Annotation.from_dict(annotation)
+                for annotation in coco["annotations"]
+            ]
+        )
+        ds.add_cats(
+            [Category.from_dict(category) for category in coco["categories"]]
+        )
+
+        # copy raw images
+        io.copy_files_to_directory(coco_root_dir, ds.raw_image_dir)
+
+        return ds
+
+    @classmethod
+    def from_nas(cls) -> "Dataset":
+        # download to root_dir from nas
+        # return cls(root_dir=root_dir)
+        raise NotImplementedError
+
+    @classmethod
+    def from_minio(cls) -> "Dataset":
+        # download to root_dir from minio
+        # return cls(root_dir=root_dir)
+        raise NotImplementedError
+
+    @classmethod
+    def from_ftp(cls) -> "Dataset":
+        # download to root_dir from ftp
+        # return cls(root_dir=root_dir)
+        raise NotImplementedError
+
+    def initialize(self):
+        """Initialize Dataset.
+        It creates necessary directories under {dataset_root_dir}/{dataset_name}.
+        """
+        io.make_directory(self.raw_image_dir)
+        io.make_directory(self.image_dir)
+        io.make_directory(self.annotation_dir)
+        io.make_directory(self.category_dir)
+
+    def initialized(self) -> bool:
+        """Check if Dataset has been initialized or not.
+
+        Returns:
+            bool:
+                initialized -> True
+                not initialized -> False
+        """
+        return (
+            self.raw_image_dir.exists()
+            and self.image_dir.exists()
+            and self.annotation_dir.exists()
+            and self.category_dir.exists()
+        )
+
+    # get
+    def get_imgs(self, img_ids: list[int] = None) -> list[Image]:
+        """Get "Image"s.
+
+        Args:
+            img_ids (list[int], optional): id list. None for all "Image"s. Defaults to None.
+
+        Returns:
+            list[Image]: "Image" list
+        """
+        return [
+            Image.from_json(f)
+            for f in (
+                [self.image_dir / f"{img_id}.json" for img_id in img_ids]
+                if img_ids
+                else self.image_dir.glob("*.json")
+            )
+        ]
+
+    def get_cats(self, cat_ids: list[int] = None) -> list[Category]:
+        """Get "Category"s.
+
+        Args:
+            cat_ids (list[int], optional): id list. None for all "Category"s. Defaults to None.
+
+        Returns:
+            list[Category]: "Category" list
+        """
+        return [
+            Category.from_json(f)
+            for f in (
+                [self.category_dir / f"{cat_id}.json" for cat_id in cat_ids]
+                if cat_ids
+                else self.category_dir.glob("*.json")
+            )
+        ]
+
+    def get_anns(self, img_id: int = None) -> list[Annotation]:
+        """Get "Annotation"s.
+
+        Args:
+            img_id (int, optional): image id. None for all "Annotation"s. Defaults to None.
+
+        Returns:
+            list[Annotation]: "Annotation" list
+        """
+        if img_id:
+            return [
+                Annotation.from_json(f)
+                for f in self.annotation_dir.glob(f"{img_id}/*.json")
+            ]
+        else:
+            return [
+                Annotation.from_json(f)
+                for f in self.annotation_dir.glob("*/*.json")
+            ]
+
+    def get_labeled_imgs(self) -> list[Image]:
+        """Get labeled "Image"s
+
+        Returns:
+            list[Image]: "Image" list
+        """
+        labeled_img_ids = map(
+            lambda x: x.name,
+            filter(
+                lambda x: os.path.isdir(x), Path(self.annotation_dir).glob("*")
+            ),
+        )
+        return [
+            Image.from_json(self.image_dir / f"{img_id}.json")
+            for img_id in labeled_img_ids
+        ]
+
+    # add
+    def add_imgs(self, images: list[Image]):
+        """Add "Image"s to dataset.
+
+        Args:
+            images (list[Image]): list of "Image"s
+        """
+        for item in images:
+            item_id = item.img_id
+            item_path = self.image_dir / f"{item_id}.json"
+            io.save_json(item.to_dict(), item_path)
+
+    def add_cats(self, categories: list[Category]):
+        """Add "Category"s to dataset.
+
+        Args:
+            categories (list[Category]): list of "Category"s
+        """
+        for item in categories:
+            item_id = item.cat_id
+            item_path = self.category_dir / f"{item_id}.json"
+            io.save_json(item.to_dict(), item_path)
+
+    def add_anns(self, annotations: list[Annotation]):
+        """Add "Annotation"s to dataset.
+
+        Args:
+            annotations (list[Annotation]): list of "Annotation"s
+        """
+        for item in annotations:
+            item_path = (
+                self.annotation_dir / f"{item.img_id}" / f"{item.ann_id}.json"
+            )
+            io.save_json(item.to_dict(), item_path, create_directory=True)
+
+    # functions
+    def split_train_val(self, train_split_ratio: float, seed: int = 0):
+        """Split Dataset to train and validation sets. (TODO: unlabeled set)
+
+        Args:
+            train_split_ratio (float): train num ratio
+            seed (int, optional): random seed. Defaults to 0.
+        """
+        imgs: list[Image] = self.get_labeled_imgs()
+
+        num_imgs = len(imgs)
+        idxs = list(range(num_imgs))
+
+        random.seed(seed)
+        random.shuffle(idxs)
+
+        train_num = round(num_imgs * train_split_ratio)
+
+        train_img_idxs = idxs[:train_num]
+        val_img_idxs = idxs[train_num:]
+
+        io.save_json(
+            train_img_idxs,
+            self.set_dir / self.TRAIN_SET_NAME,
+            create_directory=True,
+        )
+        io.save_json(
+            val_img_idxs,
+            self.set_dir / self.VAL_SET_NAME,
+            create_directory=True,
+        )
+
+    # export
+    def export(self, export_format: Format) -> str:
+        if export_format == Format.YOLO_DETECTION:
+            raise NotImplementedError
+        elif export_format == Format.YOLO_CLASSIFICATION:
+            raise NotImplementedError
+        elif export_format == Format.YOLO_SEGMENTATION:
+            raise NotImplementedError
+        elif export_format == Format.COCO_DETECTION:
+            raise NotImplementedError
diff --git a/waffle_utils/dataset/fields/__init__.py b/waffle_utils/dataset/fields/__init__.py
new file mode 100644
index 0000000..e0b38e7
--- /dev/null
+++ b/waffle_utils/dataset/fields/__init__.py
@@ -0,0 +1,6 @@
+from .annotation import Annotation
+from .base_field import BaseField
+from .category import Category
+from .image import Image
+
+__all__ = ["BaseField", "Annotation", "Category", "Image"]
diff --git a/waffle_utils/dataset/fields/annotation.py b/waffle_utils/dataset/fields/annotation.py
new file mode 100644
index 0000000..1f2b94f
--- /dev/null
+++ b/waffle_utils/dataset/fields/annotation.py
@@ -0,0 +1,445 @@
+from waffle_utils.file import io
+from waffle_utils.utils import type_validator
+
+from .base_field import BaseField
+
+
+class Annotation(BaseField):
+    def __init__(
+        self,
+        # required
+        ann_id: int,
+        img_id: int,
+        # optional
+        cat_id: int = None,
+        bbox: list[int] = None,
+        mask: list[int] = None,
+        area: int = None,
+        keypoints: list[int] = None,
+        num_keypoints: int = None,
+        caption: str = None,
+        value: float = None,
+        #
+        iscrowd: bool = None,
+    ):
+
+        self.ann_id = self._ann_id = ann_id
+        self.img_id = self._img_id = img_id
+        self.cat_id = self._cat_id = cat_id
+        self.bbox = self._bbox = bbox
+        self.mask = self._mask = mask
+        self.area = self._area = area
+        self.keypoints = self._keypoints = keypoints
+        self.num_keypoints = self._num_keypoints = num_keypoints
+        self.caption = self._caption = caption
+        self.value = self._value = value
+        self.iscrowd = self._iscrowd = iscrowd
+
+    # properties
+    @property
+    def ann_id(self):
+        return self._ann_id
+
+    @ann_id.setter
+    @type_validator(int)
+    def ann_id(self, v):
+        if v and v < 1:
+            raise ValueError("id should be greater than 0.")
+        self._ann_id = v
+
+    @property
+    def img_id(self):
+        return self._img_id
+
+    @img_id.setter
+    @type_validator(int)
+    def img_id(self, v):
+        if v and v < 1:
+            raise ValueError("id should be greater than 0.")
+        self._img_id = v
+
+    @property
+    def cat_id(self):
+        return self._cat_id
+
+    @cat_id.setter
+    @type_validator(int)
+    def cat_id(self, v):
+        if v and v < 1:
+            raise ValueError("id should be greater than 0.")
+        self._cat_id = v
+
+    @property
+    def bbox(self):
+        return self._bbox
+
+    @bbox.setter
+    @type_validator(list)
+    def bbox(self, v):
+        if v and len(v) != 4:
+            raise ValueError("the length of bbox should be 4.")
+        self._bbox = v
+
+    @property
+    def mask(self):
+        return self._mask
+
+    @mask.setter
+    @type_validator(list)
+    def mask(self, v):
+        if v and len(v) % 2 != 0 and len(v) < 6:
+            raise ValueError(
+                "the length of mask should be at least 6 and divisible by 2."
+            )
+        self._mask = v
+
+    @property
+    def area(self):
+        return self._area
+
+    @area.setter
+    @type_validator(int)
+    def area(self, v):
+        self._area = v
+
+    @property
+    def keypoints(self):
+        return self._keypoints
+
+    @keypoints.setter
+    @type_validator(list)
+    def keypoints(self, v):
+        if v and len(v) % 3 != 0 and len(v) < 2:
+            raise ValueError(
+                "the length of keypoints should be at least 2 and divisible by 3."
+            )
+        self._keypoints = v
+
+    @property
+    def num_keypoints(self):
+        return self._num_keypoints
+
+    @num_keypoints.setter
+    @type_validator(int)
+    def num_keypoints(self, v):
+        self._num_keypoints = v
+
+    @property
+    def caption(self):
+        return self._caption
+
+    @caption.setter
+    @type_validator(str)
+    def caption(self, v):
+        self._caption = v
+
+    @property
+    def value(self):
+        return self._value
+
+    @value.setter
+    @type_validator(float)
+    def value(self, v):
+        self._value = v
+
+    @property
+    def iscrowd(self):
+        return self._iscrowd
+
+    @iscrowd.setter
+    @type_validator(bool)
+    def iscrowd(self, v):
+        self._iscrowd = v
+
+    # factories
+    @classmethod
+    def new(
+        cls,
+        ann_id: int,
+        img_id: int,
+        cat_id: int = None,
+        bbox: list[int] = None,
+        mask: list[int] = None,
+        area: int = None,
+        keypoints: list[int] = None,
+        num_keypoints: int = None,
+        caption: str = None,
+        value: float = None,
+        iscrowd: bool = None,
+    ) -> "Annotation":
+        """Annotation Format
+
+        Args:
+            ann_id (int): annotaion id. natural number.
+            img_id (int): image id. natural number.
+            cat_id (int): category id. natural number.
+            bbox (list[int]): [x1, y1, w, h].
+            mask (list[int]): [x1, y1, x2, y2, x3, y3, ...].
+            area (int): bbox area.
+            keypoints (list[int]):
+                [x1, y1, v1(visible flag), x2, y2, v2(visible flag), ...].
+                visible flag is one of [0(Not labeled), 1(Labeled but not visible), 2(labeled and visible)]
+            num_keypoints: number of labeled keypoints
+            caption (str): string.
+            value (float): regression value.
+            iscrowd (bool, optional): is crowd or not. Default to False.
+
+        Returns:
+            Annotation: annotation class
+        """
+        return cls(
+            ann_id,
+            img_id,
+            cat_id,
+            bbox,
+            mask,
+            area,
+            keypoints,
+            num_keypoints,
+            caption,
+            value,
+            iscrowd,
+        )
+
+    @classmethod
+    def from_dict(cls, d: dict) -> "Annotation":
+        """Annotation Format from dictionary
+
+        Args:
+            d (dict): Annotation dictionary
+
+        Returns:
+            Annotation: Annotation class
+        """
+
+        ann_id = d.get("id", None)
+        img_id = d.get("image_id", None)
+        cat_id = d.get("category_id", None)
+        bbox = d.get("bbox", None)
+        mask = d.get("mask", None)
+        area = d.get("area", None)
+        keypoints = d.get("keypoints", None)
+        num_keypoints = d.get("num_keypoints", None)
+        caption = d.get("caption", None)
+        value = d.get("value", None)
+        iscrowd = bool(d.get("iscrowd", None))
+
+        if ann_id is None:
+            raise ValueError("id field missing")
+        if img_id is None:
+            raise ValueError("image_id field missing")
+
+        return cls(
+            ann_id,
+            img_id,
+            cat_id,
+            bbox,
+            mask,
+            area,
+            keypoints,
+            num_keypoints,
+            caption,
+            value,
+            iscrowd,
+        )
+
+    @classmethod
+    def from_json(cls, f: str) -> "Annotation":
+        """Annotation Format from json file
+
+        Args:
+            d (dict): Annotation json file
+
+        Returns:
+            Annotation: Annotation class
+        """
+        d: dict = io.load_json(f)
+        return cls.from_dict(d)
+
+    @classmethod
+    def classification(
+        cls, ann_id: int, img_id: int, cat_id: int
+    ) -> "Annotation":
+        """Classification Annotation Format
+
+        Args:
+            ann_id (int): annotaion id. natural number.
+            img_id (int): image id. natural number.
+            cat_id (int): category id. natural number.
+
+        Returns:
+            Annotation: annotation class
+        """
+        return cls(ann_id, img_id, cat_id=cat_id)
+
+    @classmethod
+    def object_detection(
+        cls,
+        ann_id: int,
+        img_id: int,
+        cat_id: int,
+        bbox: list[int],
+        area: int,
+        iscrowd: bool = False,
+    ) -> "Annotation":
+        """Object Detection Annotation Format
+
+        Args:
+            ann_id (int): annotaion id. natural number.
+            img_id (int): image id. natural number.
+            cat_id (int): category id. natural number.
+            bbox (list[int]): [x1, y1, w, h].
+            area (int): bbox area.
+            iscrowd (bool, optional): is crowd or not. Default to False.
+
+        Returns:
+            Annotation: annotation class
+        """
+        return cls(
+            ann_id,
+            img_id,
+            cat_id=cat_id,
+            bbox=bbox,
+            area=area,
+            iscrowd=iscrowd,
+        )
+
+    @classmethod
+    def segmentation(
+        cls,
+        ann_id: int,
+        img_id: int,
+        cat_id: int,
+        bbox: list[int],
+        mask: list[int],
+        area: int,
+        iscrowd: bool = False,
+    ) -> "Annotation":
+        """Segmentation Annotation Format
+
+        Args:
+            ann_id (int): annotaion id. natural number.
+            img_id (int): image id. natural number.
+            cat_id (int): category id. natural number.
+            bbox (list[int]): [x1, y1, w, h].
+            mask (list[int]): [x1, y1, x2, y2, x3, y3, ...].
+            area (int): segmentation mask area.
+            iscrowd (bool, optional): is crowd or not. Default to False.
+
+        Returns:
+            Annotation: annotation class
+        """
+        return cls(
+            ann_id,
+            img_id,
+            cat_id=cat_id,
+            bbox=bbox,
+            mask=mask,
+            area=area,
+            iscrowd=iscrowd,
+        )
+
+    @classmethod
+    def keypoint_detection(
+        cls,
+        ann_id: int,
+        img_id: int,
+        cat_id: int,
+        bbox: list[int],
+        keypoints: list[int],
+        num_keypoints: int,
+        area: int,
+        mask: list[int] = None,
+        iscrowd: bool = False,
+    ) -> "Annotation":
+        """Keypoint Detection Annotation Format
+
+        Args:
+            ann_id (int): annotaion id. natural number.
+            img_id (int): image id. natural number.
+            cat_id (int): category id. natural number.
+            bbox (list[int]): [x1, y1, w, h].
+            keypoints (list[int]):
+                [x1, y1, v1(visible flag), x2, y2, v2(visible flag), ...].
+                visible flag is one of [0(Not labeled), 1(Labeled but not visible), 2(labeled and visible)]
+            num_keypoints: number of labeled keypoints
+            area (int): segmentation mask or bbox area.
+            mask (list[int], optional): [x1, y1, x2, y2, x3, y3, ...].
+            iscrowd (bool, optional): is crowd or not. Default to False.
+
+        Returns:
+            Annotation: annotation class
+        """
+        return cls(
+            ann_id,
+            img_id,
+            cat_id=cat_id,
+            bbox=bbox,
+            keypoints=keypoints,
+            num_keypoints=num_keypoints,
+            mask=mask,
+            area=area,
+            iscrowd=iscrowd,
+        )
+
+    @classmethod
+    def regression(
+        cls, ann_id: int, img_id: int, value: float
+    ) -> "Annotation":
+        """Regression Annotation Format
+
+        Args:
+            ann_id (int): annotaion id. natural number.
+            img_id (int): image id. natural number.
+            cat_id (int): category id. natural number.
+            value (float): regression value.
+
+        Returns:
+            Annotation: annotation class
+        """
+        return cls(ann_id, img_id, value=value)
+
+    @classmethod
+    def text_recognition(
+        cls, ann_id: int, img_id: int, caption: str
+    ) -> "Annotation":
+        """Text Recognition Annotation Format
+
+        Args:
+            ann_id (int): annotaion id. natural number.
+            img_id (int): image id. natural number.
+            cat_id (int): category id. natural number.
+            caption (str): string.
+
+        Returns:
+            Annotation: annotation class
+        """
+        return cls(ann_id, img_id, caption=caption)
+
+    def to_dict(self) -> dict:
+        """Get Dictionary of Annotation Data
+
+        Returns:
+            dict: annotation dictionary.
+        """
+
+        ann = {"id": self.ann_id, "image_id": self.img_id}
+
+        if self.cat_id is not None:
+            ann["category_id"] = self.cat_id
+        if self.bbox is not None:
+            ann["bbox"] = self.bbox
+        if self.mask is not None:
+            ann["segmentations"] = self.mask
+        if self.area is not None:
+            ann["area"] = self.area
+        if self.keypoints is not None:
+            ann["keypoints"] = self.keypoints
+        if self.caption is not None:
+            ann["caption"] = self.caption
+        if self.value is not None:
+            ann["value"] = self.value
+        if self.iscrowd is not None:
+            ann["iscrowd"] = self.iscrowd * 1
+
+        return ann
diff --git a/waffle_utils/dataset/fields/base_field.py b/waffle_utils/dataset/fields/base_field.py
new file mode 100644
index 0000000..d725ea9
--- /dev/null
+++ b/waffle_utils/dataset/fields/base_field.py
@@ -0,0 +1,25 @@
+from abc import ABC, abstractmethod
+
+
+class BaseField(ABC):
+    def __init__(self):
+        pass
+
+    @abstractmethod
+    def to_dict(self):
+        pass
+
+    @classmethod
+    @abstractmethod
+    def new(cls):
+        pass
+
+    @classmethod
+    @abstractmethod
+    def from_dict(cls):
+        pass
+
+    @classmethod
+    @abstractmethod
+    def from_json(cls):
+        pass
diff --git a/waffle_utils/dataset/fields/category.py b/waffle_utils/dataset/fields/category.py
new file mode 100644
index 0000000..10b9567
--- /dev/null
+++ b/waffle_utils/dataset/fields/category.py
@@ -0,0 +1,242 @@
+from waffle_utils.file import io
+from waffle_utils.utils import type_validator
+
+from . import BaseField
+
+
+class Category(BaseField):
+    def __init__(
+        self,
+        # required
+        cat_id: int,
+        supercategory: str,
+        name: str,
+        # for keypoint detection
+        keypoints: list[str] = None,
+        skeleton: list[list[int]] = None,
+    ):
+
+        self.cat_id = self._cat_id = cat_id
+        self.supercategory = self._supercategory = supercategory
+        self.name = self._name = name
+        self.keypoints = self._keypoints = keypoints
+        self.skeleton = self._skeleton = skeleton
+
+    # properties
+    @property
+    def cat_id(self):
+        return self._cat_id
+
+    @cat_id.setter
+    @type_validator(int)
+    def cat_id(self, v):
+        if v and v < 1:
+            raise ValueError("id should be greater than 0.")
+        self._cat_id = v
+
+    @property
+    def supercategory(self):
+        return self._supercategory
+
+    @supercategory.setter
+    @type_validator(str)
+    def supercategory(self, v):
+        self._supercategory = v
+
+    @property
+    def name(self):
+        return self._name
+
+    @name.setter
+    @type_validator(str)
+    def name(self, v):
+        self._name = v
+
+    @property
+    def keypoints(self):
+        return self._keypoints
+
+    @keypoints.setter
+    @type_validator(list)
+    def keypoints(self, v):
+        self._keypoints = v
+
+    @property
+    def skeleton(self):
+        return self._skeleton
+
+    @skeleton.setter
+    @type_validator(list)
+    def skeleton(self, v):
+        self._skeleton = v
+
+    # factories
+    @classmethod
+    def new(
+        cls,
+        cat_id: int,
+        supercategory: str,
+        name: str,
+        keypoints: list[str] = None,
+        skeleton: list[list[int]] = None,
+    ) -> "Category":
+        """Category Format
+
+        Args:
+            cat_id (int): category id. natural number.
+            supercategory (str): supercategory name.
+            name (str): category name.
+            keypoints (list[str]): category name.
+            skeleton (list[list[int]]): skeleton edges.
+
+        Returns:
+            Category: category class
+        """
+        return cls(cat_id, supercategory, name, keypoints, skeleton)
+
+    @classmethod
+    def from_dict(cls, d: dict) -> "Category":
+        """Category Format from dictionary
+
+        Args:
+            d (dict): Category dictionary
+
+        Returns:
+            Category: category class
+        """
+        cat_id = d.get("id", None)
+        supercategory = d.get("supercategory", None)
+        name = d.get("name", None)
+        keypoints = d.get("keypoints", None)
+        skeleton = d.get("skeleton", None)
+
+        if cat_id is None:
+            raise ValueError("id field missing")
+        if supercategory is None:
+            raise ValueError("file_name field missing")
+        if name is None:
+            raise ValueError("width field missing")
+
+        return cls(cat_id, supercategory, name, keypoints, skeleton)
+
+    @classmethod
+    def from_json(cls, f: str) -> "Category":
+        """Category Format from json file
+
+        Args:
+            d (dict): Category json file
+
+        Returns:
+            Category: Category class
+        """
+        d: dict = io.load_json(f)
+        return cls.from_dict(d)
+
+    @classmethod
+    def classification(
+        cls, cat_id: int, supercategory: str, name: str
+    ) -> "Category":
+        """Classification Category Format
+
+        Args:
+            cat_id (int): category id. natural number.
+            supercategory (str): supercategory name.
+            name (str): category name.
+
+        Returns:
+            Category: category class
+        """
+        return cls(cat_id, supercategory, name)
+
+    @classmethod
+    def object_detection(
+        cls, cat_id: int, supercategory: str, name: str
+    ) -> "Category":
+        """Object Detection Category Format
+
+        Args:
+            cat_id (int): category id. natural number.
+            supercategory (str): supercategory name.
+            name (str): category name.
+
+        Returns:
+            Category: category class
+        """
+        return cls(cat_id, supercategory, name)
+
+    @classmethod
+    def segmentation(
+        cls, cat_id: int, supercategory: str, name: str
+    ) -> "Category":
+        """Segmentation Category Format
+
+        Args:
+            cat_id (int): category id. natural number.
+            supercategory (str): supercategory name.
+            name (str): category name.
+
+        Returns:
+            Category: category class
+        """
+        return cls(cat_id, supercategory, name)
+
+    @classmethod
+    def keypoint_detection(
+        cls,
+        cat_id: int,
+        supercategory: str,
+        name: str,
+        keypoints: list[str],
+        skeleton: list[list[int]],
+    ) -> "Category":
+        """Keypoint Detection Category Format
+
+        Args:
+            cat_id (int): category id. natural number.
+            supercategory (str): supercategory name.
+            name (str): category name.
+            keypoints (list[str]): category name.
+            skeleton (list[list[int]]): skeleton edges.
+
+        Returns:
+            Category: category class
+        """
+        return cls(
+            cat_id, supercategory, name, keypoints=keypoints, skeleton=skeleton
+        )
+
+    @classmethod
+    def text_recognition(
+        cls, cat_id: int, supercategory: str, name: str
+    ) -> "Category":
+        """Text Recognition Category Format
+
+        Args:
+            cat_id (int): category id. natural number.
+            supercategory (str): supercategory name.
+            name (str): category name.
+
+        Returns:
+            Category: category class
+        """
+        return cls(cat_id, supercategory, name)
+
+    def to_dict(self) -> dict:
+        """Get Dictionary of Category
+
+        Returns:
+            dict: annotation dictionary.
+        """
+
+        cat = {
+            "id": self.cat_id,
+            "supercategory": self.supercategory,
+            "name": self.name,
+        }
+
+        if self.keypoints is not None:
+            cat["keypoints"] = self.keypoints
+        if self.skeleton is not None:
+            cat["skeleton"] = self.skeleton
+
+        return cat
diff --git a/waffle_utils/dataset/fields/image.py b/waffle_utils/dataset/fields/image.py
new file mode 100644
index 0000000..f374ef2
--- /dev/null
+++ b/waffle_utils/dataset/fields/image.py
@@ -0,0 +1,155 @@
+from waffle_utils.file import io
+from waffle_utils.log import datetime_now
+from waffle_utils.utils import type_validator
+
+from . import BaseField
+
+
+class Image(BaseField):
+    def __init__(
+        self,
+        # required
+        img_id: int,
+        file_name: str,
+        width: int,
+        height: int,
+        # optional
+        date_captured: str = None,
+    ):
+
+        self.img_id = self._img_id = img_id
+        self.file_name = self._file_name = file_name
+        self.width = self._width = width
+        self.height = self._height = height
+        self.date_captured = self._date_captured = date_captured
+
+    # properties
+    @property
+    def img_id(self):
+        return self._img_id
+
+    @img_id.setter
+    @type_validator(int)
+    def img_id(self, v):
+        if v and v < 1:
+            raise ValueError("id should be greater than 0.")
+        self._img_id = v
+
+    @property
+    def file_name(self):
+        return self._file_name
+
+    @file_name.setter
+    @type_validator(str)
+    def file_name(self, v):
+        self._file_name = v
+
+    @property
+    def width(self):
+        return self._width
+
+    @width.setter
+    @type_validator(int)
+    def width(self, v):
+        self._width = v
+
+    @property
+    def height(self):
+        return self._height
+
+    @height.setter
+    @type_validator(int)
+    def height(self, v):
+        self._height = v
+
+    @property
+    def date_captured(self):
+        return self._date_captured
+
+    @date_captured.setter
+    @type_validator(str)
+    def date_captured(self, v):
+        if v is None:
+            self._date_captured = datetime_now()
+        else:
+            self._date_captured = v
+
+    @classmethod
+    def new(
+        cls,
+        img_id: int,
+        file_name: str,
+        width: int,
+        height: int,
+        date_captured: str = None,
+    ) -> "Image":
+        """Image Format
+
+        Args:
+            img_id (int): image id. natural number.
+            file_name (str): file name. relative file path.
+            width (int): image width.
+            height (int): image height.
+            date_captured (str): date_captured string. "%Y-%m-%d %H:%M:%S"
+
+        Returns:
+            Image: image class
+        """
+        return cls(img_id, file_name, width, height, date_captured)
+
+    @classmethod
+    def from_dict(cls, d: dict) -> "Image":
+        """Image Format from dictionary
+
+        Args:
+            d (dict): Image dictionary
+
+        Returns:
+            Image: image class
+        """
+        img_id = d.get("id", None)
+        file_name = d.get("file_name", None)
+        width = d.get("width", None)
+        height = d.get("height", None)
+        date_captured = d.get("date_captured", datetime_now())
+
+        if img_id is None:
+            raise ValueError("id field missing")
+        if file_name is None:
+            raise ValueError("file_name field missing")
+        if width is None:
+            raise ValueError("width field missing")
+        if height is None:
+            raise ValueError("height field missing")
+
+        return cls(img_id, file_name, width, height, date_captured)
+
+    @classmethod
+    def from_json(cls, f: str) -> "Image":
+        """Image Format from json file
+
+        Args:
+            d (dict): Image json file
+
+        Returns:
+            Image: Image class
+        """
+        d: dict = io.load_json(f)
+        return cls.from_dict(d)
+
+    def to_dict(self) -> dict:
+        """Get Dictionary of Category
+
+        Returns:
+            dict: annotation dictionary.
+        """
+
+        cat = {
+            "id": self.img_id,
+            "file_name": self.file_name,
+            "width": self.width,
+            "height": self.height,
+            "date_captured": self.date_captured,
+        }
+
+        return cat
diff --git a/waffle_utils/dataset/format.py b/waffle_utils/dataset/format.py
new file mode 100644
index 0000000..cf49641
--- /dev/null
+++ b/waffle_utils/dataset/format.py
@@ -0,0 +1,10 @@
+import enum
+
+
+# enums
+class Format(enum.Flag):
+    YOLO_CLASSIFICATION = enum.auto()
+    YOLO_DETECTION = enum.auto()
+    YOLO_SEGMENTATION = enum.auto()
+
+    COCO_DETECTION = enum.auto()
diff --git a/waffle_utils/file/io.py b/waffle_utils/file/io.py
new file mode 100644
index 0000000..9caf248
--- /dev/null
+++ b/waffle_utils/file/io.py
@@ -0,0 +1,198 @@
+import json
+import os
+import shutil
+import zipfile
+from pathlib import Path
+from typing import Any, Union
+
+import yaml
+
+
+def save_json(obj: Any, fp: Union[str, Path], create_directory: bool = False):
+    """save json file
+
+    Args:
+        obj (Any): Any object that can be converted to json format.
+        fp (Union[str, Path]): file path.
+        create_directory (bool, optional): this determines whether create parent directory or not. Default to False.
+    """
+
+    fp = Path(fp)
+    if create_directory:
+        make_directory(fp.parent)
+
+    with open(fp, "w") as f:
+        json.dump(obj, f, ensure_ascii=False, indent=4)
+
+
+def load_json(fp: Union[str, Path]) -> dict:
+    """load json file
+
+    Args:
+        fp (Union[str, Path]): file path.
+
+    Returns:
+        dict: dictionary
+    """
+
+    fp = Path(fp)
+
+    if not fp.exists():
+        raise FileNotFoundError(f"{fp} does not exists")
+
+    with open(fp) as f:
+        d = json.load(f)
+
+    return d
+
+
+def save_yaml(obj: Any, fp: Union[str, Path], create_directory: bool = False):
+    """save yaml file
+
+    Args:
+        obj (Any): Any object that can be converted to yaml format.
+        fp (Union[str, Path]): file path.
+        create_directory (bool, optional): this determines whether create parent directory or not. Default to False.
+    """
+
+    fp = Path(fp)
+    if create_directory:
+        make_directory(fp.parent)
+
+    with open(fp, "w") as f:
+        yaml.safe_dump(obj, f, indent=4, sort_keys=False)
+
+
+def load_yaml(fp: Union[str, Path]) -> dict:
+    """load yaml file
+
+    Args:
+        fp (Union[str, Path]): file path.
+
+    Returns:
+        dict: dictionary
+    """
+
+    fp = Path(fp)
+
+    if not fp.exists():
+        raise FileNotFoundError(f"{fp} does not exists")
+
+    with open(fp) as f:
+        d = yaml.safe_load(f)
+
+    return d
+
+
+def copy_files_to_directory(
+    src: Union[list, str, Path],
+    dst: Union[str, Path],
+    create_directory: bool = False,
+):
+    """Copy files to directory
+
+    Args:
+        src (Union[list, str, Path]): 'file list' or 'file' or 'directory'.
+        dst (Union[str, Path]): destination directory.
+        create_directory (bool, optional): create destination directory or not. Defaults to False.
+
+    Raises:
+        FileNotFoundError: if src is unknown
+        ValueError: if dst is not directory format
+        FileNotFoundError: if dst is not exists. you can bypass this error with create_directory argument.
+    """
+
+    src_list = None
+    if isinstance(src, list):
+        src_prefix = os.path.commonpath(src)
+        src_list = list(map(Path, src))
+    elif isinstance(src, str) or isinstance(src, Path):
+        src = Path(src)
+        if src.is_file():
+            src = Path(src)
+            src_list = [src]
+            src_prefix = src.parent
+        elif src.is_dir():
+            src = Path(src)
+            src_list = src.glob("**/*")
+            src_prefix = src
+
+    if src_list is None:
+        raise FileNotFoundError(f"unknown source {src}")
+
+    dst = Path(dst)
+    if dst.suffix != "":
+        raise ValueError(
+            f"dst should be directory format. but got {dst.suffix}"
+        )
+    elif create_directory:
+        make_directory(dst)
+
+    if not dst.exists():
+        raise FileNotFoundError(
+            f"{dst} directory does not exist. please set 'create_directory' argument to be True to make directory."
+        )
+
+    for src_file in src_list:
+        dst_file = Path(str(src_file).replace(str(src_prefix), str(dst)))
+        make_directory(dst_file.parent)
+        if src_file.is_file():
+            shutil.copy(src_file, dst_file)
+
+
+def copy_file(
+    src: Union[str, Path],
+    dst: Union[str, Path],
+    create_directory: bool = False,
+):
+    """Copy file
+
+    Args:
+        src (Union[str, Path]): source file path.
+        dst (Union[str, Path]): destination file path.
+        create_directory (bool, optional): create destination directory or not. Defaults to False.
+
+    Raises:
+    """
+    dst = Path(dst)
+
+    if create_directory:
+        make_directory(dst.parent)
+
+    shutil.copy(src, dst)
+
+
+def make_directory(src: str):
+    """Create Directory
+
+    Args:
+        src (str): directory
+    """
+    Path(src).mkdir(mode=0o766, parents=True, exist_ok=True)
+
+
+def remove_file(src: str):
+    """Remove File
+
+    Args:
+        src (str): file to remove
+    """
+    os.remove(src)
+
+
+def remove_directory(src: str):
+    """Remove Directory
+
+    Args:
+        src (str): file to remove
+    """
+    shutil.rmtree(str(src))
+
+
+def unzip(src: str, dst: str, create_directory: bool = False):
+
+    if create_directory:
+        make_directory(dst)
+
+    with zipfile.ZipFile(src, "r") as f:
+        f.extractall(dst)
diff --git a/waffle_utils/file/network.py b/waffle_utils/file/network.py
new file mode 100644
index 0000000..0c8cb4e
--- /dev/null
+++ b/waffle_utils/file/network.py
@@ -0,0 +1,22 @@
+from pathlib import Path
+
+import wget
+
+from . import io
+
+
+def get_file_from_url(url: str, out: str, create_directory: bool = False):
+    """Download file from url using wget
+
+    Args:
+        url (str): file url
+        out (str): output file path
+        create_directory (bool, optional): create destination directory or not. Defaults to False.
+    """
+
+    out = Path(out)
+
+    if create_directory:
+        io.make_directory(out.parent)
+
+    wget.download(url, str(out))
diff --git a/waffle_utils/log/__init__.py b/waffle_utils/log/__init__.py
index e69de29..56e130b 100644
--- a/waffle_utils/log/__init__.py
+++ b/waffle_utils/log/__init__.py
@@ -0,0 +1,5 @@
+from .time import datetime_now
+
+__all__ = [
+    "datetime_now",
+]
diff --git a/waffle_utils/log/time.py b/waffle_utils/log/time.py
new file mode 100644
index 0000000..f2127fb
--- /dev/null
+++ b/waffle_utils/log/time.py
@@ -0,0 +1,12 @@
+import datetime
+
+DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
+
+
+def datetime_now() -> str:
+    """Return string of datetime now
+
+    Returns:
+        str: datetime (%Y-%m-%d %H:%M:%S)
+    """
+    return datetime.datetime.now().strftime(DATE_FORMAT)
diff --git a/waffle_utils/utils/__init__.py b/waffle_utils/utils/__init__.py
new file mode 100644
index 0000000..7681d6f
--- /dev/null
+++ b/waffle_utils/utils/__init__.py
@@ -0,0 +1,3 @@
+from .validators import type_validator
+
+__all__ = ["type_validator"]
diff --git a/waffle_utils/utils/validators.py b/waffle_utils/utils/validators.py
new file mode 100644
index 0000000..10a9971
--- /dev/null
+++ b/waffle_utils/utils/validators.py
@@ -0,0 +1,11 @@
+def type_validator(_type: type):
+    def type_check(f):
+        def input_check(self, v, *args, **kwargs):
+            if v is not None:
+                if not isinstance(v, _type):
+                    raise TypeError(f"value should be {_type}")
+            return f(self, v, *args, **kwargs)
+
+        return input_check
+
+    return type_check


Test patch:
diff --git a/tests/dataset/test_dataset.py b/tests/dataset/test_dataset.py
new file mode 100644
index 0000000..5bdc2c4
--- /dev/null
+++ b/tests/dataset/test_dataset.py
@@ -0,0 +1,112 @@
+from waffle_utils.dataset import Dataset
+from waffle_utils.dataset.fields import Annotation as A
+from waffle_utils.dataset.fields import Category as C
+from waffle_utils.dataset.fields import Image as I
+from waffle_utils.file import io, network
+
+
+def test_annotations():
+
+    bbox = [100, 100, 100, 100]
+    mask = [110, 110, 130, 130, 110, 130]
+    keypoints = [0, 0, 0, 130, 130, 1, 110, 130, 2]
+
+    ann = A.classification(ann_id=1, img_id=1, cat_id=1)
+    A.from_dict(ann.to_dict())
+
+    ann = A.object_detection(
+        ann_id=1, img_id=1, cat_id=1, bbox=bbox, area=10000
+    )
+    A.from_dict(ann.to_dict())
+
+    ann = A.segmentation(
+        ann_id=1, img_id=1, cat_id=1, bbox=bbox, mask=mask, area=200
+    )
+    A.from_dict(ann.to_dict())
+
+    ann = A.keypoint_detection(
+        ann_id=1,
+        img_id=1,
+        cat_id=1,
+        bbox=bbox,
+        keypoints=keypoints,
+        num_keypoints=2,
+        area=200,
+    )
+    A.from_dict(ann.to_dict())
+
+    ann = A.regression(ann_id=1, img_id=1, value=3.0)
+    A.from_dict(ann.to_dict())
+
+    ann = A.text_recognition(ann_id=1, img_id=1, caption="hello world")
+    A.from_dict(ann.to_dict())
+
+
+def test_images():
+
+    img = I.new(img_id=1, file_name="a.png", width=100, height=100)
+    I.from_dict(img.to_dict())
+
+    img = I.new(
+        img_id=1,
+        file_name="a.png",
+        width=100,
+        height=100,
+        date_captured="2020-09-26 18:00:00",
+    )
+    I.from_dict(img.to_dict())
+
+
+def test_categories():
+    cat = C.classification(cat_id=1, supercategory="animal", name="cat")
+    C.from_dict(cat.to_dict())
+
+    cat = C.object_detection(cat_id=1, supercategory="animal", name="cat")
+    C.from_dict(cat.to_dict())
+
+    cat = C.segmentation(cat_id=1, supercategory="animal", name="cat")
+    C.from_dict(cat.to_dict())
+
+    cat = C.keypoint_detection(
+        cat_id=1,
+        supercategory="animal",
+        name="cat",
+        keypoints=["eye", "nose", "mouse"],
+        skeleton=[[1, 2], [2, 3]],
+    )
+    C.from_dict(cat.to_dict())
+
+    cat = C.text_recognition(cat_id=1, supercategory="animal", name="cat")
+    C.from_dict(cat.to_dict())
+
+
+def test_import_coco():
+    url = "https://github.com/snuailab/waffle_utils/raw/main/mnist.zip"
+
+    dummy_tmp_dir = "tmp"
+    dummy_zip_file = "tmp/mnist.zip"
+    dummy_data_root_dir = "tmp/dataset"
+    dummy_dataset_name = "mnist"
+
+    dummy_extract_dir = "tmp/extract"
+    dummy_coco_root_dir = "tmp/extract/raw"
+    dummy_coco_file = "tmp/extract/exports/coco.json"
+
+    network.get_file_from_url(url, dummy_zip_file, create_directory=True)
+    io.unzip(dummy_zip_file, dummy_extract_dir, create_directory=True)
+
+    ds = Dataset.from_coco(
+        dummy_dataset_name,
+        dummy_coco_file,
+        dummy_coco_root_dir,
+        root_dir=dummy_data_root_dir,
+    )
+
+    ds = Dataset.from_directory(dummy_dataset_name, dummy_data_root_dir)
+
+    ds.split_train_val(train_split_ratio=0.8)
+
+    # todo export
+    # ds.export
+
+    io.remove_directory(dummy_tmp_dir)
diff --git a/tests/file/test_io.py b/tests/file/test_io.py
new file mode 100644
index 0000000..def8c3c
--- /dev/null
+++ b/tests/file/test_io.py
@@ -0,0 +1,21 @@
+from waffle_utils.file import io, network
+
+
+def test_io():
+    dummy_data = {"dummy": "hi"}
+    dummy_json = "tmp/a.json"
+    dummy_yaml = "tmp/a.yaml"
+
+    dummy_src = "tmp"
+    dummy_dst = "tmp_copy"
+
+    io.save_json(dummy_data, dummy_json, create_directory=True)
+    io.save_yaml(dummy_data, dummy_yaml, create_directory=True)
+    assert io.load_json(dummy_json) == io.load_yaml(dummy_yaml)
+
+    io.copy_files_to_directory(dummy_src, dummy_dst, create_directory=True)
+
+    io.remove_file(dummy_json)
+
+    io.remove_directory(dummy_src)
+    io.remove_directory(dummy_dst)