Repository: dashmug/glue-utils (language: python)

Issue:
Integrate pre-commit



Discussion / hints:

Fix patch:
diff --git a/.dockerignore b/.dockerignore
index 53dd0e3..d75a09c 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -12,4 +12,4 @@ mypy-results
 poetry.*
 test
 Dockerfile
-Makefile
\ No newline at end of file
+Makefile
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2a444a7..b7adb8e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -87,4 +87,4 @@ jobs:
     steps:
       - uses: actions/checkout@v4.1.6
       - name: Check Makefile for errors
-        uses: Uno-Takashi/checkmake-action@main
\ No newline at end of file
+        uses: Uno-Takashi/checkmake-action@main
diff --git a/.gitignore b/.gitignore
index 2814d1c..7e3742f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,4 +12,4 @@ wheels/
 # coverage
 .coverage
 coverage
-htmlcov
\ No newline at end of file
+htmlcov
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..d1aa69c
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,62 @@
+# See https://pre-commit.com for more information
+# See https://pre-commit.com/hooks.html for more hooks
+repos:
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v4.6.0
+    hooks:
+      - id: check-case-conflict
+      - id: check-executables-have-shebangs
+      - id: check-merge-conflict
+      - id: end-of-file-fixer
+      - id: mixed-line-ending
+      - id: trailing-whitespace
+  - repo: local
+    hooks:
+      - id: poetry-check
+        name: Validate poetry config
+        entry: poetry check
+        language: python
+        pass_filenames: false
+        files: ^(.*/)?pyproject\.toml$
+      - id: poetry-lock
+        name: Update poetry lock file
+        entry: poetry lock
+        language: python
+        pass_filenames: false
+        files: ^(.*/)?(poetry\.lock|pyproject\.toml)$
+      - id: poetry-install
+        name: Install dependencies from the poetry lock file
+        entry: poetry install --sync
+        language: python
+        pass_filenames: false
+        stages: [post-checkout, post-merge]
+        always_run: true
+      - id: format-python
+        name: Format Python code
+        language: system
+        types: [python]
+        entry: poetry run ruff format
+      - id: lint-python
+        name: Lint Python code
+        language: system
+        types: [python]
+        entry: poetry run ruff check
+        args: [--fix]
+      - id: check-python-type-annotations
+        name: Check Python Type Annotations
+        language: system
+        types: [python]
+        entry: env MYPYPATH=src poetry run mypy .
+        pass_filenames: false
+      - id: check-python-imports
+        name: Check Python Imports
+        language: system
+        types: [python]
+        entry: poetry run lint-imports
+        pass_filenames: false
+      - id: update-requirements
+        name: Update requirements.txt
+        language: system
+        files: poetry.lock
+        entry: poetry export --with=dev --output docker/requirements.txt
+        pass_filenames: false
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 81588b5..a5db25f 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -19,4 +19,3 @@
     "python.testing.pytestEnabled": true,
     "autoDocstring.docstringFormat": "numpy-notypes"
   }
-  
\ No newline at end of file
diff --git a/Makefile b/Makefile
index 78b7b81..d0cbcd2 100644
--- a/Makefile
+++ b/Makefile
@@ -61,7 +61,7 @@ shell: docker/requirements.txt ## Enter a shell in the container
 
 
 .PHONY: checks
-checks: format typecheck importcheck test 
+checks: format typecheck importcheck test
 
 
 .PHONY: clean
@@ -91,4 +91,11 @@ bumpver-minor: ## Bump minor version
 
 .PHONY: bumpver-major
 bumpver-major: ## Bump major version
-	@poetry run bumpver update --no-fetch --major --tag=final
\ No newline at end of file
+	@poetry run bumpver update --no-fetch --major --tag=final
+
+
+.PHONY: githooks
+githooks: ## Install/update project git hooks
+	@poetry run pre-commit install --install-hooks
+	@poetry run pre-commit autoupdate
+	@poetry run pre-commit run --all-files
diff --git a/README.md b/README.md
index 97eee9a..31cf8de 100644
--- a/README.md
+++ b/README.md
@@ -22,13 +22,13 @@ pip install glue-utils
 
 ### For development...
 
-This library does not include `pyspark` and `aws-glue-libs` as 
-dependencies as they are already pre-installed in Glue's runtime 
+This library does not include `pyspark` and `aws-glue-libs` as
+dependencies as they are already pre-installed in Glue's runtime
 environment.
 
-To help in developing your Glue jobs locally in your IDE, it is helpful 
-to install `pyspark` and `aws-glue-libs`. Unfortunately, `aws-glue-libs` 
-is not available through PyPI so we can only install it from its git 
+To help in developing your Glue jobs locally in your IDE, it is helpful
+to install `pyspark` and `aws-glue-libs`. Unfortunately, `aws-glue-libs`
+is not available through PyPI so we can only install it from its git
 repository.
 
 ```sh
@@ -36,10 +36,10 @@ pip install pyspark==3.3.0
 pip install git+https://github.com/awslabs/aws-glue-libs.git@master
 ```
 
