Repository: ogawa-ros/necstdb

Issue:
Add file copy function
When we want to save config files, we couldn't use this database, because of a difficulty handling string data.
But the functionality is definitely needed, since there are many those files which need to be saved as a part of observation log.
So we'll add a file copying function to `necstdb` class to enable the manipulation.

Discussion:

Patch:
diff --git a/README.md b/README.md
index 0e913a7..a1bf9cb 100644
--- a/README.md
+++ b/README.md
@@ -104,6 +104,15 @@ pip install necstdb
     ***NOTE:***
     Multi-dimensional data are not supported.
 
+    ***NOTE:***
+    Shape of array or length of string in every data must be the same as the one in data information. (Mismatch of string length won't raise an error, but stored data can be broken or incomplete.)
+
+4. Save files into the database
+
+    ```python
+    >>> db.save_file("example.txt", "Content of the file.")
+    ```
+
 ### Database Reader
 
 1. Open the database instance
@@ -133,6 +142,22 @@ pip install necstdb
     ***\*1***: Array data are not supported, but will be flattened.  
     ***\*2***: *Changed in v0.2.5: Keyword ``df`` is now supported.*
 
+3. Read files saved in the database
+
+    ```python
+    >>> db.read_file("example.txt")
+    "Content of the file.", ""
+    ```
+
+    This method assumes the file can be read as `str`. This attempt will fail for binary files, so use `asbytes` option explicitly.
+
+    ```python
+    >>> db.read_file("example_binary.data", asbytes=True)
+    b"Content of the file.", b""
+    ```
+
+    This method returns 2 `str` or `bytes` values. The second value is metadata for the file saved using `db.save_file(filename, data, metadata)` method.
+
 ### Misc
 
 #### List all the tables contained in the database
diff --git a/necstdb/necstdb.py b/necstdb/necstdb.py
index cad939c..42cecd6 100644
--- a/necstdb/necstdb.py
+++ b/necstdb/necstdb.py
@@ -22,7 +22,22 @@ import pandas
 from . import utils
 
 
-class necstdb(object):
+def duplicate_rename(path: pathlib.Path, _i: int = 0) -> pathlib.Path:
+    """Return nonexistent path for new file.
+
+    Parameters
+    ----------
+    path
+        File path you want to create as *new* file.
+
+    """
+    _path = path if _i == 0 else path.parent / f"{path.stem}({_i}){path.suffix}"
+    if not _path.exists():
+        return _path
+    return duplicate_rename(path, _i + 1)
+
+
+class necstdb:
     """Database for NECST.
 
     Parameters
@@ -31,27 +46,21 @@ class necstdb(object):
         Path to the database directory, the direct parent of *.data and *.header files.
     mode: str
         Mode in which the database is opened, either "r" or "w".
+
     """
 
     def __init__(self, path: os.PathLike, mode: str) -> None:
         self.opendb(path, mode)
-        pass
 
     def opendb(self, path: os.PathLike, mode: str) -> None:
         """Catch the database directory."""
-        if not isinstance(path, pathlib.Path):
-            path = pathlib.Path(path)
-            pass
+        self.path = pathlib.Path(path)
 
-        self.path = path
-
-        if not path.exists():
+        if not self.path.exists():
             if mode.find("w") != -1:
-                path.mkdir(parents=True)
-
+                self.path.mkdir(parents=True)
             elif mode.find("r") != -1:
                 raise Exception("This directory doesn't exist!!")
-        return
 
     def list_tables(self) -> List[str]:
         """List all tables within the database."""
@@ -79,15 +88,75 @@ class necstdb(object):
         data_path.touch()
         with header_path.open("w") as f:
             json.dump(config, f)
-            pass
-        return
 
     def open_table(self, name: str, mode: str = "rb") -> "table":
         """Topic-wise data table."""
-        if getattr(self, "endian", None) is not None:
+        if hasattr(self, "endian"):
             return table(self.path, name, mode, self.endian)
         return table(self.path, name, mode)
 
+    def save_file(self, name: str, data: Union[str, bytes], info: str = "") -> None:
+        """Save a file in database.
+
+        Parameter
+        ---------
+        name
+            File name.
+        data
+            Contents to be written in the file.
+        info
+            Optional metadata for the file, will be added in first line of the file.
+
+        Examples
+        --------
+        >>> db.savefile(
+        ...     "example.txt",
+        ...     "Some contents here.",
+        ...     "Original path: /path/to/example.txt",
+        ... )
+
+        """
+        filepath = duplicate_rename(self.path / name)
+        splitter = "\n=== Written by NECSTDB ===\n"
+        if isinstance(data, bytes):
+            data = bytes(info + splitter, encoding="utf-8") + data
+            _ = filepath.write_bytes(data)
+        else:
+            data = info + splitter + data
+            _ = filepath.write_text(str(data))
+
+    def read_file(
+        self, name: str, asbytes: bool = False
+    ) -> Tuple[Union[str, bytes], Union[str, bytes]]:
+        """Read a file saved in the database.
+
+        Parameters
+        ----------
+        name
+            File name, extension inclusive.
+        asbytes
+            If True, the data will be returned as bytes.
+
+        Examples
+        --------
+        >>> db.read_file("example.txt")
+        "Some contents here.", "Original path: /path/to/example.txt"
+
+        """
+        if asbytes:
+            info, data = (
+                (self.path / name)
+                .read_bytes()
+                .split(b"\n=== Written by NECSTDB ===\n", 1)
+            )
+        else:
+            info, data = (
+                (self.path / name)
+                .read_text()
+                .split("\n=== Written by NECSTDB ===\n", 1)
+            )
+        return data, info
+
     def checkout(self, saveto: os.PathLike, compression: str = None) -> None:
         """Archive the database.
 
@@ -97,18 +166,18 @@ class necstdb(object):
             Path to the tar file to be created.
         compression: str
             Compression format/program to be used. One of ["gz", "bz2", "xz"].
+
         """
         mode = "w:"
         if compression is not None:
             mode += compression
-            pass
+
         tar = tarfile.open(saveto, mode=mode)
         tar.add(self.path)
         tar.close()
-        return
 
     def get_info(self) -> pandas.DataFrame:
-        """Metadata of all tables in the database."""
+        """Get metadata of all tables in the database."""
         names = self.list_tables()
 
         dictlist = []
@@ -123,7 +192,6 @@ class necstdb(object):
             }
             dictlist.append(dic)
             table.close()
