Repository: datnguye/dbterd (language: python)

Issue:
[FEAT] Expose Python APIs officially
**Describe the solution you'd like**
Expose Python APIs officially

- API to get a whole ERD
  ```python
  from dbterd.api import DbtErd

  erd = DbtErd().get_erd()
  print("erd (dbml):", erd)

  erd = DbtErd(target="mermaid").get_erd()
  print("erd (mermaid):", erd)
  ```
- API to get model's ERD: this model +1 level connected nodes
  ```python
  from dbterd.api import DbtErd

  dim_prize_erd = DbtErd(target="mermaid").get_model_erd(
      node_unique_id="model.dbt_resto.dim_prize"
  )
  print("erd of dim_date (mermaid):", dim_prize_erd)
  ```

  Here is the output:

  ```mermaid
  erDiagram
    "MODEL.DBT_RESTO.DIM_PRIZE" {
      varchar prize_key
      nvarchar prize_name
      int prize_order
    }
    "MODEL.DBT_RESTO.FACT_RESULT" {
      varchar fact_result_key
      varchar box_key
      varchar prize_key
      date date_key
      int no_of_won
      float prize_value
      float prize_paid
      int is_prize_taken
    }
    "MODEL.DBT_RESTO.FACT_RESULT" }|--|| "MODEL.DBT_RESTO.DIM_PRIZE": prize_key
  ```

API docs will be generated by `pdoc` 👉 [beta site](https://dbterd.datnguyen.de/1.12-beta/api-docs/dbterd/api.html)
PS: Mainly to support `dbt docs generate`'s result with the ERD enrichment

Give me a 👍 if you think it should be available asap! Thanks


Discussion / hints:
Great idea! In fact, I'm writing a script to wrap dbterd for the same purpose in my work case, so I thought it would help many people like me.

However, I think that there are many cases (including my case) where I would like to customize the output to markdown, so I hope that the `dbterd run` currently provided will be maintained as it is.
> Great idea! In fact, I'm writing a script to wrap dbterd for the same purpose in my work case, so I thought it would help many people like me.
> 
> However, I think that there are many cases (including my case) where I would like to customize the output to markdown, so I hope that the `dbterd run` currently provided will be maintained as it is.

Good point for the various of customizations. Maybe this would just support the Python API for that purpose only. 

Fix patch:
diff --git a/.github/workflows/publish-docs-beta.yml b/.github/workflows/publish-docs-beta.yml
index a826a20..e60e145 100644
--- a/.github/workflows/publish-docs-beta.yml
+++ b/.github/workflows/publish-docs-beta.yml
@@ -30,8 +30,15 @@ jobs:
           key: ${{ github.ref }}
           path: .cache
 
+      - uses: abatilo/actions-poetry@v2
+        with:
+          poetry-version: 1.6.1
+
       - name: Install dependencies
-        run: pip install mkdocs mkdocs-material mkdocs-minify-plugin mike
+        run: |
+          poetry config installer.max-workers 1
+          poetry config virtualenvs.in-project true
+          poetry install
 
       - name: Configure Git user
         run: |
@@ -41,9 +48,9 @@ jobs:
       - name: Publish site version (non-stable)
         if: ${{ inputs.delete == 'false' }}
         run: |
-          mike deploy --push --update-aliases ${{ inputs.version }}
+          poetry run mike deploy --push --update-aliases ${{ inputs.version }}
 
       - name: Delete site version
         if: ${{ inputs.delete == 'true' }}
         run: |
-          mike delete --push ${{ inputs.version }}
+          poetry run mike delete --push ${{ inputs.version }}
diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml
index 22b11b8..c06c7af 100644
--- a/.github/workflows/publish-docs.yml
+++ b/.github/workflows/publish-docs.yml
@@ -22,8 +22,15 @@ jobs:
           key: ${{ github.ref }}
           path: .cache
 
+      - uses: abatilo/actions-poetry@v2
+        with:
+          poetry-version: 1.6.1
+
       - name: Install dependencies
-        run: pip install mkdocs mkdocs-material mkdocs-minify-plugin mike
+        run: |
+          poetry config installer.max-workers 1
+          poetry config virtualenvs.in-project true
+          poetry install
 
       - name: Configure Git user
         run: |
@@ -32,4 +39,4 @@ jobs:
 
       - name: Publish site
         run: |
-          mike deploy --push --update-aliases ${{ inputs.version }} latest
+          poetry run mike deploy --push --update-aliases ${{ inputs.version }} latest
diff --git a/dbterd/adapters/adapter.py b/dbterd/adapters/adapter.py
index 6c65a09..243f4bd 100644
--- a/dbterd/adapters/adapter.py
+++ b/dbterd/adapters/adapter.py
@@ -1,7 +1,7 @@
 from importlib import import_module
 
 
-def load_executor(name: str):
+def load_target(name: str):
     """Import the target extension dynamically
 
     Args:
@@ -19,3 +19,23 @@ def load_executor(name: str):
         if exc.name == "dbterd.adapters.targets." + name:
             raise Exception(f"Could not find adapter target type {name}!")
         raise  # pragma: no cover
+
+
+def load_algo(name: str):
+    """Import the algo extension dynamically
+
+    Args:
+        name (str): Algo name e.g. test_relationship
+
+    Raises:
+        Exception: Algo not found
+
+    Returns:
+        ModuleType: Imported module
+    """
+    try:
+        return import_module(name=f".{name}", package="dbterd.adapters.algos")
+    except ModuleNotFoundError as exc:
+        if exc.name == "dbterd.adapters.algos." + name:
+            raise Exception(f"Could not find adapter algo {name}!")
+        raise  # pragma: no cover
diff --git a/dbterd/adapters/algos/base.py b/dbterd/adapters/algos/base.py
index 1ee5458..fc56317 100644
--- a/dbterd/adapters/algos/base.py
+++ b/dbterd/adapters/algos/base.py
@@ -415,6 +415,30 @@ def get_relationships_from_metadata(data=[], **kwargs) -> List[Ref]:
     return get_unique_refs(refs=refs)
 
 
+def get_test_nodes_by_rule_name(manifest: Manifest, rule_name: str) -> List:
+    """Get manifest nodes given the algo rule name.
+
+    Default algo rule name is `relationship`,
+    see `get_algo_rule` function for more details.
+
+    Args:
+        rule_name (str): Rule name
+        manifest (Manifest): Manifest data
+
+    Returns:
+        List: List of manifest nodes
+    """
+    return [
+        x
+        for x in manifest.nodes
+        if (
+            x.startswith("test")
+            and rule_name in x.lower()
+            and manifest.nodes[x].meta.get(TEST_META_IGNORE_IN_ERD, "0") == "0"
+        )
+    ]
+
+
 def get_relationships(manifest: Manifest, **kwargs) -> List[Ref]:
     """Extract relationships from dbt artifacts based on test relationship
 
@@ -456,11 +480,8 @@ def get_relationships(manifest: Manifest, **kwargs) -> List[Ref]:
                 manifest.nodes[x].meta.get(TEST_META_RELATIONSHIP_TYPE, "")
             ),
         )
-        for x in manifest.nodes
-        if (
-            x.startswith("test")
-            and rule.get("name").lower() in x.lower()
-            and manifest.nodes[x].meta.get(TEST_META_IGNORE_IN_ERD, "0") == "0"
+        for x in get_test_nodes_by_rule_name(
+            manifest=manifest, rule_name=rule.get("name").lower()
         )
     ]
 
diff --git a/dbterd/adapters/base.py b/dbterd/adapters/base.py
index 9aaea71..727b206 100644
--- a/dbterd/adapters/base.py
+++ b/dbterd/adapters/base.py
@@ -1,5 +1,6 @@
 import os
 from pathlib import Path
+from typing import List, Tuple
 
 import click
 
@@ -9,6 +10,7 @@
 from dbterd.adapters.dbt_cloud.discovery import DbtCloudMetadata
 from dbterd.adapters.dbt_core.dbt_invocation import DbtInvocation
 from dbterd.adapters.filter import has_unsupported_rule
+from dbterd.adapters.meta import Ref, Table
 from dbterd.helpers import cli_messaging
 from dbterd.helpers import file as file_handlers
 from dbterd.helpers.log import logger
@@ -26,15 +28,17 @@ def __init__(self, ctx) -> None:
         self.filename_catalog = "catalog.json"
         self.dbt: DbtInvocation = None
 
-    def run(self, **kwargs):
+    def run(
+        self, node_unique_id: str = None, **kwargs
+    ) -> Tuple[List[Table], List[Ref]]:
         """Generate ERD from files"""
         kwargs = self.evaluate_kwargs(**kwargs)
-        self.__run_by_strategy(**kwargs)
+        return self.__run_by_strategy(node_unique_id=node_unique_id, **kwargs)
 
-    def run_metadata(self, **kwargs):
+    def run_metadata(self, **kwargs) -> Tuple[List[Table], List[Ref]]:
         """Generate ERD from API metadata"""
         kwargs = self.evaluate_kwargs(**kwargs)
-        self.__run_metadata_by_strategy(**kwargs)
+        return self.__run_metadata_by_strategy(**kwargs)
 
     def evaluate_kwargs(self, **kwargs) -> dict:
         """Re-calculate the options
@@ -109,7 +113,7 @@ def __get_dir(self, **kwargs) -> str:
 
         return (str(artifact_dir), str(project_dir))
 
-    def __get_selection(self, **kwargs):
+    def __get_selection(self, **kwargs) -> List[str]:
         """Override the Selection using dbt's one with `--dbt`"""
         if not self.dbt:
             raise click.UsageError("Flag `--dbt` need to be enabled")