-To make your local environment as close to Glue's runtime as possible, 
+To make your local environment as close to Glue's runtime as possible,
 use the versions specified in [this document](https://docs.aws.amazon.com/glue/latest/dg/aws-glue-programming-python-libraries.html#glue-modules-provided).
 
 ## Documentation
 
-For more details on what you can use this library for, check out the 
-[project wiki](https://github.com/dashmug/glue-utils/wiki).
\ No newline at end of file
+For more details on what you can use this library for, check out the
+[project wiki](https://github.com/dashmug/glue-utils/wiki).
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 7fbd41c..81cada5 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -1,3 +1,4 @@
+# ----------------------- Base -----------------------
 FROM amazon/aws-glue-libs:glue_libs_4.0.0_image_01 as base
 
 # Copy requirements file that contains tooling.
@@ -9,21 +10,18 @@ RUN pip3 install --no-cache-dir --no-warn-script-location --user --upgrade pip==
   && pip3 install --no-cache-dir --no-warn-script-location --user -r requirements.txt
 
 
+# ----------------------- Test -----------------------
 FROM base as test
 
-# COPY --from=base /home/glue_user/workspace /home/glue_user/workspace
-# COPY --from=base /home/glue_user/.local/lib/python3.10/site-packages /home/glue_user/.local/lib/python3.10/site-packages
 COPY src pyproject.toml README.md ./
 
 # Install this package.
 RUN pip3 install --no-cache-dir --no-warn-script-location --user .
 
 
+# --------------------- Coverage ---------------------
 FROM base as coverage
 
-# COPY --from=base /home/glue_user/workspace /home/glue_user/workspace
-# COPY --from=base /home/glue_user/.local/lib/python3.10/site-packages /home/glue_user/.local/lib/python3.10/site-packages
-
 # Pass the host user to the container.
 ARG USER_ID
 
@@ -46,39 +44,3 @@ COPY src pyproject.toml README.md ./
 RUN pip3 install --no-cache-dir --no-warn-script-location --user . \
   # Prepare a /tmp directory needed by Spark to start.
   && mkdir -p /tmp/spark-events
-
-
-
-# FROM amazon/aws-glue-libs:glue_libs_4.0.0_image_01
-
-# # Pass the host user to the container.
-# ARG USER_ID
-
-# # Switch to root to be able to make changes in the container filesystem.
-# USER root
-
-# # Change UID of glue_user to be the same as host user. This allows
-# # JupyterLab to write to the host system as glue_user.
-# RUN usermod -u "$USER_ID" glue_user \
-#   # Clean up /tmp which may already have glue_user-owned files with the
-#   # old UID.
-#   && rm -rf /tmp/*
-
-# # Switch to glue_user to be able to make changes for the user itself.
-# USER glue_user
-
-# # Copy requirements file that contains tooling.
-# WORKDIR /home/glue_user/workspace
-# COPY docker/requirements.txt ./
-
-# RUN pip3 install --no-cache-dir --no-warn-script-location --user --upgrade pip==24.0 \
-#   # Install dev requirements.
-#   && pip3 install --no-cache-dir --no-warn-script-location --user -r requirements.txt \
-#   # Prepare a /tmp directory needed by Spark to start.
-#   && mkdir -p /tmp/spark-events
-
-# COPY src pyproject.toml README.md ./
-
-# # Install this package.
-# RUN pip3 install --no-cache-dir --no-warn-script-location --user .
-
diff --git a/docker/requirements.txt b/docker/requirements.txt
index 08beaef..fb96ab9 100644
--- a/docker/requirements.txt
+++ b/docker/requirements.txt
@@ -1,6 +1,9 @@
 bumpver==2023.1129 ; python_version >= "3.9" and python_version < "4.0" \
     --hash=sha256:2a09813066d92ae2eabf882d4f9a88ebd60135e828c424bdf7800e1723e15010 \
     --hash=sha256:b2a55c0224215b6ca1c3a0c99827749927b7c61cbb5dfef75565dbda8e75f687
+cfgv==3.4.0 ; python_version >= "3.9" and python_version < "4.0" \
+    --hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \
+    --hash=sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560
 click==8.1.7 ; python_version >= "3.9" and python_version < "4.0" \
     --hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \
     --hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de
@@ -60,9 +63,15 @@ coverage[toml]==7.5.1 ; python_version >= "3.9" and python_version < "4.0" \
     --hash=sha256:ef48e2707fb320c8f139424a596f5b69955a85b178f15af261bab871873bb987 \
     --hash=sha256:f152cbf5b88aaeb836127d920dd0f5e7edff5a66f10c079157306c4343d86c19 \
     --hash=sha256:fc0b4d8bfeabd25ea75e94632f5b6e047eef8adaed0c2161ada1e922e7f7cece
+distlib==0.3.8 ; python_version >= "3.9" and python_version < "4.0" \
+    --hash=sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784 \
+    --hash=sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64
 exceptiongroup==1.2.1 ; python_version >= "3.9" and python_version < "3.11" \
     --hash=sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad \
     --hash=sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16
+filelock==3.14.0 ; python_version >= "3.9" and python_version < "4.0" \
+    --hash=sha256:43339835842f110ca7ae60f1e1c160714c5a6afd15a2873419ab185334975c0f \
+    --hash=sha256:6ea72da3be9b8c82afd3edcf99f2fffbb5076335a5ae4d03248bb5b6c3eae78a
 grimp==3.2 ; python_version >= "3.9" and python_version < "4.0" \
     --hash=sha256:02f58a0afa5fa06f6f98bad28a5e1a33c9e32b2c8281df7069f932dc56719335 \
     --hash=sha256:038779ebb05509de9c418555e5d7b8857f90d7a095baf0f4e0ec261fcfae22ba \
@@ -137,6 +146,9 @@ grimp==3.2 ; python_version >= "3.9" and python_version < "4.0" \
     --hash=sha256:f9887a8ddbd3e3ccb2d21fec98f4ab1b311baff25013d21486abb9f63b1d0bae \
     --hash=sha256:fd1a7d82a3d4529561d3fb152797d66b0e63823c8b875662fc7c7d7231752c42 \
     --hash=sha256:ff9e87f38c6a31d7aa16f8cc93efe8a5ba3195ea8f9260eceed2b56395965276
+identify==2.5.36 ; python_version >= "3.9" and python_version < "4.0" \
+    --hash=sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa \
+    --hash=sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d
 import-linter==2.0 ; python_version >= "3.9" and python_version < "4.0" \
     --hash=sha256:200f9b46d20a055c1f6f514e4cd8074852261bc9c8733d028201a55be6058bb0 \
     --hash=sha256:b067cf0cdbf11c4f87524e32c2e3eaa62d46572e9e06511e6b453198b15b1c9a
@@ -183,12 +195,21 @@ mypy==1.10.0 ; python_version >= "3.9" and python_version < "4.0" \
     --hash=sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727 \
     --hash=sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976 \
     --hash=sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4
+nodeenv==1.8.0 ; python_version >= "3.9" and python_version < "4.0" \
+    --hash=sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2 \
+    --hash=sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec
 packaging==24.0 ; python_version >= "3.9" and python_version < "4.0" \
     --hash=sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5 \
     --hash=sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9
+platformdirs==4.2.2 ; python_version >= "3.9" and python_version < "4.0" \
+    --hash=sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee \
+    --hash=sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3
 pluggy==1.5.0 ; python_version >= "3.9" and python_version < "4.0" \
     --hash=sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1 \
     --hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669
+pre-commit==3.7.1 ; python_version >= "3.9" and python_version < "4.0" \
+    --hash=sha256:8ca3ad567bc78a4972a3f1a477e94a79d4597e8140a6e0b651c5e33899c3654a \
+    --hash=sha256:fae36fd1d7ad7d6a5a1c0b0d5adb2ed1a3bda5a21bf6c3e5372073d7a11cd4c5
 pytest-cov==5.0.0 ; python_version >= "3.9" and python_version < "4.0" \
     --hash=sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652 \
     --hash=sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857
@@ -198,6 +219,58 @@ pytest-randomly==3.15.0 ; python_version >= "3.9" and python_version < "4.0" \
 pytest==8.2.1 ; python_version >= "3.9" and python_version < "4.0" \
     --hash=sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd \
     --hash=sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1
+pyyaml==6.0.1 ; python_version >= "3.9" and python_version < "4.0" \
+    --hash=sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5 \
+    --hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \
+    --hash=sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df \
+    --hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \
+    --hash=sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206 \
+    --hash=sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27 \
+    --hash=sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595 \
+    --hash=sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62 \
+    --hash=sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98 \
+    --hash=sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696 \
+    --hash=sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290 \
+    --hash=sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9 \
+    --hash=sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d \
+    --hash=sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6 \
+    --hash=sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867 \
+    --hash=sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47 \
+    --hash=sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486 \
+    --hash=sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6 \
+    --hash=sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3 \
+    --hash=sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007 \
+    --hash=sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938 \
+    --hash=sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0 \
+    --hash=sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c \
+    --hash=sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735 \
+    --hash=sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d \
+    --hash=sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28 \
+    --hash=sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4 \
+    --hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \
+    --hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \
+    --hash=sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef \
+    --hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \
+    --hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \
+    --hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \
+    --hash=sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0 \
+    --hash=sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515 \
+    --hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c \
+    --hash=sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c \
+    --hash=sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924 \
+    --hash=sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34 \
+    --hash=sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43 \
+    --hash=sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859 \
+    --hash=sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673 \
+    --hash=sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54 \
+    --hash=sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a \
+    --hash=sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b \
+    --hash=sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab \
+    --hash=sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa \
+    --hash=sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c \
+    --hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \
+    --hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \
+    --hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f
 ruff==0.4.4 ; python_version >= "3.9" and python_version < "4.0" \
     --hash=sha256:1aecced1269481ef2894cc495647392a34b0bf3e28ff53ed95a385b13aa45768 \
     --hash=sha256:29d44ef5bb6a08e235c8249294fa8d431adc1426bfda99ed493119e6f9ea1bf6 \
@@ -216,6 +289,9 @@ ruff==0.4.4 ; python_version >= "3.9" and python_version < "4.0" \
     --hash=sha256:c51c928a14f9f0a871082603e25a1588059b7e08a920f2f9fa7157b5bf08cfe9 \
     --hash=sha256:cb53473849f011bca6e754f2cdf47cafc9c4f4ff4570003a0dad0b9b6890e876 \
     --hash=sha256:f87ea42d5cdebdc6a69761a9d0bc83ae9b3b30d0ad78952005ba6568d6c022af
+setuptools==70.0.0 ; python_version >= "3.9" and python_version < "4.0" \
+    --hash=sha256:54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4 \
+    --hash=sha256:f211a66637b8fa059bb28183da127d4e86396c991a942b028c6650d4319c3fd0
 toml==0.10.2 ; python_version >= "3.9" and python_version < "4.0" \
     --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \
     --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f
@@ -225,6 +301,9 @@ tomli==2.0.1 ; python_version >= "3.9" and python_full_version <= "3.11.0a6" \
 typing-extensions==4.11.0 ; python_version >= "3.9" and python_version < "4.0" \
     --hash=sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0 \
     --hash=sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a
+virtualenv==20.26.2 ; python_version >= "3.9" and python_version < "4.0" \
+    --hash=sha256:82bf0f4eebbb78d36ddaee0283d43fe5736b53880b8a8cdcd37390a07ac3741c \
+    --hash=sha256:a624db5e94f01ad993d476b9ee5346fdf7b9de43ccaee0e0197012dc838a0e9b
 zipp==3.18.2 ; python_version >= "3.9" and python_version < "3.10" \
     --hash=sha256:6278d9ddbcfb1f1089a88fde84481528b07b0e10474e09dcfe53dad4069fa059 \
     --hash=sha256:dce197b859eb796242b0622af1b8beb0a722d52aa2f57133ead08edd5bf5374e
diff --git a/poetry.lock b/poetry.lock
index 8ed305f..c56d192 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -33,6 +33,17 @@ lexid = "*"
 looseversion = {version = "*", markers = "python_version >= \"3.5\""}
 toml = "*"
 
+[[package]]
+name = "cfgv"
+version = "3.4.0"
+description = "Validate configuration and produce human readable error messages."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"},
+    {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"},
+]
+
 [[package]]
 name = "click"
 version = "8.1.7"
@@ -125,6 +136,17 @@ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.1
 [package.extras]
 toml = ["tomli"]
 
+[[package]]
+name = "distlib"
+version = "0.3.8"
+description = "Distribution utilities"
+optional = false
+python-versions = "*"
+files = [
+    {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"},
+    {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"},
+]
+
 [[package]]
 name = "exceptiongroup"
 version = "1.2.1"
@@ -139,6 +161,22 @@ files = [
 [package.extras]
 test = ["pytest (>=6)"]
 
+[[package]]
+name = "filelock"
+version = "3.14.0"
+description = "A platform independent file lock."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "filelock-3.14.0-py3-none-any.whl", hash = "sha256:43339835842f110ca7ae60f1e1c160714c5a6afd15a2873419ab185334975c0f"},
+    {file = "filelock-3.14.0.tar.gz", hash = "sha256:6ea72da3be9b8c82afd3edcf99f2fffbb5076335a5ae4d03248bb5b6c3eae78a"},
+]
+
+[package.extras]
+docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
+testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"]
+typing = ["typing-extensions (>=4.8)"]
+
 [[package]]
 name = "grimp"
 version = "3.2"
@@ -224,6 +262,20 @@ files = [
 [package.dependencies]
 typing-extensions = ">=3.10.0.0"
 
+[[package]]
+name = "identify"
+version = "2.5.36"
+description = "File identification library for Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "identify-2.5.36-py2.py3-none-any.whl", hash = "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa"},
+    {file = "identify-2.5.36.tar.gz", hash = "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d"},
+]
+
+[package.extras]
+license = ["ukkonen"]
+
 [[package]]
 name = "import-linter"
 version = "2.0"
@@ -351,6 +403,20 @@ files = [
     {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
 ]
 
+[[package]]
+name = "nodeenv"
+version = "1.8.0"
+description = "Node.js virtual environment builder"
+optional = false
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
+files = [
+    {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"},
+    {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"},
+]
+
+[package.dependencies]
+setuptools = "*"
+
 [[package]]
 name = "packaging"
 version = "24.0"
@@ -362,6 +428,22 @@ files = [
     {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"},
 ]
 
+[[package]]
+name = "platformdirs"
+version = "4.2.2"
+description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"},
+    {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"},
+]
+
+[package.extras]
+docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
+test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"]
+type = ["mypy (>=1.8)"]
+
 [[package]]
 name = "pluggy"
 version = "1.5.0"
@@ -377,6 +459,24 @@ files = [
 dev = ["pre-commit", "tox"]
 testing = ["pytest", "pytest-benchmark"]
 
+[[package]]
+name = "pre-commit"
+version = "3.7.1"
+description = "A framework for managing and maintaining multi-language pre-commit hooks."
+optional = false
+python-versions = ">=3.9"
+files = [
+    {file = "pre_commit-3.7.1-py2.py3-none-any.whl", hash = "sha256:fae36fd1d7ad7d6a5a1c0b0d5adb2ed1a3bda5a21bf6c3e5372073d7a11cd4c5"},
+    {file = "pre_commit-3.7.1.tar.gz", hash = "sha256:8ca3ad567bc78a4972a3f1a477e94a79d4597e8140a6e0b651c5e33899c3654a"},
+]
+
+[package.dependencies]
+cfgv = ">=2.0.0"
+identify = ">=1.0.0"
+nodeenv = ">=0.11.1"
+pyyaml = ">=5.1"
+virtualenv = ">=20.10.0"
+
 [[package]]
 name = "py4j"
 version = "0.10.9.5"
@@ -462,6 +562,66 @@ files = [
 importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""}
 pytest = "*"
 
+[[package]]
+name = "pyyaml"
+version = "6.0.1"
+description = "YAML parser and emitter for Python"
+optional = false
+python-versions = ">=3.6"
+files = [
+    {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
+    {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
+    {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
+    {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
+    {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
+    {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
+    {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
+    {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
+    {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
+    {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"},
+    {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
+    {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
+    {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
+    {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
+    {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
+    {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
+    {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
+    {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
+    {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
+    {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
+    {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
+    {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
+    {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"},
+    {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"},
+    {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
+    {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
+    {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
+    {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
+    {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
+    {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
+    {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
+    {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"},
+    {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
+    {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
+    {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
+    {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
+    {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
+    {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
+    {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
+]
+
 [[package]]
 name = "ruff"
 version = "0.4.4"
@@ -488,6 +648,21 @@ files = [
     {file = "ruff-0.4.4.tar.gz", hash = "sha256:f87ea42d5cdebdc6a69761a9d0bc83ae9b3b30d0ad78952005ba6568d6c022af"},
 ]
 
+[[package]]
+name = "setuptools"
+version = "70.0.0"
+description = "Easily download, build, install, upgrade, and uninstall Python packages"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "setuptools-70.0.0-py3-none-any.whl", hash = "sha256:54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4"},
+    {file = "setuptools-70.0.0.tar.gz", hash = "sha256:f211a66637b8fa059bb28183da127d4e86396c991a942b028c6650d4319c3fd0"},
+]
+
+[package.extras]
+docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
+testing = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
+
 [[package]]
 name = "toml"
 version = "0.10.2"
@@ -521,6 +696,26 @@ files = [
     {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"},
 ]
 
+[[package]]
+name = "virtualenv"
+version = "20.26.2"
+description = "Virtual Python Environment builder"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "virtualenv-20.26.2-py3-none-any.whl", hash = "sha256:a624db5e94f01ad993d476b9ee5346fdf7b9de43ccaee0e0197012dc838a0e9b"},
+    {file = "virtualenv-20.26.2.tar.gz", hash = "sha256:82bf0f4eebbb78d36ddaee0283d43fe5736b53880b8a8cdcd37390a07ac3741c"},
+]
+
+[package.dependencies]
+distlib = ">=0.3.7,<1"
+filelock = ">=3.12.2,<4"
+platformdirs = ">=3.9.1,<5"
+
+[package.extras]
+docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
+test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
+
 [[package]]
 name = "zipp"
 version = "3.18.2"
@@ -539,4 +734,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.9"
-content-hash = "f2132ce10d71d7b2ff0cb760881093ee2a184a0016259f4eb8b8d4bbb368e423"
+content-hash = "2f750fd4010bdfa8b68fb4cae65487524c4e706afa810b09d4898dd51b324f3f"
diff --git a/pyproject.toml b/pyproject.toml
index 7a506f9..680ad00 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -38,15 +38,16 @@ bumpver = "^2023.1129"
 import-linter = "^2.0"
 ruff = "^0.4.4"
 pytest = "^8.2.1"
+pre-commit = "^3.7.1"
 
 [tool.poetry.group.local.dependencies]
-# The "local" dependency group refers to dependencies that already 
+# The "local" dependency group refers to dependencies that already
 # exist in AWS Glue's runtime. We don't need to install these in the
-# container. We only need them for local (non-container) development to 
+# container. We only need them for local (non-container) development to
 # aid the IDE in providing code completion and type checking.
-# 
-# It is best to keep the dependencies here in sync with what's on Glue's 
-# documentation. 
+#
+# It is best to keep the dependencies here in sync with what's on Glue's
+# documentation.
 # https://docs.aws.amazon.com/glue/latest/dg/aws-glue-programming-python-libraries.html#glue-modules-provided
 aws-glue-libs = { git = "https://github.com/awslabs/aws-glue-libs.git", rev = "master" }
 pyspark = "3.3.0"
@@ -162,4 +163,4 @@ root_package = "glue_utils"
 [[tool.importlinter.contracts]]
 name = "Generic code should not import specific code"
 type = "layers"
-layers = ["glue_utils.pyspark", "glue_utils.options"]
\ No newline at end of file
+layers = ["glue_utils.pyspark", "glue_utils.options"]
diff --git a/sonar-project.properties b/sonar-project.properties
index fe657d8..39f6216 100644
--- a/sonar-project.properties
+++ b/sonar-project.properties
@@ -5,4 +5,4 @@ sonar.sources=src
 sonar.tests=test
 sonar.pullrequest.github.summary_comment=true
 sonar.python.version=3.9,3.10
-sonar.python.coverage.reportPaths=coverage/results.xml
\ No newline at end of file
+sonar.python.coverage.reportPaths=coverage/results.xml
diff --git a/src/glue_utils/pyspark/connection_options.py b/src/glue_utils/pyspark/connection_options.py
index 76dec25..1cd9e4d 100644
--- a/src/glue_utils/pyspark/connection_options.py
+++ b/src/glue_utils/pyspark/connection_options.py
@@ -34,13 +34,13 @@ class JDBCConnectionOptions(BookmarkConnectionOptions, total=False):
     password: str
     customJdbcDriverS3Path: str
     customJdbcDriverClassName: str
-    bulkSize: int
+    bulkSize: str
     hashfield: str
     hashexpression: str
-    hashpartitions: int
+    hashpartitions: str
     sampleQuery: str
-    enablePartitioningForSampleQuery: bool
-    sampleSize: int
+    enablePartitioningForSampleQuery: Literal["true", "false"]
+    sampleSize: str
 
 
 # Redshift has a connection option with a dot in it, so we need to use
@@ -48,8 +48,8 @@ class JDBCConnectionOptions(BookmarkConnectionOptions, total=False):
 RedshiftOptions = TypedDict(
     "RedshiftOptions",
     {
-        "autopushdown": bool,
-        "autopushdown.s3_result_cache": bool,
+        "autopushdown": Literal["true", "false"],
+        "autopushdown.s3_result_cache": Literal["true", "false"],
         "aws_iam_role": str,
         "csvnullstring": str,
         "DbUser": str,
@@ -89,13 +89,13 @@ class S3SourceConnectionOptions(BookmarkConnectionOptions, total=False):
     compressionType: Literal["gzip", "bzip2"]
     groupFiles: Literal["inPartition", "none"]
     groupSize: str
-    recurse: bool
-    maxBand: int
-    maxFilesInBand: int
-    isFailFast: bool
+    recurse: Literal["true", "false"]
+    maxBand: str
+    maxFilesInBand: str
+    isFailFast: Literal["true", "false"]
     catalogPartitionPredicate: str
     excludeStorageClasses: list[str]
-    useS3ListImplementation: bool
+    useS3ListImplementation: Literal["true", "false"]
 
 
 class S3SinkConnectionOptions(BookmarkConnectionOptions, total=False):
@@ -111,8 +111,8 @@ class S3SinkConnectionOptions(BookmarkConnectionOptions, total=False):
     "DynamoDBSourceConnectionOptions",
     {
         "dynamodb.input.tableName": str,
-        "dynamodb.throughput.read.percent": float,
-        "dynamodb.splits": int,
+        "dynamodb.throughput.read.percent": str,
+        "dynamodb.splits": str,
         "dynamodb.sts.roleArn": str,
         "dynamodb.sts.roleSessionName": str,
         "dynamodb.sts.region": str,
@@ -121,7 +121,7 @@ class S3SinkConnectionOptions(BookmarkConnectionOptions, total=False):
         "dynamodb.s3.bucket": str,
         "dynamodb.s3.prefix": str,
         "dynamodb.s3.bucketOwner": str,
-        "dynamodb.simplifyDDBJson": bool,
+        "dynamodb.simplifyDDBJson": Literal["true", "false"],
         "dynamodb.exportTime": str,
     },
     total=False,
@@ -132,9 +132,9 @@ class S3SinkConnectionOptions(BookmarkConnectionOptions, total=False):
     "DynamoDBSinkConnectionOptions",
     {
         "dynamodb.output.tableName": str,
-        "dynamodb.throughput.write.percent": float,
-        "dynamodb.output.numParallelTasks": int,
-        "dynamodb.output.retry": int,
+        "dynamodb.throughput.write.percent": str,
+        "dynamodb.output.numParallelTasks": str,
+        "dynamodb.output.retry": str,
         "dynamodb.sts.roleArn": str,
         "dynamodb.sts.roleSessionName": str,
     },
@@ -152,10 +152,10 @@ class KinesisConnectionOptions(TypedDict, total=False):
     """
 
     streamARN: str
-    failOnDataLoss: bool
+    failOnDataLoss: Literal["true", "false"]
     awsSTSRoleArn: str
     awsSTSSessionName: str
-    awsSTSEndpoint: str
+    awsSTSEndpostr: str
 
 
 class KinesisSourceConnectionOptions(KinesisConnectionOptions, total=False):
@@ -163,23 +163,23 @@ class KinesisSourceConnectionOptions(KinesisConnectionOptions, total=False):
 
     classification: str
     streamName: str
-    endpointUrl: str
+    endpostrUrl: str
     delimiter: str
     startingPosition: str
-    maxFetchTimeInMs: int
-    maxFetchRecordsPerShard: int
-    maxRecordPerRead: int
-    addIdleTimeBetweenReads: bool
-    idleTimeBetweenReadsInMs: int
-    describeShardInterval: str
-    numRetries: int
-    retryIntervalMs: int
-    maxRetryIntervalMs: int
-    avoidEmptyBatches: bool
+    maxFetchTimeInMs: str
+    maxFetchRecordsPerShard: str
+    maxRecordPerRead: str
+    addIdleTimeBetweenReads: Literal["true", "false"]
+    idleTimeBetweenReadsInMs: str
+    describeShardStrerval: str
+    numRetries: str
+    retryStrervalMs: str
+    maxRetryStrervalMs: str
+    avoidEmptyBatches: Literal["true", "false"]
     schema: str
-    inferSchema: bool
-    addRecordTimestamp: bool
-    emitConsumerLagMetrics: bool
+    inferSchema: Literal["true", "false"]
+    addRecordTimestamp: Literal["true", "false"]
+    emitConsumerLagMetrics: Literal["true", "false"]
     fanoutConsumerARN: str
 
 
@@ -187,13 +187,13 @@ class KinesisSinkConnectionOptions(KinesisConnectionOptions, total=False):
     """Connection options for writing to Kinesis."""
 
     partitionKey: str
-    recordMaxBufferedTime: int
-    aggregationEnabled: bool
-    aggregationMaxSize: int
-    aggregationMaxCount: int
-    producerRateLimit: int
-    collectionMaxCount: int
-    collectionMaxSize: int
+    recordMaxBufferedTime: str
+    aggregationEnabled: Literal["true", "false"]
+    aggregationMaxSize: str
+    aggregationMaxCount: str
+    producerRateLimit: str
+    collectionMaxCount: str
+    collectionMaxSize: str
 
 
 # https://docs.aws.amazon.com/glue/latest/dg/aws-glue-programming-etl-connect-documentdb-home.html
@@ -205,9 +205,9 @@ class KinesisSinkConnectionOptions(KinesisConnectionOptions, total=False):
         "collection": str,
         "username": str,
         "password": str,
-        "ssl": bool,
-        "ssl.domain_match": bool,
-        "batchSize": int,
+        "ssl": str,
+        "ssl.domain_match": str,
+        "batchSize": str,
         "partitioner": str,
         "partitionerOptions.partitionKey": str,
         "partitionerOptions.partitionSizeMB": str,
@@ -231,10 +231,10 @@ class DocumentDBSinkConnectionOptions(TypedDict, total=False):
     collection: str
     username: str
     password: str
-    extendedBSONTypes: bool
-    replaceDocument: bool
-    maxBatchSize: int
-    retryWrites: bool
+    extendedBSONTypes: str
+    replaceDocument: str
+    maxBatchSize: str
+    retryWrites: str
 
 
 # https://docs.aws.amazon.com/glue/latest/dg/aws-glue-programming-etl-connect-mongodb-home.html
@@ -248,10 +248,10 @@ class DocumentDBSinkConnectionOptions(TypedDict, total=False):
         "password": str,
         "database": str,
         "collection": str,
-        "ssl": bool,
-        "ssl.domain_match": bool,
-        "disableUpdateUri": bool,
-        "batchSize": int,
+        "ssl": Literal["true", "false"],
+        "ssl.domain_match": Literal["true", "false"],
+        "disableUpdateUri": Literal["true", "false"],
+        "batchSize": str,
         "partitioner": str,
         "partitionerOptions.partitionKey": str,
         "partitionerOptions.partitionSizeMB": str,
@@ -273,13 +273,13 @@ class DocumentDBSinkConnectionOptions(TypedDict, total=False):
         "password": str,
         "database": str,
         "collection": str,
-        "ssl": bool,
-        "ssl.domain_match": bool,
-        "disableUpdateUri": bool,
-        "extendedBSONTypes": bool,
-        "replaceDocument": bool,
-        "maxBatchSize": int,
-        "retryWrites": bool,
+        "ssl": Literal["true", "false"],
+        "ssl.domain_match": Literal["true", "false"],
+        "disableUpdateUri": Literal["true", "false"],
+        "extendedBSONTypes": Literal["true", "false"],
+        "replaceDocument": Literal["true", "false"],
+        "maxBatchSize": str,
+        "retryWrites": Literal["true", "false"],
     },
     total=False,
 )
@@ -291,7 +291,7 @@ class DocumentDBSinkConnectionOptions(TypedDict, total=False):
         "connectionName": str,
         "opensearch.resource": str,
         "opensearch.query": str,
-        "pushdown": bool,
+        "pushdown": Literal["true", "false"],
         "opensearch.read.field.as.array.include": str,
     },
     total=False,
@@ -313,25 +313,25 @@ class DocumentDBSinkConnectionOptions(TypedDict, total=False):
     {
         "connectionName": str,
         "bootstrap.servers": str,
-        "security.protocol": str,
+        "security.protocol": Literal["SSL", "PLAINTEXT"],
         "topicName": str,
         "assign": str,
         "subscribePattern": str,
         "classification": str,
         "delimiter": str,
-        "startingOffsets": str,
+        "startingOffsets": Literal["earliest", "latest"],
         "startingTimestamp": str,
         "endingOffsets": str,
-        "pollTimeoutMs": int,
-        "numRetries": int,
-        "retryIntervalMs": int,
-        "maxOffsetsPerTrigger": int,
-        "minPartitions": int,
-        "includeHeaders": bool,
+        "pollTimeoutMs": str,
+        "numRetries": str,
+        "retryStrervalMs": str,
+        "maxOffsetsPerTrigger": str,
+        "minPartitions": str,
+        "includeHeaders": Literal["true", "false"],
         "schema": str,
-        "inferSchema": bool,
-        "addRecordTimestamp": bool,
-        "emitConsumerLagMetrics": bool,
+        "inferSchema": Literal["true", "false"],
+        "addRecordTimestamp": Literal["true", "false"],
+        "emitConsumerLagMetrics": Literal["true", "false"],
     },
     total=False,
 )
@@ -345,7 +345,7 @@ class KafkaSinkConnectionOptions(TypedDict, total=False):
     """
 
     connectionName: str
-    topic: str
+    topicName: str
     partition: str
     key: str
     classification: str


Test patch:
diff --git a/test/pyspark/context/conftest.py b/test/pyspark/context/conftest.py
index 74dbb5f..d81f6fe 100644
--- a/test/pyspark/context/conftest.py
+++ b/test/pyspark/context/conftest.py
@@ -5,7 +5,7 @@
 from pyspark import SparkContext
 
 
-@pytest.fixture(scope="session")
+@pytest.fixture
 def glue_pyspark_context():
     sc = SparkContext.getOrCreate()
     yield GluePySparkContext(sc)
diff --git a/test/pyspark/context/test_documentdb.py b/test/pyspark/context/test_documentdb.py
index c05e28a..e4479e1 100644
--- a/test/pyspark/context/test_documentdb.py
+++ b/test/pyspark/context/test_documentdb.py
@@ -19,8 +19,8 @@ def test_create_dynamic_frame(
                 "collection": "coll",
                 "username": "username",
                 "password": "1234567890",
-                "ssl": True,
-                "ssl.domain_match": False,
+                "ssl": "true",
+                "ssl.domain_match": "false",
                 "partitioner": "MongoSamplePartitioner",
                 "partitionerOptions.partitionSizeMB": "10",
                 "partitionerOptions.partitionKey": "_id",
@@ -36,8 +36,8 @@ def test_create_dynamic_frame(
                 "collection": "coll",
                 "username": "username",
                 "password": "1234567890",
-                "ssl": True,
-                "ssl.domain_match": False,
+                "ssl": "true",
+                "ssl.domain_match": "false",
                 "partitioner": "MongoSamplePartitioner",
                 "partitionerOptions.partitionSizeMB": "10",
                 "partitionerOptions.partitionKey": "_id",
@@ -54,7 +54,7 @@ def test_write_dynamic_frame(
         dynamic_frame = glue_pyspark_context.write_dynamic_frame_to_documentdb(
             frame=sentinel.dynamic_frame,
             connection_options={
-                "retryWrites": False,
+                "retryWrites": "false",
                 "uri": "documentdb_write_uri",
                 "database": "test",
                 "collection": "coll",
@@ -68,7 +68,7 @@ def test_write_dynamic_frame(
             frame=sentinel.dynamic_frame,
             connection_type=ConnectionType.DOCUMENTDB.value,
             connection_options={
-                "retryWrites": False,
+                "retryWrites": "false",
                 "uri": "documentdb_write_uri",
                 "database": "test",
                 "collection": "coll",
diff --git a/test/pyspark/context/test_dynamodb.py b/test/pyspark/context/test_dynamodb.py
index 1bf10b1..6a6a6b9 100644
--- a/test/pyspark/context/test_dynamodb.py
+++ b/test/pyspark/context/test_dynamodb.py
@@ -15,8 +15,8 @@ def test_create_dynamic_frame(
         dynamic_frame = glue_pyspark_context.create_dynamic_frame_from_dynamodb(
             connection_options={
                 "dynamodb.input.tableName": "test_source",
-                "dynamodb.throughput.read.percent": 1.0,
-                "dynamodb.splits": 100,
+                "dynamodb.throughput.read.percent": "1.0",
+                "dynamodb.splits": "100",
             },
             transformation_ctx="test",
         )
@@ -25,8 +25,8 @@ def test_create_dynamic_frame(
             connection_type=ConnectionType.DYNAMODB.value,
             connection_options={
                 "dynamodb.input.tableName": "test_source",
-                "dynamodb.throughput.read.percent": 1.0,
-                "dynamodb.splits": 100,
+                "dynamodb.throughput.read.percent": "1.0",
+                "dynamodb.splits": "100",
             },
             transformation_ctx="test",
         )
@@ -41,7 +41,7 @@ def test_write_dynamic_frame(
             frame=sentinel.dynamic_frame,
             connection_options={
                 "dynamodb.output.tableName": "test_sink",
-                "dynamodb.throughput.write.percent": 1.0,
+                "dynamodb.throughput.write.percent": "1.0",
             },
             transformation_ctx="test",
         )
@@ -51,7 +51,7 @@ def test_write_dynamic_frame(
             connection_type=ConnectionType.DYNAMODB.value,
             connection_options={
                 "dynamodb.output.tableName": "test_sink",
-                "dynamodb.throughput.write.percent": 1.0,
+                "dynamodb.throughput.write.percent": "1.0",
             },
             transformation_ctx="test",
         )
diff --git a/test/pyspark/context/test_kafka.py b/test/pyspark/context/test_kafka.py
index c0504cb..4e563ac 100644
--- a/test/pyspark/context/test_kafka.py
+++ b/test/pyspark/context/test_kafka.py
@@ -21,7 +21,7 @@ def test_create_dynamic_frame(
                 connectionName="ConfluentKafka",
                 topicName="kafka-auth-topic",
                 startingOffsets="earliest",
-                inferSchema=True,
+                inferSchema="true",
                 classification="json",
             ),
             transformation_ctx="test",
@@ -33,7 +33,7 @@ def test_create_dynamic_frame(
                 "connectionName": "ConfluentKafka",
                 "topicName": "kafka-auth-topic",
                 "startingOffsets": "earliest",
-                "inferSchema": True,
+                "inferSchema": "true",
                 "classification": "json",
             },
             transformation_ctx="test",
@@ -49,7 +49,7 @@ def test_write_dynamic_frame(
             frame=sentinel.dynamic_frame,
             connection_options=KafkaSinkConnectionOptions(
                 connectionName="ConfluentKafka",
-                topic="kafka-auth-topic",
+                topicName="kafka-auth-topic",
                 classification="json",
             ),
             transformation_ctx="test",
@@ -60,7 +60,7 @@ def test_write_dynamic_frame(
             connection_type=ConnectionType.KAFKA.value,
             connection_options={
                 "connectionName": "ConfluentKafka",
-                "topic": "kafka-auth-topic",
+                "topicName": "kafka-auth-topic",
                 "classification": "json",
             },
             transformation_ctx="test",
diff --git a/test/pyspark/context/test_kinesis.py b/test/pyspark/context/test_kinesis.py
index 5128b2e..0d254d3 100644
--- a/test/pyspark/context/test_kinesis.py
+++ b/test/pyspark/context/test_kinesis.py
@@ -20,7 +20,7 @@ def test_create_dynamic_frame(
             connection_options=KinesisSourceConnectionOptions(
                 streamARN="arn:aws:kinesis:us-east-2:777788889999:stream/fromOptionsStream",
                 startingPosition="TRIM_HORIZON",
-                inferSchema=True,
+                inferSchema="true",
                 classification="json",
             ),
             transformation_ctx="test",
@@ -31,7 +31,7 @@ def test_create_dynamic_frame(
             connection_options={
                 "streamARN": "arn:aws:kinesis:us-east-2:777788889999:stream/fromOptionsStream",
                 "startingPosition": "TRIM_HORIZON",
-                "inferSchema": True,
+                "inferSchema": "true",
                 "classification": "json",
             },
             transformation_ctx="test",
diff --git a/test/pyspark/context/test_mongodb.py b/test/pyspark/context/test_mongodb.py
index 9a6deeb..ebf038d 100644
--- a/test/pyspark/context/test_mongodb.py
+++ b/test/pyspark/context/test_mongodb.py
@@ -20,7 +20,7 @@ def test_create_dynamic_frame(
                 "partitioner": "com.mongodb.spark.sql.connector.read.partitioner.SinglePartitionPartitioner",
                 "partitionerOptions.partitionSizeMB": "10",
                 "partitionerOptions.partitionKey": "_id",
-                "disableUpdateUri": False,
+                "disableUpdateUri": "false",
             },
             transformation_ctx="test",
         )
@@ -34,7 +34,7 @@ def test_create_dynamic_frame(
                 "partitioner": "com.mongodb.spark.sql.connector.read.partitioner.SinglePartitionPartitioner",
                 "partitionerOptions.partitionSizeMB": "10",
                 "partitionerOptions.partitionKey": "_id",
-                "disableUpdateUri": False,
+                "disableUpdateUri": "false",
             },
             transformation_ctx="test",
         )
@@ -51,8 +51,8 @@ def test_write_dynamic_frame(
                 "connectionName": "connectionName",
                 "database": "mongodbName",
                 "collection": "mongodbCollection",
-                "disableUpdateUri": False,
-                "retryWrites": False,
+                "disableUpdateUri": "false",
+                "retryWrites": "false",
             },
             transformation_ctx="test",
         )
@@ -64,8 +64,8 @@ def test_write_dynamic_frame(
                 "connectionName": "connectionName",
                 "database": "mongodbName",
                 "collection": "mongodbCollection",
-                "disableUpdateUri": False,
-                "retryWrites": False,
+                "disableUpdateUri": "false",
+                "retryWrites": "false",
             },
             transformation_ctx="test",
         )
diff --git a/test/pyspark/context/test_opensearch.py b/test/pyspark/context/test_opensearch.py
index ba388d8..09026b6 100644
--- a/test/pyspark/context/test_opensearch.py
+++ b/test/pyspark/context/test_opensearch.py
@@ -16,7 +16,7 @@ def test_create_dynamic_frame(
             connection_options={
                 "connectionName": "connectionName",
                 "opensearch.resource": "aosIndex",
-                "pushdown": True,
+                "pushdown": "true",
             },
             transformation_ctx="test",
         )
@@ -26,7 +26,7 @@ def test_create_dynamic_frame(
             connection_options={
                 "connectionName": "connectionName",
                 "opensearch.resource": "aosIndex",
-                "pushdown": True,
+                "pushdown": "true",
             },
             transformation_ctx="test",
         )