Repository: airtai/docstring-gen (language: python)

Issue:
Initial setup and copy docs generation notebook



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 0000000..a24d847
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,74 @@
+# For most projects, this workflow file will not need changing; you simply need
+# to commit it to your repository.
+#
+# You may wish to alter this file to override the set of languages analyzed,
+# or to provide custom queries or build logic.
+#
+# ******** NOTE ********
+# We have attempted to detect the languages in your repository. Please check
+# the `language` matrix defined below to confirm you have the correct set of
+# supported CodeQL languages.
+#
+name: "CodeQL"
+
+on:
+  push:
+    branches: [ "main" ]
+  pull_request:
+    # The branches below must be a subset of the branches above
+    branches: [ "main" ]
+  schedule:
+    - cron: '23 1 * * 5'
+
+jobs:
+  analyze:
+    name: Analyze
+    runs-on: ubuntu-latest
+    permissions:
+      actions: read
+      contents: read
+      security-events: write
+
+    strategy:
+      fail-fast: false
+      matrix:
+        language: [ 'python' ]
+        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
+        # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
+
+    steps:
+    - name: Checkout repository
+      uses: actions/checkout@v3
+
+    # Initializes the CodeQL tools for scanning.
+    - name: Initialize CodeQL
+      uses: github/codeql-action/init@v2
+      with:
+        languages: ${{ matrix.language }}
+        # If you wish to specify custom queries, you can do so here or in a config file.
+        # By default, queries listed here will override any specified in a config file.
+        # Prefix the list here with "+" to use these queries and those in the config file.
+
+        # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
+        # queries: security-extended,security-and-quality
+
+
+    # Autobuild attempts to build any compiled languages  (C/C++, C#, Go, or Java).
+    # If this step fails, then you should remove it and run the build manually (see below)
+    - name: Autobuild
+      uses: github/codeql-action/autobuild@v2
+
+    # ℹ️ Command-line programs to run using the OS shell.
+    # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
+
+    #   If the Autobuild fails above, remove it and uncomment the following three lines.
+    #   modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
+
+    # - run: |
+    #   echo "Run, Build Application using script"
+    #   ./location_of_script_within_repo/buildscript.sh
+
+    - name: Perform CodeQL Analysis
+      uses: github/codeql-action/analyze@v2
+      with:
+        category: "/language:${{matrix.language}}"
diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml
new file mode 100644
index 0000000..fe461b4
--- /dev/null
+++ b/.github/workflows/dependency-review.yml
@@ -0,0 +1,20 @@
+# Dependency Review Action
+#
+# This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging.
+#
+# Source repository: https://github.com/actions/dependency-review-action
+# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement
+name: 'Dependency Review'
+on: [pull_request]
+
+permissions:
+  contents: read
+
+jobs:
+  dependency-review:
+    runs-on: ubuntu-latest
+    steps:
+      - name: 'Checkout Repository'
+        uses: actions/checkout@v3
+      - name: 'Dependency Review'
+        uses: actions/dependency-review-action@v2
diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml
new file mode 100644
index 0000000..bb7e4ef
--- /dev/null
+++ b/.github/workflows/deploy.yaml
@@ -0,0 +1,11 @@
+name: Deploy nbdev-mkdocs generated documentation to GitHub Pages
+
+on:
+  push:
+    branches: [ "main", "master" ]
+  workflow_dispatch:
+jobs:
+  deploy:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: airtai/workflows/nbdev-mkdocs-ghp@main
diff --git a/.gitignore b/.gitignore
index b6e4761..0aa7478 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,34 @@
+token
+
+.sast_bandit
+.sast_semgrep
+.semgrepignore
+.local_install
+.local_reinstall
+.secret.baseline
+
+# nbs related
+nbs/.last_checked
+CHANGELOG.bak
+
+# Docs related
+mkdocs/docs/
+mkdocs/site/
+
+_publish.yml
+/_quarto.yml
+_proc/
+tests/settings.ini
+idx/
+.install
+conda/
+_site/
+_docs/
+test_settings.ini
+
+# Mac os related files
+.DS_Store
+
 # Byte-compiled / optimized / DLL files
 __pycache__/
 *.py[cod]
@@ -127,3 +158,14 @@ dmypy.json
 
 # Pyre type checker
 .pyre/