-            continue
 
         df = pandas.DataFrame(
             dictlist,
@@ -139,8 +207,8 @@ class necstdb(object):
         return df
 
 
-class table(object):
-    """Data table which records single topic.
+class table:
+    """Data table for single topic.
 
     Parameters
     ----------
@@ -158,6 +226,7 @@ class table(object):
     Endian specifications ["@", "!"] are not supported, since numpy doesn't recognize
     them. Though ["="] is supported, the use of it is deprecated since the behavior may
     vary between architectures this program runs on.
+
     """
 
     dbpath = ""
@@ -178,7 +247,6 @@ class table(object):
 
         self._name = name
         self._mode = mode
-        pass
 
     def open(self, table_name: str, mode: str) -> None:
         """Open a data table of specified topic."""
@@ -186,12 +254,11 @@ class table(object):
         header_path = self.dbpath / (table_name + ".header")
 
         if not (data_path.exists() and header_path.exists()):
-            raise Exception("table '{table_name}' does not exist".format(**locals()))
+            raise Exception(f"Table '{table_name}' does not exist.")
 
         self.data_file = data_path.open(mode)
         with header_path.open("r") as header_file:
             self.header = json.load(header_file)
-            pass
 
         format_list = [dat["format"] for dat in self.header["data"]]
         self.format = self.endian + "".join(format_list)
@@ -205,17 +272,14 @@ class table(object):
             for dat, size in zip(self.header["data"], struct_sizes):
                 dat["size"] = size
             self.header["struct_indices"] = True
-        return
 
     def close(self) -> None:
         """Close the data file of the table."""
         self.data_file.close()
-        return
 
     def append(self, *data: Any) -> None:
         """Append data to the table."""
         self.data_file.write(struct.pack(self.format, *data))
-        return
 
     def read(
         self, num: int = -1, start: int = 0, cols: List[str] = [], astype: str = "tuple"
@@ -234,6 +298,7 @@ class table(object):
             One of ["tuple", "dict", "structuredarray", "dataframe", "buffer"] or their
             aliases, ["structured_array", "array", "sa", "data_frame", "pandas", "df",
             "raw"].
+
         """
         mm = mmap.mmap(self.data_file.fileno(), 0, prot=mmap.PROT_READ)
         mm.seek(start * self.record_size)
@@ -244,7 +309,6 @@ class table(object):
             if isinstance(cols, str):
                 raise ValueError("Column names should be given as list of str.")
             data = self._read_specified_cols(mm, num, cols)
-            pass
 
         mm.close()
         return self._astype(data, cols, astype)
@@ -255,7 +319,6 @@ class table(object):
             size = num
         else:
             size = num * self.record_size
-            pass
         return mm.read(size)
 
     def _read_specified_cols(
@@ -267,9 +330,10 @@ class table(object):
         -----
         The byte count of this function may contain bugs. For data which are not
         aligned, the count would be correct, but byte count for aligned data is much
-        difficult, hence the implementation may not perfect.
+        difficult, hence the implementation may not be perfect.
         One resolution for this problem would be to read all columns, then drop
         unnecessary columns.
+
         """
         commands = []
         for _col in self.header["data"]:
@@ -284,15 +348,12 @@ class table(object):
             num = (mm.size() - mm.tell()) // self.record_size
 
         draw = b""
-        for i in range(num):
+        for _ in range(num):
             for _cmd in commands:
                 if _cmd["cmd"] == "seek":
                     mm.seek(_cmd["size"], os.SEEK_CUR)
                 else:
                     draw += mm.read(_cmd["size"])
-                    pass
-                continue
-            continue
         return draw
 
     def _astype(
@@ -303,7 +364,6 @@ class table(object):
             cols = self.header["data"]
         else:
             cols = [_col for _col in self.header["data"] if _col["key"] in cols]
-            pass
 
         def DataFormatError(e: Union[Exception, str] = ""):
             return ValueError(
@@ -339,7 +399,8 @@ class table(object):
         elif astype in ["buffer", "raw"]:
             return data
 
-        return
+        else:
+            raise ValueError(f"Unknown return type {astype}.")
 
     def _astype_tuple(
         self, data: bytes, cols: List[Dict[str, Any]]
@@ -362,13 +423,11 @@ class table(object):
                 dat = struct.unpack(col["format"], data[offset : offset + size])
                 if len(dat) == 1:
                     dat = dat[0]
-                    pass
+
                 dict_[col["key"]] = dat
                 offset += col["size"]
-                continue
 
             dictlist.append(dict_)
-            continue
 
         return dictlist
 
@@ -410,6 +469,7 @@ class table(object):
         When other bug is found, this property should determine what the problem is,
         based on the value of the data (e.g. timestamp contains extremely small number
         such as 1e-308)
+
         """
         self.endian = ""
         self.open(self._name, self._mode)
@@ -427,5 +487,6 @@ def opendb(path: os.PathLike, mode: str = "r") -> "necstdb":
         Path to the database directory, the direct parent of *.data and *.header files.
     mode: str
         Mode in which the database is opened (e.g. ["rb", "wb", ...]).
+
     """
     return necstdb(path, mode)
diff --git a/poetry.lock b/poetry.lock
index 57a24f7..2aeae80 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -16,17 +16,17 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 
 [[package]]
 name = "attrs"
-version = "21.2.0"
+version = "21.4.0"
 description = "Classes Without Boilerplate"
 category = "dev"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
 
 [package.extras]
-dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"]
+dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"]
 docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
-tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"]
-tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"]
+tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
+tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"]
 
 [[package]]
 name = "black"
@@ -53,7 +53,7 @@ d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
 
 [[package]]
 name = "click"
-version = "8.0.1"
+version = "8.0.4"
 description = "Composable command line interface toolkit"
 category = "dev"
 optional = false
@@ -95,7 +95,7 @@ pyflakes = ">=2.3.0,<2.4.0"
 
 [[package]]
 name = "importlib-metadata"
-version = "4.7.1"
+version = "4.8.3"
 description = "Read metadata from Python packages"
 category = "dev"
 optional = false
@@ -108,7 +108,7 @@ zipp = ">=0.5"
 [package.extras]
 docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
 perf = ["ipython"]
-testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]
+testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]
 
 [[package]]
 name = "iniconfig"
@@ -144,14 +144,14 @@ python-versions = ">=3.6"
 
 [[package]]
 name = "packaging"
-version = "21.0"
+version = "21.3"
 description = "Core utilities for Python packages"
 category = "dev"
 optional = false
 python-versions = ">=3.6"
 
 [package.dependencies]
-pyparsing = ">=2.0.2"
+pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
 
 [[package]]
 name = "pandas"
@@ -179,25 +179,26 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
 
 [[package]]
 name = "pluggy"
-version = "0.13.1"
+version = "1.0.0"
 description = "plugin and hook calling mechanisms for python"
 category = "dev"
 optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+python-versions = ">=3.6"
 
 [package.dependencies]
 importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
 
 [package.extras]
 dev = ["pre-commit", "tox"]
+testing = ["pytest", "pytest-benchmark"]
 
 [[package]]
 name = "py"
-version = "1.10.0"
+version = "1.11.0"
 description = "library with cross-python path, ini-parsing, io, code, log facilities"
 category = "dev"
 optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
 
 [[package]]
 name = "pycodestyle"
@@ -217,15 +218,18 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 
 [[package]]
 name = "pyparsing"
-version = "2.4.7"
+version = "3.0.7"
 description = "Python parsing module"
 category = "dev"
 optional = false
-python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+python-versions = ">=3.6"
+
+[package.extras]
+diagrams = ["jinja2", "railroad-diagrams"]
 
 [[package]]
 name = "pytest"
-version = "6.2.4"
+version = "6.2.5"
 description = "pytest: simple powerful testing with Python"
 category = "dev"
 optional = false
@@ -238,7 +242,7 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""}
 importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
 iniconfig = "*"
 packaging = "*"
-pluggy = ">=0.12,<1.0.0a1"
+pluggy = ">=0.12,<2.0"
 py = ">=1.8.2"
 toml = "*"
 
@@ -258,7 +262,7 @@ six = ">=1.5"
 
 [[package]]
 name = "pytz"
-version = "2021.1"
+version = "2022.1"
 description = "World timezone definitions, modern and historical"
 category = "main"
 optional = false
@@ -266,11 +270,11 @@ python-versions = "*"
 
 [[package]]
 name = "regex"
-version = "2021.8.28"
+version = "2022.3.15"
 description = "Alternative regular expression module, to replace re."
 category = "dev"
 optional = false
-python-versions = "*"
+python-versions = ">=3.6"
 
 [[package]]
 name = "six"
@@ -290,23 +294,23 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
 
 [[package]]
 name = "typed-ast"
-version = "1.4.3"
+version = "1.5.2"
 description = "a fork of Python 2 and 3 ast modules with type comment support"
 category = "dev"
 optional = false
-python-versions = "*"
+python-versions = ">=3.6"
 
 [[package]]
 name = "typing-extensions"
-version = "3.10.0.0"
-description = "Backported and Experimental Type Hints for Python 3.5+"
+version = "4.1.1"
+description = "Backported and Experimental Type Hints for Python 3.6+"
 category = "dev"
 optional = false
-python-versions = "*"
+python-versions = ">=3.6"
 
 [[package]]
 name = "zipp"
-version = "3.5.0"
+version = "3.6.0"
 description = "Backport of pathlib-compatible object wrapper for zip files"
 category = "dev"
 optional = false
@@ -318,8 +322,8 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
 
 [metadata]
 lock-version = "1.1"
-python-versions = "^3.6.1"
-content-hash = "266b0a8e6c31fc5d1089d09e1f33e2635484bfe46519ac005b716608b16690ba"
+python-versions = ">=3.6.1, <3.10"
+content-hash = "92a9725adc8fb2414f4b603c3f9fcb84b8a24059c6ff9c7cfdd292c7ade52941"
 
 [metadata.files]
 appdirs = [
@@ -331,15 +335,15 @@ atomicwrites = [
     {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
 ]
 attrs = [
-    {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"},
-    {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"},
+    {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"},
+    {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
 ]
 black = [
     {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"},
 ]
 click = [
-    {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"},
-    {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"},
+    {file = "click-8.0.4-py3-none-any.whl", hash = "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1"},
+    {file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"},
 ]
 colorama = [
     {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
@@ -354,8 +358,8 @@ flake8 = [
     {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"},
 ]
 importlib-metadata = [
-    {file = "importlib_metadata-4.7.1-py3-none-any.whl", hash = "sha256:9e04bf59076a15a9b6dd9c27806e8fcdf15280ba529c6a8cc3f4d5b4875bdd61"},
-    {file = "importlib_metadata-4.7.1.tar.gz", hash = "sha256:c4eb3dec5f697682e383a39701a7de11cd5c02daf8dd93534b69e3e6473f6b1b"},
+    {file = "importlib_metadata-4.8.3-py3-none-any.whl", hash = "sha256:65a9576a5b2d58ca44d133c42a241905cc45e34d2c06fd5ba2bafa221e5d7b5e"},
+    {file = "importlib_metadata-4.8.3.tar.gz", hash = "sha256:766abffff765960fcc18003801f7044eb6755ffae4521c8e8ce8e83b9c9b0668"},
 ]
 iniconfig = [
     {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
@@ -406,8 +410,8 @@ numpy = [
     {file = "numpy-1.19.5.zip", hash = "sha256:a76f502430dd98d7546e1ea2250a7360c065a5fdea52b2dffe8ae7180909b6f4"},
 ]
 packaging = [
-    {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"},
-    {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"},
+    {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
+    {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
 ]
 pandas = [
     {file = "pandas-1.1.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:bf23a3b54d128b50f4f9d4675b3c1857a688cc6731a32f931837d72effb2698d"},
@@ -440,12 +444,12 @@ pathspec = [
     {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"},
 ]
 pluggy = [
-    {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
-    {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
+    {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
+    {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
 ]
 py = [
-    {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"},
-    {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"},
+    {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
+    {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
 ]
 pycodestyle = [
     {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"},
@@ -456,63 +460,96 @@ pyflakes = [
     {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"},
 ]
 pyparsing = [
-    {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
-    {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
+    {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"},
+    {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"},
 ]
 pytest = [
-    {file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"},
-    {file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"},
+    {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"},
+    {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"},
 ]
 python-dateutil = [
     {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
     {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
 ]
 pytz = [
-    {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"},
-    {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"},
+    {file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"},
+    {file = "pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7"},
 ]
 regex = [
-    {file = "regex-2021.8.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9d05ad5367c90814099000442b2125535e9d77581855b9bee8780f1b41f2b1a2"},
-    {file = "regex-2021.8.28-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3bf1bc02bc421047bfec3343729c4bbbea42605bcfd6d6bfe2c07ade8b12d2a"},
-    {file = "regex-2021.8.28-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f6a808044faae658f546dd5f525e921de9fa409de7a5570865467f03a626fc0"},
-    {file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a617593aeacc7a691cc4af4a4410031654f2909053bd8c8e7db837f179a630eb"},
-    {file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79aef6b5cd41feff359acaf98e040844613ff5298d0d19c455b3d9ae0bc8c35a"},
-    {file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0fc1f8f06977c2d4f5e3d3f0d4a08089be783973fc6b6e278bde01f0544ff308"},
-    {file = "regex-2021.8.28-cp310-cp310-win32.whl", hash = "sha256:6eebf512aa90751d5ef6a7c2ac9d60113f32e86e5687326a50d7686e309f66ed"},
-    {file = "regex-2021.8.28-cp310-cp310-win_amd64.whl", hash = "sha256:ac88856a8cbccfc14f1b2d0b829af354cc1743cb375e7f04251ae73b2af6adf8"},
-    {file = "regex-2021.8.28-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c206587c83e795d417ed3adc8453a791f6d36b67c81416676cad053b4104152c"},
-    {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8690ed94481f219a7a967c118abaf71ccc440f69acd583cab721b90eeedb77c"},
-    {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:328a1fad67445550b982caa2a2a850da5989fd6595e858f02d04636e7f8b0b13"},
-    {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c7cb4c512d2d3b0870e00fbbac2f291d4b4bf2634d59a31176a87afe2777c6f0"},
-    {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66256b6391c057305e5ae9209941ef63c33a476b73772ca967d4a2df70520ec1"},
-    {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8e44769068d33e0ea6ccdf4b84d80c5afffe5207aa4d1881a629cf0ef3ec398f"},
-    {file = "regex-2021.8.28-cp36-cp36m-win32.whl", hash = "sha256:08d74bfaa4c7731b8dac0a992c63673a2782758f7cfad34cf9c1b9184f911354"},
-    {file = "regex-2021.8.28-cp36-cp36m-win_amd64.whl", hash = "sha256:abb48494d88e8a82601af905143e0de838c776c1241d92021e9256d5515b3645"},
-    {file = "regex-2021.8.28-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b4c220a1fe0d2c622493b0a1fd48f8f991998fb447d3cd368033a4b86cf1127a"},
-    {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4a332404baa6665b54e5d283b4262f41f2103c255897084ec8f5487ce7b9e8e"},
-    {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c61dcc1cf9fd165127a2853e2c31eb4fb961a4f26b394ac9fe5669c7a6592892"},
-    {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ee329d0387b5b41a5dddbb6243a21cb7896587a651bebb957e2d2bb8b63c0791"},
-    {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f60667673ff9c249709160529ab39667d1ae9fd38634e006bec95611f632e759"},
-    {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b844fb09bd9936ed158ff9df0ab601e2045b316b17aa8b931857365ea8586906"},
-    {file = "regex-2021.8.28-cp37-cp37m-win32.whl", hash = "sha256:4cde065ab33bcaab774d84096fae266d9301d1a2f5519d7bd58fc55274afbf7a"},
-    {file = "regex-2021.8.28-cp37-cp37m-win_amd64.whl", hash = "sha256:1413b5022ed6ac0d504ba425ef02549a57d0f4276de58e3ab7e82437892704fc"},
-    {file = "regex-2021.8.28-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ed4b50355b066796dacdd1cf538f2ce57275d001838f9b132fab80b75e8c84dd"},
-    {file = "regex-2021.8.28-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28fc475f560d8f67cc8767b94db4c9440210f6958495aeae70fac8faec631797"},
-    {file = "regex-2021.8.28-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdc178caebd0f338d57ae445ef8e9b737ddf8fbc3ea187603f65aec5b041248f"},
-    {file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:999ad08220467b6ad4bd3dd34e65329dd5d0df9b31e47106105e407954965256"},
-    {file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:808ee5834e06f57978da3e003ad9d6292de69d2bf6263662a1a8ae30788e080b"},
-    {file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d5111d4c843d80202e62b4fdbb4920db1dcee4f9366d6b03294f45ed7b18b42e"},
-    {file = "regex-2021.8.28-cp38-cp38-win32.whl", hash = "sha256:473858730ef6d6ff7f7d5f19452184cd0caa062a20047f6d6f3e135a4648865d"},
-    {file = "regex-2021.8.28-cp38-cp38-win_amd64.whl", hash = "sha256:31a99a4796bf5aefc8351e98507b09e1b09115574f7c9dbb9cf2111f7220d2e2"},
-    {file = "regex-2021.8.28-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:04f6b9749e335bb0d2f68c707f23bb1773c3fb6ecd10edf0f04df12a8920d468"},
-    {file = "regex-2021.8.28-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b006628fe43aa69259ec04ca258d88ed19b64791693df59c422b607b6ece8bb"},
-    {file = "regex-2021.8.28-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:121f4b3185feaade3f85f70294aef3f777199e9b5c0c0245c774ae884b110a2d"},
-    {file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a577a21de2ef8059b58f79ff76a4da81c45a75fe0bfb09bc8b7bb4293fa18983"},
-    {file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1743345e30917e8c574f273f51679c294effba6ad372db1967852f12c76759d8"},
-    {file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e1e8406b895aba6caa63d9fd1b6b1700d7e4825f78ccb1e5260551d168db38ed"},
-    {file = "regex-2021.8.28-cp39-cp39-win32.whl", hash = "sha256:ed283ab3a01d8b53de3a05bfdf4473ae24e43caee7dcb5584e86f3f3e5ab4374"},
-    {file = "regex-2021.8.28-cp39-cp39-win_amd64.whl", hash = "sha256:610b690b406653c84b7cb6091facb3033500ee81089867ee7d59e675f9ca2b73"},
-    {file = "regex-2021.8.28.tar.gz", hash = "sha256:f585cbbeecb35f35609edccb95efd95a3e35824cd7752b586503f7e6087303f1"},
+    {file = "regex-2022.3.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:42eb13b93765c6698a5ab3bcd318d8c39bb42e5fa8a7fcf7d8d98923f3babdb1"},
+    {file = "regex-2022.3.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9beb03ff6fe509d6455971c2489dceb31687b38781206bcec8e68bdfcf5f1db2"},
+    {file = "regex-2022.3.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0a5a1fdc9f148a8827d55b05425801acebeeefc9e86065c7ac8b8cc740a91ff"},
+    {file = "regex-2022.3.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cb374a2a4dba7c4be0b19dc7b1adc50e6c2c26c3369ac629f50f3c198f3743a4"},
+    {file = "regex-2022.3.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c33ce0c665dd325200209340a88438ba7a470bd5f09f7424e520e1a3ff835b52"},
+    {file = "regex-2022.3.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04c09b9651fa814eeeb38e029dc1ae83149203e4eeb94e52bb868fadf64852bc"},
+    {file = "regex-2022.3.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab5d89cfaf71807da93c131bb7a19c3e19eaefd613d14f3bce4e97de830b15df"},
+    {file = "regex-2022.3.15-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0e2630ae470d6a9f8e4967388c1eda4762706f5750ecf387785e0df63a4cc5af"},
+    {file = "regex-2022.3.15-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:df037c01d68d1958dad3463e2881d3638a0d6693483f58ad41001aa53a83fcea"},
+    {file = "regex-2022.3.15-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:940570c1a305bac10e8b2bc934b85a7709c649317dd16520471e85660275083a"},
+    {file = "regex-2022.3.15-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7f63877c87552992894ea1444378b9c3a1d80819880ae226bb30b04789c0828c"},
+    {file = "regex-2022.3.15-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:3e265b388cc80c7c9c01bb4f26c9e536c40b2c05b7231fbb347381a2e1c8bf43"},
+    {file = "regex-2022.3.15-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:058054c7a54428d5c3e3739ac1e363dc9347d15e64833817797dc4f01fb94bb8"},
+    {file = "regex-2022.3.15-cp310-cp310-win32.whl", hash = "sha256:76435a92e444e5b8f346aed76801db1c1e5176c4c7e17daba074fbb46cb8d783"},
+    {file = "regex-2022.3.15-cp310-cp310-win_amd64.whl", hash = "sha256:174d964bc683b1e8b0970e1325f75e6242786a92a22cedb2a6ec3e4ae25358bd"},
+    {file = "regex-2022.3.15-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6e1d8ed9e61f37881c8db383a124829a6e8114a69bd3377a25aecaeb9b3538f8"},
+    {file = "regex-2022.3.15-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b52771f05cff7517f7067fef19ffe545b1f05959e440d42247a17cd9bddae11b"},
+    {file = "regex-2022.3.15-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:673f5a393d603c34477dbad70db30025ccd23996a2d0916e942aac91cc42b31a"},
+    {file = "regex-2022.3.15-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8923e1c5231549fee78ff9b2914fad25f2e3517572bb34bfaa3aea682a758683"},
+    {file = "regex-2022.3.15-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:764e66a0e382829f6ad3bbce0987153080a511c19eb3d2f8ead3f766d14433ac"},
+    {file = "regex-2022.3.15-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd00859291658fe1fda48a99559fb34da891c50385b0bfb35b808f98956ef1e7"},
+    {file = "regex-2022.3.15-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:aa2ce79f3889720b46e0aaba338148a1069aea55fda2c29e0626b4db20d9fcb7"},
+    {file = "regex-2022.3.15-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:34bb30c095342797608727baf5c8aa122406aa5edfa12107b8e08eb432d4c5d7"},
+    {file = "regex-2022.3.15-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:25ecb1dffc5e409ca42f01a2b2437f93024ff1612c1e7983bad9ee191a5e8828"},
+    {file = "regex-2022.3.15-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:aa5eedfc2461c16a092a2fabc5895f159915f25731740c9152a1b00f4bcf629a"},
+    {file = "regex-2022.3.15-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:7d1a6e403ac8f1d91d8f51c441c3f99367488ed822bda2b40836690d5d0059f5"},
+    {file = "regex-2022.3.15-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:3e4d710ff6539026e49f15a3797c6b1053573c2b65210373ef0eec24480b900b"},
+    {file = "regex-2022.3.15-cp36-cp36m-win32.whl", hash = "sha256:0100f0ded953b6b17f18207907159ba9be3159649ad2d9b15535a74de70359d3"},
+    {file = "regex-2022.3.15-cp36-cp36m-win_amd64.whl", hash = "sha256:f320c070dea3f20c11213e56dbbd7294c05743417cde01392148964b7bc2d31a"},
+    {file = "regex-2022.3.15-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fc8c7958d14e8270171b3d72792b609c057ec0fa17d507729835b5cff6b7f69a"},
+    {file = "regex-2022.3.15-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ca6dcd17f537e9f3793cdde20ac6076af51b2bd8ad5fe69fa54373b17b48d3c"},
+    {file = "regex-2022.3.15-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0214ff6dff1b5a4b4740cfe6e47f2c4c92ba2938fca7abbea1359036305c132f"},
+    {file = "regex-2022.3.15-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a98ae493e4e80b3ded6503ff087a8492db058e9c68de371ac3df78e88360b374"},
+    {file = "regex-2022.3.15-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b1cc70e31aacc152a12b39245974c8fccf313187eead559ee5966d50e1b5817"},
+    {file = "regex-2022.3.15-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4829db3737480a9d5bfb1c0320c4ee13736f555f53a056aacc874f140e98f64"},
+    {file = "regex-2022.3.15-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:303b15a3d32bf5fe5a73288c316bac5807587f193ceee4eb6d96ee38663789fa"},
+    {file = "regex-2022.3.15-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:dc7b7c16a519d924c50876fb152af661a20749dcbf653c8759e715c1a7a95b18"},
+    {file = "regex-2022.3.15-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ce3057777a14a9a1399b81eca6a6bfc9612047811234398b84c54aeff6d536ea"},
+    {file = "regex-2022.3.15-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:48081b6bff550fe10bcc20c01cf6c83dbca2ccf74eeacbfac240264775fd7ecf"},
+    {file = "regex-2022.3.15-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dcbb7665a9db9f8d7642171152c45da60e16c4f706191d66a1dc47ec9f820aed"},
+    {file = "regex-2022.3.15-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c155a1a80c5e7a8fa1d9bb1bf3c8a953532b53ab1196092749bafb9d3a7cbb60"},
+    {file = "regex-2022.3.15-cp37-cp37m-win32.whl", hash = "sha256:04b5ee2b6d29b4a99d38a6469aa1db65bb79d283186e8460542c517da195a8f6"},
+    {file = "regex-2022.3.15-cp37-cp37m-win_amd64.whl", hash = "sha256:797437e6024dc1589163675ae82f303103063a0a580c6fd8d0b9a0a6708da29e"},
+    {file = "regex-2022.3.15-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8afcd1c2297bc989dceaa0379ba15a6df16da69493635e53431d2d0c30356086"},
+    {file = "regex-2022.3.15-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0066a6631c92774391f2ea0f90268f0d82fffe39cb946f0f9c6b382a1c61a5e5"},
+    {file = "regex-2022.3.15-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8248f19a878c72d8c0a785a2cd45d69432e443c9f10ab924c29adda77b324ae"},
+    {file = "regex-2022.3.15-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d1f3ea0d1924feb4cf6afb2699259f658a08ac6f8f3a4a806661c2dfcd66db1"},
+    {file = "regex-2022.3.15-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:794a6bc66c43db8ed06698fc32aaeaac5c4812d9f825e9589e56f311da7becd9"},
+    {file = "regex-2022.3.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d1445824944e642ffa54c4f512da17a953699c563a356d8b8cbdad26d3b7598"},
+    {file = "regex-2022.3.15-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f553a1190ae6cd26e553a79f6b6cfba7b8f304da2071052fa33469da075ea625"},
+    {file = "regex-2022.3.15-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:75a5e6ce18982f0713c4bac0704bf3f65eed9b277edd3fb9d2b0ff1815943327"},
+    {file = "regex-2022.3.15-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f16cf7e4e1bf88fecf7f41da4061f181a6170e179d956420f84e700fb8a3fd6b"},
+    {file = "regex-2022.3.15-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:dad3991f0678facca1a0831ec1ddece2eb4d1dd0f5150acb9440f73a3b863907"},
+    {file = "regex-2022.3.15-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:491fc754428514750ab21c2d294486223ce7385446f2c2f5df87ddbed32979ae"},
+    {file = "regex-2022.3.15-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:6504c22c173bb74075d7479852356bb7ca80e28c8e548d4d630a104f231e04fb"},
+    {file = "regex-2022.3.15-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:01c913cf573d1da0b34c9001a94977273b5ee2fe4cb222a5d5b320f3a9d1a835"},
+    {file = "regex-2022.3.15-cp38-cp38-win32.whl", hash = "sha256:029e9e7e0d4d7c3446aa92474cbb07dafb0b2ef1d5ca8365f059998c010600e6"},
+    {file = "regex-2022.3.15-cp38-cp38-win_amd64.whl", hash = "sha256:947a8525c0a95ba8dc873191f9017d1b1e3024d4dc757f694e0af3026e34044a"},
+    {file = "regex-2022.3.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:591d4fba554f24bfa0421ba040cd199210a24301f923ed4b628e1e15a1001ff4"},
+    {file = "regex-2022.3.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b9809404528a999cf02a400ee5677c81959bc5cb938fdc696b62eb40214e3632"},
+    {file = "regex-2022.3.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f08a7e4d62ea2a45557f561eea87c907222575ca2134180b6974f8ac81e24f06"},
+    {file = "regex-2022.3.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a86cac984da35377ca9ac5e2e0589bd11b3aebb61801204bd99c41fac516f0d"},
+    {file = "regex-2022.3.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:286908cbe86b1a0240a867aecfe26a439b16a1f585d2de133540549831f8e774"},
+    {file = "regex-2022.3.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b7494df3fdcc95a1f76cf134d00b54962dd83189520fd35b8fcd474c0aa616d"},
+    {file = "regex-2022.3.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b1ceede92400b3acfebc1425937454aaf2c62cd5261a3fabd560c61e74f6da3"},
+    {file = "regex-2022.3.15-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0317eb6331146c524751354ebef76a7a531853d7207a4d760dfb5f553137a2a4"},
+    {file = "regex-2022.3.15-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9c144405220c5ad3f5deab4c77f3e80d52e83804a6b48b6bed3d81a9a0238e4c"},
+    {file = "regex-2022.3.15-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:5b2e24f3ae03af3d8e8e6d824c891fea0ca9035c5d06ac194a2700373861a15c"},
+    {file = "regex-2022.3.15-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f2c53f3af011393ab5ed9ab640fa0876757498aac188f782a0c620e33faa2a3d"},
+    {file = "regex-2022.3.15-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:060f9066d2177905203516c62c8ea0066c16c7342971d54204d4e51b13dfbe2e"},
+    {file = "regex-2022.3.15-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:530a3a16e57bd3ea0dff5ec2695c09632c9d6c549f5869d6cf639f5f7153fb9c"},
+    {file = "regex-2022.3.15-cp39-cp39-win32.whl", hash = "sha256:78ce90c50d0ec970bd0002462430e00d1ecfd1255218d52d08b3a143fe4bde18"},
+    {file = "regex-2022.3.15-cp39-cp39-win_amd64.whl", hash = "sha256:c5adc854764732dbd95a713f2e6c3e914e17f2ccdc331b9ecb777484c31f73b6"},
+    {file = "regex-2022.3.15.tar.gz", hash = "sha256:0a7b75cc7bb4cc0334380053e4671c560e31272c9d2d5a6c4b8e9ae2c9bd0f82"},
 ]
 six = [
     {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
@@ -523,43 +560,36 @@ toml = [
     {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
 ]
 typed-ast = [
-    {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"},
-    {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"},
-    {file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"},
-    {file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"},
-    {file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"},
-    {file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"},
-    {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"},
-    {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"},
-    {file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"},
-    {file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"},
-    {file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"},
-    {file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"},
-    {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"},
-    {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"},
-    {file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"},
-    {file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"},
-    {file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"},
-    {file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"},
-    {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"},
-    {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"},
-    {file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"},
-    {file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"},
-    {file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"},
-    {file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"},
-    {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"},
-    {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"},
-    {file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"},
-    {file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"},
-    {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"},
-    {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"},
+    {file = "typed_ast-1.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:183b183b7771a508395d2cbffd6db67d6ad52958a5fdc99f450d954003900266"},
+    {file = "typed_ast-1.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:676d051b1da67a852c0447621fdd11c4e104827417bf216092ec3e286f7da596"},
+    {file = "typed_ast-1.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc2542e83ac8399752bc16e0b35e038bdb659ba237f4222616b4e83fb9654985"},
+    {file = "typed_ast-1.5.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74cac86cc586db8dfda0ce65d8bcd2bf17b58668dfcc3652762f3ef0e6677e76"},
+    {file = "typed_ast-1.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:18fe320f354d6f9ad3147859b6e16649a0781425268c4dde596093177660e71a"},
+    {file = "typed_ast-1.5.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:31d8c6b2df19a777bc8826770b872a45a1f30cfefcfd729491baa5237faae837"},
+    {file = "typed_ast-1.5.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:963a0ccc9a4188524e6e6d39b12c9ca24cc2d45a71cfdd04a26d883c922b4b78"},
+    {file = "typed_ast-1.5.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0eb77764ea470f14fcbb89d51bc6bbf5e7623446ac4ed06cbd9ca9495b62e36e"},
+    {file = "typed_ast-1.5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:294a6903a4d087db805a7656989f613371915fc45c8cc0ddc5c5a0a8ad9bea4d"},
+    {file = "typed_ast-1.5.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:26a432dc219c6b6f38be20a958cbe1abffcc5492821d7e27f08606ef99e0dffd"},
+    {file = "typed_ast-1.5.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7407cfcad702f0b6c0e0f3e7ab876cd1d2c13b14ce770e412c0c4b9728a0f88"},
+    {file = "typed_ast-1.5.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f30ddd110634c2d7534b2d4e0e22967e88366b0d356b24de87419cc4410c41b7"},
+    {file = "typed_ast-1.5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8c08d6625bb258179b6e512f55ad20f9dfef019bbfbe3095247401e053a3ea30"},
+    {file = "typed_ast-1.5.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:90904d889ab8e81a956f2c0935a523cc4e077c7847a836abee832f868d5c26a4"},
+    {file = "typed_ast-1.5.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bbebc31bf11762b63bf61aaae232becb41c5bf6b3461b80a4df7e791fabb3aca"},
+    {file = "typed_ast-1.5.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c29dd9a3a9d259c9fa19d19738d021632d673f6ed9b35a739f48e5f807f264fb"},
+    {file = "typed_ast-1.5.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:58ae097a325e9bb7a684572d20eb3e1809802c5c9ec7108e85da1eb6c1a3331b"},
+    {file = "typed_ast-1.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:da0a98d458010bf4fe535f2d1e367a2e2060e105978873c04c04212fb20543f7"},
+    {file = "typed_ast-1.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:33b4a19ddc9fc551ebabca9765d54d04600c4a50eda13893dadf67ed81d9a098"},
+    {file = "typed_ast-1.5.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1098df9a0592dd4c8c0ccfc2e98931278a6c6c53cb3a3e2cf7e9ee3b06153344"},
+    {file = "typed_ast-1.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42c47c3b43fe3a39ddf8de1d40dbbfca60ac8530a36c9b198ea5b9efac75c09e"},
+    {file = "typed_ast-1.5.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f290617f74a610849bd8f5514e34ae3d09eafd521dceaa6cf68b3f4414266d4e"},
+    {file = "typed_ast-1.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:df05aa5b241e2e8045f5f4367a9f6187b09c4cdf8578bb219861c4e27c443db5"},
+    {file = "typed_ast-1.5.2.tar.gz", hash = "sha256:525a2d4088e70a9f75b08b3f87a51acc9cde640e19cc523c7e41aa355564ae27"},
 ]
 typing-extensions = [
-    {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"},
-    {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"},
-    {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"},
+    {file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"},
+    {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"},
 ]
 zipp = [
-    {file = "zipp-3.5.0-py3-none-any.whl", hash = "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"},
-    {file = "zipp-3.5.0.tar.gz", hash = "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"},
+    {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"},
+    {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"},
 ]
diff --git a/pyproject.toml b/pyproject.toml
index 67eecaa..9df9bae 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -7,11 +7,19 @@ license = "MIT"
 readme = "README.md"
 homepage = "https://github.com/ogawa-ros/necstdb"
 repository = "https://github.com/ogawa-ros/necstdb"
+classifiers=[
+    "Operating System :: POSIX :: Linux",
+    "Operating System :: MacOS :: MacOS X",
+    "Programming Language :: Python",
+    "Programming Language :: Python :: 3",
+    "License :: OSI Approved :: MIT License",
+    "Topic :: Software Development :: Libraries :: Python Modules",
+]
 
 [tool.poetry.dependencies]
-python = "^3.6.1"
-numpy = "^1.19.0"
-pandas = "^1.1.0"
+python = ">=3.6.1, <3.10"
+numpy = "^1.19"
+pandas = "^1.1"
 
 [tool.poetry.dev-dependencies]
 pytest = "^6.1.0"


Test patch:
diff --git a/tests/test_necstdb.py b/tests/test_necstdb.py
index cf8ce45..0ffdad3 100644
--- a/tests/test_necstdb.py
+++ b/tests/test_necstdb.py
@@ -1,419 +1,366 @@
-import pathlib
 import struct
+from pathlib import Path
 
+import numpy as np
+import pandas as pd
 import pytest
-import numpy
-import pandas
 
 import necstdb
 
 TIME = 1630042892.423945
 
+DATA1 = ([i, -16, -32, -64] for i in range(11))
 DATA1_HEADER = {
     "data": [
-        {"key": "test_bool", "format": "?", "size": 1},
-        {"key": "test_int8", "format": "b", "size": 1},
-        {"key": "test_int16", "format": "h", "size": 2},
-        {"key": "test_int32", "format": "i", "size": 4},
-        {"key": "test_int64", "format": "q", "size": 8},
+        {"key": "int8", "format": "b", "size": 1},
+        {"key": "int16", "format": "h", "size": 2},
+        {"key": "int32", "format": "i", "size": 4},
+        {"key": "int64", "format": "q", "size": 8},
     ],
-    "memo": "generated by test_necstdb",
-    "necstdb_version": necstdb.__version__,
+    "memo": "Generated by test_necstdb.",
+    "necstdb-version": necstdb.__version__,
 }
+DATA2 = ([i, 16, 32, 64] for i in range(22))
 DATA2_HEADER = {
     "data": [
-        {"key": "test_uint8", "format": "B", "size": 1},
-        {"key": "test_uint16", "format": "H", "size": 2},
-        {"key": "test_uint32", "format": "I", "size": 4},
-        {"key": "test_uint64", "format": "Q", "size": 8},
-        {"key": "test_string", "format": "6s", "size": 6},
+        {"key": "uint8", "format": "B", "size": 1},
+        {"key": "uint16", "format": "H", "size": 2},
+        {"key": "uint32", "format": "I", "size": 4},
+        {"key": "uint64", "format": "Q", "size": 8},
     ],
-    "memo": "generated by test_necstdb",
-    "necstdb_version": necstdb.__version__,
+    "memo": "Generated by test_necstdb.",
+    "necstdb-version": necstdb.__version__,
 }
+DATA3 = ([0.32, i, b"byte", b"c"] for i in range(33))
 DATA3_HEADER = {
     "data": [
-        {"key": "test_float32", "format": "f", "size": 4},
-        {"key": "test_float64", "format": "d", "size": 8},
-        # The following 2 formats have been supported by ROS but are now deprecated.
-        {"key": "test__byte", "format": "5s", "size": 5},
-        {"key": "test__char", "format": "c", "size": 1},
-        # Array data.
-        {"key": "test_array", "format": "3d", "size": 24},
+        {"key": "float32", "format": "f", "size": 4},
+        {"key": "float64", "format": "d", "size": 8},
+        # The following 2 data types have been supported by ROS but are now deprecated.
+        {"key": "_byte", "format": "4s", "size": 5},
+        {"key": "_char", "format": "c", "size": 1},
     ],
-    "memo": "generated by test_necstdb",
-    "necstdb_version": necstdb.__version__,
+    "memo": "Generated by test_necstdb.",
+    "necstdb-version": necstdb.__version__,
 }
-
-
-def DATA1(length):
-    for i in range(length):
-        yield [True, i, -16, -32, -64]
-
-
-def DATA2(length):
-    for i in range(length):
-        yield [i, 16, 32, 64, b"string"]
-
-
-def DATA3(length):
-    for i in range(length):
-        yield [32.32, i, b"bytes", b"c", [TIME, TIME, TIME]]
-
-
-# dtypes are not preserved
-# array data are flattened
-EXPECTED_DATA1_TUPLE = (True, 0, -16, -32, -64)
-EXPECTED_DATA2_TUPLE = (0, 16, 32, 64, b"string")
-EXPECTED_DATA3_TUPLE = pytest.approx((32.32, 0, b"bytes", b"c", TIME, TIME, TIME))
-
-# dtypes are not preserved
-EXPECTED_DATA1_DICT = {
-    "test_bool": True,
-    "test_int8": 0,
-    "test_int16": -16,
-    "test_int32": -32,
-    "test_int64": -64,
-}
-EXPECTED_DATA2_DICT = {
-    "test_uint8": 0,
-    "test_uint16": 16,
-    "test_uint32": 32,
-    "test_uint64": 64,
-    "test_string": b"string",
+DATA4 = ([True, b"str", [i, TIME, TIME]] for i in range(44))
+DATA4_HEADER = {
+    "data": [
+        {"key": "bool", "format": "?", "size": 1},
+        {"key": "string", "format": "3s", "size": 6},
+        # Array data.
+        {"key": "array", "format": "3d", "size": 24},
+    ]
 }
-EXPECTED_DATA3_DICT = pytest.approx(
-    {
-        "test_float32": 32.32,
-        "test_float64": 0,
-        "test__byte": b"bytes",
-        "test__char": b"c",
-        "test_array": (TIME, TIME, TIME),
-    }
-)
 
-# dtypes are not preserved
-EXPECTED_DATA1_DF = pandas.DataFrame(
-    [(True, 0, -16, -32, -64)],
-    columns=["test_bool", "test_int8", "test_int16", "test_int32", "test_int64"],
-)
-EXPECTED_DATA2_DF = pandas.DataFrame(
-    [(0, 16, 32, 64, b"string")],
-    columns=["test_uint8", "test_uint16", "test_uint32", "test_uint64", "test_string"],
-)
-EXPECTED_DATA3_DF = pandas.DataFrame(
-    [(32.32, 0, b"bytes", b"c", [TIME, TIME, TIME])],
-    columns=["test_float32", "test_float64", "test__byte", "test__char", "test_array"],
+# DTypes (int8, int16, ...) are not preserved.
+# Array data are not reconstructed.
+EXPECTED_DATA1_TUPLE = (3, -16, -32, -64)
+EXPECTED_DATA2_TUPLE = (3, 16, 32, 64)
+EXPECTED_DATA3_TUPLE = (0.32, 3, b"byte", b"c")
+EXPECTED_DATA4_TUPLE = (True, b"str", 3, TIME, TIME)
+
+# DTypes (int8, int16, ...) are not preserved.
+EXPECTED_DATA1_DICT = {"int8": 3, "int16": -16, "int32": -32, "int64": -64}
+EXPECTED_DATA2_DICT = {"uint8": 3, "uint16": 16, "uint32": 32, "uint64": 64}
+EXPECTED_DATA3_DICT = {"float32": 0.32, "float64": 3, "_byte": b"byte", "_char": b"c"}
+EXPECTED_DATA4_DICT = {"bool": True, "string": b"str", "array": (3, TIME, TIME)}
+
+# DTypes (int8, int16, ...) are not preserved.
+EXPECTED_DATA1_DF = pd.DataFrame(
+    [(3, -16, -32, -64)], columns=["int8", "int16", "int32", "int64"]
 )
-
-EXPECTED_DATA1_ARRAY = numpy.array(
-    [(True, 0, -16, -32, -64)],
-    dtype=[
-        ("test_bool", "?"),
-        ("test_int8", "i1"),
-        ("test_int16", "i2"),
-        ("test_int32", "i4"),
-        ("test_int64", "i8"),
-    ],
+EXPECTED_DATA2_DF = pd.DataFrame(
+    [(3, 16, 32, 64)], columns=["uint8", "uint16", "uint32", "uint64"]
 )
-EXPECTED_DATA2_ARRAY = numpy.array(
-    [(0, 16, 32, 64, b"string")],
-    dtype=[
-        ("test_uint8", "u1"),
-        ("test_uint16", "u2"),
-        ("test_uint32", "u4"),
-        ("test_uint64", "u8"),
-        ("test_string", "S6"),
-    ],
+EXPECTED_DATA3_DF = pd.DataFrame(
+    [(0.32, 3, b"byte", b"c")], columns=["float32", "float64", "_byte", "_char"]
 )
-EXPECTED_DATA3_ARRAY = numpy.array(
-    [(32.32, 0, b"bytes", b"c", [TIME, TIME, TIME])],
-    dtype=[
-        ("test_float32", "f4"),
-        ("test_float64", "f8"),
-        ("test__byte", "S5"),
-        ("test__char", "S1"),
-        ("test_array", "3f8"),
-    ],
+EXPECTED_DATA4_DF = pd.DataFrame(
+    [(True, b"str", [3, TIME, TIME])], columns=["bool", "string", "array"]
 )
 
-EXPECTED_DATA1_BUFFER = (
-    b"\x01\x00\xf0\xff\xe0\xff\xff\xff\xc0\xff\xff\xff\xff\xff\xff\xff"
-)
-EXPECTED_DATA2_BUFFER = b"\x00\x10\x00 \x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00string"
-EXPECTED_DATA3_BUFFER = (
-    b"\xaeG\x01B\x00\x00\x00\x00\x00\x00\x00\x00bytesc"
-    b"\xea!\x1b\xc3\x1eJ\xd8A\xea!\x1b\xc3\x1eJ\xd8A\xea!\x1b\xc3\x1eJ\xd8A"
+EXPECTED_DATA1_ARRAY = np.array(
+    [(3, -16, -32, -64)],
+    dtype=[("int8", "i1"), ("int16", "i2"), ("int32", "i4"), ("int64", "i8")],
 )
-
-
-# The data order is not preserved.
-EXPECTED_PARTIAL_DATA3_TUPLE = (b"bytes", TIME, TIME, TIME, b"c", 3)
-
-EXPECTED_PARTIAL_DATA3_DICT = pytest.approx(
-    {
-        "test__byte": b"bytes",
-        "test_array": (TIME, TIME, TIME),
-        "test__char": b"c",
-        "test_float64": 3,
-    }
+EXPECTED_DATA2_ARRAY = np.array(
+    [(3, 16, 32, 64)],
+    dtype=[("uint8", "u1"), ("uint16", "u2"), ("uint32", "u4"), ("uint64", "u8")],
 )
-EXPECTED_PARTIAL_DATA3_DF = pandas.DataFrame(
-    [(b"bytes", [TIME, TIME, TIME], b"c", 3)],
-    columns=["test__byte", "test_array", "test__char", "test_float64"],
+EXPECTED_DATA3_ARRAY = np.array(
+    [(0.32, 3, b"byte", b"c")],
+    dtype=[("float32", "f4"), ("float64", "f8"), ("_byte", "S4"), ("_char", "S1")],
 )
-EXPECTED_PARTIAL_DATA3_ARRAY = numpy.array(
-    [(b"bytes", [TIME, TIME, TIME], b"c", 3)],
-    dtype=[
-        ("test__byte", "S5"),
-        ("test_array", "3f8"),
-        ("test__char", "S1"),
-        ("test_float64", "f8"),
-    ],
+EXPECTED_DATA4_ARRAY = np.array(
+    [(True, b"str", [3, TIME, TIME])],
+    dtype=[("bool", "?"), ("string", "S3"), ("array", "3f8")],
 )
-EXPECTED_PARTIAL_DATA3_BUFFER = (
-    b"bytes\xea!\x1b\xc3\x1eJ\xd8A\xea!\x1b\xc3\x1eJ\xd8A\xea!\x1b\xc3\x1eJ\xd8A"
-    b"c\x00\x00\x00\x00\x00\x00\x08@"
+
+EXPECTED_DATA1_BYTE = b"\x03\xf0\xff\xe0\xff\xff\xff\xc0\xff\xff\xff\xff\xff\xff\xff"
+EXPECTED_DATA2_BYTE = b"\x03\x10\x00 \x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00"
+EXPECTED_DATA3_BYTE = b"\n\xd7\xa3>\x00\x00\x00\x00\x00\x00\x08@bytec"
+EXPECTED_DATA4_BYTE = (
+    b"\x01str"
+    b"\x00\x00\x00\x00\x00\x00\x08@\xea!\x1b\xc3\x1eJ\xd8A\xea!\x1b\xc3\x1eJ\xd8A"
 )
 
 
 @pytest.fixture(scope="module")
-def tmp_db_path(tmp_path_factory) -> pathlib.Path:
-    """Make variable 'tmp_db_path' available from throughout this test module."""
+def db_path(tmp_path_factory) -> Path:
+    """Path to temporary database directory."""
     return tmp_path_factory.mktemp("test_db")
 
 
-@pytest.mark.usefixtures("tmp_db_path")
+@pytest.mark.usefixtures("db_path")
 class TestWriteDatabase:
-    def test_write_db(self, tmp_db_path):
-        db = necstdb.opendb(tmp_db_path, mode="w")
+    def test_create_table(self, db_path):
+        db = necstdb.opendb(db_path, mode="w")
         db.create_table("data1", DATA1_HEADER)
         db.create_table("data2", DATA2_HEADER)
         db.create_table("data3", DATA3_HEADER)
-        table = {
-            name: db.open_table(name, mode="ab") for name in ["data1", "data2", "data3"]
+        db.create_table("data4", DATA4_HEADER)
+
+    def test_write_table(self, db_path):
+        db = necstdb.opendb(db_path, mode="w")
+        tables = {
+            name: db.open_table(name, mode="ab")
+            for name in ["data1", "data2", "data3", "data4"]
         }
-        for data in DATA1(19):
-            table["data1"].append(*data)
-        for data in DATA2(31):
-            table["data2"].append(*data)
-        for data in DATA3(43):
-            data_ = []
-            _ = [  # Flatten the data.
-                data_.extend(dat) if isinstance(dat, list) else data_.append(dat)
+
+        _ = [tables["data1"].append(*data) for data in DATA1]
+        _ = [tables["data2"].append(*data) for data in DATA2]
+        _ = [tables["data3"].append(*data) for data in DATA3]
+        for data in DATA4:
+            flat_data = []
+            _ = [
+                flat_data.extend(dat)
+                if isinstance(dat, list)
+                else flat_data.append(dat)
                 for dat in data
-            ]
-            table["data3"].append(*data_)
-        _ = [tab.close() for tab in table.values()]
+            ]  # Flatten the data.
+            tables["data4"].append(*flat_data)
 
+        _ = [tab.close() for tab in tables.values()]
 
-@pytest.fixture(scope="module")
-def db_read(tmp_db_path) -> necstdb.necstdb:
-    """Make variable 'db_read' available from throughout this test module."""
-    return necstdb.opendb(tmp_db_path)
+    def test_write_file(self, db_path):
+        db = necstdb.opendb(db_path, mode="w")
+        files = [
+            ("test.txt", "Test text."),
+            ("test.txt", "Test text, file name duplicated."),
+            ("test.log", b"Test text, bytes data."),
+            ("test.log", b"Test text.", "Original: 127.0.0.1:/path/to/test.log"),
+            ("test1.txt", "Test text.", "Original: 127.0.0.1:/path/to/test1.txt"),
+        ]
+        for args in files:
+            db.save_file(*args)
 
 
-@pytest.mark.usefixtures("db_read")
-class TestReadDatabase:
-    def test_read_types(self, db_read):
-        """Keys test."""
-        tuple_key = ["tuple"]
-        dict_key = ["dict"]
-        df_key = ["pandas", "dataframe", "data_frame", "df"]
-        array_key = ["structuredarray", "structured_array", "array", "sa"]
-        buffer_key = ["buffer", "raw"]
-
-        for type_ in tuple_key + dict_key + df_key + array_key + buffer_key:
-            _ = db_read.open_table("data1").read(astype=type_)
-
-    def test_read_tuple(self, db_read):
-        """Read as tuple."""
-        actual = {
-            name: db_read.open_table(name).read(astype="tuple")
-            for name in ["data1", "data2", "data3"]
-        }
+table_name = ["data1", "data2", "data3", "data4"]
 
-        assert len(actual["data1"]) == 19, "[tuple] Data number error."
-        assert EXPECTED_DATA1_TUPLE == actual["data1"][0], "[tuple] Data error."
-        assert EXPECTED_DATA2_TUPLE == actual["data2"][0], "[tuple] Data error."
-        assert EXPECTED_DATA3_TUPLE == actual["data3"][0], "[tuple] Data error."
+
+@pytest.mark.usefixtures("db_path")
+class TestReadDatabase:
+    def test_read_types(self, db_path):
+        # Just a astype keys test.
+        tuple_keys = ["tuple"]
+        dict_keys = ["dict"]
+        df_keys = ["pandas", "dataframe", "data_frame", "df"]
+        array_keys = ["structuredarray", "structured_array", "array", "sa"]
+        bytes_keys = ["buffer", "raw"]
+
+        db = necstdb.opendb(db_path)
+        for k in tuple_keys + dict_keys + df_keys + array_keys + bytes_keys:
+            _ = db.open_table("data1").read(astype=k)
+
+    def test_read_as_tuple(self, db_path):
+        db = necstdb.opendb(db_path)
+        actual = {name: db.open_table(name).read(astype="tuple") for name in table_name}
+
+        assert len(actual["data1"]) == 11
+        assert EXPECTED_DATA1_TUPLE == actual["data1"][3]
+        assert EXPECTED_DATA2_TUPLE == actual["data2"][3]
+        assert EXPECTED_DATA3_TUPLE == pytest.approx(actual["data3"][3])
+        assert EXPECTED_DATA4_TUPLE == actual["data4"][3]
         with pytest.raises(TypeError):
             # Array data are not supported for tuple.
-            assert len(actual["data3"][0][-1]) == 3, "[tuple] Array handling error."
-
-    def test_read_dict(self, db_read):
-        """Read as dict."""
-        actual = {
-            name: db_read.open_table(name).read(astype="dict")
-            for name in ["data1", "data2", "data3"]
-        }
-
-        assert len(actual["data1"]) == 19, "[dict] Data number error."
-        assert EXPECTED_DATA1_DICT == actual["data1"][0], "[dict] Data error."
-        assert EXPECTED_DATA2_DICT == actual["data2"][0], "[dict] Data error."
-        assert EXPECTED_DATA3_DICT == actual["data3"][0], "[dict] Data error."
-        err_msg = "[dict] Array handling error."
-        assert len(actual["data3"][0]["test_array"]) == 3, err_msg
-
-    def test_read_df(self, db_read):
-        """Read as pandas.DataFrame."""
-        actual = {
-            name: db_read.open_table(name).read(astype="pandas")
-            for name in ["data1", "data2", "data3"]
-        }
-
-        assert len(actual["data1"]) == 19, "[pandas] Data number error."
-        assert all(EXPECTED_DATA1_DF == actual["data1"].loc[0]), "[pandas] Data error."
-        assert all(EXPECTED_DATA2_DF == actual["data2"].loc[0]), "[pandas] Data error."
-        assert all(EXPECTED_DATA3_DF == actual["data3"].loc[0]), "[pandas] Data error."
-        err_msg = "[pandas] Array handling error."
-        assert len(actual["data3"].loc[0, "test_array"]) == 3, err_msg
-
-    def test_read_array(self, db_read):
-        """Read as numpy.ndarray."""
-        actual = {
-            name: db_read.open_table(name).read(astype="array")
-            for name in ["data1", "data2", "data3"]
-        }
-
-        assert len(actual["data1"]) == 19, "[array] Data number error."
-        assert all(EXPECTED_DATA1_ARRAY == actual["data1"][0]), "[array] Data error."
-        assert all(EXPECTED_DATA2_ARRAY == actual["data2"][0]), "[array] Data error."
-        assert all(EXPECTED_DATA3_ARRAY == actual["data3"][0]), "[array] Data error."
-        err_msg = "[array] Array handling error."
-        assert len(actual["data3"][0]["test_array"]) == 3, err_msg
-
-    def test_read_buffer(self, db_read):
-        """Read raw data."""
-        actual = {
-            name: db_read.open_table(name).read(astype="buffer")
-            for name in ["data1", "data2", "data3"]
-        }
+            assert len(actual["data4"][3][-1]) == 3
+
+    def test_read_as_dict(self, db_path):
+        db = necstdb.opendb(db_path)
+        actual = {name: db.open_table(name).read(astype="dict") for name in table_name}
+
+        assert len(actual["data1"]) == 11
+        assert EXPECTED_DATA1_DICT == actual["data1"][3]
+        assert EXPECTED_DATA2_DICT == actual["data2"][3]
+        assert EXPECTED_DATA3_DICT == pytest.approx(actual["data3"][3])
+        assert EXPECTED_DATA4_DICT == actual["data4"][3]
+        assert len(actual["data4"][3]["array"]) == 3
+
+    def test_read_as_df(self, db_path):
+        db = necstdb.opendb(db_path)
+        actual = {name: db.open_table(name).read(astype="df") for name in table_name}
+
+        assert len(actual["data1"]) == 11
+        assert all(EXPECTED_DATA1_DF == actual["data1"].loc[3])
+        assert all(EXPECTED_DATA2_DF == actual["data2"].loc[3])
+        assert all(EXPECTED_DATA3_DF == actual["data3"].loc[3])
+        assert all(EXPECTED_DATA4_DF == actual["data4"].loc[3])
+        assert len(actual["data4"].loc[3, "array"]) == 3
+
+    def test_read_as_array(self, db_path):
+        db = necstdb.opendb(db_path)
+        actual = {name: db.open_table(name).read(astype="array") for name in table_name}
+
+        assert len(actual["data1"]) == 11
+        assert all(EXPECTED_DATA1_ARRAY == actual["data1"][3])
+        assert all(EXPECTED_DATA2_ARRAY == actual["data2"][3])
+        assert all(EXPECTED_DATA3_ARRAY == actual["data3"][3])
+        assert all(EXPECTED_DATA4_ARRAY == actual["data4"][3])
+        assert len(actual["data4"][3]["array"]) == 3
+
+    def test_read_as_bytes(self, db_path):
+        db = necstdb.opendb(db_path)
+        actual = {name: db.open_table(name).read(astype="raw") for name in table_name}
+        formats = ["<bhiq", "<BHIQ", "<fd4sc", "<?3s3d"]
         unpacked = {
             k: tuple(struct.iter_unpack(fmt, v))
-            for fmt, (k, v) in zip(["<?bhiq", "<BHIQ6s", "<fd5sc3d"], actual.items())
-        }
-        size = {
-            k: struct.calcsize(fmt)
-            for k, fmt in zip(actual.keys(), ["<?bhiq", "<BHIQ6s", "<fd5sc3d"])
+            for fmt, (k, v) in zip(formats, actual.items())
         }
-
-        assert len(unpacked["data1"]) == 19, "[buffer] Data number error."
-        err_msg = "[buffer] Data error."
-        assert EXPECTED_DATA1_BUFFER == actual["data1"][: size["data1"]], err_msg
-        assert EXPECTED_DATA2_BUFFER == actual["data2"][: size["data2"]], err_msg
-        assert EXPECTED_DATA3_BUFFER == actual["data3"][: size["data3"]], err_msg
+        size = {k: struct.calcsize(fmt) for k, fmt in zip(actual.keys(), formats)}
+
+        idx = {k: slice(size[k] * 3, size[k] * 4) for k in table_name}
+        assert len(unpacked["data1"]) == 11
+        assert EXPECTED_DATA1_BYTE == actual["data1"][idx["data1"]]
+        assert EXPECTED_DATA2_BYTE == actual["data2"][idx["data2"]]
+        assert EXPECTED_DATA3_BYTE == actual["data3"][idx["data3"]]
+        assert EXPECTED_DATA4_BYTE == actual["data4"][idx["data4"]]
         with pytest.raises(TypeError):
-            # Array data cannot be supported.
-            assert len(unpacked["data3"][0][-1]) == 3, "[buffer] Array handling error."
-
-
-@pytest.mark.usefixtures("db_read")
+            assert len(unpacked["data4"][3][-1]) == 3
+
+    def test_read_file(self, db_path):
+        db = necstdb.opendb(db_path)
+        files_string = [
+            ("test.txt", "Test text.", ""),
+            ("test(1).txt", "Test text, file name duplicated.", ""),
+            ("test.log", "Test text, bytes data.", ""),
+            ("test(1).log", "Test text.", "Original: 127.0.0.1:/path/to/test.log"),
+            ("test1.txt", "Test text.", "Original: 127.0.0.1:/path/to/test1.txt"),
+        ]
+        for filename, data, info in files_string:
+            assert db.read_file(filename) == (data, info)
+
+        files_bytes = [
+            ("test.txt", b"Test text.", b""),
+            ("test(1).txt", b"Test text, file name duplicated.", b""),
+            ("test.log", b"Test text, bytes data.", b""),
+            ("test(1).log", b"Test text.", b"Original: 127.0.0.1:/path/to/test.log"),
+            ("test1.txt", b"Test text.", b"Original: 127.0.0.1:/path/to/test1.txt"),
+        ]
+        for filename, data, info in files_bytes:
+            assert db.read_file(filename, asbytes=True) == (data, info)
+
+
+@pytest.mark.usefixtures("db_path")
 class TestPartialRead:
-    def test_partial_read_tuple(self, db_read):
-        actual = db_read.open_table("data3").read(
-            astype="tuple",
-            start=3,
-            num=5,
-            cols=["test__byte", "test_array", "test__char", "test_float64"],
+    def test_partial_read_as_tuple(self, db_path):
+        db = necstdb.opendb(db_path)
+        actual = db.open_table("data4").read(
+            astype="tuple", start=3, num=5, cols=["array", "bool"]
         )
 
-        assert len(actual) == 5, "[tuple] Length doesn't match."
+        assert len(actual) == 5
         with pytest.raises(AssertionError):
             # Array data are not supported for tuple.
-            assert len(actual[0]) == 4, "[tuple] Length doesn't match."
-        err_msg = "[tuple] Start index error."
+            assert len(actual[0]) == 2
         with pytest.raises(AssertionError):
             # Data order is not preserved.
-            assert actual[0] == EXPECTED_PARTIAL_DATA3_TUPLE, err_msg
-        # Insensitive to the order.
-        assert all(val in EXPECTED_PARTIAL_DATA3_TUPLE for val in actual[0]), err_msg
-
-    def test_partial_read_dict(self, db_read):
-        actual = db_read.open_table("data3").read(
-            astype="dict",
-            start=3,
-            num=5,
-            cols=["test__byte", "test_array", "test__char", "test_float64"],
+            assert actual[0] == tuple(EXPECTED_DATA4_TUPLE[idx] for idx in [2, 3, 4, 0])
+        # Order insensitive matching
+        assert all(val in EXPECTED_DATA4_TUPLE for val in actual[0])
+
+    def test_partial_read_as_dict(self, db_path):
+        db = necstdb.opendb(db_path)
+        actual = db.open_table("data4").read(
+            astype="dict", start=3, num=5, cols=["array", "bool"]
         )
 
-        assert len(actual) == 5, "[dict] Length doesn't match."
-        assert len(actual[0]) == 4, "[dict] Length doesn't match."
-        assert actual[0] == EXPECTED_PARTIAL_DATA3_DICT, "[dict] Start index error."
+        assert len(actual) == 5
+        assert len(actual[0]) == 2
+        _ = [print(EXPECTED_DATA4_DICT[k] for k in ["array", "bool"])]
+        assert actual[0] == {k: EXPECTED_DATA4_DICT[k] for k in ["array", "bool"]}
 
-    def test_partial_read_df(self, db_read):
-        actual = db_read.open_table("data3").read(
-            astype="pandas",
-            start=3,
-            num=5,
-            cols=["test__byte", "test_array", "test__char", "test_float64"],
+    def test_partial_read_as_df(self, db_path):
+        db = necstdb.opendb(db_path)
+        actual = db.open_table("data4").read(
+            astype="df", start=3, num=5, cols=["array", "bool"]
         )
 
-        assert len(actual) == 5, "[pandas] Length doesn't match."
-        assert len(actual.loc[0]) == 4, "[pandas] Length doesn't match."
-        err_msg = "[pandas] Start index error."
-        assert all(actual.loc[0] == EXPECTED_PARTIAL_DATA3_DF), err_msg
-
-    def test_partial_read_array(self, db_read):
-        actual = db_read.open_table("data3").read(
-            astype="array",
-            start=3,
-            num=5,
-            cols=["test__byte", "test_array", "test__char", "test_float64"],
+        assert len(actual) == 5
+        assert len(actual.loc[0]) == 2
+        assert all(actual.loc[0] == EXPECTED_DATA4_DF[["array", "bool"]])
+
+    def test_partial_read_as_array(self, db_path):
+        db = necstdb.opendb(db_path)
+        actual = db.open_table("data4").read(
+            astype="array", start=3, num=5, cols=["array", "bool"]
         )
 
-        assert len(actual) == 5, "[array] Length doesn't match."
-        assert len(actual[0]) == 4, "[array] Length doesn't match."
-        err_msg = "[array] Start index error."
-        assert all(actual[0] == EXPECTED_PARTIAL_DATA3_ARRAY), err_msg
-
-    def test_partial_read_buffer(self, db_read):
-        actual = db_read.open_table("data3").read(
-            astype="buffer",
-            start=3,
-            num=5,
-            cols=["test__byte", "test_array", "test__char", "test_float64"],
+        assert len(actual) == 5
+        assert len(actual[0]) == 2
+        assert all(actual[0] == EXPECTED_DATA4_ARRAY[["array", "bool"]])
+
+    def test_partial_read_as_bytes(self, db_path):
+        db = necstdb.opendb(db_path)
+        actual = db.open_table("data4").read(
+            astype="raw", start=3, num=5, cols=["array", "bool"]
         )
-        unpacked = tuple(struct.iter_unpack("<5s3dcd", actual))
-        size = struct.calcsize("<5s3dcd")
+        unpacked = tuple(struct.iter_unpack("<3d?", actual))
+        size = struct.calcsize("<3d?")
+
+        slices = [slice(8, 16), slice(16, 24), slice(24, 32), slice(0, 1)]
 
-        assert len(actual) == size * 5, "[buffer] Length doesn't match."
+        assert len(actual) == size * 5
         with pytest.raises(AssertionError):
-            # Array data are not supported for tuple.
-            assert len(unpacked[0]) == 4, "[buffer] Length doesn't match."
-        err_msg = "[buffer] Start index error."
+            # Array data are not supported.
+            assert len(unpacked[0]) == 2
         with pytest.raises(AssertionError):
             # Data order is not preserved.
-            assert actual[:size] == EXPECTED_PARTIAL_DATA3_BUFFER, err_msg
-        assert set(actual[:size]) == set(EXPECTED_PARTIAL_DATA3_BUFFER), err_msg
+            assert actual[:size] == b"".join(
+                [EXPECTED_DATA4_BYTE[idx] for idx in slices]
+            )
+        # Order insensitive match
+        assert all(EXPECTED_DATA4_BYTE[idx] in actual for idx in slices)
 
 
 @pytest.fixture(scope="module")
-def tmp_archive_path(tmp_path_factory) -> pathlib.Path:
-    """Make variable 'tmp_archive_path' available from throughout this test module."""
+def archive_dir_path(tmp_path_factory) -> Path:
+    """Path to a directory archive file will saved in."""
     return tmp_path_factory.mktemp("archive")
 
 
-@pytest.mark.usefixtures("db_read", "tmp_archive_path")
-class TestFunction:
-    def test_list_tables(self, db_read):
-        err_msg = "Table name not correctly listed."
-        assert db_read.list_tables() == ["data1", "data2", "data3"], err_msg
+@pytest.mark.usefixtures("db_path", "archive_dir_path")
+class TestMethods:
+    def test_list_tables(self, db_path):
+        db = necstdb.opendb(db_path)
+        assert db.list_tables() == ["data1", "data2", "data3", "data4"]
 
-    def test_checkout(self, tmp_archive_path, db_read):
-        save_path = tmp_archive_path / "archive.tar.gz"
-        db_read.checkout(saveto=save_path, compression="gz")
+    def test_checkout(self, db_path, archive_dir_path):
+        db = necstdb.opendb(db_path)
+        save_path = archive_dir_path / "archive.tar.gz"
+        db.checkout(saveto=save_path, compression="gz")
 
-        err_msg = "Archive file not correctly created."
-        assert save_path.exists(), err_msg
+        assert save_path.exists()
 
-    def test_get_info(self, db_read):
-        actual = db_read.get_info()
-        expected = pandas.DataFrame(
+    def test_get_info(self, db_path):
+        db = necstdb.opendb(db_path)
+        actual = db.get_info()
+        expected = pd.DataFrame(
             [
-                ("data1", 304, 19, 16, "<?bhiq"),
-                ("data2", 651, 31, 21, "<BHIQ6s"),
-                ("data3", 1806, 43, 42, "<fd5sc3d"),
+                ("data1", 165, 11, 15, "<bhiq"),
+                ("data2", 330, 22, 15, "<BHIQ"),
+                ("data3", 594, 33, 17, "<fd4sc"),
+                ("data4", 1364, 44, 28, "<?3s3d"),
             ],
             columns=[
                 "table name",
@@ -424,9 +371,7 @@ class TestFunction:
             ],
         ).set_index("table name")
 
-        err_msg = "Information not correct."
-        assert all(actual.index == expected.index), err_msg
-        assert all(actual["#records"] == expected["#records"]), err_msg
-        rec_size_key = "record size [byte]"
-        assert all(actual[rec_size_key] == expected[rec_size_key]), err_msg
-        assert all(actual["format"] == expected["format"]), err_msg
+        assert all(actual.index == expected.index)
+        assert all(actual["#records"] == expected["#records"])
+        assert all(actual["record size [byte]"] == expected["record size [byte]"])
+        assert all(actual["format"] == expected["format"])