Repository: datnguye/dbterd (language: python)

Issue:
Allow multiple sections using -s and -ns flags, or regex pattern
**Is your feature request related to a problem? Please describe.**
Sometimes the naming convention of the model alone is not enough to use a single -s or -ns to get desired selection.

**Describe the solution you'd like**
Allow multiple sections using -s and -ns flags, or regex pattern

**Describe alternatives you've considered**
Generate all models then manually edit. 


Discussion / hints:

Fix patch:
diff --git a/SECURITY.md b/SECURITY.md
index 7948e7f..67c7427 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -7,6 +7,7 @@ currently being supported with security updates.
 
 | Version | Supported          |
 | ------- | ------------------ |
+| 1.1.x   | :white_check_mark: |
 | 1.0.x   | :white_check_mark: |
 | < 1.0   | :white_check_mark: |
 
diff --git a/dbterd/adapters/algos/base.py b/dbterd/adapters/algos/base.py
new file mode 100644
index 0000000..1ace8c9
--- /dev/null
+++ b/dbterd/adapters/algos/base.py
@@ -0,0 +1,142 @@
+import copy
+
+from dbterd.adapters.algos.meta import Column, Table
+
+
+def get_tables(manifest, catalog):
+    """Extract tables from dbt artifacts
+
+    Args:
+        manifest (dict): dbt manifest json
+        catalog (dict): dbt catalog json
+
+    Returns:
+        List[Table]: All tables parsed from dbt artifacts
+    """
+    tables = []
+
+    if hasattr(manifest, "nodes"):
+        for table_name, node in manifest.nodes.items():
+            if (
+                table_name.startswith("model.")
+                or table_name.startswith("seed.")
+                or table_name.startswith("snapshot.")
+            ):
+                catalog_node = catalog.nodes.get(table_name)
+                table = get_table(
+                    table_name=table_name, manifest_node=node, catalog_node=catalog_node
+                )
+                tables.append(table)
+
+    if hasattr(manifest, "sources"):
+        for table_name, source in manifest.sources.items():
+            if table_name.startswith("source"):
+                catalog_source = catalog.sources.get(table_name)
+                table = get_table(
+                    table_name=table_name,
+                    manifest_node=source,
+                    catalog_node=catalog_source,
+                )
+                tables.append(table)
+
+    return tables
+
+
+def enrich_tables_from_relationships(tables, relationships):
+    """Fullfill columns in Table due to `select *`
+
+    Args:
+        tables (List[Table]): List of Tables
+        relationships (List[Ref]): List of Relationships between Tables
+
+    Returns:
+        List[Table]: Enriched tables
+    """
+    copied_tables = copy.deepcopy(tables)
+    for relationship in relationships:
+        for table in copied_tables:
+            table_columns = [x.name.lower() for x in table.columns]
+            if (
+                table.name == relationship.table_map[0]
+                and relationship.column_map[0].lower() not in table_columns
+            ):
+                table.columns.append(Column(name=relationship.column_map[0]))
+            if (
+                table.name == relationship.table_map[1]
+                and relationship.column_map[1].lower() not in table_columns
+            ):
+                table.columns.append(Column(name=relationship.column_map[1]))
+    return copied_tables
+
+
+def get_table(table_name, manifest_node, catalog_node=None):
+    """Construct a single Table object
+
+    Args:
+        table_name (str): Table name
+        manifest_node (dict): Manifest node
+        catalog_node (dict, optional): Catalog node. Defaults to None.
+
+    Returns:
+        Table: Parsed table
+    """
+    table = Table(
+        name=table_name,
+        raw_sql=get_compiled_sql(manifest_node),
+        database=manifest_node.database.lower(),
+        schema=manifest_node.schema_.lower(),
+        columns=[],
+        resource_type=table_name.split(".")[0],
+    )
+
+    if catalog_node:
+        for column, metadata in catalog_node.columns.items():
+            table.columns.append(
+                Column(
+                    name=str(column).lower(),
+                    data_type=str(metadata.type).lower(),
+                )
+            )
+
+    for column_name, column_metadata in manifest_node.columns.items():
+        column_name = column_name.strip('"')
+        if not any(c.name.lower() == column_name.lower() for c in table.columns):
+            table.columns.append(
+                Column(
+                    name=column_name.lower(),
+                    data_type=str(column_metadata.data_type or "unknown").lower(),
+                )
+            )
+
+    if not table.columns:
+        table.columns.append(Column())
+
+    return table
+
+
+def get_compiled_sql(manifest_node):
+    """Retrieve compiled SQL from manifest node
+
+    Args:
+        manifest_node (dict): Manifest node
+
+    Returns:
+        str: Compiled SQL
+    """
+    if hasattr(manifest_node, "compiled_sql"):  # up to v6
+        return manifest_node.compiled_sql
+
+    if hasattr(manifest_node, "compiled_code"):  # from v7
+        return manifest_node.compiled_code
+
+    if hasattr(
+        manifest_node, "columns"
+    ):  # nodes having no compiled but just list of columns
+        return """select
+            {columns}
+        from {table}""".format(
+            columns=",\n".join([f"{x}" for x in manifest_node.columns]),
+            table=f"{manifest_node.database}.{manifest_node.schema}.undefined",
+        )
+
+    return manifest_node.raw_sql  # fallback to raw dbt code
diff --git a/dbterd/adapters/algos/filter.py b/dbterd/adapters/algos/filter.py
new file mode 100644
index 0000000..1968824
--- /dev/null
+++ b/dbterd/adapters/algos/filter.py
@@ -0,0 +1,75 @@
+import sys
+from fnmatch import fnmatch
+from typing import List
+
+from dbterd.adapters.algos.meta import Table
+
+
+def is_selected_table(
+    table: Table,
+    select_rules: List[str] = [],
+    exclude_rules: List[str] = [],
+    resource_types: List[str] = ["model"],
+):
+    """Check if Table is selected with defined selection criteria
+
+    Args:
+        table (Table): Table object
+        select_rules (List[str]): Selection rules. Defaults to [].
+        exclude_rules (List[str], optional): Exclusion rules. Defaults to [].
+        resource_types (List[str], optional): Selected resource types. Defaults to [].
+
+    Returns:
+        bool: True if Table is selected. False if Tables is excluded
+    """
+    # Selection
+    selected = True
+    if select_rules:
+        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:
+        excluded = any(
+            [evaluate_rule(table=table, rule=rule) for rule in exclude_rules]
+        )
+
+    return selected and not excluded
+
+
+def evaluate_rule(table: Table, rule: str):
+    and_parts = rule.split(",")
+    results = []
+    for x in and_parts:
+        rule_parts = x.lower().split(":")
+        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}")
+        results.append(selected_func(table=table, rule=rule))
+    return all(results)
+
+
+def __is_satisfied_by_name(table: Table, rule: str = ""):
+    if not rule:
+        return True
+    return table.name.startswith(rule)
+
+
+def __is_satisfied_by_schema(table: Table, rule: str = ""):
+    if not rule:
+        return True
+
+    parts = rule.split(".")
+    selected_schema = parts[-1]
+    selected_database = parts[0] if len(parts) > 1 else table.database
+    return f"{table.database}.{table.schema}".startswith(
+        f"{selected_database}.{selected_schema}"
+    )
+
+
+def __is_satisfied_by_wildcard(table: Table, rule: str = "*"):
+    if not rule:
+        return True
+    return fnmatch(table.name, rule)
diff --git a/dbterd/adapters/algos/meta.py b/dbterd/adapters/algos/meta.py
index 4960c26..1940efd 100644
--- a/dbterd/adapters/algos/meta.py
+++ b/dbterd/adapters/algos/meta.py
@@ -4,9 +4,7 @@
 
 @dataclass
 class Column:
-    """
-    Sample DBML: id varchar [primary key]
-    """
+    """Parsed Column object"""
 
     name: str = "unknown"
     data_type: str = "unknown"
@@ -14,12 +12,7 @@ class Column:
 
 @dataclass
 class Table:
-    """
-    Sample DBML:
-    Table posts {
-        id varchar [primary key]
-    }
-    """
+    """Parsed Table object"""
 
     name: str
     database: str
@@ -31,7 +24,7 @@ class Table:
 
 @dataclass
 class Ref:
-    """Sample DBML: Ref: posts.user_id > users.id"""
+    """Parsed Relationship object"""
 
     name: str
     table_map: Tuple[str, str]
diff --git a/dbterd/adapters/base.py b/dbterd/adapters/base.py
index d482332..a9141ef 100644
--- a/dbterd/adapters/base.py
+++ b/dbterd/adapters/base.py
@@ -1,14 +1,16 @@
 import abc
-from asyncio.log import logger
 
 import click
 
 from dbterd.adapters import factory
 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"""
+
     ctx: click.Context
 
     def __init__(self, ctx) -> None:
@@ -19,20 +21,39 @@ def __init__(self, ctx) -> None:
 
     @abc.abstractmethod
     def run(self, **kwargs):
+        """Main function helps to run by the target strategy"""
         self.__run_by_strategy(**kwargs)
 
     def __read_manifest(self, mp: str, mv: int = None):
+        """Read the Manifest content
+
+        Args:
+            mp (str): manifest.json json file path
+            mv (int, optional): Manifest version. Defaults to None.
+
+        Returns:
+            dict: Manifest dict
+        """
         cli_messaging.check_existence(mp, self.filename_manifest)
         conditional = f" or provided version {mv} is incorrect" if mv else ""
         with cli_messaging.handle_read_errors(self.filename_manifest, conditional):
             return file_handlers.read_manifest(mp, mv)
 
     def __read_catalog(self, cp: str):
+        """Read the Catalog content
+
+        Args:
+            cp (str): catalog.json file path
+
+        Returns:
+            dict: Catalog dict
+        """
         cli_messaging.check_existence(cp, self.filename_catalog)
         with cli_messaging.handle_read_errors(self.filename_catalog):
             return file_handlers.read_catalog(cp)
 
     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}
         run_operation_dispatcher = getattr(target, "run_operation_dispatcher")
         operation_default = getattr(target, "run_operation_default")
diff --git a/dbterd/adapters/factory.py b/dbterd/adapters/factory.py
index ac27d63..f2a7fde 100644
--- a/dbterd/adapters/factory.py
+++ b/dbterd/adapters/factory.py
@@ -2,6 +2,17 @@
 
 
 def load_executor(name: str):
+    """Import the target extension dynamically
+
+    Args:
+        name (str): Target name e.g. dbml, mermaid
+
+    Raises:
+        Exception: Target not found
+
+    Returns:
+        ModuleType: Imported module
+    """
     try:
         return import_module(name=f".{name}", package="dbterd.adapters.targets")
     except ModuleNotFoundError as exc:
diff --git a/dbterd/adapters/targets/default.py b/dbterd/adapters/targets/default.py
index 2ffa292..2643235 100644
--- a/dbterd/adapters/targets/default.py
+++ b/dbterd/adapters/targets/default.py
@@ -1,2 +1,7 @@
 def default(**kwargs):
+    """Fallback option if the target is not supported yet
+
+    Returns:
+        NotImplemented: NotImplemented
+    """
     return NotImplemented
diff --git a/dbterd/adapters/worker.py b/dbterd/adapters/worker.py
index 038e800..91589a3 100644
--- a/dbterd/adapters/worker.py
+++ b/dbterd/adapters/worker.py
@@ -2,6 +2,8 @@
 
 
 class DbtWorker(Executor):
+    """dbt executor"""
+
     def __init__(self, ctx) -> None:
         super().__init__(ctx)
 
diff --git a/dbterd/cli/main.py b/dbterd/cli/main.py
index 7327f14..9285148 100644
--- a/dbterd/cli/main.py
+++ b/dbterd/cli/main.py
@@ -13,10 +13,21 @@
 
 # Programmatic invocation
 class dbterdRunner:
+    """Support runner for the programatic call"""
+
     def __init__(self) -> None:
         pass
 
     def invoke(self, args: List[str]):
+        """Invoke a command of dbterd programatically
+
+        Args:
+            args (List[str]): dbterd arguments
+
+        Raises:
+            Exception: Unhandled exception
+            Exception: Not Supported command exception
+        """
         try:
             dbt_ctx = dbterd.make_context(dbterd.name, args)
             return dbterd.invoke(dbt_ctx)
diff --git a/dbterd/cli/params.py b/dbterd/cli/params.py
index 3078f8b..d31242a 100644
--- a/dbterd/cli/params.py
+++ b/dbterd/cli/params.py
@@ -14,13 +14,6 @@ def common_params(func):
         show_default=True,
         type=click.STRING,
     )
-    @click.option(
-        "--manifest-path",
-        "-mp",
-        help="__DEPRECATED_WARNING__: Specified the full directory path to dbt manifest.json file. Use --artifacts-dir instead.",
-        default=None,
-        type=click.STRING,
-    )
     @click.option(
         "--output",
         "-o",
@@ -32,15 +25,17 @@ def common_params(func):
     @click.option(
         "--select",
         "-s",
-        help="Selecttion criteria. Support 'starts with' a string",
-        default=None,
+        help="Selecttion criteria",
+        default=[],
+        multiple=True,
         type=click.STRING,
     )
     @click.option(
         "--exclude",
         "-ns",
-        help="Exclusion criteria. Support 'not starts with' a string",
-        default=None,
+        help="Exclusion criteria",
+        default=[],
+        multiple=True,
         type=click.STRING,
     )
     @click.option(
diff --git a/dbterd/helpers/file.py b/dbterd/helpers/file.py
index cd8c373..5e2fcf8 100644
--- a/dbterd/helpers/file.py
+++ b/dbterd/helpers/file.py
@@ -33,9 +33,14 @@ def open_json(fp):
 
 
 def convert_path(path: str) -> str:
-    """Convert a path which might be >260 characters long, to one
-    that will be writable/readable on Windows.
+    """Convert a path which might be >260 characters long, to one that will be writable/readable on Windows.
     On other platforms, this is a no-op.
+
+    Args:
+        path (str): Path string
+
+    Returns:
+        str: Converted path string
     """
     # some parts of python seem to append '\*.*' to strings, better safe than
     # sorry.
@@ -80,9 +85,7 @@ def supports_long_paths(windll_name="ntdll") -> bool:  # pragma: no cover
 
 
 def win_prepare_path(path: str) -> str:  # pragma: no cover
-    """Given a windows path, prepare it for use by making sure it is absolute
-    and normalized.
-    """
+    """Given a windows path, prepare it for use by making sure it is absolute and normalized."""
     path = os.path.normpath(path)
 
     # if a path starts with '\', splitdrive() on it will return '' for the
