Repository: datnguye/dbterd (language: python)

Issue:
[FEAT] Include all refs when rendering a single model
**Is your feature request related to a problem? Please describe.**

Currently when you render a single model w/ `dbterd` it will only output that model itself without its referenced tables. I couldn't find an option to include referenced models in the output with the model I want to render. It would be nice if there was an option to include all of the referenced models in the final output as well. Refs can be found in the manifest under the node information for a given model.

**Describe the solution you'd like**
Possibly some sort of flag to include referenced tables in the final output 
```bash
dbterd run -s "my_model" --include-refs
```



Discussion / hints:
Thanks @dylan-murray for raising the request, this is definitely a good one 👍 
I'm thinking about to mimic dbt's downstream/upstream selection with `+` sign, but it might add the complexity to the code base unecessarily 🤔 

Can I check further on which dbt version are you using? Is it v1.5?
For sure! I think if you could implement the `+` operator that would work fine as well. It would provide more flexibility, allowing a user to also render everything downstream of a model and not just upstream based off of the refs. The downstream part might be complicated though, I'm not sure if downstream models are readily listed anywhere or if you would have to loop the manifest to find which models are downstream. 

I am currently on v1.5 of dbt.
Check point! 
So, I don't want to repeat the logic of dbt-core here (not `DRY`, but `Don't Repeat dbt` 😆), I will try to utilize the[ dbt Programmatic Invocation](https://docs.getdbt.com/reference/programmatic-invocations) bit. This way we can re-use any dbt's selection -- not sure how it's gonna work, but the expected result is:
```
dbterd --select ... --exclude ... --dbt-selection
# OR
dbterd --select ... --exclude ... --dbt
```

Fix patch:
diff --git a/.gitignore b/.gitignore
index c1051c7..8ec7dbe 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
 /target
 /samples/local
 CHANGELOG.md
+/dbt_packages
 
 # Byte-compiled / optimized / DLL files
 __pycache__/
diff --git a/SECURITY.md b/SECURITY.md
index 6d7dc40..cd9b4aa 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -7,6 +7,7 @@ currently being supported with security updates.
 
 | Version | Supported          |
 | ------- | ------------------ |
+| 1.3+    | :white_check_mark: |
 | 1.2.x   | :white_check_mark: |
 | 1.1.x   | :white_check_mark: |
 | 1.0.x   | :white_check_mark: |
diff --git a/dbterd/adapters/factory.py b/dbterd/adapters/adapter.py
similarity index 100%
rename from dbterd/adapters/factory.py
rename to dbterd/adapters/adapter.py
diff --git a/dbterd/adapters/algos/base.py b/dbterd/adapters/algos/base.py
index 6060bec..b8d3a2f 100644
--- a/dbterd/adapters/algos/base.py
+++ b/dbterd/adapters/algos/base.py
@@ -1,6 +1,6 @@
 import copy
 
-from dbterd.adapters.algos.meta import Column, Table
+from dbterd.adapters.meta import Column, Table
 
 
 def get_tables(manifest, catalog):
diff --git a/dbterd/adapters/base.py b/dbterd/adapters/base.py
index 69db2b7..7b12171 100644
--- a/dbterd/adapters/base.py
+++ b/dbterd/adapters/base.py
@@ -1,15 +1,19 @@
-import abc
+import os
+from pathlib import Path
 
 import click
 
-from dbterd.adapters import factory
+from dbterd import default
+from dbterd.adapters import adapter
+from dbterd.adapters.dbt_invocation import DbtInvocation
+from dbterd.adapters.filter import has_unsupported_rule
 from dbterd.helpers import cli_messaging
 from dbterd.helpers import file as file_handlers
 from dbterd.helpers.log import logger
 
 
-class Executor(abc.ABC):
-    """Base executor"""
+class Executor:
+    """Main Executor"""
 
     ctx: click.Context
 
@@ -19,11 +23,82 @@ def __init__(self, ctx) -> None:
         self.filename_manifest = "manifest.json"
         self.filename_catalog = "catalog.json"
 
-    @abc.abstractmethod
     def run(self, **kwargs):
         """Main function helps to run by the target strategy"""
+        kwargs = self.evaluate_kwargs(**kwargs)
         self.__run_by_strategy(**kwargs)
 
+    def evaluate_kwargs(self, **kwargs) -> dict:
+        """Re-calculate the options
+
+        Raises:
+            click.UsageError: Not Supported exception
+
+        Returns:
+            dict: kwargs dict
+        """
+        artifacts_dir, dbt_project_dir = self.__get_dir(**kwargs)
+        logger.info(f"Using dbt artifact dir at: {artifacts_dir}")
+        logger.info(f"Using dbt project  dir at: {dbt_project_dir}")
+
+        select = list(kwargs.get("select")) or []
+        exclude = list(kwargs.get("exclude")) or []
+        if kwargs.get("dbt"):
+            select = self.__get_selection(**kwargs)
+            exclude = []
+        else:
+            unsupported, rule = has_unsupported_rule(
+                rules=select.extend(exclude) if exclude else select
+            )
+            if unsupported:
+                message = f"Unsupported Selection found: {rule}"
+                logger.error(message)
+                raise click.UsageError(message)
+
+        kwargs["artifacts_dir"] = artifacts_dir
+        kwargs["dbt_project_dir"] = dbt_project_dir
+        kwargs["select"] = select
+        kwargs["exclude"] = exclude
+
+        return kwargs
+
+    def __get_dir(self, **kwargs) -> str:
+        """Calculate the dbt artifact directory and dbt project directory
+
+        Returns:
+            tuple(str, str): Path to target directory and dbt project directory
+        """
+        artifact_dir = (
+            f"{kwargs.get('artifacts_dir') or kwargs.get('dbt_project_dir')}"  # default
+        )
+        project_dir = (
+            f"{kwargs.get('dbt_project_dir') or kwargs.get('artifacts_dir')}"  # default
+        )
+
+        if not artifact_dir:
+            return (
+                default.default_artifact_path(),
+                str(Path(default.default_artifact_path()).parent.absolute()),
+            )
+
+        artifact_dir = Path(artifact_dir).absolute()
+        project_dir = Path(project_dir).absolute()
+
+        if not os.path.isfile(f"{artifact_dir}/{self.filename_manifest}"):
+            artifact_dir = f"{project_dir}/target"  # try child target
+
+        return (str(artifact_dir), str(project_dir))
+
+    def __get_selection(self, **kwargs):
+        """Override the Selection using dbt's one with `--dbt`"""
+        return DbtInvocation(
+            dbt_project_dir=kwargs.get("dbt_project_dir"),
+            dbt_target=kwargs.get("dbt_target"),
+        ).get_selection(
+            select_rules=kwargs.get("select"),
+            exclude_rules=kwargs.get("exclude"),
+        )
+
     def __read_manifest(self, mp: str, mv: int = None):
         """Read the Manifest content
 
@@ -55,24 +130,29 @@ def __read_catalog(self, cp: str, cv: int = None):
 
     def __run_by_strategy(self, **kwargs):
         """Read artifacts and export the diagram file following the target"""
-        target = factory.load_executor(name=kwargs["target"])  # import {target}
+        target = adapter.load_executor(name=kwargs["target"])  # import {target}
         run_operation_dispatcher = getattr(target, "run_operation_dispatcher")
         operation_default = getattr(target, "run_operation_default")
         operation = run_operation_dispatcher.get(
             f"{kwargs['target']}_{kwargs['algo'].split(':')[0]}",
             operation_default,
         )
+
         manifest = self.__read_manifest(
-            mp=kwargs.get("manifest_path") or kwargs["artifacts_dir"],
-            mv=kwargs["manifest_version"],
+            mp=kwargs.get("artifacts_dir"),
+            mv=kwargs.get("manifest_version"),
         )
         catalog = self.__read_catalog(
-            cp=kwargs.get("manifest_path") or kwargs["artifacts_dir"],
-            cv=kwargs["catalog_version"],
+            cp=kwargs.get("artifacts_dir"),
+            cv=kwargs.get("catalog_version"),
         )
 
         result = operation(manifest=manifest, catalog=catalog, **kwargs)
-        path = kwargs["output"] + f"/{result[0]}"
-        with open(path, "w") as f:
-            logger.info(path)
-            f.write(result[1])
+        path = kwargs.get("output") + f"/{result[0]}"
+        try:
+            with open(path, "w") as f:
+                logger.info(path)
+                f.write(result[1])
+        except Exception as e:
+            logger.error(str(e))
+            raise click.FileError(f"Could not save the output: {str(e)}")
diff --git a/dbterd/adapters/dbt_invocation.py b/dbterd/adapters/dbt_invocation.py
new file mode 100644
index 0000000..fb90dd1
--- /dev/null
+++ b/dbterd/adapters/dbt_invocation.py
@@ -0,0 +1,76 @@
+import os
+import importlib.util
+from importlib.metadata import version
+from pathlib import Path
+from typing import List
+
+import click
+from dbt.cli.main import dbtRunner, dbtRunnerResult
+
+from dbterd.helpers.log import logger
+
+
+class DbtInvocation:
+    """Runner of dbt (https://docs.getdbt.com/reference/programmatic-invocations)"""
+
+    def __init__(self, dbt_project_dir: str = None, dbt_target: str = None) -> None:
+        """Initialization
+
+        Args:
+            dbt_project_dir (str, optional): Custom dbt project directory path. Defaults to None.
+            dbt_target (str, optional): Custom dbt target name. Defaults to None - using default target
+        """
+        self.__ensure_dbt_installed()
+        self.dbt = dbtRunner()
+        self.project_dir = (
+            dbt_project_dir or os.environ.get("DBT_PROJECT_DIR") or str(Path.cwd())
+        )
+        self.target = dbt_target
+
+    def __ensure_dbt_installed(self):
+        dbt_spec = importlib.util.find_spec("dbt")
+        if dbt_spec and dbt_spec.loader:
+            installed_path = dbt_spec.submodule_search_locations[0]
+            logger.debug(
+                f"Found dbt v{version('dbt-core')} installed at {installed_path}"
+            )
+        else:
+            message = (
+                "dbt module is not found or unsupported version, "
+                "please try to install dbt-core v1.5 or later, "
+                "OR let's try again without `--dbt` flag"
+            )
+            logger.error(message)
+            raise click.UsageError(message)
+
+    def get_selection(
+        self, select_rules: List[str] = [], exclude_rules: List[str] = []
+    ) -> List[str]:
+        """Get dbt selected models
+
+        Args:
+            select_rules (List[str], optional): Model inclusives. Defaults to [].
+            exclude_rules (List[str], optional): Model exclusives. Defaults to [].
+
+        Returns:
+            List[str]: Selected node names with 'exact' rule
+        """
+        args = ["ls", "--project-dir", self.project_dir, "--resource-type", "model"]
+        if select_rules:
+            args.extend(["--select", " ".join(select_rules)])
+        if exclude_rules:
+            args.extend(["--exclude", " ".join(exclude_rules)])
+        if self.target:
+            args.extend(["--target", self.target])
+
+        logger.info(f"Invoking: `dbt {' '.join(args)}` at {self.project_dir}")
+        r: dbtRunnerResult = self.dbt.invoke(args)
+
+        if not r.success:
+            logger.error(str(r))
+            raise click.UsageError("str(r)")
+
+        return [
+            f"exact:model.{str(x).split('.')[0]}.{str(x).split('.')[-1]}"
+            for x in r.result
+        ]
diff --git a/dbterd/adapters/algos/filter.py b/dbterd/adapters/filter.py
similarity index 68%
rename from dbterd/adapters/algos/filter.py
rename to dbterd/adapters/filter.py
index dc7bad3..a0e4403 100644
--- a/dbterd/adapters/algos/filter.py
+++ b/dbterd/adapters/filter.py
@@ -2,7 +2,29 @@
 from fnmatch import fnmatch
 from typing import List
 
-from dbterd.adapters.algos.meta import Table
+from dbterd.adapters.meta import Table
+
+RULE_FUNC_PREFIX = "is_satisfied_by_"
+
+
+def has_unsupported_rule(rules: List[str] = []) -> bool:
+    """Verify if existing the unsupported selection rule
+
+    Args:
+        rules (List[str]): Any (selection or/and exclusion) rules
+
+    Returns:
+        bool: True if existing any unsupported one
+    """
+    for rule in rules:
+        type = rule.split(":")
+        if len(type) == 1:
+            continue
+        rule_func = f"{RULE_FUNC_PREFIX}{type[0]}"
+        if not hasattr(sys.modules[__name__], rule_func):
+            return (True, type[0])
+
+    return (False, None)
 
 
 def is_selected_table(
@@ -28,6 +50,7 @@ def is_selected_table(
         selected = any([evaluate_rule(table=table, rule=rule) for rule in select_rules])
     if resource_types:
         selected = selected and table.resource_type in resource_types
+
     # Exclusion
     excluded = False
     if exclude_rules:
@@ -55,12 +78,15 @@ def evaluate_rule(table: Table, rule: str):
         type, rule = "name", rule_parts[0]
         if len(rule_parts) > 1:
             type, rule = tuple(rule_parts[:2])
-        selected_func = getattr(sys.modules[__name__], f"__is_satisfied_by_{type}")
+
+        rule_func = f"{RULE_FUNC_PREFIX}{type}"
+        selected_func = getattr(sys.modules[__name__], rule_func)
         results.append(selected_func(table=table, rule=rule))
+
     return all(results)
 
 
-def __is_satisfied_by_name(table: Table, rule: str = ""):
+def is_satisfied_by_name(table: Table, rule: str = ""):
     """Evaluate rule by Name
 
     Args:
@@ -75,7 +101,22 @@ def __is_satisfied_by_name(table: Table, rule: str = ""):
     return table.name.startswith(rule)
 
 
-def __is_satisfied_by_schema(table: Table, rule: str = ""):
+def is_satisfied_by_exact(table: Table, rule: str = ""):
+    """Evaluate rule by model name with exact match
+
+    Args:
+        table (Table): Table object
+        rule (str, optional): Rule def. Defaults to "".
+
+    Returns:
+        bool: True if satisfied `equal` logic applied to Table name
+    """
+    if not rule:
+        return True
+    return table.name == rule
+
+
+def is_satisfied_by_schema(table: Table, rule: str = ""):
     """Evaluate rule by Schema name
 
     Args:
@@ -96,7 +137,7 @@ def __is_satisfied_by_schema(table: Table, rule: str = ""):
     )
 
 
-def __is_satisfied_by_wildcard(table: Table, rule: str = "*"):
+def is_satisfied_by_wildcard(table: Table, rule: str = "*"):
     """Evaluate rule by Wildcard (Unix Style)
 
     Args:
@@ -111,7 +152,7 @@ def __is_satisfied_by_wildcard(table: Table, rule: str = "*"):
     return fnmatch(table.name, rule)
 
 
-def __is_satisfied_by_exposure(table: Table, rule: str = ""):
+def is_satisfied_by_exposure(table: Table, rule: str = ""):
     """Evaluate rule by dbt Exposure name
 
     Args:
@@ -119,7 +160,7 @@ def __is_satisfied_by_exposure(table: Table, rule: str = ""):
         rule (str, optional): Rule def. Defaults to "".
 
     Returns:
-        bool: True if satisfied table name matched the pattern
+        bool: True if satisfied exposure name exists in the table's exposures
     """
     if not rule:
         return True
diff --git a/dbterd/adapters/algos/meta.py b/dbterd/adapters/meta.py
similarity index 79%
rename from dbterd/adapters/algos/meta.py
rename to dbterd/adapters/meta.py
index e4a20fa..36f52c0 100644
--- a/dbterd/adapters/algos/meta.py
+++ b/dbterd/adapters/meta.py
@@ -1,4 +1,5 @@
 from dataclasses import dataclass, field
+from enum import Enum
 from typing import List, Optional, Tuple
 
 
@@ -32,3 +33,11 @@ class Ref:
     table_map: Tuple[str, str]
     column_map: Tuple[str, str]
     type: str = "n1"
+
+
+class SelectionType(Enum):
+    START_WITH_NAME = ""
+    EXACT_NAME = "exact"
+    SCHEMA = "schema"
+    WILDCARD = "wildcard"
+    EXPOSURE = "exposure"
diff --git a/dbterd/adapters/worker.py b/dbterd/adapters/worker.py
deleted file mode 100644
index 91589a3..0000000
--- a/dbterd/adapters/worker.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from dbterd.adapters.base import Executor
-
-
-class DbtWorker(Executor):
-    """dbt executor"""
-
-    def __init__(self, ctx) -> None:
-        super().__init__(ctx)
-
-    def run(self, **kwargs):
-        super().run(**kwargs)
diff --git a/dbterd/cli/main.py b/dbterd/cli/main.py
index 9285148..6cd5fd9 100644
--- a/dbterd/cli/main.py
+++ b/dbterd/cli/main.py
@@ -3,7 +3,7 @@
 
 import click
 
-from dbterd.adapters.worker import DbtWorker
+from dbterd.adapters.base import Executor
 from dbterd.cli import params
 from dbterd.helpers import jsonify
 from dbterd.helpers.log import logger
@@ -62,8 +62,8 @@ def debug(ctx, **kwargs):
     """Inspect the hidden magics"""
     logger.info("**Arguments used**")
     logger.debug(jsonify.to_json(kwargs))
-    logger.info("**Context used**")
-    logger.debug(jsonify.to_json(ctx.obj))
+    logger.info("**Arguments evaluated**")
+    logger.debug(jsonify.to_json(Executor(ctx).evaluate_kwargs(**kwargs)))
 
 
 # dbterd run
@@ -72,4 +72,4 @@ def debug(ctx, **kwargs):
 @params.common_params
 def run(ctx, **kwargs):
     """Run the convert"""
-    DbtWorker(ctx).run(**kwargs)
+    Executor(ctx).run(**kwargs)
diff --git a/dbterd/cli/params.py b/dbterd/cli/params.py
index 7e8040f..45f8cfc 100644
--- a/dbterd/cli/params.py
+++ b/dbterd/cli/params.py
@@ -9,15 +9,14 @@ def common_params(func):
     @click.option(
         "--artifacts-dir",
         "-ad",
-        help="Specified the full path to dbt artifacts path which known as /target directory",
-        default=default.default_artifact_path(),
-        show_default=True,
+        help="Specified the path to dbt artifact directory which known as /target directory",
+        default="",
         type=click.STRING,
     )
     @click.option(
         "--output",
         "-o",
-        help="Output the result file. Default to the same target dir",
+        help="Output the result file. Default to the cwd/target",
         default=default.default_output_path(),
         show_default=True,
         type=click.STRING,
@@ -76,6 +75,28 @@ def common_params(func):
         multiple=True,
         type=click.STRING,
     )
+    @click.option(
+        "--dbt",
+        help="Flag to indicate the Selecton to follow dbt's one leveraging Programmatic Invocation",
+        is_flag=True,
+        default=False,
+        show_default=True,
+    )
+    @click.option(
+        "--dbt-project-dir",
+        "-dpd",
+        help="Specified dbt project directory path",
+        default="",
+        show_default=True,
+        type=click.STRING,
+    )
+    @click.option(
+        "--dbt-target",
+        "-dt",
+        help="Specified dbt target name",
+        default=None,
+        type=click.STRING,
+    )
     @functools.wraps(func)
     def wrapper(*args, **kwargs):
         return func(*args, **kwargs)  # pragma: no cover
diff --git a/docs/nav/guide/cli-references.md b/docs/nav/guide/cli-references.md
index c1ce0d5..b4f1746 100644
--- a/docs/nav/guide/cli-references.md
+++ b/docs/nav/guide/cli-references.md
@@ -61,7 +61,10 @@ Command to generate diagram-as-a-code file
 ### --artifacts-dir (-ad)
 
 Configure the path to directory containing dbt artifact files.
-> Default to `./target`
+
+It will take the the nested `/target` directory of `--dbt-project-dir` if not specified.
+
+> Default to the current directory's `/target` if both this option and `--dbt-project-dir` option are not specified
 
 **Examples:**
 === "CLI"
@@ -274,6 +277,59 @@ Specified dbt resource type(seed, model, source, snapshot).
     dbterd run --resource-type model
     ```
 