+
+
+# nbdev_mkdocs
+mkdocs/docs/
+mkdocs/site/
+
+
+
+# nbdev_mkdocs
+mkdocs/docs/
+mkdocs/site/
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..48087d4
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,8 @@
+# Release notes
+
+<!-- do not remove -->
+
+## 0.0.1rc0
+
+
+
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..5c0e7ce
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,5 @@
+include settings.ini
+include LICENSE
+include CONTRIBUTING.md
+include README.md
+recursive-exclude * __pycache__
diff --git a/README.md b/README.md
index c69412c..b991ebb 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,23 @@
-# docstring-generator
-Generator for docstrings
+docstring-gen
+================
+
+<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->
+
+This file will become your README and also the index of your
+documentation.
+
+## Install
+
+``` sh
+pip install docstring_gen
+```
+
+## How to use
+
+Fill me in please! Don’t forget code examples:
+
+``` python
+1+1
+```
+
+    2
diff --git a/docstring_gen/__init__.py b/docstring_gen/__init__.py
new file mode 100644
index 0000000..16f1bc8
--- /dev/null
+++ b/docstring_gen/__init__.py
@@ -0,0 +1,1 @@
+__version__ = "0.0.1rc0"
diff --git a/docstring_gen/_modidx.py b/docstring_gen/_modidx.py
new file mode 100644
index 0000000..f3d89c9
--- /dev/null
+++ b/docstring_gen/_modidx.py
@@ -0,0 +1,25 @@
+# Autogenerated by nbdev
+
+d = { 'settings': { 'branch': 'main',
+                'doc_baseurl': '/docstring-gen',
+                'doc_host': 'https://airtai.github.io',
+                'git_url': 'https://github.com/airtai/docstring-gen',
+                'lib_path': 'docstring_gen'},
+  'syms': { 'docstring_gen.docstring_generator': { 'docstring_gen.docstring_generator._add_docstring': ( 'docstring_generator.html#_add_docstring',
+                                                                                                         'docstring_gen/docstring_generator.py'),
+                                                   'docstring_gen.docstring_generator._calculate_end_lineno': ( 'docstring_generator.html#_calculate_end_lineno',
+                                                                                                                'docstring_gen/docstring_generator.py'),
+                                                   'docstring_gen.docstring_generator._check_and_add_docstrings_to_source': ( 'docstring_generator.html#_check_and_add_docstrings_to_source',
+                                                                                                                              'docstring_gen/docstring_generator.py'),
+                                                   'docstring_gen.docstring_generator._generate_docstring_using_codex': ( 'docstring_generator.html#_generate_docstring_using_codex',
+                                                                                                                          'docstring_gen/docstring_generator.py'),
+                                                   'docstring_gen.docstring_generator._get_code_from_source': ( 'docstring_generator.html#_get_code_from_source',
+                                                                                                                'docstring_gen/docstring_generator.py'),
+                                                   'docstring_gen.docstring_generator._get_start_line_for_class_or_func': ( 'docstring_generator.html#_get_start_line_for_class_or_func',
+                                                                                                                            'docstring_gen/docstring_generator.py'),
+                                                   'docstring_gen.docstring_generator._inject_docstring_to_source': ( 'docstring_generator.html#_inject_docstring_to_source',
+                                                                                                                      'docstring_gen/docstring_generator.py'),
+                                                   'docstring_gen.docstring_generator._line_has_class_or_method': ( 'docstring_generator.html#_line_has_class_or_method',
+                                                                                                                    'docstring_gen/docstring_generator.py'),
+                                                   'docstring_gen.docstring_generator.add_docstring_to_notebook': ( 'docstring_generator.html#add_docstring_to_notebook',
+                                                                                                                    'docstring_gen/docstring_generator.py')}}}
diff --git a/docstring_gen/docstring_generator.py b/docstring_gen/docstring_generator.py
new file mode 100644
index 0000000..f2e8dca
--- /dev/null
+++ b/docstring_gen/docstring_generator.py
@@ -0,0 +1,232 @@
+# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/Docstring_Generator.ipynb.
+
+# %% auto 0
+__all__ = ['add_docstring_to_notebook']
+
+# %% ../nbs/Docstring_Generator.ipynb 2
+import ast
+import tokenize
+from typing import *
+from pathlib import Path
+from io import BytesIO
+
+import nbformat
+
+# %% ../nbs/Docstring_Generator.ipynb 5
+def _generate_docstring_using_codex(code: str) -> str:
+    return """Sample docstring
+
+    Args:
+        s: sample args
+
+    Returns:
+        sample return
+"""
+
+# %% ../nbs/Docstring_Generator.ipynb 6
+def _inject_docstring_to_source(
+    source: str, docstring: str, lineno: int, node_col_offset: int
+) -> str:
+    """Inject a docstring into the source code at a specified line number.
+
+    Args:
+        source: the source code
+        docstring: the docstring to be added
+        lineno: the line number at which the docstring will be inserted
+        node_col_offset: the number of spaces to indent the docstring
+
+    Returns:
+        The updated source code with the docstring injected
+    """
+    lines = source.split("\n")
+    indented_docstring = "\n".join(
+        [
+            line
+            if i == 0 or i == len(docstring.split("\n")) - 1
+            else f"{' ' * node_col_offset}{line}"
+            for i, line in enumerate(docstring.split("\n"))
+        ]
+    )
+    indent = node_col_offset + 4
+    lines.insert(lineno, f'{" " * indent}"""{indented_docstring}{" " * indent}"""')
+    return "\n".join(lines)
+
+# %% ../nbs/Docstring_Generator.ipynb 8
+def _get_code_from_source(source: str, start_line_no: int, end_line_no: int) -> str:
+    """Get a block of lines from a given source string.
+
+    Args:
+        source: The source string.
+        start_line_no: The line number of the start of the block of lines.
+        end_line_no: The line number of the end of the block of lines.
+
+    Returns:
+        The extracted block of lines from the source
+    """
+    source_lines = source.split("\n")
+    extracted_lines = source_lines[start_line_no - 1 : end_line_no]
+    return "\n".join(extracted_lines)
+
+# %% ../nbs/Docstring_Generator.ipynb 10
+def _calculate_end_lineno(source: str, start_line_no: int) -> int:
+    """Calculate the end line number of a function in a Python source code.
+
+    Args:
+        source: The source code string.
+        start_line_no: The line number of the start of the function.
+
+    Returns:
+        The end line number of the function.
+    """
+    lines = source.split("\n")[start_line_no - 1 :]
+    first_indent = len(lines[0]) - len(lines[0].lstrip())
+    end_line_in_source = 0
+
+    for i, line in enumerate(lines[1:]):
+        if len(line) - len(line.lstrip()) == first_indent and line.strip() != "":
+            end_line_in_source = i
+            break
+
+    ret_val = (
+        len(source.split("\n"))
+        if end_line_in_source == 0
+        else end_line_in_source + start_line_no
+    )
+    return ret_val - 1
+
+# %% ../nbs/Docstring_Generator.ipynb 12
+def _line_has_class_or_method(source: str, lineno: int) -> bool:
+    """Check if a line in the source code contains a class or method definition.
+
+    Args:
+        source: The source code as a string.
+        lineno: The line number to check.
+
+    Returns:
+        True if the line contains a class or method definition, False otherwise.
+    """
+    line = "".join(source.split("\n")[lineno - 1])
+    tokens = list(tokenize.tokenize(BytesIO(line.encode("utf-8")).readline))
+    return tokens[1].type == tokenize.NAME and tokens[1].string in {
+        "class",
+        "def",
+        "async",
+    }
+
+
+def _get_start_line_for_class_or_func(source: str, lineno: int) -> int:
+    """Get the line number of the first line containing a class or function definition.
+
+    Args:
+        source: The source code as a string.
+        lineno: The line number to start from.
+
+    Returns:
+        The line number of the first line containing a class or function definition,
+        or the original line number if no such line is found.
+    """
+    if _line_has_class_or_method(source, lineno):
+        return lineno
+
+    original_lineno = lineno
+    total_lines = source.split("\n")
+    for i in total_lines:
+        lineno += 1
+        if lineno > len(total_lines):
+            break
+        if _line_has_class_or_method(source, lineno):
+            return lineno
+    return original_lineno
+
+# %% ../nbs/Docstring_Generator.ipynb 15
+def _add_docstring(
+    source: str,
+    node: Union[ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef],
+    line_offset: int,
+) -> Tuple[str, int]:
+    """Add a docstring to the given node and update the source code.
+
+    Args:
+        source: the source code from the notebook cell
+        node: the AST node representing a class definition, function definition,
+            or async function definition
+        line_offset: the number of lines added before the current
+            node in the source
+
+    Returns:
+        A tuple containing the updated source code and the new line number offset
+    """
+    line_no = node.lineno + line_offset
+
+    # Fix for ast's node.lineno giving line number of decorator
+    # instead of function/class definition in Python 3.7
+    line_no = _get_start_line_for_class_or_func(source, line_no)
+
+    if hasattr(node, "end_lineno") and node.end_lineno is not None:
+        end_line_no = node.end_lineno + line_offset
+    else:
+        end_line_no = _calculate_end_lineno(source, line_no)
+
+    code = _get_code_from_source(source, line_no, end_line_no)
+    docstring = _generate_docstring_using_codex(code)
+
+    source = _inject_docstring_to_source(source, docstring, line_no, node.col_offset)
+    line_offset += len(docstring.split("\n"))
+    return source, line_offset
+
+# %% ../nbs/Docstring_Generator.ipynb 17
+def _check_and_add_docstrings_to_source(source: str) -> str:
+    """Check for missing docstrings in the source code and add them if necessary.
+
+    Args:
+        source: the source code from the notebook cell
+
+    Returns:
+        The updated source code with added docstrings
+    """
+
+    tree = ast.parse(source)
+    line_offset = 0
+
+    for node in tree.body:
+        if not isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
+            continue
+        if ast.get_docstring(node) is not None:
+            continue
+
+        # A class or a function without docstring
+        source, line_offset = _add_docstring(source, node, line_offset)
+        if not isinstance(node, ast.ClassDef):
+            continue
+        # Is a class and we need to check the functions inside
+        # 29 - 36 make it as a recursive function
+        for f in node.body:
+            if not isinstance(f, (ast.FunctionDef, ast.AsyncFunctionDef)):
+                continue
+            if ast.get_docstring(f) is not None:
+                continue
+
+            # should be a function inside the class for which there is no docstring
+            source, line_offset = _add_docstring(source, f, line_offset)
+
+    return source
+
+# %% ../nbs/Docstring_Generator.ipynb 19
+def add_docstring_to_notebook(nb_path: Union[str, Path], version: int = 4):
+    """Add docstrings to the source
+
+    This function reads through a Jupyter notebook cell by cell and
+    adds docstrings for classes and methods that do not have them.
+
+    Args:
+        nb_path: The notebook file path
+        version: The version of the Jupyter notebook format
+    """
+    nb_path = Path(nb_path)
+    nb = nbformat.read(nb_path, as_version=version)
+
+    for cell in nb.cells:
+        if cell.cell_type == "code":
+            cell["source"] = _check_and_add_docstrings_to_source(cell["source"])
+
+    nbformat.write(nb, nb_path)
diff --git a/fixtures/Test_Data.ipynb b/fixtures/Test_Data.ipynb
new file mode 100644
index 0000000..01b9fa3
--- /dev/null
+++ b/fixtures/Test_Data.ipynb
@@ -0,0 +1,194 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "id": "d2c32d11",
+   "metadata": {},
+   "source": [
+    "# Test notebook for docstring generator\n",
+    "\n",
+    "> Test notebook for docstring generator"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "0227401e",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# | export\n",
+    "\n",
+    "from typing import *\n",
+    "import os\n",
+    "from pathlib import Path\n",
+    "# from contextlib import contextmanager\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "f632424c",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from contextlib import contextmanager\n",
+    "\n",
+    "import shutil\n",
+    "from tempfile import TemporaryDirectory\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "b3a459a8",
+   "metadata": {},
+   "source": [
+    "# Title"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "69511ac7",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# | export\n",
+    "\n",
+    "# Vehicle class\n",
+    "class Vehicle:\n",
+    "    # Constructor function\n",
+    "    def __init__(self, brand, model, type):\n",
+    "        \"\"\"Constructor function\n",
+    "        \n",
+    "        Args:\n",
+    "            brand: Vehicle's brand\n",
+    "            model: Vehicle's model\n",
+    "            type: Vehicle's type\n",
+    "        \"\"\"\n",
+    "        self.brand = brand\n",
+    "        self.model = model\n",
+    "        self.type = type\n",
+    "        self.gas_tank_size = 14\n",
+    "        self.fuel_level = 0\n",
+    "    \n",
+    "    # fuel_up function\n",
+    "    def fuel_up(self):\n",
+    "        # comment goes here\n",
+    "        self.fuel_level = self.gas_tank_size\n",
+    "        return 'Gas tank is now full.'\n",
+    "\n",
+    "    # drive function\n",
+    "    async def drive(self):\n",
+    "        print(f'The {self.model} is now driving.')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "ca9f5c9a",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Gas tank is now full.\n"
+     ]
+    }
+   ],
+   "source": [
+    "v = Vehicle(\"honda\", \"city\", \"hatchback\")\n",
+    "actual = v.fuel_up()\n",
+    "\n",
+    "print(actual)\n",
+    "assert actual == 'Gas tank is now full.'"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "fa0de4a6",
+   "metadata": {},
+   "source": [
+    "\n",
+    "This is a sample markdown cell. The texts goes here\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "d420f665",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def say_hello(name: str) -> str:\n",
+    "    return f\"Hello, {name}\"\n",
+    "    \n",
+    "async def function_with_docstring(name: str) -> str:\n",
+    "    \"\"\" This function already has docstring\"\"\"\n",
+    "    return f\"I won't say hello, {name}\"\n",
+    "    \n",
+    "async def dont_say_hello(name: str) -> str:\n",
+    "    return f\"I won't say hello, {name}\"\n",
+    "    "
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "a09dce5e",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def decorator1(func):\n",
+    "    def inner():\n",
+    "        func()\n",
+    "    return inner\n",
+    "\n",
+    "def decorator2(func):\n",
+    "    def inner():\n",
+    "        func()\n",
+    "    return inner\n",
+    "\n",
+    "@decorator1\n",
+    "@decorator2\n",
+    "def outer_func():\n",
+    "    def inner_func():\n",
+    "        print(\"Hello, World!\")\n",
+    "    inner_func()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "0f15cbf9",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "@contextmanager\n",
+    "def my_context_mgr():\n",
+    "    try:\n",
+    "        yield\n",
+    "    finally:\n",
+    "        pass"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "67848556",
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/mkdocs/docs_overrides/css/extra.css b/mkdocs/docs_overrides/css/extra.css
new file mode 100644
index 0000000..e69de29
diff --git a/mkdocs/docs_overrides/images/compass-outline.png b/mkdocs/docs_overrides/images/compass-outline.png
new file mode 100644
index 0000000..17125bc
Binary files /dev/null and b/mkdocs/docs_overrides/images/compass-outline.png differ
diff --git a/mkdocs/docs_overrides/images/default_social_logo.png b/mkdocs/docs_overrides/images/default_social_logo.png
new file mode 100644
index 0000000..d53cbad
Binary files /dev/null and b/mkdocs/docs_overrides/images/default_social_logo.png differ
diff --git a/mkdocs/docs_overrides/js/extra.js b/mkdocs/docs_overrides/js/extra.js
new file mode 100644
index 0000000..e69de29
diff --git a/mkdocs/docs_overrides/js/mathjax.js b/mkdocs/docs_overrides/js/mathjax.js
new file mode 100644
index 0000000..1d586b3
--- /dev/null
+++ b/mkdocs/docs_overrides/js/mathjax.js
@@ -0,0 +1,18 @@
+window.MathJax = {
+  tex: {
+    inlineMath: [["\\(", "\\)"]],
+    displayMath: [["\\[", "\\]"]],
+    processEscapes: true,
+    processEnvironments: true
+  },
+  options: {
+    ignoreHtmlClass: ".*|",
+    processHtmlClass: "arithmatex"
+  }
+};
+
+document$.subscribe(() => {
+  MathJax.typesetPromise()
+})
+
+
diff --git a/mkdocs/mkdocs.yml b/mkdocs/mkdocs.yml
new file mode 100644
index 0000000..2d4f7f9
--- /dev/null
+++ b/mkdocs/mkdocs.yml
@@ -0,0 +1,104 @@
+# Site
+site_name: docstring-gen
+site_url: https://airtai.github.io/docstring-gen
+site_author: airt
+site_description: This project generates missing docstrings for classes and methods
+  using OpenAI's Codex model.
+
+# Repository
+repo_name: docstring-gen
+repo_url: https://github.com/airtai/docstring-gen
+edit_uri: ''
+
+copyright: Copyright © 2023 onwards airt technologies ltd.
+
+docs_dir: docs
+site_dir: site
+
+plugins:
+- literate-nav:
+    nav_file: SUMMARY.md
+- search
+- mkdocstrings:
+    handlers:
+      python:
+        import:
+        - https://docs.python.org/3/objects.inv
+        options:
+          heading_level: 2
+          show_category_heading: true
+          show_root_heading: true
+          show_root_toc_entry: true
+          show_signature_annotations: true
+          show_if_no_docstring: true
+
+markdown_extensions:
+- md_in_html
+- pymdownx.arithmatex:
+    generic: true
+- pymdownx.inlinehilite
+- pymdownx.details
+- pymdownx.emoji
+- pymdownx.magiclink
+- pymdownx.superfences:
+    custom_fences:
+    - name: mermaid
+      class: mermaid
+      format: !!python/name:pymdownx.superfences.fence_code_format
+- pymdownx.tasklist
+- pymdownx.highlight:
+    linenums: false
+- pymdownx.snippets:
+    check_paths: true
+- pymdownx.tabbed:
+    alternate_style: true
+- admonition
+- toc:
+    permalink: ¤
+#     - callouts
+theme:
+  name: material
+  custom_dir: site_overrides
+  features:
+  - navigation.instant
+#     - navigation.tabs
+#     - navigation.tabs.sticky
+#     - navigation.sections
+#     - navigation.expand
+  - navigation.indexes
+  - navigation.top
+#     - toc.integrates
+  - search.suggest
+  - search.highlight
+  - search.share
+  palette:
+  - scheme: slate
+    primary: black
+    accent: yellow
+    toggle:
+      icon: material/toggle-switch
+      name: Switch to light mode
+  - scheme: default
+    primary: black   # deep orange
+    accent: red
+    toggle:
+      icon: material/toggle-switch-off-outline
+      name: Switch to dark mode
+  icon:
+    repo: fontawesome/brands/github
+#     repo: fontawesome/brands/gitlab
+    logo: material/compass-outline
+#     admonition:
+#         <type>: <icon> 
+  favicon: overrides/images/compass-outline.png
+
+extra_css:
+- overrides/css/extra.css
+
+extra_javascript:
+- overrides/js/extra.js
+- overrides/js/mathjax.js
+- https://polyfill.io/v3/polyfill.min.js?features=es6
+- https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js
+extra:
+  social_image: https://opengraph.githubassets.com/1672891972.013074/airtai/docstring-gen
diff --git a/mkdocs/site_overrides/main.html b/mkdocs/site_overrides/main.html
new file mode 100644
index 0000000..1b33d05
--- /dev/null
+++ b/mkdocs/site_overrides/main.html
@@ -0,0 +1,25 @@
+{% extends "base.html" %}
+
+{% block extrahead %}
+  {% set title = config.site_name %}
+  {% if page and page.meta and page.meta.title %}
+    {% set title = title ~ " - " ~ page.meta.title %}
+  {% elif page and page.title and not page.is_homepage %}
+    {% set title = title ~ " - " ~ page.title | striptags %}
+  {% endif %}
+  {% set image_url = config.extra.social_image %}
+  <meta property="og:type" content="website" />
+  <meta property="og:title" content="{{ title }}" />
+  <meta property="og:description" content="{{ config.site_description }}" />
+  <meta property="og:url" content="{{ page.canonical_url }}" />
+  <meta property="og:image" content="{{ image_url }}" />
+  <meta property="og:image:type" content="image/png" />
+  <meta property="og:image:width" content="1200" />
+  <meta property="og:image:height" content="630" />
+
+  <meta name="twitter:card" content="summary_large_image" />
+  <meta name="twitter:title" content="{{ title }}" />
+  <meta name="twitter:description" content="{{ config.site_description }}" />
+  <meta name="twitter:image" content="{{ image_url }}" />
+{% endblock %}
+
diff --git a/mkdocs/site_overrides/partials/copyright.html b/mkdocs/site_overrides/partials/copyright.html
new file mode 100644
index 0000000..c06bae1
--- /dev/null
+++ b/mkdocs/site_overrides/partials/copyright.html
@@ -0,0 +1,17 @@
+<div class="md-copyright">
+  {% if config.copyright %}
+    <div class="md-copyright__highlight">
+      {{ config.copyright }}
+    </div>
+  {% endif %}
+  {% if not config.extra.generator == false %}
+    Made with
+    <a href="https://squidfunk.github.io/mkdocs-material/" target="_blank" rel="noopener">
+      Material for MkDocs
+    </a>
+    and 
+    <a href="https://nbdev-mkdocs.airt.ai/" target="_blank" rel="noopener">
+      Material for nbdev
+    </a>
+  {% endif %}
+</div>
diff --git a/mkdocs/summary_template.txt b/mkdocs/summary_template.txt
new file mode 100644
index 0000000..695eb25
--- /dev/null
+++ b/mkdocs/summary_template.txt
@@ -0,0 +1,6 @@
+{sidebar}
+- API
+{api}
+- CLI
+{cli}
+- [Releases]{changelog}
diff --git a/mypy.ini b/mypy.ini
new file mode 100644
index 0000000..4c872a5
--- /dev/null
+++ b/mypy.ini
@@ -0,0 +1,19 @@
+# Global options:
+
+[mypy]
+ignore_missing_imports = True
+install_types = True
+non_interactive = True
+
+# from https://blog.wolt.com/engineering/2021/09/30/professional-grade-mypy-configuration/
+# disallow_untyped_defs = True
+no_implicit_optional = True
+check_untyped_defs = True
+warn_return_any = True
+show_error_codes = True 
+warn_unused_ignores = True
+
+#disallow_incomplete_defs = True
+#disallow_untyped_decorators = True
+disallow_any_unimported = True
+
diff --git a/nbs/Docstring_Generator.ipynb b/nbs/Docstring_Generator.ipynb
new file mode 100644
index 0000000..c1c6f9f
--- /dev/null
+++ b/nbs/Docstring_Generator.ipynb
@@ -0,0 +1,1066 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "2ae108fa",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# | default_exp docstring_generator"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "8b32fc4f",
+   "metadata": {},
+   "source": [
+    "# Docstring Generator"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "63941987",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# | export\n",
+    "\n",
+    "import ast\n",
+    "import tokenize\n",
+    "from typing import *\n",
+    "from pathlib import Path\n",
+    "from io import BytesIO\n",
+    "\n",
+    "import nbformat"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "fab9b3ba",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import shutil\n",
+    "from tempfile import TemporaryDirectory"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "602a7fff",
+   "metadata": {},
+   "source": [
+    "## Docstring generator functions"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "6cdabd36",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# | export\n",
+    "\n",
+    "\n",
+    "def _generate_docstring_using_codex(code: str) -> str:\n",
+    "    return \"\"\"Sample docstring\n",
+    "\n",
+    "    Args:\n",
+    "        s: sample args\n",
+    "\n",
+    "    Returns:\n",
+    "        sample return\n",
+    "\"\"\""
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "f0c01fc7",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# | export\n",
+    "\n",
+    "\n",
+    "def _inject_docstring_to_source(\n",
+    "    source: str, docstring: str, lineno: int, node_col_offset: int\n",
+    ") -> str:\n",
+    "    \"\"\"Inject a docstring into the source code at a specified line number.\n",
+    "\n",
+    "    Args:\n",
+    "        source: the source code\n",
+    "        docstring: the docstring to be added\n",
+    "        lineno: the line number at which the docstring will be inserted\n",
+    "        node_col_offset: the number of spaces to indent the docstring\n",
+    "\n",
+    "    Returns:\n",
+    "        The updated source code with the docstring injected\n",
+    "    \"\"\"\n",
+    "    lines = source.split(\"\\n\")\n",
+    "    indented_docstring = \"\\n\".join(\n",
+    "        [\n",
+    "            line\n",
+    "            if i == 0 or i == len(docstring.split(\"\\n\")) - 1\n",
+    "            else f\"{' ' * node_col_offset}{line}\"\n",
+    "            for i, line in enumerate(docstring.split(\"\\n\"))\n",
+    "        ]\n",
+    "    )\n",
+    "    indent = node_col_offset + 4\n",
+    "    lines.insert(lineno, f'{\" \" * indent}\"\"\"{indented_docstring}{\" \" * indent}\"\"\"')\n",
+    "    return \"\\n\".join(lines)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "3af581c1",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "\n",
+      "async def drive(self):\n",
+      "    \"\"\"Sample docstring\n",
+      "\n",
+      "    Args:\n",
+      "        s: sample args\n",
+      "\n",
+      "    Returns:\n",
+      "        sample return\n",
+      "    \"\"\"\n",
+      "    print(f'The {self.model} is now driving.')\n",
+      "\n"
+     ]
+    }
+   ],
+   "source": [
+    "source = \"\"\"\n",
+    "async def drive(self):\n",
+    "    print(f'The {self.model} is now driving.')\n",
+    "\"\"\"\n",
+    "\n",
+    "docstring = \"\"\"Sample docstring\n",
+    "\n",
+    "    Args:\n",
+    "        s: sample args\n",
+    "\n",
+    "    Returns:\n",
+    "        sample return\n",
+    "\"\"\"\n",
+    "\n",
+    "expected = \"\"\"\n",
+    "async def drive(self):\n",
+    "    \\\"\"\"Sample docstring\n",
+    "\n",
+    "    Args:\n",
+    "        s: sample args\n",
+    "\n",
+    "    Returns:\n",
+    "        sample return\n",
+    "    \\\"\"\"\n",
+    "    print(f'The {self.model} is now driving.')\n",
+    "\"\"\"\n",
+    "\n",
+    "lineno = 2\n",
+    "node_col_offset = 0\n",
+    "actual = _inject_docstring_to_source(source, docstring, lineno, node_col_offset)\n",
+    "print(actual)\n",
+    "\n",
+    "assert actual == expected"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "a7b4e26e",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# | export\n",
+    "\n",
+    "def _get_code_from_source(source: str, start_line_no: int, end_line_no: int) -> str:\n",
+    "    \"\"\"Get a block of lines from a given source string.\n",
+    "    \n",
+    "    Args:\n",
+    "        source: The source string.\n",
+    "        start_line_no: The line number of the start of the block of lines.\n",
+    "        end_line_no: The line number of the end of the block of lines.\n",
+    "    \n",
+    "    Returns:\n",
+    "        The extracted block of lines from the source\n",
+    "    \"\"\"\n",
+    "    source_lines = source.split(\"\\n\")\n",
+    "    extracted_lines = source_lines[start_line_no-1:end_line_no]\n",
+    "    return \"\\n\".join(extracted_lines)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "710980d2",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "    async def drive(self):\n",
+      "        print(f'The {self.model} is now driving.')\n"
+     ]
+    }
+   ],
+   "source": [
+    "source = \"\"\"\n",
+    "class test:\n",
+    "    CONST_VAL = 1\n",
+    "    def __init__(self, a):\n",
+    "        self.a = a\n",
+    "        \n",
+    "    async def drive(self):\n",
+    "        print(f'The {self.model} is now driving.')\n",
+    "\"\"\"\n",
+    "start_line_no = 7\n",
+    "end_line_no = 8\n",
+    "\n",
+    "expected = \"\"\"    async def drive(self):\n",
+    "        print(f'The {self.model} is now driving.')\"\"\"\n",
+    "\n",
+    "actual = _get_code_from_source(source, start_line_no, end_line_no)\n",
+    "print(actual)\n",
+    "\n",
+    "assert actual == expected\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "d5526570",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# | export\n",
+    "\n",
+    "\n",
+    "def _calculate_end_lineno(source: str, start_line_no: int) -> int:\n",
+    "    \"\"\"Calculate the end line number of a function in a Python source code.\n",
+    "    \n",
+    "    Args:\n",
+    "        source: The source code string.\n",
+    "        start_line_no: The line number of the start of the function.\n",
+    "        \n",
+    "    Returns:\n",
+    "        The end line number of the function.\n",
+    "    \"\"\"\n",
+    "    lines = source.split(\"\\n\")[start_line_no - 1 :]\n",
+    "    first_indent = len(lines[0]) - len(lines[0].lstrip())\n",
+    "    end_line_in_source = 0\n",
+    "\n",
+    "    for i, line in enumerate(lines[1:]):\n",
+    "        if len(line) - len(line.lstrip()) == first_indent and line.strip() != \"\":\n",
+    "            end_line_in_source = i\n",
+    "            break\n",
+    "\n",
+    "    ret_val = (\n",
+    "        len(source.split(\"\\n\"))\n",
+    "        if end_line_in_source == 0\n",
+    "        else end_line_in_source + start_line_no\n",
+    "    )\n",
+    "    return ret_val - 1"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "18c65bc0",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "[8, 5, 9, 19, 13, 16, 20, 27, 24, 28, 30]\n"
+     ]
+    }
+   ],
+   "source": [
+    "source = \"\"\"\n",
+    "class A:\n",
+    "    CONST_VAL = 1\n",
+    "    def __init__(self, a):\n",
+    "        self.a = a\n",
+    "        \n",
+    "    async def drive(self):\n",
+    "        print(f'The {self.model} is now driving.')\n",
+    "\n",
+    "class B:\n",
+    "    CONST_VAL = 1\n",
+    "    def __init__(self, a):\n",
+    "        self.a = a\n",
+    "        \n",
+    "    async def drive(self):\n",
+    "        print(f'The {self.model} is now driving.')\n",
+    "        \n",
+    "    async def stop(self):\n",
+    "        print(f'The {self.model} is now stopped.')\n",
+    "        \n",
+    "class C:\n",
+    "    CONST_VAL = 1\n",
+    "    def __init__(self, a):\n",
+    "        self.a = a\n",
+    "\n",
+    "    async def drive(self):\n",
+    "        print(f'The {self.model} is now driving.')\n",
+    "        \n",
+    "async def drive(self):\n",
+    "    print(f'The {self.model} is now driving.')\n",
+    "\"\"\"\n",
+    "\n",
+    "tree = ast.parse(source)\n",
+    "expected = [8, 5, 9, 19, 13, 16, 20, 27, 24, 28, 30]\n",
+    "actual = []\n",
+    "for i, node in enumerate(tree.body):\n",
+    "    res = _calculate_end_lineno(source, node.lineno)\n",
+    "    actual.append(res)\n",
+    "#     print(node.end_lineno)\n",
+    "\n",
+    "    if isinstance(node, ast.ClassDef):\n",
+    "        for f in node.body:\n",
+    "            if not isinstance(f, (ast.FunctionDef, ast.AsyncFunctionDef)):\n",
+    "                continue\n",
+    "            res = _calculate_end_lineno(source, f.lineno)\n",
+    "            actual.append(res)\n",
+    "#             print(f.end_lineno)\n",
+    "print(actual)\n",
+    "assert actual == expected"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "acae29db",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# | export\n",
+    "\n",
+    "\n",
+    "def _line_has_class_or_method(source: str, lineno: int) -> bool:\n",
+    "    \"\"\"Check if a line in the source code contains a class or method definition.\n",
+    "    \n",
+    "    Args:\n",
+    "        source: The source code as a string.\n",
+    "        lineno: The line number to check.\n",
+    "    \n",
+    "    Returns:\n",
+    "        True if the line contains a class or method definition, False otherwise.\n",
+    "    \"\"\"\n",
+    "    line = \"\".join(source.split(\"\\n\")[lineno - 1])\n",
+    "    tokens = list(tokenize.tokenize(BytesIO(line.encode(\"utf-8\")).readline))\n",
+    "    return tokens[1].type == tokenize.NAME and tokens[1].string in {\n",
+    "        \"class\",\n",
+    "        \"def\",\n",
+    "        \"async\",\n",
+    "    }\n",
+    "\n",
+    "def _get_start_line_for_class_or_func(source: str, lineno: int) -> int:\n",
+    "    \"\"\"Get the line number of the first line containing a class or function definition.\n",
+    "    \n",
+    "    Args:\n",
+    "        source: The source code as a string.\n",
+    "        lineno: The line number to start from.\n",
+    "    \n",
+    "    Returns:\n",
+    "        The line number of the first line containing a class or function definition, \n",
+    "        or the original line number if no such line is found.\n",
+    "    \"\"\"\n",
+    "    if _line_has_class_or_method(source, lineno):\n",
+    "        return lineno\n",
+    "\n",
+    "    original_lineno = lineno\n",
+    "    total_lines = source.split(\"\\n\")\n",
+    "    for i in total_lines:\n",
+    "        lineno += 1\n",
+    "        if lineno > len(total_lines):\n",
+    "            break\n",
+    "        if _line_has_class_or_method(source, lineno):\n",
+    "            return lineno\n",
+    "    return original_lineno"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "e0d08807",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "30\n"
+     ]
+    }
+   ],
+   "source": [
+    "source = \"\"\"def decorator1(func):\n",
+    "    \\\"\"\"Sample docstring\n",
+    "\n",
+    "    Args:\n",
+    "        s: sample args\n",
+    "\n",
+    "    Returns:\n",
+    "        sample return\n",
+    "    \\\"\"\"\n",
+    "    def inner():\n",
+    "        func()\n",
+    "    return inner\n",
+    "\n",
+    "def decorator2(func):\n",
+    "    \\\"\"\"Sample docstring\n",
+    "\n",
+    "    Args:\n",
+    "        s: sample args\n",
+    "\n",
+    "    Returns:\n",
+    "        sample return\n",
+    "    \\\"\"\"\n",
+    "    def inner():\n",
+    "        func()\n",
+    "    return inner\n",
+    "\n",
+    "\n",
+    "@decorator1\n",
+    "@decorator2\n",
+    "def outer_func():\n",
+    "    def inner_func():\n",
+    "        print(\"Hello, World!\")\n",
+    "    inner_func()\n",
+    "\"\"\"\n",
+    "\n",
+    "line_no = 28\n",
+    "expected = 30\n",
+    "actual = _get_start_line_for_class_or_func(source, line_no)\n",
+    "print(actual)\n",
+    "assert actual == expected"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "0e963690",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "2\n",
+      "4\n",
+      "3\n",
+      "5\n",
+      "2\n"
+     ]
+    }
+   ],
+   "source": [
+    "source = \"\"\"\n",
+    "async def drive(self):\n",
+    "    print(f'The {self.model} is now driving.')\n",
+    "\"\"\"\n",
+    "lineno = 2\n",
+    "expected = 2\n",
+    "actual = _get_start_line_for_class_or_func(source, lineno)\n",
+    "print(actual)\n",
+    "\n",
+    "assert actual == expected\n",
+    "\n",
+    "source = \"\"\"\n",
+    "@foo(x=5)\n",
+    "@decorator2\n",
+    "async def drive(self):\n",
+    "    print(f'The {self.model} is now driving.')\n",
+    "\"\"\"\n",
+    "lineno = 2\n",
+    "expected = 4\n",
+    "actual = _get_start_line_for_class_or_func(source, lineno)\n",
+    "print(actual)\n",
+    "\n",
+    "assert actual == expected\n",
+    "\n",
+    "source = \"\"\"\n",
+    "@foo(x=5)\n",
+    "async def drive(self):\n",
+    "    print(f'The {self.model} is now driving.')\n",
+    "\"\"\"\n",
+    "lineno = 2\n",
+    "expected = 3\n",
+    "actual = _get_start_line_for_class_or_func(source, lineno)\n",
+    "print(actual)\n",
+    "\n",
+    "assert actual == expected\n",
+    "\n",
+    "source = \"\"\"\n",
+    "@foo(x=5)\n",
+    "@bar(x=5)\n",
+    "@zar(x=5)\n",
+    "class A:\n",
+    "    CONST_VAL = 1\n",
+    "    def __init__(self, a):\n",
+    "        self.a = a\n",
+    "        \n",
+    "    async def drive(self):\n",
+    "        print(f'The {self.model} is now driving.')\n",
+    "\"\"\"\n",
+    "lineno = 2\n",
+    "expected = 5\n",
+    "actual = _get_start_line_for_class_or_func(source, lineno)\n",
+    "print(actual)\n",
+    "\n",
+    "assert actual == expected\n",
+    "\n",
+    "source = \"\"\"\n",
+    "@foo(x=5)\n",
+    "@bar(x=5)\n",
+    "\n",
+    "\"\"\"\n",
+    "lineno = 2\n",
+    "expected = 2\n",
+    "actual = _get_start_line_for_class_or_func(source, lineno)\n",
+    "print(actual)\n",
+    "\n",
+    "assert actual == expected\n",
+    "\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "7cc5fd4f",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# | export\n",
+    "\n",
+    "\n",
+    "def _add_docstring(\n",
+    "    source: str,\n",
+    "    node: Union[ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef],\n",
+    "    line_offset: int,\n",
+    ") -> Tuple[str, int]:\n",
+    "    \"\"\"Add a docstring to the given node and update the source code.\n",
+    "\n",
+    "    Args:\n",
+    "        source: the source code from the notebook cell\n",
+    "        node: the AST node representing a class definition, function definition,\n",
+    "            or async function definition\n",
+    "        line_offset: the number of lines added before the current\n",
+    "            node in the source\n",
+    "\n",
+    "    Returns:\n",
+    "        A tuple containing the updated source code and the new line number offset\n",
+    "    \"\"\"\n",
+    "    line_no = node.lineno + line_offset\n",
+    "    \n",
+    "    # Fix for ast's node.lineno giving line number of decorator \n",
+    "    # instead of function/class definition in Python 3.7\n",
+    "    line_no = _get_start_line_for_class_or_func(source, line_no)\n",
+    "    \n",
+    "    if hasattr(node, 'end_lineno') and node.end_lineno is not None:\n",
+    "        end_line_no = node.end_lineno + line_offset\n",
+    "    else:\n",
+    "        end_line_no = _calculate_end_lineno(source, line_no)\n",
+    "    \n",
+    "    code = _get_code_from_source(source, line_no, end_line_no)\n",
+    "    docstring = _generate_docstring_using_codex(code)\n",
+    "    \n",
+    "    source = _inject_docstring_to_source(source, docstring, line_no, node.col_offset)\n",
+    "    line_offset += len(docstring.split(\"\\n\"))\n",
+    "    return source, line_offset"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "d2ffe631",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "\n",
+      "class test:\n",
+      "    \"\"\"Sample docstring\n",
+      "\n",
+      "    Args:\n",
+      "        s: sample args\n",
+      "\n",
+      "    Returns:\n",
+      "        sample return\n",
+      "    \"\"\"\n",
+      "    CONST_VAL = 1\n",
+      "    def __init__(self, a):\n",
+      "        \"\"\"Sample docstring\n",
+      "    \n",
+      "        Args:\n",
+      "            s: sample args\n",
+      "    \n",
+      "        Returns:\n",
+      "            sample return\n",
+      "        \"\"\"\n",
+      "        self.a = a\n",
+      "        \n",
+      "    async def drive(self):\n",
+      "        \"\"\"Sample docstring\n",
+      "    \n",
+      "        Args:\n",
+      "            s: sample args\n",
+      "    \n",
+      "        Returns:\n",
+      "            sample return\n",
+      "        \"\"\"\n",
+      "        print(f'The {self.model} is now driving.')\n",
+      "\n"
+     ]
+    }
+   ],
+   "source": [
+    "source = \"\"\"\n",
+    "class test:\n",
+    "    CONST_VAL = 1\n",
+    "    def __init__(self, a):\n",
+    "        self.a = a\n",
+    "        \n",
+    "    async def drive(self):\n",
+    "        print(f'The {self.model} is now driving.')\n",
+    "\"\"\"\n",
+    "\n",
+    "expected = \"\"\"\n",
+    "class test:\n",
+    "    \\\"\"\"Sample docstring\n",
+    "\n",
+    "    Args:\n",
+    "        s: sample args\n",
+    "\n",
+    "    Returns:\n",
+    "        sample return\n",
+    "    \\\"\"\"\n",
+    "    CONST_VAL = 1\n",
+    "    def __init__(self, a):\n",
+    "        \\\"\"\"Sample docstring\n",
+    "    \n",
+    "        Args:\n",
+    "            s: sample args\n",
+    "    \n",
+    "        Returns:\n",
+    "            sample return\n",
+    "        \\\"\"\"\n",
+    "        self.a = a\n",
+    "        \n",
+    "    async def drive(self):\n",
+    "        \\\"\"\"Sample docstring\n",
+    "    \n",
+    "        Args:\n",
+    "            s: sample args\n",
+    "    \n",
+    "        Returns:\n",
+    "            sample return\n",
+    "        \\\"\"\"\n",
+    "        print(f'The {self.model} is now driving.')\n",
+    "\"\"\"\n",
+    "\n",
+    "tree = ast.parse(source)\n",
+    "line_offset = 0\n",
+    "for node in tree.body:\n",
+    "    source, line_offset = _add_docstring(source, node, line_offset)\n",
+    "\n",
+    "    for f in node.body:\n",
+    "        if not isinstance(f, (ast.FunctionDef, ast.AsyncFunctionDef)):\n",
+    "            continue\n",
+    "        source, line_offset = _add_docstring(source, f, line_offset)\n",
+    "\n",
+    "print(source)\n",
+    "assert source == expected"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "52938a28",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# | export\n",
+    "\n",
+    "\n",
+    "def _check_and_add_docstrings_to_source(source: str) -> str:\n",
+    "    \"\"\"Check for missing docstrings in the source code and add them if necessary.\n",
+    "\n",
+    "    Args:\n",
+    "        source: the source code from the notebook cell\n",
+    "\n",
+    "    Returns:\n",
+    "        The updated source code with added docstrings\n",
+    "    \"\"\"\n",
+    "\n",
+    "    tree = ast.parse(source)\n",
+    "    line_offset = 0\n",
+    "\n",
+    "    for node in tree.body:\n",
+    "        if not isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):\n",
+    "            continue\n",
+    "        if ast.get_docstring(node) is not None:\n",
+    "            continue\n",
+    "\n",
+    "        # A class or a function without docstring\n",
+    "        source, line_offset = _add_docstring(source, node, line_offset)\n",
+    "        if not isinstance(node, ast.ClassDef):\n",
+    "            continue\n",
+    "        # Is a class and we need to check the functions inside\n",
+    "        # 29 - 36 make it as a recursive function\n",
+    "        for f in node.body:\n",
+    "            if not isinstance(f, (ast.FunctionDef, ast.AsyncFunctionDef)):\n",
+    "                continue\n",
+    "            if ast.get_docstring(f) is not None:\n",
+    "                continue\n",
+    "\n",
+    "            # should be a function inside the class for which there is no docstring\n",
+    "            source, line_offset = _add_docstring(source, f, line_offset)\n",
+    "\n",
+    "    return source"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "e54920d5",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "\n",
+      "def decorator1(func):\n",
+      "    \"\"\"Sample docstring\n",
+      "\n",
+      "    Args:\n",
+      "        s: sample args\n",
+      "\n",
+      "    Returns:\n",
+      "        sample return\n",
+      "    \"\"\"\n",
+      "    def inner():\n",
+      "        func()\n",
+      "    return inner\n",
+      "\n",
+      "def decorator2(func):\n",
+      "    \"\"\"Sample docstring\n",
+      "\n",
+      "    Args:\n",
+      "        s: sample args\n",
+      "\n",
+      "    Returns:\n",
+      "        sample return\n",
+      "    \"\"\"\n",
+      "    def inner():\n",
+      "        func()\n",
+      "    return inner\n",
+      "\n",
+      "@decorator1\n",
+      "@decorator2\n",
+      "def outer_func():\n",
+      "    \"\"\"Sample docstring\n",
+      "\n",
+      "    Args:\n",
+      "        s: sample args\n",
+      "\n",
+      "    Returns:\n",
+      "        sample return\n",
+      "    \"\"\"\n",
+      "    def inner_func():\n",
+      "        print(\"Hello, World!\")\n",
+      "    inner_func()\n",
+      "\n"
+     ]
+    }
+   ],
+   "source": [
+    "source = \"\"\"\n",
+    "def decorator1(func):\n",
+    "    def inner():\n",
+    "        func()\n",
+    "    return inner\n",
+    "\n",
+    "def decorator2(func):\n",
+    "    def inner():\n",
+    "        func()\n",
+    "    return inner\n",
+    "\n",
+    "@decorator1\n",
+    "@decorator2\n",
+    "def outer_func():\n",
+    "    def inner_func():\n",
+    "        print(\"Hello, World!\")\n",
+    "    inner_func()\n",
+    "\"\"\"\n",
+    "actual = _check_and_add_docstrings_to_source(source)\n",
+    "print(actual)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "ba209b63",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# | export\n",
+    "\n",
+    "\n",
+    "def add_docstring_to_notebook(nb_path: Union[str, Path], version: int = 4):\n",
+    "    \"\"\"Add docstrings to the source\n",
+    "    \n",
+    "    This function reads through a Jupyter notebook cell by cell and \n",
+    "    adds docstrings for classes and methods that do not have them.\n",
+    "    \n",
+    "    Args:\n",
+    "        nb_path: The notebook file path\n",
+    "        version: The version of the Jupyter notebook format\n",
+    "    \"\"\"\n",
+    "    nb_path = Path(nb_path)\n",
+    "    nb = nbformat.read(nb_path, as_version=version)\n",
+    "\n",
+    "    for cell in nb.cells:\n",
+    "        if cell.cell_type == \"code\":\n",
+    "            cell[\"source\"] = _check_and_add_docstrings_to_source(cell[\"source\"])\n",
+    "\n",
+    "    nbformat.write(nb, nb_path)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "5abee4bd",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "# Test notebook for docstring generator\n",
+      "\n",
+      "> Test notebook for docstring generator\n",
+      "# | export\n",
+      "\n",
+      "from typing import *\n",
+      "import os\n",
+      "from pathlib import Path\n",
+      "# from contextlib import contextmanager\n",
+      "\n",
+      "from contextlib import contextmanager\n",
+      "\n",
+      "import shutil\n",
+      "from tempfile import TemporaryDirectory\n",
+      "\n",
+      "# Title\n",
+      "# | export\n",
+      "\n",
+      "# Vehicle class\n",
+      "class Vehicle:\n",
+      "    \"\"\"Sample docstring\n",
+      "\n",
+      "    Args:\n",
+      "        s: sample args\n",
+      "\n",
+      "    Returns:\n",
+      "        sample return\n",
+      "    \"\"\"\n",
+      "    # Constructor function\n",
+      "    def __init__(self, brand, model, type):\n",
+      "        \"\"\"Constructor function\n",
+      "        \n",
+      "        Args:\n",
+      "            brand: Vehicle's brand\n",
+      "            model: Vehicle's model\n",
+      "            type: Vehicle's type\n",
+      "        \"\"\"\n",
+      "        self.brand = brand\n",
+      "        self.model = model\n",
+      "        self.type = type\n",
+      "        self.gas_tank_size = 14\n",
+      "        self.fuel_level = 0\n",
+      "    \n",
+      "    # fuel_up function\n",
+      "    def fuel_up(self):\n",
+      "        \"\"\"Sample docstring\n",
+      "    \n",
+      "        Args:\n",
+      "            s: sample args\n",
+      "    \n",
+      "        Returns:\n",
+      "            sample return\n",
+      "        \"\"\"\n",
+      "        # comment goes here\n",
+      "        self.fuel_level = self.gas_tank_size\n",
+      "        return 'Gas tank is now full.'\n",
+      "\n",
+      "    # drive function\n",
+      "    async def drive(self):\n",
+      "        \"\"\"Sample docstring\n",
+      "    \n",
+      "        Args:\n",
+      "            s: sample args\n",
+      "    \n",
+      "        Returns:\n",
+      "            sample return\n",
+      "        \"\"\"\n",
+      "        print(f'The {self.model} is now driving.')\n",
+      "v = Vehicle(\"honda\", \"city\", \"hatchback\")\n",
+      "actual = v.fuel_up()\n",
+      "\n",
+      "print(actual)\n",
+      "assert actual == 'Gas tank is now full.'\n",
+      "\n",
+      "This is a sample markdown cell. The texts goes here\n",
+      "\n",
+      "def say_hello(name: str) -> str:\n",
+      "    \"\"\"Sample docstring\n",
+      "\n",
+      "    Args:\n",
+      "        s: sample args\n",
+      "\n",
+      "    Returns:\n",
+      "        sample return\n",
+      "    \"\"\"\n",
+      "    return f\"Hello, {name}\"\n",
+      "    \n",
+      "async def function_with_docstring(name: str) -> str:\n",
+      "    \"\"\" This function already has docstring\"\"\"\n",
+      "    return f\"I won't say hello, {name}\"\n",
+      "    \n",
+      "async def dont_say_hello(name: str) -> str:\n",
+      "    \"\"\"Sample docstring\n",
+      "\n",
+      "    Args:\n",
+      "        s: sample args\n",
+      "\n",
+      "    Returns:\n",
+      "        sample return\n",
+      "    \"\"\"\n",
+      "    return f\"I won't say hello, {name}\"\n",
+      "    \n",
+      "def decorator1(func):\n",
+      "    \"\"\"Sample docstring\n",
+      "\n",
+      "    Args:\n",
+      "        s: sample args\n",
+      "\n",
+      "    Returns:\n",
+      "        sample return\n",
+      "    \"\"\"\n",
+      "    def inner():\n",
+      "        func()\n",
+      "    return inner\n",
+      "\n",
+      "def decorator2(func):\n",
+      "    \"\"\"Sample docstring\n",
+      "\n",
+      "    Args:\n",
+      "        s: sample args\n",
+      "\n",
+      "    Returns:\n",
+      "        sample return\n",
+      "    \"\"\"\n",
+      "    def inner():\n",
+      "        func()\n",
+      "    return inner\n",
+      "\n",
+      "@decorator1\n",
+      "@decorator2\n",
+      "def outer_func():\n",
+      "    \"\"\"Sample docstring\n",
+      "\n",
+      "    Args:\n",
+      "        s: sample args\n",
+      "\n",
+      "    Returns:\n",
+      "        sample return\n",
+      "    \"\"\"\n",
+      "    def inner_func():\n",
+      "        print(\"Hello, World!\")\n",
+      "    inner_func()\n",
+      "@contextmanager\n",
+      "def my_context_mgr():\n",
+      "    \"\"\"Sample docstring\n",
+      "\n",
+      "    Args:\n",
+      "        s: sample args\n",
+      "\n",
+      "    Returns:\n",
+      "        sample return\n",
+      "    \"\"\"\n",
+      "    try:\n",
+      "        yield\n",
+      "    finally:\n",
+      "        pass\n",
+      "\n"
+     ]
+    }
+   ],
+   "source": [
+    "with TemporaryDirectory() as d:\n",
+    "    nbs_path = Path(d) / \"nbs\"\n",
+    "    nbs_path.mkdir(parents=True)\n",
+    "\n",
+    "    nb_path = nbs_path / \"test.ipynb\"\n",
+    "    shutil.copyfile(Path(\"..\") / \"fixtures\" / \"Test_Data.ipynb\", nb_path)\n",
+    "    \n",
+    "    assert nb_path.exists()\n",
+    "\n",
+    "    add_docstring_to_notebook(nb_path)\n",
+    "\n",
+    "    with nb_path.open(\"r\") as f:\n",
+    "        nb = nbformat.read(f, as_version=4)\n",
+    "\n",
+    "for cell in nb.cells:\n",
+    "#     print(cell[\"source\"])\n",
+    "    \n",
+    "    if cell.cell_type == \"code\":\n",
+    "        tree = ast.parse(cell[\"source\"])        \n",
+    "        print(cell[\"source\"])\n",
+    "        for node in tree.body:\n",
+    "            if not isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):\n",
+    "                continue\n",
+    "            assert ast.get_docstring(node)\n",
+    "            \n",
+    "    else:\n",
+    "        print(cell[\"source\"])\n",
+    "\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "af3e621b",
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/nbs/_quarto.yml b/nbs/_quarto.yml
new file mode 100644
index 0000000..0a6dfcb
--- /dev/null
+++ b/nbs/_quarto.yml
@@ -0,0 +1,20 @@
+project:
+  type: website
+
+format:
+  html:
+    theme: cosmo
+    css: styles.css
+    toc: true
+
+website:
+  twitter-card: true
+  open-graph: true
+  repo-actions: [issue]
+  navbar:
+    background: primary
+    search: true
+  sidebar:
+    style: floating
+
+metadata-files: [nbdev.yml, sidebar.yml]
\ No newline at end of file
diff --git a/nbs/index.ipynb b/nbs/index.ipynb
new file mode 100644
index 0000000..776f71f
--- /dev/null
+++ b/nbs/index.ipynb
@@ -0,0 +1,86 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# docstring-gen\n",
+    "\n",
+    "> Generator for docstrings"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "This file will become your README and also the index of your documentation."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Install"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "```sh\n",
+    "pip install docstring_gen\n",
+    "```"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## How to use"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Fill me in please! Don't forget code examples:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "2"
+      ]
+     },
+     "execution_count": null,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "1+1"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3 (ipykernel)",
+   "language": "python",
+   "name": "python3"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/nbs/nbdev.yml b/nbs/nbdev.yml
new file mode 100644
index 0000000..0e693e1
--- /dev/null
+++ b/nbs/nbdev.yml
@@ -0,0 +1,9 @@
+project:
+  output-dir: _docs
+
+website:
+  title: "docstring-gen"
+  site-url: "https://airtai.github.io/docstring-gen"
+  description: "This project generates missing docstrings for classes and methods using OpenAI's Codex model."
+  repo-branch: main
+  repo-url: "https://github.com/airtai/docstring-gen"
diff --git a/nbs/sidebar.yml b/nbs/sidebar.yml
new file mode 100644
index 0000000..1390528
--- /dev/null
+++ b/nbs/sidebar.yml
@@ -0,0 +1,4 @@
+website:
+  sidebar:
+    contents:
+      - index.ipynb
diff --git a/nbs/styles.css b/nbs/styles.css
new file mode 100644
index 0000000..66ccc49
--- /dev/null
+++ b/nbs/styles.css
@@ -0,0 +1,37 @@
+.cell {
+  margin-bottom: 1rem;
+}
+
+.cell > .sourceCode {
+  margin-bottom: 0;
+}
+
+.cell-output > pre {
+  margin-bottom: 0;
+}
+
+.cell-output > pre, .cell-output > .sourceCode > pre, .cell-output-stdout > pre {
+  margin-left: 0.8rem;
+  margin-top: 0;
+  background: none;
+  border-left: 2px solid lightsalmon;
+  border-top-left-radius: 0;
+  border-top-right-radius: 0;
+}
+
+.cell-output > .sourceCode {
+  border: none;
+}
+
+.cell-output > .sourceCode {
+  background: none;
+  margin-top: 0;
+}
+
+div.description {
+  padding-left: 2px;
+  padding-top: 5px;
+  font-style: italic;
+  font-size: 135%;
+  opacity: 70%;
+}
diff --git a/settings.ini b/settings.ini
new file mode 100644
index 0000000..ba757e3
--- /dev/null
+++ b/settings.ini
@@ -0,0 +1,65 @@
+[DEFAULT]
+# All sections below are required unless otherwise specified.
+# See https://github.com/fastai/nbdev/blob/master/settings.ini for examples.
+
+### Python library ###
+repo = docstring-gen
+lib_name = %(repo)s
+version = 0.0.1rc0
+min_python = 3.7
+license = apache2
+
+### nbdev ###
+doc_path = _docs
+lib_path = docstring_gen
+nbs_path = nbs
+recursive = True
+tst_flags = notest
+put_version_in_init = True
+black_formatting = True
+
+### Docs ###
+branch = main
+custom_sidebar = True
+doc_host = https://%(user)s.github.io
+doc_baseurl = /%(repo)s
+git_url = https://github.com/%(user)s/%(repo)s
+title = %(lib_name)s
+
+### PyPI ###
+audience = Developers
+author = airt
+author_email = info@airt.ai
+copyright = 2023 onwards, %(author)s
+description = This project generates missing docstrings for classes and methods using OpenAI's Codex model.
+keywords = nbdev jupyter notebook python mkdocs material docstring generator
+language = English
+status = 3
+user = airtai
+
+### Optional ###
+requirements = \
+    nbdev>=2.3.9 \
+    nbformat>=5.7.1 \
+    typer[all]>=0.7.0 \
+    openai>=0.25.0
+    
+dev_requirements =     \
+    nbdev-mkdocs>=0.0.3 \
+    bandit>=1.7.4 \
+    semgrep>=0.115.0 \
+    pytest>=7.1.3 \
+    black>=22.8.0 \
+    mypy>=0.960 \
+    mkdocs>=1.4.2 \
+    mkdocstrings==0.18.1 \
+    mkdocs-material>=8.5.11 \
+    mkdocs-literate-nav>=0.5.0 \
+    mkdocs-section-index>=0.3.4 \
+    configupdater>=3.1.1 \
+    griffe>=0.24.1 \
+    ruamel.yaml>=0.17.21 \
+    openai>=0.25.0 \
+    playwright>=1.28.0
+
+# console_scripts =
\ No newline at end of file
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..5cf5922
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,57 @@
+from pkg_resources import parse_version
+from configparser import ConfigParser
+import setuptools
+assert parse_version(setuptools.__version__)>=parse_version('36.2')
+
+# note: all settings are in settings.ini; edit there, not here
+config = ConfigParser(delimiters=['='])
+config.read('settings.ini')
+cfg = config['DEFAULT']
+
+cfg_keys = 'version description keywords author author_email'.split()
+expected = cfg_keys + "lib_name user branch license status min_python audience language".split()
+for o in expected: assert o in cfg, "missing expected setting: {}".format(o)
+setup_cfg = {o:cfg[o] for o in cfg_keys}
+
+licenses = {
+    'apache2': ('Apache Software License 2.0','OSI Approved :: Apache Software License'),
+    'mit': ('MIT License', 'OSI Approved :: MIT License'),
+    'gpl2': ('GNU General Public License v2', 'OSI Approved :: GNU General Public License v2 (GPLv2)'),
+    'gpl3': ('GNU General Public License v3', 'OSI Approved :: GNU General Public License v3 (GPLv3)'),
+    'bsd3': ('BSD License', 'OSI Approved :: BSD License'),
+}
+statuses = [ '1 - Planning', '2 - Pre-Alpha', '3 - Alpha',
+    '4 - Beta', '5 - Production/Stable', '6 - Mature', '7 - Inactive' ]
+py_versions = '3.6 3.7 3.8 3.9 3.10'.split()
+
+requirements = cfg.get('requirements','').split()
+if cfg.get('pip_requirements'): requirements += cfg.get('pip_requirements','').split()
+min_python = cfg['min_python']
+lic = licenses.get(cfg['license'].lower(), (cfg['license'], None))
+dev_requirements = (cfg.get('dev_requirements') or '').split()
+
+setuptools.setup(
+    name = cfg['lib_name'],
+    license = lic[0],
+    classifiers = [
+        'Development Status :: ' + statuses[int(cfg['status'])],
+        'Intended Audience :: ' + cfg['audience'].title(),
+        'Natural Language :: ' + cfg['language'].title(),
+    ] + ['Programming Language :: Python :: '+o for o in py_versions[py_versions.index(min_python):]] + (['License :: ' + lic[1] ] if lic[1] else []),
+    url = cfg['git_url'],
+    packages = setuptools.find_packages(),
+    include_package_data = True,
+    install_requires = requirements,
+    extras_require={ 'dev': dev_requirements },
+    dependency_links = cfg.get('dep_links','').split(),
+    python_requires  = '>=' + cfg['min_python'],
+    long_description = open('README.md').read(),
+    long_description_content_type = 'text/markdown',
+    zip_safe = False,
+    entry_points = {
+        'console_scripts': cfg.get('console_scripts','').split(),
+        'nbdev': [f'{cfg.get("lib_path")}={cfg.get("lib_path")}._modidx:d']
+    },
+    **setup_cfg)
+
+