@@ -154,7 +158,7 @@ def __get_operation(self, kwargs):
         Returns:
             func: Operation function
         """
-        target = adapter.load_executor(name=kwargs["target"])  # import {target}
+        target = adapter.load_target(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(
@@ -182,7 +186,35 @@ def __save_result(self, path, data):
             logger.error(str(e))
             raise click.FileError(f"Could not save the output: {str(e)}")
 
-    def __run_by_strategy(self, **kwargs):
+    def __set_single_node_selection(
+        self, manifest, node_unique_id: str, type: str = None, **kwargs
+    ) -> dict:
+        """Override the Selection for the specific manifest node
+
+        Args:
+            manifest (Union[Manifest, dict]): Manifest data of dbt project
+            node_unique_id (str): Manifest node unique ID
+            type (str, optional): |
+                Determine manifest type e.g. from file or from metadata.
+                Defaults to None.
+
+        Returns:
+            dict: Editted kwargs dict
+        """
+        if not node_unique_id:
+            return kwargs
+
+        algo_module = adapter.load_algo(name=kwargs["algo"])
+        kwargs["select"] = algo_module.find_related_nodes_by_id(
+            manifest=manifest, node_unique_id=node_unique_id, type=type, **kwargs
+        )
+        kwargs["exclude"] = []
+
+        return kwargs
+
+    def __run_by_strategy(
+        self, node_unique_id: str = None, **kwargs
+    ) -> Tuple[List[Table], List[Ref]]:
         """Local File - Read artifacts and export the diagram file following the target"""
         if kwargs.get("dbt_cloud"):
             DbtCloudArtifact(**kwargs).get(artifacts_dir=kwargs.get("artifacts_dir"))
@@ -196,14 +228,26 @@ def __run_by_strategy(self, **kwargs):
             cv=kwargs.get("catalog_version"),
         )
 
+        if node_unique_id:
+            kwargs = self.__set_single_node_selection(
+                manifest=manifest, node_unique_id=node_unique_id, **kwargs
+            )
         operation = self.__get_operation(kwargs)
         result = operation(manifest=manifest, catalog=catalog, **kwargs)
-        self.__save_result(path=kwargs.get("output"), data=result)
 
-    def __run_metadata_by_strategy(self, **kwargs):
+        if not kwargs.get("api"):
+            self.__save_result(path=kwargs.get("output"), data=result)
+
+        return result
+
+    def __run_metadata_by_strategy(self, **kwargs) -> Tuple[List[Table], List[Ref]]:
         """Metadata - Read artifacts and export the diagram file following the target"""
         data = DbtCloudMetadata(**kwargs).query_erd_data()
         operation = self.__get_operation(kwargs)
 
         result = operation(manifest=data, catalog="metadata", **kwargs)
-        self.__save_result(path=kwargs.get("output"), data=result)
+
+        if not kwargs.get("api"):
+            self.__save_result(path=kwargs.get("output"), data=result)
+
+        return result
diff --git a/dbterd/api/__init__.py b/dbterd/api/__init__.py
new file mode 100644
index 0000000..c3307a6
--- /dev/null
+++ b/dbterd/api/__init__.py
@@ -0,0 +1,110 @@
+from typing import List, Tuple
+
+from click import Command, Context
+
+from dbterd import default
+from dbterd.adapters.base import Executor
+from dbterd.adapters.meta import Ref, Table
+
+
+class DbtErd:
+    """
+    dbt ERD official API functions.
+
+
+    Usage:
+
+    - Get a whole ERD:
+        ```python
+        from dbterd.api import DbtErd
+        erd = DbtErd().get_erd()
+        ```
+
+    - Get a whole ERD given all models attached to `my_exposure_name`:
+        ```python
+        from dbterd.api import DbtErd
+        erd = DbtErd(select="exposure:my_exposure_name").get_erd()
+        ```
+        See the
+        [Selection](https://dbterd.datnguyen.de/latest/nav/guide/cli-references.html#dbterd-run-select-s)
+        page for more details.
+
+    - Get a model (named `model.jaffle_shop.my_model`)'s ERD:
+        ```python
+        from dbterd.api import DbtErd
+        erd = DbtErd().get_model_erd(s
+            node_fqn="model.jaffle_shop.my_model"
+        )
+        ```
+    """
+
+    def __init__(self, **kwargs) -> None:
+        """Initialize the main Executor given similar input CLI parameters"""
+        self.params: dict = kwargs
+        """
+        Mimic CLI params with overriding `api = True`.\n
+        Check
+        [CLI reference page](https://dbterd.datnguyen.de/latest/nav/guide/cli-references.html)
+        for more details of how the params are used.
+        """
+        self.__set_params_default_if_not_specified()
+        ctx_command = self.params.get("api_context_command")
+        self.executor: Executor = Executor(
+            Context(Command(name=ctx_command if ctx_command else "run"))
+        )
+        """
+        Mimic CLI's executor.\n
+        The context command is `run` by default
+        unless specifying a param named `api_context_command`
+        """
+
+    def __set_params_default_if_not_specified(self) -> None:
+        """Set base params' default value (mimic CLI behaviors where possible)"""
+        self.params["api"] = True
+
+        self.params["select"] = self.params.get("select", [])
+        self.params["exclude"] = self.params.get("exclude", [])
+        self.params["resource_type"] = self.params.get(
+            "resource_type", default.default_resource_types()
+        )
+        self.params["algo"] = self.params.get("algo", default.deafult_algo())
+        self.params["entity_name_format"] = self.params.get(
+            "entity_name_format", default.default_entity_name_format()
+        )
+        self.params["omit_columns"] = self.params.get("omit_columns", False)
+
+    def get_erd(self) -> Tuple[List[Table], List[Ref]]:
+        """Generate ERD code for a whole project
+
+        Usage:
+        ```python
+        from dbterd.api import DbtErd
+        erd = DbtErd().get_erd()
+        ```
+
+        Returns:
+            Tuple[List[Table], List[Ref]]: Tables and Refs
+        """
+        return self.executor.run(**self.params)
+
+    def get_model_erd(self, node_unique_id: str) -> Tuple[List[Table], List[Ref]]:
+        """Generate ERD code for a model.
+
+        Result contains the input model and 1 level relationship model(s) (if any).
+
+        Usage:
+
+            ```python
+            from dbterd.api import DbtErd
+            erd = DbtErd().get_model_erd(
+                node_unique_id="model.jaffle_shop.my_model"
+            )
+            ```
+
+        Args:
+            - node_unique_id (str): Manifest node unique ID
+
+        Returns:
+            Tuple[List[Table], List[Ref]]: Tables and Refs
+        """
+        return self.executor.run(node_unique_id=node_unique_id, **self.params)
diff --git a/dbterd/cli/params.py b/dbterd/cli/params.py
index e60e251..18d13ee 100644
--- a/dbterd/cli/params.py
+++ b/dbterd/cli/params.py
@@ -35,7 +35,7 @@ def common_params(func):
         "--resource-type",
         "-rt",
         help="Specified dbt resource type(seed, model, source, snapshot),default:model, use examples, -rt model -rt source",
-        default=["model"],
+        default=default.default_resource_types(),
         multiple=True,
         type=click.STRING,
     )
@@ -51,7 +51,7 @@ def common_params(func):
         "--entity-name-format",
         "-enf",
         help="Specified the format of the entity node's name",
-        default="resource.package.model",
+        default=default.default_entity_name_format(),
         show_default=True,
         type=click.STRING,
     )
diff --git a/dbterd/default.py b/dbterd/default.py
index 0277cfa..56f6b0e 100644
--- a/dbterd/default.py
+++ b/dbterd/default.py
@@ -1,4 +1,5 @@
 from pathlib import Path
+from typing import List
 
 
 def default_artifact_path() -> str:
@@ -15,3 +16,11 @@ def default_target() -> str:
 
 def deafult_algo() -> str:
     return "test_relationship"