+### --dbt
+
+Flag to indicate the Selecton to follow dbt's one leveraging Programmatic Invocation
+> Default to `False`
+
+**Examples:**
+=== "CLI (use dbt selection)"
+
+    ```bash
+    dbterd run -s +something --dbt
+    # select 'something' and the upstream
+    ```
+=== "CLI (use dbterd selection)"
+
+    ```bash
+    dbterd run -s something
+    # select starts with 'something'
+    ```
+
+### --dbt-project-dir (-dpd)
+
+Specified dbt project directory path
+
+You should specified this option if your CWD is not the dbt project dir, and normally used with `--dbt` enabled. It will take the value of `--artifacts-dir` if not specified.
+
+> Default to the current directory if both this option and `--artifacts-dir` option are not specified
+
+**Examples:**
+=== "CLI"
+
+    ```bash
+    dbterd run -s +something --dbt-project-dir /path/to/dbt/project --dbt
+    # select 'something' and the upstream of the dbt project located at /path/to/dbt/project
+    # the artifacts dir will probably be assumed as: /path/to/dbt/project/target
+    ```
+
+### --dbt-target (-dt)
+
+Specified dbt target name
+
+Probably used with `--dbt` enabled.
+
+> Default to the dbt's profile configuration
+
+**Examples:**
+=== "CLI"
+
+    ```bash
+    dbterd run -s +something --dbt-project-dir /path/to/dbt/project --dbt --dbt-target prod
+    # select 'something' and the upstream of the dbt project located at /path/to/dbt/project using target name 'prod'
+    # the artifacts dir will probably be assumed as: /path/to/dbt/project/target
+    ```
+
 ## debug
 
 Shows hidden configured values
