khmerns 0.0.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,93 @@
1
+ name: Wheels
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ pull_request:
6
+ push:
7
+ branches:
8
+ - main
9
+ release:
10
+ types:
11
+ - published
12
+
13
+ env:
14
+ FORCE_COLOR: 3
15
+
16
+ concurrency:
17
+ group: ${{ github.workflow }}-${{ github.ref }}
18
+ cancel-in-progress: true
19
+
20
+ jobs:
21
+ build_sdist:
22
+ name: Build SDist
23
+ runs-on: ubuntu-latest
24
+ steps:
25
+ - uses: actions/checkout@v5
26
+ with:
27
+ submodules: true
28
+
29
+ - name: Build SDist
30
+ run: pipx run build --sdist
31
+
32
+ - name: Check metadata
33
+ run: pipx run twine check dist/*
34
+
35
+ - uses: actions/upload-artifact@v5
36
+ with:
37
+ name: cibw-sdist
38
+ path: dist/*.tar.gz
39
+
40
+ build_wheels:
41
+ name: Wheels on ${{ matrix.os }}
42
+ runs-on: ${{ matrix.os }}
43
+ strategy:
44
+ fail-fast: false
45
+ matrix:
46
+ os: [ubuntu-latest, macos-latest, macos-15-intel, windows-latest, ubuntu-24.04-arm]
47
+ env:
48
+ MACOSX_DEPLOYMENT_TARGET: "11.0"
49
+ steps:
50
+ - uses: actions/checkout@v5
51
+ with:
52
+ submodules: true
53
+
54
+ - uses: astral-sh/setup-uv@v7
55
+
56
+ - uses: pypa/cibuildwheel@v3.3
57
+
58
+ - name: Verify clean directory
59
+ run: git diff --exit-code
60
+ shell: bash
61
+
62
+ - uses: actions/upload-artifact@v5
63
+ with:
64
+ name: cibw-wheels-${{ matrix.os }}
65
+ path: wheelhouse/*.whl
66
+
67
+ upload_all:
68
+ name: Upload if release
69
+ needs: [build_wheels, build_sdist]
70
+ runs-on: ubuntu-latest
71
+ if: github.event_name == 'release' && github.event.action == 'published'
72
+ environment: pypi
73
+ permissions:
74
+ id-token: write
75
+ attestations: write
76
+
77
+ steps:
78
+ - uses: actions/setup-python@v6
79
+ with:
80
+ python-version: "3.x"
81
+
82
+ - uses: actions/download-artifact@v6
83
+ with:
84
+ pattern: cibw-*
85
+ merge-multiple: true
86
+ path: dist
87
+
88
+ - name: Generate artifact attestation for sdist and wheels
89
+ uses: actions/attest-build-provenance@v3
90
+ with:
91
+ subject-path: "dist/*"
92
+
93
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,248 @@
1
+ # General
2
+ .DS_Store
3
+ __MACOSX/
4
+ .AppleDouble
5
+ .LSOverride
6
+ Icon[
7
+ ]
8
+
9
+ # Thumbnails
10
+ ._*
11
+
12
+ # Files that might appear in the root of a volume
13
+ .DocumentRevisions-V100
14
+ .fseventsd
15
+ .Spotlight-V100
16
+ .TemporaryItems
17
+ .Trashes
18
+ .VolumeIcon.icns
19
+ .com.apple.timemachine.donotpresent
20
+
21
+ # Directories potentially created on remote AFP share
22
+ .AppleDB
23
+ .AppleDesktop
24
+ Network Trash Folder
25
+ Temporary Items
26
+ .apdisk
27
+
28
+ # Byte-compiled / optimized / DLL files
29
+ __pycache__/
30
+ *.py[codz]
31
+ *$py.class
32
+
33
+ # C extensions
34
+ *.so
35
+
36
+ # Distribution / packaging
37
+ .Python
38
+ build/
39
+ develop-eggs/
40
+ dist/
41
+ downloads/
42
+ eggs/
43
+ .eggs/
44
+ lib/
45
+ lib64/
46
+ parts/
47
+ sdist/
48
+ var/
49
+ wheels/
50
+ share/python-wheels/
51
+ *.egg-info/
52
+ .installed.cfg
53
+ *.egg
54
+ MANIFEST
55
+
56
+ # PyInstaller
57
+ # Usually these files are written by a python script from a template
58
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
59
+ *.manifest
60
+ *.spec
61
+
62
+ # Installer logs
63
+ pip-log.txt
64
+ pip-delete-this-directory.txt
65
+
66
+ # Unit test / coverage reports
67
+ htmlcov/
68
+ .tox/
69
+ .nox/
70
+ .coverage
71
+ .coverage.*
72
+ .cache
73
+ nosetests.xml
74
+ coverage.xml
75
+ *.cover
76
+ *.py.cover
77
+ .hypothesis/
78
+ .pytest_cache/
79
+ cover/
80
+
81
+ # Translations
82
+ *.mo
83
+ *.pot
84
+
85
+ # Django stuff:
86
+ *.log
87
+ local_settings.py
88
+ db.sqlite3
89
+ db.sqlite3-journal
90
+
91
+ # Flask stuff:
92
+ instance/
93
+ .webassets-cache
94
+
95
+ # Scrapy stuff:
96
+ .scrapy
97
+
98
+ # Sphinx documentation
99
+ docs/_build/
100
+
101
+ # PyBuilder
102
+ .pybuilder/
103
+ target/
104
+
105
+ # Jupyter Notebook
106
+ .ipynb_checkpoints
107
+
108
+ # IPython
109
+ profile_default/
110
+ ipython_config.py
111
+
112
+ # pyenv
113
+ # For a library or package, you might want to ignore these files since the code is
114
+ # intended to run in multiple environments; otherwise, check them in:
115
+ # .python-version
116
+
117
+ # pipenv
118
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
119
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
120
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
121
+ # install all needed dependencies.
122
+ # Pipfile.lock
123
+
124
+ # UV
125
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
126
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
127
+ # commonly ignored for libraries.
128
+ # uv.lock
129
+
130
+ # poetry
131
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
132
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
133
+ # commonly ignored for libraries.
134
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
135
+ # poetry.lock
136
+ # poetry.toml
137
+
138
+ # pdm
139
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
140
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
141
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
142
+ # pdm.lock
143
+ # pdm.toml
144
+ .pdm-python
145
+ .pdm-build/
146
+
147
+ # pixi
148
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
149
+ # pixi.lock
150
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
151
+ # in the .venv directory. It is recommended not to include this directory in version control.
152
+ .pixi
153
+
154
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
155
+ __pypackages__/
156
+
157
+ # Celery stuff
158
+ celerybeat-schedule
159
+ celerybeat.pid
160
+
161
+ # Redis
162
+ *.rdb
163
+ *.aof
164
+ *.pid
165
+
166
+ # RabbitMQ
167
+ mnesia/
168
+ rabbitmq/
169
+ rabbitmq-data/
170
+
171
+ # ActiveMQ
172
+ activemq-data/
173
+
174
+ # SageMath parsed files
175
+ *.sage.py
176
+
177
+ # Environments
178
+ .env
179
+ .envrc
180
+ .venv
181
+ env/
182
+ venv/
183
+ ENV/
184
+ env.bak/
185
+ venv.bak/
186
+
187
+ # Spyder project settings
188
+ .spyderproject
189
+ .spyproject
190
+
191
+ # Rope project settings
192
+ .ropeproject
193
+
194
+ # mkdocs documentation
195
+ /site
196
+
197
+ # mypy
198
+ .mypy_cache/
199
+ .dmypy.json
200
+ dmypy.json
201
+
202
+ # Pyre type checker
203
+ .pyre/
204
+
205
+ # pytype static type analyzer
206
+ .pytype/
207
+
208
+ # Cython debug symbols
209
+ cython_debug/
210
+
211
+ # PyCharm
212
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
213
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
214
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
215
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
216
+ # .idea/
217
+
218
+ # Abstra
219
+ # Abstra is an AI-powered process automation framework.
220
+ # Ignore directories containing user credentials, local state, and settings.
221
+ # Learn more at https://abstra.io/docs
222
+ .abstra/
223
+
224
+ # Visual Studio Code
225
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
226
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
227
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
228
+ # you could uncomment the following to ignore the entire vscode folder
229
+ # .vscode/
230
+
231
+ # Ruff stuff:
232
+ .ruff_cache/
233
+
234
+ # PyPI configuration file
235
+ .pypirc
236
+
237
+ # Marimo
238
+ marimo/_static/
239
+ marimo/_lsp/
240
+ __marimo__/
241
+
242
+ # Streamlit
243
+ .streamlit/secrets.toml
244
+ data/
245
+ assets
246
+ .claude
247
+ training/CLAUDE.md
248
+ CLAUDE.md
@@ -0,0 +1,56 @@
1
+ # Require CMake 3.15+ (matching scikit-build-core)
2
+ cmake_minimum_required(VERSION 3.15...4.0)
3
+
4
+ # Scikit-build-core sets these values
5
+ project(
6
+ ${SKBUILD_PROJECT_NAME}
7
+ VERSION ${SKBUILD_PROJECT_VERSION}
8
+ LANGUAGES C CXX)
9
+
10
+ set(CMAKE_CXX_STANDARD 17)
11
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
12
+ set(CMAKE_POSITION_INDEPENDENT_CODE ON)
13
+
14
+ if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
15
+ set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
16
+ endif()
17
+
18
+ # Find pybind11 and Python
19
+ find_package(pybind11 CONFIG REQUIRED)
20
+
21
+ # ===== Fetch GGML Library =====
22
+ include(FetchContent)
23
+ FetchContent_Declare(
24
+ ggml
25
+ GIT_REPOSITORY https://github.com/ggerganov/ggml.git
26
+ GIT_TAG a8db410a252c8c8f2d120c6f2e7133ebe032f35d
27
+ )
28
+ set(GGML_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
29
+ set(GGML_BUILD_TESTS OFF CACHE BOOL "" FORCE)
30
+ set(GGML_STATIC ON CACHE BOOL "" FORCE)
31
+ set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
32
+ FetchContent_MakeAvailable(ggml)
33
+
34
+ # ===== Create Python Module =====
35
+ python_add_library(_core MODULE
36
+ src/main.cpp
37
+ src/tokenizer.cpp
38
+ src/crf.cpp
39
+ src/khmer-segmenter.cpp
40
+ WITH_SOABI
41
+ )
42
+
43
+ target_include_directories(_core PRIVATE
44
+ ${CMAKE_CURRENT_SOURCE_DIR}/src
45
+ )
46
+
47
+ target_link_libraries(_core PRIVATE
48
+ pybind11::headers
49
+ ggml
50
+ )
51
+
52
+ # Pass version info
53
+ target_compile_definitions(_core PRIVATE VERSION_INFO="${PROJECT_VERSION}")
54
+
55
+ # Install to the khmerns package directory
56
+ install(TARGETS _core DESTINATION khmerns)
khmerns-0.0.3/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Seanghay Yath
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
khmerns-0.0.3/PKG-INFO ADDED
@@ -0,0 +1,121 @@
1
+ Metadata-Version: 2.4
2
+ Name: khmerns
3
+ Version: 0.0.3
4
+ Summary: Khmer Neural Segmenter
5
+ Keywords: khmer,nlp,segmentation,tokenization,neural-network
6
+ Author-Email: Seanghay Yath <seanghay.dev@gmail.com>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Programming Language :: Python :: 3 :: Only
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Project-URL: Homepage, https://github.com/seanghay/khmer-neural-segmenter
18
+ Project-URL: Repository, https://github.com/seanghay/khmer-neural-segmenter
19
+ Project-URL: Issues, https://github.com/seanghay/khmer-neural-segmenter/issues
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+
23
+ # Khmer Neural Segmenter
24
+
25
+ A fast Khmer word segmentation library.
26
+
27
+ <img src="img/graph.png" alt="" width=500>
28
+
29
+ ## Installation
30
+
31
+ ```
32
+ pip install khmerns
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ ```python
38
+ from khmerns import tokenize
39
+
40
+ # Returns a list of words
41
+ words = tokenize("សួស្តីបងប្អូន")
42
+ # ['សួស្តី', 'បង', 'ប្អូន']
43
+ ```
44
+
45
+ You can also use the class-based API if you prefer:
46
+
47
+ ```python
48
+ from khmerns import KhmerSegmenter
49
+
50
+ segmenter = KhmerSegmenter()
51
+ words = segmenter.tokenize("សួស្តីបងប្អូន")
52
+ # or
53
+ words = segmenter("សួស្តីបងប្អូន")
54
+ ```
55
+
56
+ ## Training
57
+
58
+ The training pipeline lives in the `training/` directory. It trains a BiGRU + CRF model on character-level BIO tags, then converts the result to GGUF for the C++ inference backend.
59
+
60
+ ### Data format
61
+
62
+ Training data is a plain text file at `training/data/train.txt`. One word per line. Words that appear on consecutive lines are treated as part of the same sentence. The model learns word boundaries from this.
63
+
64
+ Example `training/data/train.txt`:
65
+
66
+ ```
67
+ សួស្តី
68
+ បង
69
+ ប្អូន
70
+ ខ្ញុំ
71
+ ទៅ
72
+ ផ្សារ
73
+ ```
74
+
75
+ Non-Khmer tokens (spaces, punctuation, numbers, Latin text) are tagged as `NON-KHMER`. Khmer tokens get `B-WORD` on the first character and `I-WORD` on the rest.
76
+
77
+ ### Steps
78
+
79
+ ```bash
80
+ cd training
81
+ pip install -r requirements.txt
82
+ ```
83
+
84
+ **1. Prepare training data**
85
+
86
+ Place your segmented text in `data/train.txt` (one word per line). If you have raw unsegmented Khmer text, you can use the generation script to pre-segment it:
87
+
88
+ ```bash
89
+ python generate.py
90
+ ```
91
+
92
+ This requires `khmersegment` and a source text file. Edit the path in `generate.py` to point to your raw text.
93
+
94
+ **2. Train**
95
+
96
+ ```bash
97
+ python train.py
98
+ ```
99
+
100
+ Trains for 20 epochs with AdamW (lr=1e-5) and ReduceLROnPlateau. Saves `best_model.pt` (best eval loss) and `model.pt` (final). Uses CUDA if available.
101
+
102
+ **3. Convert to GGUF**
103
+
104
+ ```bash
105
+ python convert_to_gguf.py best_model.pt model.gguf
106
+ ```
107
+
108
+ This produces a GGUF file (~3.3MB) containing all model weights.
109
+
110
+ **4. Embed in the C++ binary**
111
+
112
+ To use the new model in the library, convert the GGUF file to a C header and replace `src/model_data.h`, then rebuild:
113
+
114
+ ```bash
115
+ xxd -i model.gguf > ../src/model_data.h
116
+ pip install -e ..
117
+ ```
118
+
119
+ ## License
120
+
121
+ MIT
@@ -0,0 +1,99 @@
1
+ # Khmer Neural Segmenter
2
+
3
+ A fast Khmer word segmentation library.
4
+
5
+ <img src="img/graph.png" alt="" width=500>
6
+
7
+ ## Installation
8
+
9
+ ```
10
+ pip install khmerns
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```python
16
+ from khmerns import tokenize
17
+
18
+ # Returns a list of words
19
+ words = tokenize("សួស្តីបងប្អូន")
20
+ # ['សួស្តី', 'បង', 'ប្អូន']
21
+ ```
22
+
23
+ You can also use the class-based API if you prefer:
24
+
25
+ ```python
26
+ from khmerns import KhmerSegmenter
27
+
28
+ segmenter = KhmerSegmenter()
29
+ words = segmenter.tokenize("សួស្តីបងប្អូន")
30
+ # or
31
+ words = segmenter("សួស្តីបងប្អូន")
32
+ ```
33
+
34
+ ## Training
35
+
36
+ The training pipeline lives in the `training/` directory. It trains a BiGRU + CRF model on character-level BIO tags, then converts the result to GGUF for the C++ inference backend.
37
+
38
+ ### Data format
39
+
40
+ Training data is a plain text file at `training/data/train.txt`. One word per line. Words that appear on consecutive lines are treated as part of the same sentence. The model learns word boundaries from this.
41
+
42
+ Example `training/data/train.txt`:
43
+
44
+ ```
45
+ សួស្តី
46
+ បង
47
+ ប្អូន
48
+ ខ្ញុំ
49
+ ទៅ
50
+ ផ្សារ
51
+ ```
52
+
53
+ Non-Khmer tokens (spaces, punctuation, numbers, Latin text) are tagged as `NON-KHMER`. Khmer tokens get `B-WORD` on the first character and `I-WORD` on the rest.
54
+
55
+ ### Steps
56
+
57
+ ```bash
58
+ cd training
59
+ pip install -r requirements.txt
60
+ ```
61
+
62
+ **1. Prepare training data**
63
+
64
+ Place your segmented text in `data/train.txt` (one word per line). If you have raw unsegmented Khmer text, you can use the generation script to pre-segment it:
65
+
66
+ ```bash
67
+ python generate.py
68
+ ```
69
+
70
+ This requires `khmersegment` and a source text file. Edit the path in `generate.py` to point to your raw text.
71
+
72
+ **2. Train**
73
+
74
+ ```bash
75
+ python train.py
76
+ ```
77
+
78
+ Trains for 20 epochs with AdamW (lr=1e-5) and ReduceLROnPlateau. Saves `best_model.pt` (best eval loss) and `model.pt` (final). Uses CUDA if available.
79
+
80
+ **3. Convert to GGUF**
81
+
82
+ ```bash
83
+ python convert_to_gguf.py best_model.pt model.gguf
84
+ ```
85
+
86
+ This produces a GGUF file (~3.3MB) containing all model weights.
87
+
88
+ **4. Embed in the C++ binary**
89
+
90
+ To use the new model in the library, convert the GGUF file to a C header and replace `src/model_data.h`, then rebuild:
91
+
92
+ ```bash
93
+ xxd -i model.gguf > ../src/model_data.h
94
+ pip install -e ..
95
+ ```
96
+
97
+ ## License
98
+
99
+ MIT
Binary file
@@ -0,0 +1,51 @@
1
+ [build-system]
2
+ requires = ["scikit-build-core>=0.11", "pybind11>=3.0"]
3
+ build-backend = "scikit_build_core.build"
4
+
5
+ [project]
6
+ name = "khmerns"
7
+ version = "0.0.3"
8
+ license = "MIT"
9
+ license-files = ["LICENSE"]
10
+ description = "Khmer Neural Segmenter"
11
+ readme = "README.md"
12
+ authors = [{ name = "Seanghay Yath", email = "seanghay.dev@gmail.com" }]
13
+ requires-python = ">=3.9"
14
+ keywords = ["khmer", "nlp", "segmentation", "tokenization", "neural-network"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Programming Language :: Python :: 3 :: Only",
18
+ "Programming Language :: Python :: 3.9",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Programming Language :: Python :: 3.14",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/seanghay/khmer-neural-segmenter"
28
+ Repository = "https://github.com/seanghay/khmer-neural-segmenter"
29
+ Issues = "https://github.com/seanghay/khmer-neural-segmenter/issues"
30
+
31
+ [tool.scikit-build]
32
+ minimum-version = "build-system.requires"
33
+ wheel.packages = ["src/khmerns"]
34
+
35
+ [tool.cibuildwheel]
36
+ build-frontend = "build[uv]"
37
+ enable = ["pypy"]
38
+
39
+ [tool.cibuildwheel.pyodide]
40
+ build-frontend = { name = "build", args = ["--exports", "whole_archive"] }
41
+
42
+ [tool.cibuildwheel.ios]
43
+ build-frontend = "build"
44
+ xbuild-tools = ["cmake", "ninja"]
45
+
46
+ [tool.cibuildwheel.android]
47
+ build-frontend = "build"
48
+ environment.ANDROID_API_LEVEL = "24"
49
+
50
+ [tool.ruff]
51
+ indent-width = 2