@@ -105,7 +108,15 @@ def win_prepare_path(path: str) -> str:  # pragma: no cover
 
 
 def read_manifest(path: str, version: int = None):
-    """Reads in the manifest.json file, with optional version specification"""
+    """Reads in the manifest.json file, with optional version specification
+
+    Args:
+        path (str): manifest.json file path
+        version (int, optional): Manifest version. Defaults to None.
+
+    Returns:
+        dict: Manifest dict
+    """
     _dict = open_json(f"{path}/manifest.json")
     parser_version = f"parse_manifest_v{version}" if version else "parse_manifest"
     parse_func = getattr(parser, parser_version)
@@ -113,7 +124,15 @@ def read_manifest(path: str, version: int = None):
 
 
 def read_catalog(path: str, version: int = None):
-    """Reads in the catalog.json file, with optional version specification"""
+    """Reads in the catalog.json file, with optional version specification
+
+    Args:
+        path (str): catalog.json file path
+        version (int, optional): Catalog version. Defaults to None.
+
+    Returns:
+        dict: Catalog dict
+    """
     _dict = open_json(f"{path}/catalog.json")
     parser_version = f"parse_catalog_v{version}" if version else "parse_catalog"
     parse_func = getattr(parser, parser_version)
diff --git a/dbterd/main.py b/dbterd/main.py
index 254b1ed..ac13eb8 100644
--- a/dbterd/main.py
+++ b/dbterd/main.py
@@ -2,5 +2,5 @@
 
 
 def main():
-    # executing entry point
+    """dbterd entrypoint"""
     cli.dbterd()
diff --git a/docs/nav/guide/cli-references.md b/docs/nav/guide/cli-references.md
index 3f9f331..bcda3aa 100644
--- a/docs/nav/guide/cli-references.md
+++ b/docs/nav/guide/cli-references.md
@@ -32,7 +32,7 @@ Supports:
 
     ```bash
     dbt docs generate
-    dbterd run [-t dbml or mermaid]
+    dbterd run [-t dbml or -t mermaid]
     ```
 
 === "--help (-h)"
@@ -42,29 +42,24 @@ Supports:
     Run the convert
 
     Options:
-    -ad, --artifacts-dir TEXT     Specified the full path to dbt artifacts path
-                                    which known as /target directory  [default:
-                                    C:\Users\DAT\Documents\Sources\dbterd\target]
-    -mp, --manifest-path TEXT     __DEPRECATED_WARNING__: Specified the full
-                                    directory path to dbt manifest.json file. Use
-                                    --artifacts-dir instead.
-    -o, --output TEXT             Output the result file. Default to the same
-                                    target dir  [default:
-                                    C:\Users\DAT\Documents\Sources\dbterd\target]
-    -s, --select TEXT             Selecttion criteria. Support 'starts with' a
-                                    string
-    -ns, --exclude TEXT           Exclusion criteria. Support 'not starts with'
-                                    a string
-    -t, --target TEXT             Target to the diagram-as-code platform
-                                    [default: dbml]
-    -a, --algo TEXT               Specified algorithm in the way to detect
-                                    diagram connectors  [default:
-                                    test_relationship]
-    -mv, --manifest-version TEXT  Specified dbt manifest.json version
-    -rt, --resource-type TEXT     Specified dbt resource type(seed, model,
-                                    source, snapshot),default:model, use examples,
-                                    -rt model -rt source
-    -h, --help                    Show this message and exit.
+        -ad, --artifacts-dir TEXT     Specified the full path to dbt artifacts path
+                                        which known as /target directory  [default:
+                                        C:\Users\DAT\Documents\Sources\dbterd\target]
+        -o, --output TEXT             Output the result file. Default to the same
+                                        target dir  [default:
+                                        C:\Users\DAT\Documents\Sources\dbterd\target]
+        -s, --select TEXT             Selecttion criteria
+        -ns, --exclude TEXT           Exclusion criteria
+        -t, --target TEXT             Target to the diagram-as-code platform
+                                        [default: dbml]
+        -a, --algo TEXT               Specified algorithm in the way to detect
+                                        diagram connectors  [default:
+                                        test_relationship]
+        -mv, --manifest-version TEXT  Specified dbt manifest.json version
+        -rt, --resource-type TEXT     Specified dbt resource type(seed, model,
+                                        source, snapshot),default:model, use examples,
+                                        -rt model -rt source
+        -h, --help                    Show this message and exit.
     ```
 
 ### --artifacts-dir (-ad)
@@ -84,18 +79,6 @@ Configure the path to directory containing dbt artifact files.
     dbterd run --artifacts-dir "./target"
     ```
 
-**Examples:**
-=== "CLI"
-
-    ```bash
-    dbterd run -mp "./target"
-    ```
-=== "CLI (long style)"
-
-    ```bash
-    dbterd run --manifest-path "./target"
-    ```
-
 ### --output (-o)
 
 Configure the path to directory containing the output diagram file.
@@ -115,49 +98,75 @@ Configure the path to directory containing the output diagram file.
 
 ### --select (-s)
 