@@ -282,21 +338,41 @@ Shows hidden configured values
 === "Output"
 
     ```
-    2023-04-08 10:15:03,611 - dbterd - INFO - Run with dbterd==0.2.0 (main.py:43)
-    2023-04-08 10:15:03,612 - dbterd - INFO - **Arguments used** (main.py:52)
-    2023-04-08 10:15:03,613 - dbterd - DEBUG - {
+    2023-09-08 16:43:45,066 - dbterd - INFO - Run with dbterd==1.0.0 (main.py:54)
+    2023-09-08 16:43:45,071 - dbterd - INFO - **Arguments used** (main.py:63)
+    2023-09-08 16:43:45,073 - dbterd - DEBUG - {
+        "artifacts_dir": "",
+        "output": "C:\\Users\\DAT\\Documents\\Sources\\dbterd\\target",
+        "select": [],
+        "exclude": [],
+        "target": "dbml",
+        "algo": "test_relationship",
+        "manifest_version": null,
+        "catalog_version": null,
+        "resource_type": [
+            "model"
+        ],
+        "dbt": false,
+        "dbt_project_dir": "",
+        "dbt_target": null
+    } (main.py:64)
+    2023-09-08 16:43:45,079 - dbterd - INFO - **Arguments evaluated** (main.py:65)
+    2023-09-08 16:43:45,081 - dbterd - INFO - Using dbt artifact dir at: C:\Users\DAT\Documents\Sources\dbterd\target (base.py:41)
+    2023-09-08 16:43:45,082 - dbterd - INFO - Using dbt project  dir at: C:\Users\DAT\Documents\Sources\dbterd (base.py:42)
+    2023-09-08 16:43:45,084 - dbterd - DEBUG - {
         "artifacts_dir": "C:\\Users\\DAT\\Documents\\Sources\\dbterd\\target",
-        "manifest_path": null,
         "output": "C:\\Users\\DAT\\Documents\\Sources\\dbterd\\target",
-        "select": null,
-        "exclude": null,
+        "select": [],
+        "exclude": [],
         "target": "dbml",
         "algo": "test_relationship",
         "manifest_version": null,
+        "catalog_version": null,
         "resource_type": [
             "model"
-        ]
-    } (main.py:53)
-    2023-04-08 10:15:03,614 - dbterd - INFO - **Context used** (main.py:54)
-    2023-04-08 10:15:03,614 - dbterd - DEBUG - {} (main.py:55)
+        ],
+        "dbt": false,
+        "dbt_project_dir": "C:\\Users\\DAT\\Documents\\Sources\\dbterd",
+        "dbt_target": null
+    } (main.py:66)
     ```
diff --git a/poetry.lock b/poetry.lock
index 7356ab7..89c9326 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,5 +1,29 @@
 # This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand.
 
+[[package]]
+name = "agate"
+version = "1.7.0"
+description = "A data analysis library that is optimized for humans instead of machines."
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+    {file = "agate-1.7.0-py2.py3-none-any.whl", hash = "sha256:ad529c80fe6943906ab3d3bc59c12307e1181d35993e6055db59fa72dc79a6bd"},
+    {file = "agate-1.7.0.tar.gz", hash = "sha256:a835a1069247b39b0c340e31eb56e1a95e79f679ad37512192118a5ea3336020"},
+]
+
+[package.dependencies]
+Babel = ">=2.0"
+isodate = ">=0.5.4"
+leather = ">=0.3.2"
+parsedatetime = ">=2.1,<2.5 || >2.5,<2.6 || >2.6"
+python-slugify = ">=1.2.1"
+pytimeparse = ">=1.1.5"
+
+[package.extras]
+docs = ["Sphinx (>=1.2.2)", "sphinx-rtd-theme (>=0.1.6)"]
+test = ["PyICU (>=2.4.2)", "coverage (>=3.7.1)", "cssselect (>=0.9.1)", "lxml (>=3.6.0)", "pytest", "pytest-cov", "pytz (>=2015.4)"]
+
 [[package]]
 name = "argcomplete"
 version = "3.1.1"