+
+
+def default_resource_types() -> List[str]:
+    return ["model", "source"]
+
+
+def default_entity_name_format() -> str:
+    return "resource.package.model"
diff --git a/docs/assets/css/syntax-highlighting.css b/docs/assets/css/syntax-highlighting.css
new file mode 100644
index 0000000..b0a7fe3
--- /dev/null
+++ b/docs/assets/css/syntax-highlighting.css
@@ -0,0 +1,80 @@
+/* monokai color scheme, see pdoc/template/README.md */
+pre { line-height: 125%; }
+span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 20px; }
+.pdoc-code .hll { background-color: #49483e }
+.pdoc-code { background: #272822; color: #f8f8f2 }
+.pdoc-code .c { color: #75715e } /* Comment */
+.pdoc-code .err { color: #960050; background-color: #1e0010 } /* Error */
+.pdoc-code .esc { color: #f8f8f2 } /* Escape */
+.pdoc-code .g { color: #f8f8f2 } /* Generic */
+.pdoc-code .k { color: #66d9ef } /* Keyword */
+.pdoc-code .l { color: #ae81ff } /* Literal */
+.pdoc-code .n { color: #f8f8f2 } /* Name */
+.pdoc-code .o { color: #f92672 } /* Operator */
+.pdoc-code .x { color: #f8f8f2 } /* Other */
+.pdoc-code .p { color: #f8f8f2 } /* Punctuation */
+.pdoc-code .ch { color: #75715e } /* Comment.Hashbang */
+.pdoc-code .cm { color: #75715e } /* Comment.Multiline */
+.pdoc-code .cp { color: #75715e } /* Comment.Preproc */
+.pdoc-code .cpf { color: #75715e } /* Comment.PreprocFile */
+.pdoc-code .c1 { color: #75715e } /* Comment.Single */
+.pdoc-code .cs { color: #75715e } /* Comment.Special */
+.pdoc-code .gd { color: #f92672 } /* Generic.Deleted */
+.pdoc-code .ge { color: #f8f8f2; font-style: italic } /* Generic.Emph */
+.pdoc-code .gr { color: #f8f8f2 } /* Generic.Error */
+.pdoc-code .gh { color: #f8f8f2 } /* Generic.Heading */
+.pdoc-code .gi { color: #a6e22e } /* Generic.Inserted */
+.pdoc-code .go { color: #66d9ef } /* Generic.Output */
+.pdoc-code .gp { color: #f92672; font-weight: bold } /* Generic.Prompt */
+.pdoc-code .gs { color: #f8f8f2; font-weight: bold } /* Generic.Strong */
+.pdoc-code .gu { color: #75715e } /* Generic.Subheading */
+.pdoc-code .gt { color: #f8f8f2 } /* Generic.Traceback */
+.pdoc-code .kc { color: #66d9ef } /* Keyword.Constant */
+.pdoc-code .kd { color: #66d9ef } /* Keyword.Declaration */
+.pdoc-code .kn { color: #f92672 } /* Keyword.Namespace */
+.pdoc-code .kp { color: #66d9ef } /* Keyword.Pseudo */
+.pdoc-code .kr { color: #66d9ef } /* Keyword.Reserved */
+.pdoc-code .kt { color: #66d9ef } /* Keyword.Type */
+.pdoc-code .ld { color: #e6db74 } /* Literal.Date */
+.pdoc-code .m { color: #ae81ff } /* Literal.Number */
+.pdoc-code .s { color: #e6db74 } /* Literal.String */
+.pdoc-code .na { color: #a6e22e } /* Name.Attribute */
+.pdoc-code .nb { color: #f8f8f2 } /* Name.Builtin */
+.pdoc-code .nc { color: #a6e22e } /* Name.Class */
+.pdoc-code .no { color: #66d9ef } /* Name.Constant */
+.pdoc-code .nd { color: #a6e22e } /* Name.Decorator */
+.pdoc-code .ni { color: #f8f8f2 } /* Name.Entity */
+.pdoc-code .ne { color: #a6e22e } /* Name.Exception */
+.pdoc-code .nf { color: #a6e22e } /* Name.Function */
+.pdoc-code .nl { color: #f8f8f2 } /* Name.Label */
+.pdoc-code .nn { color: #f8f8f2 } /* Name.Namespace */
+.pdoc-code .nx { color: #a6e22e } /* Name.Other */
+.pdoc-code .py { color: #f8f8f2 } /* Name.Property */
+.pdoc-code .nt { color: #f92672 } /* Name.Tag */
+.pdoc-code .nv { color: #f8f8f2 } /* Name.Variable */
+.pdoc-code .ow { color: #f92672 } /* Operator.Word */
+.pdoc-code .w { color: #f8f8f2 } /* Text.Whitespace */
+.pdoc-code .mb { color: #ae81ff } /* Literal.Number.Bin */
+.pdoc-code .mf { color: #ae81ff } /* Literal.Number.Float */
+.pdoc-code .mh { color: #ae81ff } /* Literal.Number.Hex */
+.pdoc-code .mi { color: #ae81ff } /* Literal.Number.Integer */
+.pdoc-code .mo { color: #ae81ff } /* Literal.Number.Oct */
+.pdoc-code .sa { color: #e6db74 } /* Literal.String.Affix */
+.pdoc-code .sb { color: #e6db74 } /* Literal.String.Backtick */
+.pdoc-code .sc { color: #e6db74 } /* Literal.String.Char */
+.pdoc-code .dl { color: #e6db74 } /* Literal.String.Delimiter */
+.pdoc-code .sd { color: #e6db74 } /* Literal.String.Doc */
+.pdoc-code .s2 { color: #e6db74 } /* Literal.String.Double */
+.pdoc-code .se { color: #ae81ff } /* Literal.String.Escape */
+.pdoc-code .sh { color: #e6db74 } /* Literal.String.Heredoc */
+.pdoc-code .si { color: #e6db74 } /* Literal.String.Interpol */
+.pdoc-code .sx { color: #e6db74 } /* Literal.String.Other */
+.pdoc-code .sr { color: #e6db74 } /* Literal.String.Regex */
+.pdoc-code .s1 { color: #e6db74 } /* Literal.String.Single */
+.pdoc-code .ss { color: #e6db74 } /* Literal.String.Symbol */
+.pdoc-code .bp { color: #f8f8f2 } /* Name.Builtin.Pseudo */
+.pdoc-code .fm { color: #a6e22e } /* Name.Function.Magic */
+.pdoc-code .vc { color: #f8f8f2 } /* Name.Variable.Class */
+.pdoc-code .vg { color: #f8f8f2 } /* Name.Variable.Global */
+.pdoc-code .vi { color: #f8f8f2 } /* Name.Variable.Instance */
+.pdoc-code .vm { color: #f8f8f2 } /* Name.Variable.Magic */
diff --git a/docs/assets/css/theme.css b/docs/assets/css/theme.css
new file mode 100644
index 0000000..1dfe7c4
--- /dev/null
+++ b/docs/assets/css/theme.css
@@ -0,0 +1,20 @@
+:root {
+    --pdoc-background: #212529;
+}
+
+.pdoc {
+    --text: #f7f7f7;
+    --muted: #9d9d9d;
+    --link: #58a6ff;
+    --link-hover: #3989ff;
+    --code: #333;
+    --active: #555;
+
+    --accent: #343434;
+    --accent2: #555;
+
+    --nav-hover: rgba(0, 0, 0, 0.1);
+    --name: #77C1FF;
+    --def: #0cdd0c;
+    --annotation: #00c037;
+}
diff --git a/docs/hooks.py b/docs/hooks.py
new file mode 100644
index 0000000..6547294
--- /dev/null
+++ b/docs/hooks.py
@@ -0,0 +1,8 @@
+import subprocess
+
+
+def on_post_build(config, **kwargs):
+    site_dir = config["site_dir"]
+    subprocess.run(
+        ["pdoc", "-t", "docs/assets/css", "dbterd/api/", "-o", f"{site_dir}/api-docs"]
+    )
diff --git a/mkdocs.yml b/mkdocs.yml
index 56783f7..d2d3ec0 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -13,7 +13,6 @@ nav:
   - dbterd:
     - index.md
     - User Guide 📖:
-      - CLI Reference: nav/guide/cli-references.md
       - Generate the Targets:
         - DBML: nav/guide/targets/generate-dbml.md
         - Mermaid: nav/guide/targets/generate-markdown-mermaid-erd.md
@@ -27,10 +26,15 @@ nav:
         - Download artifacts from a Job Run: nav/guide/dbt-cloud/download-artifact-from-a-job-run.md
         - Download the latest artifacts from a Job: nav/guide/dbt-cloud/download-artifact-from-a-job.md
         - Read the latest artifacts from an environment: nav/guide/dbt-cloud/read-artifact-from-an-environment.md
+      - CLI Reference: nav/guide/cli-references.md
+      - Python API Reference ↗️: api-docs/index.html" target="_blank # managed by hook
     - Contribution Guideline ❤️: nav/development/contributing-guide.md
     - License 🔑: license.md
     - Change Log ↗️: https://github.com/datnguye/dbterd/releases" target="_blank
 
+hooks:
+  - docs/hooks.py
+
 theme:
   name: material
   custom_dir: docs/overrides
@@ -122,6 +126,8 @@ extra:
 
 extra_css:
   - assets/css/termynal.css
+  - assets/css/syntax-highlighting.css
+  - assets/css/theme.css
 
 extra_javascript:
   - assets/js/feedback.js
diff --git a/poetry.lock b/poetry.lock
index b2c7c8a..c80674c 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -54,13 +54,13 @@ tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "p
 
 [[package]]
 name = "autoflake"
-version = "2.2.1"
+version = "2.3.0"
 description = "Removes unused imports and unused variables"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "autoflake-2.2.1-py3-none-any.whl", hash = "sha256:265cde0a43c1f44ecfb4f30d95b0437796759d07be7706a2f70e4719234c0f79"},
-    {file = "autoflake-2.2.1.tar.gz", hash = "sha256:62b7b6449a692c3c9b0c916919bbc21648da7281e8506bcf8d3f8280e431ebc1"},
+    {file = "autoflake-2.3.0-py3-none-any.whl", hash = "sha256:79a51eb8c0744759d2efe052455ab20aa6a314763510c3fd897499a402126327"},
+    {file = "autoflake-2.3.0.tar.gz", hash = "sha256:8c2011fa34701b9d7dcf05b9873bc4859d4fce4e62dfea90dffefd1576f5f01d"},
 ]
 
 [package.dependencies]
@@ -421,13 +421,13 @@ test = ["black (==21.9b0)", "flake8 (>=3.8.3,<4.0.0)", "isort (>=5.0.6,<6.0.0)",
 
 [[package]]
 name = "dbt-core"
-version = "1.7.7"
+version = "1.7.8"
 description = "With dbt, data analysts and engineers can build analytics the way engineers build applications."
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "dbt-core-1.7.7.tar.gz", hash = "sha256:af672fee1385427d2a622e8ffd8c02216336c0e29552e9a50d051be6cff4de77"},
-    {file = "dbt_core-1.7.7-py3-none-any.whl", hash = "sha256:f7c403abdff5b23e09819eae454a9102b0d589c93c6cb4d88eefa4b3d9943e33"},
+    {file = "dbt-core-1.7.8.tar.gz", hash = "sha256:20cb170ab1f6d5b9e1d44f07666b12dc4c3427d8bbe3c013d46d5d9ca8318201"},
+    {file = "dbt_core-1.7.8-py3-none-any.whl", hash = "sha256:1f9f9988a4ca84ace3329d640475111036c4cb27c9599ea7fb7c065f8347a3c1"},
 ]
 
 [package.dependencies]
@@ -482,18 +482,18 @@ files = [
 
 [[package]]
 name = "dbt-postgres"
-version = "1.7.7"
+version = "1.7.8"
 description = "The postgres adapter plugin for dbt (data build tool)"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "dbt-postgres-1.7.7.tar.gz", hash = "sha256:90d12ea7ad135a5ffb59750512dec5c3668553e557f2b656913117e903b08703"},
-    {file = "dbt_postgres-1.7.7-py3-none-any.whl", hash = "sha256:ea064ba6de2b68236ccb66fe2394cf8324737593229902ceb9d7d99497733a49"},
+    {file = "dbt-postgres-1.7.8.tar.gz", hash = "sha256:fe62fd1bbef037b0d58b39f3a23155cb6ca20ec142374b1d8ea50b1759c384e9"},
+    {file = "dbt_postgres-1.7.8-py3-none-any.whl", hash = "sha256:d0b4683f7c96af6e5a0c3e7a9567c7dda9df0d1f8c0aef8e43eda1621662dfdc"},
 ]
 
 [package.dependencies]
 agate = "*"
-dbt-core = "1.7.7"
+dbt-core = "1.7.8"
 psycopg2-binary = ">=2.8,<3.0"
 
 [[package]]
@@ -590,13 +590,13 @@ files = [
 
 [[package]]
 name = "identify"
-version = "2.5.34"
+version = "2.5.35"
 description = "File identification library for Python"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "identify-2.5.34-py2.py3-none-any.whl", hash = "sha256:a4316013779e433d08b96e5eabb7f641e6c7942e4ab5d4c509ebd2e7a8994aed"},
-    {file = "identify-2.5.34.tar.gz", hash = "sha256:ee17bc9d499899bc9eaec1ac7bf2dc9eedd480db9d88b96d123d3b64a9d34f5d"},
+    {file = "identify-2.5.35-py2.py3-none-any.whl", hash = "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e"},
+    {file = "identify-2.5.35.tar.gz", hash = "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791"},
 ]
 
 [package.extras]
@@ -974,13 +974,13 @@ min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-imp
 
 [[package]]
 name = "mkdocs-material"
-version = "9.5.9"
+version = "9.5.10"
 description = "Documentation that simply works"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "mkdocs_material-9.5.9-py3-none-any.whl", hash = "sha256:a5d62b73b3b74349e45472bfadc129c871dd2d4add68d84819580597b2f50d5d"},
-    {file = "mkdocs_material-9.5.9.tar.gz", hash = "sha256:635df543c01c25c412d6c22991872267723737d5a2f062490f33b2da1c013c6d"},
+    {file = "mkdocs_material-9.5.10-py3-none-any.whl", hash = "sha256:3c6c46b57d2ee3c8890e6e0406e68b6863cf65768f0f436990a742702d198442"},
+    {file = "mkdocs_material-9.5.10.tar.gz", hash = "sha256:6ad626dbb31070ebbaedff813323a16a406629620e04b96458f16e6e9c7008fe"},
 ]
 
 [package.dependencies]
@@ -1202,6 +1202,25 @@ files = [
     {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"},
 ]
 
+[[package]]
+name = "pdoc"
+version = "14.4.0"
+description = "API Documentation for Python Projects"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "pdoc-14.4.0-py3-none-any.whl", hash = "sha256:6ea4fe07620b1f7601e2708a307a257636ec206e20b5611640b30f2e3cab47d6"},
+    {file = "pdoc-14.4.0.tar.gz", hash = "sha256:c92edc425429ccbe287ace2a027953c24f13de53eab484c1a6d31ca72dd2fda9"},
+]
+
+[package.dependencies]
+Jinja2 = ">=2.11.0"
+MarkupSafe = "*"
+pygments = ">=2.12.0"
+
+[package.extras]
+dev = ["hypothesis", "mypy", "pdoc-pyo3-sample-library (==1.0.11)", "pygments (>=2.14.0)", "pytest", "pytest-cov", "pytest-timeout", "ruff", "tox", "types-pygments"]
+
 [[package]]
 name = "platformdirs"
 version = "4.2.0"
@@ -1270,22 +1289,22 @@ virtualenv = ">=20.10.0"
 
 [[package]]
 name = "protobuf"
-version = "4.25.2"
+version = "4.25.3"
 description = ""
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "protobuf-4.25.2-cp310-abi3-win32.whl", hash = "sha256:b50c949608682b12efb0b2717f53256f03636af5f60ac0c1d900df6213910fd6"},
-    {file = "protobuf-4.25.2-cp310-abi3-win_amd64.whl", hash = "sha256:8f62574857ee1de9f770baf04dde4165e30b15ad97ba03ceac65f760ff018ac9"},
-    {file = "protobuf-4.25.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:2db9f8fa64fbdcdc93767d3cf81e0f2aef176284071507e3ede160811502fd3d"},
-    {file = "protobuf-4.25.2-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:10894a2885b7175d3984f2be8d9850712c57d5e7587a2410720af8be56cdaf62"},
-    {file = "protobuf-4.25.2-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:fc381d1dd0516343f1440019cedf08a7405f791cd49eef4ae1ea06520bc1c020"},
-    {file = "protobuf-4.25.2-cp38-cp38-win32.whl", hash = "sha256:33a1aeef4b1927431d1be780e87b641e322b88d654203a9e9d93f218ee359e61"},
-    {file = "protobuf-4.25.2-cp38-cp38-win_amd64.whl", hash = "sha256:47f3de503fe7c1245f6f03bea7e8d3ec11c6c4a2ea9ef910e3221c8a15516d62"},
-    {file = "protobuf-4.25.2-cp39-cp39-win32.whl", hash = "sha256:5e5c933b4c30a988b52e0b7c02641760a5ba046edc5e43d3b94a74c9fc57c1b3"},
-    {file = "protobuf-4.25.2-cp39-cp39-win_amd64.whl", hash = "sha256:d66a769b8d687df9024f2985d5137a337f957a0916cf5464d1513eee96a63ff0"},
-    {file = "protobuf-4.25.2-py3-none-any.whl", hash = "sha256:a8b7a98d4ce823303145bf3c1a8bdb0f2f4642a414b196f04ad9853ed0c8f830"},
-    {file = "protobuf-4.25.2.tar.gz", hash = "sha256:fe599e175cb347efc8ee524bcd4b902d11f7262c0e569ececcb89995c15f0a5e"},
+    {file = "protobuf-4.25.3-cp310-abi3-win32.whl", hash = "sha256:d4198877797a83cbfe9bffa3803602bbe1625dc30d8a097365dbc762e5790faa"},
+    {file = "protobuf-4.25.3-cp310-abi3-win_amd64.whl", hash = "sha256:209ba4cc916bab46f64e56b85b090607a676f66b473e6b762e6f1d9d591eb2e8"},
+    {file = "protobuf-4.25.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f1279ab38ecbfae7e456a108c5c0681e4956d5b1090027c1de0f934dfdb4b35c"},
+    {file = "protobuf-4.25.3-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:e7cb0ae90dd83727f0c0718634ed56837bfeeee29a5f82a7514c03ee1364c019"},
+    {file = "protobuf-4.25.3-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:7c8daa26095f82482307bc717364e7c13f4f1c99659be82890dcfc215194554d"},
+    {file = "protobuf-4.25.3-cp38-cp38-win32.whl", hash = "sha256:f4f118245c4a087776e0a8408be33cf09f6c547442c00395fbfb116fac2f8ac2"},
+    {file = "protobuf-4.25.3-cp38-cp38-win_amd64.whl", hash = "sha256:c053062984e61144385022e53678fbded7aea14ebb3e0305ae3592fb219ccfa4"},
+    {file = "protobuf-4.25.3-cp39-cp39-win32.whl", hash = "sha256:19b270aeaa0099f16d3ca02628546b8baefe2955bbe23224aaf856134eccf1e4"},
+    {file = "protobuf-4.25.3-cp39-cp39-win_amd64.whl", hash = "sha256:e3c97a1555fd6388f857770ff8b9703083de6bf1f9274a002a332d65fbb56c8c"},
+    {file = "protobuf-4.25.3-py3-none-any.whl", hash = "sha256:f0700d54bcf45424477e46a9f0944155b46fb0639d69728739c0e47bab83f2b9"},
+    {file = "protobuf-4.25.3.tar.gz", hash = "sha256:25b5d0b42fd000320bd7830b349e3b696435f3b329810427a6bcce6a5492cc5c"},
 ]
 
 [[package]]
@@ -1807,110 +1826,110 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
 
 [[package]]
 name = "rpds-py"
-version = "0.17.1"
+version = "0.18.0"
 description = "Python bindings to Rust's persistent data structures (rpds)"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "rpds_py-0.17.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:4128980a14ed805e1b91a7ed551250282a8ddf8201a4e9f8f5b7e6225f54170d"},
-    {file = "rpds_py-0.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ff1dcb8e8bc2261a088821b2595ef031c91d499a0c1b031c152d43fe0a6ecec8"},
-    {file = "rpds_py-0.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d65e6b4f1443048eb7e833c2accb4fa7ee67cc7d54f31b4f0555b474758bee55"},
-    {file = "rpds_py-0.17.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a71169d505af63bb4d20d23a8fbd4c6ce272e7bce6cc31f617152aa784436f29"},
-    {file = "rpds_py-0.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:436474f17733c7dca0fbf096d36ae65277e8645039df12a0fa52445ca494729d"},
-    {file = "rpds_py-0.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:10162fe3f5f47c37ebf6d8ff5a2368508fe22007e3077bf25b9c7d803454d921"},
-    {file = "rpds_py-0.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:720215373a280f78a1814becb1312d4e4d1077b1202a56d2b0815e95ccb99ce9"},
-    {file = "rpds_py-0.17.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:70fcc6c2906cfa5c6a552ba7ae2ce64b6c32f437d8f3f8eea49925b278a61453"},
-    {file = "rpds_py-0.17.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:91e5a8200e65aaac342a791272c564dffcf1281abd635d304d6c4e6b495f29dc"},
-    {file = "rpds_py-0.17.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:99f567dae93e10be2daaa896e07513dd4bf9c2ecf0576e0533ac36ba3b1d5394"},
-    {file = "rpds_py-0.17.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24e4900a6643f87058a27320f81336d527ccfe503984528edde4bb660c8c8d59"},
-    {file = "rpds_py-0.17.1-cp310-none-win32.whl", hash = "sha256:0bfb09bf41fe7c51413f563373e5f537eaa653d7adc4830399d4e9bdc199959d"},
-    {file = "rpds_py-0.17.1-cp310-none-win_amd64.whl", hash = "sha256:20de7b7179e2031a04042e85dc463a93a82bc177eeba5ddd13ff746325558aa6"},
-    {file = "rpds_py-0.17.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:65dcf105c1943cba45d19207ef51b8bc46d232a381e94dd38719d52d3980015b"},
-    {file = "rpds_py-0.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:01f58a7306b64e0a4fe042047dd2b7d411ee82e54240284bab63e325762c1147"},
-    {file = "rpds_py-0.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:071bc28c589b86bc6351a339114fb7a029f5cddbaca34103aa573eba7b482382"},
-    {file = "rpds_py-0.17.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae35e8e6801c5ab071b992cb2da958eee76340e6926ec693b5ff7d6381441745"},
-    {file = "rpds_py-0.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149c5cd24f729e3567b56e1795f74577aa3126c14c11e457bec1b1c90d212e38"},
-    {file = "rpds_py-0.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e796051f2070f47230c745d0a77a91088fbee2cc0502e9b796b9c6471983718c"},
-    {file = "rpds_py-0.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60e820ee1004327609b28db8307acc27f5f2e9a0b185b2064c5f23e815f248f8"},
-    {file = "rpds_py-0.17.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1957a2ab607f9added64478a6982742eb29f109d89d065fa44e01691a20fc20a"},
-    {file = "rpds_py-0.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8587fd64c2a91c33cdc39d0cebdaf30e79491cc029a37fcd458ba863f8815383"},
-    {file = "rpds_py-0.17.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4dc889a9d8a34758d0fcc9ac86adb97bab3fb7f0c4d29794357eb147536483fd"},
-    {file = "rpds_py-0.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2953937f83820376b5979318840f3ee47477d94c17b940fe31d9458d79ae7eea"},
-    {file = "rpds_py-0.17.1-cp311-none-win32.whl", hash = "sha256:1bfcad3109c1e5ba3cbe2f421614e70439f72897515a96c462ea657261b96518"},
-    {file = "rpds_py-0.17.1-cp311-none-win_amd64.whl", hash = "sha256:99da0a4686ada4ed0f778120a0ea8d066de1a0a92ab0d13ae68492a437db78bf"},
-    {file = "rpds_py-0.17.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1dc29db3900cb1bb40353772417800f29c3d078dbc8024fd64655a04ee3c4bdf"},
-    {file = "rpds_py-0.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82ada4a8ed9e82e443fcef87e22a3eed3654dd3adf6e3b3a0deb70f03e86142a"},
-    {file = "rpds_py-0.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d36b2b59e8cc6e576f8f7b671e32f2ff43153f0ad6d0201250a7c07f25d570e"},
-    {file = "rpds_py-0.17.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3677fcca7fb728c86a78660c7fb1b07b69b281964673f486ae72860e13f512ad"},
-    {file = "rpds_py-0.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:516fb8c77805159e97a689e2f1c80655c7658f5af601c34ffdb916605598cda2"},
-    {file = "rpds_py-0.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df3b6f45ba4515632c5064e35ca7f31d51d13d1479673185ba8f9fefbbed58b9"},
-    {file = "rpds_py-0.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a967dd6afda7715d911c25a6ba1517975acd8d1092b2f326718725461a3d33f9"},
-    {file = "rpds_py-0.17.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dbbb95e6fc91ea3102505d111b327004d1c4ce98d56a4a02e82cd451f9f57140"},
-    {file = "rpds_py-0.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02866e060219514940342a1f84303a1ef7a1dad0ac311792fbbe19b521b489d2"},
-    {file = "rpds_py-0.17.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2528ff96d09f12e638695f3a2e0c609c7b84c6df7c5ae9bfeb9252b6fa686253"},
-    {file = "rpds_py-0.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd345a13ce06e94c753dab52f8e71e5252aec1e4f8022d24d56decd31e1b9b23"},
-    {file = "rpds_py-0.17.1-cp312-none-win32.whl", hash = "sha256:2a792b2e1d3038daa83fa474d559acfd6dc1e3650ee93b2662ddc17dbff20ad1"},
-    {file = "rpds_py-0.17.1-cp312-none-win_amd64.whl", hash = "sha256:292f7344a3301802e7c25c53792fae7d1593cb0e50964e7bcdcc5cf533d634e3"},
-    {file = "rpds_py-0.17.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:8ffe53e1d8ef2520ebcf0c9fec15bb721da59e8ef283b6ff3079613b1e30513d"},
-    {file = "rpds_py-0.17.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4341bd7579611cf50e7b20bb8c2e23512a3dc79de987a1f411cb458ab670eb90"},
-    {file = "rpds_py-0.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f4eb548daf4836e3b2c662033bfbfc551db58d30fd8fe660314f86bf8510b93"},
-    {file = "rpds_py-0.17.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b686f25377f9c006acbac63f61614416a6317133ab7fafe5de5f7dc8a06d42eb"},
-    {file = "rpds_py-0.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e21b76075c01d65d0f0f34302b5a7457d95721d5e0667aea65e5bb3ab415c25"},
-    {file = "rpds_py-0.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b86b21b348f7e5485fae740d845c65a880f5d1eda1e063bc59bef92d1f7d0c55"},
-    {file = "rpds_py-0.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f175e95a197f6a4059b50757a3dca33b32b61691bdbd22c29e8a8d21d3914cae"},
-    {file = "rpds_py-0.17.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1701fc54460ae2e5efc1dd6350eafd7a760f516df8dbe51d4a1c79d69472fbd4"},
-    {file = "rpds_py-0.17.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:9051e3d2af8f55b42061603e29e744724cb5f65b128a491446cc029b3e2ea896"},
-    {file = "rpds_py-0.17.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:7450dbd659fed6dd41d1a7d47ed767e893ba402af8ae664c157c255ec6067fde"},
-    {file = "rpds_py-0.17.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5a024fa96d541fd7edaa0e9d904601c6445e95a729a2900c5aec6555fe921ed6"},
-    {file = "rpds_py-0.17.1-cp38-none-win32.whl", hash = "sha256:da1ead63368c04a9bded7904757dfcae01eba0e0f9bc41d3d7f57ebf1c04015a"},
-    {file = "rpds_py-0.17.1-cp38-none-win_amd64.whl", hash = "sha256:841320e1841bb53fada91c9725e766bb25009cfd4144e92298db296fb6c894fb"},
-    {file = "rpds_py-0.17.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:f6c43b6f97209e370124baf2bf40bb1e8edc25311a158867eb1c3a5d449ebc7a"},
-    {file = "rpds_py-0.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e7d63ec01fe7c76c2dbb7e972fece45acbb8836e72682bde138e7e039906e2c"},
-    {file = "rpds_py-0.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81038ff87a4e04c22e1d81f947c6ac46f122e0c80460b9006e6517c4d842a6ec"},
-    {file = "rpds_py-0.17.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:810685321f4a304b2b55577c915bece4c4a06dfe38f6e62d9cc1d6ca8ee86b99"},
-    {file = "rpds_py-0.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:25f071737dae674ca8937a73d0f43f5a52e92c2d178330b4c0bb6ab05586ffa6"},
-    {file = "rpds_py-0.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa5bfb13f1e89151ade0eb812f7b0d7a4d643406caaad65ce1cbabe0a66d695f"},
-    {file = "rpds_py-0.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfe07308b311a8293a0d5ef4e61411c5c20f682db6b5e73de6c7c8824272c256"},
-    {file = "rpds_py-0.17.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a000133a90eea274a6f28adc3084643263b1e7c1a5a66eb0a0a7a36aa757ed74"},
-    {file = "rpds_py-0.17.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d0e8a6434a3fbf77d11448c9c25b2f25244226cfbec1a5159947cac5b8c5fa4"},
-    {file = "rpds_py-0.17.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:efa767c220d94aa4ac3a6dd3aeb986e9f229eaf5bce92d8b1b3018d06bed3772"},
-    {file = "rpds_py-0.17.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:dbc56680ecf585a384fbd93cd42bc82668b77cb525343170a2d86dafaed2a84b"},
-    {file = "rpds_py-0.17.1-cp39-none-win32.whl", hash = "sha256:270987bc22e7e5a962b1094953ae901395e8c1e1e83ad016c5cfcfff75a15a3f"},
-    {file = "rpds_py-0.17.1-cp39-none-win_amd64.whl", hash = "sha256:2a7b2f2f56a16a6d62e55354dd329d929560442bd92e87397b7a9586a32e3e76"},
-    {file = "rpds_py-0.17.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a3264e3e858de4fc601741498215835ff324ff2482fd4e4af61b46512dd7fc83"},
-    {file = "rpds_py-0.17.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:f2f3b28b40fddcb6c1f1f6c88c6f3769cd933fa493ceb79da45968a21dccc920"},
-    {file = "rpds_py-0.17.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9584f8f52010295a4a417221861df9bea4c72d9632562b6e59b3c7b87a1522b7"},
-    {file = "rpds_py-0.17.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c64602e8be701c6cfe42064b71c84ce62ce66ddc6422c15463fd8127db3d8066"},
-    {file = "rpds_py-0.17.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:060f412230d5f19fc8c8b75f315931b408d8ebf56aec33ef4168d1b9e54200b1"},
-    {file = "rpds_py-0.17.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9412abdf0ba70faa6e2ee6c0cc62a8defb772e78860cef419865917d86c7342"},
-    {file = "rpds_py-0.17.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9737bdaa0ad33d34c0efc718741abaafce62fadae72c8b251df9b0c823c63b22"},
-    {file = "rpds_py-0.17.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9f0e4dc0f17dcea4ab9d13ac5c666b6b5337042b4d8f27e01b70fae41dd65c57"},
-    {file = "rpds_py-0.17.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:1db228102ab9d1ff4c64148c96320d0be7044fa28bd865a9ce628ce98da5973d"},
-    {file = "rpds_py-0.17.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:d8bbd8e56f3ba25a7d0cf980fc42b34028848a53a0e36c9918550e0280b9d0b6"},
-    {file = "rpds_py-0.17.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:be22ae34d68544df293152b7e50895ba70d2a833ad9566932d750d3625918b82"},
-    {file = "rpds_py-0.17.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:bf046179d011e6114daf12a534d874958b039342b347348a78b7cdf0dd9d6041"},
-    {file = "rpds_py-0.17.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:1a746a6d49665058a5896000e8d9d2f1a6acba8a03b389c1e4c06e11e0b7f40d"},
-    {file = "rpds_py-0.17.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0b8bf5b8db49d8fd40f54772a1dcf262e8be0ad2ab0206b5a2ec109c176c0a4"},
-    {file = "rpds_py-0.17.1-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f7f4cb1f173385e8a39c29510dd11a78bf44e360fb75610594973f5ea141028b"},
-    {file = "rpds_py-0.17.1-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7fbd70cb8b54fe745301921b0816c08b6d917593429dfc437fd024b5ba713c58"},
-    {file = "rpds_py-0.17.1-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bdf1303df671179eaf2cb41e8515a07fc78d9d00f111eadbe3e14262f59c3d0"},
-    {file = "rpds_py-0.17.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad059a4bd14c45776600d223ec194e77db6c20255578bb5bcdd7c18fd169361"},
-    {file = "rpds_py-0.17.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3664d126d3388a887db44c2e293f87d500c4184ec43d5d14d2d2babdb4c64cad"},
-    {file = "rpds_py-0.17.1-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:698ea95a60c8b16b58be9d854c9f993c639f5c214cf9ba782eca53a8789d6b19"},
-    {file = "rpds_py-0.17.1-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:c3d2010656999b63e628a3c694f23020322b4178c450dc478558a2b6ef3cb9bb"},
-    {file = "rpds_py-0.17.1-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:938eab7323a736533f015e6069a7d53ef2dcc841e4e533b782c2bfb9fb12d84b"},
-    {file = "rpds_py-0.17.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1e626b365293a2142a62b9a614e1f8e331b28f3ca57b9f05ebbf4cf2a0f0bdc5"},
-    {file = "rpds_py-0.17.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:380e0df2e9d5d5d339803cfc6d183a5442ad7ab3c63c2a0982e8c824566c5ccc"},
-    {file = "rpds_py-0.17.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b760a56e080a826c2e5af09002c1a037382ed21d03134eb6294812dda268c811"},
-    {file = "rpds_py-0.17.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5576ee2f3a309d2bb403ec292d5958ce03953b0e57a11d224c1f134feaf8c40f"},
-    {file = "rpds_py-0.17.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3c3461ebb4c4f1bbc70b15d20b565759f97a5aaf13af811fcefc892e9197ba"},
-    {file = "rpds_py-0.17.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:637b802f3f069a64436d432117a7e58fab414b4e27a7e81049817ae94de45d8d"},
-    {file = "rpds_py-0.17.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffee088ea9b593cc6160518ba9bd319b5475e5f3e578e4552d63818773c6f56a"},
-    {file = "rpds_py-0.17.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3ac732390d529d8469b831949c78085b034bff67f584559340008d0f6041a049"},
-    {file = "rpds_py-0.17.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:93432e747fb07fa567ad9cc7aaadd6e29710e515aabf939dfbed8046041346c6"},
-    {file = "rpds_py-0.17.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:7b7d9ca34542099b4e185b3c2a2b2eda2e318a7dbde0b0d83357a6d4421b5296"},
-    {file = "rpds_py-0.17.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:0387ce69ba06e43df54e43968090f3626e231e4bc9150e4c3246947567695f68"},
-    {file = "rpds_py-0.17.1.tar.gz", hash = "sha256:0210b2668f24c078307260bf88bdac9d6f1093635df5123789bfee4d8d7fc8e7"},
+    {file = "rpds_py-0.18.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:5b4e7d8d6c9b2e8ee2d55c90b59c707ca59bc30058269b3db7b1f8df5763557e"},
+    {file = "rpds_py-0.18.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c463ed05f9dfb9baebef68048aed8dcdc94411e4bf3d33a39ba97e271624f8f7"},
+    {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01e36a39af54a30f28b73096dd39b6802eddd04c90dbe161c1b8dbe22353189f"},
+    {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d62dec4976954a23d7f91f2f4530852b0c7608116c257833922a896101336c51"},
+    {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd18772815d5f008fa03d2b9a681ae38d5ae9f0e599f7dda233c439fcaa00d40"},
+    {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:923d39efa3cfb7279a0327e337a7958bff00cc447fd07a25cddb0a1cc9a6d2da"},
+    {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39514da80f971362f9267c600b6d459bfbbc549cffc2cef8e47474fddc9b45b1"},
+    {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a34d557a42aa28bd5c48a023c570219ba2593bcbbb8dc1b98d8cf5d529ab1434"},
+    {file = "rpds_py-0.18.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:93df1de2f7f7239dc9cc5a4a12408ee1598725036bd2dedadc14d94525192fc3"},
+    {file = "rpds_py-0.18.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:34b18ba135c687f4dac449aa5157d36e2cbb7c03cbea4ddbd88604e076aa836e"},
+    {file = "rpds_py-0.18.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c0b5dcf9193625afd8ecc92312d6ed78781c46ecbf39af9ad4681fc9f464af88"},
+    {file = "rpds_py-0.18.0-cp310-none-win32.whl", hash = "sha256:c4325ff0442a12113a6379af66978c3fe562f846763287ef66bdc1d57925d337"},
+    {file = "rpds_py-0.18.0-cp310-none-win_amd64.whl", hash = "sha256:7223a2a5fe0d217e60a60cdae28d6949140dde9c3bcc714063c5b463065e3d66"},
+    {file = "rpds_py-0.18.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3a96e0c6a41dcdba3a0a581bbf6c44bb863f27c541547fb4b9711fd8cf0ffad4"},
+    {file = "rpds_py-0.18.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30f43887bbae0d49113cbaab729a112251a940e9b274536613097ab8b4899cf6"},
+    {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fcb25daa9219b4cf3a0ab24b0eb9a5cc8949ed4dc72acb8fa16b7e1681aa3c58"},
+    {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d68c93e381010662ab873fea609bf6c0f428b6d0bb00f2c6939782e0818d37bf"},
+    {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b34b7aa8b261c1dbf7720b5d6f01f38243e9b9daf7e6b8bc1fd4657000062f2c"},
+    {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2e6d75ab12b0bbab7215e5d40f1e5b738aa539598db27ef83b2ec46747df90e1"},
+    {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b8612cd233543a3781bc659c731b9d607de65890085098986dfd573fc2befe5"},
+    {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aec493917dd45e3c69d00a8874e7cbed844efd935595ef78a0f25f14312e33c6"},
+    {file = "rpds_py-0.18.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:661d25cbffaf8cc42e971dd570d87cb29a665f49f4abe1f9e76be9a5182c4688"},
+    {file = "rpds_py-0.18.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1df3659d26f539ac74fb3b0c481cdf9d725386e3552c6fa2974f4d33d78e544b"},
+    {file = "rpds_py-0.18.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1ce3ba137ed54f83e56fb983a5859a27d43a40188ba798993812fed73c70836"},
+    {file = "rpds_py-0.18.0-cp311-none-win32.whl", hash = "sha256:69e64831e22a6b377772e7fb337533c365085b31619005802a79242fee620bc1"},
+    {file = "rpds_py-0.18.0-cp311-none-win_amd64.whl", hash = "sha256:998e33ad22dc7ec7e030b3df701c43630b5bc0d8fbc2267653577e3fec279afa"},
+    {file = "rpds_py-0.18.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7f2facbd386dd60cbbf1a794181e6aa0bd429bd78bfdf775436020172e2a23f0"},
+    {file = "rpds_py-0.18.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1d9a5be316c15ffb2b3c405c4ff14448c36b4435be062a7f578ccd8b01f0c4d8"},
+    {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd5bf1af8efe569654bbef5a3e0a56eca45f87cfcffab31dd8dde70da5982475"},
+    {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5417558f6887e9b6b65b4527232553c139b57ec42c64570569b155262ac0754f"},
+    {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:56a737287efecafc16f6d067c2ea0117abadcd078d58721f967952db329a3e5c"},
+    {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8f03bccbd8586e9dd37219bce4d4e0d3ab492e6b3b533e973fa08a112cb2ffc9"},
+    {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4457a94da0d5c53dc4b3e4de1158bdab077db23c53232f37a3cb7afdb053a4e3"},
+    {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0ab39c1ba9023914297dd88ec3b3b3c3f33671baeb6acf82ad7ce883f6e8e157"},
+    {file = "rpds_py-0.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9d54553c1136b50fd12cc17e5b11ad07374c316df307e4cfd6441bea5fb68496"},
+    {file = "rpds_py-0.18.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0af039631b6de0397ab2ba16eaf2872e9f8fca391b44d3d8cac317860a700a3f"},
+    {file = "rpds_py-0.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:84ffab12db93b5f6bad84c712c92060a2d321b35c3c9960b43d08d0f639d60d7"},
+    {file = "rpds_py-0.18.0-cp312-none-win32.whl", hash = "sha256:685537e07897f173abcf67258bee3c05c374fa6fff89d4c7e42fb391b0605e98"},
+    {file = "rpds_py-0.18.0-cp312-none-win_amd64.whl", hash = "sha256:e003b002ec72c8d5a3e3da2989c7d6065b47d9eaa70cd8808b5384fbb970f4ec"},
+    {file = "rpds_py-0.18.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:08f9ad53c3f31dfb4baa00da22f1e862900f45908383c062c27628754af2e88e"},
+    {file = "rpds_py-0.18.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0013fe6b46aa496a6749c77e00a3eb07952832ad6166bd481c74bda0dcb6d58"},
+    {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e32a92116d4f2a80b629778280103d2a510a5b3f6314ceccd6e38006b5e92dcb"},
+    {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e541ec6f2ec456934fd279a3120f856cd0aedd209fc3852eca563f81738f6861"},
+    {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bed88b9a458e354014d662d47e7a5baafd7ff81c780fd91584a10d6ec842cb73"},
+    {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2644e47de560eb7bd55c20fc59f6daa04682655c58d08185a9b95c1970fa1e07"},
+    {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e8916ae4c720529e18afa0b879473049e95949bf97042e938530e072fde061d"},
+    {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:465a3eb5659338cf2a9243e50ad9b2296fa15061736d6e26240e713522b6235c"},
+    {file = "rpds_py-0.18.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ea7d4a99f3b38c37eac212dbd6ec42b7a5ec51e2c74b5d3223e43c811609e65f"},
+    {file = "rpds_py-0.18.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:67071a6171e92b6da534b8ae326505f7c18022c6f19072a81dcf40db2638767c"},
+    {file = "rpds_py-0.18.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:41ef53e7c58aa4ef281da975f62c258950f54b76ec8e45941e93a3d1d8580594"},
+    {file = "rpds_py-0.18.0-cp38-none-win32.whl", hash = "sha256:fdea4952db2793c4ad0bdccd27c1d8fdd1423a92f04598bc39425bcc2b8ee46e"},
+    {file = "rpds_py-0.18.0-cp38-none-win_amd64.whl", hash = "sha256:7cd863afe7336c62ec78d7d1349a2f34c007a3cc6c2369d667c65aeec412a5b1"},
+    {file = "rpds_py-0.18.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:5307def11a35f5ae4581a0b658b0af8178c65c530e94893345bebf41cc139d33"},
+    {file = "rpds_py-0.18.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:77f195baa60a54ef9d2de16fbbfd3ff8b04edc0c0140a761b56c267ac11aa467"},
+    {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39f5441553f1c2aed4de4377178ad8ff8f9d733723d6c66d983d75341de265ab"},
+    {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a00312dea9310d4cb7dbd7787e722d2e86a95c2db92fbd7d0155f97127bcb40"},
+    {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f2fc11e8fe034ee3c34d316d0ad8808f45bc3b9ce5857ff29d513f3ff2923a1"},
+    {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:586f8204935b9ec884500498ccc91aa869fc652c40c093bd9e1471fbcc25c022"},
+    {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddc2f4dfd396c7bfa18e6ce371cba60e4cf9d2e5cdb71376aa2da264605b60b9"},
+    {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ddcba87675b6d509139d1b521e0c8250e967e63b5909a7e8f8944d0f90ff36f"},
+    {file = "rpds_py-0.18.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7bd339195d84439cbe5771546fe8a4e8a7a045417d8f9de9a368c434e42a721e"},
+    {file = "rpds_py-0.18.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:d7c36232a90d4755b720fbd76739d8891732b18cf240a9c645d75f00639a9024"},
+    {file = "rpds_py-0.18.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6b0817e34942b2ca527b0e9298373e7cc75f429e8da2055607f4931fded23e20"},
+    {file = "rpds_py-0.18.0-cp39-none-win32.whl", hash = "sha256:99f70b740dc04d09e6b2699b675874367885217a2e9f782bdf5395632ac663b7"},
+    {file = "rpds_py-0.18.0-cp39-none-win_amd64.whl", hash = "sha256:6ef687afab047554a2d366e112dd187b62d261d49eb79b77e386f94644363294"},
+    {file = "rpds_py-0.18.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ad36cfb355e24f1bd37cac88c112cd7730873f20fb0bdaf8ba59eedf8216079f"},
+    {file = "rpds_py-0.18.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:36b3ee798c58ace201289024b52788161e1ea133e4ac93fba7d49da5fec0ef9e"},
+    {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8a2f084546cc59ea99fda8e070be2fd140c3092dc11524a71aa8f0f3d5a55ca"},
+    {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e4461d0f003a0aa9be2bdd1b798a041f177189c1a0f7619fe8c95ad08d9a45d7"},
+    {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8db715ebe3bb7d86d77ac1826f7d67ec11a70dbd2376b7cc214199360517b641"},
+    {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:793968759cd0d96cac1e367afd70c235867831983f876a53389ad869b043c948"},
+    {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66e6a3af5a75363d2c9a48b07cb27c4ea542938b1a2e93b15a503cdfa8490795"},
+    {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6ef0befbb5d79cf32d0266f5cff01545602344eda89480e1dd88aca964260b18"},
+    {file = "rpds_py-0.18.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:1d4acf42190d449d5e89654d5c1ed3a4f17925eec71f05e2a41414689cda02d1"},
+    {file = "rpds_py-0.18.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:a5f446dd5055667aabaee78487f2b5ab72e244f9bc0b2ffebfeec79051679984"},
+    {file = "rpds_py-0.18.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:9dbbeb27f4e70bfd9eec1be5477517365afe05a9b2c441a0b21929ee61048124"},
+    {file = "rpds_py-0.18.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:22806714311a69fd0af9b35b7be97c18a0fc2826e6827dbb3a8c94eac6cf7eeb"},
+    {file = "rpds_py-0.18.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:b34ae4636dfc4e76a438ab826a0d1eed2589ca7d9a1b2d5bb546978ac6485461"},
+    {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c8370641f1a7f0e0669ddccca22f1da893cef7628396431eb445d46d893e5cd"},
+    {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c8362467a0fdeccd47935f22c256bec5e6abe543bf0d66e3d3d57a8fb5731863"},
+    {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11a8c85ef4a07a7638180bf04fe189d12757c696eb41f310d2426895356dcf05"},
+    {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b316144e85316da2723f9d8dc75bada12fa58489a527091fa1d5a612643d1a0e"},
+    {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf1ea2e34868f6fbf070e1af291c8180480310173de0b0c43fc38a02929fc0e3"},
+    {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e546e768d08ad55b20b11dbb78a745151acbd938f8f00d0cfbabe8b0199b9880"},
+    {file = "rpds_py-0.18.0-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:4901165d170a5fde6f589acb90a6b33629ad1ec976d4529e769c6f3d885e3e80"},
+    {file = "rpds_py-0.18.0-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:618a3d6cae6ef8ec88bb76dd80b83cfe415ad4f1d942ca2a903bf6b6ff97a2da"},
+    {file = "rpds_py-0.18.0-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ed4eb745efbff0a8e9587d22a84be94a5eb7d2d99c02dacf7bd0911713ed14dd"},
+    {file = "rpds_py-0.18.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6c81e5f372cd0dc5dc4809553d34f832f60a46034a5f187756d9b90586c2c307"},
+    {file = "rpds_py-0.18.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:43fbac5f22e25bee1d482c97474f930a353542855f05c1161fd804c9dc74a09d"},
+    {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d7faa6f14017c0b1e69f5e2c357b998731ea75a442ab3841c0dbbbfe902d2c4"},
+    {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:08231ac30a842bd04daabc4d71fddd7e6d26189406d5a69535638e4dcb88fe76"},
+    {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:044a3e61a7c2dafacae99d1e722cc2d4c05280790ec5a05031b3876809d89a5c"},
+    {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3f26b5bd1079acdb0c7a5645e350fe54d16b17bfc5e71f371c449383d3342e17"},
+    {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:482103aed1dfe2f3b71a58eff35ba105289b8d862551ea576bd15479aba01f66"},
+    {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1374f4129f9bcca53a1bba0bb86bf78325a0374577cf7e9e4cd046b1e6f20e24"},
+    {file = "rpds_py-0.18.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:635dc434ff724b178cb192c70016cc0ad25a275228f749ee0daf0eddbc8183b1"},
+    {file = "rpds_py-0.18.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:bc362ee4e314870a70f4ae88772d72d877246537d9f8cb8f7eacf10884862432"},
+    {file = "rpds_py-0.18.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:4832d7d380477521a8c1644bbab6588dfedea5e30a7d967b5fb75977c45fd77f"},
+    {file = "rpds_py-0.18.0.tar.gz", hash = "sha256:42821446ee7a76f5d9f71f9e33a4fb2ffd724bb3e7f93386150b61a43115788d"},
 ]
 
 [[package]]
@@ -2123,4 +2142,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.9"
-content-hash = "6927db40a8e597918bf529a95ae91d49f18f8e31beb7deb2ff75cad228012599"
+content-hash = "e5557792dd12eee8320616e335a103459d4a9c34bdb7f2bf45d8f300f22c0e4e"
diff --git a/pyproject.toml b/pyproject.toml
index 33736ea..7a3e778 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -46,6 +46,7 @@ mkdocs = "^1.4.2"
 mkdocs-minify-plugin = "^0.6.4"
 mkdocs-material = "^9.1.5"
 mike = "^1.1.2"
+pdoc = "^14.4.0"
 
 [build-system]
 requires = ["poetry-core>=1.0.0"]


Test patch:
diff --git a/dbterd/adapters/algos/test_relationship.py b/dbterd/adapters/algos/test_relationship.py
index 6940898..a979cae 100644
--- a/dbterd/adapters/algos/test_relationship.py
+++ b/dbterd/adapters/algos/test_relationship.py
@@ -1,5 +1,7 @@
 from typing import List, Tuple, Union
 
+import click
+
 from dbterd.adapters.algos import base
 from dbterd.adapters.filter import is_selected_table
 from dbterd.adapters.meta import Ref, Table
@@ -110,3 +112,37 @@ def parse(
         sorted(tables, key=lambda tbl: tbl.node_name),
         sorted(relationships, key=lambda rel: rel.name),
     )
+
+
+def find_related_nodes_by_id(
+    manifest: Union[Manifest, dict], node_unique_id: str, type: str = None, **kwargs
+) -> List[str]:
+    """Find the FK models which are related to the input model ID inclusively
+
+    given the manifest data of dbt project
+
+    Args:
+        manifest (Union[Manifest, dict]): Manifest data
+        node_unique_id (str): Manifest node unique ID
+        type (str, optional): Manifest type (local file or metadata). Defaults to None.
+
+    Raises:
+        click.BadParameter: Not Supported manifest type
+
+    Returns:
+        List[str]: Manifest nodes' unique ID
+    """
+    if type is not None:
+        raise click.BadParameter("Not supported manifest type")
+
+    rule = base.get_algo_rule(**kwargs)
+    test_nodes = base.get_test_nodes_by_rule_name(
+        manifest=manifest, rule_name=rule.get("name").lower()
+    )
+    found_nodes = [node_unique_id]
+    for test_node in test_nodes:
+        nodes = manifest.nodes[test_node].depends_on.nodes or []
+        if node_unique_id in nodes:
+            found_nodes.extend(nodes)
+
+    return list(set(found_nodes))
diff --git a/tests/unit/adapters/algos/test_test_relationship.py b/tests/unit/adapters/algos/test_test_relationship.py
index 2961ddc..2f19634 100644
--- a/tests/unit/adapters/algos/test_test_relationship.py
+++ b/tests/unit/adapters/algos/test_test_relationship.py
@@ -6,6 +6,7 @@
 import pytest
 
 from dbterd.adapters.algos import base as base_algo
+from dbterd.adapters.algos import test_relationship
 from dbterd.adapters.meta import Column, Ref, Table
 
 
@@ -864,3 +865,21 @@ def test_get_relationships_from_metadata(self, data, kwargs, expected):
     def test_get_relationships_from_metadata_error(self, data, kwargs):
         with pytest.raises(click.BadParameter):
             base_algo.get_relationships_from_metadata(data=data, **kwargs)
+
+    def test_find_related_nodes_by_id_error(self):
+        with pytest.raises(click.BadParameter):
+            test_relationship.find_related_nodes_by_id(
+                manifest="irrelevant", type="metadata", node_unique_id="irrelevant"
+            )
+
+    def test_find_related_nodes_by_id(self):
+        assert sorted(["model.dbt_resto.table1", "model.dbt_resto.table2"]) == sorted(
+            test_relationship.find_related_nodes_by_id(
+                manifest=DummyManifestRel(), node_unique_id="model.dbt_resto.table2"
+            )
+        )
+        assert [
+            "model.dbt_resto.not-exists"
+        ] == test_relationship.find_related_nodes_by_id(
+            manifest=DummyManifestRel(), node_unique_id="model.dbt_resto.not-exists"
+        )
diff --git a/tests/unit/adapters/test_adapter.py b/tests/unit/adapters/test_adapter.py
index ea4a9f8..7d0f474 100644
--- a/tests/unit/adapters/test_adapter.py
+++ b/tests/unit/adapters/test_adapter.py
@@ -1,13 +1,21 @@
 import pytest
 
 from dbterd.adapters import adapter
+from dbterd.adapters.algos import test_relationship
 from dbterd.adapters.targets import dbml
 
 
 class TestFactory:
-    def test_load_executor_failed(self):
+    def test_load_target_failed(self):
         with pytest.raises(Exception):
-            adapter.load_executor(name="dummy")
+            adapter.load_target(name="dummy-target")
 
-    def test_load_executor(self):
-        assert adapter.load_executor(name="dbml") == dbml
+    def test_load_target(self):
+        assert adapter.load_target(name="dbml") == dbml
+
+    def test_load_algo_failed(self):
+        with pytest.raises(Exception):
+            adapter.load_algo(name="dummy-algo")
+
+    def test_load_algo(self):
+        assert adapter.load_algo(name="test_relationship") == test_relationship
diff --git a/tests/unit/adapters/test_base.py b/tests/unit/adapters/test_base.py
index 79d14c3..2e5bec3 100644
--- a/tests/unit/adapters/test_base.py
+++ b/tests/unit/adapters/test_base.py
@@ -7,6 +7,7 @@
 from dbterd import default
 from dbterd.adapters.base import Executor
 from dbterd.adapters.dbt_core.dbt_invocation import DbtInvocation
+from dbterd.adapters.targets.dbml import dbml_test_relationship
 
 
 class TestBase:
@@ -215,3 +216,60 @@ 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
+
+    def test__set_single_node_selection__none_id(self):
+        worker = Executor(ctx=click.Context(command=click.BaseCommand("dummy")))
+        assert worker._Executor__set_single_node_selection(
+            manifest="irrelevant", node_unique_id=None, **dict(i="irr")
+        ) == dict(i="irr")
+
+    @mock.patch("dbterd.adapters.algos.test_relationship.find_related_nodes_by_id")
+    def test__set_single_node_selection(self, mock_find_related_nodes_by_id):
+        worker = Executor(ctx=click.Context(command=click.BaseCommand("dummy")))
+        mock_find_related_nodes_by_id.return_value = ["irr"]
+        assert worker._Executor__set_single_node_selection(
+            manifest="irrelevant",
+            node_unique_id="irrelevant",
+            **dict(algo="test_relationship"),
+        ) == dict(algo="test_relationship", select=["irr"], exclude=[])
+        assert mock_find_related_nodes_by_id.call_count == 1
+
+    @mock.patch("dbterd.adapters.targets.dbml.dbml_test_relationship.run")
+    @mock.patch("dbterd.adapters.base.Executor._Executor__get_operation")
+    @mock.patch("dbterd.adapters.base.Executor._Executor__read_manifest")
+    @mock.patch("dbterd.adapters.base.Executor._Executor__read_catalog")
+    @mock.patch("dbterd.adapters.base.Executor._Executor__set_single_node_selection")
+    def test__run_by_strategy__for_api_simple(
+        self,
+        mock_set_single_node_selection,
+        mock_read_catalog,
+        mock_read_manifest,
+        mock_get_operation,
+        mock_dbml_run,
+    ):
+        worker = Executor(ctx=click.Context(command=click.BaseCommand("dummy")))
+
+        mock_parent = mock.Mock()
+        mock_get_operation.return_value = dbml_test_relationship.run
+        mock_parent.attach_mock(mock_get_operation, "mock_get_operation")
+        mock_read_catalog.return_value = dict()
+        mock_parent.attach_mock(mock_read_catalog, "mock_read_catalog")
+        mock_read_manifest.return_value = dict()
+        mock_parent.attach_mock(mock_read_manifest, "mock_read_manifest")
+        mock_set_single_node_selection.return_value = dict(api=True)
+        mock_parent.attach_mock(
+            mock_set_single_node_selection, "mock_set_single_node_selection"
+        )
+        mock_dbml_run.return_value = dict(i="irr")
+        mock_parent.attach_mock(mock_dbml_run, "mock_dbml_run")
+
+        assert worker._Executor__run_by_strategy(node_unique_id="irr") == dict(i="irr")
+        assert mock_parent.mock_calls == [
+            mock.call.mock_read_manifest(mp=None, mv=None),
+            mock.call.mock_read_catalog(cp=None, cv=None),
+            mock.call.mock_set_single_node_selection(
+                manifest=dict(), node_unique_id="irr"
+            ),
+            mock.call.mock_get_operation(dict(api=True)),
+            mock.call.mock_dbml_run(manifest=dict(), catalog=dict(), **dict(api=True)),
+        ]
diff --git a/tests/unit/test_api.py b/tests/unit/test_api.py
new file mode 100644
index 0000000..94b54f2
--- /dev/null
+++ b/tests/unit/test_api.py
@@ -0,0 +1,12 @@
+class TestDbtErd:
+    def test__init__(self):
+        pass  # TODO
+
+    def test__set_params_default_if_not_specified(self):
+        pass  # TODO
+
+    def test_get_erd(self):
+        pass  # TODO
+
+    def test_get_model_erd(self):
+        pass  # TODO