-Selection criteria. Support 'starts with' a string where string is:
-  - table name (or part of it)
-  - schema name (or part of it) formed as `schema:<your_schema_name>`
-> Select all dbt models if not specified
+Selection criteria.
+> Select all dbt models if not specified, supports mulitple options
+
+Rules:
+- By `name`
+    - model name starts with input string
+- By `schema`
+    - schema name starts with an input string, formed as `schema:<your_schema_name>`
+- By `wildcard`
+    - model name matches to a [wildcard pattern](https://docs.python.org/3/library/fnmatch.html), formed as `wildcard:<your_wildcard>`
 
 **Examples:**
-=== "CLI (by table)"
+=== "CLI (by name)"
 
     ```bash
-    dbterd run -s "model.package_name.model_name"
+    dbterd run -s "model.package_name.model_partital_name"
     ```
 === "CLI (by schema)"
 
     ```bash
     dbterd run -s "schema:my_schema_name"
     ```
-=== "CLI (long style)"
+=== "CLI (by wildcard)"
 
     ```bash
     dbterd run --select "<your-criteria>"
     ```
 
+#### `AND` and `OR` logic
+- `AND` logic is applied to a single selection splitted by comma (,)
+- `OR` logic is applied to 2+ selection
+
+**Examples:**
+=== "AND"
+
+    ```bash
+    # All models belong to 'abc' schema AND also need to match the pattern of '*xyz.*'
+    dbterd run -s schema:abc,wildcard:*xyz.*
+    ```
+=== "OR"
+
+    ```bash
+    # All models belong to 'abc' schema, OR match to the pattern of '*xyz.*'
+    dbterd run -s schema:abc -s wildcard:*xyz.*
+    ```
+
+
 ### --exclude (-ns)
 
-Exclusion criteria. Support 'not starts with' a string
-> Default to `None`
+Exclusion criteria. Rules are the same as Selection Criteria.
+> Do not exclude any dbt models if not specified, supports mulitple options
+
 
 **Examples:**
 === "CLI"
 
     ```bash
-    dbterd run --exclude 'model.package_name.table'
+    dbterd run -ns 'model.package_name.table'
     ```
 === "CLI (long style)"
 
     ```bash
-    dbterd run -ns 'model.package_name.table'
+    dbterd run --exclude 'model.package_name.table'
     ```
 
 ### --target (-t)
 
 Target to the diagram-as-code platform
-> Default to `dbml`, currently only supported option
+> Default to `dbml`
 
 **Examples:**
 === "CLI"
@@ -224,7 +233,7 @@ Specified dbt resource type(seed, model, source, snapshot).
 
 ### ⚠ DEPRECATED WARNING
 
-#### --manifest-path (-mp) (Deprecated in v1.?)
+#### --manifest-path (-mp) (Deprecated in v1.1)
 
 Configure the path to directory containing dbt `manifest.json` file.
 > Default to `./target`
diff --git a/mkdocs.yml b/mkdocs.yml
index a2a2dfc..bcdd5c1 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -9,6 +9,18 @@ edit_uri: edit/main/docs/
 
 copyright: Copyright &copy; 2023 Dat Nguyen
 
+nav:
+  - dbterd:
+    - index.md
+    - User Guide:
+      - CLI Reference: nav/guide/cli-references.md
+      - Generate markdown with mermaid ERD inclusive: nav/guide/generate-markdown-mermaid-erd.md
+    - Development:
+      - nav/development/contributing-guide.md
+      - Change Log: nav/development/CHANGELOG.md
+    - About:
+      - license.md
+
 theme:
   name: material
   features:
@@ -64,18 +76,6 @@ plugins:
       minify_html: true
   - offline
 
-nav:
-    - dbterd:
-      - index.md
-      - User Guide:
-        - CLI Reference: nav/guide/cli-references.md
-        - Generate mardown with mermaid ERD inclusive: nav/guide/generate-markdown-mermaid-erd.md
-      - Development:
-        - nav/development/contributing-guide.md
-        - Change Log: nav/development/CHANGELOG.md
-      - About:
-        - license.md
-
 extra:
   annotate:
     json: [.s2]
diff --git a/poetry.lock b/poetry.lock
index 55833cc..9bd0fb9 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -28,33 +28,33 @@ files = [
 
 [[package]]
 name = "attrs"
-version = "22.2.0"
+version = "23.1.0"
 description = "Classes Without Boilerplate"
 category = "main"
 optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
 files = [
-    {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"},
-    {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"},
+    {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"},
+    {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"},
 ]
 
 [package.extras]
-cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"]
-dev = ["attrs[docs,tests]"]
-docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"]
-tests = ["attrs[tests-no-zope]", "zope.interface"]
-tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"]
+cov = ["attrs[tests]", "coverage[toml] (>=5.3)"]
+dev = ["attrs[docs,tests]", "pre-commit"]
+docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"]
+tests = ["attrs[tests-no-zope]", "zope-interface"]
+tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
 
 [[package]]
 name = "autoflake"
-version = "2.0.2"
+version = "2.1.1"
 description = "Removes unused imports and unused variables"
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "autoflake-2.0.2-py3-none-any.whl", hash = "sha256:a82d8efdcbbb7129a8a23238c529fb9d9919c562e26bb7963ea6890fbfff7d02"},
-    {file = "autoflake-2.0.2.tar.gz", hash = "sha256:e0164421ff13f805f08a023e249d84200bd00463d213b490906bfefa67e83830"},
+    {file = "autoflake-2.1.1-py3-none-any.whl", hash = "sha256:94e330a2bcf5ac01384fb2bf98bea60c6383eaa59ea62be486e376622deba985"},
+    {file = "autoflake-2.1.1.tar.gz", hash = "sha256:75524b48d42d6537041d91f17573b8a98cb645642f9f05c7fcc68de10b1cade3"},
 ]
 
 [package.dependencies]
@@ -324,14 +324,14 @@ files = [
 
 [[package]]
 name = "datamodel-code-generator"
-version = "0.17.2"
+version = "0.18.0"
 description = "Datamodel Code Generator"
 category = "main"
 optional = false
 python-versions = ">=3.7,<4.0"
 files = [
-    {file = "datamodel_code_generator-0.17.2-py3-none-any.whl", hash = "sha256:b42b8b4da4055ea96ddf303efecfa37316bb025e88513fffc4dad960cb56b43c"},
-    {file = "datamodel_code_generator-0.17.2.tar.gz", hash = "sha256:e88e000b254b07937a45902e95c08c9edec21eee3492c3a3ad4b6a4115345924"},
+    {file = "datamodel_code_generator-0.18.0-py3-none-any.whl", hash = "sha256:d8a1a2c1fb115e26ac3880d3b373d14c178849f0c842d6b4738a57ee43beeff0"},
+    {file = "datamodel_code_generator-0.18.0.tar.gz", hash = "sha256:5acf69bcf3c440e90a7d0c7b99ca100c8f6ce0348aa4b3da8ee4f9b0242d164c"},
 ]
 
 [package.dependencies]
@@ -345,16 +345,12 @@ openapi-spec-validator = ">=0.2.8,<=0.5.1"
 packaging = "*"
 prance = ">=0.18.2,<1.0"
 pydantic = [
-    {version = ">=1.10.0,<2.0", extras = ["email"], markers = "python_version >= \"3.11\""},
-    {version = ">=1.5.1,<2.0", extras = ["email"], markers = "python_version < \"3.10\""},
-    {version = ">=1.9.0,<2.0", extras = ["email"], markers = "python_version >= \"3.10\" and python_version < \"3.11\""},
+    {version = ">=1.10.0,<2.0.0", extras = ["email"], markers = "python_version >= \"3.11\" and python_version < \"4.0\""},
+    {version = ">=1.5.1,<2.0.0", extras = ["email"], markers = "python_version < \"3.10\""},
+    {version = ">=1.9.0,<2.0.0", extras = ["email"], markers = "python_version >= \"3.10\" and python_version < \"3.11\""},
 ]
 PySnooper = ">=0.4.1,<2.0.0"
 toml = ">=0.10.0,<1.0.0"
-typed-ast = [
-    {version = ">=1.5.0", markers = "python_full_version >= \"3.9.8\""},
-    {version = ">=1.4.2", markers = "python_full_version < \"3.9.8\""},
-]
 
 [package.extras]
 http = ["httpx"]
@@ -414,35 +410,35 @@ wmi = ["wmi (>=1.5.1,<2.0.0)"]
 
 [[package]]
 name = "email-validator"
-version = "1.3.1"
+version = "2.0.0.post2"
 description = "A robust email address syntax and deliverability validation library."
 category = "main"
 optional = false
-python-versions = ">=3.5"
+python-versions = ">=3.7"
 files = [
-    {file = "email_validator-1.3.1-py2.py3-none-any.whl", hash = "sha256:49a72f5fa6ed26be1c964f0567d931d10bf3fdeeacdf97bc26ef1cd2a44e0bda"},
-    {file = "email_validator-1.3.1.tar.gz", hash = "sha256:d178c5c6fa6c6824e9b04f199cf23e79ac15756786573c190d2ad13089411ad2"},
+    {file = "email_validator-2.0.0.post2-py3-none-any.whl", hash = "sha256:2466ba57cda361fb7309fd3d5a225723c788ca4bbad32a0ebd5373b99730285c"},
+    {file = "email_validator-2.0.0.post2.tar.gz", hash = "sha256:1ff6e86044200c56ae23595695c54e9614f4a9551e0e393614f764860b3d7900"},
 ]
 
 [package.dependencies]
-dnspython = ">=1.15.0"
+dnspython = ">=2.0.0"
 idna = ">=2.0.0"
 
 [[package]]
 name = "filelock"
-version = "3.10.7"
+version = "3.12.0"
 description = "A platform independent file lock."
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "filelock-3.10.7-py3-none-any.whl", hash = "sha256:bde48477b15fde2c7e5a0713cbe72721cb5a5ad32ee0b8f419907960b9d75536"},
-    {file = "filelock-3.10.7.tar.gz", hash = "sha256:892be14aa8efc01673b5ed6589dbccb95f9a8596f0507e232626155495c18105"},
+    {file = "filelock-3.12.0-py3-none-any.whl", hash = "sha256:ad98852315c2ab702aeb628412cbf7e95b7ce8c3bf9565670b4eaecf1db370a9"},
+    {file = "filelock-3.12.0.tar.gz", hash = "sha256:fc03ae43288c013d2ea83c8597001b1129db351aad9c57fe2409327916b8e718"},
 ]
 
 [package.extras]
-docs = ["furo (>=2022.12.7)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"]
-testing = ["covdefaults (>=2.3)", "coverage (>=7.2.2)", "diff-cover (>=7.5)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"]
+docs = ["furo (>=2023.3.27)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"]
+testing = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"]
 
 [[package]]
 name = "flake8"
@@ -530,14 +526,14 @@ files = [
 
 [[package]]
 name = "importlib-metadata"
-version = "6.1.0"
+version = "6.5.1"
 description = "Read metadata from Python packages"
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "importlib_metadata-6.1.0-py3-none-any.whl", hash = "sha256:ff80f3b5394912eb1b108fcfd444dc78b7f1f3e16b16188054bd01cb9cb86f09"},
-    {file = "importlib_metadata-6.1.0.tar.gz", hash = "sha256:43ce9281e097583d758c2c708c4376371261a02c34682491a8e98352365aad20"},
+    {file = "importlib_metadata-6.5.1-py3-none-any.whl", hash = "sha256:cd4687a8df60d9aefd424ed9364a8f29def203a9482ec8eb8e8070ef06075f89"},
+    {file = "importlib_metadata-6.5.1.tar.gz", hash = "sha256:b986d197242e4e9960a12743a6ec5a9fc8b3d7054612d90489452170785c98a5"},
 ]
 
 [package.dependencies]
@@ -859,14 +855,14 @@ min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-imp
 
 [[package]]
 name = "mkdocs-material"
-version = "9.1.5"
+version = "9.1.6"
 description = "Documentation that simply works"
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "mkdocs_material-9.1.5-py3-none-any.whl", hash = "sha256:981e1ef0250e2fbcc23610e9b20e5f242fe1f808079b9bcfbb6c49aa2999343c"},
-    {file = "mkdocs_material-9.1.5.tar.gz", hash = "sha256:744519bca52b1e8fe7c2e80e15ed59baf8948111ec763ae6ae629c409bd16d6e"},
+    {file = "mkdocs_material-9.1.6-py3-none-any.whl", hash = "sha256:f2eb1d40db89da9922944833c1387207408f8937e1c2b46ab86e0c8f170b71e0"},
+    {file = "mkdocs_material-9.1.6.tar.gz", hash = "sha256:2e555152f9771646bfa62dc78a86052876183eff69ce30db03a33e85702b21fc"},
 ]
 
 [package.dependencies]