@@ -121,6 +145,83 @@ files = [
     {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"},
 ]
 
+[[package]]
+name = "cffi"
+version = "1.15.1"
+description = "Foreign Function Interface for Python calling C code."
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+    {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"},
+    {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"},
+    {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"},
+    {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"},
+    {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"},
+    {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"},
+    {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"},
+    {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"},
+    {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"},
+    {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"},
+    {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"},
+    {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"},
+    {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"},
+    {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"},
+    {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"},
+    {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"},
+    {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"},
+    {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"},
+    {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"},
+    {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"},
+    {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"},
+    {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"},
+    {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"},
+    {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"},
+    {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"},
+    {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"},
+    {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"},
+    {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"},
+    {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"},
+    {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"},
+    {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"},
+    {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"},
+    {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"},
+    {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"},
+    {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"},
+    {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"},
+    {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"},
+    {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"},
+    {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"},
+    {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"},
+    {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"},
+    {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"},
+    {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"},
+    {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"},
+    {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"},
+    {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"},
+    {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"},
+    {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"},
+    {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"},
+    {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"},
+    {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"},
+    {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"},
+    {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"},
+    {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"},
+    {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"},
+    {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"},
+    {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"},
+    {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"},
+    {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"},
+    {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"},
+    {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"},
+    {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"},
+    {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"},
+    {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"},
+]
+
+[package.dependencies]
+pycparser = "*"
+
 [[package]]
 name = "cfgv"
 version = "3.4.0"
@@ -387,6 +488,84 @@ pydantic = ">=1.6,<2.0"
 dev = ["build (==0.7.0)", "flit (==3.7.1)", "pdoc3 (>=0.9.2)", "pre-commit (>=2.15.0)", "pyyaml (>=5.3)", "yapf (>=0.29.0)"]
 test = ["black (==21.9b0)", "flake8 (>=3.8.3,<4.0.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "pylint (>=2.12.0)", "pytest (>=6.2.4,<7.0.0)", "yapf (>=0.29.0)"]
 
+[[package]]
+name = "dbt-core"
+version = "1.5.6"
+description = "With dbt, data analysts and engineers can build analytics the way engineers build applications."
+category = "dev"
+optional = false
+python-versions = ">=3.7.2"
+files = [
+    {file = "dbt-core-1.5.6.tar.gz", hash = "sha256:af3c03cd4a1fc92481362888014ca1ffed2ffef0b0e0d98463ad0f26c49ef458"},
+    {file = "dbt_core-1.5.6-py3-none-any.whl", hash = "sha256:030d2179f9efbf8ccea079296d0c79278d963bb2475c0bcce9ca4bbb0d8c393c"},
+]
+
+[package.dependencies]
+agate = ">=1.6,<1.7.1"
+cffi = ">=1.9,<2.0.0"
+click = "<9"
+colorama = ">=0.3.9,<0.4.7"
+dbt-extractor = ">=0.4.1,<0.5.0"
+hologram = ">=0.0.14,<=0.0.16"
+idna = ">=2.5,<4"
+isodate = ">=0.6,<0.7"
+Jinja2 = "3.1.2"
+logbook = ">=1.5,<1.6"
+mashumaro = {version = "3.6", extras = ["msgpack"]}
+minimal-snowplow-tracker = "0.0.2"
+networkx = {version = ">=2.3,<3", markers = "python_version >= \"3.8\""}
+packaging = ">20.9"
+pathspec = ">=0.9,<0.12"
+protobuf = ">=4.0.0"
+pytz = ">=2015.7"
+pyyaml = ">=6.0"
+requests = "<3.0.0"
+sqlparse = ">=0.2.3,<0.5"
+typing-extensions = ">=3.7.4"
+werkzeug = ">=1,<3"
+
+[[package]]
+name = "dbt-extractor"
+version = "0.4.1"
+description = "A tool to analyze and extract information from Jinja used in dbt projects."
+category = "dev"
+optional = false
+python-versions = ">=3.6.1"
+files = [
+    {file = "dbt_extractor-0.4.1-cp36-abi3-macosx_10_7_x86_64.whl", hash = "sha256:4dc715bd740e418d8dc1dd418fea508e79208a24cf5ab110b0092a3cbe96bf71"},
+    {file = "dbt_extractor-0.4.1-cp36-abi3-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:bc9e0050e3a2f4ea9fe58e8794bc808e6709a0c688ed710fc7c5b6ef3e5623ec"},
+    {file = "dbt_extractor-0.4.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76872cdee659075d6ce2df92dc62e59a74ba571be62acab2e297ca478b49d766"},
+    {file = "dbt_extractor-0.4.1-cp36-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:81435841610be1b07806d72cd89b1956c6e2a84c360b9ceb3f949c62a546d569"},
+    {file = "dbt_extractor-0.4.1-cp36-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7c291f9f483eae4f60dd5859097d7ba51d5cb6c4725f08973ebd18cdea89d758"},
+    {file = "dbt_extractor-0.4.1-cp36-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:822b1e911db230e1b9701c99896578e711232001027b518c44c32f79a46fa3f9"},
+    {file = "dbt_extractor-0.4.1-cp36-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:554d27741a54599c39e5c0b7dbcab77400d83f908caba284a3e960db812e5814"},
+    {file = "dbt_extractor-0.4.1-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a805d51a25317f53cbff951c79b9cf75421cf48e4b3e1dfb3e9e8de6d824b76c"},
+    {file = "dbt_extractor-0.4.1-cp36-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cad90ddc708cb4182dc16fe2c87b1f088a1679877b93e641af068eb68a25d582"},
+    {file = "dbt_extractor-0.4.1-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:34783d788b133f223844e280e37b3f5244f2fb60acc457aa75c2667e418d5442"},
+    {file = "dbt_extractor-0.4.1-cp36-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:9da211869a1220ea55c5552c1567a3ea5233a6c52fa89ca87a22465481c37bc9"},
+    {file = "dbt_extractor-0.4.1-cp36-abi3-musllinux_1_2_i686.whl", hash = "sha256:7d7c47774dc051b8c18690281a55e2e3d3320e823b17e04b06bc3ff81b1874ba"},
+    {file = "dbt_extractor-0.4.1-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:037907a7c7ae0391045d81338ca77ddaef899a91d80f09958f09fe374594e19b"},
+    {file = "dbt_extractor-0.4.1-cp36-abi3-win32.whl", hash = "sha256:3fe8d8e28a7bd3e0884896147269ca0202ca432d8733113386bdc84c824561bf"},
+    {file = "dbt_extractor-0.4.1-cp36-abi3-win_amd64.whl", hash = "sha256:35265a0ae0a250623b0c2e3308b2738dc8212e40e0aa88407849e9ea090bb312"},
+    {file = "dbt_extractor-0.4.1.tar.gz", hash = "sha256:75b1c665699ec0f1ffce1ba3d776f7dfce802156f22e70a7b9c8f0b4d7e80f42"},
+]
+
+[[package]]
+name = "dbt-postgres"
+version = "1.5.6"
+description = "The postgres adapter plugin for dbt (data build tool)"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "dbt-postgres-1.5.6.tar.gz", hash = "sha256:b74e471dc661819a3d4bda2d11497935661ac2e25786c8a5b7314d8241b18582"},
+    {file = "dbt_postgres-1.5.6-py3-none-any.whl", hash = "sha256:bc5711c9ab0ec4b57ab814b2c4e4c973554c8374b7da94b06814ac81c91f67ef"},
+]
+
+[package.dependencies]
+dbt-core = "1.5.6"
+psycopg2-binary = ">=2.8,<3.0"
+
 [[package]]
 name = "distlib"
 version = "0.3.7"
@@ -471,6 +650,17 @@ mccabe = ">=0.7.0,<0.8.0"
 pycodestyle = ">=2.11.0,<2.12.0"
 pyflakes = ">=3.1.0,<3.2.0"
 
+[[package]]
+name = "future"
+version = "0.18.3"
+description = "Clean single-source support for Python 3 and 2"
+category = "dev"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+files = [
+    {file = "future-0.18.3.tar.gz", hash = "sha256:34a17436ed1e96697a86f9de3d15a3b0be01d8bc8de9c1dffd59fb8234ed5307"},
+]
+
 [[package]]
 name = "genson"
 version = "1.2.2"
@@ -500,6 +690,22 @@ python-dateutil = ">=2.8.1"
 [package.extras]
 dev = ["flake8", "markdown", "twine", "wheel"]
 
+[[package]]
+name = "hologram"
+version = "0.0.16"
+description = "JSON schema generation from dataclasses"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+    {file = "hologram-0.0.16-py3-none-any.whl", hash = "sha256:4e56bd525336bb64a18916f871977a4125b64be8aaa750233583003333cda361"},
+    {file = "hologram-0.0.16.tar.gz", hash = "sha256:1c2c921b4e575361623ea0e0d0aa5aee377b1a333cc6c6a879e213ed34583e55"},
+]
+
+[package.dependencies]
+jsonschema = ">=3.0"
+python-dateutil = ">=2.8,<2.9"
+
 [[package]]
 name = "htmlmin2"
 version = "0.1.13"
@@ -586,6 +792,21 @@ files = [
     {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
 ]
 
+[[package]]
+name = "isodate"
+version = "0.6.1"
+description = "An ISO 8601 date/time/duration parser and formatter"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+    {file = "isodate-0.6.1-py2.py3-none-any.whl", hash = "sha256:0751eece944162659049d35f4f549ed815792b38793f07cf73381c1c87cbed96"},
+    {file = "isodate-0.6.1.tar.gz", hash = "sha256:48c5881de7e8b0a0d648cb024c8062dc84e7b840ed81e864c7614fd3c127bde9"},
+]
+
+[package.dependencies]
+six = "*"
+
 [[package]]
 name = "isort"
 version = "5.12.0"
@@ -717,6 +938,51 @@ files = [
     {file = "lazy_object_proxy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f"},
 ]
 
+[[package]]
+name = "leather"
+version = "0.3.4"
+description = "Python charting for 80% of humans."
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+    {file = "leather-0.3.4-py2.py3-none-any.whl", hash = "sha256:5e741daee96e9f1e9e06081b8c8a10c4ac199301a0564cdd99b09df15b4603d2"},
+    {file = "leather-0.3.4.tar.gz", hash = "sha256:b43e21c8fa46b2679de8449f4d953c06418666dc058ce41055ee8a8d3bb40918"},
+]
+
+[package.dependencies]
+six = ">=1.6.1"
+
+[[package]]
+name = "logbook"
+version = "1.5.3"
+description = "A logging replacement for Python"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+    {file = "Logbook-1.5.3-cp27-cp27m-win32.whl", hash = "sha256:56ee54c11df3377314cedcd6507638f015b4b88c0238c2e01b5eb44fd3a6ad1b"},
+    {file = "Logbook-1.5.3-cp27-cp27m-win_amd64.whl", hash = "sha256:2dc85f1510533fddb481e97677bb7bca913560862734c0b3b289bfed04f78c92"},
+    {file = "Logbook-1.5.3-cp35-cp35m-win32.whl", hash = "sha256:94e2e11ff3c2304b0d09a36c6208e5ae756eb948b210e5cbd63cd8d27f911542"},
+    {file = "Logbook-1.5.3-cp35-cp35m-win_amd64.whl", hash = "sha256:97fee1bd9605f76335b169430ed65e15e457a844b2121bd1d90a08cf7e30aba0"},
+    {file = "Logbook-1.5.3-cp36-cp36m-win32.whl", hash = "sha256:7c533eb728b3d220b1b5414ba4635292d149d79f74f6973b4aa744c850ca944a"},
+    {file = "Logbook-1.5.3-cp36-cp36m-win_amd64.whl", hash = "sha256:e18f7422214b1cf0240c56f884fd9c9b4ff9d0da2eabca9abccba56df7222f66"},
+    {file = "Logbook-1.5.3-cp37-cp37m-win32.whl", hash = "sha256:8f76a2e7b1f72595f753228732f81ce342caf03babc3fed6bbdcf366f2f20f18"},
+    {file = "Logbook-1.5.3-cp37-cp37m-win_amd64.whl", hash = "sha256:0cf2cdbfb65a03b5987d19109dacad13417809dcf697f66e1a7084fb21744ea9"},
+    {file = "Logbook-1.5.3.tar.gz", hash = "sha256:66f454ada0f56eae43066f604a222b09893f98c1adc18df169710761b8f32fe8"},
+]
+
+[package.extras]
+all = ["Jinja2", "brotli", "cython", "execnet (>=1.0.9)", "mock", "pytest", "pytest-cov (<2.6)", "pyzmq", "redis", "sqlalchemy"]
+compression = ["brotli"]
+dev = ["cython", "mock", "pytest", "pytest-cov (<2.6)"]
+execnet = ["execnet (>=1.0.9)"]
+jinja = ["Jinja2"]
+redis = ["redis"]
+sqlalchemy = ["sqlalchemy"]
+test = ["mock", "pytest", "pytest-cov (<2.6)"]
+zmq = ["pyzmq"]
+
 [[package]]
 name = "markdown"
 version = "3.4.4"
@@ -796,6 +1062,28 @@ files = [
     {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"},
 ]
 
+[[package]]
+name = "mashumaro"
+version = "3.6"
+description = "Fast serialization library on top of dataclasses"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "mashumaro-3.6-py3-none-any.whl", hash = "sha256:77403e3e2ecd0a7d0e22d472c08e33282460e48726eabe356c5163efbdf9c7ee"},
+    {file = "mashumaro-3.6.tar.gz", hash = "sha256:ceb3de53029219bbbb0385ca600b59348dcd14e0c68523986c6d51889ad338f5"},
+]
+
+[package.dependencies]
+msgpack = {version = ">=0.5.6", optional = true, markers = "extra == \"msgpack\""}
+typing-extensions = ">=4.1.0"
+
+[package.extras]
+msgpack = ["msgpack (>=0.5.6)"]
+orjson = ["orjson"]
+toml = ["tomli (>=1.1.0)", "tomli-w (>=1.0)"]
+yaml = ["pyyaml (>=3.13)"]
+
 [[package]]
 name = "mccabe"
 version = "0.7.0"
@@ -842,6 +1130,21 @@ verspec = "*"
 dev = ["coverage", "flake8 (>=3.0)", "shtab"]
 test = ["coverage", "flake8 (>=3.0)", "shtab"]
 
+[[package]]
+name = "minimal-snowplow-tracker"
+version = "0.0.2"
+description = "A minimal snowplow event tracker for Python. Add analytics to your Python and Django apps, webapps and games"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+    {file = "minimal-snowplow-tracker-0.0.2.tar.gz", hash = "sha256:acabf7572db0e7f5cbf6983d495eef54081f71be392330eb3aadb9ccb39daaa4"},
+]
+
+[package.dependencies]
+requests = ">=2.2.1,<3.0"
+six = ">=1.9.0,<2.0"
+
 [[package]]
 name = "mkdocs"
 version = "1.5.2"
@@ -929,6 +1232,79 @@ htmlmin2 = ">=0.1.13"
 jsmin = ">=3.0.1"
 mkdocs = ">=1.4.1"
 
+[[package]]
+name = "msgpack"
+version = "1.0.5"
+description = "MessagePack serializer"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+    {file = "msgpack-1.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:525228efd79bb831cf6830a732e2e80bc1b05436b086d4264814b4b2955b2fa9"},
+    {file = "msgpack-1.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f8d8b3bf1ff2672567d6b5c725a1b347fe838b912772aa8ae2bf70338d5a198"},
+    {file = "msgpack-1.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdc793c50be3f01106245a61b739328f7dccc2c648b501e237f0699fe1395b81"},
+    {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cb47c21a8a65b165ce29f2bec852790cbc04936f502966768e4aae9fa763cb7"},
+    {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e42b9594cc3bf4d838d67d6ed62b9e59e201862a25e9a157019e171fbe672dd3"},
+    {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:55b56a24893105dc52c1253649b60f475f36b3aa0fc66115bffafb624d7cb30b"},
+    {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1967f6129fc50a43bfe0951c35acbb729be89a55d849fab7686004da85103f1c"},
+    {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20a97bf595a232c3ee6d57ddaadd5453d174a52594bf9c21d10407e2a2d9b3bd"},
+    {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d25dd59bbbbb996eacf7be6b4ad082ed7eacc4e8f3d2df1ba43822da9bfa122a"},
+    {file = "msgpack-1.0.5-cp310-cp310-win32.whl", hash = "sha256:382b2c77589331f2cb80b67cc058c00f225e19827dbc818d700f61513ab47bea"},
+    {file = "msgpack-1.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:4867aa2df9e2a5fa5f76d7d5565d25ec76e84c106b55509e78c1ede0f152659a"},
+    {file = "msgpack-1.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9f5ae84c5c8a857ec44dc180a8b0cc08238e021f57abdf51a8182e915e6299f0"},
+    {file = "msgpack-1.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9e6ca5d5699bcd89ae605c150aee83b5321f2115695e741b99618f4856c50898"},
+    {file = "msgpack-1.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5494ea30d517a3576749cad32fa27f7585c65f5f38309c88c6d137877fa28a5a"},
+    {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ab2f3331cb1b54165976a9d976cb251a83183631c88076613c6c780f0d6e45a"},
+    {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28592e20bbb1620848256ebc105fc420436af59515793ed27d5c77a217477705"},
+    {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe5c63197c55bce6385d9aee16c4d0641684628f63ace85f73571e65ad1c1e8d"},
+    {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ed40e926fa2f297e8a653c954b732f125ef97bdd4c889f243182299de27e2aa9"},
+    {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b2de4c1c0538dcb7010902a2b97f4e00fc4ddf2c8cda9749af0e594d3b7fa3d7"},
+    {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bf22a83f973b50f9d38e55c6aade04c41ddda19b00c4ebc558930d78eecc64ed"},
+    {file = "msgpack-1.0.5-cp311-cp311-win32.whl", hash = "sha256:c396e2cc213d12ce017b686e0f53497f94f8ba2b24799c25d913d46c08ec422c"},
+    {file = "msgpack-1.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c4c68d87497f66f96d50142a2b73b97972130d93677ce930718f68828b382e2"},
+    {file = "msgpack-1.0.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a2b031c2e9b9af485d5e3c4520f4220d74f4d222a5b8dc8c1a3ab9448ca79c57"},
+    {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f837b93669ce4336e24d08286c38761132bc7ab29782727f8557e1eb21b2080"},
+    {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1d46dfe3832660f53b13b925d4e0fa1432b00f5f7210eb3ad3bb9a13c6204a6"},
+    {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:366c9a7b9057e1547f4ad51d8facad8b406bab69c7d72c0eb6f529cf76d4b85f"},
+    {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:4c075728a1095efd0634a7dccb06204919a2f67d1893b6aa8e00497258bf926c"},
+    {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:f933bbda5a3ee63b8834179096923b094b76f0c7a73c1cfe8f07ad608c58844b"},
+    {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:36961b0568c36027c76e2ae3ca1132e35123dcec0706c4b7992683cc26c1320c"},
+    {file = "msgpack-1.0.5-cp36-cp36m-win32.whl", hash = "sha256:b5ef2f015b95f912c2fcab19c36814963b5463f1fb9049846994b007962743e9"},
+    {file = "msgpack-1.0.5-cp36-cp36m-win_amd64.whl", hash = "sha256:288e32b47e67f7b171f86b030e527e302c91bd3f40fd9033483f2cacc37f327a"},
+    {file = "msgpack-1.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:137850656634abddfb88236008339fdaba3178f4751b28f270d2ebe77a563b6c"},
+    {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c05a4a96585525916b109bb85f8cb6511db1c6f5b9d9cbcbc940dc6b4be944b"},
+    {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56a62ec00b636583e5cb6ad313bbed36bb7ead5fa3a3e38938503142c72cba4f"},
+    {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef8108f8dedf204bb7b42994abf93882da1159728a2d4c5e82012edd92c9da9f"},
+    {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1835c84d65f46900920b3708f5ba829fb19b1096c1800ad60bae8418652a951d"},
+    {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:e57916ef1bd0fee4f21c4600e9d1da352d8816b52a599c46460e93a6e9f17086"},
+    {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:17358523b85973e5f242ad74aa4712b7ee560715562554aa2134d96e7aa4cbbf"},
+    {file = "msgpack-1.0.5-cp37-cp37m-win32.whl", hash = "sha256:cb5aaa8c17760909ec6cb15e744c3ebc2ca8918e727216e79607b7bbce9c8f77"},
+    {file = "msgpack-1.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:ab31e908d8424d55601ad7075e471b7d0140d4d3dd3272daf39c5c19d936bd82"},
+    {file = "msgpack-1.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b72d0698f86e8d9ddf9442bdedec15b71df3598199ba33322d9711a19f08145c"},
+    {file = "msgpack-1.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:379026812e49258016dd84ad79ac8446922234d498058ae1d415f04b522d5b2d"},
+    {file = "msgpack-1.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:332360ff25469c346a1c5e47cbe2a725517919892eda5cfaffe6046656f0b7bb"},
+    {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:476a8fe8fae289fdf273d6d2a6cb6e35b5a58541693e8f9f019bfe990a51e4ba"},
+    {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9985b214f33311df47e274eb788a5893a761d025e2b92c723ba4c63936b69b1"},
+    {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48296af57cdb1d885843afd73c4656be5c76c0c6328db3440c9601a98f303d87"},
+    {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:addab7e2e1fcc04bd08e4eb631c2a90960c340e40dfc4a5e24d2ff0d5a3b3edb"},
+    {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:916723458c25dfb77ff07f4c66aed34e47503b2eb3188b3adbec8d8aa6e00f48"},
+    {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:821c7e677cc6acf0fd3f7ac664c98803827ae6de594a9f99563e48c5a2f27eb0"},
+    {file = "msgpack-1.0.5-cp38-cp38-win32.whl", hash = "sha256:1c0f7c47f0087ffda62961d425e4407961a7ffd2aa004c81b9c07d9269512f6e"},
+    {file = "msgpack-1.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:bae7de2026cbfe3782c8b78b0db9cbfc5455e079f1937cb0ab8d133496ac55e1"},
+    {file = "msgpack-1.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:20c784e66b613c7f16f632e7b5e8a1651aa5702463d61394671ba07b2fc9e025"},
+    {file = "msgpack-1.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:266fa4202c0eb94d26822d9bfd7af25d1e2c088927fe8de9033d929dd5ba24c5"},
+    {file = "msgpack-1.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:18334484eafc2b1aa47a6d42427da7fa8f2ab3d60b674120bce7a895a0a85bdd"},
+    {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57e1f3528bd95cc44684beda696f74d3aaa8a5e58c816214b9046512240ef437"},
+    {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:586d0d636f9a628ddc6a17bfd45aa5b5efaf1606d2b60fa5d87b8986326e933f"},
+    {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a740fa0e4087a734455f0fc3abf5e746004c9da72fbd541e9b113013c8dc3282"},
+    {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3055b0455e45810820db1f29d900bf39466df96ddca11dfa6d074fa47054376d"},
+    {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a61215eac016f391129a013c9e46f3ab308db5f5ec9f25811e811f96962599a8"},
+    {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:362d9655cd369b08fda06b6657a303eb7172d5279997abe094512e919cf74b11"},
+    {file = "msgpack-1.0.5-cp39-cp39-win32.whl", hash = "sha256:ac9dd47af78cae935901a9a500104e2dea2e253207c924cc95de149606dc43cc"},
+    {file = "msgpack-1.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:06f5174b5f8ed0ed919da0e62cbd4ffde676a374aba4020034da05fab67b9164"},
+    {file = "msgpack-1.0.5.tar.gz", hash = "sha256:c075544284eadc5cddc70f4757331d99dcbc16b2bbd4849d15f8aae4cf36d31c"},
+]
+
 [[package]]
 name = "mypy-extensions"
 version = "1.0.0"
@@ -941,6 +1317,25 @@ files = [
     {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
 ]
 
+[[package]]
+name = "networkx"
+version = "2.8.8"
+description = "Python package for creating and manipulating graphs and networks"
+category = "dev"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "networkx-2.8.8-py3-none-any.whl", hash = "sha256:e435dfa75b1d7195c7b8378c3859f0445cd88c6b0375c181ed66823a9ceb7524"},
+    {file = "networkx-2.8.8.tar.gz", hash = "sha256:230d388117af870fce5647a3c52401fcf753e94720e6ea6b4197a5355648885e"},
+]
+
+[package.extras]
+default = ["matplotlib (>=3.4)", "numpy (>=1.19)", "pandas (>=1.3)", "scipy (>=1.8)"]
+developer = ["mypy (>=0.982)", "pre-commit (>=2.20)"]
+doc = ["nb2plots (>=0.6)", "numpydoc (>=1.5)", "pillow (>=9.2)", "pydata-sphinx-theme (>=0.11)", "sphinx (>=5.2)", "sphinx-gallery (>=0.11)", "texext (>=0.6.6)"]
+extra = ["lxml (>=4.6)", "pydot (>=1.4.2)", "pygraphviz (>=1.9)", "sympy (>=1.10)"]
+test = ["codecov (>=2.1)", "pytest (>=7.2)", "pytest-cov (>=4.0)"]
+
 [[package]]
 name = "nodeenv"
 version = "1.8.0"
@@ -1016,6 +1411,21 @@ files = [
     {file = "paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d"},
 ]
 
+[[package]]
+name = "parsedatetime"
+version = "2.4"
+description = "Parse human-readable date/time text."
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+    {file = "parsedatetime-2.4-py2-none-any.whl", hash = "sha256:9ee3529454bf35c40a77115f5a596771e59e1aee8c53306f346c461b8e913094"},
+    {file = "parsedatetime-2.4.tar.gz", hash = "sha256:3d817c58fb9570d1eec1dd46fa9448cd644eeed4fb612684b02dfda3a79cb84b"},
+]
+
+[package.dependencies]
+future = "*"
+
 [[package]]
 name = "pastel"
 version = "0.2.1"
@@ -1149,6 +1559,99 @@ nodeenv = ">=0.11.1"
 pyyaml = ">=5.1"
 virtualenv = ">=20.10.0"
 
+[[package]]
+name = "protobuf"
+version = "4.24.3"
+description = ""
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "protobuf-4.24.3-cp310-abi3-win32.whl", hash = "sha256:20651f11b6adc70c0f29efbe8f4a94a74caf61b6200472a9aea6e19898f9fcf4"},
+    {file = "protobuf-4.24.3-cp310-abi3-win_amd64.whl", hash = "sha256:3d42e9e4796a811478c783ef63dc85b5a104b44aaaca85d4864d5b886e4b05e3"},
+    {file = "protobuf-4.24.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:6e514e8af0045be2b56e56ae1bb14f43ce7ffa0f68b1c793670ccbe2c4fc7d2b"},
+    {file = "protobuf-4.24.3-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:ba53c2f04798a326774f0e53b9c759eaef4f6a568ea7072ec6629851c8435959"},
+    {file = "protobuf-4.24.3-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:f6ccbcf027761a2978c1406070c3788f6de4a4b2cc20800cc03d52df716ad675"},
+    {file = "protobuf-4.24.3-cp37-cp37m-win32.whl", hash = "sha256:1b182c7181a2891e8f7f3a1b5242e4ec54d1f42582485a896e4de81aa17540c2"},
+    {file = "protobuf-4.24.3-cp37-cp37m-win_amd64.whl", hash = "sha256:b0271a701e6782880d65a308ba42bc43874dabd1a0a0f41f72d2dac3b57f8e76"},
+    {file = "protobuf-4.24.3-cp38-cp38-win32.whl", hash = "sha256:e29d79c913f17a60cf17c626f1041e5288e9885c8579832580209de8b75f2a52"},
+    {file = "protobuf-4.24.3-cp38-cp38-win_amd64.whl", hash = "sha256:067f750169bc644da2e1ef18c785e85071b7c296f14ac53e0900e605da588719"},
+    {file = "protobuf-4.24.3-cp39-cp39-win32.whl", hash = "sha256:2da777d34b4f4f7613cdf85c70eb9a90b1fbef9d36ae4a0ccfe014b0b07906f1"},
+    {file = "protobuf-4.24.3-cp39-cp39-win_amd64.whl", hash = "sha256:f631bb982c5478e0c1c70eab383af74a84be66945ebf5dd6b06fc90079668d0b"},
+    {file = "protobuf-4.24.3-py3-none-any.whl", hash = "sha256:f6f8dc65625dadaad0c8545319c2e2f0424fede988368893ca3844261342c11a"},
+    {file = "protobuf-4.24.3.tar.gz", hash = "sha256:12e9ad2ec079b833176d2921be2cb24281fa591f0b119b208b788adc48c2561d"},
+]
+
+[[package]]
+name = "psycopg2-binary"
+version = "2.9.7"
+description = "psycopg2 - Python-PostgreSQL Database Adapter"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+files = [
+    {file = "psycopg2-binary-2.9.7.tar.gz", hash = "sha256:1b918f64a51ffe19cd2e230b3240ba481330ce1d4b7875ae67305bd1d37b041c"},
+    {file = "psycopg2_binary-2.9.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ea5f8ee87f1eddc818fc04649d952c526db4426d26bab16efbe5a0c52b27d6ab"},
+    {file = "psycopg2_binary-2.9.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2993ccb2b7e80844d534e55e0f12534c2871952f78e0da33c35e648bf002bbff"},
+    {file = "psycopg2_binary-2.9.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbbc3c5d15ed76b0d9db7753c0db40899136ecfe97d50cbde918f630c5eb857a"},
+    {file = "psycopg2_binary-2.9.7-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:692df8763b71d42eb8343f54091368f6f6c9cfc56dc391858cdb3c3ef1e3e584"},
+    {file = "psycopg2_binary-2.9.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9dcfd5d37e027ec393a303cc0a216be564b96c80ba532f3d1e0d2b5e5e4b1e6e"},
+    {file = "psycopg2_binary-2.9.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17cc17a70dfb295a240db7f65b6d8153c3d81efb145d76da1e4a096e9c5c0e63"},
+    {file = "psycopg2_binary-2.9.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e5666632ba2b0d9757b38fc17337d84bdf932d38563c5234f5f8c54fd01349c9"},
+    {file = "psycopg2_binary-2.9.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7db7b9b701974c96a88997d458b38ccb110eba8f805d4b4f74944aac48639b42"},
+    {file = "psycopg2_binary-2.9.7-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c82986635a16fb1fa15cd5436035c88bc65c3d5ced1cfaac7f357ee9e9deddd4"},
+    {file = "psycopg2_binary-2.9.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4fe13712357d802080cfccbf8c6266a3121dc0e27e2144819029095ccf708372"},
+    {file = "psycopg2_binary-2.9.7-cp310-cp310-win32.whl", hash = "sha256:122641b7fab18ef76b18860dd0c772290566b6fb30cc08e923ad73d17461dc63"},
+    {file = "psycopg2_binary-2.9.7-cp310-cp310-win_amd64.whl", hash = "sha256:f8651cf1f144f9ee0fa7d1a1df61a9184ab72962531ca99f077bbdcba3947c58"},
+    {file = "psycopg2_binary-2.9.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4ecc15666f16f97709106d87284c136cdc82647e1c3f8392a672616aed3c7151"},
+    {file = "psycopg2_binary-2.9.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fbb1184c7e9d28d67671992970718c05af5f77fc88e26fd7136613c4ece1f89"},
+    {file = "psycopg2_binary-2.9.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a7968fd20bd550431837656872c19575b687f3f6f98120046228e451e4064df"},
+    {file = "psycopg2_binary-2.9.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:094af2e77a1976efd4956a031028774b827029729725e136514aae3cdf49b87b"},
+    {file = "psycopg2_binary-2.9.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26484e913d472ecb6b45937ea55ce29c57c662066d222fb0fbdc1fab457f18c5"},
+    {file = "psycopg2_binary-2.9.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f309b77a7c716e6ed9891b9b42953c3ff7d533dc548c1e33fddc73d2f5e21f9"},
+    {file = "psycopg2_binary-2.9.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6d92e139ca388ccfe8c04aacc163756e55ba4c623c6ba13d5d1595ed97523e4b"},
+    {file = "psycopg2_binary-2.9.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:2df562bb2e4e00ee064779902d721223cfa9f8f58e7e52318c97d139cf7f012d"},
+    {file = "psycopg2_binary-2.9.7-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:4eec5d36dbcfc076caab61a2114c12094c0b7027d57e9e4387b634e8ab36fd44"},
+    {file = "psycopg2_binary-2.9.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1011eeb0c51e5b9ea1016f0f45fa23aca63966a4c0afcf0340ccabe85a9f65bd"},
+    {file = "psycopg2_binary-2.9.7-cp311-cp311-win32.whl", hash = "sha256:ded8e15f7550db9e75c60b3d9fcbc7737fea258a0f10032cdb7edc26c2a671fd"},
+    {file = "psycopg2_binary-2.9.7-cp311-cp311-win_amd64.whl", hash = "sha256:8a136c8aaf6615653450817a7abe0fc01e4ea720ae41dfb2823eccae4b9062a3"},
+    {file = "psycopg2_binary-2.9.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2dec5a75a3a5d42b120e88e6ed3e3b37b46459202bb8e36cd67591b6e5feebc1"},
+    {file = "psycopg2_binary-2.9.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc10da7e7df3380426521e8c1ed975d22df678639da2ed0ec3244c3dc2ab54c8"},
+    {file = "psycopg2_binary-2.9.7-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee919b676da28f78f91b464fb3e12238bd7474483352a59c8a16c39dfc59f0c5"},
+    {file = "psycopg2_binary-2.9.7-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb1c0e682138f9067a58fc3c9a9bf1c83d8e08cfbee380d858e63196466d5c86"},
+    {file = "psycopg2_binary-2.9.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00d8db270afb76f48a499f7bb8fa70297e66da67288471ca873db88382850bf4"},
+    {file = "psycopg2_binary-2.9.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:9b0c2b466b2f4d89ccc33784c4ebb1627989bd84a39b79092e560e937a11d4ac"},
+    {file = "psycopg2_binary-2.9.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:51d1b42d44f4ffb93188f9b39e6d1c82aa758fdb8d9de65e1ddfe7a7d250d7ad"},
+    {file = "psycopg2_binary-2.9.7-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:11abdbfc6f7f7dea4a524b5f4117369b0d757725798f1593796be6ece20266cb"},
+    {file = "psycopg2_binary-2.9.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:f02f4a72cc3ab2565c6d9720f0343cb840fb2dc01a2e9ecb8bc58ccf95dc5c06"},
+    {file = "psycopg2_binary-2.9.7-cp37-cp37m-win32.whl", hash = "sha256:81d5dd2dd9ab78d31a451e357315f201d976c131ca7d43870a0e8063b6b7a1ec"},
+    {file = "psycopg2_binary-2.9.7-cp37-cp37m-win_amd64.whl", hash = "sha256:62cb6de84d7767164a87ca97e22e5e0a134856ebcb08f21b621c6125baf61f16"},
+    {file = "psycopg2_binary-2.9.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:59f7e9109a59dfa31efa022e94a244736ae401526682de504e87bd11ce870c22"},
+    {file = "psycopg2_binary-2.9.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:95a7a747bdc3b010bb6a980f053233e7610276d55f3ca506afff4ad7749ab58a"},
+    {file = "psycopg2_binary-2.9.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c721ee464e45ecf609ff8c0a555018764974114f671815a0a7152aedb9f3343"},
+    {file = "psycopg2_binary-2.9.7-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4f37bbc6588d402980ffbd1f3338c871368fb4b1cfa091debe13c68bb3852b3"},
+    {file = "psycopg2_binary-2.9.7-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac83ab05e25354dad798401babaa6daa9577462136ba215694865394840e31f8"},
+    {file = "psycopg2_binary-2.9.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:024eaeb2a08c9a65cd5f94b31ace1ee3bb3f978cd4d079406aef85169ba01f08"},
+    {file = "psycopg2_binary-2.9.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1c31c2606ac500dbd26381145684d87730a2fac9a62ebcfbaa2b119f8d6c19f4"},
+    {file = "psycopg2_binary-2.9.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:42a62ef0e5abb55bf6ffb050eb2b0fcd767261fa3faf943a4267539168807522"},
+    {file = "psycopg2_binary-2.9.7-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:7952807f95c8eba6a8ccb14e00bf170bb700cafcec3924d565235dffc7dc4ae8"},
+    {file = "psycopg2_binary-2.9.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e02bc4f2966475a7393bd0f098e1165d470d3fa816264054359ed4f10f6914ea"},
+    {file = "psycopg2_binary-2.9.7-cp38-cp38-win32.whl", hash = "sha256:fdca0511458d26cf39b827a663d7d87db6f32b93efc22442a742035728603d5f"},
+    {file = "psycopg2_binary-2.9.7-cp38-cp38-win_amd64.whl", hash = "sha256:d0b16e5bb0ab78583f0ed7ab16378a0f8a89a27256bb5560402749dbe8a164d7"},
+    {file = "psycopg2_binary-2.9.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6822c9c63308d650db201ba22fe6648bd6786ca6d14fdaf273b17e15608d0852"},
+    {file = "psycopg2_binary-2.9.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f94cb12150d57ea433e3e02aabd072205648e86f1d5a0a692d60242f7809b15"},
+    {file = "psycopg2_binary-2.9.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5ee89587696d808c9a00876065d725d4ae606f5f7853b961cdbc348b0f7c9a1"},
+    {file = "psycopg2_binary-2.9.7-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad5ec10b53cbb57e9a2e77b67e4e4368df56b54d6b00cc86398578f1c635f329"},
+    {file = "psycopg2_binary-2.9.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:642df77484b2dcaf87d4237792246d8068653f9e0f5c025e2c692fc56b0dda70"},
+    {file = "psycopg2_binary-2.9.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6a8b575ac45af1eaccbbcdcf710ab984fd50af048fe130672377f78aaff6fc1"},
+    {file = "psycopg2_binary-2.9.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f955aa50d7d5220fcb6e38f69ea126eafecd812d96aeed5d5f3597f33fad43bb"},
+    {file = "psycopg2_binary-2.9.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ad26d4eeaa0d722b25814cce97335ecf1b707630258f14ac4d2ed3d1d8415265"},
+    {file = "psycopg2_binary-2.9.7-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ced63c054bdaf0298f62681d5dcae3afe60cbae332390bfb1acf0e23dcd25fc8"},
+    {file = "psycopg2_binary-2.9.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2b04da24cbde33292ad34a40db9832a80ad12de26486ffeda883413c9e1b1d5e"},
+    {file = "psycopg2_binary-2.9.7-cp39-cp39-win32.whl", hash = "sha256:18f12632ab516c47c1ac4841a78fddea6508a8284c7cf0f292cb1a523f2e2379"},
+    {file = "psycopg2_binary-2.9.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb3b8d55924a6058a26db69fb1d3e7e32695ff8b491835ba9f479537e14dcf9f"},
+]
+
 [[package]]
 name = "py"
 version = "1.11.0"
@@ -1173,6 +1676,18 @@ files = [
     {file = "pycodestyle-2.11.0.tar.gz", hash = "sha256:259bcc17857d8a8b3b4a2327324b79e5f020a13c16074670f9c8c8f872ea76d0"},
 ]
 
+[[package]]
+name = "pycparser"
+version = "2.21"
+description = "C parser in Python"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+files = [
+    {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
+    {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
+]
+
 [[package]]
 name = "pydantic"
 version = "1.10.12"
@@ -1385,6 +1900,48 @@ files = [
 [package.dependencies]
 six = ">=1.5"
 
+[[package]]
+name = "python-slugify"
+version = "8.0.1"
+description = "A Python slugify application that also handles Unicode"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "python-slugify-8.0.1.tar.gz", hash = "sha256:ce0d46ddb668b3be82f4ed5e503dbc33dd815d83e2eb6824211310d3fb172a27"},
+    {file = "python_slugify-8.0.1-py2.py3-none-any.whl", hash = "sha256:70ca6ea68fe63ecc8fa4fcf00ae651fc8a5d02d93dcd12ae6d4fc7ca46c4d395"},
+]
+
+[package.dependencies]
+text-unidecode = ">=1.3"
+
+[package.extras]
+unidecode = ["Unidecode (>=1.1.1)"]
+
+[[package]]
+name = "pytimeparse"
+version = "1.1.8"
+description = "Time expression parser"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+    {file = "pytimeparse-1.1.8-py2.py3-none-any.whl", hash = "sha256:04b7be6cc8bd9f5647a6325444926c3ac34ee6bc7e69da4367ba282f076036bd"},
+    {file = "pytimeparse-1.1.8.tar.gz", hash = "sha256:e86136477be924d7e670646a98561957e8ca7308d44841e21f5ddea757556a0a"},
+]
+
+[[package]]
+name = "pytz"
+version = "2023.3.post1"
+description = "World timezone definitions, modern and historical"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+    {file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"},
+    {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"},
+]
+
 [[package]]
 name = "pyyaml"
 version = "6.0.1"
@@ -1679,6 +2236,23 @@ files = [
     {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
 ]
 
+[[package]]
+name = "sqlparse"
+version = "0.4.4"
+description = "A non-validating SQL parser."
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+files = [
+    {file = "sqlparse-0.4.4-py3-none-any.whl", hash = "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3"},
+    {file = "sqlparse-0.4.4.tar.gz", hash = "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c"},
+]
+
+[package.extras]
+dev = ["build", "flake8"]
+doc = ["sphinx"]
+test = ["pytest", "pytest-cov"]
+
 [[package]]
 name = "termcolor"
 version = "2.3.0"
@@ -1694,6 +2268,18 @@ files = [
 [package.extras]
 tests = ["pytest", "pytest-cov"]
 
+[[package]]
+name = "text-unidecode"
+version = "1.3"
+description = "The most basic Text::Unidecode port"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+    {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"},
+    {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"},
+]
+
 [[package]]
 name = "toml"
 version = "0.10.2"
@@ -1824,6 +2410,24 @@ files = [
 [package.extras]
 watchmedo = ["PyYAML (>=3.10)"]
 
+[[package]]
+name = "werkzeug"
+version = "2.3.7"
+description = "The comprehensive WSGI web application library."
+category = "dev"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "werkzeug-2.3.7-py3-none-any.whl", hash = "sha256:effc12dba7f3bd72e605ce49807bbe692bd729c3bb122a3b91747a6ae77df528"},
+    {file = "werkzeug-2.3.7.tar.gz", hash = "sha256:2b8c0e447b4b9dbcc85dd97b6eeb4dcbaf6c8b6c3be0bd654e25553e0a2157d8"},
+]
+
+[package.dependencies]
+MarkupSafe = ">=2.1.1"
+
+[package.extras]
+watchdog = ["watchdog (>=2.3)"]
+
 [[package]]
 name = "zipp"
 version = "3.16.2"
@@ -1843,4 +2447,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.9"
-content-hash = "0659ce885d2120a04911ed12b3f6b15e72fa40903ffa721cc66672c988f7101d"
+content-hash = "82222dccc8b8416318fde948f1dc8f1e9047870b864dbf8c1fde6e18021b55cf"
diff --git a/pyproject.toml b/pyproject.toml
index a27ae8f..b4f1a8f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -32,6 +32,7 @@ click = "^8.1.3"
 dbt-artifacts-parser = "^0.4"
 
 [tool.poetry.group.dev.dependencies]
+dbt-postgres = "^1.5"
 pytest = "^6.2.5"
 pytest-sugar = "^0.9.6"
 black = "^22.10.0"


Test patch:
diff --git a/dbterd/adapters/algos/test_relationship.py b/dbterd/adapters/algos/test_relationship.py
index f7ca267..92fb913 100644
--- a/dbterd/adapters/algos/test_relationship.py
+++ b/dbterd/adapters/algos/test_relationship.py
@@ -1,11 +1,12 @@
 from dbterd.adapters.algos import base
-from dbterd.adapters.algos.filter import is_selected_table
-from dbterd.adapters.algos.meta import Ref
+from dbterd.adapters.filter import is_selected_table
+from dbterd.adapters.meta import Ref
 from dbterd.constants import (
     DEFAULT_ALGO_RULE,
     TEST_META_IGNORE_IN_ERD,
     TEST_META_RELATIONSHIP_TYPE,
 )
+from dbterd.helpers.log import logger
 
 
 def parse(manifest, catalog, **kwargs):
@@ -27,9 +28,9 @@ def parse(manifest, catalog, **kwargs):
         for table in tables
         if is_selected_table(
             table=table,
-            select_rules=(kwargs.get("select") or []),
+            select_rules=kwargs.get("select") or [],
             resource_types=kwargs.get("resource_type", []),
-            exclude_rules=kwargs.get("exclude", []),
+            exclude_rules=kwargs.get("exclude") or [],
         )
     ]
 
@@ -47,6 +48,9 @@ def parse(manifest, catalog, **kwargs):
         tables=tables, relationships=relationships
     )
 
+    logger.info(
+        f"Collected {len(tables)} table(s) and {len(relationships)} relationship(s)"
+    )
     return (tables, relationships)
 
 
diff --git a/tests/unit/adapters/algos/test_test_relationship.py b/tests/unit/adapters/algos/test_test_relationship.py
index 24a7e51..1987949 100644
--- a/tests/unit/adapters/algos/test_test_relationship.py
+++ b/tests/unit/adapters/algos/test_test_relationship.py
@@ -6,7 +6,7 @@
 
 from dbterd.adapters.algos import base as base_algo
 from dbterd.adapters.algos import test_relationship as algo
-from dbterd.adapters.algos.meta import Column, Ref, Table
+from dbterd.adapters.meta import Column, Ref, Table
 
 
 @dataclass
diff --git a/tests/unit/adapters/targets/d2/test_d2_test_relationship.py b/tests/unit/adapters/targets/d2/test_d2_test_relationship.py
index 16ee9ad..0278bf1 100644
--- a/tests/unit/adapters/targets/d2/test_d2_test_relationship.py
+++ b/tests/unit/adapters/targets/d2/test_d2_test_relationship.py
@@ -2,7 +2,7 @@
 
 import pytest
 
-from dbterd.adapters.algos.meta import Column, Ref, Table
+from dbterd.adapters.meta import Column, Ref, Table
 from dbterd.adapters.targets.d2 import d2_test_relationship as engine
 
 
diff --git a/tests/unit/adapters/targets/dbml/test_dbml_test_relationship.py b/tests/unit/adapters/targets/dbml/test_dbml_test_relationship.py
index c74756c..d365444 100644
--- a/tests/unit/adapters/targets/dbml/test_dbml_test_relationship.py
+++ b/tests/unit/adapters/targets/dbml/test_dbml_test_relationship.py
@@ -2,7 +2,7 @@
 
 import pytest
 
-from dbterd.adapters.algos.meta import Column, Ref, Table
+from dbterd.adapters.meta import Column, Ref, Table
 from dbterd.adapters.targets.dbml import dbml_test_relationship as engine
 
 
diff --git a/tests/unit/adapters/targets/graphviz/test_graphviz_test_relationship.py b/tests/unit/adapters/targets/graphviz/test_graphviz_test_relationship.py
index 6991a5b..0f86050 100644
--- a/tests/unit/adapters/targets/graphviz/test_graphviz_test_relationship.py
+++ b/tests/unit/adapters/targets/graphviz/test_graphviz_test_relationship.py
@@ -2,7 +2,7 @@
 
 import pytest
 
-from dbterd.adapters.algos.meta import Column, Ref, Table
+from dbterd.adapters.meta import Column, Ref, Table
 from dbterd.adapters.targets.graphviz import graphviz_test_relationship as engine
 
 
diff --git a/tests/unit/adapters/targets/mermaid/test_mermaid_test_relationship.py b/tests/unit/adapters/targets/mermaid/test_mermaid_test_relationship.py
index b130d2f..e178842 100644
--- a/tests/unit/adapters/targets/mermaid/test_mermaid_test_relationship.py
+++ b/tests/unit/adapters/targets/mermaid/test_mermaid_test_relationship.py
@@ -2,7 +2,7 @@
 
 import pytest
 
-from dbterd.adapters.algos.meta import Column, Ref, Table
+from dbterd.adapters.meta import Column, Ref, Table
 from dbterd.adapters.targets.mermaid import mermaid_test_relationship as engine
 
 
diff --git a/tests/unit/adapters/targets/plantuml/test_plantuml_test_relationship.py b/tests/unit/adapters/targets/plantuml/test_plantuml_test_relationship.py
index 68b3f7d..5d39823 100644
--- a/tests/unit/adapters/targets/plantuml/test_plantuml_test_relationship.py
+++ b/tests/unit/adapters/targets/plantuml/test_plantuml_test_relationship.py
@@ -2,7 +2,7 @@
 
 import pytest
 
-from dbterd.adapters.algos.meta import Column, Ref, Table
+from dbterd.adapters.meta import Column, Ref, Table
 from dbterd.adapters.targets.plantuml import plantuml_test_relationship as engine
 
 
diff --git a/tests/unit/adapters/test_factory.py b/tests/unit/adapters/test_adapter.py
similarity index 57%
rename from tests/unit/adapters/test_factory.py
rename to tests/unit/adapters/test_adapter.py
index 9f81108..ea4a9f8 100644
--- a/tests/unit/adapters/test_factory.py
+++ b/tests/unit/adapters/test_adapter.py
@@ -1,13 +1,13 @@
 import pytest
 
-from dbterd.adapters import factory
+from dbterd.adapters import adapter
 from dbterd.adapters.targets import dbml
 
 
 class TestFactory:
     def test_load_executor_failed(self):
         with pytest.raises(Exception):
-            factory.load_executor(name="dummy")
+            adapter.load_executor(name="dummy")
 
     def test_load_executor(self):
-        assert factory.load_executor(name="dbml") == dbml
+        assert adapter.load_executor(name="dbml") == dbml
diff --git a/tests/unit/adapters/test_base.py b/tests/unit/adapters/test_base.py
index 5f6404e..6b96051 100644
--- a/tests/unit/adapters/test_base.py
+++ b/tests/unit/adapters/test_base.py
@@ -2,8 +2,10 @@
 from unittest import mock
 
 import click
+import pytest
 
-from dbterd.adapters.worker import DbtWorker
+from dbterd import default
+from dbterd.adapters.base import Executor
 
 
 class TestBase:
@@ -15,12 +17,12 @@ class TestBase:
     # )
 
     def test_worker(self):
-        worker = DbtWorker(ctx=click.Context(command=click.BaseCommand("dummy")))
+        worker = Executor(ctx=click.Context(command=click.BaseCommand("dummy")))
         assert worker.filename_manifest == "manifest.json"
         assert worker.filename_catalog == "catalog.json"
 
     def test___read_manifest(self):
-        worker = DbtWorker(ctx=click.Context(command=click.BaseCommand("dummy")))
+        worker = Executor(ctx=click.Context(command=click.BaseCommand("dummy")))
         with mock.patch(
             "dbterd.helpers.file.read_manifest", return_value=dict({})
         ) as mock_read_manifest:
@@ -32,7 +34,7 @@ def test___read_manifest(self):
         mock_read_manifest.assert_called_once_with(path=Path.cwd(), version=None)
 
     def test___read_catalog(self):
-        worker = DbtWorker(ctx=click.Context(command=click.BaseCommand("dummy")))
+        worker = Executor(ctx=click.Context(command=click.BaseCommand("dummy")))
         with mock.patch(
             "dbterd.helpers.file.read_catalog", return_value=dict({})
         ) as mock_read_catalog:
@@ -42,3 +44,88 @@ def test___read_catalog(self):
                 assert worker._Executor__read_catalog(cp=Path.cwd()) == dict({})
         mock_check_existence.assert_called_once_with(Path.cwd(), "catalog.json")
         mock_read_catalog.assert_called_once_with(path=Path.cwd(), version=None)
+
+    @mock.patch(
+        "dbterd.adapters.base.DbtInvocation.get_selection", return_value="dummy"
+    )
+    def test__get_selection(self, mock_dbt_invocation):
+        worker = Executor(ctx=click.Context(command=click.BaseCommand("dummy")))
+        assert "dummy" == worker._Executor__get_selection(
+            select_rules=[], exclude_rules=[]
+        )
+        mock_dbt_invocation.assert_called_once()
+
+    @pytest.mark.parametrize(
+        "kwargs, expected",
+        [
+            (
+                dict(
+                    select=[],
+                    exclude=[],
+                ),
+                dict(
+                    artifacts_dir="/path/ad",
+                    dbt_project_dir="/path/dpd",
+                    select=[],
+                    exclude=[],
+                ),
+            ),
+            (
+                dict(select=[], exclude=[], dbt=True),
+                dict(
+                    dbt=True,
+                    artifacts_dir="/path/ad",
+                    dbt_project_dir="/path/dpd",
+                    select=["yolo"],
+                    exclude=[],
+                ),
+            ),
+        ],
+    )
+    @mock.patch("dbterd.adapters.base.Executor._Executor__get_dir")
+    @mock.patch("dbterd.adapters.base.Executor._Executor__get_selection")
+    def test_evaluate_kwargs(self, mock_get_selection, mock_get_dir, kwargs, expected):
+        worker = Executor(ctx=click.Context(command=click.BaseCommand("dummy")))
+        mock_get_dir.return_value = ("/path/ad", "/path/dpd")
+        mock_get_selection.return_value = ["yolo"]
+        assert expected == worker.evaluate_kwargs(**kwargs)
+        mock_get_dir.assert_called_once()
+
+    @pytest.mark.parametrize(
+        "kwargs, mock_isfile_se, expected",
+        [
+            (
+                dict(artifacts_dir=Path.cwd(), dbt_project_dir=Path.cwd()),
+                [False],
+                (str(f"{Path.cwd()}/target"), str(Path.cwd())),
+            ),
+            (
+                dict(artifacts_dir=Path.cwd(), dbt_project_dir=Path.cwd()),
+                [True],
+                (str(Path.cwd()), str(Path.cwd())),
+            ),
+            (
+                dict(artifacts_dir=None, dbt_project_dir=Path.cwd()),
+                [True],
+                (str(Path.cwd()), str(Path.cwd())),
+            ),
+            (
+                dict(artifacts_dir=Path.cwd(), dbt_project_dir=None),
+                [True],
+                (str(Path.cwd()), str(Path.cwd())),
+            ),
+            (
+                dict(artifacts_dir="", dbt_project_dir=""),
+                [True],
+                (
+                    str(default.default_artifact_path()),
+                    str(Path(default.default_artifact_path()).parent.absolute()),
+                ),
+            ),
+        ],
+    )
+    @mock.patch("os.path.isfile")
+    def test__get_dir(self, mock_isfile, kwargs, mock_isfile_se, expected):
+        worker = Executor(ctx=click.Context(command=click.BaseCommand("dummy")))
+        mock_isfile.side_effect = mock_isfile_se
+        assert worker._Executor__get_dir(**kwargs) == expected
diff --git a/tests/unit/adapters/test_dbt_invocation.py b/tests/unit/adapters/test_dbt_invocation.py
new file mode 100644
index 0000000..5fdf354
--- /dev/null
+++ b/tests/unit/adapters/test_dbt_invocation.py
@@ -0,0 +1,65 @@
+from importlib.machinery import ModuleSpec
+from unittest import mock
+
+import click
+import pytest
+from dbt.cli.main import dbtRunnerResult
+
+from dbterd.adapters.dbt_invocation import DbtInvocation
+
+
+class TestDbtInvocation:
+    @mock.patch("importlib.util.find_spec")
+    def test__ensure_dbt_installed__no_dbt_installed(self, mock_find_spec):
+        mock_find_spec.return_value = ModuleSpec(name="dbt", loader=None)
+        with pytest.raises(click.UsageError):
+            DbtInvocation()._DbtInvocation__ensure_dbt_installed()
+
+    @pytest.mark.parametrize(
+        "select_rules, exclude_rules, mock_dbtRunner_invoke_rv, expected",
+        [
+            ([], [], dbtRunnerResult(success=True, result=[]), []),
+            (
+                [],
+                [],
+                dbtRunnerResult(success=True, result=["p1.p2.p3.p4"]),
+                ["exact:model.p1.p4"],
+            ),
+            (
+                ["dummy"],
+                ["dummy"],
+                dbtRunnerResult(success=True, result=["p1.p2.p3.p4"]),
+                ["exact:model.p1.p4"],
+            ),
+        ],
+    )
+    @mock.patch("dbterd.adapters.dbt_invocation.dbtRunner.invoke")
+    def test_get_selection(
+        self,
+        mock_dbtRunner_invoke,
+        select_rules,
+        exclude_rules,
+        mock_dbtRunner_invoke_rv,
+        expected,
+    ):
+        mock_dbtRunner_invoke.return_value = mock_dbtRunner_invoke_rv
+        invoker = DbtInvocation()
+        actual = invoker.get_selection(
+            select_rules=select_rules, exclude_rules=exclude_rules
+        )
+        assert actual == expected
+
+        args = ["ls", "--project-dir", invoker.project_dir, "--resource-type", "model"]
+        if select_rules:
+            args.extend(["--select", " ".join(select_rules)])
+        if exclude_rules:
+            args.extend(["--exclude", " ".join(exclude_rules)])
+        mock_dbtRunner_invoke.assert_called_once_with(args)
+
+    @mock.patch("dbterd.adapters.dbt_invocation.dbtRunner.invoke")
+    def test_get_selection__failed(self, mock_dbtRunner_invoke):
+        mock_dbtRunner_invoke.return_value = dbtRunnerResult(success=False)
+        with pytest.raises(click.UsageError):
+            DbtInvocation(dbt_target="dummy").get_selection(
+                select_rules=[], exclude_rules=[]
+            )
diff --git a/tests/unit/adapters/test_filter.py b/tests/unit/adapters/test_filter.py
new file mode 100644
index 0000000..3950640
--- /dev/null
+++ b/tests/unit/adapters/test_filter.py
@@ -0,0 +1,51 @@
+import pytest
+
+from dbterd.adapters import filter
+from dbterd.adapters.meta import Table
+
+
+class TestFilter:
+    @pytest.mark.parametrize(
+        "rule, expected",
+        [
+            ([], (False, None)),
+            (["dummy_name"], (False, None)),
+            (["schema:dummy_name"], (False, None)),
+            (["exact:dummy_name"], (False, None)),
+            (["exposure:dummy_name"], (False, None)),
+            (["dummy:dummy_name"], (True, "dummy")),
+        ],
+    )
+    def test_has_unsupported_rule(self, rule, expected):
+        actual = filter.has_unsupported_rule(rules=rule)
+        assert actual == expected
+
+    @pytest.mark.parametrize(
+        "table, rule, expected",
+        [
+            (
+                Table(
+                    name="model.dummy.table1", database="dummydb", schema="dummyschema"
+                ),
+                "",
+                True,
+            ),
+            (
+                Table(
+                    name="model.dummy.table1", database="dummydb", schema="dummyschema"
+                ),
+                "table1",
+                False,
+            ),
+            (
+                Table(
+                    name="model.dummy.table1", database="dummydb", schema="dummyschema"
+                ),
+                "model.dummy.table1",
+                True,
+            ),
+        ],
+    )
+    def test_is_satisfied_by_exact(self, table, rule, expected):
+        actual = filter.is_satisfied_by_exact(table=table, rule=rule)
+        assert actual == expected
diff --git a/tests/unit/cli/test_runner.py b/tests/unit/cli/test_runner.py
index acc2348..100c96a 100644
--- a/tests/unit/cli/test_runner.py
+++ b/tests/unit/cli/test_runner.py
@@ -100,3 +100,37 @@ def test_invoke_run_ok(self, target, output, dbterd: dbterdRunner) -> None:
                             ["run", "--target", target, "--output", "/custom/path"]
                         )
                         mock_open_w.assert_called_with(f"/custom/path/{output}", "w")
+
+    def test_command_invalid_selection_rule(self, dbterd: dbterdRunner) -> None:
+        with pytest.raises(Exception):
+            dbterd.invoke(["run", "--select", "notexist:dummy"])
+
+    @pytest.mark.parametrize(
+        "target, output",
+        [
+            ("dbml", "output.dbml"),
+        ],
+    )
+    def test_invoke_run_failed_to_write_output(
+        self, target, output, dbterd: dbterdRunner
+    ) -> None:
+        with mock.patch(
+            "dbterd.adapters.base.Executor._Executor__read_manifest", return_value=None
+        ) as mock_read_m:
+            with mock.patch(
+                "dbterd.adapters.base.Executor._Executor__read_catalog",
+                return_value=None,
+            ) as mock_read_c:
+                with mock.patch(
+                    f"dbterd.adapters.targets.{target}.{target}_test_relationship.parse",
+                    return_value="--irrelevant--",
+                ) as mock_engine_parse:
+                    with mock.patch(
+                        "builtins.open", return_value=PermissionError()
+                    ) as mock_open_w:
+                        with pytest.raises(click.FileError):
+                            dbterd.invoke(["run", "--target", target])
+                        mock_read_m.assert_called_once()
+                        mock_read_c.assert_called_once()
+                        mock_engine_parse.assert_called_once()
+                        mock_open_w.assert_called_once()