Repository: epoch8/datapipe (language: python)

Issue:
Merge MetaTable into DataTable



Discussion / hints:

Fix patch:
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 767f3d62..cf0c4b8c 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -18,5 +18,6 @@
     "python.linting.flake8Enabled": true,
     "python.linting.flake8Args": [
         "--max-line-length=127"
-    ]
+    ],
+    "python.formatting.provider": "black"
 }
\ No newline at end of file
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 00000000..ca823edb
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,38 @@
+{
+    // See https://go.microsoft.com/fwlink/?LinkId=733558
+    // for the documentation about the tasks.json format
+    "version": "2.0.0",
+    "tasks": [
+        {
+            "label": "mypy-whole-project",
+            "type": "shell",
+            "command": "source .venv/bin/activate; mypy -p datapipe --show-column-numbers --show-error-codes --ignore-missing-imports",
+            "presentation": {
+                "echo": true,
+                "reveal": "never",
+                "focus": false,
+                "panel": "shared",
+                "showReuseMessage": false,
+                "clear": true,
+                "revealProblems": "never"
+            },
+            "problemMatcher": {
+                "owner": "mypy",
+                "source": "mypy-whole-project",
+                "fileLocation": [
+                    "relative",
+                    "${workspaceFolder}"
+                ],
+                "pattern": {
+                    "regexp": "^([\\w.]+):(\\d+):(\\d+): (\\w+): (.+)  \\[([\\w-]+)\\]$",
+                    "file": 1,
+                    "line": 2,
+                    "column": 3,
+                    "code": 6,
+                    "severity": 4,
+                    "message": 5
+                }
+            }
+        }
+    ]
+}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 97aff13b..b242b1b2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,11 @@
+# 0.9.0
+
+## Обратной совместимости нет
+
+* **Индексация данных теперь множественная**
+* Класса `MetaStore` больше нет, его роль выполняет `DataStore`
+* `DataTable.store_chunk` теперь принимает `processed_idx`, отдельного метода `sync_meta_for_store_chunk` больше нет
+
 # 0.8.2
 
 * `inc_process_many` работает полностью инкрементально
diff --git a/TODO b/TODO
index 52fd2cea..6e844f42 100644
--- a/TODO
+++ b/TODO
@@ -1,28 +1,49 @@
-TODO:
-[ ] Сделать прогрессбар для gen_* шага
-[ ] Добавить возможность передавать аргументы в *Generate и *Transform
-[ ] Сделать возможность передавать fsspec urls в TableStoreFiledir, ex: zip://{label}/{id}_test.jpg::./CATALOG_DIR/archive.zip
-[ ] Ускорить обновление метадаты: заменить удаление/создание на апдейт
-[ ] Не удалять метаданные при удалении строк
-[ ] Many-to-one, one-to-many
-[ ] Переехать на SQLAlchemy 1.4 https://www.sqlalchemy.org/blog/2021/03/15/sqlalchemy-1.4.0-released/
-[ ] Сделать "контекст записи"/"контекст чтения" для TableDataStore чтобы можно было кешировать операции чтения
-[ ] Сделать синхронизацию DataStore с метадатой, как отдельный шаг
-
-Ветка Label Studio
-[ ] Починить баг с авторизацией в 1.0.2+ лейбел студии (в 1.0.1 работает)
-    UPD: Баг на их стороне, починить пока не смогу, см. https://github.com/heartexlabs/label-studio/issues/1049
-    [x] Добавить тест на sign up
-[x] Унести Project Setting в Лейбел Студию внутрь
-[x] Унести добавление unique_id в Лейбел Студию внутрь
-[x] Переписать получение текущей разметки через yield
-[x] Добавить параметры и тесты на data, annotations и predictions
-[ ] Подумать об обработке Run в лейбел студии в случае, когда сервис не запущен
-   Сценарий: если LS будет уничтожен и пересоздан, то наша синхронизация не зальет туда задачи заново (возможно, с разметкой)
+One-to-many:
+    ☐ Добавить схему данных как класс
+    ☐ Считать хеш-сумму в Store
+    ☐ Сделать MetaTable декоратором над транзакцией (или DataTable?)
+    ☐ Объединить DataTable и MetaTable
+        ✔ Перенести создание MetaTable в DataStore @done(21-09-26 21:03)
+        ✔ Перенести логику работы со списком таблиц из dsl.Catalog в DataStore @done(21-09-26 21:03)
+        ✔ Передвинуть глобальные функции inc_process, gen_process вниз datatable @done(21-09-26 21:05)
+        ☐ get_metadata
+        ☐ get_existing_idx
+        ☐ get_table_debug_info
+        ☐ store_chunk
+    ✔ Реализовать общие тесты для всех table_store @done(21-09-26 16:48)
+    ☐ Причесать работу с primary_schema
 
+TODO:
+    ☐ Сделать прогрессбар для gen_* шага
+    ☐ Добавить возможность передавать аргументы в *Generate и *Transform
+    ☐ Сделать возможность передавать fsspec urls в TableStoreFiledir, ex: zip://{label}/{id}_test.jpg::./CATALOG_DIR/archive.zip
+    ☐ Ускорить обновление метадаты: заменить удаление/создание на апдейт
+    ☐ Many-to-one, one-to-many
+    ☐ Переехать на SQLAlchemy 1.4 https://www.sqlalchemy.org/blog/2021/03/15/sqlalchemy-1.4.0-released/
+    ☐ Сделать "контекст записи"/"контекст чтения" для TableDataStore чтобы можно было кешировать операции чтения
+    ☐ Сделать синхронизацию DataStore с метадатой, как отдельный шаг
+    
+Ветка Label Studio:
+    ☐ Починить баг с авторизацией в 1.0.2+ лейбел студии (в 1.0.1 работает)
+        UPD: Баг на их стороне, починить пока не смогу, см. https://github.com/heartexlabs/label-studio/issues/1049
+    ☐ Подумать об обработке Run в лейбел студии в случае, когда сервис не запущен
+        Сценарий: если LS будет уничтожен и пересоздан, то наша синхронизация не зальет туда задачи заново (возможно, с разметкой)
+    ☐ Идемпотентная запись задач в LS, если задача уже есть, то она должна обновляться, а не дублироваться
 
-Done:
-[x] Поддержка внешних хранилищ для данных (папка с файлами)
-[x] Полноценные тесты
-[x] Использование конструктора запросов SQLAlchemy вместо сырого SQL
-[x] Поддержка SQLite
+Archive:
+  ✔ Реализовать честный апдейт данных в MetaTable @done(21-07-31 01:03) @project(One-to-many)
+  ✔ Вынести работу с БД в MetaTable отдельно от DBStore @done(21-07-24 21:40) @project(One-to-many)
+  ✔ Добавить поддержку дополнительных полей в MetaTable @done(21-09-26 14:46) @project(One-to-many)
+  ✔ Добавить индексацию Store на основе индексного датафрейма (а не индекса) @done(21-09-26 14:46) @project(One-to-many)
+  ✔ Перейти с id индекса, на произвольный индекс в DataTable @done(21-09-26 14:46) @project(One-to-many)
+  ✔ Удалить ConstIdx из Store @done(21-09-26 14:46) @project(TODO)
+  ✔ Не удалять метаданные при удалении строк @done(21-09-26 14:47) @project(TODO)
+  ✔ Поддержка внешних хранилищ для данных (папка с файлами) @project(TODO)
+  ✔ Полноценные тесты @project(TODO)
+  ✔ Использование конструктора запросов SQLAlchemy вместо сырого SQL @project(TODO)
+  ✔ Поддержка SQLite @project(TODO)
+  ✔ Добавить тест на sign up @project(Ветка Label Studio)
+  ✔ Унести Project Setting в Лейбел Студию внутрь @project(Ветка Label Studio)
+  ✔ Унести добавление unique_id в Лейбел Студию внутрь @project(Ветка Label Studio)
+  ✔ Переписать получение текущей разметки через yield @project(Ветка Label Studio)
+  ✔ Добавить параметры и тесты на data, annotations и predictions @project(Ветка Label Studio)
diff --git a/datapipe/cli.py b/datapipe/cli.py
index fc3834c8..66e453b2 100644
--- a/datapipe/cli.py
+++ b/datapipe/cli.py
@@ -1,26 +1,30 @@
 import click
 
-from datapipe.metastore import MetaStore
+from datapipe.datatable import DataStore
 from datapipe.dsl import Catalog, Pipeline
 
 
-def main(ms: MetaStore, catalog: Catalog, pipeline: Pipeline):
-    import logging
-    logging.basicConfig(level=logging.INFO)
-
+def main(ds: DataStore, catalog: Catalog, pipeline: Pipeline):
     @click.group()
-    def cli():
-        pass
+    @click.option('--debug', is_flag=True, help='Log debug output')
+    def cli(debug):
+        import logging
+        if debug:
+            logging.basicConfig(level=logging.DEBUG)
+        else:
+            logging.basicConfig(level=logging.INFO)
 
     @cli.command()
     def run():
         from .compute import run_pipeline
-        run_pipeline(ms, catalog, pipeline)
+        run_pipeline(ds, catalog, pipeline)
 
     @cli.command()
     def ui():
+        from .compute import build_compute
         from .debug_ui import ui_main
-        app = ui_main(ms, catalog, pipeline)
+        build_compute(ds, catalog, pipeline)
+        app = ui_main(ds, catalog, pipeline)
         app.run_server(host='0.0.0.0')
 
     cli()
diff --git a/datapipe/compute.py b/datapipe/compute.py
index 8b05ad6a..bbb5c563 100644
--- a/datapipe/compute.py
+++ b/datapipe/compute.py
@@ -4,8 +4,7 @@
 import logging
 from datapipe.label_studio.session import LabelStudioModerationStep
 
-from datapipe.metastore import MetaStore
-from datapipe.datatable import gen_process_many, inc_process_many, ExternalTableUpdater
+from datapipe.datatable import DataStore, gen_process_many, inc_process_many, ExternalTableUpdater
 
 from .dsl import BatchGenerate, ExternalTable, Catalog, Pipeline, BatchTransform, LabelStudioModeration
 from .step import ComputeStep
@@ -17,7 +16,7 @@
 class BatchGenerateStep(ComputeStep):
     func: Callable
 
-    def run(self, ms: MetaStore):
+    def run(self, ds: DataStore):
         gen_process_many(
             self.output_dts,
             self.func
@@ -29,9 +28,9 @@ class BatchTransformIncStep(ComputeStep):
     func: Callable
     chunk_size: int
 
-    def run(self, ms: MetaStore):
+    def run(self, ds: DataStore):
         inc_process_many(
-            ms,
+            ds,
             self.input_dts,
             self.output_dts,
             self.func,
@@ -39,19 +38,19 @@ def run(self, ms: MetaStore):
         )
 
 
-def build_compute(ms: MetaStore, catalog: Catalog, pipeline: Pipeline) -> List[ComputeStep]:
+def build_compute(ds: DataStore, catalog: Catalog, pipeline: Pipeline) -> List[ComputeStep]:
     res: List[ComputeStep] = []
 
     for name, tbl in catalog.catalog.items():
         if isinstance(tbl, ExternalTable):
             res.append(ExternalTableUpdater(
                 name=f'update_{name}',
-                table=catalog.get_datatable(ms, name)
+                table=catalog.get_datatable(ds, name)
             ))
 
     for step in pipeline.steps:
         if isinstance(step, BatchGenerate):
-            output_dts = [catalog.get_datatable(ms, name) for name in step.outputs]
+            output_dts = [catalog.get_datatable(ds, name) for name in step.outputs]
 
             res.append(BatchGenerateStep(
                 f'{step.func.__name__}',
@@ -61,8 +60,8 @@ def build_compute(ms: MetaStore, catalog: Catalog, pipeline: Pipeline) -> List[C
             ))
 
         if isinstance(step, BatchTransform):
-            input_dts = [catalog.get_datatable(ms, name) for name in step.inputs]
-            output_dts = [catalog.get_datatable(ms, name) for name in step.outputs]
+            input_dts = [catalog.get_datatable(ds, name) for name in step.inputs]
+            output_dts = [catalog.get_datatable(ds, name) for name in step.outputs]
 
             res.append(BatchTransformIncStep(
                 f'{step.func.__name__}',
@@ -73,8 +72,8 @@ def build_compute(ms: MetaStore, catalog: Catalog, pipeline: Pipeline) -> List[C
             ))
 
         if isinstance(step, LabelStudioModeration):
-            input_dts = [catalog.get_datatable(ms, name) for name in step.inputs]
-            output_dts = [catalog.get_datatable(ms, name) for name in step.outputs]
+            input_dts = [catalog.get_datatable(ds, name) for name in step.inputs]
+            output_dts = [catalog.get_datatable(ds, name) for name in step.outputs]
 
             res.append(LabelStudioModerationStep(
                 name=f"LabelStudioModeration (Project {step.project_title})",
@@ -101,13 +100,13 @@ def print_compute(steps: List[ComputeStep]) -> None:
     )
 
 
-def run_steps(ms: MetaStore, steps: List[ComputeStep]) -> None:
+def run_steps(ds: DataStore, steps: List[ComputeStep]) -> None:
     for step in steps:
         logger.info(f'Running {step.name} {[i.name for i in step.input_dts]} -> {[i.name for i in step.output_dts]}')
 
-        step.run(ms)
+        step.run(ds)
 
 
-def run_pipeline(ms: MetaStore, catalog: Catalog, pipeline: Pipeline) -> None:
-    steps = build_compute(ms, catalog, pipeline)
-    run_steps(ms, steps)
+def run_pipeline(ds: DataStore, catalog: Catalog, pipeline: Pipeline) -> None:
+    steps = build_compute(ds, catalog, pipeline)
+    run_steps(ds, steps)
diff --git a/datapipe/datatable.py b/datapipe/datatable.py
index 8470903e..ed7e0f68 100644
--- a/datapipe/datatable.py
+++ b/datapipe/datatable.py
@@ -1,4 +1,4 @@
-from typing import Callable, Generator, Iterator, List, Optional, Tuple, Union
+from typing import Callable, Dict, Iterator, List, Optional, Tuple, Union
 
 import inspect
 import logging
@@ -6,11 +6,14 @@
 import math
 
 import pandas as pd
+from sqlalchemy import alias, func, select, union, and_, or_
 import tqdm
 
-from datapipe.metastore import MetaTable, MetaStore
-from datapipe.store.types import Index, ChunkMeta
+from datapipe.types import DataDF, MetadataDF, IndexDF, data_to_index, index_difference
+from datapipe.store.database import DBConn
+from datapipe.metastore import MetaTable
 from datapipe.store.table_store import TableStore
+from datapipe.event_logger import EventLogger
 
 from datapipe.step import ComputeStep
 
@@ -22,103 +25,214 @@ class DataTable:
     def __init__(
         self,
         name: str,
+        meta_dbconn: DBConn,
         meta_table: MetaTable,
-        table_store: TableStore,  # Если None - создается по дефолту
+        table_store: TableStore,
+        event_logger: EventLogger,
     ):
         self.name = name
-
+        self.meta_dbconn = meta_dbconn
         self.meta_table = meta_table
         self.table_store = table_store
+        self.event_logger = event_logger
 
-    def _make_deleted_meta_df(self, now, old_meta_df, deleted_idx) -> pd.DataFrame:
-        res = old_meta_df.loc[deleted_idx]
-        res.loc[:, 'delete_ts'] = now
-        return res
+        self.primary_keys = meta_table.primary_keys
 
-    def get_metadata(self, idx: Optional[Index] = None) -> pd.DataFrame:
+    def get_metadata(self, idx: Optional[IndexDF] = None) -> MetadataDF:
         return self.meta_table.get_metadata(idx)
 
-    def get_data(self, idx: Optional[Index] = None) -> pd.DataFrame:
+    def get_data(self, idx: Optional[IndexDF] = None) -> DataDF:
         return self.table_store.read_rows(self.meta_table.get_existing_idx(idx))
 
-    def store_chunk(self, data_df: pd.DataFrame, now: float = None) -> ChunkMeta:
-        logger.debug(f'Inserting chunk {len(data_df)} rows into {self.name}')
+    def store_chunk(self, data_df: DataDF, processed_idx: IndexDF = None, now: float = None) -> None:
+        '''
+        Записать новые данные в таблицу.
 
-        new_idx, changed_idx, new_meta_df = self.meta_table.get_changes_for_store_chunk(data_df, now)
+        При указанном `processed_idx` удалить те строки, которые находятся внутри `processed_idx`, но
+        отсутствуют в `data_df`.
+        '''
 
-        self.table_store.insert_rows(data_df.loc[new_idx])
-        self.table_store.update_rows(data_df.loc[changed_idx])
+        logger.debug(f'Inserting chunk {len(data_df.index)} rows into {self.name}')
 
-        self.meta_table.update_meta_for_store_chunk(new_meta_df)
+        new_df, changed_df, new_meta_df, changed_meta_df = self.meta_table.get_changes_for_store_chunk(data_df, now)
+        # TODO implement transaction meckanism
+        self.table_store.insert_rows(new_df)
+        self.table_store.update_rows(changed_df)
 
-        return list(data_df.index)
+        self.meta_table.insert_meta_for_store_chunk(new_meta_df)
+        self.meta_table.update_meta_for_store_chunk(changed_meta_df)
 
-    def sync_meta_by_idx_chunks(self, chunks: List[ChunkMeta], processed_idx: pd.Index = None) -> None:
-        ''' Пометить удаленными объекты, которых больше нет '''
-        deleted_idx = self.meta_table.get_changes_for_sync_meta(chunks, processed_idx)
+        data_idx = data_to_index(data_df, self.primary_keys)
 
-        if len(deleted_idx) > 0:
-            self.table_store.delete_rows(deleted_idx)
+        if processed_idx is not None:
+            existing_idx = self.meta_table.get_existing_idx(processed_idx)
+            deleted_idx = index_difference(existing_idx, data_idx)
 
-        self.meta_table.update_meta_for_sync_meta(deleted_idx)
+            self.table_store.delete_rows(deleted_idx)
+            self.meta_table.mark_rows_deleted(deleted_idx)
 
-    def sync_meta_by_process_ts(self, process_ts: float) -> None:
-        deleted_dfs = self.meta_table.get_stale_idx(process_ts)
+    def delete_by_idx(self, idx: IndexDF, now: float = None) -> None:
+        self.table_store.delete_rows(idx)
+        self.meta_table.mark_rows_deleted(idx, now=now)
 
-        for deleted_df in deleted_dfs:
-            deleted_idx = deleted_df.index
+    def delete_stale_by_process_ts(self, process_ts: float, now: float = None) -> None:
+        for deleted_df in self.meta_table.get_stale_idx(process_ts):
+            deleted_idx = data_to_index(deleted_df, self.primary_keys)
             self.table_store.delete_rows(deleted_idx)
-            self.meta_table.update_meta_for_sync_meta(deleted_idx)
+            self.meta_table.mark_rows_deleted(deleted_idx, now=now)
 
-    def store(self, df: pd.DataFrame) -> None:
-        now = time.time()
 
-        chunk = self.store_chunk(
-            data_df=df,
-            now=now
-        )
-        self.sync_meta_by_idx_chunks(
-            chunks=[chunk],
+class DataStore:
+    def __init__(
+        self,
+        meta_dbconn: DBConn
+    ) -> None:
+        self.meta_dbconn = meta_dbconn
+        self.event_logger = EventLogger(self.meta_dbconn)
+        self.tables: Dict[str, DataTable] = {}
+
+    def create_table(self, name: str, table_store: TableStore) -> DataTable:
+        assert(name not in self.tables)
+
+        primary_schema = table_store.get_primary_schema()
+
+        res = DataTable(
+            name=name,
+            meta_dbconn=self.meta_dbconn,
+            meta_table=MetaTable(
+                dbconn=self.meta_dbconn,
+                name=name,
+                primary_schema=primary_schema,
+                event_logger=self.event_logger,
+            ),
+            table_store=table_store,
+            event_logger=self.event_logger,
         )
 
-    def get_data_chunked(self, chunksize: int = 1000) -> Generator[pd.DataFrame, None, None]:
-        meta_df = self.meta_table.get_metadata(idx=None)
-
-        for i in range(0, len(meta_df.index), chunksize):
-            yield self.get_data(meta_df.index[i:i+chunksize])
+        self.tables[name] = res
 
-    def get_indexes(self, idx: Optional[Index] = None) -> Index:
-        return self.meta_table.get_metadata(idx).index.tolist()
-
-
-def get_process_chunks(
-    ms: MetaStore,
-    inputs: List[DataTable],
-    outputs: List[DataTable],
-    chunksize: int = 1000,
-) -> Tuple[pd.Index, Iterator[List[pd.DataFrame]]]:
-    idx_count, idx_gen = ms.get_process_ids(
-        inputs=[i.meta_table for i in inputs],
-        outputs=[i.meta_table for i in outputs],
-        chunksize=chunksize
-    )
-
-    logger.info(f'Items to update {idx_count}')
+        return res
 
-    def gen():
-        if idx_count > 0:
-            for idx in idx_gen:
-                yield idx, [inp.get_data(idx.index) for inp in inputs]
+    def get_or_create_table(self, name: str, table_store: TableStore) -> DataTable:
+        if name in self.tables:
+            return self.tables[name]
+        else:
+            return self.create_table(name, table_store)
 
-    return idx_count, gen()
+    def get_process_ids(
+        self,
+        inputs: List[DataTable],
+        outputs: List[DataTable],
+        chunksize: int = 1000,
+    ) -> Tuple[int, Iterator[IndexDF]]:
+        '''
+        Метод для получения перечня индексов для обработки.
+
+        Returns: (idx_size, iterator<idx_df>)
+            idx_size - количество индексов требующих обработки
+            idx_df - датафрейм без колонок с данными, только индексная колонка
+        '''
+
+        if len(inputs) == 0:
+            return (0, iter([]))
+
+        inp_p_keys = [set(inp.primary_keys) for inp in inputs]
+        out_p_keys = [set(out.primary_keys) for out in outputs]
+        join_keys = set.intersection(*inp_p_keys, *out_p_keys)
+
+        if not join_keys:
+            raise ValueError("Impossible to carry out transformation. datatables do not contain intersecting ids")
+
+        def left_join(tbl_a, tbl_bbb):
+            q = tbl_a.join(
+                tbl_bbb[0],
+                and_(tbl_a.c[key] == tbl_bbb[0].c[key] for key in join_keys),
+                isouter=True
+            )
+            for tbl_b in tbl_bbb[1:]:
+                q = q.join(
+                    tbl_b,
+                    and_(tbl_a.c[key] == tbl_b.c[key] for key in join_keys),
+                    isouter=True
+                )
+
+            return q
+
+        inp_tbls = [inp.meta_table.sql_table.alias(f"inp_{inp.meta_table.sql_table.name}") for inp in inputs]
+        out_tbls = [out.meta_table.sql_table.alias(f"out_{out.meta_table.sql_table.name}") for out in outputs]
+        sql_requests = []
+
+        for inp in inp_tbls:
+            fields = [inp.c[key] for key in join_keys]
+            sql = select(fields).select_from(
+                left_join(
+                    inp,
+                    out_tbls
+                )
+            ).where(
+                or_(
+                    and_(
+                        or_(
+                            (
+                                out.c.process_ts
+                                <
+                                inp.c.update_ts
+                            ),
+                            out.c.process_ts.is_(None)
+                        ),
+                        inp.c.delete_ts.is_(None)
+                    )
+                    for out in out_tbls
+                )
+            )
+
+            sql_requests.append(sql)
+
+        for out in out_tbls:
+            fields = [out.c[key] for key in join_keys]
+            sql = select(fields).select_from(
+                left_join(
+                    out,
+                    inp_tbls
+                )
+            ).where(
+                or_(
+                    or_(
+                        (
+                            out.c.process_ts
+                            <
+                            inp.c.delete_ts
+                        ),
+                        inp.c.create_ts.is_(None)
+                    )
+                    for inp in inp_tbls
+                )
+            )
+
+            sql_requests.append(sql)
+
+        u1 = union(*sql_requests)
+
+        idx_count = self.meta_dbconn.con.execute(
+            select([func.count()])
+            .select_from(
+                alias(u1, name='union_select')
+            )
+        ).scalar()
+
+        return idx_count, pd.read_sql_query(
+            u1,
+            con=self.meta_dbconn.con,
+            chunksize=chunksize
+        )
 
 
-# FIXME перенести в compute.BatchGenerateStep
+# TODO перенести в compute.BatchGenerateStep
 def gen_process_many(
     dts: List[DataTable],
     proc_func: Callable[
         ...,
-        Iterator[Tuple[pd.DataFrame, ...]]
+        Iterator[Tuple[DataDF, ...]]
     ],
     **kwargs
 ) -> None:
@@ -139,43 +253,68 @@ def gen_process_many(
     while True:
         try:
             chunk_dfs = next(iterable)
+
+            if isinstance(chunk_dfs, pd.DataFrame):
+                chunk_dfs = [chunk_dfs]
         except StopIteration:
             break
         except Exception as e:
             logger.exception(f"Generating failed ({proc_func.__name__}): {str(e)}")
 
-            # FIXME перенести get_process* в compute.BatchGenerateStep и пользоваться event_logger из metastore
+            # TODO перенести get_process* в compute.BatchGenerateStep и пользоваться event_logger из metastore
             if dts:
-                dts[0].meta_table.event_logger.log_exception(e)
+                dts[0].event_logger.log_exception(e)
             return
 
         for k, dt_k in enumerate(dts):
-            chunk_df_kth = chunk_dfs[k] if len(dts) > 1 else chunk_dfs
-            dt_k.store_chunk(chunk_df_kth)
+            dt_k.store_chunk(chunk_dfs[k])
 
     for k, dt_k in enumerate(dts):
-        dt_k.sync_meta_by_process_ts(now)
+        dt_k.delete_stale_by_process_ts(now)
 
 
-# FIXME перенести в compute.BatchGenerateStep
+# TODO перенести в compute.BatchGenerateStep
 def gen_process(
     dt: DataTable,
     proc_func: Callable[[], Union[
-        pd.DataFrame,
-        Iterator[pd.DataFrame]]
+        DataDF,
+        Iterator[DataDF]]
     ],
     **kwargs
 ) -> None:
+    def proc_func_many():
+        for i in proc_func():
+            yield (i,)
+
     return gen_process_many(
         dts=[dt],
+        proc_func=proc_func_many,
+        **kwargs
+    )
+
+
+# TODO перенести в compute.BatchTransformStep
+def inc_process(
+    ds: DataStore,
+    input_dts: List[DataTable],
+    res_dt: DataTable,
+    proc_func: Callable,
+    chunksize: int = 1000,
+    **kwargs
+) -> None:
+    inc_process_many(
+        ds=ds,
+        input_dts=input_dts,
+        res_dts=[res_dt],
         proc_func=proc_func,
+        chunksize=chunksize,
         **kwargs
     )
 
 
-# FIXME перенести в compute.BatchGenerateStep
+# TODO перенести в compute.BatchGenerateStep
 def inc_process_many(
-    ms: MetaStore,
+    ds: DataStore,
     input_dts: List[DataTable],
     res_dts: List[DataTable],
     proc_func: Callable,
@@ -186,23 +325,26 @@ def inc_process_many(
     Множественная инкрементальная обработка `input_dts' на основе изменяющихся индексов
     '''
 
-    idx_count, input_dfs_gen = get_process_chunks(
-        ms,
+    idx_count, idx_gen = ds.get_process_ids(
         inputs=input_dts,
         outputs=res_dts,
         chunksize=chunksize
     )
 
+    logger.info(f'Items to update {idx_count}')
+
     if idx_count > 0:
-        for idx, input_dfs in tqdm.tqdm(input_dfs_gen, total=math.ceil(idx_count / chunksize)):
+        for idx in tqdm.tqdm(idx_gen, total=math.ceil(idx_count / chunksize)):
+            logger.debug(f'Idx to process: {idx.to_records()}')
+
+            input_dfs = [inp.get_data(idx) for inp in input_dts]
+
             if sum(len(j) for j in input_dfs) > 0:
                 try:
                     chunks_df = proc_func(*input_dfs, **kwargs)
                 except Exception as e:
                     logger.error(f"Transform failed ({proc_func.__name__}): {str(e)}")
-                    ms.event_logger.log_exception(e)
-
-                    idx = pd.concat(input_dfs).index
+                    ds.event_logger.log_exception(e)
 
                     continue
 
@@ -211,31 +353,11 @@ def inc_process_many(
                     chunk_df_k = chunks_df[k] if len(res_dts) > 1 else chunks_df
 
                     # Добавляем результат в результирующие чанки
-                    res_index = res_dt.store_chunk(chunk_df_k)
-                    res_dt.sync_meta_by_idx_chunks([res_index], processed_idx=idx.index)
+                    res_dt.store_chunk(data_df=chunk_df_k, processed_idx=idx)
 
             else:
                 for k, res_dt in enumerate(res_dts):
-                    res_dt.sync_meta_by_idx_chunks([], processed_idx=idx.index)
-
-
-# FIXME перенести в compute.BatchTransformStep
-def inc_process(
-    ds: MetaStore,
-    input_dts: List[DataTable],
-    res_dt: DataTable,
-    proc_func: Callable,
-    chunksize: int = 1000,
-    **kwargs
-) -> None:
-    inc_process_many(
-        ms=ds,
-        input_dts=input_dts,
-        res_dts=[res_dt],
-        proc_func=proc_func,
-        chunksize=chunksize,
-        **kwargs
-    )
+                    res_dt.delete_by_idx(idx)
 
 
 class ExternalTableUpdater(ComputeStep):
@@ -245,11 +367,19 @@ def __init__(self, name: str, table: DataTable):
         self.input_dts = []
         self.output_dts = [table]
 
-    def run(self, ms: MetaStore) -> None:
+    def run(self, ds: DataStore) -> None:
         ps_df = self.table.table_store.read_rows_meta_pseudo_df()
 
-        _, _, new_meta_df = self.table.meta_table.get_changes_for_store_chunk(ps_df)
-        self.table.meta_table.update_meta_for_store_chunk(new_meta_df)
+        _, _, new_meta_df, changed_meta_df = self.table.meta_table.get_changes_for_store_chunk(ps_df)
+
+        # TODO switch to iterative store_chunk and self.table.sync_meta_by_process_ts
+
+        self.table.meta_table.insert_meta_for_store_chunk(new_meta_df)
+        self.table.meta_table.update_meta_for_store_chunk(changed_meta_df)
+
+        deleted_idx = index_difference(
+            self.table.meta_table.get_existing_idx(),
+            data_to_index(ps_df, self.table.primary_keys)
+        )
 
-        deleted_idx = self.table.meta_table.get_changes_for_sync_meta([ps_df.index])
-        self.table.meta_table.update_meta_for_sync_meta(deleted_idx)
+        self.table.meta_table.mark_rows_deleted(deleted_idx)
diff --git a/datapipe/debug_ui/__init__.py b/datapipe/debug_ui/__init__.py
index d9ca3594..917d0cf4 100644
--- a/datapipe/debug_ui/__init__.py
+++ b/datapipe/debug_ui/__init__.py
@@ -4,20 +4,20 @@
 from dash import Dash
 from dash.dependencies import Input, Output
 
-from datapipe.metastore import MetaStore
 from datapipe.dsl import Catalog, Pipeline
+from datapipe.datatable import DataStore
 
 from .overview import ui_overview_setup, ui_overview_index
 from .tables import ui_tables_setup, ui_tables_index
 from .e2e import ui_e2e_setup, ui_e2e_index
 
 
-def ui_main(ms: MetaStore, catalog: Catalog, pipeline: Pipeline):
+def ui_main(ds: DataStore, catalog: Catalog, pipeline: Pipeline):
     app = Dash('Datapipe', external_stylesheets=[dbc.themes.BOOTSTRAP])
 
-    ui_overview_setup(app, ms, catalog, pipeline)
-    ui_tables_setup(app, ms, catalog, pipeline)
-    ui_e2e_setup(app, ms, catalog, pipeline)
+    ui_overview_setup(app, ds, catalog, pipeline)
+    ui_tables_setup(app, ds, catalog, pipeline)
+    ui_e2e_setup(app, ds, catalog, pipeline)
 
     sidebar = html.Div(
         [
@@ -57,11 +57,11 @@ def ui_main(ms: MetaStore, catalog: Catalog, pipeline: Pipeline):
     )
     def route(url):
         if url == '/':
-            return ui_overview_index(app, ms, catalog, pipeline)
+            return ui_overview_index(app, ds, catalog, pipeline)
         elif url == '/tables':
-            return ui_tables_index(app, ms, catalog, pipeline)
+            return ui_tables_index(app, ds, catalog, pipeline)
         elif url == '/e2e':
-            return ui_e2e_index(app, ms, catalog, pipeline)
+            return ui_e2e_index(app, ds, catalog, pipeline)
 
         else:
             return f'Unknown route {url}'
diff --git a/datapipe/debug_ui/overview.py b/datapipe/debug_ui/overview.py
index c0297936..f55f1460 100644
--- a/datapipe/debug_ui/overview.py
+++ b/datapipe/debug_ui/overview.py
@@ -1,14 +1,13 @@
 import dash_html_components as html
-import dash_core_components as dcc
 import dash_interactive_graphviz as gv
 from dash.dependencies import Input, Output
 
-from datapipe.metastore import MetaStore
 from datapipe.dsl import Catalog, Pipeline
 from datapipe.compute import build_compute
+from datapipe.datatable import DataStore
 
 
-def ui_overview_setup(app, ms: MetaStore, catalog: Catalog, pipeline: Pipeline):
+def ui_overview_setup(app, ds: DataStore, catalog: Catalog, pipeline: Pipeline):
     @app.callback(
         Output('overview_text', 'children'),
         [Input('pipeline_graph', 'selected_node')]
@@ -16,13 +15,17 @@ def ui_overview_setup(app, ms: MetaStore, catalog: Catalog, pipeline: Pipeline):
     def graph_node_selected(node_name):
         return node_name
 
-def ui_overview_index(app, ms: MetaStore, catalog: Catalog, pipeline: Pipeline):
-    steps = build_compute(ms, catalog, pipeline)
+
+def ui_overview_index(app, ds: DataStore, catalog: Catalog, pipeline: Pipeline):
+    steps = build_compute(ds, catalog, pipeline)
 
     steps_dots = []
 
+    for table_name, table in ds.tables.items():
+        steps_dots.append(f'{table_name} [shape=box3d label=<<B>{table_name}</B><BR/>{"<BR/>".join(table.primary_keys)}>]')
+
     for step in steps:
-        steps_dots.append(f'{step.name} [shape=parallelogram]')
+        steps_dots.append(f'{step.name} [shape=box]')
         for inp in step.input_dts:
             steps_dots.append(f'{inp.name} -> {step.name}')
         for out in step.output_dts:
@@ -40,7 +43,7 @@ def _build_dash_catalog_list():
         res = []
 
         for name, tbl in catalog.catalog.items():
-            di = ms.get_table_debug_info(name)
+            di = ds.get_table_debug_info(name)
 
             res.append(html.Li([
                 di.name,
@@ -51,7 +54,6 @@ def _build_dash_catalog_list():
 
         return html.Ul(res)
 
-
     def _build_dash_pipeline_list():
         res = []
 
@@ -59,14 +61,13 @@ def _build_dash_pipeline_list():
             res.append(html.Li([
                 step.name,
                 html.Ul([
-                    html.Li(f'Inputs: [' + ', '.join(i.name for i in step.input_dts) + ']'),
-                    html.Li(f'Outputs: [' + ', '.join(i.name for i in step.output_dts) + ']'),
-                    html.Li(f'To process: {len(ms.get_process_ids(step.input_dts, step.output_dts))}')
+                    html.Li('Inputs: [' + ', '.join(i.name for i in step.input_dts) + ']'),
+                    html.Li('Outputs: [' + ', '.join(i.name for i in step.output_dts) + ']'),
+                    html.Li(f'To process: {len(ds.get_process_ids(step.input_dts, step.output_dts))}')
                 ])
             ]))
-        
-        return html.Ul(res)
 
+        return html.Ul(res)
 
     def _dash_index():
         return [
@@ -81,5 +82,4 @@ def _dash_index():
             html.Div(id='overview_text'),
         ]
 
-
     return _dash_index()
diff --git a/datapipe/debug_ui/tables.py b/datapipe/debug_ui/tables.py
index b3dd0fb2..a54360cf 100644
--- a/datapipe/debug_ui/tables.py
+++ b/datapipe/debug_ui/tables.py
@@ -4,27 +4,31 @@
 
 from dash.dependencies import Input, Output
 
-def ui_tables_index(app, ms, catalog, pipeline):
+
+def ui_tables_index(app, ds, catalog, pipeline):
     return [
         html.H1('Tables data'),
         dcc.Dropdown(
             id='table-name',
-            options=[{'label': k, 'value': k} for k,v in catalog.catalog.items()],
+            options=[{'label': k, 'value': k} for k, v in catalog.catalog.items()],
         ),
         html.Div(id='table-content')
     ]
 
 
-def ui_tables_setup(app, ms, catalog, pipeline):
+def ui_tables_setup(app, ds, catalog, pipeline):
     @app.callback(
         Output('table-content', 'children'),
         [Input('table-name', 'value')]
     )
     def update_table(table_name):
         if table_name:
-            meta_df = ms.get_metadata(table_name).head(20)
-            data_df = catalog.catalog[table_name].store.read_rows(meta_df.index)
-            data_df = data_df.reset_index().applymap(str).applymap(lambda x: x if len(x) < 100 else x[:100] + '...')
+            tbl = catalog.get_datatable(ds, table_name)
+
+            idx_df = tbl.meta_table.get_existing_idx().head(20)
+            data_df = tbl.get_data(idx_df)
+
+            data_df = data_df.applymap(str).applymap(lambda x: x if len(x) < 100 else x[:100] + '...')
 
             return [dt.DataTable(
                 columns=[{"name": i if i != 'id' else '_id', "id": i} for i in data_df.columns],
diff --git a/datapipe/dsl.py b/datapipe/dsl.py
index 4e34c16d..0049cc14 100644
--- a/datapipe/dsl.py
+++ b/datapipe/dsl.py
@@ -3,9 +3,8 @@
 from abc import ABC
 from dataclasses import dataclass
 
-from datapipe.datatable import DataTable
+from datapipe.datatable import DataStore, DataTable
 from datapipe.store.table_store import TableStore
-from datapipe.metastore import MetaStore
 
 
 @dataclass
@@ -25,18 +24,13 @@ class ExternalTable(Table):
 class Catalog:
     def __init__(self, catalog: Dict[str, Table]):
         self.catalog = catalog
-
         self.data_tables: Dict[str, DataTable] = {}
 
-    def get_datatable(self, ms: MetaStore, name: str) -> DataTable:
-        if name not in self.data_tables:
-            self.data_tables[name] = DataTable(
-                name=name,
-                meta_table=ms.create_meta_table(name),
-                table_store=self.catalog[name].store
-            )
-
-        return self.data_tables[name]
+    def get_datatable(self, ds: DataStore, name: str) -> DataTable:
+        return ds.get_or_create_table(
+            name=name,
+            table_store=self.catalog[name].store
+        )
 
 
 class PipelineStep(ABC):
@@ -63,7 +57,7 @@ class BatchGenerate(PipelineStep):
 
 
 @dataclass
-class LabelStudioModeration:
+class LabelStudioModeration(PipelineStep):
     ls_url: str
     inputs: List[str]
     outputs: List[str]
diff --git a/datapipe/label_studio/session.py b/datapipe/label_studio/session.py
index c6fb9751..8bc60305 100644
--- a/datapipe/label_studio/session.py
+++ b/datapipe/label_studio/session.py
@@ -5,8 +5,7 @@
 import pandas as pd
 import requests
 
-from datapipe.datatable import gen_process_many, inc_process_many
-from datapipe.metastore import MetaStore
+from datapipe.datatable import DataStore, gen_process_many, inc_process_many
 from datapipe.step import ComputeStep
 
 from tqdm import tqdm
@@ -201,7 +200,7 @@ def upload_tasks_from_df(
         data = [
             {
                 'data': {
-                    'LabelStudioModerationStep__unique_id': idx,
+                    'LabelStudioModerationStep__unique_id': "-".join(input_df.iloc[idx][self.input_dts[0].primary_keys].apply(str)),  # noqa: E501
                     **{
                         column: input_df.loc[idx, column]
                         for column in self.data
@@ -217,7 +216,7 @@ def upload_tasks_from_df(
         input_df['annotations'] = input_df[self.annotations] if self.annotations is not None else [
             [] for _ in input_df.index
         ]
-        input_df = input_df[['tasks_id', 'annotations']]
+        input_df = input_df[self.input_dts[0].primary_keys + ['tasks_id', 'annotations']]
         return input_df
 
     def get_current_tasks_as_df(self):
@@ -238,6 +237,14 @@ def _cleanup_annotations(annotations):
                     del ann['created_ago']
             return annotations
 
+        df_input_keys = self.input_dts[0].get_data()[self.input_dts[0].primary_keys]
+        df_input_keys["LabelStudioModerationStep__unique_id"] = ""
+        splitter = ""
+        for key in self.input_dts[0].primary_keys:
+            df_input_keys["LabelStudioModerationStep__unique_id"] += splitter + df_input_keys[key].apply(str)
+            splitter = "-"
+        df_input_keys = df_input_keys.set_index("LabelStudioModerationStep__unique_id")
+
         for page in tqdm(range(1, total_pages + 1), desc='Getting tasks from Label Studio Projects...'):
             tasks_page, status_code = self.label_studio_session.get_tasks(
                 project_id=self.project_id,
@@ -255,17 +262,19 @@ def _cleanup_annotations(annotations):
                 },
                 index=[task['data']['LabelStudioModerationStep__unique_id'] for task in tasks_page]
             )
+            output_df = output_df.merge(df_input_keys, left_index=True, right_index=True)
+            output_df = output_df.reset_index(drop=True)
 
             yield output_df
 
-    def run(self, ms: MetaStore) -> None:
+    def run(self, ds: DataStore) -> None:
         if self.label_studio_session.is_service_up():
             if self.project_id is None:
                 self.__post_init__()
 
             # Upload Tasks from inputs to outputs
             inc_process_many(
-                ms,
+                ds,
                 self.input_dts,
                 self.output_dts,
                 self.upload_tasks_from_df,
diff --git a/datapipe/metastore.py b/datapipe/metastore.py
index 052321b9..d3f4392a 100644
--- a/datapipe/metastore.py
+++ b/datapipe/metastore.py
@@ -1,21 +1,21 @@
 from dataclasses import dataclass
-from typing import List, Tuple, Optional, Dict, Union, Iterator
+from typing import Iterator, Tuple, Dict, cast
 
 import logging
 import time
 
-from sqlalchemy.sql.expression import and_, or_, select
-from sqlalchemy import Column, Numeric, Float, func, union, alias
+from sqlalchemy.sql.expression import and_, bindparam, or_, select, update
+from sqlalchemy import Table, Column, Numeric, Float, func
+
 import pandas as pd
 
-from datapipe.store.types import Index, ChunkMeta
-from datapipe.store.database import TableStoreDB, DBConn
+from datapipe.types import IndexDF, DataSchema, DataDF, MetadataDF, data_to_index
+from datapipe.store.database import DBConn, sql_schema_to_sqltype
 from datapipe.event_logger import EventLogger
 
 
 logger = logging.getLogger('datapipe.metastore')
 
-
 METADATA_SQL_SCHEMA = [
     Column('hash', Numeric),
     Column('create_ts', Float),   # Время создания строки
@@ -32,217 +32,269 @@ class TableDebugInfo:
 
 
 class MetaTable:
-    def __init__(self, dbconn: DBConn, name: str, event_logger: EventLogger):
+    def __init__(
+        self,
+        dbconn: DBConn,
+        name: str,
+        primary_schema: DataSchema,
+        event_logger: EventLogger = None
+    ):
         self.dbconn = dbconn
         self.name = name
-        self.event_logger = event_logger
 
-        self.store = TableStoreDB(
-            dbconn=self.dbconn,
-            name=f'{name}_meta',
-            data_sql_schema=METADATA_SQL_SCHEMA,
-            create_table=True,
-        )
+        if event_logger is None:
+            self.event_logger = EventLogger(dbconn)
+        else:
+            self.event_logger = event_logger
 
-    def get_metadata(self, idx: Optional[Index] = None) -> pd.DataFrame:
-        return self.store.read_rows(idx)
-
-    def _make_new_metadata_df(self, now, df) -> pd.DataFrame:
-        # data
-
-        return pd.DataFrame(
-            {
-                'hash': pd.util.hash_pandas_object(df.apply(lambda x: str(list(x)), axis=1)),
-                'create_ts': now,
-                'update_ts': now,
-                'process_ts': now,
-                'delete_ts': None,
-            },
-            index=df.index
-        )
+        self.primary_keys = [column.name for column in primary_schema]
 
-    def get_existing_idx(self, idx: Index = None) -> Index:
-        return self.get_metadata(idx).index
+        for item in primary_schema:
+            item.primary_key = True
 
-    def get_table_debug_info(self, name: str) -> TableDebugInfo:
-        return TableDebugInfo(
-            name=name,
-            size=self.dbconn.con.execute(select([func.count()]).select_from(self.store.data_table)).fetchone()[0]
-        )
+        sql_schema = primary_schema + METADATA_SQL_SCHEMA
 
-    # TODO Может быть переделать работу с метадатой на контекстный менеджер?
-    def get_changes_for_store_chunk(
-        self,
-        data_df: pd.DataFrame,
-        now: float = None
-    ) -> Tuple[pd.Index, pd.Index, pd.DataFrame]:
-        if now is None:
-            now = time.time()
+        self.sql_schema = [i.copy() for i in sql_schema]
 
-        # получить meta по чанку
-        existing_meta_df = self.get_metadata(data_df.index)
+        self.sql_table = Table(
+            f'{self.name}_meta',
+            self.dbconn.sqla_metadata,
+            *self.sql_schema,
+        )
+
+        self.sql_table.create(self.dbconn.con, checkfirst=True)
 
-        # найти что изменилось
-        new_meta_df = self._make_new_metadata_df(now, data_df)
+    def get_metadata(self, idx: IndexDF = None, include_deleted: bool = False) -> MetadataDF:
+        '''
+        Получить датафрейм с метаданными.
 
-        common_idx = existing_meta_df.index.intersection(new_meta_df.index)
-        changed_idx = common_idx[new_meta_df.loc[common_idx, 'hash'] != existing_meta_df.loc[common_idx, 'hash']]
+        idx - опциональный фильтр по целевым строкам
+        include_deleted - флаг, возвращать ли удаленные строки, по умолчанию = False
+        '''
+        sql = select(self.sql_schema)
 
-        # найти что добавилось
-        new_idx = new_meta_df.index.difference(existing_meta_df.index)
+        if idx is not None:
+            row_queries = []
 
-        # обновить метаданные (удалить и записать всю new_meta_df, потому что изменился processed_ts)
-        if len(new_meta_df) > 0:
-            not_changed_idx = existing_meta_df.index.difference(changed_idx)
+            for _, row in idx.iterrows():
+                and_params = [self.sql_table.c[key] == self._get_sql_param(row[key]) for key in self.primary_keys]
+                and_query = and_(*and_params)
+                row_queries.append(and_query)
 
-            new_meta_df.loc[changed_idx, 'create_ts'] = existing_meta_df.loc[changed_idx, 'create_ts']
-            new_meta_df.loc[not_changed_idx, 'update_ts'] = existing_meta_df.loc[not_changed_idx, 'update_ts']
+            sql = sql.where(or_(*row_queries))
 
-        # TODO вынести в compute
-        if len(new_idx) > 0 or len(changed_idx) > 0:
-            self.event_logger.log_state(
-                self.name, added_count=len(new_idx), updated_count=len(changed_idx), deleted_count=0
-            )
+        if not include_deleted:
+            sql = sql.where(self.sql_table.c.delete_ts.is_(None))
 
-        return new_idx, changed_idx, new_meta_df
+        return pd.read_sql_query(
+            sql,
+            con=self.dbconn.con
+        )
 
-    def update_meta_for_store_chunk(self, new_meta_df: pd.DataFrame) -> None:
-        if len(new_meta_df) > 0:
-            self.store.update_rows(new_meta_df)
+    def _make_new_metadata_df(self, now: float, df: DataDF) -> MetadataDF:
+        res_df = df[self.primary_keys]
 
-    def update_meta_for_sync_meta(self, deleted_idx: pd.Index) -> None:
-        if len(deleted_idx) > 0:
-            self.store.delete_rows(deleted_idx)
+        res_df['hash'] = self._get_hash_for_df(df)
+        res_df['create_ts'] = now
+        res_df['update_ts'] = now
+        res_df['process_ts'] = now
+        res_df['delete_ts'] = None
 
-    def get_changes_for_sync_meta(self, chunks: List[ChunkMeta], processed_idx: pd.Index = None) -> pd.Index:
-        idx = pd.Index([])
-        for chunk in chunks:
-            idx = idx.union(chunk)
+        return cast(MetadataDF, res_df)
 
-        existing_meta_df = self.get_metadata(idx=processed_idx)
+    def _get_meta_data_columns(self):
+        return self.primary_keys + [column.name for column in METADATA_SQL_SCHEMA]
 
-        deleted_idx = existing_meta_df.index.difference(idx)
+    def _get_hash_for_df(self, df) -> pd.DataFrame:
+        return pd.util.hash_pandas_object(df.apply(lambda x: str(list(x)), axis=1))
 
-        if len(deleted_idx) > 0:
-            # TODO вынести в compute
-            self.event_logger.log_state(self.name, added_count=0, updated_count=0, deleted_count=len(deleted_idx))
+    # Fix numpy types in Index
+    # FIXME разобраться, что это за грязный хак
+    def _get_sql_param(self, param):
+        return param.item() if hasattr(param, "item") else param
 
-        return deleted_idx
+    def get_existing_idx(self, idx: IndexDF = None) -> IndexDF:
+        sql = select(self.sql_schema)
 
-    def get_stale_idx(self, process_ts: float) -> Iterator[pd.DataFrame]:
-        return pd.read_sql_query(
-            select([self.store.data_table.c.id]).where(
-                self.store.data_table.c.process_ts < process_ts
-            ),
-            con=self.store.dbconn.con,
-            index_col='id',
-            chunksize=1000
-        )
+        if idx is not None:
+            idx_cols = list(set(idx.columns) & set(self.primary_keys))
 
+            if not idx_cols:
+                raise ValueError("Index does not contain any primary key ")
 
-class MetaStore:
-    def __init__(self, dbconn: Union[str, DBConn]) -> None:
-        if isinstance(dbconn, str):
-            self.dbconn = DBConn(dbconn)
-        else:
-            self.dbconn = dbconn
-        self.event_logger = EventLogger(self.dbconn)
+            row_queries = []
 
-        self.meta_tables: Dict[str, MetaTable] = {}
+            for _, row in idx.iterrows():
+                and_params = [self.sql_table.c[key] == self._get_sql_param(row[key]) for key in idx_cols]
+                and_query = and_(*and_params)
+                row_queries.append(and_query)
 
-    def create_meta_table(self, name: str) -> MetaTable:
-        assert(name not in self.meta_tables)
+            sql = sql.where(or_(*row_queries))
 
-        res = MetaTable(
-            dbconn=self.dbconn,
-            name=name,
-            event_logger=self.event_logger,
+        sql = sql.where(self.sql_table.c.delete_ts.is_(None))
+        res_df = pd.read_sql_query(
+            sql,
+            con=self.dbconn.con,
         )
 
-        self.meta_tables[name] = res
+        return res_df[self.primary_keys]
 
-        return res
+    def get_table_debug_info(self, name: str) -> TableDebugInfo:
+        return TableDebugInfo(
+            name=name,
+            size=self.dbconn.con.execute(select([func.count()]).select_from(self.sql_table)).fetchone()[0]
+        )
 
-    def get_process_ids(
+    # TODO Может быть переделать работу с метадатой на контекстный менеджер?
+    # FIXME поправить возвращаемые структуры данных, _meta_df должны содержать только _meta колонки
+    def get_changes_for_store_chunk(
         self,
-        inputs: List[MetaTable],
-        outputs: List[MetaTable],
-        chunksize: int = 1000,
-    ) -> Tuple[int, Iterator[pd.DataFrame]]:
+        data_df: DataDF,
+        now: float = None
+    ) -> Tuple[DataDF, DataDF, MetadataDF, MetadataDF]:
         '''
-        Метод для получения перечня индексов для обработки.
+        Анализирует блок данных data_df, выделяет строки new_ которые нужно добавить и строки changed_ которые нужно обновить
 
-        Returns: (idx_size, iterator<idx_df>)
-            idx_size - количество индексов требующих обработки
-            idx_df - датафрейм без колонок с данными, только индексная колонка
+        Returns tuple:
+            new_data_df     - строки данных, которые нужно добавить
+            changed_data_df - строки данных, которые нужно изменить
+            new_meta_df     - строки метаданных, которые нужно добавить
+            changed_meta_df - строки метаданных, которые нужно изменить
         '''
 
-        if len(inputs) == 0:
-            return (0, iter([]))
+        if now is None:
+            now = time.time()
+
+        # получить meta по чанку
+        existing_meta_df = self.get_metadata(data_to_index(data_df, self.primary_keys), include_deleted=True)
+        data_cols = list(data_df.columns)
+        meta_cols = self._get_meta_data_columns()
+
+        # Дополняем данные методанными
+        merged_df = pd.merge(data_df, existing_meta_df,  how='left', left_on=self.primary_keys, right_on=self.primary_keys)
+        merged_df['data_hash'] = self._get_hash_for_df(data_df)
+
+        new_idx = (merged_df['hash'].isna() | merged_df['delete_ts'].notnull())
+
+        # Ищем новые записи
+        new_df = data_df.iloc[new_idx.values][data_cols]
+
+        # Создаем мета данные для новых записей
+        new_meta_data_df = merged_df.iloc[merged_df['hash'].isna().values][data_cols]
+        new_meta_df = self._make_new_metadata_df(now, new_meta_data_df)
+
+        # Ищем изменившиеся записи
+        changed_idx = (
+            (merged_df['hash'].notna()) &
+            (merged_df['delete_ts'].isnull()) &
+            (merged_df['hash'] != merged_df['data_hash'])
+        )
+        changed_df = merged_df.iloc[changed_idx.values][data_cols]
+
+        # Меняем мета данные для существующих записей
+        changed_meta_idx = (
+            (merged_df['hash'].notna()) &
+            (merged_df['hash'] != merged_df['data_hash']) |
+            (merged_df['delete_ts'].notnull())
+        )
+        changed_meta_df = merged_df[merged_df['hash'].notna()]
+
+        changed_meta_df.loc[changed_meta_idx, 'update_ts'] = now
+        changed_meta_df['process_ts'] = now
+        changed_meta_df['delete_ts'] = None
+        changed_meta_df['hash'] = changed_meta_df['data_hash']
+
+        if len(new_df.index) > 0 or len(changed_idx) > 0:
+            self.event_logger.log_state(
+                self.name, added_count=len(new_df.index), updated_count=len(changed_idx), deleted_count=0
+            )
 
-        def left_join(tbl_a, tbl_bbb):
-            q = tbl_a.join(
-                tbl_bbb[0],
-                tbl_a.c.id == tbl_bbb[0].c.id,
-                isouter=True
+        return (
+            cast(DataDF, new_df),
+            cast(DataDF, changed_df),
+            cast(MetadataDF, new_meta_df),
+            cast(MetadataDF, changed_meta_df[meta_cols]),
+        )
+
+    def _insert_rows(self, df: MetadataDF) -> None:
+        if len(df) > 0:
+            logger.debug(f'Inserting {len(df)} rows into {self.name} data')
+
+            df.to_sql(
+                name=self.sql_table.name,
+                con=self.dbconn.con,
+                schema=self.dbconn.schema,
+                if_exists='append',
+                index=False,
+                chunksize=1000,
+                method='multi',
+                dtype=sql_schema_to_sqltype(self.sql_schema),
             )
-            for tbl_b in tbl_bbb[1:]:
-                q = q.join(
-                    tbl_b,
-                    tbl_a.c.id == tbl_b.c.id,
-                    isouter=True
-                )
-
-            return q
-
-        sql_requests = []
-
-        for inp in inputs:
-            sql = select([inp.store.data_table.c.id]).select_from(
-                left_join(
-                    inp.store.data_table,
-                    [out.store.data_table for out in outputs]
-                )
-            ).where(
-                or_(
-                    or_(
-                        (
-                            out.store.data_table.c.process_ts
-                            <
-                            inp.store.data_table.c.update_ts
-                        ),
-                        out.store.data_table.c.process_ts.is_(None)
-                    )
-                    for out in outputs
-                )
+
+    def _update_existing_metadata_rows(self, df: MetadataDF) -> None:
+        if len(df) > 0:
+            stmt = (
+                update(self.sql_table)
+                .values({
+                    'hash': bindparam('b_hash'),
+                    'update_ts': bindparam('b_update_ts'),
+                    'process_ts': bindparam('b_process_ts'),
+                    'delete_ts': bindparam('b_delete_ts'),
+                })
             )
 
-            sql_requests.append(sql)
+            for key in self.primary_keys:
+                stmt = stmt.where(self.sql_table.c[key] == bindparam(f'b_{key}'))
+
+            columns = self.primary_keys + ['hash', 'update_ts', 'process_ts', 'delete_ts']
 
-        for out in outputs:
-            sql = select([out.store.data_table.c.id]).select_from(
-                left_join(
-                    out.store.data_table,
-                    [inp.store.data_table for inp in inputs]
-                )
-            ).where(
-                and_(inp.store.data_table.c.id.is_(None) for inp in inputs)
+            self.dbconn.con.execute(
+                stmt,
+                [
+                    {f'b_{k}': v for k, v in row.items()}
+                    for row in
+                    cast(
+                        Dict,
+                        df.reset_index()[columns].to_dict(orient='records')
+                    )
+                ]
             )
 
-            sql_requests.append(sql)
+    # TODO объединить
+    def insert_meta_for_store_chunk(self, new_meta_df: MetadataDF) -> None:
+        if len(new_meta_df) > 0:
+            self._insert_rows(new_meta_df)
 
-        u1 = union(*sql_requests)
+    def update_meta_for_store_chunk(self, changed_meta_df: MetadataDF) -> None:
+        if len(changed_meta_df) > 0:
+            self._update_existing_metadata_rows(changed_meta_df)
 
-        idx_count = self.dbconn.con.execute(
-            select([func.count()])
-            .select_from(
-                alias(u1, name='union_select')
+    def mark_rows_deleted(self, deleted_idx: IndexDF, now: float = None) -> None:
+        if len(deleted_idx) > 0:
+            logger.debug(f'Deleting {len(deleted_idx.index)} rows from {self.name} data')
+            self.event_logger.log_state(self.name, added_count=0, updated_count=0, deleted_count=len(deleted_idx))
+
+            if now is None:
+                now = time.time()
+
+            meta_df = self.get_metadata(deleted_idx)
+
+            meta_df["delete_ts"] = now
+            meta_df["process_ts"] = now
+
+            self._update_existing_metadata_rows(meta_df)
+
+    def get_stale_idx(self, process_ts: float) -> Iterator[IndexDF]:
+        idx_cols = [self.sql_table.c[key] for key in self.primary_keys]
+        sql = select(idx_cols).where(
+            and_(
+                self.sql_table.c.process_ts < process_ts,
+                self.sql_table.c.delete_ts.is_(None)
             )
-        ).scalar()
+        )
 
-        return idx_count, pd.read_sql_query(
-            u1,
+        return pd.read_sql_query(
+            sql,
             con=self.dbconn.con,
-            index_col='id',
-            chunksize=chunksize
+            chunksize=1000
         )
diff --git a/datapipe/step.py b/datapipe/step.py
index 93224e7e..4b0cfd5f 100644
--- a/datapipe/step.py
+++ b/datapipe/step.py
@@ -2,8 +2,7 @@
 from dataclasses import dataclass
 
 if TYPE_CHECKING:
-    from datapipe.metastore import MetaStore
-    from datapipe.datatable import DataTable
+    from datapipe.datatable import DataStore, DataTable
 
 
 @dataclass
@@ -12,5 +11,5 @@ class ComputeStep:
     input_dts: List['DataTable']
     output_dts: List['DataTable']
 
-    def run(self, ms: 'MetaStore'):
+    def run(self, ds: 'DataStore'):
         raise NotImplementedError
diff --git a/datapipe/store/database.py b/datapipe/store/database.py
index 82fb9867..52de183f 100644
--- a/datapipe/store/database.py
+++ b/datapipe/store/database.py
@@ -3,21 +3,31 @@
 import logging
 import pandas as pd
 
-from dataclasses import dataclass
-from sqlalchemy import Column, Table, String, create_engine, MetaData
-from sqlalchemy.sql.expression import select, delete
+from sqlalchemy import Column, Table, create_engine, MetaData, String, Integer
+from sqlalchemy.sql.expression import select, delete, and_, or_
+from sqlalchemy.pool import SingletonThreadPool
 
-from datapipe.store.types import Index
+from datapipe.types import DataDF, IndexDF, DataSchema, data_to_index
 from datapipe.store.table_store import TableStore
 
 
 logger = logging.getLogger('datapipe.store.database')
 
 
-PRIMARY_KEY = [Column('id', String(100), primary_key=True)]
+SCHEMA_TO_DTYPE_LOOKUP = {
+    String: str,
+    Integer: int,
+}
 
 
 def sql_schema_to_dtype(schema: List[Column]) -> Dict[str, Any]:
+    return {
+        i.name: SCHEMA_TO_DTYPE_LOOKUP[i.type.__class__]
+        for i in schema
+    }
+
+
+def sql_schema_to_sqltype(schema: List[Column]) -> Dict[str, Any]:
     return {
         i.name: i.type for i in schema
     }
@@ -33,6 +43,7 @@ def _init(self, connstr: str, schema: Optional[str]) -> None:
 
         self.con = create_engine(
             connstr,
+            poolclass=SingletonThreadPool,
         )
 
         self.sqla_metadata = MetaData(schema=schema)
@@ -47,12 +58,6 @@ def __setstate__(self, state):
         self._init(state['connstr'], state['schema'])
 
 
-@dataclass
-class ConstIdx:
-    column: Column
-    value: Any
-
-
 class TableStoreDB(TableStore):
     def __init__(
         self,
@@ -60,25 +65,14 @@ def __init__(
         name: str,
         data_sql_schema: List[Column],
         create_table: bool = True,
-        const_idx: List[ConstIdx] = []
     ) -> None:
         if isinstance(dbconn, str):
             self.dbconn = DBConn(dbconn)
         else:
             self.dbconn = dbconn
         self.name = name
-        self.filters = {}
-
-        const_idx_schema = []
-
-        if const_idx:
-            for item in const_idx:
-                item.column.primary_key = True
-                self.filters[item.column.name] = item.value
-
-                const_idx_schema.append(item.column)
 
-        self.data_sql_schema = PRIMARY_KEY + const_idx_schema + data_sql_schema
+        self.data_sql_schema = data_sql_schema
 
         self.data_table = Table(
             self.name, self.dbconn.sqla_metadata,
@@ -89,67 +83,64 @@ def __init__(
         if create_table:
             self.data_table.create(self.dbconn.con, checkfirst=True)
 
-    def delete_rows(self, idx: Index) -> None:
-        if len(idx) > 0:
-            logger.debug(f'Deleting {len(idx)} rows from {self.name} data')
+    def get_primary_schema(self) -> DataSchema:
+        return [column for column in self.data_sql_schema if column.primary_key]
 
-            sql = delete(self.data_table).where(
-                self.data_table.c.id.in_(list(idx))
-            )
+    def delete_rows(self, idx: IndexDF) -> None:
+        if len(idx.index):
+            logger.debug(f'Deleting {len(idx.index)} rows from {self.name} data')
+
+            row_queries = []
+
+            for _, row in idx.iterrows():
+                and_params = [self.data_table.c[key] == self._get_sql_param(row[key]) for key in self.primary_keys]
+                and_query = and_(*and_params)
+                row_queries.append(and_query)
 
-            if self.filters:
-                for key in self.filters.keys():
-                    column = getattr(self.data_table.c, key)
-                    sql = sql.where(column == self.filters[key])
+            sql = delete(self.data_table).where(or_(*row_queries))
 
             self.dbconn.con.execute(sql)
 
-    def insert_rows(self, df: pd.DataFrame) -> None:
+    def insert_rows(self, df: DataDF) -> None:
         if len(df) > 0:
             logger.debug(f'Inserting {len(df)} rows into {self.name} data')
 
-            if self.filters:
-                if set(self.filters.keys()) & set(df.columns):
-                    raise ValueError("DataFrame has constant index columns")
-
-                for key in self.filters.keys():
-                    df[key] = self.filters[key]
-
             df.to_sql(
                 name=self.name,
                 con=self.dbconn.con,
                 schema=self.dbconn.schema,
                 if_exists='append',
-                index_label='id',
+                index=False,
                 chunksize=1000,
                 method='multi',
-                dtype=sql_schema_to_dtype(self.data_sql_schema),
+                dtype=sql_schema_to_sqltype(self.data_sql_schema),
             )
 
-            if self.filters:
-                for key in self.filters.keys():
-                    del df[key]
-
-    def update_rows(self, df: pd.DataFrame) -> None:
-        self.delete_rows(df.index)
+    def update_rows(self, df: DataDF) -> None:
+        self.delete_rows(data_to_index(df, self.primary_keys))
         self.insert_rows(df)
 
-    def read_rows(self, idx: Optional[Index] = None) -> pd.DataFrame:
-        exclude_cols = self.filters.keys() if self.filters else []
-        sql_fields = [col for col in self.data_table.columns
-                      if col.name not in exclude_cols]
-        sql = select(sql_fields)
+    # Fix numpy types in IndexDF
+    def _get_sql_param(self, param):
+        return param.item() if hasattr(param, "item") else param
+
+    def read_rows(self, idx: Optional[IndexDF] = None) -> pd.DataFrame:
+        sql = select(self.data_table.c)
 
         if idx is not None:
-            sql = sql.where(self.data_table.c.id.in_(list(idx)))
+            row_queries = []
+
+            for _, row in idx.iterrows():
+                and_params = [self.data_table.c[key] == self._get_sql_param(row[key]) for key in self.primary_keys]
+                and_query = and_(*and_params)
+                row_queries.append(and_query)
+
+            if not row_queries:
+                return pd.DataFrame(columns=[column.name for column in self.data_sql_schema])
 
-        if self.filters:
-            for key in self.filters.keys():
-                column = getattr(self.data_table.c, key)
-                sql = sql.where(column == self.filters[key])
+            sql = sql.where(or_(*row_queries))
 
         return pd.read_sql_query(
             sql,
-            con=self.dbconn.con,
-            index_col='id',
+            con=self.dbconn.con
         )
diff --git a/datapipe/store/filedir.py b/datapipe/store/filedir.py
index e6175035..40ba5807 100644
--- a/datapipe/store/filedir.py
+++ b/datapipe/store/filedir.py
@@ -1,5 +1,5 @@
 from abc import ABC
-from typing import IO, Optional, Any, Dict, List, Union
+from typing import IO, Optional, Any, Dict, List, Union, cast
 from pathlib import Path
 
 import re
@@ -7,9 +7,10 @@
 import fsspec
 import pandas as pd
 
+from sqlalchemy import Column, String
 from PIL import Image
 
-from datapipe.store.types import Index
+from datapipe.types import DataDF, DataSchema, IndexDF, data_to_index
 from datapipe.store.table_store import TableStore
 
 
@@ -101,16 +102,22 @@ def __init__(
         self.adapter = adapter
         self.add_filepath_column = add_filepath_column
 
-        # Другие схемы идентификации еще не реализованы
+        # FIXME Другие схемы идентификации еще не реализованы
         assert(_pattern_to_attrnames(self.filename_pattern) == ['id'])
 
         self.filename_glob = _pattern_to_glob(self.filename_pattern)
         self.filename_match = _pattern_to_match(filename_pattern_for_match)
 
-    def delete_rows(self, idx: Index) -> None:
+    def get_primary_schema(self) -> DataSchema:
+        # FIXME реализовать поддержку других схем
+        return [Column('id', String(100), primary_key=True)]
+
+    def delete_rows(self, idx: IndexDF) -> None:
         # FIXME: Реализовать
         # Do not delete old files for now
         # Consider self.readonly as well
+        assert(not self.readonly)
+
         pass
 
     def _filename(self, item_id: str) -> str:
@@ -119,30 +126,30 @@ def _filename(self, item_id: str) -> str:
     def insert_rows(self, df: pd.DataFrame) -> None:
         assert(not self.readonly)
 
-        for i, data in zip(df.index, df.to_dict('records')):
-            filename = self._filename(i)
+        for i, data in zip(df['id'], cast(List[Dict[str, Any]], df.to_dict('records'))):
+            filename = self._filename(str(i))
 
             with fsspec.open(filename, f'w{self.adapter.mode}+') as f:
                 self.adapter.dump(data, f)
 
-    def read_rows(self, idx: Optional[Index] = None) -> pd.DataFrame:
+    def read_rows(self, idx: IndexDF = None) -> DataDF:
         if idx is None:
-            idx = self.read_rows_meta_pseudo_df().index
+            idx = data_to_index(self.read_rows_meta_pseudo_df(), self.primary_keys)
 
         def _gen():
-            for i in idx:
+            for i in idx['id']:
                 with (file_open := fsspec.open(self._filename(i), f'r{self.adapter.mode}')) as f:
                     data = self.adapter.load(f)
+                    data['id'] = i
                     if self.add_filepath_column:
                         data['filepath'] = f"{self.protocol_str}{file_open.path}"
                     yield data
 
         return pd.DataFrame.from_records(
-            _gen(),
-            index=idx
+            _gen()
         )
 
-    def read_rows_meta_pseudo_df(self, idx: Optional[Index] = None) -> pd.DataFrame:
+    def read_rows_meta_pseudo_df(self, idx: Optional[IndexDF] = None) -> DataDF:
         # Not implemented yet
         assert(idx is None)
 
@@ -162,15 +169,15 @@ def read_rows_meta_pseudo_df(self, idx: Optional[Index] = None) -> pd.DataFrame:
         if len(ids) > 0:
             pseudo_data_df = pd.DataFrame.from_records(
                 {
+                    'id': ids,
                     'ukey': ukeys,
-                },
-                index=ids
+                }
             )
             return pseudo_data_df
         else:
             return pd.DataFrame(
                 {
+                    'id': [],
                     'ukey': []
-                },
-                index=pd.Series([], dtype=str)
+                }
             )
diff --git a/datapipe/store/pandas.py b/datapipe/store/pandas.py
index 95010fe6..14c2f18b 100644
--- a/datapipe/store/pandas.py
+++ b/datapipe/store/pandas.py
@@ -1,41 +1,43 @@
 from abc import ABC
-from dataclasses import dataclass
-from typing import Callable, Optional
+from datapipe.types import DataDF
+from typing import Optional
 
 import fsspec
 import pandas as pd
 
 from datapipe.store.table_store import TableDataSingleFileStore
+from datapipe.store.database import sql_schema_to_dtype
 
 
-@dataclass
 class TableStoreExcel(TableDataSingleFileStore, ABC):
-    read_adapter: Optional[Callable[[pd.DataFrame], pd.DataFrame]] = None
+    def load_file(self) -> Optional[pd.DataFrame]:
+        of = fsspec.open(self.filename)
 
-    def load_file(self) -> pd.DataFrame:
-        with fsspec.open(self.filename) as f:
-            df = pd.read_excel(f, engine='openpyxl')
+        if of.fs.exists(of.path):
+            dtypes = sql_schema_to_dtype(self.primary_schema)
+            df = pd.read_excel(of.open(), engine='openpyxl', dtype=dtypes)
 
-        if self.read_adapter is not None:
-            df = self.read_adapter(df)
+            return df
+        else:
+            return None
 
-        return df
+    def save_file(self, df: DataDF) -> None:
+        with fsspec.open(self.filename, 'wb+') as f:
+            df.to_excel(f, engine='openpyxl', index=False)
 
 
-@dataclass
 class TableStoreJsonLine(TableDataSingleFileStore):
     def load_file(self) -> Optional[pd.DataFrame]:
         of = fsspec.open(self.filename)
 
         if of.fs.exists(of.path):
-            df = pd.read_json(of.open(), orient='records', lines=True, dtype={'id': str})
-            df = df.set_index('id')
+            dtypes = sql_schema_to_dtype(self.primary_schema)
+            df = pd.read_json(of.open(), orient='records', lines=True, dtype=dtypes)
 
             return df
         else:
             return None
 
-    def save_file(self, df: pd.DataFrame) -> None:
-        df.index.rename('id', inplace=True)
+    def save_file(self, df: DataDF) -> None:
         with fsspec.open(self.filename, 'w+') as f:
-            df.reset_index().to_json(f, orient='records', lines=True, force_ascii=False)
+            df.to_json(f, orient='records', lines=True, force_ascii=False)
diff --git a/datapipe/store/table_store.py b/datapipe/store/table_store.py
index 5d054dfc..4d573b12 100644
--- a/datapipe/store/table_store.py
+++ b/datapipe/store/table_store.py
@@ -1,86 +1,118 @@
 from abc import ABC
-from typing import Optional, Union
-from dataclasses import dataclass
+from typing import List, Optional, Union
 
+from sqlalchemy import Column, String
 import pandas as pd
 from pathlib import Path
 
-from datapipe.store.types import Index
+from datapipe.types import IndexDF, DataDF, DataSchema
 
 
 class TableStore(ABC):
-    def delete_rows(self, idx: Index) -> None:
+    def get_primary_schema(self) -> DataSchema:
         raise NotImplementedError
 
-    def insert_rows(self, df: pd.DataFrame) -> None:
+    @property
+    def primary_keys(self) -> List[str]:
+        return [i.name for i in self.get_primary_schema()]
+
+    def delete_rows(self, idx: IndexDF) -> None:
+        raise NotImplementedError
+
+    def insert_rows(self, df: DataDF) -> None:
         raise NotImplementedError
 
-    def update_rows(self, df: pd.DataFrame) -> None:
+    def update_rows(self, df: DataDF) -> None:
         self.delete_rows(df.index)
         self.insert_rows(df)
 
-    def read_rows(self, idx: Optional[Index] = None) -> pd.DataFrame:
+    def read_rows(self, idx: IndexDF = None) -> DataDF:
         raise NotImplementedError
 
-    def read_rows_meta_pseudo_df(self, idx: Optional[Index] = None) -> pd.DataFrame:
+    def read_rows_meta_pseudo_df(self, idx: Optional[IndexDF] = None) -> DataDF:
         '''
         Подготовить датафрейм с "какбы данными" на основе которых посчитается хеш и обновятся метаданные
-
-        По умолчанию используется read_rows
         '''
+
+        # TODO переделать на чанкированную обработку
         return self.read_rows(idx)
 
 
-@dataclass
 class TableDataSingleFileStore(TableStore):
-    filename: Union[Path, str]
 
-    def load_file(self) -> Optional[pd.DataFrame]:
+    def __init__(self, filename: Union[Path, str] = None, primary_schema: DataSchema = None):
+        if not primary_schema:
+            primary_schema = [Column("id", String(), primary_key=True)]
+
+        self.primary_schema = primary_schema
+        self.filename = filename
+
+    def get_primary_schema(self) -> DataSchema:
+        return self.primary_schema
+
+    def load_file(self) -> Optional[DataDF]:
         raise NotImplementedError
 
-    def save_file(self, df: pd.DataFrame) -> None:
+    def save_file(self, df: DataDF) -> None:
         raise NotImplementedError
 
-    def read_rows(self, idx: Optional[Index] = None) -> pd.DataFrame:
+    def read_rows(self, index_df: Optional[IndexDF] = None) -> DataDF:
         file_df = self.load_file()
 
         if file_df is not None:
-            if idx is not None:
-                return file_df.loc[idx]
+            if index_df is not None:
+                file_df = file_df.set_index(self.primary_keys)
+                idx = index_df.set_index(self.primary_keys)
+
+                return file_df.loc[idx.index].reset_index()
             else:
                 return file_df
-
         else:
             return pd.DataFrame()
 
-    def insert_rows(self, df: pd.DataFrame) -> None:
+    def insert_rows(self, df: DataDF) -> None:
         file_df = self.load_file()
 
+        if set(self.primary_keys) - set(df.columns):
+            raise ValueError("DataDf does not contains all primary keys")
+
         if file_df is None:
             new_df = df
         else:
             new_df = file_df.append(df)
 
+        check_df = new_df.drop_duplicates(subset=self.primary_keys)
+
+        if len(new_df) > len(check_df):
+            raise ValueError("DataDf contains duplicate rows by primary keys")
+
         self.save_file(new_df)
 
-    def delete_rows(self, idx: Index) -> None:
+    def delete_rows(self, index_df: IndexDF) -> None:
         file_df = self.load_file()
 
         if file_df is not None:
-            new_df = file_df.loc[file_df.index.difference(idx)]
+            file_df = file_df.set_index(self.primary_keys)
+            idx = index_df.set_index(self.primary_keys)
 
-            self.save_file(new_df)
+            new_df = file_df.loc[file_df.index.difference(idx.index)]
 
-    def update_rows(self, df: pd.DataFrame) -> None:
+            self.save_file(new_df.reset_index())
+
+    def update_rows(self, df: DataDF) -> None:
         file_df = self.load_file()
 
-        if file_df is None:
+        if set(self.primary_keys) - set(df.columns):
+            raise ValueError("DataDf does not contains all primary keys")
+
+        if file_df is None or file_df.empty:
             file_df = df
         else:
-            updated_idx = df.index.intersection(file_df.index)
-            new_idx = df.index.difference(file_df.index)
+            file_df = file_df.set_index(self.primary_keys)
+            df = df.set_index(self.primary_keys)
+
+            file_df.loc[df.index] = df
 
-            file_df.loc[updated_idx] = df.loc[updated_idx]
-            file_df = file_df.append(df.loc[new_idx])
+            file_df = file_df.reset_index()
 
         self.save_file(file_df)
diff --git a/datapipe/store/types.py b/datapipe/store/types.py
deleted file mode 100644
index 3c10a330..00000000
--- a/datapipe/store/types.py
+++ /dev/null
@@ -1,9 +0,0 @@
-from typing import Union, List
-
-import pandas as pd
-from sqlalchemy import Column
-
-Index = Union[List, pd.Index]
-ChunkMeta = Index
-
-DataSchema = List[Column]
diff --git a/datapipe/types.py b/datapipe/types.py
new file mode 100644
index 00000000..4b9226cf
--- /dev/null
+++ b/datapipe/types.py
@@ -0,0 +1,34 @@
+from typing import List, NewType, cast
+
+import pandas as pd
+from sqlalchemy import Column
+
+DataSchema = List[Column]
+
+# Dataframe with columns (<index_cols ...>)
+IndexDF = NewType('IndexDF', pd.DataFrame)
+
+# Dataframe with columns (<index_cols ...>, hash, create_ts, update_ts, process_ts, delete_ts)
+MetadataDF = NewType('MetadataDF', pd.DataFrame)
+
+# Dataframe with columns (<index_cols ...>, <data_cols ...>)
+# DataDF = NewType('DataDF', pd.DataFrame)
+DataDF = pd.DataFrame
+
+
+def data_to_index(data_df: DataDF, primary_keys: List[str]) -> IndexDF:
+    return cast(IndexDF, data_df[primary_keys])
+
+
+def meta_to_index(meta_df: MetadataDF, primary_keys: List[str]) -> IndexDF:
+    return cast(IndexDF, meta_df[primary_keys])
+
+
+def index_difference(idx1_df: IndexDF, idx2_df: IndexDF) -> IndexDF:
+    assert(list(idx1_df.columns) == list(idx2_df.columns))
+    cols = idx1_df.columns.to_list()
+
+    idx1_idx = idx1_df.set_index(cols).index
+    idx2_idx = idx2_df.set_index(cols).index
+
+    return cast(IndexDF, idx1_idx.difference(idx2_idx).to_frame(index=False))
diff --git a/examples/00_label_studio_projects/00_detection/project.py b/examples/00_label_studio_projects/00_detection/project.py
index 9d799d9a..c2335a1d 100644
--- a/examples/00_label_studio_projects/00_detection/project.py
+++ b/examples/00_label_studio_projects/00_detection/project.py
@@ -1,3 +1,4 @@
+from datapipe.store.database import DBConn
 import time
 
 from subprocess import Popen
@@ -6,7 +7,7 @@
 from urllib.parse import urljoin
 
 from datapipe.compute import build_compute, run_steps
-from datapipe.metastore import MetaStore
+from datapipe.datatable import DataStore
 from datapipe.store.filedir import JSONFile, TableStoreFiledir, PILFile
 from datapipe.dsl import BatchTransform, LabelStudioModeration, Catalog, ExternalTable, Table, Pipeline
 
@@ -81,7 +82,7 @@ def parse_annotation(annotations):
 
 (DATA_DIR / 'xx_datatables').mkdir(exist_ok=True)
 
-ms = MetaStore('sqlite:///' + str(DATA_DIR / 'xx_datatables/metadata.sqlite'))
+ds = DataStore(DBConn('sqlite:///' + str(DATA_DIR / 'xx_datatables/metadata.sqlite')))
 catalog = Catalog({
     '00_input_images': ExternalTable(
         store=TableStoreFiledir(DATA_DIR / '00_dataset' / '{id}.jpeg', PILFile('jpg')),
@@ -123,7 +124,7 @@ def parse_annotation(annotations):
     )
 ])
 
-steps = build_compute(ms, catalog, pipeline)
+steps = build_compute(ds, catalog, pipeline)
 
 
 if __name__ == '__main__':
@@ -144,7 +145,7 @@ def parse_annotation(annotations):
 
     try:
         while True:
-            run_steps(ms, steps)
+            run_steps(ds, steps)
             time.sleep(5)
     finally:
         label_studio_service.terminate()
diff --git a/examples/00_label_studio_projects/01_text_classification/project.py b/examples/00_label_studio_projects/01_text_classification/project.py
index 9074d8d2..4d62cbde 100644
--- a/examples/00_label_studio_projects/01_text_classification/project.py
+++ b/examples/00_label_studio_projects/01_text_classification/project.py
@@ -1,3 +1,4 @@
+from datapipe.store.database import DBConn
 import time
 from subprocess import Popen
 from pathlib import Path
@@ -5,7 +6,7 @@
 import pandas as pd
 
 from datapipe.compute import build_compute, run_steps
-from datapipe.metastore import MetaStore
+from datapipe.datatable import DataStore
 from datapipe.dsl import Catalog, Table, ExternalTable, Pipeline, BatchTransform, LabelStudioModeration
 from datapipe.store.pandas import TableStoreJsonLine
 
@@ -62,7 +63,7 @@ def get_category(val):
 
 
 (DATA_DIR / 'xx_datatables').mkdir(exist_ok=True, parents=True)
-ms = MetaStore('sqlite:///' + str(DATA_DIR / 'xx_datatables' / 'metadata.sqlite'))
+ds = DataStore(DBConn('sqlite:///' + str(DATA_DIR / 'xx_datatables' / 'metadata.sqlite')))
 catalogue = Catalog({
     "00_input_texts": ExternalTable(
         store=TableStoreJsonLine(DATA_DIR / '00_data.json'),
@@ -92,7 +93,7 @@ def get_category(val):
         outputs=["02_annotation_parsed"],
     ),
 ])
-steps = build_compute(ms, catalogue, pipeline)
+steps = build_compute(ds, catalogue, pipeline)
 
 
 if __name__ == "__main__":
@@ -105,7 +106,7 @@ def get_category(val):
     ])
     try:
         while True:
-            run_steps(ms, steps)
+            run_steps(ds, steps)
             time.sleep(5)
     finally:
         label_studio_service.terminate()
diff --git a/examples/goal_v0.4.0.py b/examples/goal_v0.4.0.py
index 168ddf06..098588ad 100644
--- a/examples/goal_v0.4.0.py
+++ b/examples/goal_v0.4.0.py
@@ -1,8 +1,9 @@
+from datapipe.store.database import DBConn
 from pathlib import Path
 
 import pandas as pd
 
-from datapipe.metastore import MetaStore
+from datapipe.datatable import DataStore
 from datapipe.store.filedir import TableStoreFiledir, PILFile
 from datapipe.dsl import Catalog, ExternalTable, Table, Pipeline, BatchTransform
 from datapipe.cli import main
@@ -39,8 +40,8 @@ def batch_preprocess_images(df: pd.DataFrame) -> pd.DataFrame:
 ])
 
 
-ms = MetaStore('sqlite:///./test_data/metadata.sqlite')
+ds = DataStore(DBConn('sqlite:///./test_data/metadata.sqlite'))
 
 
 if __name__ == '__main__':
-    main(ms, catalog, pipeline)
+    main(ds, catalog, pipeline)
diff --git a/examples/v0.9.0/.gitignore b/examples/v0.9.0/.gitignore
new file mode 100644
index 00000000..9b1dffd9
--- /dev/null
+++ b/examples/v0.9.0/.gitignore
@@ -0,0 +1,1 @@
+*.sqlite
diff --git a/examples/v0.9.0/one_to_many_pipeline.py b/examples/v0.9.0/one_to_many_pipeline.py
new file mode 100644
index 00000000..eeb217f3
--- /dev/null
+++ b/examples/v0.9.0/one_to_many_pipeline.py
@@ -0,0 +1,245 @@
+import pandas as pd
+
+from sqlalchemy.sql.schema import Column
+from sqlalchemy.sql.sqltypes import Integer, String, Boolean, JSON
+
+from datapipe.store.database import TableStoreDB
+from datapipe.datatable import DataStore
+from datapipe.dsl import Catalog, Pipeline, Table, BatchGenerate, BatchTransform
+from datapipe.store.database import DBConn
+from datapipe.cli import main
+
+
+dbconn = DBConn('sqlite:///db.sqlite')
+ds = DataStore(dbconn)
+
+
+def generate_products():
+    for pipeline_id in range(1, 3):
+        df_data = []
+
+        for offer_id in range(1, 6):
+            df_data.append({
+                "pipeline_id": pipeline_id,
+                "offer_id": offer_id,
+                "attributes": {f'attr_{id}': id for id in range(1, offer_id + 1)}
+            })
+
+        yield pd.DataFrame(data=df_data)
+
+
+def unpack_attr(df: pd.DataFrame) -> pd.DataFrame:
+    dfs = []
+
+    for _, row in df.iterrows():
+        data = [{
+            "pipeline_id": row["pipeline_id"],
+            "offer_id": row["offer_id"],
+            "name": key,
+            "value": value
+        } for key, value in row["attributes"].items()]
+
+        dfs.append(pd.DataFrame(data=data))
+
+    res_df = pd.concat(dfs, ignore_index=True)
+
+    if not len(res_df.index):
+        return pd.DataFrame(columns=["pipeline_id", "offer_id", "name"])
+
+    return res_df
+
+
+def pack_attr(df: pd.DataFrame) -> pd.DataFrame:
+    data = {}
+
+    for _, row in df.iterrows():
+        key = f'{row["pipeline_id"]}_{row["offer_id"]}'
+
+        if key not in data:
+            data[key] = {
+                "pipeline_id": row["pipeline_id"],
+                "offer_id": row["offer_id"],
+                "attributes": {}
+            }
+
+        data[key]["attributes"][row["value"]] = row["name"]
+
+    if not len(data.keys()):
+        return pd.DataFrame(columns=["pipeline_id", "offer_id"])
+
+    return pd.DataFrame(data=data.values())
+
+
+def pack_offers(df: pd.DataFrame) -> pd.DataFrame:
+    data = {}
+
+    for _, row in df.iterrows():
+        key = f'{row["pipeline_id"]}_{row["value"]}'
+
+        if key not in data:
+            data[key] = {
+                "pipeline_id": row["pipeline_id"],
+                "name": row["name"],
+                "offers": {}
+            }
+
+        data[key]["offers"][row["offer_id"]] = row["value"]
+
+    if not len(data.keys()):
+        return pd.DataFrame(columns=["pipeline_id", "name"])
+
+    return pd.DataFrame(data=data.values())
+
+
+def gen_product_all(df1: pd.DataFrame, df2: pd.DataFrame) -> pd.DataFrame:
+    keys = ["pipeline_id", "offer_id"]
+    merged_df = pd.merge(df1, df2,  how='outer', left_on=keys, right_on=keys, suffixes=("_base", "_new"))
+
+    return merged_df[['pipeline_id', "offer_id", "attributes_base", "attributes_new"]]
+
+
+def gen_product_store(df1: pd.DataFrame, df2: pd.DataFrame) -> pd.DataFrame:
+
+    keys = ["pipeline_id", "offer_id"]
+    merged_df = pd.merge(df1, df2,  how='outer', left_on=keys, right_on=keys)
+
+    merged_df['attributes'] = merged_df.apply(
+        lambda x: x["attributes_x"] if pd.notna(x["attributes_x"]) else x["attributes_y"], axis=1)
+    merged_df['is_deleted'] = merged_df.apply(
+        lambda x: pd.isna(x["attributes_x"]), axis=1)
+
+    return merged_df[['pipeline_id', 'offer_id', 'attributes', 'is_deleted']]
+
+
+def filter(df: pd.DataFrame) -> pd.DataFrame:
+    return df[df["pipeline_id"] == 1]
+
+
+catalog = Catalog({
+    'test_products': Table(
+        store=TableStoreDB(
+            dbconn,
+            'test_products_data',
+            [
+                Column('pipeline_id', Integer(), primary_key=True),
+                Column('offer_id', Integer(), primary_key=True),
+                Column('attributes', JSON)
+            ]
+        )
+    ),
+    'test_attr_products': Table(
+        store=TableStoreDB(
+            dbconn,
+            'test_attr_products_data',
+            [
+                Column('pipeline_id', Integer(), primary_key=True),
+                Column('offer_id', Integer(), primary_key=True),
+                Column('name', String(), primary_key=True),
+                Column('value', Integer()),
+            ]
+        )
+    ),
+    'test_ozon_products': Table(
+        store=TableStoreDB(
+            dbconn,
+            'test_ozon_products_data',
+            [
+                Column('pipeline_id', Integer(), primary_key=True),
+                Column('offer_id', Integer(), primary_key=True),
+                Column('attributes', JSON)
+            ]
+        )
+    ),
+    'test_offers_products': Table(
+        store=TableStoreDB(
+            dbconn,
+            'test_offers_products_data',
+            [
+                Column('pipeline_id', Integer(), primary_key=True),
+                Column('name', String(), primary_key=True),
+                Column('offers', JSON),
+            ]
+        )
+    ),
+    'test_all_products': Table(
+        store=TableStoreDB(
+            dbconn,
+            'test_all_products_data',
+            [
+                Column('pipeline_id', Integer(), primary_key=True),
+                Column('offer_id', Integer(), primary_key=True),
+                Column('attributes_base', JSON),
+                Column('attributes_new', JSON)
+            ]
+        )
+    ),
+    'test_store_products': Table(
+        store=TableStoreDB(
+            dbconn,
+            'test_store_products_data',
+            [
+                Column('pipeline_id', Integer(), primary_key=True),
+                Column('offer_id', Integer(), primary_key=True),
+                Column('attributes', JSON),
+                Column('is_deleted', Boolean())
+            ]
+        )
+    ),
+    'test_filter_products': Table(
+        store=TableStoreDB(
+            dbconn,
+            'test_filter_products_data',
+            [
+                Column('pipeline_id', Integer(), primary_key=True),
+                Column('offer_id', Integer(), primary_key=True),
+                Column('attributes', JSON)
+            ]
+        )
+    )
+})
+
+pipeline = Pipeline([
+    BatchGenerate(
+        generate_products,
+        outputs=["test_products"]
+    ),
+    BatchTransform(
+        unpack_attr,
+        inputs=["test_products"],
+        outputs=["test_attr_products"],
+        chunk_size=2
+    ),
+    BatchTransform(
+        pack_attr,
+        inputs=["test_attr_products"],
+        outputs=["test_ozon_products"],
+        chunk_size=2
+    ),
+    BatchTransform(
+        pack_offers,
+        inputs=["test_attr_products"],
+        outputs=["test_offers_products"],
+        chunk_size=2
+    ),
+    BatchTransform(
+        gen_product_all,
+        inputs=["test_products", "test_ozon_products"],
+        outputs=["test_all_products"],
+        chunk_size=2
+    ),
+    BatchTransform(
+        gen_product_store,
+        inputs=["test_products", "test_store_products"],
+        outputs=["test_store_products"],
+        chunk_size=2
+    ),
+    BatchTransform(
+        filter,
+        inputs=["test_products"],
+        outputs=["test_filter_products"],
+        chunk_size=2
+    ),
+])
+
+
+main(ds, catalog, pipeline)
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 00000000..6335b661
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,3084 @@
+[[package]]
+name = "aiohttp"
+version = "3.7.4.post0"
+description = "Async http client/server framework (asyncio)"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+async-timeout = ">=3.0,<4.0"
+attrs = ">=17.3.0"
+chardet = ">=2.0,<5.0"
+multidict = ">=4.5,<7.0"
+typing-extensions = ">=3.6.5"
+yarl = ">=1.0,<2.0"
+
+[package.extras]
+speedups = ["aiodns", "brotlipy", "cchardet"]
+
+[[package]]
+name = "appdirs"
+version = "1.4.4"
+description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+category = "main"
+optional = true
+python-versions = "*"
+
+[[package]]
+name = "asgiref"
+version = "3.4.1"
+description = "ASGI specs, helper code, and adapters"
+category = "main"
+optional = true
+python-versions = ">=3.6"
+
+[package.extras]
+tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"]
+
+[[package]]
+name = "async-timeout"
+version = "3.0.1"
+description = "Timeout context manager for asyncio programs"
+category = "main"
+optional = false
+python-versions = ">=3.5.3"
+
+[[package]]
+name = "atomicwrites"
+version = "1.4.0"
+description = "Atomic file writes."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "attr"
+version = "0.3.1"
+description = "Simple decorator to set attributes of target function or class in a DRY way."
+category = "main"
+optional = true
+python-versions = "*"
+
+[[package]]
+name = "attrs"
+version = "21.2.0"
+description = "Classes Without Boilerplate"
+category = "main"
+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"]
+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"]
+
+[[package]]
+name = "azure-core"
+version = "1.18.0"
+description = "Microsoft Azure Core Library for Python"
+category = "main"
+optional = true
+python-versions = "*"
+
+[package.dependencies]
+requests = ">=2.18.4"
+six = ">=1.11.0"
+
+[[package]]
+name = "azure-storage-blob"
+version = "12.9.0"
+description = "Microsoft Azure Blob Storage Client Library for Python"
+category = "main"
+optional = true
+python-versions = "*"
+
+[package.dependencies]
+azure-core = ">=1.10.0,<2.0.0"
+cryptography = ">=2.1.4"
+msrest = ">=0.6.21"
+
+[[package]]
+name = "black"
+version = "21.9b0"
+description = "The uncompromising code formatter."
+category = "dev"
+optional = false
+python-versions = ">=3.6.2"
+
+[package.dependencies]
+click = ">=7.1.2"
+mypy-extensions = ">=0.4.3"
+pathspec = ">=0.9.0,<1"
+platformdirs = ">=2"
+regex = ">=2020.1.8"
+tomli = ">=0.2.6,<2.0.0"
+typing-extensions = [
+    {version = ">=3.10.0.0", markers = "python_version < \"3.10\""},
+    {version = "!=3.10.0.1", markers = "python_version >= \"3.10\""},
+]
+
+[package.extras]
+colorama = ["colorama (>=0.4.3)"]
+d = ["aiohttp (>=3.6.0)", "aiohttp-cors (>=0.4.0)"]
+jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
+python2 = ["typed-ast (>=1.4.2)"]
+uvloop = ["uvloop (>=0.15.2)"]
+
+[[package]]
+name = "boto"
+version = "2.49.0"
+description = "Amazon Web Services Library"
+category = "main"
+optional = true
+python-versions = "*"
+
+[[package]]
+name = "boto3"
+version = "1.16.28"
+description = "The AWS SDK for Python"
+category = "main"
+optional = true
+python-versions = "*"
+
+[package.dependencies]
+botocore = ">=1.19.28,<1.20.0"
+jmespath = ">=0.7.1,<1.0.0"
+s3transfer = ">=0.3.0,<0.4.0"
+
+[[package]]
+name = "botocore"
+version = "1.19.28"
+description = "Low-level, data-driven core of boto 3."
+category = "main"
+optional = true
+python-versions = "*"
+
+[package.dependencies]
+jmespath = ">=0.7.1,<1.0.0"
+python-dateutil = ">=2.1,<3.0.0"
+urllib3 = {version = ">=1.25.4,<1.27", markers = "python_version != \"3.4\""}
+
+[[package]]
+name = "boxing"
+version = "0.1.4"
+description = "Draw boxes like never before!"
+category = "main"
+optional = true
+python-versions = "*"
+
+[[package]]
+name = "brotli"
+version = "1.0.9"
+description = "Python bindings for the Brotli compression library"
+category = "main"
+optional = true
+python-versions = "*"
+
+[[package]]
+name = "cachetools"
+version = "4.2.2"
+description = "Extensible memoizing collections and decorators"
+category = "main"
+optional = false
+python-versions = "~=3.5"
+
+[[package]]
+name = "certifi"
+version = "2021.5.30"
+description = "Python package for providing Mozilla's CA Bundle."
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "cffi"
+version = "1.14.6"
+description = "Foreign Function Interface for Python calling C code."
+category = "main"
+optional = true
+python-versions = "*"
+
+[package.dependencies]
+pycparser = "*"
+
+[[package]]
+name = "chardet"
+version = "4.0.0"
+description = "Universal encoding detector for Python 2 and 3"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[[package]]
+name = "charset-normalizer"
+version = "2.0.6"
+description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
+category = "main"
+optional = false
+python-versions = ">=3.5.0"
+
+[package.extras]
+unicode_backport = ["unicodedata2"]
+
+[[package]]
+name = "click"
+version = "8.0.1"
+description = "Composable command line interface toolkit"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+
+[[package]]
+name = "cloudpickle"
+version = "2.0.0"
+description = "Extended pickling support for Python objects"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "colorama"
+version = "0.4.4"
+description = "Cross-platform colored terminal text."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[[package]]
+name = "coreapi"
+version = "2.3.3"
+description = "Python client library for Core API."
+category = "main"
+optional = true
+python-versions = "*"
+
+[package.dependencies]
+coreschema = "*"
+itypes = "*"
+requests = "*"
+uritemplate = "*"
+
+[[package]]
+name = "coreschema"
+version = "0.0.4"
+description = "Core Schema."
+category = "main"
+optional = true
+python-versions = "*"
+
+[package.dependencies]
+jinja2 = "*"
+
+[[package]]
+name = "cryptography"
+version = "3.4.8"
+description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
+category = "main"
+optional = true
+python-versions = ">=3.6"
+
+[package.dependencies]
+cffi = ">=1.12"
+
+[package.extras]
+docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"]
+docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"]
+pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"]
+sdist = ["setuptools-rust (>=0.11.4)"]
+ssh = ["bcrypt (>=3.1.5)"]
+test = ["pytest (>=6.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"]
+
+[[package]]
+name = "dash"
+version = "1.20.0"
+description = "A Python framework for building reactive web-apps. Developed by Plotly."
+category = "main"
+optional = true
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[package.dependencies]
+dash-core-components = "1.16.0"
+dash-html-components = "1.1.3"
+dash_renderer = "1.9.1"
+dash-table = "4.11.3"
+Flask = ">=1.0.4"
+flask-compress = "*"
+future = "*"
+plotly = "*"
+
+[package.extras]
+dev = ["dash_flow_example (==0.0.5)", "dash-dangerously-set-inner-html", "flake8 (==3.8.4)", "PyYAML (==5.3.1)", "fire (==0.3.1)", "coloredlogs (==15.0)", "flask-talisman (==0.7.0)", "isort (==4.3.21)", "pylint (==1.9.5)", "mock (==3.0.5)", "virtualenv (==20.2.2)", "mock (==4.0.3)", "black (==19.10b0)", "pylint (==2.6.0)", "astroid (==2.4.2)"]
+testing = ["pytest-sugar (>=0.9.4)", "lxml (>=4.6.2)", "selenium (>=3.141.0)", "percy (>=2.0.2)", "requests[security] (>=2.21.0)", "waitress (>=1.4.4)", "cryptography (<3.4)", "pytest (>=4.6,<5)", "pytest-mock (>=2.0.0,<3)", "beautifulsoup4 (>=4.8.2,<=4.9.3)", "pytest (>=6.0.2)", "pytest-mock (>=3.2.0)", "beautifulsoup4 (>=4.8.2)"]
+
+[[package]]
+name = "dash-bootstrap-components"
+version = "0.12.2"
+description = "Bootstrap themed components for use in Plotly Dash"
+category = "main"
+optional = true
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
+
+[package.dependencies]
+dash = ">=1.9.0"
+
+[package.extras]
+pandas = ["numpy", "pandas"]
+
+[[package]]
+name = "dash-core-components"
+version = "1.16.0"
+description = "Core component suite for Dash"
+category = "main"
+optional = true
+python-versions = "*"
+
+[[package]]
+name = "dash-html-components"
+version = "1.1.3"
+description = "Vanilla HTML components for Dash"
+category = "main"
+optional = true
+python-versions = "*"
+
+[[package]]
+name = "dash-interactive-graphviz"
+version = "0.3.0"
+description = "An interactive graphviz viewer for Dash"
+category = "main"
+optional = true
+python-versions = "*"
+
+[[package]]
+name = "dash-renderer"
+version = "1.9.1"
+description = "Front-end component renderer for Dash"
+category = "main"
+optional = true
+python-versions = "*"
+
+[[package]]
+name = "dash-table"
+version = "4.11.3"
+description = "Dash table"
+category = "main"
+optional = true
+python-versions = "*"
+
+[[package]]
+name = "decopatch"
+version = "1.4.8"
+description = "Create decorators easily in python."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+makefun = ">=1.5.0"
+
+[[package]]
+name = "decorator"
+version = "5.1.0"
+description = "Decorators for Humans"
+category = "main"
+optional = false
+python-versions = ">=3.5"
+
+[[package]]
+name = "django"
+version = "3.1.4"
+description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design."
+category = "main"
+optional = true
+python-versions = ">=3.6"
+
+[package.dependencies]
+asgiref = ">=3.2.10,<4"
+pytz = "*"
+sqlparse = ">=0.2.2"
+
+[package.extras]
+argon2 = ["argon2-cffi (>=16.1.0)"]
+bcrypt = ["bcrypt"]
+
+[[package]]
+name = "django-annoying"
+version = "0.10.6"
+description = "This is a django application that tries to eliminate annoying things in the Django framework."
+category = "main"
+optional = true
+python-versions = "*"
+
+[package.dependencies]
+Django = ">=1.11"
+six = "*"
+
+[[package]]
+name = "django-cors-headers"
+version = "3.6.0"
+description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)."
+category = "main"
+optional = true
+python-versions = ">=3.6"
+
+[package.dependencies]
+Django = ">=2.2"
+
+[[package]]
+name = "django-debug-toolbar"
+version = "3.2"
+description = "A configurable set of panels that display various debug information about the current request/response."
+category = "main"
+optional = true
+python-versions = ">=3.6"
+
+[package.dependencies]
+Django = ">=2.2"
+sqlparse = ">=0.2.0"
+
+[[package]]
+name = "django-extensions"
+version = "3.1.0"
+description = "Extensions for Django"
+category = "main"
+optional = true
+python-versions = ">=3.5"
+
+[[package]]
+name = "django-filter"
+version = "2.4.0"
+description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically."
+category = "main"
+optional = true
+python-versions = ">=3.5"
+
+[package.dependencies]
+Django = ">=2.2"
+
+[[package]]
+name = "django-model-utils"
+version = "4.1.1"
+description = "Django model mixins and utilities"
+category = "main"
+optional = true
+python-versions = "*"
+
+[package.dependencies]
+Django = ">=2.0.1"
+
+[[package]]
+name = "django-redis-cache"
+version = "3.0.0"
+description = "Redis Cache Backend for Django"
+category = "main"
+optional = true
+python-versions = "*"
+
+[package.dependencies]
+redis = "<4.0"
+
+[[package]]
+name = "django-rest-swagger"
+version = "2.2.0"
+description = "Swagger UI for Django REST Framework 3.5+"
+category = "main"
+optional = true
+python-versions = "*"
+
+[package.dependencies]
+coreapi = ">=2.3.0"
+djangorestframework = ">=3.5.4"
+openapi-codec = ">=1.3.1"
+simplejson = "*"
+
+[[package]]
+name = "django-rq"
+version = "2.3.2"
+description = "An app that provides django integration for RQ (Redis Queue)"
+category = "main"
+optional = true
+python-versions = "*"
+
+[package.dependencies]
+django = ">=2.0"
+redis = ">=3"
+rq = ">=1.2"
+
+[package.extras]
+sentry = ["raven (>=6.1.0)"]
+testing = ["mock (>=2.0.0)"]
+
+[[package]]
+name = "django-user-agents"
+version = "0.4.0"
+description = "A django package that allows easy identification of visitors' browser, operating system and device information (mobile phone, tablet or has touch capabilities)."
+category = "main"
+optional = true
+python-versions = "*"
+
+[package.dependencies]
+django = "*"
+user-agents = "*"
+
+[[package]]
+name = "djangorestframework"
+version = "3.12.4"
+description = "Web APIs for Django, made easy."
+category = "main"
+optional = true
+python-versions = ">=3.5"
+
+[package.dependencies]
+django = ">=2.2"
+
+[[package]]
+name = "drf-dynamic-fields"
+version = "0.3.0"
+description = "Dynamically return subset of Django REST Framework serializer fields"
+category = "main"
+optional = true
+python-versions = "*"
+
+[[package]]
+name = "drf-generators"
+version = "0.3.0"
+description = "Generate DRF Serializers, Views, and urls for your API aplication."
+category = "main"
+optional = true
+python-versions = "*"
+
+[package.dependencies]
+Django = ">=1.7"
+
+[[package]]
+name = "drf-yasg"
+version = "1.20.0"
+description = "Automated generation of real Swagger/OpenAPI 2.0 schemas from Django Rest Framework code."
+category = "main"
+optional = true
+python-versions = ">=3.6"
+
+[package.dependencies]
+coreapi = ">=2.3.3"
+coreschema = ">=0.0.4"
+Django = ">=2.2.16"
+djangorestframework = ">=3.10.3"
+inflection = ">=0.3.1"
+packaging = "*"
+"ruamel.yaml" = ">=0.15.34"
+uritemplate = ">=3.0.0"
+
+[package.extras]
+validation = ["swagger-spec-validator (>=2.1.0)"]
+
+[[package]]
+name = "et-xmlfile"
+version = "1.1.0"
+description = "An implementation of lxml.xmlfile for the standard library"
+category = "main"
+optional = true
+python-versions = ">=3.6"
+
+[[package]]
+name = "flake8"
+version = "3.9.2"
+description = "the modular source code checker: pep8 pyflakes and co"
+category = "dev"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+
+[package.dependencies]
+mccabe = ">=0.6.0,<0.7.0"
+pycodestyle = ">=2.7.0,<2.8.0"
+pyflakes = ">=2.3.0,<2.4.0"
+
+[[package]]
+name = "flask"
+version = "1.1.2"
+description = "A simple framework for building complex web applications."
+category = "main"
+optional = true
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[package.dependencies]
+click = ">=5.1"
+itsdangerous = ">=0.24"
+Jinja2 = ">=2.10.1"
+Werkzeug = ">=0.15"
+
+[package.extras]
+dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"]
+docs = ["sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"]
+dotenv = ["python-dotenv"]
+
+[[package]]
+name = "flask-compress"
+version = "1.10.1"
+description = "Compress responses in your Flask app with gzip, deflate or brotli."
+category = "main"
+optional = true
+python-versions = "*"
+
+[package.dependencies]
+brotli = "*"
+flask = "*"
+
+[[package]]
+name = "fsspec"
+version = "2021.5.0"
+description = "File-system specification"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+abfs = ["adlfs"]
+adl = ["adlfs"]
+dask = ["dask", "distributed"]
+dropbox = ["dropboxdrivefs", "requests", "dropbox"]
+entrypoints = ["importlib-metadata"]
+gcs = ["gcsfs"]
+git = ["pygit2"]
+github = ["requests"]
+gs = ["gcsfs"]
+hdfs = ["pyarrow (>=1)"]
+http = ["requests", "aiohttp"]
+s3 = ["s3fs"]
+sftp = ["paramiko"]
+smb = ["smbprotocol"]
+ssh = ["paramiko"]
+
+[[package]]
+name = "future"
+version = "0.18.2"
+description = "Clean single-source support for Python 3 and 2"
+category = "main"
+optional = true
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "gcsfs"
+version = "2021.5.0"
+description = "Convenient Filesystem interface over GCS"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+aiohttp = "*"
+decorator = "*"
+fsspec = "2021.05.0"
+google-auth = ">=1.2"
+google-auth-oauthlib = "*"
+requests = "*"
+
+[package.extras]
+crc = ["crcmod"]
+gcsfuse = ["fusepy"]
+
+[[package]]
+name = "google-api-core"
+version = "1.31.2"
+description = "Google API client core library"
+category = "main"
+optional = true
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*"
+
+[package.dependencies]
+google-auth = ">=1.25.0,<2.0dev"
+googleapis-common-protos = ">=1.6.0,<2.0dev"
+packaging = ">=14.3"
+protobuf = ">=3.12.0"
+pytz = "*"
+requests = ">=2.18.0,<3.0.0dev"
+six = ">=1.13.0"
+
+[package.extras]
+grpc = ["grpcio (>=1.29.0,<2.0dev)"]
+grpcgcp = ["grpcio-gcp (>=0.2.2)"]
+grpcio-gcp = ["grpcio-gcp (>=0.2.2)"]
+
+[[package]]
+name = "google-auth"
+version = "1.35.0"
+description = "Google Authentication Library"
+category = "main"
+optional = false
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*"
+
+[package.dependencies]
+cachetools = ">=2.0.0,<5.0"
+pyasn1-modules = ">=0.2.1"
+rsa = {version = ">=3.1.4,<5", markers = "python_version >= \"3.6\""}
+six = ">=1.9.0"
+
+[package.extras]
+aiohttp = ["requests (>=2.20.0,<3.0.0dev)", "aiohttp (>=3.6.2,<4.0.0dev)"]
+pyopenssl = ["pyopenssl (>=20.0.0)"]
+reauth = ["pyu2f (>=0.1.5)"]
+
+[[package]]
+name = "google-auth-oauthlib"
+version = "0.4.6"
+description = "Google Authentication Library"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+google-auth = ">=1.0.0"
+requests-oauthlib = ">=0.7.0"
+
+[package.extras]
+tool = ["click (>=6.0.0)"]
+
+[[package]]
+name = "google-cloud-core"
+version = "1.7.2"
+description = "Google Cloud API client core library"
+category = "main"
+optional = true
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*"
+
+[package.dependencies]
+google-api-core = ">=1.21.0,<2.0.0dev"
+google-auth = ">=1.24.0,<2.0dev"
+six = ">=1.12.0"
+
+[package.extras]
+grpc = ["grpcio (>=1.8.2,<2.0dev)"]
+
+[[package]]
+name = "google-cloud-storage"
+version = "1.28.1"
+description = "Google Cloud Storage API client library"
+category = "main"
+optional = true
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
+
+[package.dependencies]
+google-auth = ">=1.11.0,<2.0dev"
+google-cloud-core = ">=1.2.0,<2.0dev"
+google-resumable-media = ">=0.5.0,<0.6dev"
+
+[[package]]
+name = "google-resumable-media"
+version = "0.5.1"
+description = "Utilities for Google Media Downloads and Resumable Uploads"
+category = "main"
+optional = true
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*"
+
+[package.dependencies]
+six = "*"
+
+[package.extras]
+requests = ["requests (>=2.18.0,<3.0.0dev)"]
+
+[[package]]
+name = "googleapis-common-protos"
+version = "1.53.0"
+description = "Common protobufs used in Google APIs"
+category = "main"
+optional = true
+python-versions = ">=3.6"
+
+[package.dependencies]
+protobuf = ">=3.12.0"
+
+[package.extras]
+grpc = ["grpcio (>=1.0.0)"]
+
+[[package]]
+name = "htmlmin"
+version = "0.1.12"
+description = "An HTML Minifier"
+category = "main"
+optional = true
+python-versions = "*"
+
+[[package]]
+name = "idna"
+version = "3.2"
+description = "Internationalized Domain Names in Applications (IDNA)"
+category = "main"
+optional = false
+python-versions = ">=3.5"
+
+[[package]]
+name = "inflection"
+version = "0.5.1"
+description = "A port of Ruby on Rails inflector to Python"
+category = "main"
+optional = true
+python-versions = ">=3.5"
+
+[[package]]
+name = "iniconfig"
+version = "1.1.1"
+description = "iniconfig: brain-dead simple config-ini parsing"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "isodate"
+version = "0.6.0"
+description = "An ISO 8601 date/time/duration parser and formatter"
+category = "main"
+optional = true
+python-versions = "*"
+
+[package.dependencies]
+six = "*"
+
+[[package]]
+name = "itsdangerous"
+version = "2.0.1"
+description = "Safely pass data to untrusted environments and back."
+category = "main"
+optional = true
+python-versions = ">=3.6"
+
+[[package]]
+name = "itypes"
+version = "1.2.0"
+description = "Simple immutable types for python."
+category = "main"
+optional = true
+python-versions = "*"
+
+[[package]]
+name = "jinja2"
+version = "2.11.3"
+description = "A very fast and expressive template engine."
+category = "main"
+optional = true
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[package.dependencies]
+MarkupSafe = ">=0.23"
+
+[package.extras]
+i18n = ["Babel (>=0.8)"]
+
+[[package]]
+name = "jmespath"
+version = "0.10.0"
+description = "JSON Matching Expressions"
+category = "main"
+optional = true
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "joblib"
+version = "1.0.1"
+description = "Lightweight pipelining with Python functions"
+category = "main"
+optional = true
+python-versions = ">=3.6"
+
+[[package]]
+name = "jsonschema"
+version = "3.2.0"
+description = "An implementation of JSON Schema validation for Python"
+category = "main"
+optional = true
+python-versions = "*"
+
+[package.dependencies]
+attrs = ">=17.4.0"
+pyrsistent = ">=0.14.0"
+six = ">=1.11.0"
+
+[package.extras]
+format = ["idna", "jsonpointer (>1.13)", "rfc3987", "strict-rfc3339", "webcolors"]
+format_nongpl = ["idna", "jsonpointer (>1.13)", "webcolors", "rfc3986-validator (>0.1.0)", "rfc3339-validator"]
+
+[[package]]
+name = "label-studio"
+version = "1.0.0.post3"
+description = "Label Studio annotation tool"
+category = "main"
+optional = true
+python-versions = ">=3.6"
+
+[package.dependencies]
+appdirs = ">=1.4.3"
+attr = "0.3.1"
+attrs = ">=19.1.0"
+azure-storage-blob = ">=12.6.0"
+boto = "2.49.0"
+boto3 = "1.16.28"
+botocore = "1.19.28"
+boxing = ">=0.1.4"
+colorama = ">=0.4.4"
+Django = "3.1.4"
+django-annoying = "0.10.6"
+django-cors-headers = "3.6.0"
+django-debug-toolbar = "3.2"
+django-extensions = "3.1.0"
+django-filter = "2.4.0"
+django-model-utils = "4.1.1"
+django-redis-cache = "3.0.0"
+django-rest-swagger = "2.2.0"
+django-rq = "2.3.2"
+django-user-agents = "0.4.0"
+drf-dynamic-fields = "0.3.0"
+drf-generators = "0.3.0"
+drf-yasg = "1.20.0"
+google-cloud-storage = "1.28.1"
+htmlmin = "0.1.12"
+jsonschema = "3.2.0"
+label-studio-converter = "0.0.26"
+lockfile = "0.12.2"
+lxml = ">=4.2.5"
+moto = "1.3.16.dev122"
+numpy = ">=1.19.1"
+ordered-set = "4.0.2"
+pandas = ">=0.24.0"
+protobuf = ">=3.15.5"
+psycopg2-binary = "2.8.4"
+pydantic = "1.7.3"
+python-dateutil = "2.8.1"
+pytz = "2019.3"
+pyyaml = ">=5.3.1"
+redis = ">=3.5.0"
+requests = ">=2.22.0,<3"
+rq = "1.7.0"
+rules = "2.2"
+ujson = ">=3.0.0"
+xmljson = "0.2.0"
+
+[[package]]
+name = "label-studio-converter"
+version = "0.0.26"
+description = "Format converter add-on for Label Studio"
+category = "main"
+optional = true
+python-versions = ">=3.6"
+
+[package.dependencies]
+nltk = "3.5"
+pandas = ">=0.24.0"
+Pillow = ">=7,<8"
+requests = ">=2.22.0,<3"
+
+[[package]]
+name = "lockfile"
+version = "0.12.2"
+description = "Platform-independent file locking module"
+category = "main"
+optional = true
+python-versions = "*"
+
+[[package]]
+name = "lxml"
+version = "4.6.3"
+description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
+category = "main"
+optional = true
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*"
+
+[package.extras]
+cssselect = ["cssselect (>=0.7)"]
+html5 = ["html5lib"]
+htmlsoup = ["beautifulsoup4"]
+source = ["Cython (>=0.29.7)"]
+
+[[package]]
+name = "makefun"
+version = "1.11.3"
+description = "Small library to dynamically create python functions."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "markupsafe"
+version = "1.1.1"
+description = "Safely add untrusted strings to HTML/XML markup."
+category = "main"
+optional = true
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
+
+[[package]]
+name = "mccabe"
+version = "0.6.1"
+description = "McCabe checker, plugin for flake8"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "mock"
+version = "4.0.3"
+description = "Rolling backport of unittest.mock for all Pythons"
+category = "main"
+optional = true
+python-versions = ">=3.6"
+
+[package.extras]
+build = ["twine", "wheel", "blurb"]
+docs = ["sphinx"]
+test = ["pytest (<5.4)", "pytest-cov"]
+
+[[package]]
+name = "more-itertools"
+version = "8.10.0"
+description = "More routines for operating on iterables, beyond itertools"
+category = "main"
+optional = true
+python-versions = ">=3.5"
+
+[[package]]
+name = "moto"
+version = "1.3.16.dev122"
+description = "A library that allows your python tests to easily mock out the boto library"
+category = "main"
+optional = true
+python-versions = "*"
+
+[package.dependencies]
+boto = ">=2.36.0"
+boto3 = ">=1.9.201"
+botocore = ">=1.12.201"
+cryptography = ">=2.3.0"
+Jinja2 = ">=2.10.1"
+MarkupSafe = "<2.0"
+mock = "*"
+more-itertools = "*"
+python-dateutil = ">=2.1,<3.0.0"
+pytz = "*"
+requests = ">=2.5"
+responses = ">=0.9.0"
+six = ">1.9"
+werkzeug = "*"
+xmltodict = "*"
+zipp = "*"
+
+[package.extras]
+all = ["sshpubkeys (>=3.1.0,<4.0)", "sshpubkeys (>=3.1.0)", "PyYAML (>=5.1)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "ecdsa (<0.15)", "docker (>=2.5.1)", "jsondiff (>=1.1.2)", "aws-xray-sdk (>=0.93,!=0.96)", "idna (>=2.5,<3)", "cfn-lint (>=0.4.0)"]
+apigateway = ["python-jose[cryptography] (>=3.1.0,<4.0.0)", "ecdsa (<0.15)"]
+awslambda = ["docker (>=2.5.1)"]
+batch = ["docker (>=2.5.1)"]
+cloudformation = ["docker (>=2.5.1)", "PyYAML (>=5.1)", "cfn-lint (>=0.4.0)"]
+cognitoidp = ["python-jose[cryptography] (>=3.1.0,<4.0.0)", "ecdsa (<0.15)"]
+dynamodb2 = ["docker (>=2.5.1)"]
+dynamodbstreams = ["docker (>=2.5.1)"]
+ec2 = ["docker (>=2.5.1)", "sshpubkeys (>=3.1.0,<4.0)", "sshpubkeys (>=3.1.0)"]
+iotdata = ["jsondiff (>=1.1.2)"]
+s3 = ["PyYAML (>=5.1)"]
+server = ["PyYAML (>=5.1)", "python-jose[cryptography] (>=3.1.0,<4.0.0)", "ecdsa (<0.15)", "docker (>=2.5.1)", "jsondiff (>=1.1.2)", "aws-xray-sdk (>=0.93,!=0.96)", "idna (>=2.5,<3)", "cfn-lint (>=0.4.0)", "flask", "flask-cors", "sshpubkeys (>=3.1.0,<4.0)", "sshpubkeys (>=3.1.0)"]
+ses = ["docker (>=2.5.1)"]
+sns = ["docker (>=2.5.1)"]
+sqs = ["docker (>=2.5.1)"]
+ssm = ["docker (>=2.5.1)", "PyYAML (>=5.1)", "cfn-lint (>=0.4.0)"]
+xray = ["aws-xray-sdk (>=0.93,!=0.96)"]
+
+[[package]]
+name = "msrest"
+version = "0.6.21"
+description = "AutoRest swagger generator Python client runtime."
+category = "main"
+optional = true
+python-versions = "*"
+
+[package.dependencies]
+certifi = ">=2017.4.17"
+isodate = ">=0.6.0"
+requests = ">=2.16,<3.0"
+requests-oauthlib = ">=0.5.0"
+
+[package.extras]
+async = ["aiohttp (>=3.0)", "aiodns"]
+
+[[package]]
+name = "multidict"
+version = "5.1.0"
+description = "multidict implementation"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "mypy"
+version = "0.910"
+description = "Optional static typing for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
+[package.dependencies]
+mypy-extensions = ">=0.4.3,<0.5.0"
+toml = "*"
+typing-extensions = ">=3.7.4"
+
+[package.extras]
+dmypy = ["psutil (>=4.0)"]
+python2 = ["typed-ast (>=1.4.0,<1.5.0)"]
+
+[[package]]
+name = "mypy-extensions"
+version = "0.4.3"
+description = "Experimental type system extensions for programs checked with the mypy typechecker."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "nltk"
+version = "3.5"
+description = "Natural Language Toolkit"
+category = "main"
+optional = true
+python-versions = "*"
+
+[package.dependencies]
+click = "*"
+joblib = "*"
+regex = "*"
+tqdm = "*"
+
+[package.extras]
+all = ["requests", "numpy", "python-crfsuite", "scikit-learn", "twython", "pyparsing", "scipy", "matplotlib", "gensim"]
+corenlp = ["requests"]
+machine_learning = ["gensim", "numpy", "python-crfsuite", "scikit-learn", "scipy"]
+plot = ["matplotlib"]
+tgrep = ["pyparsing"]
+twitter = ["twython"]
+
+[[package]]
+name = "numpy"
+version = "1.21.1"
+description = "NumPy is the fundamental package for array computing with Python."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "oauthlib"
+version = "3.1.1"
+description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+rsa = ["cryptography (>=3.0.0,<4)"]
+signals = ["blinker (>=1.4.0)"]
+signedtoken = ["cryptography (>=3.0.0,<4)", "pyjwt (>=2.0.0,<3)"]
+
+[[package]]
+name = "openapi-codec"
+version = "1.3.2"
+description = "An OpenAPI codec for Core API."
+category = "main"
+optional = true
+python-versions = "*"
+
+[package.dependencies]
+coreapi = ">=2.2.0"
+
+[[package]]
+name = "openpyxl"
+version = "3.0.9"
+description = "A Python library to read/write Excel 2010 xlsx/xlsm files"
+category = "main"
+optional = true
+python-versions = ">=3.6"
+
+[package.dependencies]
+et-xmlfile = "*"
+
+[[package]]
+name = "ordered-set"
+version = "4.0.2"
+description = "A set that remembers its order, and allows looking up its items by their index in that order."
+category = "main"
+optional = true
+python-versions = ">=3.5"
+
+[[package]]
+name = "packaging"
+version = "21.0"
+description = "Core utilities for Python packages"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+pyparsing = ">=2.0.2"
+
+[[package]]
+name = "pandas"
+version = "1.3.3"
+description = "Powerful data structures for data analysis, time series, and statistics"
+category = "main"
+optional = false
+python-versions = ">=3.7.1"
+
+[package.dependencies]
+numpy = ">=1.17.3"
+python-dateutil = ">=2.7.3"
+pytz = ">=2017.3"
+
+[package.extras]
+test = ["hypothesis (>=3.58)", "pytest (>=6.0)", "pytest-xdist"]
+
+[[package]]
+name = "pandas-stubs"
+version = "1.2.0.17"
+description = "Type annotations for Pandas"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "pathspec"
+version = "0.9.0"
+description = "Utility library for gitignore style pattern matching of file paths."
+category = "dev"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+
+[[package]]
+name = "pillow"
+version = "7.2.0"
+description = "Python Imaging Library (Fork)"
+category = "main"
+optional = false
+python-versions = ">=3.5"
+
+[[package]]
+name = "platformdirs"
+version = "2.3.0"
+description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"]
+test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"]
+
+[[package]]
+name = "plotly"
+version = "5.3.1"
+description = "An open-source, interactive data visualization library for Python"
+category = "main"
+optional = true
+python-versions = ">=3.6"
+
+[package.dependencies]
+six = "*"
+tenacity = ">=6.2.0"
+
+[[package]]
+name = "pluggy"
+version = "1.0.0"
+description = "plugin and hook calling mechanisms for python"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+dev = ["pre-commit", "tox"]
+testing = ["pytest", "pytest-benchmark"]
+
+[[package]]
+name = "protobuf"
+version = "3.18.0"
+description = "Protocol Buffers"
+category = "main"
+optional = true
+python-versions = "*"
+
+[[package]]
+name = "psycopg2-binary"
+version = "2.8.4"
+description = "psycopg2 - Python-PostgreSQL Database Adapter"
+category = "main"
+optional = false
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
+
+[[package]]
+name = "py"
+version = "1.10.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.*"
+
+[[package]]
+name = "pyasn1"
+version = "0.4.8"
+description = "ASN.1 types and codecs"
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "pyasn1-modules"
+version = "0.2.8"
+description = "A collection of ASN.1-based protocols modules."
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+pyasn1 = ">=0.4.6,<0.5.0"
+
+[[package]]
+name = "pycodestyle"
+version = "2.7.0"
+description = "Python style guide checker"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "pycparser"
+version = "2.20"
+description = "C parser in Python"
+category = "main"
+optional = true
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "pydantic"
+version = "1.7.3"
+description = "Data validation and settings management using python 3.6 type hinting"
+category = "main"
+optional = true
+python-versions = ">=3.6"
+
+[package.extras]
+dotenv = ["python-dotenv (>=0.10.4)"]
+email = ["email-validator (>=1.0.3)"]
+typing_extensions = ["typing-extensions (>=3.7.2)"]
+
+[[package]]
+name = "pyflakes"
+version = "2.3.1"
+description = "passive checker of Python programs"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "pyparsing"
+version = "2.4.7"
+description = "Python parsing module"
+category = "main"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "pyrsistent"
+version = "0.18.0"
+description = "Persistent/Functional/Immutable data structures"
+category = "main"
+optional = true
+python-versions = ">=3.6"
+
+[[package]]
+name = "pytest"
+version = "6.2.5"
+description = "pytest: simple powerful testing with Python"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
+attrs = ">=19.2.0"
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+iniconfig = "*"
+packaging = "*"
+pluggy = ">=0.12,<2.0"
+py = ">=1.8.2"
+toml = "*"
+
+[package.extras]
+testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
+
+[[package]]
+name = "pytest-cases"
+version = "3.6.4"
+description = "Separate test code from test cases in pytest."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+decopatch = "*"
+makefun = ">=1.9.5"
+
+[[package]]
+name = "python-dateutil"
+version = "2.8.1"
+description = "Extensions to the standard Python datetime module"
+category = "main"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+
+[package.dependencies]
+six = ">=1.5"
+
+[[package]]
+name = "pytz"
+version = "2019.3"
+description = "World timezone definitions, modern and historical"
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "pyyaml"
+version = "5.4.1"
+description = "YAML parser and emitter for Python"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
+
+[[package]]
+name = "redis"
+version = "3.5.3"
+description = "Python client for Redis key-value store"
+category = "main"
+optional = true
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[package.extras]
+hiredis = ["hiredis (>=0.1.3)"]
+
+[[package]]
+name = "regex"
+version = "2021.9.24"
+description = "Alternative regular expression module, to replace re."
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "requests"
+version = "2.26.0"
+description = "Python HTTP for Humans."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
+
+[package.dependencies]
+certifi = ">=2017.4.17"
+charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""}
+idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""}
+urllib3 = ">=1.21.1,<1.27"
+
+[package.extras]
+socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
+use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
+
+[[package]]
+name = "requests-oauthlib"
+version = "1.3.0"
+description = "OAuthlib authentication support for Requests."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[package.dependencies]
+oauthlib = ">=3.0.0"
+requests = ">=2.0.0"
+
+[package.extras]
+rsa = ["oauthlib[signedtoken] (>=3.0.0)"]
+
+[[package]]
+name = "responses"
+version = "0.14.0"
+description = "A utility library for mocking out the `requests` Python library."
+category = "main"
+optional = true
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[package.dependencies]
+requests = ">=2.0"
+six = "*"
+urllib3 = ">=1.25.10"
+
+[package.extras]
+tests = ["coverage (>=3.7.1,<6.0.0)", "pytest-cov", "pytest-localserver", "flake8", "types-mock", "types-requests", "types-six", "pytest (>=4.6,<5.0)", "pytest (>=4.6)", "mypy"]
+
+[[package]]
+name = "rq"
+version = "1.7.0"
+description = "RQ is a simple, lightweight, library for creating background jobs, and processing them."
+category = "main"
+optional = true
+python-versions = ">=3.5"
+
+[package.dependencies]
+click = ">=5.0.0"
+redis = ">=3.5.0"
+
+[[package]]
+name = "rsa"
+version = "4.7.2"
+description = "Pure-Python RSA implementation"
+category = "main"
+optional = false
+python-versions = ">=3.5, <4"
+
+[package.dependencies]
+pyasn1 = ">=0.1.3"
+
+[[package]]
+name = "ruamel.yaml"
+version = "0.17.16"
+description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order"
+category = "main"
+optional = true
+python-versions = ">=3"
+
+[package.dependencies]
+"ruamel.yaml.clib" = {version = ">=0.1.2", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.10\""}
+
+[package.extras]
+docs = ["ryd"]
+jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"]
+
+[[package]]
+name = "ruamel.yaml.clib"
+version = "0.2.6"
+description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml"
+category = "main"
+optional = true
+python-versions = ">=3.5"
+
+[[package]]
+name = "rules"
+version = "2.2"
+description = "Awesome Django authorization, without the database"
+category = "main"
+optional = true
+python-versions = "*"
+
+[[package]]
+name = "s3transfer"
+version = "0.3.7"
+description = "An Amazon S3 Transfer Manager"
+category = "main"
+optional = true
+python-versions = "*"
+
+[package.dependencies]
+botocore = ">=1.12.36,<2.0a.0"
+
+[[package]]
+name = "simplejson"
+version = "3.17.5"
+description = "Simple, fast, extensible JSON encoder/decoder for Python"
+category = "main"
+optional = true
+python-versions = ">=2.5, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "six"
+version = "1.16.0"
+description = "Python 2 and 3 compatibility utilities"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "sqlalchemy"
+version = "1.3.24"
+description = "Database Abstraction Library"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[package.extras]
+mssql = ["pyodbc"]
+mssql_pymssql = ["pymssql"]
+mssql_pyodbc = ["pyodbc"]
+mysql = ["mysqlclient"]
+oracle = ["cx-oracle"]
+postgresql = ["psycopg2"]
+postgresql_pg8000 = ["pg8000 (<1.16.6)"]
+postgresql_psycopg2binary = ["psycopg2-binary"]
+postgresql_psycopg2cffi = ["psycopg2cffi"]
+pymysql = ["pymysql (<1)", "pymysql"]
+
+[[package]]
+name = "sqlparse"
+version = "0.4.2"
+description = "A non-validating SQL parser."
+category = "main"
+optional = true
+python-versions = ">=3.5"
+
+[[package]]
+name = "tenacity"
+version = "8.0.1"
+description = "Retry code until it succeeds"
+category = "main"
+optional = true
+python-versions = ">=3.6"
+
+[package.extras]
+doc = ["reno", "sphinx", "tornado (>=4.5)"]
+
+[[package]]
+name = "toml"
+version = "0.10.2"
+description = "Python Library for Tom's Obvious, Minimal Language"
+category = "main"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "tomli"
+version = "1.2.1"
+description = "A lil' TOML parser"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "tqdm"
+version = "4.62.3"
+description = "Fast, Extensible Progress Meter"
+category = "main"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+
+[package.extras]
+dev = ["py-make (>=0.1.0)", "twine", "wheel"]
+notebook = ["ipywidgets (>=6)"]
+telegram = ["requests"]
+
+[[package]]
+name = "types-requests"
+version = "2.25.9"
+description = "Typing stubs for requests"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "typing-extensions"
+version = "3.10.0.2"
+description = "Backported and Experimental Type Hints for Python 3.5+"
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "ua-parser"
+version = "0.10.0"
+description = "Python port of Browserscope's user agent parser"
+category = "main"
+optional = true
+python-versions = "*"
+
+[[package]]
+name = "ujson"
+version = "4.2.0"
+description = "Ultra fast JSON encoder and decoder for Python"
+category = "main"
+optional = true
+python-versions = ">=3.6"
+
+[[package]]
+name = "uritemplate"
+version = "3.0.1"
+description = "URI templates"
+category = "main"
+optional = true
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "urllib3"
+version = "1.26.7"
+description = "HTTP library with thread-safe connection pooling, file post, and more."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
+
+[package.extras]
+brotli = ["brotlipy (>=0.6.0)"]
+secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
+socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
+
+[[package]]
+name = "user-agents"
+version = "2.2.0"
+description = "A library to identify devices (phones, tablets) and their capabilities by parsing browser user agent strings."
+category = "main"
+optional = true
+python-versions = "*"
+
+[package.dependencies]
+ua-parser = ">=0.10.0"
+
+[[package]]
+name = "werkzeug"
+version = "2.0.1"
+description = "The comprehensive WSGI web application library."
+category = "main"
+optional = true
+python-versions = ">=3.6"
+
+[package.extras]
+watchdog = ["watchdog"]
+
+[[package]]
+name = "xlrd"
+version = "2.0.1"
+description = "Library for developers to extract data from Microsoft Excel (tm) .xls spreadsheet files"
+category = "main"
+optional = true
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
+
+[package.extras]
+build = ["wheel", "twine"]
+docs = ["sphinx"]
+test = ["pytest", "pytest-cov"]
+
+[[package]]
+name = "xmljson"
+version = "0.2.0"
+description = "Converts XML into JSON/Python dicts/arrays and vice-versa."
+category = "main"
+optional = true
+python-versions = "*"
+
+[[package]]
+name = "xmltodict"
+version = "0.12.0"
+description = "Makes working with XML feel like you are working with JSON"
+category = "main"
+optional = true
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "yarl"
+version = "1.6.3"
+description = "Yet another URL library"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+idna = ">=2.0"
+multidict = ">=4.0"
+
+[[package]]
+name = "zipp"
+version = "3.5.0"
+description = "Backport of pathlib-compatible object wrapper for zip files"
+category = "main"
+optional = true
+python-versions = ">=3.6"
+
+[package.extras]
+docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
+testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"]
+
+[extras]
+excel = ["xlrd", "openpyxl"]
+ls = ["label-studio"]
+ui = ["dash", "dash_bootstrap_components", "dash_interactive_graphviz"]
+
+[metadata]
+lock-version = "1.1"
+python-versions = "^3.8"
+content-hash = "af348908572389f88b26b4f4bf8454bd04ba7bb5f0c7708625541cbebba4aa94"
+
+[metadata.files]
+aiohttp = [
+    {file = "aiohttp-3.7.4.post0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:3cf75f7cdc2397ed4442594b935a11ed5569961333d49b7539ea741be2cc79d5"},
+    {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:4b302b45040890cea949ad092479e01ba25911a15e648429c7c5aae9650c67a8"},
+    {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95"},
+    {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:393f389841e8f2dfc86f774ad22f00923fdee66d238af89b70ea314c4aefd290"},
+    {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:c6e9dcb4cb338d91a73f178d866d051efe7c62a7166653a91e7d9fb18274058f"},
+    {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:5df68496d19f849921f05f14f31bd6ef53ad4b00245da3195048c69934521809"},
+    {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:0563c1b3826945eecd62186f3f5c7d31abb7391fedc893b7e2b26303b5a9f3fe"},
+    {file = "aiohttp-3.7.4.post0-cp36-cp36m-win32.whl", hash = "sha256:3d78619672183be860b96ed96f533046ec97ca067fd46ac1f6a09cd9b7484287"},
+    {file = "aiohttp-3.7.4.post0-cp36-cp36m-win_amd64.whl", hash = "sha256:f705e12750171c0ab4ef2a3c76b9a4024a62c4103e3a55dd6f99265b9bc6fcfc"},
+    {file = "aiohttp-3.7.4.post0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:230a8f7e24298dea47659251abc0fd8b3c4e38a664c59d4b89cca7f6c09c9e87"},
+    {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2e19413bf84934d651344783c9f5e22dee452e251cfd220ebadbed2d9931dbf0"},
+    {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:e4b2b334e68b18ac9817d828ba44d8fcb391f6acb398bcc5062b14b2cbeac970"},
+    {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:d012ad7911653a906425d8473a1465caa9f8dea7fcf07b6d870397b774ea7c0f"},
+    {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:40eced07f07a9e60e825554a31f923e8d3997cfc7fb31dbc1328c70826e04cde"},
+    {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:209b4a8ee987eccc91e2bd3ac36adee0e53a5970b8ac52c273f7f8fd4872c94c"},
+    {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:14762875b22d0055f05d12abc7f7d61d5fd4fe4642ce1a249abdf8c700bf1fd8"},
+    {file = "aiohttp-3.7.4.post0-cp37-cp37m-win32.whl", hash = "sha256:7615dab56bb07bff74bc865307aeb89a8bfd9941d2ef9d817b9436da3a0ea54f"},
+    {file = "aiohttp-3.7.4.post0-cp37-cp37m-win_amd64.whl", hash = "sha256:d9e13b33afd39ddeb377eff2c1c4f00544e191e1d1dee5b6c51ddee8ea6f0cf5"},
+    {file = "aiohttp-3.7.4.post0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:547da6cacac20666422d4882cfcd51298d45f7ccb60a04ec27424d2f36ba3eaf"},
+    {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:af9aa9ef5ba1fd5b8c948bb11f44891968ab30356d65fd0cc6707d989cd521df"},
+    {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:64322071e046020e8797117b3658b9c2f80e3267daec409b350b6a7a05041213"},
+    {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bb437315738aa441251214dad17428cafda9cdc9729499f1d6001748e1d432f4"},
+    {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:e54962802d4b8b18b6207d4a927032826af39395a3bd9196a5af43fc4e60b009"},
+    {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:a00bb73540af068ca7390e636c01cbc4f644961896fa9363154ff43fd37af2f5"},
+    {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:79ebfc238612123a713a457d92afb4096e2148be17df6c50fb9bf7a81c2f8013"},
+    {file = "aiohttp-3.7.4.post0-cp38-cp38-win32.whl", hash = "sha256:515dfef7f869a0feb2afee66b957cc7bbe9ad0cdee45aec7fdc623f4ecd4fb16"},
+    {file = "aiohttp-3.7.4.post0-cp38-cp38-win_amd64.whl", hash = "sha256:114b281e4d68302a324dd33abb04778e8557d88947875cbf4e842c2c01a030c5"},
+    {file = "aiohttp-3.7.4.post0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:7b18b97cf8ee5452fa5f4e3af95d01d84d86d32c5e2bfa260cf041749d66360b"},
+    {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:15492a6368d985b76a2a5fdd2166cddfea5d24e69eefed4630cbaae5c81d89bd"},
+    {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bdb230b4943891321e06fc7def63c7aace16095be7d9cf3b1e01be2f10fba439"},
+    {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:cffe3ab27871bc3ea47df5d8f7013945712c46a3cc5a95b6bee15887f1675c22"},
+    {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a"},
+    {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:a5ca29ee66f8343ed336816c553e82d6cade48a3ad702b9ffa6125d187e2dedb"},
+    {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:17c073de315745a1510393a96e680d20af8e67e324f70b42accbd4cb3315c9fb"},
+    {file = "aiohttp-3.7.4.post0-cp39-cp39-win32.whl", hash = "sha256:932bb1ea39a54e9ea27fc9232163059a0b8855256f4052e776357ad9add6f1c9"},
+    {file = "aiohttp-3.7.4.post0-cp39-cp39-win_amd64.whl", hash = "sha256:02f46fc0e3c5ac58b80d4d56eb0a7c7d97fcef69ace9326289fb9f1955e65cfe"},
+    {file = "aiohttp-3.7.4.post0.tar.gz", hash = "sha256:493d3299ebe5f5a7c66b9819eacdcfbbaaf1a8e84911ddffcdc48888497afecf"},
+]
+appdirs = [
+    {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
+    {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
+]
+asgiref = [
+    {file = "asgiref-3.4.1-py3-none-any.whl", hash = "sha256:ffc141aa908e6f175673e7b1b3b7af4fdb0ecb738fc5c8b88f69f055c2415214"},
+    {file = "asgiref-3.4.1.tar.gz", hash = "sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9"},
+]
+async-timeout = [
+    {file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"},
+    {file = "async_timeout-3.0.1-py3-none-any.whl", hash = "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"},
+]
+atomicwrites = [
+    {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
+    {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
+]
+attr = [
+    {file = "attr-0.3.1-py2-none-any.whl", hash = "sha256:0b1aaddb85bd9e9c4bd75092f4440d6616ff40b0df0437f00771871670f7c9fd"},
+    {file = "attr-0.3.1.tar.gz", hash = "sha256:9091548058d17f132596e61fa7518e504f76b9a4c61ca7d86e1f96dbf7d4775d"},
+]
+attrs = [
+    {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"},
+    {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"},
+]
+azure-core = [
+    {file = "azure-core-1.18.0.zip", hash = "sha256:7f17db829c926ab3b922d63b6f0b86ef3c597487fbb264defa8eb4ccb761e8a0"},
+    {file = "azure_core-1.18.0-py2.py3-none-any.whl", hash = "sha256:3d7769c031822eab3b3ebd58299999b731b30cedb25d3a6c61e551926749c564"},
+]
+azure-storage-blob = [
+    {file = "azure-storage-blob-12.9.0.zip", hash = "sha256:cff66a115c73c90e496c8c8b3026898a3ce64100840276e9245434e28a864225"},
+    {file = "azure_storage_blob-12.9.0-py2.py3-none-any.whl", hash = "sha256:859195b4850dcfe77ffafbe53500abb74b001e52e77fe6d9492fa73639a22127"},
+]
+black = [
+    {file = "black-21.9b0-py3-none-any.whl", hash = "sha256:380f1b5da05e5a1429225676655dddb96f5ae8c75bdf91e53d798871b902a115"},
+    {file = "black-21.9b0.tar.gz", hash = "sha256:7de4cfc7eb6b710de325712d40125689101d21d25283eed7e9998722cf10eb91"},
+]
+boto = [
+    {file = "boto-2.49.0-py2.py3-none-any.whl", hash = "sha256:147758d41ae7240dc989f0039f27da8ca0d53734be0eb869ef16e3adcfa462e8"},
+    {file = "boto-2.49.0.tar.gz", hash = "sha256:ea0d3b40a2d852767be77ca343b58a9e3a4b00d9db440efb8da74b4e58025e5a"},
+]
+boto3 = [
+    {file = "boto3-1.16.28-py2.py3-none-any.whl", hash = "sha256:ca62b4517ddb6a81a9b0113cca05b2dba5faab14d2b35b81f402b052955738c5"},
+    {file = "boto3-1.16.28.tar.gz", hash = "sha256:7f44988fc605fa264046d7d16f773836ac1e8b3f6721b79ba796b875bbf9ea75"},
+]
+botocore = [
+    {file = "botocore-1.19.28-py2.py3-none-any.whl", hash = "sha256:d8dab9b74dc0ad2e5439e5ea3c119e234d2f02952f37f379d9a74adfa305ed75"},
+    {file = "botocore-1.19.28.tar.gz", hash = "sha256:97741448e63850895818193c29b93ec6319135587ae4994a6f611ffccb1a978e"},
+]
+boxing = [
+    {file = "boxing-0.1.4-py2-none-any.whl", hash = "sha256:707221c4d946b330d4ca885bdbf48ed46644fae0abf34246a0dcdd24e4faa628"},
+    {file = "boxing-0.1.4-py2.7.egg", hash = "sha256:42618438974b81008d92203f4133a26c0cd584eadaf11993a03f839bbbf5ebff"},
+    {file = "boxing-0.1.4-py3-none-any.whl", hash = "sha256:76fa658edab8344c0917db4b3d543c89340deca602c6736837c206f127ded296"},
+    {file = "boxing-0.1.4-py3.6.egg", hash = "sha256:35c702569b5b673a1760a3a746dd2ad32e8e6ccdb7820cd7bd9a439ad45014e5"},
+    {file = "boxing-0.1.4.tar.gz", hash = "sha256:b9141bd02d0fb5ebda881e727e16ce7c89034773282b29b8ce938a8117e79d1b"},
+]
+brotli = [
+    {file = "Brotli-1.0.9-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:268fe94547ba25b58ebc724680609c8ee3e5a843202e9a381f6f9c5e8bdb5c70"},
+    {file = "Brotli-1.0.9-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:c2415d9d082152460f2bd4e382a1e85aed233abc92db5a3880da2257dc7daf7b"},
+    {file = "Brotli-1.0.9-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5913a1177fc36e30fcf6dc868ce23b0453952c78c04c266d3149b3d39e1410d6"},
+    {file = "Brotli-1.0.9-cp27-cp27m-win32.whl", hash = "sha256:afde17ae04d90fbe53afb628f7f2d4ca022797aa093e809de5c3cf276f61bbfa"},
+    {file = "Brotli-1.0.9-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7cb81373984cc0e4682f31bc3d6be9026006d96eecd07ea49aafb06897746452"},
+    {file = "Brotli-1.0.9-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:db844eb158a87ccab83e868a762ea8024ae27337fc7ddcbfcddd157f841fdfe7"},
+    {file = "Brotli-1.0.9-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:c83aa123d56f2e060644427a882a36b3c12db93727ad7a7b9efd7d7f3e9cc2c4"},
+    {file = "Brotli-1.0.9-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:6b2ae9f5f67f89aade1fab0f7fd8f2832501311c363a21579d02defa844d9296"},
+    {file = "Brotli-1.0.9-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:68715970f16b6e92c574c30747c95cf8cf62804569647386ff032195dc89a430"},
+    {file = "Brotli-1.0.9-cp35-cp35m-win32.whl", hash = "sha256:defed7ea5f218a9f2336301e6fd379f55c655bea65ba2476346340a0ce6f74a1"},
+    {file = "Brotli-1.0.9-cp35-cp35m-win_amd64.whl", hash = "sha256:88c63a1b55f352b02c6ffd24b15ead9fc0e8bf781dbe070213039324922a2eea"},
+    {file = "Brotli-1.0.9-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:503fa6af7da9f4b5780bb7e4cbe0c639b010f12be85d02c99452825dd0feef3f"},
+    {file = "Brotli-1.0.9-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:40d15c79f42e0a2c72892bf407979febd9cf91f36f495ffb333d1d04cebb34e4"},
+    {file = "Brotli-1.0.9-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:93130612b837103e15ac3f9cbacb4613f9e348b58b3aad53721d92e57f96d46a"},
+    {file = "Brotli-1.0.9-cp36-cp36m-win32.whl", hash = "sha256:61a7ee1f13ab913897dac7da44a73c6d44d48a4adff42a5701e3239791c96e14"},
+    {file = "Brotli-1.0.9-cp36-cp36m-win_amd64.whl", hash = "sha256:1c48472a6ba3b113452355b9af0a60da5c2ae60477f8feda8346f8fd48e3e87c"},
+    {file = "Brotli-1.0.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3b78a24b5fd13c03ee2b7b86290ed20efdc95da75a3557cc06811764d5ad1126"},
+    {file = "Brotli-1.0.9-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:9d12cf2851759b8de8ca5fde36a59c08210a97ffca0eb94c532ce7b17c6a3d1d"},
+    {file = "Brotli-1.0.9-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:6c772d6c0a79ac0f414a9f8947cc407e119b8598de7621f39cacadae3cf57d12"},
+    {file = "Brotli-1.0.9-cp37-cp37m-win32.whl", hash = "sha256:f909bbbc433048b499cb9db9e713b5d8d949e8c109a2a548502fb9aa8630f0b1"},
+    {file = "Brotli-1.0.9-cp37-cp37m-win_amd64.whl", hash = "sha256:97f715cf371b16ac88b8c19da00029804e20e25f30d80203417255d239f228b5"},
+    {file = "Brotli-1.0.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:160c78292e98d21e73a4cc7f76a234390e516afcd982fa17e1422f7c6a9ce9c8"},
+    {file = "Brotli-1.0.9-cp38-cp38-manylinux1_i686.whl", hash = "sha256:b663f1e02de5d0573610756398e44c130add0eb9a3fc912a09665332942a2efb"},
+    {file = "Brotli-1.0.9-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5b6ef7d9f9c38292df3690fe3e302b5b530999fa90014853dcd0d6902fb59f26"},
+    {file = "Brotli-1.0.9-cp38-cp38-win32.whl", hash = "sha256:35a3edbe18e876e596553c4007a087f8bcfd538f19bc116917b3c7522fca0429"},
+    {file = "Brotli-1.0.9-cp38-cp38-win_amd64.whl", hash = "sha256:269a5743a393c65db46a7bb982644c67ecba4b8d91b392403ad8a861ba6f495f"},
+    {file = "Brotli-1.0.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5cb1e18167792d7d21e21365d7650b72d5081ed476123ff7b8cac7f45189c0c7"},
+    {file = "Brotli-1.0.9-cp39-cp39-manylinux1_i686.whl", hash = "sha256:16d528a45c2e1909c2798f27f7bf0a3feec1dc9e50948e738b961618e38b6a7b"},
+    {file = "Brotli-1.0.9-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:56d027eace784738457437df7331965473f2c0da2c70e1a1f6fdbae5402e0389"},
+    {file = "Brotli-1.0.9-cp39-cp39-win32.whl", hash = "sha256:cfc391f4429ee0a9370aa93d812a52e1fee0f37a81861f4fdd1f4fb28e8547c3"},
+    {file = "Brotli-1.0.9-cp39-cp39-win_amd64.whl", hash = "sha256:854c33dad5ba0fbd6ab69185fec8dab89e13cda6b7d191ba111987df74f38761"},
+    {file = "Brotli-1.0.9.zip", hash = "sha256:4d1b810aa0ed773f81dceda2cc7b403d01057458730e309856356d4ef4188438"},
+]
+cachetools = [
+    {file = "cachetools-4.2.2-py3-none-any.whl", hash = "sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001"},
+    {file = "cachetools-4.2.2.tar.gz", hash = "sha256:61b5ed1e22a0924aed1d23b478f37e8d52549ff8a961de2909c69bf950020cff"},
+]
+certifi = [
+    {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"},
+    {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"},
+]
+cffi = [
+    {file = "cffi-1.14.6-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:22b9c3c320171c108e903d61a3723b51e37aaa8c81255b5e7ce102775bd01e2c"},
+    {file = "cffi-1.14.6-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:f0c5d1acbfca6ebdd6b1e3eded8d261affb6ddcf2186205518f1428b8569bb99"},
+    {file = "cffi-1.14.6-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:99f27fefe34c37ba9875f224a8f36e31d744d8083e00f520f133cab79ad5e819"},
+    {file = "cffi-1.14.6-cp27-cp27m-win32.whl", hash = "sha256:55af55e32ae468e9946f741a5d51f9896da6b9bf0bbdd326843fec05c730eb20"},
+    {file = "cffi-1.14.6-cp27-cp27m-win_amd64.whl", hash = "sha256:7bcac9a2b4fdbed2c16fa5681356d7121ecabf041f18d97ed5b8e0dd38a80224"},
+    {file = "cffi-1.14.6-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ed38b924ce794e505647f7c331b22a693bee1538fdf46b0222c4717b42f744e7"},
+    {file = "cffi-1.14.6-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e22dcb48709fc51a7b58a927391b23ab37eb3737a98ac4338e2448bef8559b33"},
+    {file = "cffi-1.14.6-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:aedb15f0a5a5949ecb129a82b72b19df97bbbca024081ed2ef88bd5c0a610534"},
+    {file = "cffi-1.14.6-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:48916e459c54c4a70e52745639f1db524542140433599e13911b2f329834276a"},
+    {file = "cffi-1.14.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f627688813d0a4140153ff532537fbe4afea5a3dffce1f9deb7f91f848a832b5"},
+    {file = "cffi-1.14.6-cp35-cp35m-win32.whl", hash = "sha256:f0010c6f9d1a4011e429109fda55a225921e3206e7f62a0c22a35344bfd13cca"},
+    {file = "cffi-1.14.6-cp35-cp35m-win_amd64.whl", hash = "sha256:57e555a9feb4a8460415f1aac331a2dc833b1115284f7ded7278b54afc5bd218"},
+    {file = "cffi-1.14.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e8c6a99be100371dbb046880e7a282152aa5d6127ae01783e37662ef73850d8f"},
+    {file = "cffi-1.14.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:19ca0dbdeda3b2615421d54bef8985f72af6e0c47082a8d26122adac81a95872"},
+    {file = "cffi-1.14.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d950695ae4381ecd856bcaf2b1e866720e4ab9a1498cba61c602e56630ca7195"},
+    {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9dc245e3ac69c92ee4c167fbdd7428ec1956d4e754223124991ef29eb57a09d"},
+    {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8661b2ce9694ca01c529bfa204dbb144b275a31685a075ce123f12331be790b"},
+    {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b315d709717a99f4b27b59b021e6207c64620790ca3e0bde636a6c7f14618abb"},
+    {file = "cffi-1.14.6-cp36-cp36m-win32.whl", hash = "sha256:80b06212075346b5546b0417b9f2bf467fea3bfe7352f781ffc05a8ab24ba14a"},
+    {file = "cffi-1.14.6-cp36-cp36m-win_amd64.whl", hash = "sha256:a9da7010cec5a12193d1af9872a00888f396aba3dc79186604a09ea3ee7c029e"},
+    {file = "cffi-1.14.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4373612d59c404baeb7cbd788a18b2b2a8331abcc84c3ba40051fcd18b17a4d5"},
+    {file = "cffi-1.14.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f10afb1004f102c7868ebfe91c28f4a712227fe4cb24974350ace1f90e1febbf"},
+    {file = "cffi-1.14.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fd4305f86f53dfd8cd3522269ed7fc34856a8ee3709a5e28b2836b2db9d4cd69"},
+    {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d6169cb3c6c2ad50db5b868db6491a790300ade1ed5d1da29289d73bbe40b56"},
+    {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d4b68e216fc65e9fe4f524c177b54964af043dde734807586cf5435af84045c"},
+    {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33791e8a2dc2953f28b8d8d300dde42dd929ac28f974c4b4c6272cb2955cb762"},
+    {file = "cffi-1.14.6-cp37-cp37m-win32.whl", hash = "sha256:0c0591bee64e438883b0c92a7bed78f6290d40bf02e54c5bf0978eaf36061771"},
+    {file = "cffi-1.14.6-cp37-cp37m-win_amd64.whl", hash = "sha256:8eb687582ed7cd8c4bdbff3df6c0da443eb89c3c72e6e5dcdd9c81729712791a"},
+    {file = "cffi-1.14.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ba6f2b3f452e150945d58f4badd92310449876c4c954836cfb1803bdd7b422f0"},
+    {file = "cffi-1.14.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:64fda793737bc4037521d4899be780534b9aea552eb673b9833b01f945904c2e"},
+    {file = "cffi-1.14.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:9f3e33c28cd39d1b655ed1ba7247133b6f7fc16fa16887b120c0c670e35ce346"},
+    {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26bb2549b72708c833f5abe62b756176022a7b9a7f689b571e74c8478ead51dc"},
+    {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb687a11f0a7a1839719edd80f41e459cc5366857ecbed383ff376c4e3cc6afd"},
+    {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2ad4d668a5c0645d281dcd17aff2be3212bc109b33814bbb15c4939f44181cc"},
+    {file = "cffi-1.14.6-cp38-cp38-win32.whl", hash = "sha256:487d63e1454627c8e47dd230025780e91869cfba4c753a74fda196a1f6ad6548"},
+    {file = "cffi-1.14.6-cp38-cp38-win_amd64.whl", hash = "sha256:c33d18eb6e6bc36f09d793c0dc58b0211fccc6ae5149b808da4a62660678b156"},
+    {file = "cffi-1.14.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:06c54a68935738d206570b20da5ef2b6b6d92b38ef3ec45c5422c0ebaf338d4d"},
+    {file = "cffi-1.14.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:f174135f5609428cc6e1b9090f9268f5c8935fddb1b25ccb8255a2d50de6789e"},
+    {file = "cffi-1.14.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f3ebe6e73c319340830a9b2825d32eb6d8475c1dac020b4f0aa774ee3b898d1c"},
+    {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c8d896becff2fa653dc4438b54a5a25a971d1f4110b32bd3068db3722c80202"},
+    {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4922cd707b25e623b902c86188aca466d3620892db76c0bdd7b99a3d5e61d35f"},
+    {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c9e005e9bd57bc987764c32a1bee4364c44fdc11a3cc20a40b93b444984f2b87"},
+    {file = "cffi-1.14.6-cp39-cp39-win32.whl", hash = "sha256:eb9e2a346c5238a30a746893f23a9535e700f8192a68c07c0258e7ece6ff3728"},
+    {file = "cffi-1.14.6-cp39-cp39-win_amd64.whl", hash = "sha256:818014c754cd3dba7229c0f5884396264d51ffb87ec86e927ef0be140bfdb0d2"},
+    {file = "cffi-1.14.6.tar.gz", hash = "sha256:c9a875ce9d7fe32887784274dd533c57909b7b1dcadcc128a2ac21331a9765dd"},
+]
+chardet = [
+    {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"},
+    {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"},
+]
+charset-normalizer = [
+    {file = "charset-normalizer-2.0.6.tar.gz", hash = "sha256:5ec46d183433dcbd0ab716f2d7f29d8dee50505b3fdb40c6b985c7c4f5a3591f"},
+    {file = "charset_normalizer-2.0.6-py3-none-any.whl", hash = "sha256:5d209c0a931f215cee683b6445e2d77677e7e75e159f78def0db09d68fafcaa6"},
+]
+click = [
+    {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"},
+    {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"},
+]
+cloudpickle = [
+    {file = "cloudpickle-2.0.0-py3-none-any.whl", hash = "sha256:6b2df9741d06f43839a3275c4e6632f7df6487a1f181f5f46a052d3c917c3d11"},
+    {file = "cloudpickle-2.0.0.tar.gz", hash = "sha256:5cd02f3b417a783ba84a4ec3e290ff7929009fe51f6405423cfccfadd43ba4a4"},
+]
+colorama = [
+    {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
+    {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
+]
+coreapi = [
+    {file = "coreapi-2.3.3-py2.py3-none-any.whl", hash = "sha256:bf39d118d6d3e171f10df9ede5666f63ad80bba9a29a8ec17726a66cf52ee6f3"},
+    {file = "coreapi-2.3.3.tar.gz", hash = "sha256:46145fcc1f7017c076a2ef684969b641d18a2991051fddec9458ad3f78ffc1cb"},
+]
+coreschema = [
+    {file = "coreschema-0.0.4-py2-none-any.whl", hash = "sha256:5e6ef7bf38c1525d5e55a895934ab4273548629f16aed5c0a6caa74ebf45551f"},
+    {file = "coreschema-0.0.4.tar.gz", hash = "sha256:9503506007d482ab0867ba14724b93c18a33b22b6d19fb419ef2d239dd4a1607"},
+]
+cryptography = [
+    {file = "cryptography-3.4.8-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:a00cf305f07b26c351d8d4e1af84ad7501eca8a342dedf24a7acb0e7b7406e14"},
+    {file = "cryptography-3.4.8-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:f44d141b8c4ea5eb4dbc9b3ad992d45580c1d22bf5e24363f2fbf50c2d7ae8a7"},
+    {file = "cryptography-3.4.8-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0a7dcbcd3f1913f664aca35d47c1331fce738d44ec34b7be8b9d332151b0b01e"},
+    {file = "cryptography-3.4.8-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34dae04a0dce5730d8eb7894eab617d8a70d0c97da76b905de9efb7128ad7085"},
+    {file = "cryptography-3.4.8-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1eb7bb0df6f6f583dd8e054689def236255161ebbcf62b226454ab9ec663746b"},
+    {file = "cryptography-3.4.8-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:9965c46c674ba8cc572bc09a03f4c649292ee73e1b683adb1ce81e82e9a6a0fb"},
+    {file = "cryptography-3.4.8-cp36-abi3-win32.whl", hash = "sha256:21ca464b3a4b8d8e86ba0ee5045e103a1fcfac3b39319727bc0fc58c09c6aff7"},
+    {file = "cryptography-3.4.8-cp36-abi3-win_amd64.whl", hash = "sha256:3520667fda779eb788ea00080124875be18f2d8f0848ec00733c0ec3bb8219fc"},
+    {file = "cryptography-3.4.8-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d2a6e5ef66503da51d2110edf6c403dc6b494cc0082f85db12f54e9c5d4c3ec5"},
+    {file = "cryptography-3.4.8-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a305600e7a6b7b855cd798e00278161b681ad6e9b7eca94c721d5f588ab212af"},
+    {file = "cryptography-3.4.8-pp36-pypy36_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:3fa3a7ccf96e826affdf1a0a9432be74dc73423125c8f96a909e3835a5ef194a"},
+    {file = "cryptography-3.4.8-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:d9ec0e67a14f9d1d48dd87a2531009a9b251c02ea42851c060b25c782516ff06"},
+    {file = "cryptography-3.4.8-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5b0fbfae7ff7febdb74b574055c7466da334a5371f253732d7e2e7525d570498"},
+    {file = "cryptography-3.4.8-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94fff993ee9bc1b2440d3b7243d488c6a3d9724cc2b09cdb297f6a886d040ef7"},
+    {file = "cryptography-3.4.8-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:8695456444f277af73a4877db9fc979849cd3ee74c198d04fc0776ebc3db52b9"},
+    {file = "cryptography-3.4.8-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:cd65b60cfe004790c795cc35f272e41a3df4631e2fb6b35aa7ac6ef2859d554e"},
+    {file = "cryptography-3.4.8.tar.gz", hash = "sha256:94cc5ed4ceaefcbe5bf38c8fba6a21fc1d365bb8fb826ea1688e3370b2e24a1c"},
+]
+dash = [
+    {file = "dash-1.20.0.tar.gz", hash = "sha256:127c16f71d3c8345dd29ab2aed099330aafd6d558734bec5bbcccadd0a7e6b29"},
+]
+dash-bootstrap-components = [
+    {file = "dash-bootstrap-components-0.12.2.tar.gz", hash = "sha256:b3266967f16d542ce715fd8ea0ed082b56e64dfa623750bf8150b72fe7b303ff"},
+    {file = "dash_bootstrap_components-0.12.2-py3-none-any.whl", hash = "sha256:4f697a0996e458a17859614a429f093ab0e143ab359b825e7ac0c1625738c2c8"},
+]
+dash-core-components = [
+    {file = "dash_core_components-1.16.0.tar.gz", hash = "sha256:e8cdfaf3580577670bb2d1c3168efa06f5a7b439fbe5527cfaefa3e32394542f"},
+]
+dash-html-components = [
+    {file = "dash_html_components-1.1.3.tar.gz", hash = "sha256:88adb77a674d5d7d0835d71c469f6e7b4aa692f9673808a474d244b71863c58a"},
+]
+dash-interactive-graphviz = [
+    {file = "dash_interactive_graphviz-0.3.0.tar.gz", hash = "sha256:84bf319e4272bc08a23e47a643c3c69a847c7e10418099a7e21f4f2588fc37b8"},
+]
+dash-renderer = [
+    {file = "dash_renderer-1.9.1.tar.gz", hash = "sha256:73a69e3d145880e68e42723ad10182251d92b44f3efe92b8763145cfd2158e7e"},
+]
+dash-table = [
+    {file = "dash_table-4.11.3.tar.gz", hash = "sha256:0a4f22a5cf5120882a252a3348fc15ef45a1b75bf900934783e338aceac52f56"},
+]
+decopatch = [
+    {file = "decopatch-1.4.8-py2.py3-none-any.whl", hash = "sha256:29a74d5d753423b188d5b537532da4f4b88e33ddccb95a8a20a5eff5b13265d4"},
+    {file = "decopatch-1.4.8.tar.gz", hash = "sha256:c66b0815f15db04de7bb52b0b276432b76b7346fe7046f28033f48a14340d144"},
+]
+decorator = [
+    {file = "decorator-5.1.0-py3-none-any.whl", hash = "sha256:7b12e7c3c6ab203a29e157335e9122cb03de9ab7264b137594103fd4a683b374"},
+    {file = "decorator-5.1.0.tar.gz", hash = "sha256:e59913af105b9860aa2c8d3272d9de5a56a4e608db9a2f167a8480b323d529a7"},
+]
+django = [
+    {file = "Django-3.1.4-py3-none-any.whl", hash = "sha256:5c866205f15e7a7123f1eec6ab939d22d5bde1416635cab259684af66d8e48a2"},
+    {file = "Django-3.1.4.tar.gz", hash = "sha256:edb10b5c45e7e9c0fb1dc00b76ec7449aca258a39ffd613dbd078c51d19c9f03"},
+]
+django-annoying = [
+    {file = "django-annoying-0.10.6.tar.gz", hash = "sha256:083b5e26f140f86178fcb47931f69b7ff75181ccd1e318d9c218ae9babc1805e"},
+    {file = "django_annoying-0.10.6-py2.py3-none-any.whl", hash = "sha256:e230f28fec0559b4fdb621ff202068a78170ada2ac1ec7a73ba822cc1f737791"},
+]
+django-cors-headers = [
+    {file = "django-cors-headers-3.6.0.tar.gz", hash = "sha256:5665fc1b1aabf1b678885cf6f8f8bd7da36ef0a978375e767d491b48d3055d8f"},
+    {file = "django_cors_headers-3.6.0-py3-none-any.whl", hash = "sha256:ba898dd478cd4be3a38ebc3d8729fa4d044679f8c91b2684edee41129d7e968a"},
+]
+django-debug-toolbar = [
+    {file = "django-debug-toolbar-3.2.tar.gz", hash = "sha256:84e2607d900dbd571df0a2acf380b47c088efb787dce9805aefeb407341961d2"},
+    {file = "django_debug_toolbar-3.2-py3-none-any.whl", hash = "sha256:9e5a25d0c965f7e686f6a8ba23613ca9ca30184daa26487706d4829f5cfb697a"},
+]
+django-extensions = [
+    {file = "django-extensions-3.1.0.tar.gz", hash = "sha256:7cd002495ff0a0e5eb6cdd6be759600905b4e4079232ea27618fc46bdd853651"},
+    {file = "django_extensions-3.1.0-py3-none-any.whl", hash = "sha256:c7f88625a53f631745d4f2bef9ec4dcb999ed59476393bdbbe99db8596778846"},
+]
+django-filter = [
+    {file = "django-filter-2.4.0.tar.gz", hash = "sha256:84e9d5bb93f237e451db814ed422a3a625751cbc9968b484ecc74964a8696b06"},
+    {file = "django_filter-2.4.0-py3-none-any.whl", hash = "sha256:e00d32cebdb3d54273c48f4f878f898dced8d5dfaad009438fe61ebdf535ace1"},
+]
+django-model-utils = [
+    {file = "django-model-utils-4.1.1.tar.gz", hash = "sha256:eb5dd05ef7d7ce6bc79cae54ea7c4a221f6f81e2aad7722933aee66489e7264b"},
+    {file = "django_model_utils-4.1.1-py3-none-any.whl", hash = "sha256:ef7c440024e797796a3811432abdd2be8b5225ae64ef346f8bfc6de7d8e5d73c"},
+]
+django-redis-cache = [
+    {file = "django-redis-cache-3.0.0.tar.gz", hash = "sha256:9a2eebef421d996a82098a19d17ff6b321265cd73178fa398913019764e8394a"},
+]
+django-rest-swagger = [
+    {file = "django-rest-swagger-2.2.0.tar.gz", hash = "sha256:48f6aded9937e90ae7cbe9e6c932b9744b8af80cc4e010088b3278c700e0685b"},
+    {file = "django_rest_swagger-2.2.0-py2.py3-none-any.whl", hash = "sha256:b039b0288bab4665cd45dc5d16f94b13911bc4ad0ed55f74ad3b90aa31c87c17"},
+]
+django-rq = [
+    {file = "django-rq-2.3.2.tar.gz", hash = "sha256:ee9aefba814ae00b6d6a566ed0cd2a0a090e0e42d90bb9c4f1c70d3cacb47a52"},
+    {file = "django_rq-2.3.2-py2.py3-none-any.whl", hash = "sha256:f0bd7ac3b8b4b4abb161646e80a8751c81664beaa810e92fdbcf9fb3e7d72727"},
+]
+django-user-agents = [
+    {file = "django-user_agents-0.4.0.tar.gz", hash = "sha256:cda8ae2146cee30e6867a07943f56ecc570b4391d725ab5309901a8b3e4a3514"},
+    {file = "django_user_agents-0.4.0-py3-none-any.whl", hash = "sha256:cd9d9f7158b23c5237b2dacb0bc4fffdf77fefe1d2633b5814d3874288ebdb5d"},
+]
+djangorestframework = [
+    {file = "djangorestframework-3.12.4-py3-none-any.whl", hash = "sha256:6d1d59f623a5ad0509fe0d6bfe93cbdfe17b8116ebc8eda86d45f6e16e819aaf"},
+    {file = "djangorestframework-3.12.4.tar.gz", hash = "sha256:f747949a8ddac876e879190df194b925c177cdeb725a099db1460872f7c0a7f2"},
+]
+drf-dynamic-fields = [
+    {file = "drf_dynamic_fields-0.3.0-py2.py3-none-any.whl", hash = "sha256:5da7d856f411561cb25d6ce3a0a152c2bd441d41a81b279ca0fec214d14c6134"},
+    {file = "drf_dynamic_fields-0.3.0.tar.gz", hash = "sha256:245eed41664976451b51ab9b59af179692ab6d8ee260a6b85ac2d44d2b06f5b4"},
+]
+drf-generators = [
+    {file = "drf-generators-0.3.0.tar.gz", hash = "sha256:9824dcf147e18b1336ec41c0e905d45290b22862d62ee7f1b2c40c7c75435e77"},
+]
+drf-yasg = [
+    {file = "drf-yasg-1.20.0.tar.gz", hash = "sha256:d50f197c7f02545d0b736df88c6d5cf874f8fea2507ad85ad7de6ae5bf2d9e5a"},
+    {file = "drf_yasg-1.20.0-py2.py3-none-any.whl", hash = "sha256:8b72e5b1875931a8d11af407be3a9a5ba8776541492947a0df5bafda6b7f8267"},
+]
+et-xmlfile = [
+    {file = "et_xmlfile-1.1.0-py3-none-any.whl", hash = "sha256:a2ba85d1d6a74ef63837eed693bcb89c3f752169b0e3e7ae5b16ca5e1b3deada"},
+    {file = "et_xmlfile-1.1.0.tar.gz", hash = "sha256:8eb9e2bc2f8c97e37a2dc85a09ecdcdec9d8a396530a6d5a33b30b9a92da0c5c"},
+]
+flake8 = [
+    {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"},
+    {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"},
+]
+flask = [
+    {file = "Flask-1.1.2-py2.py3-none-any.whl", hash = "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557"},
+    {file = "Flask-1.1.2.tar.gz", hash = "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060"},
+]
+flask-compress = [
+    {file = "Flask-Compress-1.10.1.tar.gz", hash = "sha256:28352387efbbe772cfb307570019f81957a13ff718d994a9125fa705efb73680"},
+    {file = "Flask_Compress-1.10.1-py3-none-any.whl", hash = "sha256:a6c2d1ff51771e9b39d7a612754f4cb4e8af20cebe16b02fd19d98d8dd6966e5"},
+]
+fsspec = [
+    {file = "fsspec-2021.5.0-py3-none-any.whl", hash = "sha256:a08daa28ecb98544350d62f8307a7678e410c4b5245e04d14fcf4a3f13e959a7"},
+    {file = "fsspec-2021.5.0.tar.gz", hash = "sha256:30064d89b00c6acfe5d8e027b6c0ef3df25ca17fd1d5cad2e8208c9aa5300f8d"},
+]
+future = [
+    {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"},
+]
+gcsfs = [
+    {file = "gcsfs-2021.5.0-py2.py3-none-any.whl", hash = "sha256:c8ed85f13d97efc8b055d2b5abc3528ced98fc288a7858c23bf6b197da55a6ba"},
+    {file = "gcsfs-2021.5.0.tar.gz", hash = "sha256:f3b54c49f85b5cfb80f34e63c69680493405fc59b0d3f1d466e4400efed93c2b"},
+]
+google-api-core = [
+    {file = "google-api-core-1.31.2.tar.gz", hash = "sha256:8500aded318fdb235130bf183c726a05a9cb7c4b09c266bd5119b86cdb8a4d10"},
+    {file = "google_api_core-1.31.2-py2.py3-none-any.whl", hash = "sha256:384459a0dc98c1c8cd90b28dc5800b8705e0275a673a7144a513ae80fc77950b"},
+]
+google-auth = [
+    {file = "google-auth-1.35.0.tar.gz", hash = "sha256:b7033be9028c188ee30200b204ea00ed82ea1162e8ac1df4aa6ded19a191d88e"},
+    {file = "google_auth-1.35.0-py2.py3-none-any.whl", hash = "sha256:997516b42ecb5b63e8d80f5632c1a61dddf41d2a4c2748057837e06e00014258"},
+]
+google-auth-oauthlib = [
+    {file = "google-auth-oauthlib-0.4.6.tar.gz", hash = "sha256:a90a072f6993f2c327067bf65270046384cda5a8ecb20b94ea9a687f1f233a7a"},
+    {file = "google_auth_oauthlib-0.4.6-py2.py3-none-any.whl", hash = "sha256:3f2a6e802eebbb6fb736a370fbf3b055edcb6b52878bf2f26330b5e041316c73"},
+]
+google-cloud-core = [
+    {file = "google-cloud-core-1.7.2.tar.gz", hash = "sha256:b1030aadcbb2aeb4ee51475426351af83c1072456b918fb8fdb80666c4bb63b5"},
+    {file = "google_cloud_core-1.7.2-py2.py3-none-any.whl", hash = "sha256:5b77935f3d9573e27007749a3b522f08d764c5b5930ff1527b2ab2743e9f0c15"},
+]
+google-cloud-storage = [
+    {file = "google-cloud-storage-1.28.1.tar.gz", hash = "sha256:a7b5c326e7307a83fa1f1f0ef71aba9ad1f3a2bc6a768401e13fc02369fd8612"},
+    {file = "google_cloud_storage-1.28.1-py2.py3-none-any.whl", hash = "sha256:0b28536acab1d7e856a7a89bbfcad41f26f40b46af59786ca874ff0f94bbc0f9"},
+]
+google-resumable-media = [
+    {file = "google-resumable-media-0.5.1.tar.gz", hash = "sha256:97155236971970382b738921f978a6f86a7b5a0b0311703d991e065d3cb55773"},
+    {file = "google_resumable_media-0.5.1-py2.py3-none-any.whl", hash = "sha256:cdc64378dc9a7a7bf963a8d0c944c99b549dc0c195a9acbf1fcd465f380b9002"},
+]
+googleapis-common-protos = [
+    {file = "googleapis-common-protos-1.53.0.tar.gz", hash = "sha256:a88ee8903aa0a81f6c3cec2d5cf62d3c8aa67c06439b0496b49048fb1854ebf4"},
+    {file = "googleapis_common_protos-1.53.0-py2.py3-none-any.whl", hash = "sha256:f6d561ab8fb16b30020b940e2dd01cd80082f4762fa9f3ee670f4419b4b8dbd0"},
+]
+htmlmin = [
+    {file = "htmlmin-0.1.12.tar.gz", hash = "sha256:50c1ef4630374a5d723900096a961cff426dff46b48f34d194a81bbe14eca178"},
+]
+idna = [
+    {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"},
+    {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"},
+]
+inflection = [
+    {file = "inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"},
+    {file = "inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417"},
+]
+iniconfig = [
+    {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
+    {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
+]
+isodate = [
+    {file = "isodate-0.6.0-py2.py3-none-any.whl", hash = "sha256:aa4d33c06640f5352aca96e4b81afd8ab3b47337cc12089822d6f322ac772c81"},
+    {file = "isodate-0.6.0.tar.gz", hash = "sha256:2e364a3d5759479cdb2d37cce6b9376ea504db2ff90252a2e5b7cc89cc9ff2d8"},
+]
+itsdangerous = [
+    {file = "itsdangerous-2.0.1-py3-none-any.whl", hash = "sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c"},
+    {file = "itsdangerous-2.0.1.tar.gz", hash = "sha256:9e724d68fc22902a1435351f84c3fb8623f303fffcc566a4cb952df8c572cff0"},
+]
+itypes = [
+    {file = "itypes-1.2.0-py2.py3-none-any.whl", hash = "sha256:03da6872ca89d29aef62773672b2d408f490f80db48b23079a4b194c86dd04c6"},
+    {file = "itypes-1.2.0.tar.gz", hash = "sha256:af886f129dea4a2a1e3d36595a2d139589e4dd287f5cab0b40e799ee81570ff1"},
+]
+jinja2 = [
+    {file = "Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419"},
+    {file = "Jinja2-2.11.3.tar.gz", hash = "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"},
+]
+jmespath = [
+    {file = "jmespath-0.10.0-py2.py3-none-any.whl", hash = "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f"},
+    {file = "jmespath-0.10.0.tar.gz", hash = "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9"},
+]
+joblib = [
+    {file = "joblib-1.0.1-py3-none-any.whl", hash = "sha256:feeb1ec69c4d45129954f1b7034954241eedfd6ba39b5e9e4b6883be3332d5e5"},
+    {file = "joblib-1.0.1.tar.gz", hash = "sha256:9c17567692206d2f3fb9ecf5e991084254fe631665c450b443761c4186a613f7"},
+]
+jsonschema = [
+    {file = "jsonschema-3.2.0-py2.py3-none-any.whl", hash = "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163"},
+    {file = "jsonschema-3.2.0.tar.gz", hash = "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a"},
+]
+label-studio = [
+    {file = "label-studio-1.0.0.post3.tar.gz", hash = "sha256:65acfecc4b9795b9204ffdff484cdad6ac93521cf2d59cef19295af248800612"},
+    {file = "label_studio-1.0.0.post3-py3-none-any.whl", hash = "sha256:5ccee25c9b92c6a1b23602fcaa93dddc7b6842da7c9ffaf59fbe4979135a57b5"},
+]
+label-studio-converter = [
+    {file = "label-studio-converter-0.0.26.tar.gz", hash = "sha256:52ecd517c2ef04dea696c4d3d5bde1ecd5c3e3eb4b0b4555e42d6e57d1f5491f"},
+    {file = "label_studio_converter-0.0.26-py3-none-any.whl", hash = "sha256:cd4e7170337f83613e6a2c136452185d81e8566fd4b70f1ddb18a3a367294572"},
+]
+lockfile = [
+    {file = "lockfile-0.12.2-py2.py3-none-any.whl", hash = "sha256:6c3cb24f344923d30b2785d5ad75182c8ea7ac1b6171b08657258ec7429d50fa"},
+    {file = "lockfile-0.12.2.tar.gz", hash = "sha256:6aed02de03cba24efabcd600b30540140634fc06cfa603822d508d5361e9f799"},
+]
+lxml = [
+    {file = "lxml-4.6.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:df7c53783a46febb0e70f6b05df2ba104610f2fb0d27023409734a3ecbb78fb2"},
+    {file = "lxml-4.6.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:1b7584d421d254ab86d4f0b13ec662a9014397678a7c4265a02a6d7c2b18a75f"},
+    {file = "lxml-4.6.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:079f3ae844f38982d156efce585bc540c16a926d4436712cf4baee0cce487a3d"},
+    {file = "lxml-4.6.3-cp27-cp27m-win32.whl", hash = "sha256:bc4313cbeb0e7a416a488d72f9680fffffc645f8a838bd2193809881c67dd106"},
+    {file = "lxml-4.6.3-cp27-cp27m-win_amd64.whl", hash = "sha256:8157dadbb09a34a6bd95a50690595e1fa0af1a99445e2744110e3dca7831c4ee"},
+    {file = "lxml-4.6.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7728e05c35412ba36d3e9795ae8995e3c86958179c9770e65558ec3fdfd3724f"},
+    {file = "lxml-4.6.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:4bff24dfeea62f2e56f5bab929b4428ae6caba2d1eea0c2d6eb618e30a71e6d4"},
+    {file = "lxml-4.6.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:74f7d8d439b18fa4c385f3f5dfd11144bb87c1da034a466c5b5577d23a1d9b51"},
+    {file = "lxml-4.6.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f90ba11136bfdd25cae3951af8da2e95121c9b9b93727b1b896e3fa105b2f586"},
+    {file = "lxml-4.6.3-cp35-cp35m-win32.whl", hash = "sha256:f2380a6376dfa090227b663f9678150ef27543483055cc327555fb592c5967e2"},
+    {file = "lxml-4.6.3-cp35-cp35m-win_amd64.whl", hash = "sha256:c4f05c5a7c49d2fb70223d0d5bcfbe474cf928310ac9fa6a7c6dddc831d0b1d4"},
+    {file = "lxml-4.6.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d2e35d7bf1c1ac8c538f88d26b396e73dd81440d59c1ef8522e1ea77b345ede4"},
+    {file = "lxml-4.6.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:289e9ca1a9287f08daaf796d96e06cb2bc2958891d7911ac7cae1c5f9e1e0ee3"},
+    {file = "lxml-4.6.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bccbfc27563652de7dc9bdc595cb25e90b59c5f8e23e806ed0fd623755b6565d"},
+    {file = "lxml-4.6.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:820628b7b3135403540202e60551e741f9b6d3304371712521be939470b454ec"},
+    {file = "lxml-4.6.3-cp36-cp36m-win32.whl", hash = "sha256:5a0a14e264069c03e46f926be0d8919f4105c1623d620e7ec0e612a2e9bf1c04"},
+    {file = "lxml-4.6.3-cp36-cp36m-win_amd64.whl", hash = "sha256:92e821e43ad382332eade6812e298dc9701c75fe289f2a2d39c7960b43d1e92a"},
+    {file = "lxml-4.6.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:efd7a09678fd8b53117f6bae4fa3825e0a22b03ef0a932e070c0bdbb3a35e654"},
+    {file = "lxml-4.6.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:efac139c3f0bf4f0939f9375af4b02c5ad83a622de52d6dfa8e438e8e01d0eb0"},
+    {file = "lxml-4.6.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:0fbcf5565ac01dff87cbfc0ff323515c823081c5777a9fc7703ff58388c258c3"},
+    {file = "lxml-4.6.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:122fba10466c7bd4178b07dba427aa516286b846b2cbd6f6169141917283aae2"},
+    {file = "lxml-4.6.3-cp37-cp37m-win32.whl", hash = "sha256:3439c71103ef0e904ea0a1901611863e51f50b5cd5e8654a151740fde5e1cade"},
+    {file = "lxml-4.6.3-cp37-cp37m-win_amd64.whl", hash = "sha256:4289728b5e2000a4ad4ab8da6e1db2e093c63c08bdc0414799ee776a3f78da4b"},
+    {file = "lxml-4.6.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b007cbb845b28db4fb8b6a5cdcbf65bacb16a8bd328b53cbc0698688a68e1caa"},
+    {file = "lxml-4.6.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:76fa7b1362d19f8fbd3e75fe2fb7c79359b0af8747e6f7141c338f0bee2f871a"},
+    {file = "lxml-4.6.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:26e761ab5b07adf5f555ee82fb4bfc35bf93750499c6c7614bd64d12aaa67927"},
+    {file = "lxml-4.6.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:66e575c62792c3f9ca47cb8b6fab9e35bab91360c783d1606f758761810c9791"},
+    {file = "lxml-4.6.3-cp38-cp38-win32.whl", hash = "sha256:89b8b22a5ff72d89d48d0e62abb14340d9e99fd637d046c27b8b257a01ffbe28"},
+    {file = "lxml-4.6.3-cp38-cp38-win_amd64.whl", hash = "sha256:2a9d50e69aac3ebee695424f7dbd7b8c6d6eb7de2a2eb6b0f6c7db6aa41e02b7"},
+    {file = "lxml-4.6.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ce256aaa50f6cc9a649c51be3cd4ff142d67295bfc4f490c9134d0f9f6d58ef0"},
+    {file = "lxml-4.6.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:7610b8c31688f0b1be0ef882889817939490a36d0ee880ea562a4e1399c447a1"},
+    {file = "lxml-4.6.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f8380c03e45cf09f8557bdaa41e1fa7c81f3ae22828e1db470ab2a6c96d8bc23"},
+    {file = "lxml-4.6.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:884ab9b29feaca361f7f88d811b1eea9bfca36cf3da27768d28ad45c3ee6f969"},
+    {file = "lxml-4.6.3-cp39-cp39-win32.whl", hash = "sha256:33bb934a044cf32157c12bfcfbb6649807da20aa92c062ef51903415c704704f"},
+    {file = "lxml-4.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:542d454665a3e277f76954418124d67516c5f88e51a900365ed54a9806122b83"},
+    {file = "lxml-4.6.3.tar.gz", hash = "sha256:39b78571b3b30645ac77b95f7c69d1bffc4cf8c3b157c435a34da72e78c82468"},
+]
+makefun = [
+    {file = "makefun-1.11.3-py2.py3-none-any.whl", hash = "sha256:a19bddf07efb6bf92e3ccde5d593e49bc59001fd6c17cf7301d7a73a2647ae83"},
+    {file = "makefun-1.11.3.tar.gz", hash = "sha256:033eed65e2c1804fca84161a38d1fc8bb8650d32a89ac1c5dc7e54b2b2c2e88c"},
+]
+markupsafe = [
+    {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"},
+    {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"},
+    {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"},
+    {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"},
+    {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"},
+    {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"},
+    {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"},
+    {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"},
+    {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"},
+    {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"},
+    {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"},
+    {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"},
+    {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"},
+    {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"},
+    {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"},
+    {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"},
+    {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"},
+    {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"},
+    {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5"},
+    {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"},
+    {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"},
+    {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f"},
+    {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0"},
+    {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7"},
+    {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"},
+    {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"},
+    {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"},
+    {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193"},
+    {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"},
+    {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"},
+    {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1"},
+    {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1"},
+    {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f"},
+    {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"},
+    {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"},
+    {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"},
+    {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"},
+    {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"},
+    {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2"},
+    {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032"},
+    {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b"},
+    {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"},
+    {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"},
+    {file = "MarkupSafe-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c"},
+    {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb"},
+    {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014"},
+    {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850"},
+    {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85"},
+    {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"},
+    {file = "MarkupSafe-1.1.1-cp39-cp39-win32.whl", hash = "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39"},
+    {file = "MarkupSafe-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8"},
+    {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"},
+]
+mccabe = [
+    {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
+    {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
+]
+mock = [
+    {file = "mock-4.0.3-py3-none-any.whl", hash = "sha256:122fcb64ee37cfad5b3f48d7a7d51875d7031aaf3d8be7c42e2bee25044eee62"},
+    {file = "mock-4.0.3.tar.gz", hash = "sha256:7d3fbbde18228f4ff2f1f119a45cdffa458b4c0dee32eb4d2bb2f82554bac7bc"},
+]
+more-itertools = [
+    {file = "more-itertools-8.10.0.tar.gz", hash = "sha256:1debcabeb1df793814859d64a81ad7cb10504c24349368ccf214c664c474f41f"},
+    {file = "more_itertools-8.10.0-py3-none-any.whl", hash = "sha256:56ddac45541718ba332db05f464bebfb0768110111affd27f66e0051f276fa43"},
+]
+moto = [
+    {file = "moto-1.3.16.dev122-py2.py3-none-any.whl", hash = "sha256:db740d63dd6c28c594e65b5e6183a04ed31c6fa38b1c5a893478ba7738254771"},
+    {file = "moto-1.3.16.dev122.tar.gz", hash = "sha256:3d57adf671b4bef551ef5bb404c30faeee3a0a5a1d4ff883a1a0460372ffbc19"},
+]
+msrest = [
+    {file = "msrest-0.6.21-py2.py3-none-any.whl", hash = "sha256:c840511c845330e96886011a236440fafc2c9aff7b2df9c0a92041ee2dee3782"},
+    {file = "msrest-0.6.21.tar.gz", hash = "sha256:72661bc7bedc2dc2040e8f170b6e9ef226ee6d3892e01affd4d26b06474d68d8"},
+]
+multidict = [
+    {file = "multidict-5.1.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:b7993704f1a4b204e71debe6095150d43b2ee6150fa4f44d6d966ec356a8d61f"},
+    {file = "multidict-5.1.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:9dd6e9b1a913d096ac95d0399bd737e00f2af1e1594a787e00f7975778c8b2bf"},
+    {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281"},
+    {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:1ab820665e67373de5802acae069a6a05567ae234ddb129f31d290fc3d1aa56d"},
+    {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:9436dc58c123f07b230383083855593550c4d301d2532045a17ccf6eca505f6d"},
+    {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:830f57206cc96ed0ccf68304141fec9481a096c4d2e2831f311bde1c404401da"},
+    {file = "multidict-5.1.0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:2e68965192c4ea61fff1b81c14ff712fc7dc15d2bd120602e4a3494ea6584224"},
+    {file = "multidict-5.1.0-cp36-cp36m-win32.whl", hash = "sha256:2f1a132f1c88724674271d636e6b7351477c27722f2ed789f719f9e3545a3d26"},
+    {file = "multidict-5.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:3a4f32116f8f72ecf2a29dabfb27b23ab7cdc0ba807e8459e59a93a9be9506f6"},
+    {file = "multidict-5.1.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:46c73e09ad374a6d876c599f2328161bcd95e280f84d2060cf57991dec5cfe76"},
+    {file = "multidict-5.1.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:018132dbd8688c7a69ad89c4a3f39ea2f9f33302ebe567a879da8f4ca73f0d0a"},
+    {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:4b186eb7d6ae7c06eb4392411189469e6a820da81447f46c0072a41c748ab73f"},
+    {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:3a041b76d13706b7fff23b9fc83117c7b8fe8d5fe9e6be45eee72b9baa75f348"},
+    {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:051012ccee979b2b06be928a6150d237aec75dd6bf2d1eeeb190baf2b05abc93"},
+    {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:6a4d5ce640e37b0efcc8441caeea8f43a06addace2335bd11151bc02d2ee31f9"},
+    {file = "multidict-5.1.0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:5cf3443199b83ed9e955f511b5b241fd3ae004e3cb81c58ec10f4fe47c7dce37"},
+    {file = "multidict-5.1.0-cp37-cp37m-win32.whl", hash = "sha256:f200755768dc19c6f4e2b672421e0ebb3dd54c38d5a4f262b872d8cfcc9e93b5"},
+    {file = "multidict-5.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:05c20b68e512166fddba59a918773ba002fdd77800cad9f55b59790030bab632"},
+    {file = "multidict-5.1.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:54fd1e83a184e19c598d5e70ba508196fd0bbdd676ce159feb412a4a6664f952"},
+    {file = "multidict-5.1.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:0e3c84e6c67eba89c2dbcee08504ba8644ab4284863452450520dad8f1e89b79"},
+    {file = "multidict-5.1.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:dc862056f76443a0db4509116c5cd480fe1b6a2d45512a653f9a855cc0517456"},
+    {file = "multidict-5.1.0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:0e929169f9c090dae0646a011c8b058e5e5fb391466016b39d21745b48817fd7"},
+    {file = "multidict-5.1.0-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:d81eddcb12d608cc08081fa88d046c78afb1bf8107e6feab5d43503fea74a635"},
+    {file = "multidict-5.1.0-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:585fd452dd7782130d112f7ddf3473ffdd521414674c33876187e101b588738a"},
+    {file = "multidict-5.1.0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:37e5438e1c78931df5d3c0c78ae049092877e5e9c02dd1ff5abb9cf27a5914ea"},
+    {file = "multidict-5.1.0-cp38-cp38-win32.whl", hash = "sha256:07b42215124aedecc6083f1ce6b7e5ec5b50047afa701f3442054373a6deb656"},
+    {file = "multidict-5.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:929006d3c2d923788ba153ad0de8ed2e5ed39fdbe8e7be21e2f22ed06c6783d3"},
+    {file = "multidict-5.1.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:b797515be8743b771aa868f83563f789bbd4b236659ba52243b735d80b29ed93"},
+    {file = "multidict-5.1.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d5c65bdf4484872c4af3150aeebe101ba560dcfb34488d9a8ff8dbcd21079647"},
+    {file = "multidict-5.1.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b47a43177a5e65b771b80db71e7be76c0ba23cc8aa73eeeb089ed5219cdbe27d"},
+    {file = "multidict-5.1.0-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:806068d4f86cb06af37cd65821554f98240a19ce646d3cd24e1c33587f313eb8"},
+    {file = "multidict-5.1.0-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:46dd362c2f045095c920162e9307de5ffd0a1bfbba0a6e990b344366f55a30c1"},
+    {file = "multidict-5.1.0-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:ace010325c787c378afd7f7c1ac66b26313b3344628652eacd149bdd23c68841"},
+    {file = "multidict-5.1.0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:ecc771ab628ea281517e24fd2c52e8f31c41e66652d07599ad8818abaad38cda"},
+    {file = "multidict-5.1.0-cp39-cp39-win32.whl", hash = "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80"},
+    {file = "multidict-5.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:7df80d07818b385f3129180369079bd6934cf70469f99daaebfac89dca288359"},
+    {file = "multidict-5.1.0.tar.gz", hash = "sha256:25b4e5f22d3a37ddf3effc0710ba692cfc792c2b9edfb9c05aefe823256e84d5"},
+]
+mypy = [
+    {file = "mypy-0.910-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457"},
+    {file = "mypy-0.910-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb"},
+    {file = "mypy-0.910-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:088cd9c7904b4ad80bec811053272986611b84221835e079be5bcad029e79dd9"},
+    {file = "mypy-0.910-cp35-cp35m-win_amd64.whl", hash = "sha256:adaeee09bfde366d2c13fe6093a7df5df83c9a2ba98638c7d76b010694db760e"},
+    {file = "mypy-0.910-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ecd2c3fe726758037234c93df7e98deb257fd15c24c9180dacf1ef829da5f921"},
+    {file = "mypy-0.910-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d9dd839eb0dc1bbe866a288ba3c1afc33a202015d2ad83b31e875b5905a079b6"},
+    {file = "mypy-0.910-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:3e382b29f8e0ccf19a2df2b29a167591245df90c0b5a2542249873b5c1d78212"},
+    {file = "mypy-0.910-cp36-cp36m-win_amd64.whl", hash = "sha256:53fd2eb27a8ee2892614370896956af2ff61254c275aaee4c230ae771cadd885"},
+    {file = "mypy-0.910-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b6fb13123aeef4a3abbcfd7e71773ff3ff1526a7d3dc538f3929a49b42be03f0"},
+    {file = "mypy-0.910-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e4dab234478e3bd3ce83bac4193b2ecd9cf94e720ddd95ce69840273bf44f6de"},
+    {file = "mypy-0.910-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:7df1ead20c81371ccd6091fa3e2878559b5c4d4caadaf1a484cf88d93ca06703"},
+    {file = "mypy-0.910-cp37-cp37m-win_amd64.whl", hash = "sha256:0aadfb2d3935988ec3815952e44058a3100499f5be5b28c34ac9d79f002a4a9a"},
+    {file = "mypy-0.910-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ec4e0cd079db280b6bdabdc807047ff3e199f334050db5cbb91ba3e959a67504"},
+    {file = "mypy-0.910-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:119bed3832d961f3a880787bf621634ba042cb8dc850a7429f643508eeac97b9"},
+    {file = "mypy-0.910-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:866c41f28cee548475f146aa4d39a51cf3b6a84246969f3759cb3e9c742fc072"},
+    {file = "mypy-0.910-cp38-cp38-win_amd64.whl", hash = "sha256:ceb6e0a6e27fb364fb3853389607cf7eb3a126ad335790fa1e14ed02fba50811"},
+    {file = "mypy-0.910-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a85e280d4d217150ce8cb1a6dddffd14e753a4e0c3cf90baabb32cefa41b59e"},
+    {file = "mypy-0.910-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:42c266ced41b65ed40a282c575705325fa7991af370036d3f134518336636f5b"},
+    {file = "mypy-0.910-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:3c4b8ca36877fc75339253721f69603a9c7fdb5d4d5a95a1a1b899d8b86a4de2"},
+    {file = "mypy-0.910-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:c0df2d30ed496a08de5daed2a9ea807d07c21ae0ab23acf541ab88c24b26ab97"},
+    {file = "mypy-0.910-cp39-cp39-win_amd64.whl", hash = "sha256:c6c2602dffb74867498f86e6129fd52a2770c48b7cd3ece77ada4fa38f94eba8"},
+    {file = "mypy-0.910-py3-none-any.whl", hash = "sha256:ef565033fa5a958e62796867b1df10c40263ea9ded87164d67572834e57a174d"},
+    {file = "mypy-0.910.tar.gz", hash = "sha256:704098302473cb31a218f1775a873b376b30b4c18229421e9e9dc8916fd16150"},
+]
+mypy-extensions = [
+    {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
+    {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
+]
+nltk = [
+    {file = "nltk-3.5.zip", hash = "sha256:845365449cd8c5f9731f7cb9f8bd6fd0767553b9d53af9eb1b3abf7700936b35"},
+]
+numpy = [
+    {file = "numpy-1.21.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:38e8648f9449a549a7dfe8d8755a5979b45b3538520d1e735637ef28e8c2dc50"},
+    {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fd7d7409fa643a91d0a05c7554dd68aa9c9bb16e186f6ccfe40d6e003156e33a"},
+    {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a75b4498b1e93d8b700282dc8e655b8bd559c0904b3910b144646dbbbc03e062"},
+    {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1412aa0aec3e00bc23fbb8664d76552b4efde98fb71f60737c83efbac24112f1"},
+    {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e46ceaff65609b5399163de5893d8f2a82d3c77d5e56d976c8b5fb01faa6b671"},
+    {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c6a2324085dd52f96498419ba95b5777e40b6bcbc20088fddb9e8cbb58885e8e"},
+    {file = "numpy-1.21.1-cp37-cp37m-win32.whl", hash = "sha256:73101b2a1fef16602696d133db402a7e7586654682244344b8329cdcbbb82172"},
+    {file = "numpy-1.21.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7a708a79c9a9d26904d1cca8d383bf869edf6f8e7650d85dbc77b041e8c5a0f8"},
+    {file = "numpy-1.21.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95b995d0c413f5d0428b3f880e8fe1660ff9396dcd1f9eedbc311f37b5652e16"},
+    {file = "numpy-1.21.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:635e6bd31c9fb3d475c8f44a089569070d10a9ef18ed13738b03049280281267"},
+    {file = "numpy-1.21.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a3d5fb89bfe21be2ef47c0614b9c9c707b7362386c9a3ff1feae63e0267ccb6"},
+    {file = "numpy-1.21.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8a326af80e86d0e9ce92bcc1e65c8ff88297de4fa14ee936cb2293d414c9ec63"},
+    {file = "numpy-1.21.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:791492091744b0fe390a6ce85cc1bf5149968ac7d5f0477288f78c89b385d9af"},
+    {file = "numpy-1.21.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0318c465786c1f63ac05d7c4dbcecd4d2d7e13f0959b01b534ea1e92202235c5"},
+    {file = "numpy-1.21.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a513bd9c1551894ee3d31369f9b07460ef223694098cf27d399513415855b68"},
+    {file = "numpy-1.21.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:91c6f5fc58df1e0a3cc0c3a717bb3308ff850abdaa6d2d802573ee2b11f674a8"},
+    {file = "numpy-1.21.1-cp38-cp38-win32.whl", hash = "sha256:978010b68e17150db8765355d1ccdd450f9fc916824e8c4e35ee620590e234cd"},
+    {file = "numpy-1.21.1-cp38-cp38-win_amd64.whl", hash = "sha256:9749a40a5b22333467f02fe11edc98f022133ee1bfa8ab99bda5e5437b831214"},
+    {file = "numpy-1.21.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d7a4aeac3b94af92a9373d6e77b37691b86411f9745190d2c351f410ab3a791f"},
+    {file = "numpy-1.21.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d9e7912a56108aba9b31df688a4c4f5cb0d9d3787386b87d504762b6754fbb1b"},
+    {file = "numpy-1.21.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:25b40b98ebdd272bc3020935427a4530b7d60dfbe1ab9381a39147834e985eac"},
+    {file = "numpy-1.21.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8a92c5aea763d14ba9d6475803fc7904bda7decc2a0a68153f587ad82941fec1"},
+    {file = "numpy-1.21.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05a0f648eb28bae4bcb204e6fd14603de2908de982e761a2fc78efe0f19e96e1"},
+    {file = "numpy-1.21.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f01f28075a92eede918b965e86e8f0ba7b7797a95aa8d35e1cc8821f5fc3ad6a"},
+    {file = "numpy-1.21.1-cp39-cp39-win32.whl", hash = "sha256:88c0b89ad1cc24a5efbb99ff9ab5db0f9a86e9cc50240177a571fbe9c2860ac2"},
+    {file = "numpy-1.21.1-cp39-cp39-win_amd64.whl", hash = "sha256:01721eefe70544d548425a07c80be8377096a54118070b8a62476866d5208e33"},
+    {file = "numpy-1.21.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2d4d1de6e6fb3d28781c73fbde702ac97f03d79e4ffd6598b880b2d95d62ead4"},
+    {file = "numpy-1.21.1.zip", hash = "sha256:dff4af63638afcc57a3dfb9e4b26d434a7a602d225b42d746ea7fe2edf1342fd"},
+]
+oauthlib = [
+    {file = "oauthlib-3.1.1-py2.py3-none-any.whl", hash = "sha256:42bf6354c2ed8c6acb54d971fce6f88193d97297e18602a3a886603f9d7730cc"},
+    {file = "oauthlib-3.1.1.tar.gz", hash = "sha256:8f0215fcc533dd8dd1bee6f4c412d4f0cd7297307d43ac61666389e3bc3198a3"},
+]
+openapi-codec = [
+    {file = "openapi-codec-1.3.2.tar.gz", hash = "sha256:1bce63289edf53c601ea3683120641407ff6b708803b8954c8a876fe778d2145"},
+]
+openpyxl = [
+    {file = "openpyxl-3.0.9-py2.py3-none-any.whl", hash = "sha256:8f3b11bd896a95468a4ab162fc4fcd260d46157155d1f8bfaabb99d88cfcf79f"},
+    {file = "openpyxl-3.0.9.tar.gz", hash = "sha256:40f568b9829bf9e446acfffce30250ac1fa39035124d55fc024025c41481c90f"},
+]
+ordered-set = [
+    {file = "ordered-set-4.0.2.tar.gz", hash = "sha256:ba93b2df055bca202116ec44b9bead3df33ea63a7d5827ff8e16738b97f33a95"},
+]
+packaging = [
+    {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"},
+    {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"},
+]
+pandas = [
+    {file = "pandas-1.3.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68408a39a54ebadb9014ee5a4fae27b2fe524317bc80adf56c9ac59e8f8ea431"},
+    {file = "pandas-1.3.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86b16b1b920c4cb27fdd65a2c20258bcd9c794be491290660722bb0ea765054d"},
+    {file = "pandas-1.3.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:37d63e78e87eb3791da7be4100a65da0383670c2b59e493d9e73098d7a879226"},
+    {file = "pandas-1.3.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53e2fb11f86f6253bb1df26e3aeab3bf2e000aaa32a953ec394571bec5dc6fd6"},
+    {file = "pandas-1.3.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7326b37de08d42dd3fff5b7ef7691d0fd0bf2428f4ba5a2bdc3b3247e9a52e4c"},
+    {file = "pandas-1.3.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed2f29b4da6f6ae7c68f4b3708d9d9e59fa89b2f9e87c2b64ce055cbd39f729e"},
+    {file = "pandas-1.3.3-cp37-cp37m-win32.whl", hash = "sha256:3f5020613c1d8e304840c34aeb171377dc755521bf5e69804991030c2a48aec3"},
+    {file = "pandas-1.3.3-cp37-cp37m-win_amd64.whl", hash = "sha256:c399200631db9bd9335d013ec7fce4edb98651035c249d532945c78ad453f23a"},
+    {file = "pandas-1.3.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a800df4e101b721e94d04c355e611863cc31887f24c0b019572e26518cbbcab6"},
+    {file = "pandas-1.3.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3334a5a9eeaca953b9db1b2b165dcdc5180b5011f3bec3a57a3580c9c22eae68"},
+    {file = "pandas-1.3.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49fd2889d8116d7acef0709e4c82b8560a8b22b0f77471391d12c27596e90267"},
+    {file = "pandas-1.3.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7557b39c8e86eb0543a17a002ac1ea0f38911c3c17095bc9350d0a65b32d801c"},
+    {file = "pandas-1.3.3-cp38-cp38-win32.whl", hash = "sha256:629138b7cf81a2e55aa29ce7b04c1cece20485271d1f6c469c6a0c03857db6a4"},
+    {file = "pandas-1.3.3-cp38-cp38-win_amd64.whl", hash = "sha256:45649503e167d45360aa7c52f18d1591a6d5c70d2f3a26bc90a3297a30ce9a66"},
+    {file = "pandas-1.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ebbed7312547a924df0cbe133ff1250eeb94cdff3c09a794dc991c5621c8c735"},
+    {file = "pandas-1.3.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9f1b54d7efc9df05320b14a48fb18686f781aa66cc7b47bb62fabfc67a0985c"},
+    {file = "pandas-1.3.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9bc59855598cb57f68fdabd4897d3ed2bc3a3b3bef7b868a0153c4cd03f3207"},
+    {file = "pandas-1.3.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4def2ef2fb7fcd62f2aa51bacb817ee9029e5c8efe42fe527ba21f6a3ddf1a9f"},
+    {file = "pandas-1.3.3-cp39-cp39-win32.whl", hash = "sha256:f7d84f321674c2f0f31887ee6d5755c54ca1ea5e144d6d54b3bbf566dd9ea0cc"},
+    {file = "pandas-1.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:e574c2637c9d27f322e911650b36e858c885702c5996eda8a5a60e35e6648cf2"},
+    {file = "pandas-1.3.3.tar.gz", hash = "sha256:272c8cb14aa9793eada6b1ebe81994616e647b5892a370c7135efb2924b701df"},
+]
+pandas-stubs = [
+    {file = "pandas-stubs-1.2.0.17.tar.gz", hash = "sha256:125597f4e3f2a0addf2941ec40170527558a3b593bb89e4844a73f84159d8e10"},
+    {file = "pandas_stubs-1.2.0.17-py3-none-any.whl", hash = "sha256:7bf94305b8e41c88052c35bec86885cba96d2163c02b26f25bc0aeef2e6b5d6f"},
+]
+pathspec = [
+    {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"},
+    {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"},
+]
+pillow = [
+    {file = "Pillow-7.2.0-cp35-cp35m-macosx_10_10_intel.whl", hash = "sha256:1ca594126d3c4def54babee699c055a913efb01e106c309fa6b04405d474d5ae"},
+    {file = "Pillow-7.2.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c92302a33138409e8f1ad16731568c55c9053eee71bb05b6b744067e1b62380f"},
+    {file = "Pillow-7.2.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:8dad18b69f710bf3a001d2bf3afab7c432785d94fcf819c16b5207b1cfd17d38"},
+    {file = "Pillow-7.2.0-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:431b15cffbf949e89df2f7b48528be18b78bfa5177cb3036284a5508159492b5"},
+    {file = "Pillow-7.2.0-cp35-cp35m-win32.whl", hash = "sha256:09d7f9e64289cb40c2c8d7ad674b2ed6105f55dc3b09aa8e4918e20a0311e7ad"},
+    {file = "Pillow-7.2.0-cp35-cp35m-win_amd64.whl", hash = "sha256:0295442429645fa16d05bd567ef5cff178482439c9aad0411d3f0ce9b88b3a6f"},
+    {file = "Pillow-7.2.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:ec29604081f10f16a7aea809ad42e27764188fc258b02259a03a8ff7ded3808d"},
+    {file = "Pillow-7.2.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:612cfda94e9c8346f239bf1a4b082fdd5c8143cf82d685ba2dba76e7adeeb233"},
+    {file = "Pillow-7.2.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0a80dd307a5d8440b0a08bd7b81617e04d870e40a3e46a32d9c246e54705e86f"},
+    {file = "Pillow-7.2.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:06aba4169e78c439d528fdeb34762c3b61a70813527a2c57f0540541e9f433a8"},
+    {file = "Pillow-7.2.0-cp36-cp36m-win32.whl", hash = "sha256:f7e30c27477dffc3e85c2463b3e649f751789e0f6c8456099eea7ddd53be4a8a"},
+    {file = "Pillow-7.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:ffe538682dc19cc542ae7c3e504fdf54ca7f86fb8a135e59dd6bc8627eae6cce"},
+    {file = "Pillow-7.2.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:94cf49723928eb6070a892cb39d6c156f7b5a2db4e8971cb958f7b6b104fb4c4"},
+    {file = "Pillow-7.2.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6edb5446f44d901e8683ffb25ebdfc26988ee813da3bf91e12252b57ac163727"},
+    {file = "Pillow-7.2.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:52125833b070791fcb5710fabc640fc1df07d087fc0c0f02d3661f76c23c5b8b"},
+    {file = "Pillow-7.2.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:9ad7f865eebde135d526bb3163d0b23ffff365cf87e767c649550964ad72785d"},
+    {file = "Pillow-7.2.0-cp37-cp37m-win32.whl", hash = "sha256:c79f9c5fb846285f943aafeafda3358992d64f0ef58566e23484132ecd8d7d63"},
+    {file = "Pillow-7.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d350f0f2c2421e65fbc62690f26b59b0bcda1b614beb318c81e38647e0f673a1"},
+    {file = "Pillow-7.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:6d7741e65835716ceea0fd13a7d0192961212fd59e741a46bbed7a473c634ed6"},
+    {file = "Pillow-7.2.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:edf31f1150778abd4322444c393ab9c7bd2af271dd4dafb4208fb613b1f3cdc9"},
+    {file = "Pillow-7.2.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:d08b23fdb388c0715990cbc06866db554e1822c4bdcf6d4166cf30ac82df8c41"},
+    {file = "Pillow-7.2.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:5e51ee2b8114def244384eda1c82b10e307ad9778dac5c83fb0943775a653cd8"},
+    {file = "Pillow-7.2.0-cp38-cp38-win32.whl", hash = "sha256:725aa6cfc66ce2857d585f06e9519a1cc0ef6d13f186ff3447ab6dff0a09bc7f"},
+    {file = "Pillow-7.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:a060cf8aa332052df2158e5a119303965be92c3da6f2d93b6878f0ebca80b2f6"},
+    {file = "Pillow-7.2.0-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:9c87ef410a58dd54b92424ffd7e28fd2ec65d2f7fc02b76f5e9b2067e355ebf6"},
+    {file = "Pillow-7.2.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:e901964262a56d9ea3c2693df68bc9860b8bdda2b04768821e4c44ae797de117"},
+    {file = "Pillow-7.2.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:25930fadde8019f374400f7986e8404c8b781ce519da27792cbe46eabec00c4d"},
+    {file = "Pillow-7.2.0.tar.gz", hash = "sha256:97f9e7953a77d5a70f49b9a48da7776dc51e9b738151b22dacf101641594a626"},
+]
+platformdirs = [
+    {file = "platformdirs-2.3.0-py3-none-any.whl", hash = "sha256:8003ac87717ae2c7ee1ea5a84a1a61e87f3fbd16eb5aadba194ea30a9019f648"},
+    {file = "platformdirs-2.3.0.tar.gz", hash = "sha256:15b056538719b1c94bdaccb29e5f81879c7f7f0f4a153f46086d155dffcd4f0f"},
+]
+plotly = [
+    {file = "plotly-5.3.1-py2.py3-none-any.whl", hash = "sha256:c3da90d41cab36722048eaa5d0eee7a286a55982835980e5f183e5536b823f07"},
+    {file = "plotly-5.3.1.tar.gz", hash = "sha256:6598393e898a9c5ae78397f76f07002ec41fd92e5f746d3b9806248d53885643"},
+]
+pluggy = [
+    {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
+    {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
+]
+protobuf = [
+    {file = "protobuf-3.18.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9072cb18fca8998b77f969fb74d25a11d7f4a39a8b1ddc3cf76cd5abda8499cb"},
+    {file = "protobuf-3.18.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f589346b5b3f702c1d30e2343c9897e6c35e7bd495c10a0e17d11ecb5ee5bd06"},
+    {file = "protobuf-3.18.0-cp36-cp36m-win32.whl", hash = "sha256:93c077fd83879cf48f327a2491c24da447a09da6a7ab3cc311a6f5a61fcb5de0"},
+    {file = "protobuf-3.18.0-cp36-cp36m-win_amd64.whl", hash = "sha256:3b5b81bb665aac548b413480f4e0d8c38a74bc4dea57835f288a3ce74f63dfe9"},
+    {file = "protobuf-3.18.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d11465040cadcea8ecf5f0b131af5099a9696f9d0bef6f88148b372bacc1c52d"},
+    {file = "protobuf-3.18.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:f6138462643adce0ed6e49007a63b7fd7dc4fda1ef4e15a70fcebe76c1407a71"},
+    {file = "protobuf-3.18.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:877664b1b8d1e23553634f625e4e12aae4ff16cbbef473f8118c239d478f422a"},
+    {file = "protobuf-3.18.0-cp37-cp37m-win32.whl", hash = "sha256:5201333b7aa711965c5769b250f8565a9924e8e27f8b622bbc5e6847aeaab1b1"},
+    {file = "protobuf-3.18.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f3ecec3038c2fb4dad952d3d6cb9ca301999903a09e43794fb348da48f7577f"},
+    {file = "protobuf-3.18.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:17181fc0814655812aac108e755bd5185d71aa8d81bd241cec6e232c84097918"},
+    {file = "protobuf-3.18.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:7646c20605fbee57e77fdbc4a90175538281b152f46ba17019916593f8062c2a"},
+    {file = "protobuf-3.18.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80b0a5157f3a53043daf8eb7cfa1220b27a5a63dd6655dbd8e1e6f7b5dcd6347"},
+    {file = "protobuf-3.18.0-cp38-cp38-win32.whl", hash = "sha256:5730de255c95b3403eedd1a568eb28203b913b6192ff5a3fdc3ff30f37107a38"},
+    {file = "protobuf-3.18.0-cp38-cp38-win_amd64.whl", hash = "sha256:9147565f93e6699d7512747766598afe63205f226ac7b61f47954974c9aab852"},
+    {file = "protobuf-3.18.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:568c049ff002a7523ed33fb612e6b97da002bf87ffb619a1fc3eadf2257a3b31"},
+    {file = "protobuf-3.18.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:7e791a94db391ae22b3943fc88f6ba0e1f62b6ad58b33db7517df576c7834d23"},
+    {file = "protobuf-3.18.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42c04e66ec5a38ad2171639dc9860c2f9594668f709ea3a4a192acf7346853a7"},
+    {file = "protobuf-3.18.0-cp39-cp39-win32.whl", hash = "sha256:0a59ea8da307118372750e2fdfe0961622e675b8dd35e05c42384d618189a938"},
+    {file = "protobuf-3.18.0-cp39-cp39-win_amd64.whl", hash = "sha256:f7c8193ec805324ff6024242b00f64a24b94d56b895f62bf28a9d72a228d4fca"},
+    {file = "protobuf-3.18.0-py2.py3-none-any.whl", hash = "sha256:615099e52e9fbc9fde00177267a94ca820ecf4e80093e390753568b7d8cb3c1a"},
+    {file = "protobuf-3.18.0.tar.gz", hash = "sha256:18b308946a592e245299391e53c01b5b8efc2794f49986e80f37d7b5e60a270f"},
+]
+psycopg2-binary = [
+    {file = "psycopg2-binary-2.8.4.tar.gz", hash = "sha256:3a2522b1d9178575acee4adf8fd9f979f9c0449b00b4164bb63c3475ea6528ed"},
+    {file = "psycopg2_binary-2.8.4-cp27-cp27m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:890167d5091279a27e2505ff0e1fb273f8c48c41d35c5b92adbf4af80e6b2ed6"},
+    {file = "psycopg2_binary-2.8.4-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:dbc5cd56fff1a6152ca59445178652756f4e509f672e49ccdf3d79c1043113a4"},
+    {file = "psycopg2_binary-2.8.4-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:7f42a8490c4fe854325504ce7a6e4796b207960dabb2cbafe3c3959cb00d1d7e"},
+    {file = "psycopg2_binary-2.8.4-cp27-cp27m-win32.whl", hash = "sha256:8578d6b8192e4c805e85f187bc530d0f52ba86c39172e61cd51f68fddd648103"},
+    {file = "psycopg2_binary-2.8.4-cp27-cp27m-win_amd64.whl", hash = "sha256:5dd90c5438b4f935c9d01fcbad3620253da89d19c1f5fca9158646407ed7df35"},
+    {file = "psycopg2_binary-2.8.4-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:9aadff9032e967865f9778485571e93908d27dab21d0fdfdec0ca779bb6f8ad9"},
+    {file = "psycopg2_binary-2.8.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:659c815b5b8e2a55193ede2795c1e2349b8011497310bb936da7d4745652823b"},
+    {file = "psycopg2_binary-2.8.4-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:2166e770cb98f02ed5ee2b0b569d40db26788e0bf2ec3ae1a0d864ea6f1d8309"},
+    {file = "psycopg2_binary-2.8.4-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:7e6e3c52e6732c219c07bd97fff6c088f8df4dae3b79752ee3a817e6f32e177e"},
+    {file = "psycopg2_binary-2.8.4-cp34-cp34m-win32.whl", hash = "sha256:040234f8a4a8dfd692662a8308d78f63f31a97e1c42d2480e5e6810c48966a29"},
+    {file = "psycopg2_binary-2.8.4-cp34-cp34m-win_amd64.whl", hash = "sha256:69b13fdf12878b10dc6003acc8d0abf3ad93e79813fd5f3812497c1c9fb9be49"},
+    {file = "psycopg2_binary-2.8.4-cp35-cp35m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:19dc39616850342a2a6db70559af55b22955f86667b5f652f40c0e99253d9881"},
+    {file = "psycopg2_binary-2.8.4-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:9f24f383a298a0c0f9b3113b982e21751a8ecde6615494a3f1470eb4a9d70e9e"},
+    {file = "psycopg2_binary-2.8.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:eaed1c65f461a959284649e37b5051224f4db6ebdc84e40b5e65f2986f101a08"},
+    {file = "psycopg2_binary-2.8.4-cp35-cp35m-win32.whl", hash = "sha256:4c6717962247445b4f9e21c962ea61d2e884fc17df5ddf5e35863b016f8a1f03"},
+    {file = "psycopg2_binary-2.8.4-cp35-cp35m-win_amd64.whl", hash = "sha256:84156313f258eafff716b2961644a4483a9be44a5d43551d554844d15d4d224e"},
+    {file = "psycopg2_binary-2.8.4-cp36-cp36m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:3b5deaa3ee7180585a296af33e14c9b18c218d148e735c7accf78130765a47e3"},
+    {file = "psycopg2_binary-2.8.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5057669b6a66aa9ca118a2a860159f0ee3acf837eda937bdd2a64f3431361a2d"},
+    {file = "psycopg2_binary-2.8.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:afd96845e12638d2c44d213d4810a08f4dc4a563f9a98204b7428e567014b1cd"},
+    {file = "psycopg2_binary-2.8.4-cp36-cp36m-win32.whl", hash = "sha256:a73021b44813b5c84eda4a3af5826dd72356a900bac9bd9dd1f0f81ee1c22c2f"},
+    {file = "psycopg2_binary-2.8.4-cp36-cp36m-win_amd64.whl", hash = "sha256:407af6d7e46593415f216c7f56ba087a9a42bd6dc2ecb86028760aa45b802bd7"},
+    {file = "psycopg2_binary-2.8.4-cp37-cp37m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:3aa773580f85a28ffdf6f862e59cb5a3cc7ef6885121f2de3fca8d6ada4dbf3b"},
+    {file = "psycopg2_binary-2.8.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:eac8a3499754790187bb00574ab980df13e754777d346f85e0ff6df929bcd964"},
+    {file = "psycopg2_binary-2.8.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:7a1cb80e35e1ccea3e11a48afe65d38744a0e0bde88795cc56a4d05b6e4f9d70"},
+    {file = "psycopg2_binary-2.8.4-cp37-cp37m-win32.whl", hash = "sha256:086f7e89ec85a6704db51f68f0dcae432eff9300809723a6e8782c41c2f48e03"},
+    {file = "psycopg2_binary-2.8.4-cp37-cp37m-win_amd64.whl", hash = "sha256:b73ddf033d8cd4cc9dfed6324b1ad2a89ba52c410ef6877998422fcb9c23e3a8"},
+    {file = "psycopg2_binary-2.8.4-cp38-cp38-macosx_10_9_x86_64.macosx_10_9_intel.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:4c3c09fb674401f630626310bcaf6cd6285daf0d5e4c26d6e55ca26a2734e39b"},
+    {file = "psycopg2_binary-2.8.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:18ca813fdb17bc1db73fe61b196b05dd1ca2165b884dd5ec5568877cabf9b039"},
+    {file = "psycopg2_binary-2.8.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:50446fae5681fc99f87e505d4e77c9407e683ab60c555ec302f9ac9bffa61103"},
+    {file = "psycopg2_binary-2.8.4-cp38-cp38-win32.whl", hash = "sha256:98e10634792ac0e9e7a92a76b4991b44c2325d3e7798270a808407355e7bb0a1"},
+    {file = "psycopg2_binary-2.8.4-cp38-cp38-win_amd64.whl", hash = "sha256:b8f490f5fad1767a1331df1259763b3bad7d7af12a75b950c2843ba319b2415f"},
+]
+py = [
+    {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"},
+    {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"},
+]
+pyasn1 = [
+    {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"},
+    {file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"},
+    {file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"},
+    {file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"},
+    {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"},
+    {file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"},
+    {file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"},
+    {file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"},
+    {file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"},
+    {file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"},
+    {file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"},
+    {file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"},
+    {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"},
+]
+pyasn1-modules = [
+    {file = "pyasn1-modules-0.2.8.tar.gz", hash = "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e"},
+    {file = "pyasn1_modules-0.2.8-py2.4.egg", hash = "sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199"},
+    {file = "pyasn1_modules-0.2.8-py2.5.egg", hash = "sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405"},
+    {file = "pyasn1_modules-0.2.8-py2.6.egg", hash = "sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb"},
+    {file = "pyasn1_modules-0.2.8-py2.7.egg", hash = "sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8"},
+    {file = "pyasn1_modules-0.2.8-py2.py3-none-any.whl", hash = "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74"},
+    {file = "pyasn1_modules-0.2.8-py3.1.egg", hash = "sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d"},
+    {file = "pyasn1_modules-0.2.8-py3.2.egg", hash = "sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45"},
+    {file = "pyasn1_modules-0.2.8-py3.3.egg", hash = "sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4"},
+    {file = "pyasn1_modules-0.2.8-py3.4.egg", hash = "sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811"},
+    {file = "pyasn1_modules-0.2.8-py3.5.egg", hash = "sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed"},
+    {file = "pyasn1_modules-0.2.8-py3.6.egg", hash = "sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0"},
+    {file = "pyasn1_modules-0.2.8-py3.7.egg", hash = "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd"},
+]
+pycodestyle = [
+    {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"},
+    {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"},
+]
+pycparser = [
+    {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"},
+    {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"},
+]
+pydantic = [
+    {file = "pydantic-1.7.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c59ea046aea25be14dc22d69c97bee629e6d48d2b2ecb724d7fe8806bf5f61cd"},
+    {file = "pydantic-1.7.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a4143c8d0c456a093387b96e0f5ee941a950992904d88bc816b4f0e72c9a0009"},
+    {file = "pydantic-1.7.3-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:d8df4b9090b595511906fa48deda47af04e7d092318bfb291f4d45dfb6bb2127"},
+    {file = "pydantic-1.7.3-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:514b473d264671a5c672dfb28bdfe1bf1afd390f6b206aa2ec9fed7fc592c48e"},
+    {file = "pydantic-1.7.3-cp36-cp36m-win_amd64.whl", hash = "sha256:dba5c1f0a3aeea5083e75db9660935da90216f8a81b6d68e67f54e135ed5eb23"},
+    {file = "pydantic-1.7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:59e45f3b694b05a69032a0d603c32d453a23f0de80844fb14d55ab0c6c78ff2f"},
+    {file = "pydantic-1.7.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:5b24e8a572e4b4c18f614004dda8c9f2c07328cb5b6e314d6e1bbd536cb1a6c1"},
+    {file = "pydantic-1.7.3-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:b2b054d095b6431cdda2f852a6d2f0fdec77686b305c57961b4c5dd6d863bf3c"},
+    {file = "pydantic-1.7.3-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:025bf13ce27990acc059d0c5be46f416fc9b293f45363b3d19855165fee1874f"},
+    {file = "pydantic-1.7.3-cp37-cp37m-win_amd64.whl", hash = "sha256:6e3874aa7e8babd37b40c4504e3a94cc2023696ced5a0500949f3347664ff8e2"},
+    {file = "pydantic-1.7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e682f6442ebe4e50cb5e1cfde7dda6766fb586631c3e5569f6aa1951fd1a76ef"},
+    {file = "pydantic-1.7.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:185e18134bec5ef43351149fe34fda4758e53d05bb8ea4d5928f0720997b79ef"},
+    {file = "pydantic-1.7.3-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:f5b06f5099e163295b8ff5b1b71132ecf5866cc6e7f586d78d7d3fd6e8084608"},
+    {file = "pydantic-1.7.3-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:24ca47365be2a5a3cc3f4a26dcc755bcdc9f0036f55dcedbd55663662ba145ec"},
+    {file = "pydantic-1.7.3-cp38-cp38-win_amd64.whl", hash = "sha256:d1fe3f0df8ac0f3a9792666c69a7cd70530f329036426d06b4f899c025aca74e"},
+    {file = "pydantic-1.7.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f6864844b039805add62ebe8a8c676286340ba0c6d043ae5dea24114b82a319e"},
+    {file = "pydantic-1.7.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:ecb54491f98544c12c66ff3d15e701612fc388161fd455242447083350904730"},
+    {file = "pydantic-1.7.3-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:ffd180ebd5dd2a9ac0da4e8b995c9c99e7c74c31f985ba090ee01d681b1c4b95"},
+    {file = "pydantic-1.7.3-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:8d72e814c7821125b16f1553124d12faba88e85405b0864328899aceaad7282b"},
+    {file = "pydantic-1.7.3-cp39-cp39-win_amd64.whl", hash = "sha256:475f2fa134cf272d6631072554f845d0630907fce053926ff634cc6bc45bf1af"},
+    {file = "pydantic-1.7.3-py3-none-any.whl", hash = "sha256:38be427ea01a78206bcaf9a56f835784afcba9e5b88fbdce33bbbfbcd7841229"},
+    {file = "pydantic-1.7.3.tar.gz", hash = "sha256:213125b7e9e64713d16d988d10997dabc6a1f73f3991e1ff8e35ebb1409c7dc9"},
+]
+pyflakes = [
+    {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"},
+    {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"},
+]
+pyrsistent = [
+    {file = "pyrsistent-0.18.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f4c8cabb46ff8e5d61f56a037974228e978f26bfefce4f61a4b1ac0ba7a2ab72"},
+    {file = "pyrsistent-0.18.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:da6e5e818d18459fa46fac0a4a4e543507fe1110e808101277c5a2b5bab0cd2d"},
+    {file = "pyrsistent-0.18.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5e4395bbf841693eaebaa5bb5c8f5cdbb1d139e07c975c682ec4e4f8126e03d2"},
+    {file = "pyrsistent-0.18.0-cp36-cp36m-win32.whl", hash = "sha256:527be2bfa8dc80f6f8ddd65242ba476a6c4fb4e3aedbf281dfbac1b1ed4165b1"},
+    {file = "pyrsistent-0.18.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2aaf19dc8ce517a8653746d98e962ef480ff34b6bc563fc067be6401ffb457c7"},
+    {file = "pyrsistent-0.18.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58a70d93fb79dc585b21f9d72487b929a6fe58da0754fa4cb9f279bb92369396"},
+    {file = "pyrsistent-0.18.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:4916c10896721e472ee12c95cdc2891ce5890898d2f9907b1b4ae0f53588b710"},
+    {file = "pyrsistent-0.18.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:73ff61b1411e3fb0ba144b8f08d6749749775fe89688093e1efef9839d2dcc35"},
+    {file = "pyrsistent-0.18.0-cp37-cp37m-win32.whl", hash = "sha256:b29b869cf58412ca5738d23691e96d8aff535e17390128a1a52717c9a109da4f"},
+    {file = "pyrsistent-0.18.0-cp37-cp37m-win_amd64.whl", hash = "sha256:097b96f129dd36a8c9e33594e7ebb151b1515eb52cceb08474c10a5479e799f2"},
+    {file = "pyrsistent-0.18.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:772e94c2c6864f2cd2ffbe58bb3bdefbe2a32afa0acb1a77e472aac831f83427"},
+    {file = "pyrsistent-0.18.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c1a9ff320fa699337e05edcaae79ef8c2880b52720bc031b219e5b5008ebbdef"},
+    {file = "pyrsistent-0.18.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cd3caef37a415fd0dae6148a1b6957a8c5f275a62cca02e18474608cb263640c"},
+    {file = "pyrsistent-0.18.0-cp38-cp38-win32.whl", hash = "sha256:e79d94ca58fcafef6395f6352383fa1a76922268fa02caa2272fff501c2fdc78"},
+    {file = "pyrsistent-0.18.0-cp38-cp38-win_amd64.whl", hash = "sha256:a0c772d791c38bbc77be659af29bb14c38ced151433592e326361610250c605b"},
+    {file = "pyrsistent-0.18.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d5ec194c9c573aafaceebf05fc400656722793dac57f254cd4741f3c27ae57b4"},
+    {file = "pyrsistent-0.18.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:6b5eed00e597b5b5773b4ca30bd48a5774ef1e96f2a45d105db5b4ebb4bca680"},
+    {file = "pyrsistent-0.18.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:48578680353f41dca1ca3dc48629fb77dfc745128b56fc01096b2530c13fd426"},
+    {file = "pyrsistent-0.18.0-cp39-cp39-win32.whl", hash = "sha256:f3ef98d7b76da5eb19c37fda834d50262ff9167c65658d1d8f974d2e4d90676b"},
+    {file = "pyrsistent-0.18.0-cp39-cp39-win_amd64.whl", hash = "sha256:404e1f1d254d314d55adb8d87f4f465c8693d6f902f67eb6ef5b4526dc58e6ea"},
+    {file = "pyrsistent-0.18.0.tar.gz", hash = "sha256:773c781216f8c2900b42a7b638d5b517bb134ae1acbebe4d1e8f1f41ea60eb4b"},
+]
+pytest = [
+    {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"},
+    {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"},
+]
+pytest-cases = [
+    {file = "pytest-cases-3.6.4.tar.gz", hash = "sha256:9b6a3113bab8e9359a761660a704c01ca7a9e5937d2c6d5534244990fcf95b51"},
+    {file = "pytest_cases-3.6.4-py2.py3-none-any.whl", hash = "sha256:258226f1335d3771e42356b2335467716616cea793ffab7375c12416b32d4fc7"},
+]
+python-dateutil = [
+    {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"},
+    {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"},
+]
+pytz = [
+    {file = "pytz-2019.3-py2.py3-none-any.whl", hash = "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d"},
+    {file = "pytz-2019.3.tar.gz", hash = "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"},
+]
+pyyaml = [
+    {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"},
+    {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"},
+    {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"},
+    {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"},
+    {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"},
+    {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"},
+    {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347"},
+    {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541"},
+    {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"},
+    {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"},
+    {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"},
+    {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"},
+    {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa"},
+    {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"},
+    {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"},
+    {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"},
+    {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"},
+    {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"},
+    {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247"},
+    {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc"},
+    {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"},
+    {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"},
+    {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"},
+    {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"},
+    {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122"},
+    {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6"},
+    {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"},
+    {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"},
+    {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"},
+]
+redis = [
+    {file = "redis-3.5.3-py2.py3-none-any.whl", hash = "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"},
+    {file = "redis-3.5.3.tar.gz", hash = "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2"},
+]
+regex = [
+    {file = "regex-2021.9.24-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0628ed7d6334e8f896f882a5c1240de8c4d9b0dd7c7fb8e9f4692f5684b7d656"},
+    {file = "regex-2021.9.24-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3baf3eaa41044d4ced2463fd5d23bf7bd4b03d68739c6c99a59ce1f95599a673"},
+    {file = "regex-2021.9.24-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c000635fd78400a558bd7a3c2981bb2a430005ebaa909d31e6e300719739a949"},
+    {file = "regex-2021.9.24-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:295bc8a13554a25ad31e44c4bedabd3c3e28bba027e4feeb9bb157647a2344a7"},
+    {file = "regex-2021.9.24-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0e3f59d3c772f2c3baaef2db425e6fc4149d35a052d874bb95ccfca10a1b9f4"},
+    {file = "regex-2021.9.24-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:aea4006b73b555fc5bdb650a8b92cf486d678afa168cf9b38402bb60bf0f9c18"},
+    {file = "regex-2021.9.24-cp310-cp310-win32.whl", hash = "sha256:09eb62654030f39f3ba46bc6726bea464069c29d00a9709e28c9ee9623a8da4a"},
+    {file = "regex-2021.9.24-cp310-cp310-win_amd64.whl", hash = "sha256:8d80087320632457aefc73f686f66139801959bf5b066b4419b92be85be3543c"},
+    {file = "regex-2021.9.24-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:7e3536f305f42ad6d31fc86636c54c7dafce8d634e56fef790fbacb59d499dd5"},
+    {file = "regex-2021.9.24-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c31f35a984caffb75f00a86852951a337540b44e4a22171354fb760cefa09346"},
+    {file = "regex-2021.9.24-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c7cb25adba814d5f419733fe565f3289d6fa629ab9e0b78f6dff5fa94ab0456"},
+    {file = "regex-2021.9.24-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:85c61bee5957e2d7be390392feac7e1d7abd3a49cbaed0c8cee1541b784c8561"},
+    {file = "regex-2021.9.24-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c94722bf403b8da744b7d0bb87e1f2529383003ceec92e754f768ef9323f69ad"},
+    {file = "regex-2021.9.24-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6adc1bd68f81968c9d249aab8c09cdc2cbe384bf2d2cb7f190f56875000cdc72"},
+    {file = "regex-2021.9.24-cp36-cp36m-win32.whl", hash = "sha256:2054dea683f1bda3a804fcfdb0c1c74821acb968093d0be16233873190d459e3"},
+    {file = "regex-2021.9.24-cp36-cp36m-win_amd64.whl", hash = "sha256:7783d89bd5413d183a38761fbc68279b984b9afcfbb39fa89d91f63763fbfb90"},
+    {file = "regex-2021.9.24-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b15dc34273aefe522df25096d5d087abc626e388a28a28ac75a4404bb7668736"},
+    {file = "regex-2021.9.24-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10a7a9cbe30bd90b7d9a1b4749ef20e13a3528e4215a2852be35784b6bd070f0"},
+    {file = "regex-2021.9.24-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb9f5844db480e2ef9fce3a72e71122dd010ab7b2920f777966ba25f7eb63819"},
+    {file = "regex-2021.9.24-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:17310b181902e0bb42b29c700e2c2346b8d81f26e900b1328f642e225c88bce1"},
+    {file = "regex-2021.9.24-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bba1f6df4eafe79db2ecf38835c2626dbd47911e0516f6962c806f83e7a99ae"},
+    {file = "regex-2021.9.24-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:821e10b73e0898544807a0692a276e539e5bafe0a055506a6882814b6a02c3ec"},
+    {file = "regex-2021.9.24-cp37-cp37m-win32.whl", hash = "sha256:9c371dd326289d85906c27ec2bc1dcdedd9d0be12b543d16e37bad35754bde48"},
+    {file = "regex-2021.9.24-cp37-cp37m-win_amd64.whl", hash = "sha256:1e8d1898d4fb817120a5f684363b30108d7b0b46c7261264b100d14ec90a70e7"},
+    {file = "regex-2021.9.24-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8a5c2250c0a74428fd5507ae8853706fdde0f23bfb62ee1ec9418eeacf216078"},
+    {file = "regex-2021.9.24-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8aec4b4da165c4a64ea80443c16e49e3b15df0f56c124ac5f2f8708a65a0eddc"},
+    {file = "regex-2021.9.24-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:650c4f1fc4273f4e783e1d8e8b51a3e2311c2488ba0fcae6425b1e2c248a189d"},
+    {file = "regex-2021.9.24-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2cdb3789736f91d0b3333ac54d12a7e4f9efbc98f53cb905d3496259a893a8b3"},
+    {file = "regex-2021.9.24-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e61100200fa6ab7c99b61476f9f9653962ae71b931391d0264acfb4d9527d9c"},
+    {file = "regex-2021.9.24-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8c268e78d175798cd71d29114b0a1f1391c7d011995267d3b62319ec1a4ecaa1"},
+    {file = "regex-2021.9.24-cp38-cp38-win32.whl", hash = "sha256:658e3477676009083422042c4bac2bdad77b696e932a3de001c42cc046f8eda2"},
+    {file = "regex-2021.9.24-cp38-cp38-win_amd64.whl", hash = "sha256:a731552729ee8ae9c546fb1c651c97bf5f759018fdd40d0e9b4d129e1e3a44c8"},
+    {file = "regex-2021.9.24-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:86f9931eb92e521809d4b64ec8514f18faa8e11e97d6c2d1afa1bcf6c20a8eab"},
+    {file = "regex-2021.9.24-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcbbc9cfa147d55a577d285fd479b43103188855074552708df7acc31a476dd9"},
+    {file = "regex-2021.9.24-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29385c4dbb3f8b3a55ce13de6a97a3d21bd00de66acd7cdfc0b49cb2f08c906c"},
+    {file = "regex-2021.9.24-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c50a6379763c733562b1fee877372234d271e5c78cd13ade5f25978aa06744db"},
+    {file = "regex-2021.9.24-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f74b6d8f59f3cfb8237e25c532b11f794b96f5c89a6f4a25857d85f84fbef11"},
+    {file = "regex-2021.9.24-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6c4d83d21d23dd854ffbc8154cf293f4e43ba630aa9bd2539c899343d7f59da3"},
+    {file = "regex-2021.9.24-cp39-cp39-win32.whl", hash = "sha256:95e89a8558c8c48626dcffdf9c8abac26b7c251d352688e7ab9baf351e1c7da6"},
+    {file = "regex-2021.9.24-cp39-cp39-win_amd64.whl", hash = "sha256:835962f432bce92dc9bf22903d46c50003c8d11b1dc64084c8fae63bca98564a"},
+    {file = "regex-2021.9.24.tar.gz", hash = "sha256:6266fde576e12357b25096351aac2b4b880b0066263e7bc7a9a1b4307991bb0e"},
+]
+requests = [
+    {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"},
+    {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"},
+]
+requests-oauthlib = [
+    {file = "requests-oauthlib-1.3.0.tar.gz", hash = "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"},
+    {file = "requests_oauthlib-1.3.0-py2.py3-none-any.whl", hash = "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d"},
+    {file = "requests_oauthlib-1.3.0-py3.7.egg", hash = "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"},
+]
+responses = [
+    {file = "responses-0.14.0-py2.py3-none-any.whl", hash = "sha256:57bab4e9d4d65f31ea5caf9de62095032c4d81f591a8fac2f5858f7777b8567b"},
+    {file = "responses-0.14.0.tar.gz", hash = "sha256:93f774a762ee0e27c0d9d7e06227aeda9ff9f5f69392f72bb6c6b73f8763563e"},
+]
+rq = [
+    {file = "rq-1.7.0-py2.py3-none-any.whl", hash = "sha256:e0435db8e8afd2f83fd17262722d02a4455403a8a6344670186497474d74ce3a"},
+    {file = "rq-1.7.0.tar.gz", hash = "sha256:0291603f0c5bb0ef18363f6b325ae76076c1d79bb918e052aac1f8da2297dbfb"},
+]
+rsa = [
+    {file = "rsa-4.7.2-py3-none-any.whl", hash = "sha256:78f9a9bf4e7be0c5ded4583326e7461e3a3c5aae24073648b4bdfa797d78c9d2"},
+    {file = "rsa-4.7.2.tar.gz", hash = "sha256:9d689e6ca1b3038bc82bf8d23e944b6b6037bc02301a574935b2dd946e0353b9"},
+]
+"ruamel.yaml" = [
+    {file = "ruamel.yaml-0.17.16-py3-none-any.whl", hash = "sha256:ea21da1198c4b41b8e7a259301cc9710d3b972bf8ba52f06218478e6802dd1f1"},
+    {file = "ruamel.yaml-0.17.16.tar.gz", hash = "sha256:1a771fc92d3823682b7f0893ad56cb5a5c87c48e62b5399d6f42c8759a583b33"},
+]
+"ruamel.yaml.clib" = [
+    {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:cfdb9389d888c5b74af297e51ce357b800dd844898af9d4a547ffc143fa56751"},
+    {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7b2927e92feb51d830f531de4ccb11b320255ee95e791022555971c466af4527"},
+    {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-win32.whl", hash = "sha256:ada3f400d9923a190ea8b59c8f60680c4ef8a4b0dfae134d2f2ff68429adfab5"},
+    {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-win_amd64.whl", hash = "sha256:de9c6b8a1ba52919ae919f3ae96abb72b994dd0350226e28f3686cb4f142165c"},
+    {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d67f273097c368265a7b81e152e07fb90ed395df6e552b9fa858c6d2c9f42502"},
+    {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:72a2b8b2ff0a627496aad76f37a652bcef400fd861721744201ef1b45199ab78"},
+    {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-win32.whl", hash = "sha256:9efef4aab5353387b07f6b22ace0867032b900d8e91674b5d8ea9150db5cae94"},
+    {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-win_amd64.whl", hash = "sha256:846fc8336443106fe23f9b6d6b8c14a53d38cef9a375149d61f99d78782ea468"},
+    {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0847201b767447fc33b9c235780d3aa90357d20dd6108b92be544427bea197dd"},
+    {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:78988ed190206672da0f5d50c61afef8f67daa718d614377dcd5e3ed85ab4a99"},
+    {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-win32.whl", hash = "sha256:a49e0161897901d1ac9c4a79984b8410f450565bbad64dbfcbf76152743a0cdb"},
+    {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-win_amd64.whl", hash = "sha256:bf75d28fa071645c529b5474a550a44686821decebdd00e21127ef1fd566eabe"},
+    {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a32f8d81ea0c6173ab1b3da956869114cae53ba1e9f72374032e33ba3118c233"},
+    {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7f7ecb53ae6848f959db6ae93bdff1740e651809780822270eab111500842a84"},
+    {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-win32.whl", hash = "sha256:89221ec6d6026f8ae859c09b9718799fea22c0e8da8b766b0b2c9a9ba2db326b"},
+    {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-win_amd64.whl", hash = "sha256:31ea73e564a7b5fbbe8188ab8b334393e06d997914a4e184975348f204790277"},
+    {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dc6a613d6c74eef5a14a214d433d06291526145431c3b964f5e16529b1842bed"},
+    {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:1866cf2c284a03b9524a5cc00daca56d80057c5ce3cdc86a52020f4c720856f0"},
+    {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-win32.whl", hash = "sha256:3fb9575a5acd13031c57a62cc7823e5d2ff8bc3835ba4d94b921b4e6ee664104"},
+    {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-win_amd64.whl", hash = "sha256:825d5fccef6da42f3c8eccd4281af399f21c02b32d98e113dbc631ea6a6ecbc7"},
+    {file = "ruamel.yaml.clib-0.2.6.tar.gz", hash = "sha256:4ff604ce439abb20794f05613c374759ce10e3595d1867764dd1ae675b85acbd"},
+]
+rules = [
+    {file = "rules-2.2.tar.gz", hash = "sha256:9bae429f9d4f91a375402990da1541f9e093b0ac077221d57124d06eeeca4405"},
+]
+s3transfer = [
+    {file = "s3transfer-0.3.7-py2.py3-none-any.whl", hash = "sha256:efa5bd92a897b6a8d5c1383828dca3d52d0790e0756d49740563a3fb6ed03246"},
+    {file = "s3transfer-0.3.7.tar.gz", hash = "sha256:35627b86af8ff97e7ac27975fe0a98a312814b46c6333d8a6b889627bcd80994"},
+]
+simplejson = [
+    {file = "simplejson-3.17.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:376023f51edaf7290332dacfb055bc00ce864cb013c0338d0dea48731f37e42f"},
+    {file = "simplejson-3.17.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b2a5688606dffbe95e1347a05b77eb90489fe337edde888e23bbb7fd81b0d93b"},
+    {file = "simplejson-3.17.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:3ba82f8b421886f4a2311c43fb98faaf36c581976192349fef2a89ed0fcdbdef"},
+    {file = "simplejson-3.17.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:7332f7b06d42153255f7bfeb10266141c08d48cc1a022a35473c95238ff2aebc"},
+    {file = "simplejson-3.17.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:c2d5334d935af711f6d6dfeec2d34e071cdf73ec0df8e8bd35ac435b26d8da97"},
+    {file = "simplejson-3.17.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:417b7e119d66085dc45bdd563dcb2c575ee10a3b1c492dd3502a029448d4be1c"},
+    {file = "simplejson-3.17.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:42b7c7264229860fe879be961877f7466d9f7173bd6427b3ba98144a031d49fb"},
+    {file = "simplejson-3.17.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:5fe8c6dcb9e6f7066bdc07d3c410a2fca78c0d0b4e0e72510ffd20a60a20eb8e"},
+    {file = "simplejson-3.17.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:b92fbc2bc549c5045c8233d954f3260ccf99e0f3ec9edfd2372b74b350917752"},
+    {file = "simplejson-3.17.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5f7f53b1edd4b23fb112b89208377480c0bcee45d43a03ffacf30f3290e0ed85"},
+    {file = "simplejson-3.17.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:40ece8fa730d1a947bff792bcc7824bd02d3ce6105432798e9a04a360c8c07b0"},
+    {file = "simplejson-3.17.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:10defa88dd10a0a4763f16c1b5504e96ae6dc68953cfe5fc572b4a8fcaf9409b"},
+    {file = "simplejson-3.17.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa86cfdeb118795875855589934013e32895715ec2d9e8eb7a59be3e7e07a7e1"},
+    {file = "simplejson-3.17.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ce66f730031b9b3683b2fc6ad4160a18db86557c004c3d490a29bf8d450d7ab9"},
+    {file = "simplejson-3.17.5-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:352c11582aa1e49a2f0f7f7d8fd5ec5311da890d1354287e83c63ab6af857cf5"},
+    {file = "simplejson-3.17.5-cp310-cp310-win32.whl", hash = "sha256:8e595de17178dd3bbeb2c5b8ea97536341c63b7278639cb8ee2681a84c0ef037"},
+    {file = "simplejson-3.17.5-cp310-cp310-win_amd64.whl", hash = "sha256:cb0afc3bad49eb89a579103616574a54b523856d20fc539a4f7a513a0a8ba4b2"},
+    {file = "simplejson-3.17.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ade09aa3c284d11f39640aebdcbb748e1996f0c60504f8c4a0c5a9fec821e67a"},
+    {file = "simplejson-3.17.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87572213965fd8a4fb7a97f837221e01d8fddcfb558363c671b8aa93477fb6a2"},
+    {file = "simplejson-3.17.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2b59acd09b02da97728d0bae8ff48876d7efcbbb08e569c55e2d0c2e018324f5"},
+    {file = "simplejson-3.17.5-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e29b9cea4216ec130df85d8c36efb9985fda1c9039e4706fb30e0fb6a67602ff"},
+    {file = "simplejson-3.17.5-cp36-cp36m-win32.whl", hash = "sha256:f550730d18edec4ff9d4252784b62adfe885d4542946b6d5a54c8a6521b56afd"},
+    {file = "simplejson-3.17.5-cp36-cp36m-win_amd64.whl", hash = "sha256:1c2688365743b0f190392e674af5e313ebe9d621813d15f9332e874b7c1f2d04"},
+    {file = "simplejson-3.17.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f13c48cc4363829bdfecc0c181b6ddf28008931de54908a492dc8ccd0066cd60"},
+    {file = "simplejson-3.17.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a6943816e10028eeed512ea03be52b54ea83108b408d1049b999f58a760089b"},
+    {file = "simplejson-3.17.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:3d72aa9e73134dacd049a2d6f9bd219f7be9c004d03d52395831611d66cedb71"},
+    {file = "simplejson-3.17.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5b94df70bd34a3b946c0eb272022fb0f8a9eb27cad76e7f313fedbee2ebe4317"},
+    {file = "simplejson-3.17.5-cp37-cp37m-win32.whl", hash = "sha256:065230b9659ac38c8021fa512802562d122afb0cf8d4b89e257014dcddb5730a"},
+    {file = "simplejson-3.17.5-cp37-cp37m-win_amd64.whl", hash = "sha256:86fcffc06f1125cb443e2bed812805739d64ceb78597ac3c1b2d439471a09717"},
+    {file = "simplejson-3.17.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:78c6f0ed72b440ebe1892d273c1e5f91e55e6861bea611d3b904e673152a7a4c"},
+    {file = "simplejson-3.17.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:36b08b886027eac67e7a0e822e3a5bf419429efad7612e69501669d6252a21f2"},
+    {file = "simplejson-3.17.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fe1c33f78d2060719d52ea9459d97d7ae3a5b707ec02548575c4fbed1d1d345b"},
+    {file = "simplejson-3.17.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:140eb58809f24d843736edb8080b220417e22c82ac07a3dfa473f57e78216b5f"},
+    {file = "simplejson-3.17.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c9b30a2524ae6983b708f12741a31fbc2fb8d6fecd0b6c8584a62fd59f59e09"},
+    {file = "simplejson-3.17.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:24e413bd845bd17d4d72063d64e053898543fb7abc81afeae13e5c43cef9c171"},
+    {file = "simplejson-3.17.5-cp38-cp38-win32.whl", hash = "sha256:5f5051a13e7d53430a990604b532c9124253c5f348857e2d5106d45fc8533860"},
+    {file = "simplejson-3.17.5-cp38-cp38-win_amd64.whl", hash = "sha256:188f2c78a8ac1eb7a70a4b2b7b9ad11f52181044957bf981fb3e399c719e30ee"},
+    {file = "simplejson-3.17.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:457d9cfe7ece1571770381edccdad7fc255b12cd7b5b813219441146d4f47595"},
+    {file = "simplejson-3.17.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa843ee0d34c7193f5a816e79df8142faff851549cab31e84b526f04878ac778"},
+    {file = "simplejson-3.17.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e2cc4b68e59319e3de778325e34fbff487bfdb2225530e89995402989898d681"},
+    {file = "simplejson-3.17.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e90d2e219c3dce1500dda95f5b893c293c4d53c4e330c968afbd4e7a90ff4a5b"},
+    {file = "simplejson-3.17.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:917f01db71d5e720b731effa3ff4a2c702a1b6dacad9bcdc580d86a018dfc3ca"},
+    {file = "simplejson-3.17.5-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:07707ba69324eaf58f0c6f59d289acc3e0ed9ec528dae5b0d4219c0d6da27dc5"},
+    {file = "simplejson-3.17.5-cp39-cp39-win32.whl", hash = "sha256:2df15814529a4625ea6f7b354a083609b3944c269b954ece0d0e7455872e1b2a"},
+    {file = "simplejson-3.17.5-cp39-cp39-win_amd64.whl", hash = "sha256:71a54815ec0212b0cba23adc1b2a731bdd2df7b9e4432718b2ed20e8aaf7f01a"},
+    {file = "simplejson-3.17.5.tar.gz", hash = "sha256:91cfb43fb91ff6d1e4258be04eee84b51a4ef40a28d899679b9ea2556322fb50"},
+]
+six = [
+    {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
+    {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
+]
+sqlalchemy = [
+    {file = "SQLAlchemy-1.3.24-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:87a2725ad7d41cd7376373c15fd8bf674e9c33ca56d0b8036add2d634dba372e"},
+    {file = "SQLAlchemy-1.3.24-cp27-cp27m-win32.whl", hash = "sha256:f597a243b8550a3a0b15122b14e49d8a7e622ba1c9d29776af741f1845478d79"},
+    {file = "SQLAlchemy-1.3.24-cp27-cp27m-win_amd64.whl", hash = "sha256:fc4cddb0b474b12ed7bdce6be1b9edc65352e8ce66bc10ff8cbbfb3d4047dbf4"},
+    {file = "SQLAlchemy-1.3.24-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:f1149d6e5c49d069163e58a3196865e4321bad1803d7886e07d8710de392c548"},
+    {file = "SQLAlchemy-1.3.24-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:14f0eb5db872c231b20c18b1e5806352723a3a89fb4254af3b3e14f22eaaec75"},
+    {file = "SQLAlchemy-1.3.24-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:e98d09f487267f1e8d1179bf3b9d7709b30a916491997137dd24d6ae44d18d79"},
+    {file = "SQLAlchemy-1.3.24-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:fc1f2a5a5963e2e73bac4926bdaf7790c4d7d77e8fc0590817880e22dd9d0b8b"},
+    {file = "SQLAlchemy-1.3.24-cp35-cp35m-win32.whl", hash = "sha256:f3c5c52f7cb8b84bfaaf22d82cb9e6e9a8297f7c2ed14d806a0f5e4d22e83fb7"},
+    {file = "SQLAlchemy-1.3.24-cp35-cp35m-win_amd64.whl", hash = "sha256:0352db1befcbed2f9282e72843f1963860bf0e0472a4fa5cf8ee084318e0e6ab"},
+    {file = "SQLAlchemy-1.3.24-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:2ed6343b625b16bcb63c5b10523fd15ed8934e1ed0f772c534985e9f5e73d894"},
+    {file = "SQLAlchemy-1.3.24-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:34fcec18f6e4b24b4a5f6185205a04f1eab1e56f8f1d028a2a03694ebcc2ddd4"},
+    {file = "SQLAlchemy-1.3.24-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:e47e257ba5934550d7235665eee6c911dc7178419b614ba9e1fbb1ce6325b14f"},
+    {file = "SQLAlchemy-1.3.24-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:816de75418ea0953b5eb7b8a74933ee5a46719491cd2b16f718afc4b291a9658"},
+    {file = "SQLAlchemy-1.3.24-cp36-cp36m-win32.whl", hash = "sha256:26155ea7a243cbf23287f390dba13d7927ffa1586d3208e0e8d615d0c506f996"},
+    {file = "SQLAlchemy-1.3.24-cp36-cp36m-win_amd64.whl", hash = "sha256:f03bd97650d2e42710fbe4cf8a59fae657f191df851fc9fc683ecef10746a375"},
+    {file = "SQLAlchemy-1.3.24-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:a006d05d9aa052657ee3e4dc92544faae5fcbaafc6128217310945610d862d39"},
+    {file = "SQLAlchemy-1.3.24-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:1e2f89d2e5e3c7a88e25a3b0e43626dba8db2aa700253023b82e630d12b37109"},
+    {file = "SQLAlchemy-1.3.24-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:0d5d862b1cfbec5028ce1ecac06a3b42bc7703eb80e4b53fceb2738724311443"},
+    {file = "SQLAlchemy-1.3.24-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:0172423a27fbcae3751ef016663b72e1a516777de324a76e30efa170dbd3dd2d"},
+    {file = "SQLAlchemy-1.3.24-cp37-cp37m-win32.whl", hash = "sha256:d37843fb8df90376e9e91336724d78a32b988d3d20ab6656da4eb8ee3a45b63c"},
+    {file = "SQLAlchemy-1.3.24-cp37-cp37m-win_amd64.whl", hash = "sha256:c10ff6112d119f82b1618b6dc28126798481b9355d8748b64b9b55051eb4f01b"},
+    {file = "SQLAlchemy-1.3.24-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:861e459b0e97673af6cc5e7f597035c2e3acdfb2608132665406cded25ba64c7"},
+    {file = "SQLAlchemy-1.3.24-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5de2464c254380d8a6c20a2746614d5a436260be1507491442cf1088e59430d2"},
+    {file = "SQLAlchemy-1.3.24-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d375d8ccd3cebae8d90270f7aa8532fe05908f79e78ae489068f3b4eee5994e8"},
+    {file = "SQLAlchemy-1.3.24-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:014ea143572fee1c18322b7908140ad23b3994036ef4c0d630110faf942652f8"},
+    {file = "SQLAlchemy-1.3.24-cp38-cp38-win32.whl", hash = "sha256:6607ae6cd3a07f8a4c3198ffbf256c261661965742e2b5265a77cd5c679c9bba"},
+    {file = "SQLAlchemy-1.3.24-cp38-cp38-win_amd64.whl", hash = "sha256:fcb251305fa24a490b6a9ee2180e5f8252915fb778d3dafc70f9cc3f863827b9"},
+    {file = "SQLAlchemy-1.3.24-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:01aa5f803db724447c1d423ed583e42bf5264c597fd55e4add4301f163b0be48"},
+    {file = "SQLAlchemy-1.3.24-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4d0e3515ef98aa4f0dc289ff2eebb0ece6260bbf37c2ea2022aad63797eacf60"},
+    {file = "SQLAlchemy-1.3.24-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:bce28277f308db43a6b4965734366f533b3ff009571ec7ffa583cb77539b84d6"},
+    {file = "SQLAlchemy-1.3.24-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:8110e6c414d3efc574543109ee618fe2c1f96fa31833a1ff36cc34e968c4f233"},
+    {file = "SQLAlchemy-1.3.24-cp39-cp39-win32.whl", hash = "sha256:ee5f5188edb20a29c1cc4a039b074fdc5575337c9a68f3063449ab47757bb064"},
+    {file = "SQLAlchemy-1.3.24-cp39-cp39-win_amd64.whl", hash = "sha256:09083c2487ca3c0865dc588e07aeaa25416da3d95f7482c07e92f47e080aa17b"},
+    {file = "SQLAlchemy-1.3.24.tar.gz", hash = "sha256:ebbb777cbf9312359b897bf81ba00dae0f5cb69fba2a18265dcc18a6f5ef7519"},
+]
+sqlparse = [
+    {file = "sqlparse-0.4.2-py3-none-any.whl", hash = "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d"},
+    {file = "sqlparse-0.4.2.tar.gz", hash = "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae"},
+]
+tenacity = [
+    {file = "tenacity-8.0.1-py3-none-any.whl", hash = "sha256:f78f4ea81b0fabc06728c11dc2a8c01277bfc5181b321a4770471902e3eb844a"},
+    {file = "tenacity-8.0.1.tar.gz", hash = "sha256:43242a20e3e73291a28bcbcacfd6e000b02d3857a9a9fff56b297a27afdc932f"},
+]
+toml = [
+    {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
+    {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
+]
+tomli = [
+    {file = "tomli-1.2.1-py3-none-any.whl", hash = "sha256:8dd0e9524d6f386271a36b41dbf6c57d8e32fd96fd22b6584679dc569d20899f"},
+    {file = "tomli-1.2.1.tar.gz", hash = "sha256:a5b75cb6f3968abb47af1b40c1819dc519ea82bcc065776a866e8d74c5ca9442"},
+]
+tqdm = [
+    {file = "tqdm-4.62.3-py2.py3-none-any.whl", hash = "sha256:8dd278a422499cd6b727e6ae4061c40b48fce8b76d1ccbf5d34fca9b7f925b0c"},
+    {file = "tqdm-4.62.3.tar.gz", hash = "sha256:d359de7217506c9851b7869f3708d8ee53ed70a1b8edbba4dbcb47442592920d"},
+]
+types-requests = [
+    {file = "types-requests-2.25.9.tar.gz", hash = "sha256:4ec8b71da73e5344adb9bee725a74ec8598e7286f9bcb17500d627f259fe4fb9"},
+    {file = "types_requests-2.25.9-py3-none-any.whl", hash = "sha256:543ba8b3b23e38ac028da1d163aecbbc27d3cc8f654ae64339da539a191a2b1c"},
+]
+typing-extensions = [
+    {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"},
+    {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"},
+    {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"},
+]
+ua-parser = [
+    {file = "ua-parser-0.10.0.tar.gz", hash = "sha256:47b1782ed130d890018d983fac37c2a80799d9e0b9c532e734c67cf70f185033"},
+    {file = "ua_parser-0.10.0-py2.py3-none-any.whl", hash = "sha256:46ab2e383c01dbd2ab284991b87d624a26a08f72da4d7d413f5bfab8b9036f8a"},
+]
+ujson = [
+    {file = "ujson-4.2.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:02f5a6acc5aa5b054c32bdccd17064c2cbd07c81b3a49449589a4552888f1961"},
+    {file = "ujson-4.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7c8cffd9e45248569fa576d19b043951a3edc67cbee3dca2a6b77e6998cb1ec"},
+    {file = "ujson-4.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0abc1526d32ebe2f2b6fefcf82b9ee8661da3f45ecac087beee6aeaa21b39ec"},
+    {file = "ujson-4.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:26ebe3ac1e0d18d58e11dbf7cdd0459adcefd5c025ede99be7fceae47bd1bfc6"},
+    {file = "ujson-4.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6e8c30186eaa4f982b9b56cad30b173b71aac9d6a393d97cbcbc4ca805e87943"},
+    {file = "ujson-4.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0db1acd8dbbf120095ce4ab118585219066ea2860332df2385a435c585036cae"},
+    {file = "ujson-4.2.0-cp310-cp310-win32.whl", hash = "sha256:68088596e237dcda862c1760d1feb2236a8cc36804507a9fcfaa3ed21442ff64"},
+    {file = "ujson-4.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:dd98d659365fb9271d9f9ba21160670ddfecb93deaec8a5350519a0ab3604654"},
+    {file = "ujson-4.2.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:42f8def3e44d880774f644b53b4701a46889a9506e3fdc191c314c9e2e9b2a38"},
+    {file = "ujson-4.2.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc9e2e6933ecec17a7d351116f555caee84b798076a4c5276ab1e8654c83bd90"},
+    {file = "ujson-4.2.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:adca402a97b8388c378d92c55b2bf4e8402baa5dfa059a85870a41e2f142a0d0"},
+    {file = "ujson-4.2.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0076de81aadc287f025525969bbc1272703be92933e988e5663a76ec5863628f"},
+    {file = "ujson-4.2.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:ff0ae4b1dc70c3362b04f4127e228b17e84946de611f85b32f565b990762deaf"},
+    {file = "ujson-4.2.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:40235c3704ac7f29b24a7a785f856e1c45a567c8cd4b57a025f516733e358972"},
+    {file = "ujson-4.2.0-cp36-cp36m-win32.whl", hash = "sha256:32f4340c8d6a34e7fa1f9447919ebd4b6256afef0965868d51ea3cdf0694a8f7"},
+    {file = "ujson-4.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:bac42d0131740b6f1d2b6a15f770c1fef0db97d05aba8564f3c75bb59a9c91c7"},
+    {file = "ujson-4.2.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:0ebc82847938e417092fd463e593da39a280fd12585b998e435a2a45bb7a33f7"},
+    {file = "ujson-4.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bccfff9910fbe3dae6a17ec080c48fca0bfe939b18e4b2622122109d4d2570d"},
+    {file = "ujson-4.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ec19982b7a40fb526b8ffd6edfff2c5c10556a54416d554c4bc83b1e4140a4c"},
+    {file = "ujson-4.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3697cae44a92c2264cf307d4cdd9ea436faa75a009d35a4097362a6bbf221a1b"},
+    {file = "ujson-4.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b55dde52028f3c426197b9a928618fbb2b548172eb45824ddb19cdcdf8e493f9"},
+    {file = "ujson-4.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d9a0252da73d8b69de60811252cbfde215d76ee6eea935a519e223353352026c"},
+    {file = "ujson-4.2.0-cp37-cp37m-win32.whl", hash = "sha256:a6e27ff0f92c719de004b806d84f471fff0157679e57517092b8f89345eb3a79"},
+    {file = "ujson-4.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:50f413177206373b3568dff28c091b2d8c9145e5e54b0a524d3823293d7a29b8"},
+    {file = "ujson-4.2.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:944dfdfae0cb4e4274a23e5070be913f7ff8f05666d8d143f2356cf873f9a77a"},
+    {file = "ujson-4.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50ddff58727cc4f90ade4c818884c4f0fbeeb1f78f764ab2b2bc89cf1f4db126"},
+    {file = "ujson-4.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78f8da27231b20d589319f743bd65011862691c102922be85c9f27e40656a0f7"},
+    {file = "ujson-4.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99322e08e53580867c9909637747343354b7028a8208dc5df3d09233c8f52c30"},
+    {file = "ujson-4.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5f863b12d27867b733d380a9878fc8c3ad11d3aea8a3650c33e7a8c2a888e1eb"},
+    {file = "ujson-4.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c53653a12f6ce67b12e7806c190cce1db09c75de31212a5a9adbdbd7a2aba116"},
+    {file = "ujson-4.2.0-cp38-cp38-win32.whl", hash = "sha256:c9723a5e2743ded9bc6106cb04f86244dd779ad847d8ac189cfe808ab16946c1"},
+    {file = "ujson-4.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:fae1e251d9f9362ebc4adbbb252d0f4a023f66f180390624826fcb1812b808e9"},
+    {file = "ujson-4.2.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:ab70a29914fc202f7c8f6353bb0622b91f878e2892e57a4c7293b341d5a85133"},
+    {file = "ujson-4.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf272e6302c62a42ed0125efcc1d891dd909bcfb3c6aa075d89de209b2b8e930"},
+    {file = "ujson-4.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6182898d7031d083017f9be47c6d57f9c2155d9f4391b77ed6c020cab8e5a471"},
+    {file = "ujson-4.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a885675252e041e19cb92ff76419251fdd1ab4c2ca9766cb3ecfa6ae07884d2"},
+    {file = "ujson-4.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:535f1e5b336f7a0573da59bd9352e9cc6a93185bf4b1d96fb3379e07b4fc619a"},
+    {file = "ujson-4.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:26e299caa9bfc2e532e16de38de2af48cde48258922cda5efc0dd447d42692fc"},
+    {file = "ujson-4.2.0-cp39-cp39-win32.whl", hash = "sha256:fa68b25c987b73fb1c83ece34e52856c2c6da3031384f72638696c56fa8baca9"},
+    {file = "ujson-4.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:c7b2643b063f33e5e0d539cf2811793a7df9da525e63483f70c9262a343bd59d"},
+    {file = "ujson-4.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d61ae7a505105a16220a4282dbd552bef76968203e5e4f5edfdbb03d3159e3a"},
+    {file = "ujson-4.2.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bff484c162bd77877bc9ae6333b0a684493037ce3c5d8b664e8339becf9ad139"},
+    {file = "ujson-4.2.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74961587e9c1682d3e04fe29e02b67ec9c88cb0c3406ad94cc617d04c6fa6db2"},
+    {file = "ujson-4.2.0.tar.gz", hash = "sha256:fffe509f556861c7343c6cba57ed05fe7bcf4b48a934a5b946ccb45428cf8883"},
+]
+uritemplate = [
+    {file = "uritemplate-3.0.1-py2.py3-none-any.whl", hash = "sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f"},
+    {file = "uritemplate-3.0.1.tar.gz", hash = "sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae"},
+]
+urllib3 = [
+    {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"},
+    {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"},
+]
+user-agents = [
+    {file = "user-agents-2.2.0.tar.gz", hash = "sha256:d36d25178db65308d1458c5fa4ab39c9b2619377010130329f3955e7626ead26"},
+    {file = "user_agents-2.2.0-py3-none-any.whl", hash = "sha256:a98c4dc72ecbc64812c4534108806fb0a0b3a11ec3fd1eafe807cee5b0a942e7"},
+]
+werkzeug = [
+    {file = "Werkzeug-2.0.1-py3-none-any.whl", hash = "sha256:6c1ec500dcdba0baa27600f6a22f6333d8b662d22027ff9f6202e3367413caa8"},
+    {file = "Werkzeug-2.0.1.tar.gz", hash = "sha256:1de1db30d010ff1af14a009224ec49ab2329ad2cde454c8a708130642d579c42"},
+]
+xlrd = [
+    {file = "xlrd-2.0.1-py2.py3-none-any.whl", hash = "sha256:6a33ee89877bd9abc1158129f6e94be74e2679636b8a205b43b85206c3f0bbdd"},
+    {file = "xlrd-2.0.1.tar.gz", hash = "sha256:f72f148f54442c6b056bf931dbc34f986fd0c3b0b6b5a58d013c9aef274d0c88"},
+]
+xmljson = [
+    {file = "xmljson-0.2.0-py2.py3-none-any.whl", hash = "sha256:5cfcd641e3057a4bef256adbd75b738fb9ca6bd49e859840c6f1001c8ac8fc57"},
+    {file = "xmljson-0.2.0.tar.gz", hash = "sha256:9a0d3e658093c93af70bea4ddfaf15012e1a7d583c84f9c7ede0aadad0fe14e7"},
+]
+xmltodict = [
+    {file = "xmltodict-0.12.0-py2.py3-none-any.whl", hash = "sha256:8bbcb45cc982f48b2ca8fe7e7827c5d792f217ecf1792626f808bf41c3b86051"},
+    {file = "xmltodict-0.12.0.tar.gz", hash = "sha256:50d8c638ed7ecb88d90561beedbf720c9b4e851a9fa6c47ebd64e99d166d8a21"},
+]
+yarl = [
+    {file = "yarl-1.6.3-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:0355a701b3998dcd832d0dc47cc5dedf3874f966ac7f870e0f3a6788d802d434"},
+    {file = "yarl-1.6.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:bafb450deef6861815ed579c7a6113a879a6ef58aed4c3a4be54400ae8871478"},
+    {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:547f7665ad50fa8563150ed079f8e805e63dd85def6674c97efd78eed6c224a6"},
+    {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:63f90b20ca654b3ecc7a8d62c03ffa46999595f0167d6450fa8383bab252987e"},
+    {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:97b5bdc450d63c3ba30a127d018b866ea94e65655efaf889ebeabc20f7d12406"},
+    {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:d8d07d102f17b68966e2de0e07bfd6e139c7c02ef06d3a0f8d2f0f055e13bb76"},
+    {file = "yarl-1.6.3-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:15263c3b0b47968c1d90daa89f21fcc889bb4b1aac5555580d74565de6836366"},
+    {file = "yarl-1.6.3-cp36-cp36m-win32.whl", hash = "sha256:b5dfc9a40c198334f4f3f55880ecf910adebdcb2a0b9a9c23c9345faa9185721"},
+    {file = "yarl-1.6.3-cp36-cp36m-win_amd64.whl", hash = "sha256:b2e9a456c121e26d13c29251f8267541bd75e6a1ccf9e859179701c36a078643"},
+    {file = "yarl-1.6.3-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:ce3beb46a72d9f2190f9e1027886bfc513702d748047b548b05dab7dfb584d2e"},
+    {file = "yarl-1.6.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2ce4c621d21326a4a5500c25031e102af589edb50c09b321049e388b3934eec3"},
+    {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:d26608cf178efb8faa5ff0f2d2e77c208f471c5a3709e577a7b3fd0445703ac8"},
+    {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:4c5bcfc3ed226bf6419f7a33982fb4b8ec2e45785a0561eb99274ebbf09fdd6a"},
+    {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:4736eaee5626db8d9cda9eb5282028cc834e2aeb194e0d8b50217d707e98bb5c"},
+    {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:68dc568889b1c13f1e4745c96b931cc94fdd0defe92a72c2b8ce01091b22e35f"},
+    {file = "yarl-1.6.3-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:7356644cbed76119d0b6bd32ffba704d30d747e0c217109d7979a7bc36c4d970"},
+    {file = "yarl-1.6.3-cp37-cp37m-win32.whl", hash = "sha256:00d7ad91b6583602eb9c1d085a2cf281ada267e9a197e8b7cae487dadbfa293e"},
+    {file = "yarl-1.6.3-cp37-cp37m-win_amd64.whl", hash = "sha256:69ee97c71fee1f63d04c945f56d5d726483c4762845400a6795a3b75d56b6c50"},
+    {file = "yarl-1.6.3-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:e46fba844f4895b36f4c398c5af062a9808d1f26b2999c58909517384d5deda2"},
+    {file = "yarl-1.6.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:31ede6e8c4329fb81c86706ba8f6bf661a924b53ba191b27aa5fcee5714d18ec"},
+    {file = "yarl-1.6.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71"},
+    {file = "yarl-1.6.3-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:72a660bdd24497e3e84f5519e57a9ee9220b6f3ac4d45056961bf22838ce20cc"},
+    {file = "yarl-1.6.3-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:324ba3d3c6fee56e2e0b0d09bf5c73824b9f08234339d2b788af65e60040c959"},
+    {file = "yarl-1.6.3-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:e6b5460dc5ad42ad2b36cca524491dfcaffbfd9c8df50508bddc354e787b8dc2"},
+    {file = "yarl-1.6.3-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:6d6283d8e0631b617edf0fd726353cb76630b83a089a40933043894e7f6721e2"},
+    {file = "yarl-1.6.3-cp38-cp38-win32.whl", hash = "sha256:9ede61b0854e267fd565e7527e2f2eb3ef8858b301319be0604177690e1a3896"},
+    {file = "yarl-1.6.3-cp38-cp38-win_amd64.whl", hash = "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a"},
+    {file = "yarl-1.6.3-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:329412812ecfc94a57cd37c9d547579510a9e83c516bc069470db5f75684629e"},
+    {file = "yarl-1.6.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:c49ff66d479d38ab863c50f7bb27dee97c6627c5fe60697de15529da9c3de724"},
+    {file = "yarl-1.6.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f040bcc6725c821a4c0665f3aa96a4d0805a7aaf2caf266d256b8ed71b9f041c"},
+    {file = "yarl-1.6.3-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:d5c32c82990e4ac4d8150fd7652b972216b204de4e83a122546dce571c1bdf25"},
+    {file = "yarl-1.6.3-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:d597767fcd2c3dc49d6eea360c458b65643d1e4dbed91361cf5e36e53c1f8c96"},
+    {file = "yarl-1.6.3-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:8aa3decd5e0e852dc68335abf5478a518b41bf2ab2f330fe44916399efedfae0"},
+    {file = "yarl-1.6.3-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:73494d5b71099ae8cb8754f1df131c11d433b387efab7b51849e7e1e851f07a4"},
+    {file = "yarl-1.6.3-cp39-cp39-win32.whl", hash = "sha256:5b883e458058f8d6099e4420f0cc2567989032b5f34b271c0827de9f1079a424"},
+    {file = "yarl-1.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:4953fb0b4fdb7e08b2f3b3be80a00d28c5c8a2056bb066169de00e6501b986b6"},
+    {file = "yarl-1.6.3.tar.gz", hash = "sha256:8a9066529240171b68893d60dca86a763eae2139dd42f42106b03cf4b426bf10"},
+]
+zipp = [
+    {file = "zipp-3.5.0-py3-none-any.whl", hash = "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"},
+    {file = "zipp-3.5.0.tar.gz", hash = "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"},
+]
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 00000000..96da2cfc
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,52 @@
+[tool.poetry]
+name = "datapipe"
+version = "0.8.2+otm2.0"
+description = ""
+authors = ["Andrey Tatarinov <a@tatarinov.co>"]
+
+exclude=["tests", "examples"]
+
+[tool.poetry.dependencies]
+python = "^3.8"
+fsspec = "==2021.5.0"
+gcsfs = "==2021.5.0"
+
+pandas = "^1.2.0"
+numpy = "^1.21.0"
+SQLAlchemy = ">=1.3.20, <1.4"
+psycopg2_binary = ">=2.8.4"
+cloudpickle = ">=1.6.0"
+PyYAML = ">=5.3.1"
+Pillow = ">=7.2.0, <8"
+tqdm = ">=4.60.0"
+toml = ">=0.10.2"
+click = ">=7.1.2"
+requests = ">=2.24.0"
+
+xlrd = {version=">=2.0.1", optional=true}
+openpyxl = {version=">=3.0.7", optional=true}
+
+dash = {version="==1.20.*", optional=true}
+dash_bootstrap_components = {version="== 0.12.*", optional=true}
+dash_interactive_graphviz = {version="==0.3.*", optional=true}
+
+label-studio = {version="==1.0.*", optional=true}
+
+[tool.poetry.extras]
+
+excel = ["xlrd", "openpyxl"]
+ui = ["dash", "dash_bootstrap_components", "dash_interactive_graphviz"]
+ls = ["label-studio"]
+
+[tool.poetry.dev-dependencies]
+pytest = "*"
+flake8 = "^3.9.2"
+mypy = "^0.910"
+black = "^21.7b0"
+pandas-stubs = "^1.2.0"
+types-requests = "^2.25.6"
+pytest-cases = "^3.6.4"
+
+[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"
diff --git a/setup.py b/setup.py
deleted file mode 100644
index e82c1836..00000000
--- a/setup.py
+++ /dev/null
@@ -1,38 +0,0 @@
-import setuptools
-
-setuptools.setup(
-    name='datapipe',
-    version='0.8.2',
-    include_package_data=True,
-    packages=setuptools.find_packages(),
-    install_requires=[
-        "fsspec ==2021.5.0",
-        "gcsfs ==2021.5.0",
-
-        "pandas >=1.1.4",
-        "SQLAlchemy >=1.3.20, <1.4",
-        "psycopg2_binary >=2.8.4",
-        "cloudpickle >=1.6.0",
-        "PyYAML >=5.3.1",
-        "Pillow >=7.2.0, <8",
-        "tqdm >=4.60.0",
-        "toml >=0.10.2",
-        "click >=7.1.2",
-        "requests >= 2.24.0",
-    ],
-    extras_require={
-        "excel": [
-            "xlrd >=2.0.1",
-            "openpyxl >=3.0.7",
-        ],
-        "ui": [
-            "dash >=1.20.0",
-            "dash_bootstrap_components >= 0.12.0",
-            "dash_interactive_graphviz >=0.3.0",
-        ],
-        "label-studio": [
-            "label-studio >=1.0.1",
-        ]
-    },
-    python_requires='>=3.8'
-)


Test patch:
diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml
index 1d093e74..414a8746 100644
--- a/.github/workflows/pytest.yaml
+++ b/.github/workflows/pytest.yaml
@@ -1,9 +1,51 @@
-name: Python package
+name: Run tests
 
 on: [push]
 
 jobs:
-  build_and_test:
+  lint:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        python-version: [3.8]
+
+    steps:
+    - uses: actions/checkout@v2
+
+    - name: Set up Python ${{ matrix.python-version }}
+      uses: actions/setup-python@v2
+      with:
+        python-version: ${{ matrix.python-version }}
+    
+    - name: Setup poetry
+      run: |
+        pip install poetry==1.1.6
+
+    - name: Cache Python dependencies
+      uses: actions/cache@v2
+      with:
+        path: /home/runner/.cache/pypoetry/virtualenvs
+        key: ${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('poetry.lock') }}
+        restore-keys: |
+          ${{ runner.os }}-${{ matrix.python-version }}
+
+    - name: Install dependencies
+      run: |
+        poetry install -E excel -E ui
+
+    - name: Lint with flake8
+      run: |
+        # stop the build if there are Python syntax errors or undefined names
+        poetry run flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
+        # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
+        poetry run flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
+
+    - name: Lint with mypy
+      run: |
+        poetry run mypy -p datapipe --ignore-missing-imports --follow-imports=silent
+
+  test:
+    needs: lint
 
     runs-on: ubuntu-latest
     strategy:
@@ -28,31 +70,38 @@ jobs:
         ports:
           - 5432:5432
 
-      label-studio:
-        image: heartexlabs/label-studio:1.0.1
-        ports:
-            - 8080:8080
+    #   label-studio:
+    #     image: heartexlabs/label-studio:1.0.1
+    #     ports:
+    #         - 8080:8080
 
     steps:
     - uses: actions/checkout@v2
+
     - name: Set up Python ${{ matrix.python-version }}
       uses: actions/setup-python@v2
       with:
         python-version: ${{ matrix.python-version }}
-    - name: Install dependencies
+    
+    - name: Setup poetry
       run: |
-        python -m pip install --upgrade pip
-        pip install flake8 pytest
-        pip install .
-    - name: Lint with flake8
+        pip install poetry==1.1.6
+
+    - name: Cache Python dependencies
+      uses: actions/cache@v2
+      with:
+        path: /home/runner/.cache/pypoetry/virtualenvs
+        key: ${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('poetry.lock') }}
+        restore-keys: |
+          ${{ runner.os }}-${{ matrix.python-version }}
+
+    - name: Install dependencies
       run: |
-        # stop the build if there are Python syntax errors or undefined names
-        flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
-        # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
-        flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
+        poetry install -E excel -E ui
+
     - name: Test with pytest
       run: |
-        pytest
+        poetry run pytest
       env:
         TEST_DB_ENV: ${{ matrix.test-db-env }}
         # The hostname used to communicate with the PostgreSQL service container
diff --git a/datapipe/debug_ui/e2e.py b/datapipe/debug_ui/e2e.py
index bfb26300..0b9bacbd 100644
--- a/datapipe/debug_ui/e2e.py
+++ b/datapipe/debug_ui/e2e.py
@@ -28,6 +28,7 @@ def ui_e2e_index(app, ms, catalog, pipeline):
         )
     ]
 
+
 def ui_e2e_setup(app, ms, catalog, pipeline):
     @app.callback(
         Output('e2e-content', 'children'),
@@ -36,7 +37,7 @@ def ui_e2e_setup(app, ms, catalog, pipeline):
     def update_e2e(id_value):
         if not id_value:
             raise PreventUpdate
-        
+
         res = []
 
         for name, tbl in catalog.catalog.items():
@@ -47,5 +48,5 @@ def update_e2e(id_value):
                 html.H1(name),
                 str(val.to_dict()),
             ])
-        
+
         return res
diff --git a/tests/test_chunked_processing_pipeline.py b/tests/test_chunked_processing_pipeline.py
index b3c86e9a..bf780b4a 100644
--- a/tests/test_chunked_processing_pipeline.py
+++ b/tests/test_chunked_processing_pipeline.py
@@ -1,13 +1,14 @@
 import os
 import numpy as np
 import pandas as pd
+
+from datapipe.datatable import DataStore
 from datapipe.compute import build_compute, run_steps
 from datapipe.store.pandas import TableStoreJsonLine
-from datapipe.metastore import MetaStore
 from datapipe.dsl import Catalog, ExternalTable, Pipeline, BatchTransform, Table
 
 
-CHUNK_SIZE = 1000
+CHUNK_SIZE = 100
 
 
 def test_table_store_json_line_reading(tmp_dir, dbconn):
@@ -24,7 +25,7 @@ def conversion(df):
     test_output_fname = os.path.join(tmp_dir, "table-output-pandas.json")
     test_df.to_json(test_input_fname, orient="records", lines=True)
 
-    ms = MetaStore(dbconn)
+    ds = DataStore(dbconn)
     catalog = Catalog({
         "input_data": ExternalTable(
             store=TableStoreJsonLine(test_input_fname),
@@ -42,10 +43,10 @@ def conversion(df):
         ),
     ])
 
-    steps = build_compute(ms, catalog, pipeline)
-    run_steps(ms, steps)
+    steps = build_compute(ds, catalog, pipeline)
+    run_steps(ds, steps)
 
-    df_transformed = catalog.get_datatable(ms, 'output_data').get_data()
+    df_transformed = catalog.get_datatable(ds, 'output_data').get_data()
     assert len(df_transformed) == 2 * CHUNK_SIZE
     assert all(df_transformed["y"].values == (df_transformed["x"].values ** 2))
     assert len(set(df_transformed["x"].values).symmetric_difference(set(x))) == 0
diff --git a/tests/test_datatable.py b/tests/test_datatable.py
index 7567eef1..5d7ab484 100644
--- a/tests/test_datatable.py
+++ b/tests/test_datatable.py
@@ -1,3 +1,4 @@
+from typing import cast
 import pytest
 
 import cloudpickle
@@ -6,21 +7,22 @@
 from sqlalchemy.sql.sqltypes import Integer
 
 from datapipe.store.database import TableStoreDB
-from datapipe.datatable import DataTable, gen_process, gen_process_many, inc_process, inc_process_many
-from datapipe.metastore import MetaStore
+from datapipe.datatable import DataStore, gen_process, gen_process_many, inc_process, inc_process_many
+from datapipe.types import IndexDF, data_to_index
 
-from .util import assert_df_equal, assert_idx_equal
+from .util import assert_df_equal, assert_datatable_equal
 
 
 TEST_SCHEMA = [
+    Column('id', Integer, primary_key=True),
     Column('a', Integer),
 ]
 
 TEST_DF = pd.DataFrame(
     {
-        'a': range(10)
+        'id': range(10),
+        'a': range(10),
     },
-    index=pd.Index([f'id_{i}' for i in range(10)], name='id'),
 )
 
 
@@ -37,86 +39,87 @@ def f(*args, **kwargs):
 
 
 def test_cloudpickle(dbconn) -> None:
-    ms = MetaStore(dbconn)
+    ds = DataStore(meta_dbconn=dbconn)
 
-    tbl = DataTable(
-        'test',
-        meta_table=ms.create_meta_table('test'),
+    tbl = ds.create_table(
+        name='test',
         table_store=TableStoreDB(dbconn, 'test_data', TEST_SCHEMA, True)
     )
 
-    cloudpickle.dumps([ms, tbl])
+    cloudpickle.dumps([ds, tbl])
 
 
 def test_simple(dbconn) -> None:
-    ms = MetaStore(dbconn)
+    ds = DataStore(dbconn)
 
-    tbl = DataTable(
+    tbl = ds.create_table(
         'test',
-        meta_table=ms.create_meta_table('test'),
         table_store=TableStoreDB(dbconn, 'test_data', TEST_SCHEMA, True)
     )
 
-    tbl.store(TEST_DF)
+    tbl.store_chunk(TEST_DF)
+
+    assert_datatable_equal(tbl, TEST_DF)
 
 
 def test_store_less_values(dbconn) -> None:
-    ms = MetaStore(dbconn)
+    ds = DataStore(dbconn)
 
-    tbl = DataTable(
+    tbl = ds.create_table(
         'test',
-        meta_table=ms.create_meta_table('test'),
-        table_store=TableStoreDB(dbconn, 'test_data', TEST_SCHEMA, True))
+        table_store=TableStoreDB(dbconn, 'test_data', TEST_SCHEMA, True)
+    )
 
-    tbl.store(TEST_DF)
-    assert_idx_equal(tbl.get_metadata().index, TEST_DF.index)
+    tbl.store_chunk(TEST_DF)
+    assert_datatable_equal(tbl, TEST_DF)
 
-    tbl.store(TEST_DF[:5])
-    assert_idx_equal(tbl.get_metadata().index, TEST_DF[:5].index)
+    tbl.store_chunk(TEST_DF[:5], processed_idx=data_to_index(TEST_DF, tbl.primary_keys))
+    assert_datatable_equal(tbl, TEST_DF[:5])
 
 
 def test_get_process_ids(dbconn) -> None:
-    ms = MetaStore(dbconn)
+    ds = DataStore(dbconn)
 
-    tbl1 = DataTable(
+    tbl1 = ds.create_table(
         'tbl1',
-        meta_table=ms.create_meta_table('tbl1'),
-        table_store=TableStoreDB(dbconn, 'tbl1_data', TEST_SCHEMA, True))
-    tbl2 = DataTable(
+        table_store=TableStoreDB(dbconn, 'tbl1_data', TEST_SCHEMA, True)
+    )
+    tbl2 = ds.create_table(
         'tbl2',
-        meta_table=ms.create_meta_table('tbl2'),
-        table_store=TableStoreDB(dbconn, 'tbl2_data', TEST_SCHEMA, True))
+        table_store=TableStoreDB(dbconn, 'tbl2_data', TEST_SCHEMA, True)
+    )
 
-    tbl1.store(TEST_DF)
+    tbl1.store_chunk(TEST_DF)
 
-    count, idx_dfs = ms.get_process_ids([tbl1.meta_table], [tbl2.meta_table])
+    count, idx_dfs = ds.get_process_ids([tbl1], [tbl2])
     idx = pd.concat(list(idx_dfs))
 
     assert(sorted(list(idx.index)) == list(TEST_DF.index))
 
-    tbl2.store(tbl1.get_data())
+    tbl2.store_chunk(tbl1.get_data())
 
     upd_df = TEST_DF[:5].copy()
     upd_df['a'] += 1
 
     tbl1.store_chunk(upd_df)
 
-    count, idx_dfs = ms.get_process_ids([tbl1.meta_table], [tbl2.meta_table])
+    count, idx_dfs = ds.get_process_ids([tbl1], [tbl2])
     idx = pd.concat(list(idx_dfs))
-    assert(sorted(list(idx.index)) == list(upd_df.index))
+
+    assert_df_equal(idx, upd_df[['id']])
 
 
 def test_gen_process(dbconn) -> None:
-    ms = MetaStore(dbconn)
+    ds = DataStore(dbconn)
 
-    tbl1_gen = DataTable(
+    tbl1_gen = ds.create_table(
         'tbl1_gen',
-        meta_table=ms.create_meta_table('tbl1_gen'),
-        table_store=TableStoreDB(dbconn, 'tbl1_gen_data', TEST_SCHEMA, True))
-    tbl1 = DataTable(
+        table_store=TableStoreDB(dbconn, 'tbl1_gen_data', TEST_SCHEMA, True)
+    )
+    tbl1 = ds.create_table(
         'tbl1',
-        meta_table=ms.create_meta_table('tbl1'),
-        table_store=TableStoreDB(dbconn, 'tbl1_data', TEST_SCHEMA, True))
+        table_store=TableStoreDB(dbconn, 'tbl1_data', TEST_SCHEMA, True)
+    )
 
     def gen():
         yield TEST_DF
@@ -135,7 +138,7 @@ def func():
             func
         )
 
-    assert(assert_df_equal(tbl1_gen.get_data(), TEST_DF))
+    assert_datatable_equal(tbl1_gen, TEST_DF)
 
     def gen2():
         yield TEST_DF[:5]
@@ -154,178 +157,178 @@ def func2():
             func2
         )
 
-    assert(assert_df_equal(tbl1_gen.get_data(), TEST_DF[:5]))
+    assert_datatable_equal(tbl1_gen, TEST_DF[:5])
 
 
 def test_inc_process_modify_values(dbconn) -> None:
-    ms = MetaStore(dbconn)
+    ds = DataStore(dbconn)
 
-    tbl1 = DataTable(
+    tbl1 = ds.create_table(
         'tbl1',
-        meta_table=ms.create_meta_table('tbl1'),
-        table_store=TableStoreDB(dbconn, 'tbl1_data', TEST_SCHEMA, True))
-    tbl2 = DataTable(
+        table_store=TableStoreDB(dbconn, 'tbl1_data', TEST_SCHEMA, True)
+    )
+    tbl2 = ds.create_table(
         'tbl2',
-        meta_table=ms.create_meta_table('tbl2'),
-        table_store=TableStoreDB(dbconn, 'tbl2_data', TEST_SCHEMA, True))
+        table_store=TableStoreDB(dbconn, 'tbl2_data', TEST_SCHEMA, True)
+    )
 
     def id_func(df):
         return df
 
-    tbl1.store(TEST_DF)
+    tbl1.store_chunk(TEST_DF)
 
-    inc_process(ms, [tbl1], tbl2, id_func)
+    inc_process(ds, [tbl1], tbl2, id_func)
 
-    assert(assert_df_equal(tbl2.get_data(), TEST_DF))
+    assert_datatable_equal(tbl2, TEST_DF)
 
     ##########################
-    tbl1.store(TEST_DF_INC1)
+    tbl1.store_chunk(TEST_DF_INC1)
 
-    inc_process(ms, [tbl1], tbl2, id_func)
+    inc_process(ds, [tbl1], tbl2, id_func)
 
-    assert(assert_df_equal(tbl2.get_data(), TEST_DF_INC1))
+    assert_datatable_equal(tbl2, TEST_DF_INC1)
 
 
 def test_inc_process_delete_values_from_input(dbconn) -> None:
-    ms = MetaStore(dbconn)
+    ds = DataStore(dbconn)
 
-    tbl1 = DataTable(
+    tbl1 = ds.create_table(
         'tbl1',
-        meta_table=ms.create_meta_table('tbl1'),
-        table_store=TableStoreDB(dbconn, 'tbl1_data', TEST_SCHEMA, True))
-    tbl2 = DataTable(
+        table_store=TableStoreDB(dbconn, 'tbl1_data', TEST_SCHEMA, True)
+    )
+    tbl2 = ds.create_table(
         'tbl2',
-        meta_table=ms.create_meta_table('tbl2'),
-        table_store=TableStoreDB(dbconn, 'tbl2_data', TEST_SCHEMA, True))
+        table_store=TableStoreDB(dbconn, 'tbl2_data', TEST_SCHEMA, True)
+    )
 
     def id_func(df):
         return df
 
-    tbl1.store(TEST_DF)
+    tbl1.store_chunk(TEST_DF)
 
-    inc_process(ms, [tbl1], tbl2, id_func)
+    inc_process(ds, [tbl1], tbl2, id_func)
 
-    assert(assert_df_equal(tbl2.get_data(), TEST_DF))
+    assert_datatable_equal(tbl2, TEST_DF)
 
     ##########################
-    tbl1.store(TEST_DF[:5])
+    tbl1.store_chunk(TEST_DF[:5], processed_idx=data_to_index(TEST_DF, tbl1.primary_keys))
 
-    inc_process(ms, [tbl1], tbl2, id_func, chunksize=2)
+    inc_process(ds, [tbl1], tbl2, id_func, chunksize=2)
 
-    assert(assert_df_equal(tbl2.get_data(), TEST_DF[:5]))
+    assert_datatable_equal(tbl2, TEST_DF[:5])
 
 
 def test_inc_process_delete_values_from_proc(dbconn) -> None:
-    ms = MetaStore(dbconn)
+    ds = DataStore(dbconn)
 
-    tbl1 = DataTable(
+    tbl1 = ds.create_table(
         'tbl1',
-        meta_table=ms.create_meta_table('tbl1'),
-        table_store=TableStoreDB(dbconn, 'tbl1_data', TEST_SCHEMA, True))
-    tbl2 = DataTable(
+        table_store=TableStoreDB(dbconn, 'tbl1_data', TEST_SCHEMA, True)
+    )
+    tbl2 = ds.create_table(
         'tbl2',
-        meta_table=ms.create_meta_table('tbl2'),
-        table_store=TableStoreDB(dbconn, 'tbl2_data', TEST_SCHEMA, True))
+        table_store=TableStoreDB(dbconn, 'tbl2_data', TEST_SCHEMA, True)
+    )
 
     def id_func(df):
         return df[:5]
 
-    tbl2.store(TEST_DF)
+    tbl2.store_chunk(TEST_DF)
 
-    tbl1.store(TEST_DF)
+    tbl1.store_chunk(TEST_DF)
 
-    inc_process(ms, [tbl1], tbl2, id_func)
+    inc_process(ds, [tbl1], tbl2, id_func)
 
-    assert(assert_df_equal(tbl2.get_data(), TEST_DF[:5]))
+    assert_datatable_equal(tbl2, TEST_DF[:5])
 
 
 def test_inc_process_proc_no_change(dbconn) -> None:
-    ms = MetaStore(dbconn)
+    ds = DataStore(dbconn)
 
-    tbl1 = DataTable(
+    tbl1 = ds.create_table(
         'tbl1',
-        meta_table=ms.create_meta_table('tbl1'),
-        table_store=TableStoreDB(dbconn, 'tbl1_data', TEST_SCHEMA, True))
-    tbl2 = DataTable(
+        table_store=TableStoreDB(dbconn, 'tbl1_data', TEST_SCHEMA, True)
+    )
+    tbl2 = ds.create_table(
         'tbl2',
-        meta_table=ms.create_meta_table('tbl2'),
-        table_store=TableStoreDB(dbconn, 'tbl2_data', TEST_SCHEMA, True))
+        table_store=TableStoreDB(dbconn, 'tbl2_data', TEST_SCHEMA, True)
+    )
 
     def id_func(df):
         return TEST_DF
 
-    tbl2.store(TEST_DF)
-    tbl1.store(TEST_DF)
+    tbl2.store_chunk(TEST_DF)
+    tbl1.store_chunk(TEST_DF)
 
-    count, idx_gen = ms.get_process_ids([tbl1.meta_table], [tbl2.meta_table])
+    count, idx_gen = ds.get_process_ids([tbl1], [tbl2])
     idx_dfs = list(idx_gen)
-    idx = pd.concat(idx_dfs) if len(idx_dfs) > 0 else []
+    idx_len = len(pd.concat(idx_dfs)) if len(idx_dfs) > 0 else 0
 
-    assert(len(idx) == len(TEST_DF))
+    assert(idx_len == len(TEST_DF))
 
-    inc_process(ms, [tbl1], tbl2, id_func)
+    inc_process(ds, [tbl1], tbl2, id_func)
 
-    count, idx_gen = ms.get_process_ids([tbl1.meta_table], [tbl2.meta_table])
+    count, idx_gen = ds.get_process_ids([tbl1], [tbl2])
     idx_dfs = list(idx_gen)
-    idx = pd.concat(idx_dfs) if len(idx_dfs) > 0 else []
+    idx_len = len(pd.concat(idx_dfs)) if len(idx_dfs) > 0 else 0
 
-    assert(len(idx) == 0)
+    assert(idx_len == 0)
 
-    tbl1.store(TEST_DF_INC1)
+    tbl1.store_chunk(TEST_DF_INC1)
 
-    count, idx_gen = ms.get_process_ids([tbl1.meta_table], [tbl2.meta_table])
+    count, idx_gen = ds.get_process_ids([tbl1], [tbl2])
     idx_dfs = list(idx_gen)
-    idx = pd.concat(idx_dfs) if len(idx_dfs) > 0 else []
+    idx_len = len(pd.concat(idx_dfs)) if len(idx_dfs) > 0 else 0
 
-    assert(len(idx) == len(TEST_DF))
+    assert(idx_len == len(TEST_DF))
 
-    inc_process(ms, [tbl1], tbl2, id_func)
+    inc_process(ds, [tbl1], tbl2, id_func)
 
-    count, idx_gen = ms.get_process_ids([tbl1.meta_table], [tbl2.meta_table])
+    count, idx_gen = ds.get_process_ids([tbl1], [tbl2])
     idx_dfs = list(idx_gen)
-    idx = pd.concat(idx_dfs) if len(idx_dfs) > 0 else []
+    idx_len = len(pd.concat(idx_dfs)) if len(idx_dfs) > 0 else 0
 
-    assert(len(idx) == 0)
+    assert(idx_len == 0)
 
 # TODO тест inc_process 2->1
 # TODO тест inc_process 2->1, удаление строки, 2->1
 
 
 def test_gen_process_many(dbconn) -> None:
-    ms = MetaStore(dbconn)
+    ds = DataStore(dbconn)
 
-    tbl_gen = DataTable(
+    tbl_gen = ds.create_table(
         'tbl_gen',
-        meta_table=ms.create_meta_table('tbl_gen'),
-        table_store=TableStoreDB(dbconn, 'tbl_gen_data', TEST_SCHEMA, True))
-    tbl1_gen = DataTable(
+        table_store=TableStoreDB(dbconn, 'tbl_gen_data', TEST_SCHEMA, True)
+    )
+    tbl1_gen = ds.create_table(
         'tbl1_gen',
-        meta_table=ms.create_meta_table('tbl1_gen'),
-        table_store=TableStoreDB(dbconn, 'tbl1_gen_data', TEST_SCHEMA, True))
-    tbl2_gen = DataTable(
+        table_store=TableStoreDB(dbconn, 'tbl1_gen_data', TEST_SCHEMA, True)
+    )
+    tbl2_gen = ds.create_table(
         'tbl2_gen',
-        meta_table=ms.create_meta_table('tbl2_gen'),
-        table_store=TableStoreDB(dbconn, 'tbl2_gen_data', TEST_SCHEMA, True))
-    tbl3_gen = DataTable(
+        table_store=TableStoreDB(dbconn, 'tbl2_gen_data', TEST_SCHEMA, True)
+    )
+    tbl3_gen = ds.create_table(
         'tbl3_gen',
-        meta_table=ms.create_meta_table('tbl3_gen'),
-        table_store=TableStoreDB(dbconn, 'tbl3_gen_data', TEST_SCHEMA, True))
-    tbl = DataTable(
+        table_store=TableStoreDB(dbconn, 'tbl3_gen_data', TEST_SCHEMA, True)
+    )
+    tbl = ds.create_table(
         'tbl',
-        meta_table=ms.create_meta_table('tbl'),
-        table_store=TableStoreDB(dbconn, 'tbl_data', TEST_SCHEMA, True))
-    tbl1 = DataTable(
+        table_store=TableStoreDB(dbconn, 'tbl_data', TEST_SCHEMA, True)
+    )
+    tbl1 = ds.create_table(
         'tbl1',
-        meta_table=ms.create_meta_table('tbl1'),
-        table_store=TableStoreDB(dbconn, 'tbl1_data', TEST_SCHEMA, True))
-    tbl2 = DataTable(
+        table_store=TableStoreDB(dbconn, 'tbl1_data', TEST_SCHEMA, True)
+    )
+    tbl2 = ds.create_table(
         'tbl2',
-        meta_table=ms.create_meta_table('tbl2'),
-        table_store=TableStoreDB(dbconn, 'tbl2_data', TEST_SCHEMA, True))
-    tbl3 = DataTable(
+        table_store=TableStoreDB(dbconn, 'tbl2_data', TEST_SCHEMA, True)
+    )
+    tbl3 = ds.create_table(
         'tbl3',
-        meta_table=ms.create_meta_table('tbl3'),
-        table_store=TableStoreDB(dbconn, 'tbl3_data', TEST_SCHEMA, True))
+        table_store=TableStoreDB(dbconn, 'tbl3_data', TEST_SCHEMA, True)
+    )
 
     def gen():
         yield (TEST_DF, TEST_DF_INC1, TEST_DF_INC2, TEST_DF_INC3)
@@ -344,31 +347,31 @@ def func():
             func
         )
 
-    assert(assert_df_equal(tbl_gen.get_data(), TEST_DF))
-    assert(assert_df_equal(tbl1_gen.get_data(), TEST_DF_INC1))
-    assert(assert_df_equal(tbl2_gen.get_data(), TEST_DF_INC2))
-    assert(assert_df_equal(tbl3_gen.get_data(), TEST_DF_INC3))
+    assert_datatable_equal(tbl_gen, TEST_DF)
+    assert_datatable_equal(tbl1_gen, TEST_DF_INC1)
+    assert_datatable_equal(tbl2_gen, TEST_DF_INC2)
+    assert_datatable_equal(tbl3_gen, TEST_DF_INC3)
 
 
 def test_inc_process_many_modify_values(dbconn) -> None:
-    ms = MetaStore(dbconn)
+    ds = DataStore(dbconn)
 
-    tbl = DataTable(
+    tbl = ds.create_table(
         'tbl',
-        meta_table=ms.create_meta_table('tbl'),
-        table_store=TableStoreDB(dbconn, 'tbl_data', TEST_SCHEMA, True))
-    tbl1 = DataTable(
+        table_store=TableStoreDB(dbconn, 'tbl_data', TEST_SCHEMA, True)
+    )
+    tbl1 = ds.create_table(
         'tbl1',
-        meta_table=ms.create_meta_table('tbl1'),
-        table_store=TableStoreDB(dbconn, 'tbl1_data', TEST_SCHEMA, True))
-    tbl2 = DataTable(
+        table_store=TableStoreDB(dbconn, 'tbl1_data', TEST_SCHEMA, True)
+    )
+    tbl2 = ds.create_table(
         'tbl2',
-        meta_table=ms.create_meta_table('tbl2'),
-        table_store=TableStoreDB(dbconn, 'tbl2_data', TEST_SCHEMA, True))
-    tbl3 = DataTable(
+        table_store=TableStoreDB(dbconn, 'tbl2_data', TEST_SCHEMA, True)
+    )
+    tbl3 = ds.create_table(
         'tbl3',
-        meta_table=ms.create_meta_table('tbl3'),
-        table_store=TableStoreDB(dbconn, 'tbl3_data', TEST_SCHEMA, True))
+        table_store=TableStoreDB(dbconn, 'tbl3_data', TEST_SCHEMA, True)
+    )
 
     def inc_func(df):
         df1 = df.copy()
@@ -379,16 +382,16 @@ def inc_func(df):
         df3['a'] += 3
         return df1, df2, df3
 
-    tbl.store(TEST_DF)
+    tbl.store_chunk(TEST_DF)
 
-    inc_process_many(ms, [tbl], [tbl1, tbl2, tbl3], inc_func)
+    inc_process_many(ds, [tbl], [tbl1, tbl2, tbl3], inc_func)
 
-    assert(assert_df_equal(tbl1.get_data(), TEST_DF_INC1))
-    assert(assert_df_equal(tbl2.get_data(), TEST_DF_INC2))
-    assert(assert_df_equal(tbl3.get_data(), TEST_DF_INC3))
+    assert_datatable_equal(tbl1, TEST_DF_INC1)
+    assert_datatable_equal(tbl2, TEST_DF_INC2)
+    assert_datatable_equal(tbl3, TEST_DF_INC3)
 
     ##########################
-    tbl.store(TEST_DF[:5])
+    tbl.store_chunk(TEST_DF[:5], processed_idx=data_to_index(TEST_DF, tbl.primary_keys))
 
     def inc_func_inv(df):
         df1 = df.copy()
@@ -399,201 +402,199 @@ def inc_func_inv(df):
         df3['a'] += 3
         return df3, df2, df1
 
-    inc_process_many(ms, [tbl], [tbl3, tbl2, tbl1], inc_func_inv)
+    inc_process_many(ds, [tbl], [tbl3, tbl2, tbl1], inc_func_inv)
 
-    assert(assert_df_equal(tbl1.get_data(), TEST_DF_INC1[:5]))
-    assert(assert_df_equal(tbl2.get_data(), TEST_DF_INC2[:5]))
-    assert(assert_df_equal(tbl3.get_data(), TEST_DF_INC3[:5]))
+    assert_datatable_equal(tbl1, TEST_DF_INC1[:5])
+    assert_datatable_equal(tbl2, TEST_DF_INC2[:5])
+    assert_datatable_equal(tbl3, TEST_DF_INC3[:5])
 
     ##########################
 
     tbl.store_chunk(TEST_DF[5:])
 
-    inc_process_many(ms, [tbl], [tbl1, tbl2, tbl3], inc_func)
-    assert(assert_df_equal(tbl1.get_data(), TEST_DF_INC1))
-    assert(assert_df_equal(tbl2.get_data(), TEST_DF_INC2))
-    assert(assert_df_equal(tbl3.get_data(), TEST_DF_INC3))
+    inc_process_many(ds, [tbl], [tbl1, tbl2, tbl3], inc_func)
+    assert_datatable_equal(tbl1, TEST_DF_INC1)
+    assert_datatable_equal(tbl2, TEST_DF_INC2)
+    assert_datatable_equal(tbl3, TEST_DF_INC3)
 
 
 def test_inc_process_many_several_inputs(dbconn) -> None:
-    ms = MetaStore(dbconn)
+    ds = DataStore(dbconn)
 
-    tbl = DataTable(
+    tbl = ds.create_table(
         'tbl',
-        meta_table=ms.create_meta_table('tbl'),
         table_store=TableStoreDB(
-            dbconn, 'tbl_data', [Column('a_first', Integer), Column('a_second', Integer)], True
+            dbconn,
+            'tbl_data',
+            [
+                Column('id', Integer, primary_key=True),
+                Column('a_first', Integer),
+                Column('a_second', Integer)
+            ],
+            True
         )
     )
-    tbl1 = DataTable(
+    tbl1 = ds.create_table(
         'tbl1',
-        meta_table=ms.create_meta_table('tbl1'),
-        table_store=TableStoreDB(dbconn, 'tbl1_data', TEST_SCHEMA, True))
-    tbl2 = DataTable(
+        table_store=TableStoreDB(dbconn, 'tbl1_data', TEST_SCHEMA, True)
+    )
+    tbl2 = ds.create_table(
         'tbl2',
-        meta_table=ms.create_meta_table('tbl2'),
-        table_store=TableStoreDB(dbconn, 'tbl2_data', TEST_SCHEMA, True))
+        table_store=TableStoreDB(dbconn, 'tbl2_data', TEST_SCHEMA, True)
+    )
 
     def inc_func(df1, df2):
         df = pd.merge(
             left=df1,
             right=df2,
-            left_index=True,
-            right_index=True,
+            on=['id'],
             suffixes=('_first', '_second')
         )
         df['a_first'] += 1
         df['a_second'] += 2
         return df
 
-    tbl1.store(TEST_DF)
-    tbl2.store(TEST_DF)
-
-    inc_process_many(ms, [tbl1, tbl2], [tbl], inc_func)
-    assert(
-        assert_df_equal(
-            tbl.get_data(),
-            pd.DataFrame(
-                {
-                    'a_first': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
-                    'a_second': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
-                },
-                index=[f'id_{i}' for i in range(10)]
-            )
+    tbl1.store_chunk(TEST_DF)
+    tbl2.store_chunk(TEST_DF)
+
+    inc_process_many(ds, [tbl1, tbl2], [tbl], inc_func)
+    assert_datatable_equal(
+        tbl,
+        pd.DataFrame(
+            {
+                'id': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+                'a_first': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
+                'a_second': [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
+            }
         )
     )
 
-    changed_indexes = [f'id_{i}' for i in [0, 4, 6]]
-    not_changed_indexes = [f'id_{i}' for i in [1, 2, 3, 5, 7, 8, 9]]
+    changed_ids = [0, 4, 6]
+    changed_ids_df = cast(IndexDF, pd.DataFrame({'id': changed_ids}))
+    not_changed_ids = [1, 2, 3, 5, 7, 8, 9]
+    not_changed_ids_df = cast(IndexDF, pd.DataFrame({'id': not_changed_ids}))
 
     tbl2.store_chunk(
         pd.DataFrame(
             {
+                'id': changed_ids,
                 'a': [10, 10, 10]
-            },
-            index=changed_indexes
+            }
         )
     )
 
-    inc_process_many(ms, [tbl1, tbl2], [tbl], inc_func)
+    inc_process_many(ds, [tbl1, tbl2], [tbl], inc_func)
 
-    assert(
-        assert_df_equal(
-            tbl.get_data(idx=changed_indexes),
-            pd.DataFrame(
-                {
-                    'a_first': [1, 5, 7],
-                    'a_second': [12, 12, 12]
-                },
-                index=changed_indexes
-            )
+    assert_df_equal(
+        tbl.get_data(idx=changed_ids_df),
+        pd.DataFrame(
+            {
+                'id': changed_ids,
+                'a_first': [1, 5, 7],
+                'a_second': [12, 12, 12]
+            }
         )
     )
-    assert(
-        assert_df_equal(
-            tbl.get_data(idx=not_changed_indexes),
-            pd.DataFrame(
-                {
-                    'a_first': [2, 3, 4, 6, 8, 9, 10],
-                    'a_second': [3, 4, 5, 7, 9, 10, 11]
-                },
-                index=not_changed_indexes
-            )
+
+    assert_df_equal(
+        tbl.get_data(idx=not_changed_ids_df),
+        pd.DataFrame(
+            {
+                'id': not_changed_ids,
+                'a_first': [2, 3, 4, 6, 8, 9, 10],
+                'a_second': [3, 4, 5, 7, 9, 10, 11]
+            }
         )
     )
 
     tbl1.store_chunk(
         pd.DataFrame(
             {
+                'id': changed_ids,
                 'a': [20, 20, 20]
-            },
-            index=changed_indexes
+            }
         )
     )
 
-    inc_process_many(ms, [tbl1, tbl2], [tbl], inc_func)
+    inc_process_many(ds, [tbl1, tbl2], [tbl], inc_func)
 
-    assert(
-        assert_df_equal(
-            tbl.get_data(idx=changed_indexes),
-            pd.DataFrame(
-                {
-                    'a_first': [21, 21, 21],
-                    'a_second': [12, 12, 12]
-                },
-                index=changed_indexes
-            )
+    assert_df_equal(
+        tbl.get_data(idx=changed_ids_df),
+        pd.DataFrame(
+            {
+                'id': changed_ids,
+                'a_first': [21, 21, 21],
+                'a_second': [12, 12, 12]
+            }
         )
     )
-    assert(
-        assert_df_equal(
-            tbl.get_data(idx=not_changed_indexes),
-            pd.DataFrame(
-                {
-                    'a_first': [2, 3, 4, 6, 8, 9, 10],
-                    'a_second': [3, 4, 5, 7, 9, 10, 11]
-                },
-                index=not_changed_indexes
-            )
+
+    assert_df_equal(
+        tbl.get_data(idx=not_changed_ids_df),
+        pd.DataFrame(
+            {
+                'id': not_changed_ids,
+                'a_first': [2, 3, 4, 6, 8, 9, 10],
+                'a_second': [3, 4, 5, 7, 9, 10, 11]
+            }
         )
     )
 
 
 def test_inc_process_many_several_outputs(dbconn) -> None:
-    ms = MetaStore(dbconn)
+    ds = DataStore(dbconn)
 
-    BAD_IDXS = ['id_0', 'id_1', 'id_5', 'id_8']
-    GOOD_IDXS = ['id_2', 'id_3', 'id_4', 'id_6', 'id_7', 'id_9']
+    bad_ids = [0, 1, 5, 8]
+    good_ids = [2, 3, 4, 6, 7, 9]
 
-    tbl = DataTable(
+    tbl = ds.create_table(
         'tbl',
-        meta_table=ms.create_meta_table('tbl'),
-        table_store=TableStoreDB(dbconn, 'tbl_data', TEST_SCHEMA, True))
-    tbl_good = DataTable(
+        table_store=TableStoreDB(dbconn, 'tbl_data', TEST_SCHEMA, True)
+    )
+    tbl_good = ds.create_table(
         'tbl_good',
-        meta_table=ms.create_meta_table('tbl_good'),
-        table_store=TableStoreDB(dbconn, 'tbl_good_data', TEST_SCHEMA, True))
-    tbl_bad = DataTable(
+        table_store=TableStoreDB(dbconn, 'tbl_good_data', TEST_SCHEMA, True)
+    )
+    tbl_bad = ds.create_table(
         'tbl_bad',
-        meta_table=ms.create_meta_table('tbl_bad'),
-        table_store=TableStoreDB(dbconn, 'tbl_bad_data', TEST_SCHEMA, True))
+        table_store=TableStoreDB(dbconn, 'tbl_bad_data', TEST_SCHEMA, True)
+    )
 
-    tbl.store(TEST_DF)
+    tbl.store_chunk(TEST_DF)
 
     def inc_func(df):
-        df_good = df.drop(index=BAD_IDXS)
-        df_bad = df.drop(index=GOOD_IDXS)
+        df_good = df[df['id'].isin(good_ids)]
+        df_bad = df[df['id'].isin(bad_ids)]
         return df_good, df_bad
 
-    inc_process_many(ms, [tbl], [tbl_good, tbl_bad], inc_func)
-    assert(assert_df_equal(tbl.get_data(), TEST_DF))
-    assert(assert_df_equal(tbl_good.get_data(), TEST_DF.loc[GOOD_IDXS]))
-    assert(assert_df_equal(tbl_bad.get_data(), TEST_DF.loc[BAD_IDXS]))
+    inc_process_many(ds, [tbl], [tbl_good, tbl_bad], inc_func)
+    assert_datatable_equal(tbl, TEST_DF)
+    assert_datatable_equal(tbl_good, TEST_DF.loc[good_ids])
+    assert_datatable_equal(tbl_bad, TEST_DF.loc[bad_ids])
 
     # Check this not delete the tables
-    inc_process_many(ms, [tbl], [tbl_good, tbl_bad], inc_func)
-    assert(assert_df_equal(tbl.get_data(), TEST_DF))
-    assert(assert_df_equal(tbl_good.get_data(), TEST_DF.loc[GOOD_IDXS]))
-    assert(assert_df_equal(tbl_bad.get_data(), TEST_DF.loc[BAD_IDXS]))
+    inc_process_many(ds, [tbl], [tbl_good, tbl_bad], inc_func)
+    assert_datatable_equal(tbl, TEST_DF)
+    assert_datatable_equal(tbl_good, TEST_DF.loc[good_ids])
+    assert_datatable_equal(tbl_bad, TEST_DF.loc[bad_ids])
 
 
 def test_error_handling(dbconn) -> None:
-    BAD_ID = 'id_3'
-    GOOD_IDXS1 = ['id_0', 'id_1', 'id_2', 'id_3', 'id_4', 'id_5']
-    GOOD_IDXS2 = ['id_0', 'id_1', 'id_4', 'id_5']
+    BAD_ID = 3
+    GOOD_IDXS1 = [0, 1, 2, 3, 4, 5]
     CHUNKSIZE = 2
 
-    ms = MetaStore(dbconn)
+    ds = DataStore(dbconn)
 
-    tbl = DataTable(
+    tbl = ds.create_table(
         'tbl',
-        meta_table=ms.create_meta_table('tbl'),
-        table_store=TableStoreDB(dbconn, 'tbl1_data', TEST_SCHEMA, True))
+        table_store=TableStoreDB(dbconn, 'tbl1_data', TEST_SCHEMA, True)
+    )
 
-    tbl_good = DataTable(
+    tbl_good = ds.create_table(
         'tbl_good',
-        meta_table=ms.create_meta_table('tbl_good'),
-        table_store=TableStoreDB(dbconn, 'tbl_good_data', TEST_SCHEMA, True))
+        table_store=TableStoreDB(dbconn, 'tbl_good_data', TEST_SCHEMA, True)
+    )
 
     def gen_bad1(chunksize: int = 1000):
         idx = TEST_DF.index
@@ -619,10 +620,10 @@ def gen_bad2(chunksize: int = 1000):
         chunksize=CHUNKSIZE
     )
 
-    assert(assert_df_equal(tbl.get_data(), TEST_DF.loc[GOOD_IDXS1]))
+    assert_datatable_equal(tbl, TEST_DF.loc[GOOD_IDXS1])
 
     def inc_func_bad(df):
-        if BAD_ID in df.index:
+        if BAD_ID in df['id'].values:
             raise Exception('TEST')
         return df
 
@@ -630,24 +631,24 @@ def inc_func_good(df):
         return df
 
     inc_process_many(
-        ms,
+        ds,
         [tbl],
         [tbl_good],
         inc_func_bad,
-        chunksize=CHUNKSIZE
+        chunksize=1
     )
 
-    assert(assert_df_equal(tbl_good.get_data(), TEST_DF.loc[GOOD_IDXS2]))
+    assert_datatable_equal(tbl_good, TEST_DF.loc[[0, 1, 2, 4, 5]])
 
     inc_process_many(
-        ms,
+        ds,
         [tbl],
         [tbl_good],
         inc_func_good,
         chunksize=CHUNKSIZE
     )
 
-    assert(assert_df_equal(tbl_good.get_data().sort_index(), TEST_DF.loc[GOOD_IDXS1]))
+    assert_datatable_equal(tbl_good, TEST_DF.loc[GOOD_IDXS1])
     # Checks that records are not being deleted
     gen_process_many(
         dts=[tbl],
@@ -655,14 +656,14 @@ def inc_func_good(df):
         chunksize=CHUNKSIZE
     )
 
-    assert(assert_df_equal(tbl.get_data().sort_index(), TEST_DF.loc[GOOD_IDXS1]))
+    assert_datatable_equal(tbl, TEST_DF.loc[GOOD_IDXS1])
 
     inc_process_many(
-        ms,
+        ds,
         [tbl],
         [tbl_good],
         inc_func_bad,
         chunksize=CHUNKSIZE
     )
 
-    assert(assert_df_equal(tbl_good.get_data().sort_index(), TEST_DF.loc[GOOD_IDXS1]))
+    assert_datatable_equal(tbl_good, TEST_DF.loc[GOOD_IDXS1])
diff --git a/tests/test_image_pipeline.py b/tests/test_image_pipeline.py
index 6721ab74..d4b89bb0 100644
--- a/tests/test_image_pipeline.py
+++ b/tests/test_image_pipeline.py
@@ -4,9 +4,10 @@
 import numpy as np
 from PIL import Image
 
+from sqlalchemy import Column, String
+
 from datapipe.dsl import Catalog, ExternalTable, Pipeline, Table, BatchGenerate, BatchTransform
-from datapipe.metastore import MetaStore
-from datapipe.datatable import DataTable, gen_process, inc_process
+from datapipe.datatable import DataStore, DataTable, gen_process, inc_process
 from datapipe.store.filedir import TableStoreFiledir, PILFile
 from datapipe.compute import build_compute, run_pipeline, run_steps
 
@@ -15,9 +16,9 @@ def make_df():
     idx = [f'im_{i}' for i in range(10)]
     return pd.DataFrame(
         {
+            'id': idx,
             'image': [Image.fromarray(np.random.randint(0, 256, (100, 100, 3)), 'RGB') for i in idx]
-        },
-        index=idx
+        }
     )
 
 
@@ -31,20 +32,18 @@ def resize_images(df):
 
 
 def test_image_datatables(dbconn, tmp_dir):
-    ms = MetaStore(dbconn)
+    ds = DataStore(dbconn)
 
-    tbl1 = DataTable(
+    tbl1 = ds.create_table(
         'tbl1',
-        meta_table=ms.create_meta_table('tbl1'),
         table_store=TableStoreFiledir(
             tmp_dir / 'tbl1' / '{id}.png',
             adapter=PILFile('png')
         )
     )
 
-    tbl2 = DataTable(
+    tbl2 = ds.create_table(
         'tbl2',
-        meta_table=ms.create_meta_table('tbl2'),
         table_store=TableStoreFiledir(
             tmp_dir / 'tbl2' / '{id}.png',
             adapter=PILFile('png')
@@ -60,7 +59,7 @@ def test_image_datatables(dbconn, tmp_dir):
     )
 
     inc_process(
-        ms,
+        ds,
         [tbl1],
         tbl2,
         resize_images
@@ -101,8 +100,8 @@ def test_image_pipeline(dbconn, tmp_dir):
     assert len(list(tmp_dir.glob('tbl1/*.png'))) == 0
     assert len(list(tmp_dir.glob('tbl2/*.png'))) == 0
 
-    ms = MetaStore(dbconn)
-    run_pipeline(ms, catalog, pipeline)
+    ds = DataStore(dbconn)
+    run_pipeline(ds, catalog, pipeline)
 
     assert len(list(tmp_dir.glob('tbl1/*.png'))) == 10
     assert len(list(tmp_dir.glob('tbl2/*.png'))) == 10
@@ -113,8 +112,8 @@ def test_image_batch_generate_with_later_deleting(dbconn, tmp_dir):
     # Add images to tmp_dir
     df_images = make_df()
     (tmp_dir / 'tbl1').mkdir()
-    for id in df_images.index:
-        df_images.loc[id, 'image'].save(tmp_dir / 'tbl1' / f'{id}.png')
+    for _, row in df_images[['id', 'image']].iterrows():
+        row['image'].save(tmp_dir / 'tbl1' / f'{row["id"]}.png')
 
     catalog = Catalog({
         'tbl1': ExternalTable(
@@ -143,26 +142,26 @@ def test_image_batch_generate_with_later_deleting(dbconn, tmp_dir):
     assert len(list(tmp_dir.glob('tbl1/*.png'))) == 10
     assert len(list(tmp_dir.glob('tbl2/*.png'))) == 0
 
-    ms = MetaStore(dbconn)
-    steps = build_compute(ms, catalog, pipeline)
-    run_steps(ms, steps)
+    ds = DataStore(dbconn)
+    steps = build_compute(ds, catalog, pipeline)
+    run_steps(ds, steps)
 
     assert len(list(tmp_dir.glob('tbl1/*.png'))) == 10
     assert len(list(tmp_dir.glob('tbl2/*.png'))) == 10
-    assert len(catalog.get_datatable(ms, 'tbl1').get_data()) == 10
-    assert len(catalog.get_datatable(ms, 'tbl2').get_data()) == 10
+    assert len(catalog.get_datatable(ds, 'tbl1').get_data()) == 10
+    assert len(catalog.get_datatable(ds, 'tbl2').get_data()) == 10
 
     # Delete some files from the folder
     for id in [0, 5, 7, 8, 9]:
         (tmp_dir / 'tbl1' / f'im_{id}.png').unlink()
 
-    run_steps(ms, steps)
+    run_steps(ds, steps)
 
     assert len(list(tmp_dir.glob('tbl1/*.png'))) == 5
-    assert len(catalog.get_datatable(ms, 'tbl1').get_data()) == 5
-    assert len(catalog.get_datatable(ms, 'tbl1').get_metadata()) == 5
+    assert len(catalog.get_datatable(ds, 'tbl1').get_data()) == 5
+    assert len(catalog.get_datatable(ds, 'tbl1').get_metadata()) == 5
 
     # TODO: uncomment follow when we make files deletion
     # assert len(list(tmp_dir.glob('tbl2/*.png'))) == 5
-    assert len(catalog.get_datatable(ms, 'tbl2').get_data()) == 5
-    assert len(catalog.get_datatable(ms, 'tbl2').get_metadata()) == 5
+    assert len(catalog.get_datatable(ds, 'tbl2').get_data()) == 5
+    assert len(catalog.get_datatable(ds, 'tbl2').get_metadata()) == 5
diff --git a/tests/test_label_studio_moderation.py b/tests/test_label_studio_moderation.py
index 420c7755..7f4668d1 100644
--- a/tests/test_label_studio_moderation.py
+++ b/tests/test_label_studio_moderation.py
@@ -12,7 +12,7 @@
 from PIL import Image
 
 from datapipe.dsl import Catalog, LabelStudioModeration, Pipeline, Table, BatchGenerate, BatchTransform
-from datapipe.metastore import MetaStore
+from datapipe.datatable import DataStore
 from datapipe.store.filedir import JSONFile, TableStoreFiledir, PILFile
 from datapipe.compute import build_compute, run_steps
 from datapipe.label_studio.session import LabelStudioModerationStep, LabelStudioSession
@@ -35,6 +35,9 @@
 PROJECT_LABEL_CONFIG_TEST = LABEL_CONFIG_TEST
 
 
+SKIP_LS_TEST = True
+
+
 @pytest.fixture
 def ls_url(tmp_dir):
     ls_host = os.environ.get('LABEL_STUDIO_HOST', 'localhost')
@@ -64,6 +67,7 @@ def wait_until_label_studio_is_up(label_studio_session: LabelStudioSession):
         if counter >= 60:
             raise_exception = True
 
+@pytest.mark.skipif(SKIP_LS_TEST, reason="Skipping LS for now")
 def test_sign_up(ls_url):
     label_studio_session = LabelStudioSession(ls_url=ls_url, auth=('test_auth@epoch8.co', 'qwerty123'))
     wait_until_label_studio_is_up(label_studio_session)
@@ -130,9 +134,10 @@ def convert_to_ls_input_data(
     return images_df[columns]
 
 
+@pytest.mark.skipif(SKIP_LS_TEST, reason="Skipping LS for now")
 @pytest.mark.parametrize("include_annotations,include_predictions", [(False, False), (False, True)])
 def test_label_studio_moderation(dbconn, tmp_dir, ls_url, include_annotations, include_predictions):
-    ms = MetaStore(dbconn)
+    ds = DataStore(dbconn)
     catalog = Catalog({
         '00_images': Table(
             store=TableStoreFiledir(tmp_dir / '00_images' / '{id}.jpg', PILFile('JPEG')),
@@ -170,24 +175,24 @@ def test_label_studio_moderation(dbconn, tmp_dir, ls_url, include_annotations, i
         ),
     ])
 
-    steps = build_compute(ms, catalog, pipeline)
+    steps = build_compute(ds, catalog, pipeline)
     label_studio_moderation_step : LabelStudioModerationStep = steps[-1]
 
     label_studio_session = LabelStudioSession(ls_url=ls_url, auth=LABEL_STUDIO_AUTH)
     wait_until_label_studio_is_up(label_studio_session)
 
     # This should be ok (project will be created, but without data)
-    run_steps(ms, steps)
-    run_steps(ms, steps)
+    run_steps(ds, steps)
+    run_steps(ds, steps)
 
     # These steps should upload tasks (it also can be BatchGenerate as first step of pipeline, like in the next test)
     gen_process(
-        dt=catalog.get_datatable(ms, '00_images'),
+        dt=catalog.get_datatable(ds, '00_images'),
         proc_func=gen_images
     )
-    run_steps(ms, steps)
+    run_steps(ds, steps)
 
-    assert len(catalog.get_datatable(ms, '02_annotations').get_data()) == 10
+    assert len(catalog.get_datatable(ds, '02_annotations').get_data()) == 10
 
     # Person annotation imitation & incremental processing
     tasks, _ = label_studio_session.get_tasks(
@@ -218,9 +223,9 @@ def test_label_studio_moderation(dbconn, tmp_dir, ls_url, include_annotations, i
                     }
                 ]
         )
-        run_steps(ms, steps)
+        run_steps(ds, steps)
         idxs_df = [task['data']['LabelStudioModerationStep__unique_id'] for task in tasks[idxs]]
-        df_annotation = catalog.get_datatable(ms, '02_annotations').get_data(idx=idxs_df)
+        df_annotation = catalog.get_datatable(ds, '02_annotations').get_data(idx=idxs_df)
         for idx_df in idxs_df:
             assert len(df_annotation.loc[idx_df, 'annotations']) == 1
             assert df_annotation.loc[idx_df, 'annotations'][0]['result'][0]['value']['rectanglelabels'][0] in (
@@ -228,15 +233,16 @@ def test_label_studio_moderation(dbconn, tmp_dir, ls_url, include_annotations, i
             )
 
     # Check if pipeline works after service is over
-    run_steps(ms, steps)
+    run_steps(ds, steps)
 
     # Delete the project after the test
     res = label_studio_session.delete_project(project_id=label_studio_moderation_step.project_id)
 
 
+@pytest.mark.skipif(SKIP_LS_TEST, reason="Skipping LS for now")
 @pytest.mark.parametrize("include_annotations,include_predictions", [(True, False), (True, True)])
 def test_label_studio_moderation_with_preannotations(dbconn, tmp_dir, ls_url, include_annotations, include_predictions):
-    ms = MetaStore(dbconn)
+    ds = DataStore(dbconn)
     catalog = Catalog({
         '00_images': Table(
             store=TableStoreFiledir(tmp_dir / '00_images' / '{id}.jpg', PILFile('JPEG')),
@@ -289,17 +295,17 @@ def test_label_studio_moderation_with_preannotations(dbconn, tmp_dir, ls_url, in
         ),
     ])
 
-    steps = build_compute(ms, catalog, pipeline)
+    steps = build_compute(ds, catalog, pipeline)
     label_studio_moderation_step : LabelStudioModerationStep = steps[-1]
 
     label_studio_session = LabelStudioSession(ls_url=ls_url, auth=LABEL_STUDIO_AUTH)
     wait_until_label_studio_is_up(label_studio_session)
 
     # These steps should upload tasks with preannotations
-    run_steps(ms, steps)
+    run_steps(ds, steps)
 
-    assert len(catalog.get_datatable(ms, '02_annotations').get_data()) == 10
-    df_annotation = catalog.get_datatable(ms, '02_annotations').get_data()
+    assert len(catalog.get_datatable(ds, '02_annotations').get_data()) == 10
+    df_annotation = catalog.get_datatable(ds, '02_annotations').get_data()
     for idx in df_annotation.index:
         assert len(df_annotation.loc[idx, 'annotations']) == 1
         assert df_annotation.loc[idx, 'annotations'][0]['result'][0]['value']['rectanglelabels'][0] in (
diff --git a/tests/test_metatable.py b/tests/test_metatable.py
new file mode 100644
index 00000000..f7dff37e
--- /dev/null
+++ b/tests/test_metatable.py
@@ -0,0 +1,39 @@
+from typing import cast
+import pandas as pd
+
+from sqlalchemy import Integer
+from sqlalchemy.sql.schema import Column
+
+from datapipe.metastore import MetaTable
+from datapipe.store.database import DBConn
+from datapipe.types import DataDF
+
+from .util import assert_df_equal
+
+TEST_DF = cast(DataDF, pd.DataFrame(
+    {
+        'id': range(10),
+        'a': range(10)
+    },
+))
+
+
+def test_insert_rows(dbconn: DBConn):
+    mt = MetaTable(
+        name='test',
+        dbconn=dbconn,
+        primary_schema=[
+            Column('id', Integer, primary_key=True)
+        ]
+    )
+
+    new_df, changed_df, new_meta_df, changed_meta_df = mt.get_changes_for_store_chunk(TEST_DF)
+    assert_df_equal(new_df, TEST_DF)
+    assert(len(changed_df) == 0)
+
+    assert_df_equal(new_meta_df[['id']], new_df[['id']])
+
+    mt.insert_meta_for_store_chunk(new_meta_df=new_meta_df)
+    mt.update_meta_for_store_chunk(changed_meta_df=changed_meta_df)
+
+    assert_df_equal(mt.get_metadata()[['id']], TEST_DF[['id']])
diff --git a/tests/test_table_store.py b/tests/test_table_store.py
new file mode 100644
index 00000000..2a5da2a8
--- /dev/null
+++ b/tests/test_table_store.py
@@ -0,0 +1,168 @@
+# import pytest
+from datapipe.store.filedir import JSONFile, TableStoreFiledir
+from pytest_cases import parametrize_with_cases, case
+
+import pandas as pd
+from sqlalchemy import Column, Integer, String
+
+from datapipe.store.table_store import TableStore
+from datapipe.store.database import TableStoreDB
+from datapipe.store.pandas import TableStoreJsonLine, TableStoreExcel
+
+from .util import assert_df_equal
+
+# TODO реализовать стандартный набор тестов для всех table_store.
+
+TEST_DF_INTID = pd.DataFrame(
+    {
+        'id': range(100),
+        'name': [f'Product {i}' for i in range(100)],
+        'price': [1000 + i for i in range(100)],
+    }
+)
+
+TEST_DF_STRID = pd.DataFrame(
+    {
+        'id': [f'id_{i}' for i in range(100)],
+        'name': [f'Product {i}' for i in range(100)],
+        'price': [1000 + i for i in range(100)],
+    }
+)
+
+
+class CasesTableStore:
+    @case(tags='supports_delete')
+    def case_db_intid(self, dbconn):
+        return (
+            TableStoreDB(
+                dbconn,
+                'tbl1',
+                [
+                    Column('id', Integer, primary_key=True),
+                    Column('name', String(100)),
+                    Column('price', Integer),
+                ]
+            ),
+            TEST_DF_INTID
+        )
+
+    @case(tags='supports_delete')
+    def case_db_strid(self, dbconn):
+        return (
+            TableStoreDB(
+                dbconn,
+                'tbl1',
+                [
+                    Column('id', String(100), primary_key=True),
+                    Column('name', String(100)),
+                    Column('price', Integer),
+                ]
+            ),
+            TEST_DF_STRID
+        )
+
+    @case(tags='supports_delete')
+    def case_jsonline_intid(self, tmp_dir):
+        return (
+            TableStoreJsonLine(
+                tmp_dir / "data.json",
+                primary_schema=[Column('id', Integer, primary_key=True)]
+            ),
+            TEST_DF_INTID
+        )
+
+    @case(tags='supports_delete')
+    def case_jsonline_strid(self, tmp_dir):
+        return (
+            TableStoreJsonLine(
+                tmp_dir / "data.json",
+                primary_schema=[Column('id', String(100), primary_key=True)]
+            ),
+            TEST_DF_STRID
+        )
+
+    @case(tags='supports_delete')
+    def case_excel_intid(self, tmp_dir):
+        return (
+            TableStoreExcel(
+                tmp_dir / "data.xlsx",
+                primary_schema=[Column('id', Integer, primary_key=True)]
+            ),
+            TEST_DF_INTID
+        )
+
+    @case(tags='supports_delete')
+    def case_excel_strid(self, tmp_dir):
+        return (
+            TableStoreExcel(
+                tmp_dir / "data.xlsx",
+                primary_schema=[Column('id', String(100), primary_key=True)]
+            ),
+            TEST_DF_STRID
+        )
+
+    def case_filedir_json_strid(self, tmp_dir):
+        return (
+            TableStoreFiledir(
+                f'{tmp_dir}/{{id}}.json',
+                adapter=JSONFile()
+            ),
+            TEST_DF_STRID
+        )
+
+
+@parametrize_with_cases('store,test_df', cases=CasesTableStore)
+def test_write_read_rows(store: TableStore, test_df: pd.DataFrame) -> None:
+    assert(store.read_rows().empty)
+
+    store.insert_rows(test_df)
+
+    assert_df_equal(store.read_rows(), test_df)
+
+
+@parametrize_with_cases('store,test_df', cases=CasesTableStore)
+def test_partial_update_rows(store: TableStore, test_df: pd.DataFrame) -> None:
+    assert(store.read_rows().empty)
+
+    store.insert_rows(test_df)
+
+    assert_df_equal(store.read_rows(), test_df)
+
+    test_df_mod = test_df.copy()
+    test_df_mod.loc[50:, 'price'] = test_df_mod.loc[50:, 'price'] + 1
+
+    store.update_rows(test_df_mod.loc[50:])
+
+    assert_df_equal(store.read_rows(), test_df_mod)
+
+
+@parametrize_with_cases('store,test_df', cases=CasesTableStore)
+def test_full_update_rows(store: TableStore, test_df: pd.DataFrame) -> None:
+    assert(store.read_rows().empty)
+
+    store.insert_rows(test_df)
+
+    assert_df_equal(store.read_rows(), test_df)
+
+    test_df_mod = test_df.copy()
+    test_df_mod.loc[:, 'price'] = test_df_mod.loc[:, 'price'] + 1
+
+    store.update_rows(test_df_mod)
+
+    assert_df_equal(store.read_rows(), test_df_mod)
+
+
+@parametrize_with_cases('store,test_df', cases=CasesTableStore, has_tag='supports_delete')
+def test_delete_rows(store: TableStore, test_df: pd.DataFrame) -> None:
+    assert(store.read_rows().empty)
+
+    store.insert_rows(test_df)
+
+    assert_df_equal(store.read_rows(), test_df)
+
+    store.delete_rows(test_df.loc[20:50, ['id']])
+
+    assert_df_equal(
+        store.read_rows(),
+        pd.concat([test_df.loc[0:19], test_df.loc[51:]])
+    )
diff --git a/tests/test_table_store_db.py b/tests/test_table_store_db.py
deleted file mode 100644
index cc2e1d06..00000000
--- a/tests/test_table_store_db.py
+++ /dev/null
@@ -1,160 +0,0 @@
-import pandas as pd
-
-from sqlalchemy.sql.schema import Column
-from sqlalchemy.sql.sqltypes import Integer, String
-
-from datapipe.store.database import TableStoreDB, ConstIdx
-
-FEED1 = [
-    {"id": "1", "name": "Product 1"},
-    {"id": "2", "name": "Product 2"},
-    {"id": "3", "name": "Product 3"},
-    {"id": "4", "name": "Product 4"},
-    {"id": "5", "name": "Product 5"},
-    {"id": "6", "name": "Product 6"}
-]
-
-FEED2 = [
-    {"id": "1", "name": "Product 12"},
-    {"id": "2", "name": "Product 22"},
-    {"id": "3", "name": "Product 32"},
-    {"id": "7", "name": "Product 42"},
-    {"id": "8", "name": "Product 52"},
-    {"id": "9", "name": "Product 62"}
-]
-
-FEED3 = [{"id": "1", "name": "Product 3"}]
-
-SQL_SHEMA = [
-    Column("name", String(100))
-]
-
-
-def test_for_inserting(dbconn) -> None:
-    store1 = TableStoreDB(
-        dbconn,
-        'feed_data',
-        SQL_SHEMA,
-        const_idx=[
-            ConstIdx(
-                column=Column("pipe_id", Integer()),
-                value=1
-            )
-        ]
-
-    )
-
-    store2 = TableStoreDB(
-        dbconn,
-        'feed_data',
-        SQL_SHEMA,
-        const_idx=[
-            ConstIdx(
-                column=Column("pipe_id", Integer()),
-                value=2
-            )
-        ]
-    )
-
-    df1 = pd.DataFrame(data=FEED1).set_index('id')
-    df2 = pd.DataFrame(data=FEED2).set_index('id')
-
-    store1.insert_rows(df1)
-    stored_df1_index = sorted(store1.read_rows().index)
-
-    assert(list(df1.index) == list(stored_df1_index))
-
-    store2.insert_rows(df2)
-    stored_df1_index = sorted(store1.read_rows().index)
-    stored_df2_index = sorted(store2.read_rows().index)
-
-    assert(list(df1.index) == list(stored_df1_index))
-    assert(list(df2.index) == list(stored_df2_index))
-
-
-def test_for_updating(dbconn) -> None:
-    store1 = TableStoreDB(
-        dbconn,
-        'feed_data',
-        SQL_SHEMA,
-        const_idx=[
-            ConstIdx(
-                column=Column("pipe_id", Integer()),
-                value=1
-            )
-        ]
-
-    )
-
-    store2 = TableStoreDB(
-        dbconn,
-        'feed_data',
-        SQL_SHEMA,
-        const_idx=[
-            ConstIdx(
-                column=Column("pipe_id", Integer()),
-                value=2
-            )
-        ]
-    )
-
-    df1 = pd.DataFrame(data=FEED1).set_index('id')
-    df2 = pd.DataFrame(data=FEED2).set_index('id')
-    df3 = pd.DataFrame(data=FEED3).set_index('id')
-
-    store1.insert_rows(df1)
-    store2.insert_rows(df2)
-
-    store1.update_rows(df3)
-
-    stored_df1 = store1.read_rows().sort_index()
-    stored_df2_index = sorted(store2.read_rows().index)
-
-    df1.update(df3)
-
-    assert(df1.equals(stored_df1))
-    assert(list(df2.index) == list(stored_df2_index))
-
-
-def test_for_deliting(dbconn) -> None:
-    store1 = TableStoreDB(
-        dbconn,
-        'feed_data',
-        SQL_SHEMA,
-        const_idx=[
-            ConstIdx(
-                column=Column("pipe_id", Integer()),
-                value=1
-            )
-        ]
-
-    )
-
-    store2 = TableStoreDB(
-        dbconn,
-        'feed_data',
-        SQL_SHEMA,
-        const_idx=[
-            ConstIdx(
-                column=Column("pipe_id", Integer()),
-                value=2
-            )
-        ]
-    )
-
-    df1 = pd.DataFrame(data=FEED1).set_index('id')
-    df2 = pd.DataFrame(data=FEED2).set_index('id')
-
-    store1.insert_rows(df1)
-    store2.insert_rows(df2)
-
-    df1 = df1.drop('1')
-    del_index = pd.Index(["1"])
-
-    store1.delete_rows(del_index)
-
-    stored_df1 = store1.read_rows()
-    stored_df2 = store2.read_rows()
-
-    assert(list(df1.index) == list(stored_df1.index))
-    assert(list(df2.index) == list(stored_df2.index))
diff --git a/tests/test_table_store_filedir.py b/tests/test_table_store_filedir.py
index bb6dabf5..4ac014b5 100644
--- a/tests/test_table_store_filedir.py
+++ b/tests/test_table_store_filedir.py
@@ -8,14 +8,14 @@
 
 from datapipe.store.filedir import PILFile, JSONFile, TableStoreFiledir
 
-from .util import assert_df_equal, assert_idx_equal
+from .util import assert_df_equal
 
 TEST_DF = pd.DataFrame(
     {
+        "id": ['aaa', 'bbb'],
         "a": [1, 2],
         "b": [10, 20],
-    },
-    index=['aaa', 'bbb'],
+    }
 )
 
 TEST_JSONS = {
@@ -34,7 +34,7 @@ def tmp_dir_with_json_data(tmp_dir):
 
 def get_test_df_filepath(test_df, tmp_dir_):
     test_df_filepath = test_df.copy()
-    test_df_filepath['filepath'] = test_df_filepath.index.map(
+    test_df_filepath['filepath'] = test_df_filepath["id"].map(
         lambda idx: f'{tmp_dir_}/{idx}.json'
     )
     return test_df_filepath
@@ -50,7 +50,8 @@ def test_read_json_rows(tmp_dir_with_json_data):
 
     ts_with_filepath = TableStoreFiledir(
         f'{tmp_dir_with_json_data}/{{id}}.json',
-        adapter=JSONFile(), add_filepath_column=True
+        adapter=JSONFile(),
+        add_filepath_column=True
     )
     assert_df_equal(ts_with_filepath.read_rows(), get_test_df_filepath(TEST_DF, tmp_dir_with_json_data))
 
@@ -92,12 +93,15 @@ def test_insert_json_rows(tmp_dir_with_json_data):
     )
 
     ts.insert_rows(pd.DataFrame(
-        {'a': [3], 'b': [30]},
-        index=['ccc']
+        {
+            'id': ['ccc'],
+            'a': [3],
+            'b': [30]
+        }
     ))
 
     with open(f'{tmp_dir_with_json_data}/ccc.json') as f:
-        assert(json.load(f) == {'a': 3, 'b': 30})
+        assert(json.load(f) == {'id': 'ccc', 'a': 3, 'b': 30})
 
 
 @pytest.fixture
@@ -117,7 +121,8 @@ def test_read_png_rows(tmp_dir_with_img_data):
 
     rows = ts.read_rows()
 
-    assert_idx_equal(rows.index, ['aaa'])
+    assert_df_equal(rows[["id"]], pd.DataFrame({"id": ["aaa"]}))
+
     assert('image' in rows.columns)
 
 
@@ -128,8 +133,10 @@ def test_insert_png_rows(tmp_dir_with_img_data):
     )
 
     ts.insert_rows(pd.DataFrame(
-        {'image': [Image.fromarray(np.zeros((100, 100, 3), 'u8'), 'RGB')]},
-        index=['bbb']
+        {
+            'id': ['bbb'],
+            'image': [Image.fromarray(np.zeros((100, 100, 3), 'u8'), 'RGB')]
+        }
     ))
 
     assert(fsspec.open(f'{tmp_dir_with_img_data}/bbb.png'))
@@ -148,10 +155,10 @@ def tmp_several_dirs_with_json_data(tmp_dir):
 
 TEST_DF_FOLDER0 = pd.DataFrame(
     {
+        "id": ['00', '01', '02'],
         "a": [0, 0, 0],
         "b": [0, 1, 2],
-    },
-    index=['00', '01', '02'],
+    }
 )
 
 
@@ -167,7 +174,7 @@ def test_read_json_rows_folders(tmp_several_dirs_with_json_data):
         adapter=JSONFile(), add_filepath_column=True
     )
     TEST_DF_FOLDER0_WITH_FILEPATH = TEST_DF_FOLDER0.copy()
-    TEST_DF_FOLDER0_WITH_FILEPATH['filepath'] = TEST_DF_FOLDER0_WITH_FILEPATH.index.map(
+    TEST_DF_FOLDER0_WITH_FILEPATH['filepath'] = TEST_DF_FOLDER0_WITH_FILEPATH["id"].map(
         lambda idx: (
             f"{tmp_several_dirs_with_json_data}/folder0/folder{idx[1]}/{idx}.json"
         )
@@ -177,10 +184,10 @@ def test_read_json_rows_folders(tmp_several_dirs_with_json_data):
 
 TEST_DF_FOLDER_RECURSIVELY = pd.DataFrame(
     {
+        "id": ['0', '00', '01', '02', '1', '10', '11', '12', '2', '20', '21', '22'],
         "a": [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2],
         "b": [-1, 0, 1, 2, -1, 0, 1, 2, -1, 0, 1, 2],
-    },
-    index=['0', '00', '01', '02', '1', '10', '11', '12', '2', '20', '21', '22'],
+    }
 )
 
 
@@ -196,7 +203,7 @@ def test_read_json_rows_recursively(tmp_several_dirs_with_json_data):
         adapter=JSONFile(), add_filepath_column=True
     )
     TEST_DF_FOLDER_RECURSIVELY_WITH_FILEPATH = TEST_DF_FOLDER_RECURSIVELY.copy()
-    TEST_DF_FOLDER_RECURSIVELY_WITH_FILEPATH['filepath'] = TEST_DF_FOLDER_RECURSIVELY_WITH_FILEPATH.index.map(
+    TEST_DF_FOLDER_RECURSIVELY_WITH_FILEPATH['filepath'] = TEST_DF_FOLDER_RECURSIVELY_WITH_FILEPATH["id"].map(
         lambda idx: (
             f"{tmp_several_dirs_with_json_data}/folder{idx}/{idx}.json"
             if idx in ['0', '1', '2']
diff --git a/tests/test_table_store_json_line.py b/tests/test_table_store_json_line.py
index 32d5461c..58f9a2fb 100644
--- a/tests/test_table_store_json_line.py
+++ b/tests/test_table_store_json_line.py
@@ -1,7 +1,7 @@
 import pandas as pd
 
 from datapipe.compute import build_compute, run_steps
-from datapipe.metastore import MetaStore
+from datapipe.datatable import DataStore
 from datapipe.dsl import Catalog, ExternalTable, Pipeline, BatchTransform, Table
 from datapipe.store.pandas import TableStoreJsonLine
 
@@ -38,7 +38,7 @@ def make_file2(file):
 def test_table_store_json_line_with_deleting(dbconn, tmp_dir):
     input_file = tmp_dir / "data.json"
 
-    ms = MetaStore(dbconn)
+    ds = DataStore(dbconn)
     catalog = Catalog({
         "input_data": ExternalTable(
             store=TableStoreJsonLine(tmp_dir / "data.json"),
@@ -58,17 +58,17 @@ def test_table_store_json_line_with_deleting(dbconn, tmp_dir):
     # Create data, pipeline it
     make_file1(input_file)
 
-    steps = build_compute(ms, catalog, pipeline)
-    run_steps(ms, steps)
+    steps = build_compute(ds, catalog, pipeline)
+    run_steps(ds, steps)
 
-    assert len(catalog.get_datatable(ms, 'input_data').get_data()) == 3
-    assert len(catalog.get_datatable(ms, 'transfomed_data').get_data()) == 3
+    assert len(catalog.get_datatable(ds, 'input_data').get_data()) == 3
+    assert len(catalog.get_datatable(ds, 'transfomed_data').get_data()) == 3
 
     # Remove {"id": "0"} from file, pipeline it
     make_file2(input_file)
-    run_steps(ms, steps)
+    run_steps(ds, steps)
 
     # TODO: uncomment follow when we make files deletion
     # assert len(list(tmp_dir.glob('tbl2/*.png'))) == 2
-    assert len(catalog.get_datatable(ms, 'input_data').get_data()) == 2
-    assert len(catalog.get_datatable(ms, 'transfomed_data').get_data()) == 2
+    assert len(catalog.get_datatable(ds, 'input_data').get_data()) == 2
+    assert len(catalog.get_datatable(ds, 'transfomed_data').get_data()) == 2
diff --git a/tests/util.py b/tests/util.py
index fc1332eb..71fc7784 100644
--- a/tests/util.py
+++ b/tests/util.py
@@ -1,5 +1,8 @@
 import pandas as pd
 
+from datapipe.types import DataDF
+from datapipe.datatable import DataTable
+
 
 def assert_idx_equal(a, b):
     a = sorted(list(a))
@@ -8,7 +11,10 @@ def assert_idx_equal(a, b):
     assert(a == b)
 
 
-def assert_df_equal(a: pd.DataFrame, b: pd.DataFrame) -> bool:
+def assert_df_equal(a: pd.DataFrame, b: pd.DataFrame, index_cols=['id']) -> bool:
+    a = a.set_index(index_cols)
+    b = b.set_index(index_cols)
+
     assert_idx_equal(a.index, b.index)
 
     eq_rows = (a.sort_index() == b.sort_index()).all(axis='columns')
@@ -24,3 +30,7 @@ def assert_df_equal(a: pd.DataFrame, b: pd.DataFrame) -> bool:
         print(b.loc[-eq_rows])
 
         raise AssertionError
+
+
+def assert_datatable_equal(a: DataTable, b: DataDF) -> bool:
+    return assert_df_equal(a.get_data(), b)