@@ -983,14 +979,14 @@ requests = ["requests"]
 
 [[package]]
 name = "packaging"
-version = "23.0"
+version = "23.1"
 description = "Core utilities for Python packages"
 category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"},
-    {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"},
+    {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"},
+    {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"},
 ]
 
 [[package]]
@@ -1218,14 +1214,14 @@ files = [
 
 [[package]]
 name = "pygments"
-version = "2.14.0"
+version = "2.15.1"
 description = "Pygments is a syntax highlighting package written in Python."
 category = "dev"
 optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
 files = [
-    {file = "Pygments-2.14.0-py3-none-any.whl", hash = "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717"},
-    {file = "Pygments-2.14.0.tar.gz", hash = "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297"},
+    {file = "Pygments-2.15.1-py3-none-any.whl", hash = "sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1"},
+    {file = "Pygments-2.15.1.tar.gz", hash = "sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c"},
 ]
 
 [package.extras]
@@ -1233,14 +1229,14 @@ plugins = ["importlib-metadata"]
 
 [[package]]
 name = "pymdown-extensions"
-version = "9.10"
+version = "9.11"
 description = "Extension pack for Python Markdown."
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "pymdown_extensions-9.10-py3-none-any.whl", hash = "sha256:31eaa76ce6f96aabfcea98787c2fff2c5c0611b20a53a94213970cfbf05f02b8"},
-    {file = "pymdown_extensions-9.10.tar.gz", hash = "sha256:562c38eee4ce3f101ce631b804bfc2177a8a76c7e4dc908871fb6741a90257a7"},
+    {file = "pymdown_extensions-9.11-py3-none-any.whl", hash = "sha256:a499191d8d869f30339de86fcf072a787e86c42b6f16f280f5c2cf174182b7f3"},
+    {file = "pymdown_extensions-9.11.tar.gz", hash = "sha256:f7e86c1d3981f23d9dc43294488ecb54abadd05b0be4bf8f0e15efc90f7853ff"},
 ]
 
 [package.dependencies]
@@ -1326,20 +1322,23 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xm
 
 [[package]]
 name = "pytest-sugar"
-version = "0.9.6"
+version = "0.9.7"
 description = "pytest-sugar is a plugin for pytest that changes the default look and feel of pytest (e.g. progressbar, show tests that fail instantly)."
 category = "dev"
 optional = false
 python-versions = "*"
 files = [
-    {file = "pytest-sugar-0.9.6.tar.gz", hash = "sha256:c4793495f3c32e114f0f5416290946c316eb96ad5a3684dcdadda9267e59b2b8"},
-    {file = "pytest_sugar-0.9.6-py2.py3-none-any.whl", hash = "sha256:30e5225ed2b3cc988a8a672f8bda0fc37bcd92d62e9273937f061112b3f2186d"},
+    {file = "pytest-sugar-0.9.7.tar.gz", hash = "sha256:f1e74c1abfa55f7241cf7088032b6e378566f16b938f3f08905e2cf4494edd46"},
+    {file = "pytest_sugar-0.9.7-py2.py3-none-any.whl", hash = "sha256:8cb5a4e5f8bbcd834622b0235db9e50432f4cbd71fef55b467fe44e43701e062"},
 ]
 
 [package.dependencies]
-packaging = ">=14.1"
-pytest = ">=2.9"
-termcolor = ">=1.1.0"
+packaging = ">=21.3"
+pytest = ">=6.2.0"
+termcolor = ">=2.1.0"
+
+[package.extras]
+dev = ["black", "flake8", "pre-commit"]
 
 [[package]]
 name = "python-dateutil"
@@ -1580,14 +1579,14 @@ files = [
 
 [[package]]
 name = "setuptools"
-version = "67.6.1"
+version = "67.7.1"
 description = "Easily download, build, install, upgrade, and uninstall Python packages"
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "setuptools-67.6.1-py3-none-any.whl", hash = "sha256:e728ca814a823bf7bf60162daf9db95b93d532948c4c0bea762ce62f60189078"},
-    {file = "setuptools-67.6.1.tar.gz", hash = "sha256:257de92a9d50a60b8e22abfcbb771571fde0dbf3ec234463212027a4eeecbe9a"},
+    {file = "setuptools-67.7.1-py3-none-any.whl", hash = "sha256:6f0839fbdb7e3cfef1fc38d7954f5c1c26bf4eebb155a55c9bf8faf997b9fb67"},
+    {file = "setuptools-67.7.1.tar.gz", hash = "sha256:bb16732e8eb928922eabaa022f881ae2b7cdcfaf9993ef1f5e841a96d32b8e0c"},
 ]
 
 [package.extras]
