Repository: kvankova/code-embedder

Issue:
Use Code-Embedder as pre-commit hook

Discussion:

Patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 32a52ac..1b5feda 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -27,13 +27,21 @@ jobs:
           sed -i "s/^version = .*/version = \"$TAG\"/" pyproject.toml
           cat pyproject.toml
 
-      - name: Update README.md
+      - name: Update README.md - Github Action version
         env:
           TAG: ${{ steps.read_tag.outputs.TAG }}
         run: |
           sed -i "s/kvankova\/code-embedder@.*/kvankova\/code-embedder@$TAG/" README.md
           cat README.md
 
+      - name: Update README.md - Pre-commit version
+        env:
+          TAG: ${{ steps.read_tag.outputs.TAG }}
+        run: |
+          sed -i "s/code-embedder==.*/code-embedder==$TAG/" README.md
+          sed -i "s/rev: v[0-9]\+\.[0-9]\+\.[0-9]\+/rev: $TAG/" README.md
+          cat README.md
+
       - name: Commit and push changes
         env:
           TAG: ${{ steps.read_tag.outputs.TAG }}
diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml
new file mode 100644
index 0000000..7541c97
--- /dev/null
+++ b/.pre-commit-hooks.yaml
@@ -0,0 +1,6 @@
+- id: code-embedder
+  name: Code embedder
+  description: >
+    Keep your code snippets in README up-to-date!
+  entry: code-embedder
+  language: python
diff --git a/README.md b/README.md
index a3dcecd..fd1c19e 100644
--- a/README.md
+++ b/README.md
@@ -5,26 +5,26 @@
 ## **Code Embedder**
 Seamlessly update code snippets in your **README** files! 🔄📝🚀
 