Test patch:
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
new file mode 100644
index 0000000..b07727e
--- /dev/null
+++ b/.github/workflows/test.yaml
@@ -0,0 +1,42 @@
+name: CI
+on:  [workflow_dispatch, pull_request, push]
+
+jobs:
+  static_check:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: airtai/workflows/nbdev-mkdocs-static-check@main
+  test:
+    strategy:
+      fail-fast: false
+      matrix:
+        os:  [ubuntu, macos]
+        version: ["3.7", "3.8", "3.9", "3.10"]
+    runs-on: ${{ matrix.os }}-latest
+
+    steps:
+      - name: Install quarto
+        run: |
+          if [ ${{ matrix.os }} == 'ubuntu' ]; then
+            wget -q https://www.quarto.org/download/latest/quarto-linux-amd64.deb
+            sudo dpkg -i quarto-linux-amd64.deb
+          else
+            wget -q https://www.quarto.org/download/latest/quarto-macos.pkg
+            sudo installer -pkg quarto-macos.pkg -target /
+          fi
+      - uses: fastai/workflows/nbdev-ci@master
+        with:
+          version: ${{ matrix.version }}
+      - name: test build docs
+        run: |
+          test -f setup.py && pip install -e ".[dev]"
+          nbdev_mkdocs docs
+          if [ -f "mkdocs/site/index.html" ]; then
+            echo "docs built successfully."
+          else
+            echo "index page not found in rendered docs."
+            ls -la
+            ls -la mkdocs/site/
+            exit 1
+          fi
+