@@ -1607,18 +1606,6 @@ files = [
     {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
 ]
 
-[[package]]
-name = "sqlparse"
-version = "0.4.3"
-description = "A non-validating SQL parser."
-category = "main"
-optional = false
-python-versions = ">=3.5"
-files = [
-    {file = "sqlparse-0.4.3-py3-none-any.whl", hash = "sha256:0323c0ec29cd52bceabc1b4d9d579e311f3e4961b98d174201d5622a23b85e34"},
-    {file = "sqlparse-0.4.3.tar.gz", hash = "sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268"},
-]
-
 [[package]]
 name = "termcolor"
 version = "2.2.0"
@@ -1658,40 +1645,6 @@ files = [
     {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
 ]
 
-[[package]]
-name = "typed-ast"
-version = "1.5.4"
-description = "a fork of Python 2 and 3 ast modules with type comment support"
-category = "main"
-optional = false
-python-versions = ">=3.6"
-files = [
-    {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"},
-    {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"},
-    {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"},
-    {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"},
-    {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"},
-    {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"},
-    {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"},
-    {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"},
-    {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"},
-    {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"},
-    {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"},
-    {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"},
-    {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"},
-    {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"},
-    {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"},
-    {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"},
-    {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"},
-    {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"},
-    {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"},
-    {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"},
-    {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"},
-    {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"},
-    {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"},
-    {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"},
-]
-
 [[package]]
 name = "typing-extensions"
 version = "4.5.0"
@@ -1723,24 +1676,24 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
 
 [[package]]
 name = "virtualenv"
-version = "20.21.0"
+version = "20.22.0"
 description = "Virtual Python Environment builder"
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "virtualenv-20.21.0-py3-none-any.whl", hash = "sha256:31712f8f2a17bd06234fa97fdf19609e789dd4e3e4bf108c3da71d710651adbc"},
-    {file = "virtualenv-20.21.0.tar.gz", hash = "sha256:f50e3e60f990a0757c9b68333c9fdaa72d7188caa417f96af9e52407831a3b68"},
+    {file = "virtualenv-20.22.0-py3-none-any.whl", hash = "sha256:48fd3b907b5149c5aab7c23d9790bea4cac6bc6b150af8635febc4cfeab1275a"},
+    {file = "virtualenv-20.22.0.tar.gz", hash = "sha256:278753c47aaef1a0f14e6db8a4c5e1e040e90aea654d0fc1dc7e0d8a42616cc3"},
 ]
 
 [package.dependencies]
 distlib = ">=0.3.6,<1"
-filelock = ">=3.4.1,<4"
-platformdirs = ">=2.4,<4"
+filelock = ">=3.11,<4"
+platformdirs = ">=3.2,<4"
 
 [package.extras]
-docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"]
-test = ["covdefaults (>=2.2.2)", "coverage (>=7.1)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23)", "pytest (>=7.2.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)"]
+docs = ["furo (>=2023.3.27)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"]
+test = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.3.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)"]
 
 [[package]]
 name = "watchdog"
@@ -1801,4 +1754,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.9"
-content-hash = "539da83f8b429922ce4e84e941f9266c1da07af2c91fc0abd68b61e486e09140"
+content-hash = "b946e55327778edadddf6509abea0ba4b9e5c3f32b04c7f80f00fd79db740a19"
diff --git a/pyproject.toml b/pyproject.toml
index 1f56bbd..b476515 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -30,7 +30,6 @@ dbterd = "dbterd.main:main"
 python = "^3.9"
 click = "^8.1.3"
 dbt-artifacts-parser = "^0.2.3"
-sqlparse = "^0.4.3"
 
 [tool.poetry.dev-dependencies]
 pytest = "^6.2.5"


Test patch:
diff --git a/dbterd/adapters/algos/test_relationship.py b/dbterd/adapters/algos/test_relationship.py
index 23ee210..5e89d9d 100644
--- a/dbterd/adapters/algos/test_relationship.py
+++ b/dbterd/adapters/algos/test_relationship.py
@@ -1,33 +1,35 @@
-from dbterd.adapters.algos.meta import Column, Ref, Table
+from dbterd.adapters.algos import base
+from dbterd.adapters.algos.filter import is_selected_table
+from dbterd.adapters.algos.meta import Ref
 
 
 def parse(manifest, catalog, **kwargs):
-    # Parse Table
-    tables = get_tables(manifest, catalog)
-    # Apply selection
-    select_rule = (kwargs.get("select") or "").lower().split(":")
-    resource_types = kwargs.get("resource_type") or ""
+    """Get all information (tables, relationships) needed for building diagram
 
-    def filter_table_select(table):
-        if select_rule[0].startswith("schema"):
-            schema = f"{table.database}.{table.schema}"
-            return schema.startswith(select_rule[-1]) or table.schema.startswith(
-                select_rule[-1]
-            )
-        else:
-            return table.name.startswith(select_rule[-1])
+    Args:
+        manifest (dict): Manifest json
+        catalog (dict): Catalog json
 
-    tables = [table for table in tables if filter_table_select(table)]
-    tables = [table for table in tables if table.resource_type in resource_types]
-
-    # -- apply exclusion (take care of name only)
+    Returns:
+        Tuple(List[Table], List[Ref]): Info of parsed tables and relationships
+    """
+    # Parse Table
+    tables = base.get_tables(manifest=manifest, catalog=catalog)
 
-    exclude_rule = kwargs.get("exclude")
-    if exclude_rule:
-        tables = [table for table in tables if not table.name.startswith(exclude_rule)]
+    # Apply selection
+    tables = [
+        table
+        for table in tables
+        if is_selected_table(
+            table=table,
+            select_rules=(kwargs.get("select") or []),
+            resource_types=kwargs.get("resource_type", []),
+            exclude_rules=kwargs.get("exclude", []),
+        )
+    ]
 
     # Parse Ref
-    relationships = get_relationships(manifest)
+    relationships = get_relationships(manifest=manifest)
     table_names = [x.name for x in tables]
     relationships = [
         x
@@ -36,85 +38,22 @@ def filter_table_select(table):
     ]
 
     # Fullfill columns in Tables (due to `select *`)
-    for relationship in relationships:
-        for table in tables:
-            table_columns = [x.name.lower() for x in table.columns]
-            if (
-                table.name == relationship.table_map[0]
-                and relationship.column_map[0].lower() not in table_columns
-            ):
-                table.columns.append(Column(name=relationship.column_map[0]))
-            if (
-                table.name == relationship.table_map[1]
-                and relationship.column_map[1].lower() not in table_columns
-            ):
-                table.columns.append(Column(name=relationship.column_map[1]))
+    tables = base.enrich_tables_from_relationships(
+        tables=tables, relationships=relationships
+    )
 
     return (tables, relationships)
 
 
-def get_tables(manifest, catalog):
-    """Extract tables from dbt artifacts"""
-
-    def create_table_and_columns(table_name, resource, catalog_resource=None):
-        table = Table(
-            name=table_name,
-            raw_sql=get_compiled_sql(resource),
-            database=resource.database.lower(),
-            schema=resource.schema_.lower(),
-            columns=[],
-            resource_type=table_name.split(".")[0],
-        )
-
-        if catalog_resource:
-            for column, metadata in catalog_resource.columns.items():
-                table.columns.append(
-                    Column(
-                        name=str(column).lower(),
-                        data_type=str(metadata.type).lower(),
-                    )
-                )
-
-        for column_name, column_metadata in resource.columns.items():
-            column_name = column_name.strip('"')
-            if not any(c.name.lower() == column_name.lower() for c in table.columns):
-                table.columns.append(
-                    Column(
-                        name=column_name.lower(),
-                        data_type=str(column_metadata.data_type or "unknown").lower(),
-                    )
-                )
-
-        if not table.columns:
-            table.columns.append(Column())
-
-        return table
-
-    tables = []
-
-    if hasattr(manifest, "nodes"):
-        for table_name, node in manifest.nodes.items():
-            if (
-                table_name.startswith("model.")
-                or table_name.startswith("seed.")
-                or table_name.startswith("snapshot.")
-            ):
-                catalog_node = catalog.nodes.get(table_name)
-                table = create_table_and_columns(table_name, node, catalog_node)
-                tables.append(table)
-
-    if hasattr(manifest, "sources"):
-        for table_name, source in manifest.sources.items():
-            if table_name.startswith("source"):
-                catalog_source = catalog.sources.get(table_name)
-                table = create_table_and_columns(table_name, source, catalog_source)
-                tables.append(table)
-
-    return tables
+def get_relationships(manifest):
+    """Extract relationships from dbt artifacts based on test relationship
 
+    Args:
+        manifest (dict): Manifest json
 
-def get_relationships(manifest):
-    """Extract relationships from dbt artifacts based on test relationship"""
+    Returns:
+        List[Ref]: List of parsed relationship
+    """
     refs = [
         Ref(
             name=x,
@@ -147,24 +86,3 @@ def get_relationships(manifest):
         return distinct_list
 
     return []
-
-
-def get_compiled_sql(manifest_node):
-    if hasattr(manifest_node, "compiled_sql"):  # up to v6
-        return manifest_node.compiled_sql
-
-    if hasattr(manifest_node, "compiled_code"):  # from v7
-        return manifest_node.compiled_code
-
-    if hasattr(
-        manifest_node, "columns"
-    ):  # nodes having no compiled but just list of columns
-        return """select
-            {columns}
-        from {table}
-        """.format(
-            columns=",\n".join([f"{x}" for x in manifest_node.columns]),
-            table=f"{manifest_node.database}.{manifest_node.schema}.undefined",
-        )
-
-    return manifest_node.raw_sql  # fallback to raw dbt code
diff --git a/dbterd/adapters/targets/dbml/dbml_test_relationship.py b/dbterd/adapters/targets/dbml/dbml_test_relationship.py
index 2fa3ae3..ff607ea 100644
--- a/dbterd/adapters/targets/dbml/dbml_test_relationship.py
+++ b/dbterd/adapters/targets/dbml/dbml_test_relationship.py
@@ -2,10 +2,28 @@
 
 
 def run(manifest, catalog, **kwargs):
+    """Parse dbt artifacts and export DBML file
+
+    Args:
+        manifest (dict): Manifest json
+        catalog (dict): Catalog json
+
+    Returns:
+        Tuple(str, str): File name and the DBML content
+    """
     return ("output.dbml", parse(manifest, catalog, **kwargs))
 
 
 def parse(manifest, catalog, **kwargs):
+    """Get the DBML content from dbt artifacts
+
+    Args:
+        manifest (dict): Manifest json
+        catalog (dict): Catalog json
+
+    Returns:
+        str: DBML content
+    """
     tables, relationships = test_relationship.parse(
         manifest=manifest, catalog=catalog, **kwargs
     )
diff --git a/dbterd/adapters/targets/mermaid/mermaid_test_relationship.py b/dbterd/adapters/targets/mermaid/mermaid_test_relationship.py
index 79e80de..89feb51 100644
--- a/dbterd/adapters/targets/mermaid/mermaid_test_relationship.py
+++ b/dbterd/adapters/targets/mermaid/mermaid_test_relationship.py
@@ -2,10 +2,28 @@
 
 
 def run(manifest, catalog, **kwargs):
+    """Parse dbt artifacts and export Mermaid file
+
+    Args:
+        manifest (dict): Manifest json
+        catalog (dict): Catalog json
+
+    Returns:
+        Tuple(str, str): File name and the Mermaid content
+    """
     return ("output.md", parse(manifest, catalog, **kwargs))
 
 
 def parse(manifest, catalog, **kwargs):
+    """Get the Mermaid content from dbt artifacts
+
+    Args:
+        manifest (dict): Manifest json
+        catalog (dict): Catalog json
+
+    Returns:
+        str: Mermaid content
+    """
     tables, relationships = test_relationship.parse(
         manifest=manifest, catalog=catalog, **kwargs
     )
diff --git a/tests/unit/adapters/algos/test_test_relationship.py b/tests/unit/adapters/algos/test_test_relationship.py
index bae2a25..09deb2a 100644
--- a/tests/unit/adapters/algos/test_test_relationship.py
+++ b/tests/unit/adapters/algos/test_test_relationship.py
@@ -4,6 +4,7 @@
 
 import pytest
 
+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
 
@@ -214,10 +215,10 @@ class TestAlgoTestRelationship:
     )
     def test_get_tables(self, manifest, catalog, expected):
         with mock.patch(
-            "dbterd.adapters.algos.test_relationship.get_compiled_sql",
+            "dbterd.adapters.algos.base.get_compiled_sql",
             return_value="--irrelevant--",
         ) as mock_get_compiled_sql:
-            assert algo.get_tables(manifest, catalog) == expected
+            assert base_algo.get_tables(manifest, catalog) == expected
             mock_get_compiled_sql.assert_called()
 
     @pytest.mark.parametrize(
@@ -237,9 +238,9 @@ def test_get_tables(self, manifest, catalog, expected):
         ],
     )
     def test_get_compiled(self, manifest, expected):
-        assert algo.get_compiled_sql(manifest_node=manifest).replace(" ", "").replace(
-            "\n", ""
-        ) == str(expected).replace(" ", "").replace("\n", "")
+        assert base_algo.get_compiled_sql(manifest_node=manifest).replace(
+            " ", ""
+        ).replace("\n", "") == str(expected).replace(" ", "").replace("\n", "")
 
     @pytest.mark.parametrize(
         "manifest, expected",
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 a819e39..9601de1 100644
--- a/tests/unit/adapters/targets/dbml/test_dbml_test_relationship.py
+++ b/tests/unit/adapters/targets/dbml/test_dbml_test_relationship.py
@@ -21,8 +21,8 @@ class TestDbmlTestRelationship:
                     )
                 ],
                 [],
-                "",
-                None,
+                [],
+                [],
                 ["model"],
                 """//Tables (based on the selection criteria)
                 //--configured at schema: --database--.--schema--
@@ -68,8 +68,8 @@ class TestDbmlTestRelationship:
                         column_map=["name-notexist2", "name-notexist1"],
                     ),
                 ],
-                "",
-                None,
+                [],
+                [],
                 ["model", "source"],
                 """//Tables (based on the selection criteria)
                 //--configured at schema: --database--.--schema--
@@ -115,8 +115,8 @@ class TestDbmlTestRelationship:
                         column_map=["name2", "name1"],
                     )
                 ],
-                "schema:--schema--",
-                None,
+                ["schema:--schema--"],
+                [],
                 ["model", "source"],
                 """//Tables (based on the selection criteria)
                 //--configured at schema: --database--.--schema--
@@ -137,10 +137,79 @@ class TestDbmlTestRelationship:
                     )
                 ],
                 [],
-                "",
-                "model.dbt_resto.table1",
+                [],
+                ["model.dbt_resto.table1"],
+                ["model"],
+                """//Tables (based on the selection criteria)
+                //Refs (based on the DBT Relationship Tests)
+                """,
+            ),
+            (
+                [
+                    Table(
+                        name="model.dbt_resto.table1",
+                        database="--database--",
+                        schema="--schema--",
+                        columns=[Column(name="name1", data_type="name1-type")],
+                        raw_sql="--irrelevant--",
+                    ),
+                    Table(
+                        name="model.dbt_resto.table2",
+                        database="--database--",
+                        schema="--schema--",
+                        columns=[Column(name="name2", data_type="name2-type")],
+                        raw_sql="--irrelevant--",
+                    ),
+                ],
+                [],
+                ["model.dbt_resto"],
+                ["model.dbt_resto.table2"],
                 ["model"],
                 """//Tables (based on the selection criteria)
+                //--configured at schema: --database--.--schema--
+                Table "model.dbt_resto.table1" {
+                    "name1" "name1-type"
+                }
+                //Refs (based on the DBT Relationship Tests)
+                """,
+            ),
+            (
+                [
+                    Table(
+                        name="model.dbt_resto.table1",
+                        database="--database--",
+                        schema="--schema--",
+                        columns=[Column(name="name1", data_type="name1-type")],
+                        raw_sql="--irrelevant--",
+                    ),
+                    Table(
+                        name="model.dbt_resto.view2",
+                        database="--database--",
+                        schema="--schema--",
+                        columns=[Column(name="name2", data_type="name2-type")],
+                        raw_sql="--irrelevant--",
+                    ),
+                    Table(
+                        name="source.dbt_resto.table3",
+                        database="--database3--",
+                        schema="--schema3--",
+                        columns=[Column(name="name3", data_type="name3-type3")],
+                        raw_sql="--irrelevant--",
+                    ),
+                ],
+                [],
+                ["wildcard:*.table*"],
+                [],
+                ["model", "source"],
+                """//Tables (based on the selection criteria)
+                //--configured at schema: --database--.--schema--
+                Table "model.dbt_resto.table1" {
+                    "name1" "name1-type"
+                }
+                //--configured at schema: --database3--.--schema3--
+                Table "source.dbt_resto.table3" {
+                    "name3" "name3-type3"
+                }
                 //Refs (based on the DBT Relationship Tests)
                 """,
             ),
@@ -150,7 +219,7 @@ def test_parse(
         self, tables, relationships, select, exclude, resource_type, expected
     ):
         with mock.patch(
-            "dbterd.adapters.algos.test_relationship.get_tables",
+            "dbterd.adapters.algos.base.get_tables",
             return_value=tables,
         ) as mock_get_tables:
             with mock.patch(
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 89425db..9a442d6 100644
--- a/tests/unit/adapters/targets/mermaid/test_mermaid_test_relationship.py
+++ b/tests/unit/adapters/targets/mermaid/test_mermaid_test_relationship.py
@@ -21,8 +21,8 @@ class TestMermaidTestRelationship:
                     )
                 ],
                 [],
-                "",
-                None,
+                [],
+                [],
                 ["model"],
                 """erDiagram
                   "MODEL.DBT_RESTO.TABLE1" {
@@ -66,8 +66,8 @@ class TestMermaidTestRelationship:
                         column_map=["name-notexist2", "name-notexist1"],
                     ),
                 ],
-                "",
-                None,
+                [],
+                [],
                 ["model", "source"],
                 """erDiagram
                   "MODEL.DBT_RESTO.TABLE1" {
@@ -109,8 +109,8 @@ class TestMermaidTestRelationship:
                         column_map=["name2", "name1"],
                     )
                 ],
-                "schema:--schema--",
-                None,
+                ["schema:--schema--"],
+                [],
                 ["model", "source"],
                 """erDiagram
                     "MODEL.DBT_RESTO.TABLE1" {
@@ -129,19 +129,93 @@ class TestMermaidTestRelationship:
                     )
                 ],
                 [],
-                "",
-                "model.dbt_resto.table1",
+                [],
+                ["model.dbt_resto.table1"],
                 ["model"],
                 """erDiagram
                 """,
             ),
+            (
+                [
+                    Table(
+                        name="model.dbt_resto.table1",
+                        database="--database--",
+                        schema="--schema--",
+                        columns=[Column(name="name1", data_type="name1-type")],
+                        raw_sql="--irrelevant--",
+                    ),
+                    Table(
+                        name="model.dbt_resto.table2",
+                        database="--database--",
+                        schema="--schema--",
+                        columns=[Column(name="name2", data_type="name2-type")],
+                        raw_sql="--irrelevant--",
+                    ),
+                ],
+                [],
+                ["model.dbt_resto"],
+                ["model.dbt_resto.table2"],
+                ["model"],
+                """erDiagram
+                    "MODEL.DBT_RESTO.TABLE1" {
+                        name1-type name1
+                    }
+                """,
+            ),
+            (
+                [
+                    Table(
+                        name="model.dbt_resto.table1",
+                        database="--database--",
+                        schema="--schema--",
+                        columns=[Column(name="name1", data_type="name1-type")],
+                        raw_sql="--irrelevant--",
+                    ),
+                ],
+                [],
+                ["schema:", "wildcard:", ""],
+                [],
+                ["model"],
+                """erDiagram
+                    "MODEL.DBT_RESTO.TABLE1" {
+                        name1-type name1
+                    }
+                """,
+            ),
+            (
+                [
+                    Table(
+                        name="model.dbt_resto.table1",
+                        database="--database--",
+                        schema="--schema--",
+                        columns=[Column(name="name1", data_type="name1-type")],
+                        raw_sql="--irrelevant--",
+                    ),
+                    Table(
+                        name="model.dbt_resto.table2",
+                        database="--database--",
+                        schema="--schema--",
+                        columns=[Column(name="name2", data_type="name2-type")],
+                        raw_sql="--irrelevant--",
+                    ),
+                ],
+                [],
+                ["schema:--schema--,wildcard:*dbt_resto.table*"],
+                ["wildcard:*table2"],
+                ["model"],
+                """erDiagram
+                    "MODEL.DBT_RESTO.TABLE1" {
+                        name1-type name1
+                    }
+                """,
+            ),
         ],
     )
     def test_parse(
         self, tables, relationships, select, exclude, resource_type, expected
     ):
         with mock.patch(
-            "dbterd.adapters.algos.test_relationship.get_tables",
+            "dbterd.adapters.algos.base.get_tables",
             return_value=tables,
         ) as mock_get_tables:
             with mock.patch(