-[Description](#-description) • [How it works](#-how-it-works) • [Setup](#-setup) • [Examples](#-examples) • [Contributing](#-contributing) • [Development](#️-development)
+[Description](#-description) • [How it works](#-how-it-works) • [Setup - Github Action](#-setup---github-action) • [Setup - Pre-commit Hook](#-setup---pre-commit-hook) • [Examples](#-examples) • [Contributing](#-contributing) • [Development](#️-development)
 </div>
 
 
 ## 📚 Description
 
-**Code Embedder** is a GitHub Action that automatically updates code snippets in your markdown (`README`) files. It finds code blocks in your `README` that reference specific scripts, then replaces these blocks with the current content of those scripts. This keeps your documentation in sync with your code.
+**Code Embedder** is a **GitHub Action** and a **pre-commit hook** that automatically updates code snippets in your markdown (`README`) files. It finds code blocks in your `README` that reference specific scripts, then replaces these blocks with the current content of those scripts. This keeps your documentation in sync with your code.
 
 ### ✨ Key features
 - 🔄 **Automatic synchronization**: Keep your `README` code examples up-to-date without manual intervention.
-- 🛠️ **Easy setup**: Simply add the action to your GitHub workflow and format your `README` code blocks.
-- 📝 **Section support**: Update only specific sections of the script in the `README`.
+- 🛠️ **Easy setup**: Simply add the action to your GitHub workflow / pre-commit hook and format your `README` code blocks.
+- 📝 **Section support**: Update only specific sections of the script in the `README`. This is **language agnostic**.
 - 🧩 **Object support**: Update only specific objects (functions, classes) in the `README`. *The latest version supports only 🐍 Python objects (other languages to be added soon).*
 
 
-By using **Code Embedder**, you can focus on writing and updating your actual code 💻, while letting the action take care of keeping your documentation current 📚🔄. This reduces the risk of outdated or incorrect code examples in your project documentation.
+By using **Code Embedder**, you can focus on writing and updating your actual code 💻, while letting the Code-Embedder take care of keeping your documentation current 📚🔄. This reduces the risk of outdated or incorrect code examples in your project documentation.
 
 ## 🔍 How it works
 
-The action looks for specific tags in all markdown (`README`) files, which indicate the script file path (and optionally the section to update), then it updates the code block sections in the `README` files with the content. The changes are then pushed to the repository 🚀.
+The **Code Embedder** scans markdown files for special tags that specify which parts of your scripts to embed. When it finds these tags, it automatically updates the code blocks with the latest content from your source files. When used as a GitHub Action, any changes are automatically committed and pushed to your repository 🚀.
 
 ### 📄 **Full script** updates
 In the `README` (or other markdown) file, the full script is marked with the following tag:
@@ -47,7 +47,9 @@ You must also add the following comment tags in the script file `path/to/script`
 ...
 [Comment sign] code_embedder:section_name end
 ```
-The comment sign is the one that is used in the script file, e.g. `#` for Python, or `//` for JavaScript. The `section_name` must be unique in the file, otherwise the action will use the first section found.
+The comment sign is the one that is used in the script file, e.g. `#` for Python, or `//` for JavaScript. For example, in python script you add `# code_embedder:A start` and `# code_embedder:A end` to new lines to mark the start and end of the section `A` you want to include in the `README`.
+
+The `section_name` must be unique in the file, otherwise the Code-Embedder will use the first section found.
 
 ### 🧩 **Object** updates
 In the `README` (or other markdown) file, the object of the script is marked with the following tag:
@@ -59,10 +61,10 @@ In the `README` (or other markdown) file, the object of the script is marked wit
 > Notice that the `path/to/script` is followed by `o:` in the tag to indicate that the object `object_name` is being updated.
 
 > [!Note]
-> The object name must match exactly the name of the object (function, class) in the script file, including the case (e.g. `Person` not `person`). Currently, only 🐍 Python objects are supported.
+> The object name must match exactly the name of the object (function, class) in the script file, including the case (e.g. if class is `Person` then object name must be `Person`, not `person`). Currently, only 🐍 Python objects are supported.
 
-## 🔧 Setup
-To use this action, you need to configure a yaml workflow file in `.github/workflows` folder (e.g. `.github/workflows/code-embedder.yaml`) with the following content:
+## 🔧 Setup - Github Action
+Use **Code Embedder** as a Github Action by adding the following to your `.github/workflows/code-embedder.yaml` file:
 
 ```yaml
 name: Code Embedder
@@ -89,6 +91,48 @@ jobs:
 
 ```
 
+## 🔧 Setup - Pre-commit Hook
+You can set up **Code Embedder** as a pre-commit hook using either:
+<ol type="A">
+   <li>Installation via PyPI</li>
+   <li>Direct repository reference in your <code>.pre-commit-config.yaml</code> file</li>
+</ol>
+
+### A. Installation via PyPI
+Install the package:
+```bash
+pip install code-embedder==0.5.2
+```
+
+Your `.pre-commit-config.yaml` file should look like this:
+```yaml
+- repo: local
+  hooks:
+    - id: code-embedder
+      name: Code embedder
+      entry: code-embedder run
+      language: python
+```
+
+### B. Direct repository reference
+Alternatively, you can reference the repository directly in your `.pre-commit-config.yaml` file:
+```yaml
+- repo: https://github.com/kvankova/code-embedder
+  rev: v0.5.2
+  hooks:
+    - id: code-embedder
+      name: Code embedder
+      entry: code-embedder run
+      language: python
+```
+
+### 🔧 Options
+Command `code-embedder run` has the following options:
+
+| Option | Description |
+| ------ | ----------- |
+| `--all-files` | Process all files in the repository. In pre-commit hook, it by default checks only the changed files. |
+
 ## 💡 Examples
 
 ### 📄 Full script update
@@ -107,7 +151,7 @@ The `main.py` file contains the following code:
 print("Embedding successful")
 ```
 
-Once the workflow runs, the code block sections are filled with the content of the script located at `main.py` and updated in the `README` file.
+Once code-embedder runs, the code block sections are filled with the content of the script located at `main.py` and updated in the `README` file.
 
 ````md
 # README
@@ -118,7 +162,7 @@ This is a readme.
 print("Embedding successful")
 ```
 ````
-With any changes to `main.py`, the code block section is updated in the `README` file with the next workflow run.
+With any changes to `main.py`, the code block section is updated in the `README` file with the next code-embedder run.
 
 ### 📂 Section update
 
@@ -140,7 +184,7 @@ print("Embedding successful")
 # code_embedder:A end
 ```
 
-Once the workflow runs, the code block section will be updated in the `README` file with the content of the section `A` from the script located at `main.py` and pushed to the repository 🚀.
+Once code-embedder runs, the code block section will be updated in the `README` file with the content of the section `A` from the script located at `main.py` (in case of using it as a Github Action, the changes are then pushed to the repository 🚀).
 
 ````md
 # README
@@ -152,7 +196,7 @@ print("Embedding successful")
 ```
 ````
 
-With any changes to the section `A` in `main.py`, the code block section is updated in the `README` file with the next workflow run.
+With any changes to the section `A` in `main.py`, the code block section is updated in the `README` file with the next code-embedder run.
 
 ### 🧩 Object update
 The tag used for object update follows the same convention as the tag for section update with the following changes:
@@ -192,7 +236,7 @@ class Person:
 ...
 ```
 
-Once the workflow runs, the code block section will be updated in the `README` file with the content of the function `print_hello` and class `Person` from the script located at `main.py` and pushed to the repository 🚀.
+Once code-embedder runs, the code block section will be updated in the `README` file with the content of the function `print_hello` and class `Person` from the script located at `main.py` (in case of using it as a Github Action, the changes are then pushed to the repository 🚀).
 
 ````md
 # README
@@ -215,10 +259,10 @@ class Person:
 ```
 ````
 
-With any changes to the function `print_hello` or class `Person` in `main.py`, the code block sections are updated in the `README` file with the next workflow run.
+With any changes to the function `print_hello` or class `Person` in `main.py`, the code block sections are updated in the `README` file with the next code-embedder run.
 
 ## 🤝 Contributing
-We welcome contributions to improve this tool!
+We welcome contributions to improve this package!
 - If you have an idea for a **new feature** ✨, open a [new feature request](https://github.com/kvankova/code-embedder/issues/new?labels=enhancement&template=feature_request.yaml) on GitHub.
 - If you spot a **bug** 🐛, open a [new issue](https://github.com/kvankova/code-embedder/issues/new/choose) on GitHub.
 - If you want to **contribute to the code**, please pick an issue that is not assigned to anyone and comment on it, so that we know you are working on it.
diff --git a/entrypoint.sh b/entrypoint.sh
index 36a20f5..0a3e5aa 100755
--- a/entrypoint.sh
+++ b/entrypoint.sh
@@ -4,15 +4,17 @@ git config --global user.name "github-actions"
 git config --global user.email "github-actions@github.com"
 git remote set-url origin https://x-access-token:$GITHUB_TOKEN@github.com/$GITHUB_REPOSITORY.git
 
-README_PATHS=$(find . -name "*.md" -print)
-
 BRANCH_NAME=${GITHUB_HEAD_REF:-$GITHUB_REF_NAME}
 
 git pull origin ${BRANCH_NAME}
 
 echo "Searching for code snippets in $README_PATHS..."
 
-poetry run python /app/src/main.py --readme-paths $README_PATHS
+poetry run python /app/src/main.py
+
+if [ $? -ne 0 ]; then
+    exit 1
+fi
 
 if [ -n "$(git status -s)" ]; then
 
diff --git a/poetry.lock b/poetry.lock
index 7025867..58aeea4 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -11,17 +11,6 @@ files = [
     {file = "appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee"},
 ]
 
-[[package]]
-name = "argparse"
-version = "1.4.0"
-description = "Python command-line parsing library"
-optional = false
-python-versions = "*"
-files = [
-    {file = "argparse-1.4.0-py2.py3-none-any.whl", hash = "sha256:c31647edb69fd3d465a847ea3157d37bed1f95f19760b11a47aa91c04b666314"},
-    {file = "argparse-1.4.0.tar.gz", hash = "sha256:62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4"},
-]
-
 [[package]]
 name = "asttokens"
 version = "2.4.1"
@@ -130,6 +119,20 @@ files = [
     {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"},
 ]
 
+[[package]]
+name = "click"
+version = "8.1.7"
+description = "Composable command line interface toolkit"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
+    {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+
 [[package]]
 name = "colorama"
 version = "0.4.6"
@@ -160,37 +163,37 @@ test = ["pytest"]
 
 [[package]]
 name = "debugpy"
-version = "1.8.7"
+version = "1.8.8"
 description = "An implementation of the Debug Adapter Protocol for Python"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "debugpy-1.8.7-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:95fe04a573b8b22896c404365e03f4eda0ce0ba135b7667a1e57bd079793b96b"},
-    {file = "debugpy-1.8.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:628a11f4b295ffb4141d8242a9bb52b77ad4a63a2ad19217a93be0f77f2c28c9"},
-    {file = "debugpy-1.8.7-cp310-cp310-win32.whl", hash = "sha256:85ce9c1d0eebf622f86cc68618ad64bf66c4fc3197d88f74bb695a416837dd55"},
-    {file = "debugpy-1.8.7-cp310-cp310-win_amd64.whl", hash = "sha256:29e1571c276d643757ea126d014abda081eb5ea4c851628b33de0c2b6245b037"},
-    {file = "debugpy-1.8.7-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:caf528ff9e7308b74a1749c183d6808ffbedbb9fb6af78b033c28974d9b8831f"},
-    {file = "debugpy-1.8.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cba1d078cf2e1e0b8402e6bda528bf8fda7ccd158c3dba6c012b7897747c41a0"},
-    {file = "debugpy-1.8.7-cp311-cp311-win32.whl", hash = "sha256:171899588bcd412151e593bd40d9907133a7622cd6ecdbdb75f89d1551df13c2"},
-    {file = "debugpy-1.8.7-cp311-cp311-win_amd64.whl", hash = "sha256:6e1c4ffb0c79f66e89dfd97944f335880f0d50ad29525dc792785384923e2211"},
-    {file = "debugpy-1.8.7-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:4d27d842311353ede0ad572600c62e4bcd74f458ee01ab0dd3a1a4457e7e3706"},
-    {file = "debugpy-1.8.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:703c1fd62ae0356e194f3e7b7a92acd931f71fe81c4b3be2c17a7b8a4b546ec2"},
-    {file = "debugpy-1.8.7-cp312-cp312-win32.whl", hash = "sha256:2f729228430ef191c1e4df72a75ac94e9bf77413ce5f3f900018712c9da0aaca"},
-    {file = "debugpy-1.8.7-cp312-cp312-win_amd64.whl", hash = "sha256:45c30aaefb3e1975e8a0258f5bbd26cd40cde9bfe71e9e5a7ac82e79bad64e39"},
-    {file = "debugpy-1.8.7-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:d050a1ec7e925f514f0f6594a1e522580317da31fbda1af71d1530d6ea1f2b40"},
-    {file = "debugpy-1.8.7-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2f4349a28e3228a42958f8ddaa6333d6f8282d5edaea456070e48609c5983b7"},
-    {file = "debugpy-1.8.7-cp313-cp313-win32.whl", hash = "sha256:11ad72eb9ddb436afb8337891a986302e14944f0f755fd94e90d0d71e9100bba"},
-    {file = "debugpy-1.8.7-cp313-cp313-win_amd64.whl", hash = "sha256:2efb84d6789352d7950b03d7f866e6d180284bc02c7e12cb37b489b7083d81aa"},
-    {file = "debugpy-1.8.7-cp38-cp38-macosx_14_0_x86_64.whl", hash = "sha256:4b908291a1d051ef3331484de8e959ef3e66f12b5e610c203b5b75d2725613a7"},
-    {file = "debugpy-1.8.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da8df5b89a41f1fd31503b179d0a84a5fdb752dddd5b5388dbd1ae23cda31ce9"},
-    {file = "debugpy-1.8.7-cp38-cp38-win32.whl", hash = "sha256:b12515e04720e9e5c2216cc7086d0edadf25d7ab7e3564ec8b4521cf111b4f8c"},
-    {file = "debugpy-1.8.7-cp38-cp38-win_amd64.whl", hash = "sha256:93176e7672551cb5281577cdb62c63aadc87ec036f0c6a486f0ded337c504596"},
-    {file = "debugpy-1.8.7-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:90d93e4f2db442f8222dec5ec55ccfc8005821028982f1968ebf551d32b28907"},
-    {file = "debugpy-1.8.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6db2a370e2700557a976eaadb16243ec9c91bd46f1b3bb15376d7aaa7632c81"},
-    {file = "debugpy-1.8.7-cp39-cp39-win32.whl", hash = "sha256:a6cf2510740e0c0b4a40330640e4b454f928c7b99b0c9dbf48b11efba08a8cda"},
-    {file = "debugpy-1.8.7-cp39-cp39-win_amd64.whl", hash = "sha256:6a9d9d6d31846d8e34f52987ee0f1a904c7baa4912bf4843ab39dadf9b8f3e0d"},
-    {file = "debugpy-1.8.7-py2.py3-none-any.whl", hash = "sha256:57b00de1c8d2c84a61b90880f7e5b6deaf4c312ecbde3a0e8912f2a56c4ac9ae"},
-    {file = "debugpy-1.8.7.zip", hash = "sha256:18b8f731ed3e2e1df8e9cdaa23fb1fc9c24e570cd0081625308ec51c82efe42e"},
+    {file = "debugpy-1.8.8-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:e59b1607c51b71545cb3496876544f7186a7a27c00b436a62f285603cc68d1c6"},
+    {file = "debugpy-1.8.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6531d952b565b7cb2fbd1ef5df3d333cf160b44f37547a4e7cf73666aca5d8d"},
+    {file = "debugpy-1.8.8-cp310-cp310-win32.whl", hash = "sha256:b01f4a5e5c5fb1d34f4ccba99a20ed01eabc45a4684f4948b5db17a319dfb23f"},
+    {file = "debugpy-1.8.8-cp310-cp310-win_amd64.whl", hash = "sha256:535f4fb1c024ddca5913bb0eb17880c8f24ba28aa2c225059db145ee557035e9"},
+    {file = "debugpy-1.8.8-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:c399023146e40ae373753a58d1be0a98bf6397fadc737b97ad612886b53df318"},
+    {file = "debugpy-1.8.8-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09cc7b162586ea2171eea055985da2702b0723f6f907a423c9b2da5996ad67ba"},
+    {file = "debugpy-1.8.8-cp311-cp311-win32.whl", hash = "sha256:eea8821d998ebeb02f0625dd0d76839ddde8cbf8152ebbe289dd7acf2cdc6b98"},
+    {file = "debugpy-1.8.8-cp311-cp311-win_amd64.whl", hash = "sha256:d4483836da2a533f4b1454dffc9f668096ac0433de855f0c22cdce8c9f7e10c4"},
+    {file = "debugpy-1.8.8-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:0cc94186340be87b9ac5a707184ec8f36547fb66636d1029ff4f1cc020e53996"},
+    {file = "debugpy-1.8.8-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64674e95916e53c2e9540a056e5f489e0ad4872645399d778f7c598eacb7b7f9"},
+    {file = "debugpy-1.8.8-cp312-cp312-win32.whl", hash = "sha256:5c6e885dbf12015aed73770f29dec7023cb310d0dc2ba8bfbeb5c8e43f80edc9"},
+    {file = "debugpy-1.8.8-cp312-cp312-win_amd64.whl", hash = "sha256:19ffbd84e757a6ca0113574d1bf5a2298b3947320a3e9d7d8dc3377f02d9f864"},
+    {file = "debugpy-1.8.8-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:705cd123a773d184860ed8dae99becd879dfec361098edbefb5fc0d3683eb804"},
+    {file = "debugpy-1.8.8-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:890fd16803f50aa9cb1a9b9b25b5ec321656dd6b78157c74283de241993d086f"},
+    {file = "debugpy-1.8.8-cp313-cp313-win32.whl", hash = "sha256:90244598214bbe704aa47556ec591d2f9869ff9e042e301a2859c57106649add"},
+    {file = "debugpy-1.8.8-cp313-cp313-win_amd64.whl", hash = "sha256:4b93e4832fd4a759a0c465c967214ed0c8a6e8914bced63a28ddb0dd8c5f078b"},
+    {file = "debugpy-1.8.8-cp38-cp38-macosx_14_0_x86_64.whl", hash = "sha256:143ef07940aeb8e7316de48f5ed9447644da5203726fca378f3a6952a50a9eae"},
+    {file = "debugpy-1.8.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f95651bdcbfd3b27a408869a53fbefcc2bcae13b694daee5f1365b1b83a00113"},
+    {file = "debugpy-1.8.8-cp38-cp38-win32.whl", hash = "sha256:26b461123a030e82602a750fb24d7801776aa81cd78404e54ab60e8b5fecdad5"},
+    {file = "debugpy-1.8.8-cp38-cp38-win_amd64.whl", hash = "sha256:f3cbf1833e644a3100eadb6120f25be8a532035e8245584c4f7532937edc652a"},
+    {file = "debugpy-1.8.8-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:53709d4ec586b525724819dc6af1a7703502f7e06f34ded7157f7b1f963bb854"},
+    {file = "debugpy-1.8.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a9c013077a3a0000e83d97cf9cc9328d2b0bbb31f56b0e99ea3662d29d7a6a2"},
+    {file = "debugpy-1.8.8-cp39-cp39-win32.whl", hash = "sha256:ffe94dd5e9a6739a75f0b85316dc185560db3e97afa6b215628d1b6a17561cb2"},
+    {file = "debugpy-1.8.8-cp39-cp39-win_amd64.whl", hash = "sha256:5c0e5a38c7f9b481bf31277d2f74d2109292179081f11108e668195ef926c0f9"},
+    {file = "debugpy-1.8.8-py2.py3-none-any.whl", hash = "sha256:ec684553aba5b4066d4de510859922419febc710df7bba04fe9e7ef3de15d34f"},
+    {file = "debugpy-1.8.8.zip", hash = "sha256:e6355385db85cbd666be703a96ab7351bc9e6c61d694893206f8001e22aee091"},
 ]
 
 [[package]]
@@ -247,13 +250,13 @@ typing = ["typing-extensions (>=4.12.2)"]
 
 [[package]]
 name = "identify"
-version = "2.6.1"
+version = "2.6.2"
 description = "File identification library for Python"
 optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
 files = [
-    {file = "identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0"},
-    {file = "identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98"},
+    {file = "identify-2.6.2-py2.py3-none-any.whl", hash = "sha256:c097384259f49e372f4ea00a19719d95ae27dd5ff0fd77ad630aa891306b82f3"},
+    {file = "identify-2.6.2.tar.gz", hash = "sha256:fab5c716c24d7a789775228823797296a2994b075fb6080ac83a102772a98cbd"},
 ]
 
 [package.extras]
@@ -305,13 +308,13 @@ test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio
 
 [[package]]
 name = "ipython"
-version = "8.28.0"
+version = "8.29.0"
 description = "IPython: Productive Interactive Computing"
 optional = false
 python-versions = ">=3.10"
 files = [
-    {file = "ipython-8.28.0-py3-none-any.whl", hash = "sha256:530ef1e7bb693724d3cdc37287c80b07ad9b25986c007a53aa1857272dac3f35"},
-    {file = "ipython-8.28.0.tar.gz", hash = "sha256:0d0d15ca1e01faeb868ef56bc7ee5a0de5bd66885735682e8a322ae289a13d1a"},
+    {file = "ipython-8.29.0-py3-none-any.whl", hash = "sha256:0188a1bd83267192123ccea7f4a8ed0a78910535dbaa3f37671dca76ebd429c8"},
+    {file = "ipython-8.29.0.tar.gz", hash = "sha256:40b60e15b22591450eef73e40a027cf77bd652e757523eebc5bd7c7c498290eb"},
 ]
 
 [package.dependencies]
@@ -342,22 +345,22 @@ test-extra = ["curio", "ipython[test]", "matplotlib (!=3.2.0)", "nbformat", "num
 
 [[package]]
 name = "jedi"
-version = "0.19.1"
+version = "0.19.2"
 description = "An autocompletion tool for Python that can be used for text editors."
 optional = false
 python-versions = ">=3.6"
 files = [
-    {file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"},
-    {file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"},
+    {file = "jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9"},
+    {file = "jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0"},
 ]
 
 [package.dependencies]
-parso = ">=0.8.3,<0.9.0"
+parso = ">=0.8.4,<0.9.0"
 
 [package.extras]
 docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"]
 qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"]
-testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"]
+testing = ["Django", "attrs", "colorama", "docopt", "pytest (<9.0.0)"]
 
 [[package]]
 name = "jupyter-client"
@@ -435,43 +438,43 @@ traitlets = "*"
 
 [[package]]
 name = "mypy"
-version = "1.12.1"
+version = "1.13.0"
 description = "Optional static typing for Python"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "mypy-1.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3d7d4371829184e22fda4015278fbfdef0327a4b955a483012bd2d423a788801"},
-    {file = "mypy-1.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f59f1dfbf497d473201356966e353ef09d4daec48caeacc0254db8ef633a28a5"},
-    {file = "mypy-1.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b947097fae68004b8328c55161ac9db7d3566abfef72d9d41b47a021c2fba6b1"},
-    {file = "mypy-1.12.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:96af62050971c5241afb4701c15189ea9507db89ad07794a4ee7b4e092dc0627"},
-    {file = "mypy-1.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:d90da248f4c2dba6c44ddcfea94bb361e491962f05f41990ff24dbd09969ce20"},
-    {file = "mypy-1.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1230048fec1380faf240be6385e709c8570604d2d27ec6ca7e573e3bc09c3735"},
-    {file = "mypy-1.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:02dcfe270c6ea13338210908f8cadc8d31af0f04cee8ca996438fe6a97b4ec66"},
-    {file = "mypy-1.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5a437c9102a6a252d9e3a63edc191a3aed5f2fcb786d614722ee3f4472e33f6"},
-    {file = "mypy-1.12.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:186e0c8346efc027ee1f9acf5ca734425fc4f7dc2b60144f0fbe27cc19dc7931"},
-    {file = "mypy-1.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:673ba1140a478b50e6d265c03391702fa11a5c5aff3f54d69a62a48da32cb811"},
-    {file = "mypy-1.12.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9fb83a7be97c498176fb7486cafbb81decccaef1ac339d837c377b0ce3743a7f"},
-    {file = "mypy-1.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:389e307e333879c571029d5b93932cf838b811d3f5395ed1ad05086b52148fb0"},
-    {file = "mypy-1.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94b2048a95a21f7a9ebc9fbd075a4fcd310410d078aa0228dbbad7f71335e042"},
-    {file = "mypy-1.12.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ee5932370ccf7ebf83f79d1c157a5929d7ea36313027b0d70a488493dc1b179"},
-    {file = "mypy-1.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:19bf51f87a295e7ab2894f1d8167622b063492d754e69c3c2fed6563268cb42a"},
-    {file = "mypy-1.12.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d34167d43613ffb1d6c6cdc0cc043bb106cac0aa5d6a4171f77ab92a3c758bcc"},
-    {file = "mypy-1.12.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:427878aa54f2e2c5d8db31fa9010c599ed9f994b3b49e64ae9cd9990c40bd635"},
-    {file = "mypy-1.12.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5fcde63ea2c9f69d6be859a1e6dd35955e87fa81de95bc240143cf00de1f7f81"},
-    {file = "mypy-1.12.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d54d840f6c052929f4a3d2aab2066af0f45a020b085fe0e40d4583db52aab4e4"},
-    {file = "mypy-1.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:20db6eb1ca3d1de8ece00033b12f793f1ea9da767334b7e8c626a4872090cf02"},
-    {file = "mypy-1.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b16fe09f9c741d85a2e3b14a5257a27a4f4886c171d562bc5a5e90d8591906b8"},
-    {file = "mypy-1.12.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0dcc1e843d58f444fce19da4cce5bd35c282d4bde232acdeca8279523087088a"},
-    {file = "mypy-1.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e10ba7de5c616e44ad21005fa13450cd0de7caaa303a626147d45307492e4f2d"},
-    {file = "mypy-1.12.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0e6fe449223fa59fbee351db32283838a8fee8059e0028e9e6494a03802b4004"},
-    {file = "mypy-1.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:dc6e2a2195a290a7fd5bac3e60b586d77fc88e986eba7feced8b778c373f9afe"},
-    {file = "mypy-1.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:de5b2a8988b4e1269a98beaf0e7cc71b510d050dce80c343b53b4955fff45f19"},
-    {file = "mypy-1.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:843826966f1d65925e8b50d2b483065c51fc16dc5d72647e0236aae51dc8d77e"},
-    {file = "mypy-1.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9fe20f89da41a95e14c34b1ddb09c80262edcc295ad891f22cc4b60013e8f78d"},
-    {file = "mypy-1.12.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8135ffec02121a75f75dc97c81af7c14aa4ae0dda277132cfcd6abcd21551bfd"},
-    {file = "mypy-1.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:a7b76fa83260824300cc4834a3ab93180db19876bce59af921467fd03e692810"},
-    {file = "mypy-1.12.1-py3-none-any.whl", hash = "sha256:ce561a09e3bb9863ab77edf29ae3a50e65685ad74bba1431278185b7e5d5486e"},
-    {file = "mypy-1.12.1.tar.gz", hash = "sha256:f5b3936f7a6d0e8280c9bdef94c7ce4847f5cdfc258fbb2c29a8c1711e8bb96d"},
+    {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"},
+    {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"},
+    {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"},
+    {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"},
+    {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"},
+    {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"},
+    {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"},
+    {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"},
+    {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"},
+    {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"},
+    {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"},
+    {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"},
+    {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"},
+    {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"},
+    {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"},
+    {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"},
+    {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"},
+    {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"},
+    {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"},
+    {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"},
+    {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"},
+    {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"},
+    {file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"},
+    {file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"},
+    {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"},
+    {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"},
+    {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"},
+    {file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"},
+    {file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"},
+    {file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"},
+    {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"},
+    {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"},
 ]
 
 [package.dependencies]
@@ -480,6 +483,7 @@ typing-extensions = ">=4.6.0"
 
 [package.extras]
 dmypy = ["psutil (>=4.0)"]
+faster-cache = ["orjson"]
 install-types = ["pip"]
 mypyc = ["setuptools (>=50)"]
 reports = ["lxml"]
@@ -519,13 +523,13 @@ files = [
 
 [[package]]
 name = "packaging"
-version = "24.1"
+version = "24.2"
 description = "Core utilities for Python packages"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
-    {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
+    {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
+    {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
 ]
 
 [[package]]
@@ -946,29 +950,29 @@ cffi = {version = "*", markers = "implementation_name == \"pypy\""}
 
 [[package]]
 name = "ruff"
-version = "0.7.0"
+version = "0.7.3"
 description = "An extremely fast Python linter and code formatter, written in Rust."
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "ruff-0.7.0-py3-none-linux_armv6l.whl", hash = "sha256:0cdf20c2b6ff98e37df47b2b0bd3a34aaa155f59a11182c1303cce79be715628"},
-    {file = "ruff-0.7.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:496494d350c7fdeb36ca4ef1c9f21d80d182423718782222c29b3e72b3512737"},
-    {file = "ruff-0.7.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:214b88498684e20b6b2b8852c01d50f0651f3cc6118dfa113b4def9f14faaf06"},
-    {file = "ruff-0.7.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:630fce3fefe9844e91ea5bbf7ceadab4f9981f42b704fae011bb8efcaf5d84be"},
-    {file = "ruff-0.7.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:211d877674e9373d4bb0f1c80f97a0201c61bcd1e9d045b6e9726adc42c156aa"},
-    {file = "ruff-0.7.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:194d6c46c98c73949a106425ed40a576f52291c12bc21399eb8f13a0f7073495"},
-    {file = "ruff-0.7.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:82c2579b82b9973a110fab281860403b397c08c403de92de19568f32f7178598"},
-    {file = "ruff-0.7.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9af971fe85dcd5eaed8f585ddbc6bdbe8c217fb8fcf510ea6bca5bdfff56040e"},
-    {file = "ruff-0.7.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b641c7f16939b7d24b7bfc0be4102c56562a18281f84f635604e8a6989948914"},
-    {file = "ruff-0.7.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d71672336e46b34e0c90a790afeac8a31954fd42872c1f6adaea1dff76fd44f9"},
-    {file = "ruff-0.7.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ab7d98c7eed355166f367597e513a6c82408df4181a937628dbec79abb2a1fe4"},
-    {file = "ruff-0.7.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1eb54986f770f49edb14f71d33312d79e00e629a57387382200b1ef12d6a4ef9"},
-    {file = "ruff-0.7.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:dc452ba6f2bb9cf8726a84aa877061a2462afe9ae0ea1d411c53d226661c601d"},
-    {file = "ruff-0.7.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:4b406c2dce5be9bad59f2de26139a86017a517e6bcd2688da515481c05a2cb11"},
-    {file = "ruff-0.7.0-py3-none-win32.whl", hash = "sha256:f6c968509f767776f524a8430426539587d5ec5c662f6addb6aa25bc2e8195ec"},
-    {file = "ruff-0.7.0-py3-none-win_amd64.whl", hash = "sha256:ff4aabfbaaba880e85d394603b9e75d32b0693152e16fa659a3064a85df7fce2"},
-    {file = "ruff-0.7.0-py3-none-win_arm64.whl", hash = "sha256:10842f69c245e78d6adec7e1db0a7d9ddc2fff0621d730e61657b64fa36f207e"},
-    {file = "ruff-0.7.0.tar.gz", hash = "sha256:47a86360cf62d9cd53ebfb0b5eb0e882193fc191c6d717e8bef4462bc3b9ea2b"},
+    {file = "ruff-0.7.3-py3-none-linux_armv6l.whl", hash = "sha256:34f2339dc22687ec7e7002792d1f50712bf84a13d5152e75712ac08be565d344"},
+    {file = "ruff-0.7.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fb397332a1879b9764a3455a0bb1087bda876c2db8aca3a3cbb67b3dbce8cda0"},
+    {file = "ruff-0.7.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:37d0b619546103274e7f62643d14e1adcbccb242efda4e4bdb9544d7764782e9"},
+    {file = "ruff-0.7.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d59f0c3ee4d1a6787614e7135b72e21024875266101142a09a61439cb6e38a5"},
+    {file = "ruff-0.7.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:44eb93c2499a169d49fafd07bc62ac89b1bc800b197e50ff4633aed212569299"},
+    {file = "ruff-0.7.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d0242ce53f3a576c35ee32d907475a8d569944c0407f91d207c8af5be5dae4e"},
+    {file = "ruff-0.7.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6b6224af8b5e09772c2ecb8dc9f3f344c1aa48201c7f07e7315367f6dd90ac29"},
+    {file = "ruff-0.7.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c50f95a82b94421c964fae4c27c0242890a20fe67d203d127e84fbb8013855f5"},
+    {file = "ruff-0.7.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7f3eff9961b5d2644bcf1616c606e93baa2d6b349e8aa8b035f654df252c8c67"},
+    {file = "ruff-0.7.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8963cab06d130c4df2fd52c84e9f10d297826d2e8169ae0c798b6221be1d1d2"},
+    {file = "ruff-0.7.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:61b46049d6edc0e4317fb14b33bd693245281a3007288b68a3f5b74a22a0746d"},
+    {file = "ruff-0.7.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:10ebce7696afe4644e8c1a23b3cf8c0f2193a310c18387c06e583ae9ef284de2"},
+    {file = "ruff-0.7.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3f36d56326b3aef8eeee150b700e519880d1aab92f471eefdef656fd57492aa2"},
+    {file = "ruff-0.7.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5d024301109a0007b78d57ab0ba190087b43dce852e552734ebf0b0b85e4fb16"},
+    {file = "ruff-0.7.3-py3-none-win32.whl", hash = "sha256:4ba81a5f0c5478aa61674c5a2194de8b02652f17addf8dfc40c8937e6e7d79fc"},
+    {file = "ruff-0.7.3-py3-none-win_amd64.whl", hash = "sha256:588a9ff2fecf01025ed065fe28809cd5a53b43505f48b69a1ac7707b1b7e4088"},
+    {file = "ruff-0.7.3-py3-none-win_arm64.whl", hash = "sha256:1713e2c5545863cdbfe2cbce21f69ffaf37b813bfd1fb3b90dc9a6f1963f5a8c"},
+    {file = "ruff-0.7.3.tar.gz", hash = "sha256:e1d1ba2e40b6e71a61b063354d04be669ab0d39c352461f3d789cac68b54a313"},
 ]
 
 [[package]]
@@ -1036,6 +1040,24 @@ files = [
 docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"]
 test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"]
 
+[[package]]
+name = "typer-slim"
+version = "0.13.0"
+description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "typer_slim-0.13.0-py3-none-any.whl", hash = "sha256:33aad8029b717ee98228315f208cfae2f247765796f243ca314cb09fd652dc44"},
+    {file = "typer_slim-0.13.0.tar.gz", hash = "sha256:7d7d3b0ad211ddcc9bcfe3d51e7d5903b0f10ae07938cca33f8af1f578c5007b"},
+]
+
+[package.dependencies]
+click = ">=8.0.0"
+typing-extensions = ">=3.7.4.3"
+
+[package.extras]
+standard = ["rich (>=10.11.0)", "shellingham (>=1.3.0)"]
+
 [[package]]
 name = "typing-extensions"
 version = "4.12.2"
@@ -1049,13 +1071,13 @@ files = [
 
 [[package]]
 name = "virtualenv"
-version = "20.27.0"
+version = "20.27.1"
 description = "Virtual Python Environment builder"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "virtualenv-20.27.0-py3-none-any.whl", hash = "sha256:44a72c29cceb0ee08f300b314848c86e57bf8d1f13107a5e671fb9274138d655"},
-    {file = "virtualenv-20.27.0.tar.gz", hash = "sha256:2ca56a68ed615b8fe4326d11a0dca5dfbe8fd68510fb6c6349163bed3c15f2b2"},
+    {file = "virtualenv-20.27.1-py3-none-any.whl", hash = "sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4"},
+    {file = "virtualenv-20.27.1.tar.gz", hash = "sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba"},
 ]
 
 [package.dependencies]
@@ -1095,4 +1117,4 @@ dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"]
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.11"
-content-hash = "cd68864809d988a240b95f356dda4cb84eff79826fb0617824055746ff0a7412"
+content-hash = "25b0258c603b749cedb7252fa62b6959f384f9ed0f22bd20f4738f9278f02709"
diff --git a/pyproject.toml b/pyproject.toml
index 5a07f9f..1962b0c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -3,13 +3,17 @@ name = "code-embedder"
 version = "v0.5.2"
 description = "Embed python code from script to markdown"
 authors = ["Katerina Vankova <katerinavankova1@gmail.com>"]
+readme = "README.md"
 license = "MIT"
 packages = [{include = "src"}]
 
+[tool.poetry.scripts]
+code-embedder = "src.main:app"
+
 [tool.poetry.dependencies]
 python = "^3.11"
-argparse = "^1.4.0"
 loguru = "^0.7.2"
+typer-slim = "^0.13.0"
 
 [tool.poetry.group.dev.dependencies]
 ipykernel = "^6.29.5"
diff --git a/src/code_embedding.py b/src/code_embedding.py
index a60a9f3..baea66b 100644
--- a/src/code_embedding.py
+++ b/src/code_embedding.py
@@ -9,27 +9,36 @@ class CodeEmbedder:
     def __init__(
         self,
         readme_paths: list[str],
+        changed_files: list[str] | None,
         script_metadata_extractor: ScriptMetadataExtractorInterface,
         script_content_reader: ScriptContentReaderInterface,
     ) -> None:
         self._readme_paths = readme_paths
+        self._changed_files = changed_files
         self._script_metadata_extractor = script_metadata_extractor
         self._script_content_reader = script_content_reader
 
     def __call__(self) -> None:
         for readme_path in self._readme_paths:
-            self._process_readme(readme_path)
+            self._process_readme(readme_path=readme_path)
 
     def _process_readme(self, readme_path: str) -> None:
         readme_content = self._read_readme(readme_path)
         if not readme_content:
-            logger.info(f"Empty README in path {readme_path}. Skipping.")
+            logger.info(f"Empty markdown file {readme_path}. Skipping.")
             return
 
         scripts = self._extract_scripts(readme_content=readme_content, readme_path=readme_path)
+
         if not scripts:
             return
 
+        if self._changed_files:
+            # Reduce scripts to only the ones that have changed only when there were no changes
+            # to the readme file
+            if readme_path not in self._changed_files:
+                scripts = [script for script in scripts if script.path in self._changed_files]
+
         script_contents = self._script_content_reader.read(scripts=scripts)
 
         self._update_readme(
@@ -40,7 +49,6 @@ class CodeEmbedder:
 
     def _read_readme(self, readme_path: str) -> list[str]:
         if not readme_path.endswith(".md"):
-            logger.error("README path must end with .md")
             raise ValueError("README path must end with .md")
 
         with open(readme_path) as readme_file:
@@ -51,10 +59,10 @@ class CodeEmbedder:
     ) -> list[ScriptMetadata] | None:
         scripts = self._script_metadata_extractor.extract(readme_content=readme_content)
         if not scripts:
-            logger.info(f"No script paths found in README in path {readme_path}. Skipping.")
+            logger.debug(f"No script paths found in README in path {readme_path}. Skipping.")
             return None
         logger.info(
-            f"""Found script paths in README in path {readme_path}:
+            f"""Found script paths in {readme_path}:
             {set(script.path for script in scripts)}"""
         )
         return scripts
diff --git a/src/log_level.py b/src/log_level.py
new file mode 100644
index 0000000..71e666c
--- /dev/null
+++ b/src/log_level.py
@@ -0,0 +1,7 @@
+from enum import Enum
+
+
+class LogLevel(str, Enum):
+    INFO = "INFO"
+    WARNING = "WARNING"
+    ERROR = "ERROR"
diff --git a/src/main.py b/src/main.py
index d482729..4ab0666 100644
--- a/src/main.py
+++ b/src/main.py
@@ -1,24 +1,46 @@
-import argparse
+import glob
+import sys
 
+import typer
 from loguru import logger
 
 from src.code_embedding import CodeEmbedder
 from src.script_content_reader import ScriptContentReader
 from src.script_metadata_extractor import ScriptMetadataExtractor
 
-parser = argparse.ArgumentParser()
-parser.add_argument(
-    "--readme-paths", nargs="+", type=str, help="Paths to Readme files", default="README.md"
-)
-args = parser.parse_args()
+logger.remove()
+logger.add(sys.stderr, level="ERROR")
+
+app = typer.Typer(rich_markup_mode=None)
+
+
+@app.command(help="Embed code from scripts to markdown files.")
+def run(
+    all_files: bool = typer.Option(
+        False, "--all-files", help="Process all files in the repository."
+    ),
+    changed_files: list[str] = typer.Argument(None, help="List of changed files to process."),
+):
+    readme_paths = glob.glob("**/*.md", recursive=True)
+
+    if not readme_paths:
+        logger.info("No markdown files found in the current repository.")
+        exit(0)
 
-if __name__ == "__main__":
     script_metadata_extractor = ScriptMetadataExtractor()
     script_content_reader = ScriptContentReader()
+    files = changed_files if not all_files else None  # None for all files
+
     code_embedder = CodeEmbedder(
-        readme_paths=args.readme_paths,
+        readme_paths=readme_paths,
+        changed_files=files,
         script_metadata_extractor=script_metadata_extractor,
         script_content_reader=script_content_reader,
     )
+
     code_embedder()
     logger.info("Code Embedder finished successfully.")
+
+
+if __name__ == "__main__":
+    app()
diff --git a/src/script_content_reader.py b/src/script_content_reader.py
index 82ddd5f..2227a6c 100644
--- a/src/script_content_reader.py
+++ b/src/script_content_reader.py
@@ -2,8 +2,6 @@ import ast
 import re
 from typing import Protocol
 
-from loguru import logger
-
 from src.script_metadata import ScriptMetadata
 
 
@@ -18,6 +16,7 @@ class ScriptContentReader:
 
     def read(self, scripts: list[ScriptMetadata]) -> list[ScriptMetadata]:
         scripts_with_full_contents = self._read_full_script(scripts)
+
         return self._process_scripts(scripts_with_full_contents)
 
     def _read_full_script(self, scripts: list[ScriptMetadata]) -> list[ScriptMetadata]:
@@ -27,11 +26,10 @@ class ScriptContentReader:
             try:
                 with open(script.path) as script_file:
                     script.content = script_file.read()
-
-                scripts_with_full_contents.append(script)
+                    scripts_with_full_contents.append(script)
 
             except FileNotFoundError:
-                logger.error(f"Error: {script.path} not found. Skipping.")
+                raise FileNotFoundError(f"File {script.path} not found.")
 
         return scripts_with_full_contents
 
@@ -71,14 +69,17 @@ class ScriptContentReader:
             start, end = self._extract_section_part(
                 lines=lines, section=script.extraction_part
             )
-            if not self._validate_section_bounds(start, end, script):
-                return ""
 
-        if not start and not end:
-            logger.error(
-                f"Part {script.extraction_part} not found in {script.path}. Skipping."
-            )
-            return ""
+        if not start or not end:
+            if script.extraction_type == "object":
+                raise ValueError(
+                    f"Object {script.extraction_part} not found in {script.path}. "
+                )
+            elif script.extraction_type == "section":
+                raise ValueError(
+                    f"Part {script.extraction_part} not found in {script.path}. "
+                    "Either start and/or end of the section is missing."
+                )
 
         return "\n".join(lines[start:end])
 
@@ -116,22 +117,3 @@ class ScriptContentReader:
                 return start, i
 
         return None, None
-
-    def _validate_section_bounds(
-        self, start: int | None, end: int | None, script: ScriptMetadata
-    ) -> bool:
-        if not start:
-            logger.error(
-                f"Start of section {script.extraction_part} not found in {script.path}. "
-                "Skipping."
-            )
-            return False
-
-        if not end:
-            logger.error(
-                f"End of section {script.extraction_part} not found in {script.path}. "
-                "Skipping."
-            )
-            return False
-
-        return True
diff --git a/src/script_metadata_extractor.py b/src/script_metadata_extractor.py
index 8613655..e42e956 100644
--- a/src/script_metadata_extractor.py
+++ b/src/script_metadata_extractor.py
@@ -1,8 +1,6 @@
 import re
 from typing import Protocol
 
-from loguru import logger
-
 from src.script_metadata import ScriptMetadata
 
 
@@ -48,21 +46,19 @@ class ScriptMetadataExtractor:
         extraction_type = tag_items[2].strip() if len(tag_items) > 2 else "full"
 
         if extraction_type not in self.extraction_type_mapping.keys():
-            logger.error(
+            raise ValueError(
                 f"Unknown extraction type {extraction_type}. Allowed are only "
                 f"`{self._object_separator}` for object and "
-                f"`{self._section_separator}` for section. Skipping."
+                f"`{self._section_separator}` for section."
             )
-            return None
 
         extraction_part = tag_items[3].strip() if len(tag_items) > 3 else None
 
         if not extraction_part and extraction_type != "full":
-            logger.error(
-                f"Extraction part is not provided for {extraction_type} extraction type. "
-                "Skipping."
+            raise ValueError(
+                f"Extraction part (section or object name) is not provided for "
+                f"{extraction_type} extraction type."
             )
-            return None
 
         return {
             "start": row,


Test patch:
diff --git a/tests/data/expected_readme0.md b/tests/data/expected_readme0.md
index 4e86e93..f9333cf 100644
--- a/tests/data/expected_readme0.md
+++ b/tests/data/expected_readme0.md
@@ -5,8 +5,3 @@ This will be filled:
 print("Hello, World! from script")
 
 ```
-
-This will not find the script:
-```python:unknown_path.py
-print("Hello, World!")
-```
diff --git a/tests/data/expected_readme4.md b/tests/data/expected_readme4.md
new file mode 100644
index 0000000..ba8fd9b
--- /dev/null
+++ b/tests/data/expected_readme4.md
@@ -0,0 +1,5 @@
+# README
+
+This won't find the script:
+```python:unknown_path.py
+```
diff --git a/tests/data/readme0.md b/tests/data/readme0.md
index d580a7c..f51e71e 100644
--- a/tests/data/readme0.md
+++ b/tests/data/readme0.md
@@ -3,8 +3,3 @@
 This will be filled:
 ```python:tests/data/example.py
 ```
-
-This will not find the script:
-```python:unknown_path.py
-print("Hello, World!")
-```
diff --git a/tests/data/readme4.md b/tests/data/readme4.md
new file mode 100644
index 0000000..ba8fd9b
--- /dev/null
+++ b/tests/data/readme4.md
@@ -0,0 +1,5 @@
+# README
+
+This won't find the script:
+```python:unknown_path.py
+```
diff --git a/tests/test_code_embedding.py b/tests/test_code_embedding.py
index d2ef547..02af2fb 100644
--- a/tests/test_code_embedding.py
+++ b/tests/test_code_embedding.py
@@ -48,6 +48,7 @@ def test_code_embedder(
 
     code_embedder = CodeEmbedder(
         readme_paths=[str(temp_readme_path)],
+        changed_files=None,
         script_metadata_extractor=ScriptMetadataExtractor(),
         script_content_reader=ScriptContentReader(),
     )
@@ -61,3 +62,14 @@ def test_code_embedder(
         updated_readme_content = updated_file.readlines()
 
     assert updated_readme_content == expected_readme_content
+
+
+def test_code_embedder_unknown_path() -> None:
+    code_embedder = CodeEmbedder(
+        readme_paths=["tests/data/readme4.md"],
+        changed_files=None,
+        script_metadata_extractor=ScriptMetadataExtractor(),
+        script_content_reader=ScriptContentReader(),
+    )
+    with pytest.raises(FileNotFoundError):
+        code_embedder()
diff --git a/tests/test_script_content_reader.py b/tests/test_script_content_reader.py
index 45a1481..fb4303a 100644
--- a/tests/test_script_content_reader.py
+++ b/tests/test_script_content_reader.py
@@ -91,24 +91,17 @@ def test_read_full_script(script: ScriptMetadata, expected_script: ScriptMetadat
     assert script_content_reader._read_full_script([script]) == [expected_script]
 
 
+def test_read_full_script_unknown_path() -> None:
+    script_content_reader = ScriptContentReader()
+    with pytest.raises(FileNotFoundError):
+        script_content_reader._read_full_script(
+            [create_script_metadata(path="unknown_path.py")]
+        )
+
+
 @pytest.mark.parametrize(
     "script_metadata, expected_script_metadata",
     [
-        # Missing section
-        (
-            create_script_metadata(
-                path="tests/data/example.py",
-                extraction_part="no_section",
-                extraction_type="section",
-                content='print("Hello, World! from script")\n',
-            ),
-            create_script_metadata(
-                path="tests/data/example.py",
-                extraction_part="no_section",
-                extraction_type="section",
-                content="",
-            ),
-        ),
         # Section
         (
             create_script_metadata(
@@ -203,7 +196,7 @@ def test_read_full_script(script: ScriptMetadata, expected_script: ScriptMetadat
             ),
         ),
     ],
-    ids=["missing_section", "section", "object", "object_class"],
+    ids=["section", "object", "object_class"],
 )
 def test_read_script_section(
     script_metadata: ScriptMetadata, expected_script_metadata: ScriptMetadata
@@ -213,3 +206,37 @@ def test_read_script_section(
     assert script_content_reader._update_script_content_with_extraction_part(
         [script_metadata]
     ) == [expected_script_metadata]
+
+
+@pytest.mark.parametrize(
+    "script_metadata, error_message",
+    [
+        # Unknown section
+        (
+            create_script_metadata(
+                path="tests/data/example.py",
+                extraction_part="no_section",
+                extraction_type="section",
+                content='print("Hello, World! from script")\n',
+            ),
+            "Part no_section not found in tests/data/example.py. "
+            "Either start and/or end of the section is missing.",
+        ),
+        # Unknown object
+        (
+            create_script_metadata(
+                path="tests/data/example.py",
+                extraction_part="no_object",
+                extraction_type="object",
+            ),
+            "Object no_object not found in tests/data/example.py. ",
+        ),
+    ],
+    ids=["unknown_section", "unknown_object"],
+)
+def test_read_script_unknown_extraction_part(
+    script_metadata: ScriptMetadata, error_message: str
+) -> None:
+    script_content_reader = ScriptContentReader()
+    with pytest.raises(ValueError, match=error_message):
+        script_content_reader._update_script_content_with_extraction_part([script_metadata])