jupyterhub-datamountspawner 0.1.0__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,174 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+
110
+ # pdm
111
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
112
+ #pdm.lock
113
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
114
+ # in version control.
115
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
116
+ .pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
121
+ __pypackages__/
122
+
123
+ # Celery stuff
124
+ celerybeat-schedule
125
+ celerybeat.pid
126
+
127
+ # SageMath parsed files
128
+ *.sage.py
129
+
130
+ # Environments
131
+ .env
132
+ .venv
133
+ env/
134
+ venv/
135
+ ENV/
136
+ env.bak/
137
+ venv.bak/
138
+
139
+ # Spyder project settings
140
+ .spyderproject
141
+ .spyproject
142
+
143
+ # Rope project settings
144
+ .ropeproject
145
+
146
+ # mkdocs documentation
147
+ /site
148
+
149
+ # mypy
150
+ .mypy_cache/
151
+ .dmypy.json
152
+ dmypy.json
153
+
154
+ # Pyre type checker
155
+ .pyre/
156
+
157
+ # pytype static type analyzer
158
+ .pytype/
159
+
160
+ # Cython debug symbols
161
+ cython_debug/
162
+
163
+ # PyCharm
164
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
165
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
166
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
167
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
168
+ #.idea/
169
+
170
+ # Ruff stuff:
171
+ .ruff_cache/
172
+
173
+ # PyPI configuration file
174
+ .pypirc
@@ -0,0 +1,16 @@
1
+ repos:
2
+ - repo: https://github.com/asottile/reorder_python_imports
3
+ rev: v2.6.0
4
+ hooks:
5
+ - id: reorder-python-imports
6
+ - repo: https://github.com/psf/black
7
+ rev: 23.9.1
8
+ hooks:
9
+ - id: black
10
+ - repo: https://github.com/pre-commit/pre-commit-hooks
11
+ rev: v2.4.0
12
+ hooks:
13
+ - id: end-of-file-fixer
14
+ - id: check-json
15
+ - id: check-case-conflict
16
+ - id: check-executables-have-shebangs
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2025, jsc-jupyter
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,45 @@
1
+ Metadata-Version: 2.4
2
+ Name: jupyterhub-datamountspawner
3
+ Version: 0.1.0
4
+ Summary: JupyterHub Spawner for JupyterLab DataMount Extension Integration
5
+ Author-email: Tim Kreuzer <t.kreuzer@fz-juelich.de>
6
+ License: BSD 3-Clause License
7
+
8
+ Copyright (c) 2025, jsc-jupyter
9
+
10
+ Redistribution and use in source and binary forms, with or without
11
+ modification, are permitted provided that the following conditions are met:
12
+
13
+ 1. Redistributions of source code must retain the above copyright notice, this
14
+ list of conditions and the following disclaimer.
15
+
16
+ 2. Redistributions in binary form must reproduce the above copyright notice,
17
+ this list of conditions and the following disclaimer in the documentation
18
+ and/or other materials provided with the distribution.
19
+
20
+ 3. Neither the name of the copyright holder nor the names of its
21
+ contributors may be used to endorse or promote products derived from
22
+ this software without specific prior written permission.
23
+
24
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
25
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
26
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
27
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
28
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
29
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
30
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
31
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
32
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
33
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34
+ License-File: LICENSE
35
+ Keywords: data,jupyterhub,spawner,storage
36
+ Requires-Python: >=3.9
37
+ Requires-Dist: escapism
38
+ Requires-Dist: jinja2
39
+ Requires-Dist: jupyterhub>=4.0.0
40
+ Requires-Dist: traitlets
41
+ Requires-Dist: urllib3
42
+ Description-Content-Type: text/markdown
43
+
44
+ # jupyterhub-data-mount-kubespawner
45
+ A JupyterHub Spawner, based on the KubeSpawner, that launches JupyterLab instances with the DataMount extension for seamless external storage integration.
@@ -0,0 +1,2 @@
1
+ # jupyterhub-data-mount-kubespawner
2
+ A JupyterHub Spawner, based on the KubeSpawner, that launches JupyterLab instances with the DataMount extension for seamless external storage integration.
@@ -0,0 +1,2 @@
1
+ from ._version import __version__
2
+ from .spawner import KubeSpawner
@@ -0,0 +1,4 @@
1
+ # __version__ should be updated using tbump, based on configuration in
2
+ # pyproject.toml, according to instructions in RELEASE.md.
3
+ #
4
+ __version__ = "0.1.0"
@@ -0,0 +1,300 @@
1
+ import base64
2
+ import json
3
+
4
+ from kubespawner import KubeSpawner as OrigKubeSpawner
5
+ from traitlets import Callable
6
+ from traitlets import default
7
+ from traitlets import List
8
+ from traitlets import observe
9
+ from traitlets import Unicode
10
+ from traitlets import Union
11
+
12
+
13
+ class DataMountKubeSpawner(OrigKubeSpawner):
14
+ templates = Union(
15
+ trait_types=[List(), Callable()],
16
+ default_value=[],
17
+ config=True,
18
+ help="""
19
+ Configure which mount templates should be shown to the user. This also defines the order.
20
+ """,
21
+ )
22
+
23
+ def get_templates(self):
24
+ if callable(self.templates):
25
+ return self.templates(self)
26
+ return self.templates
27
+
28
+ init_mounts = List(
29
+ [],
30
+ help="""
31
+ List of dictionaries representing additional mounts to be added to the pod.
32
+
33
+ This may be a coroutine.
34
+
35
+ Example::
36
+
37
+ c.KubeSpawner.init_mounts = [
38
+ {
39
+ "path": "aws",
40
+ "options": {
41
+ "displayName": "AWS #1",
42
+ "template": "aws",
43
+ "config": {
44
+ "remotepath": "bucketname",
45
+ "type": "s3",
46
+ "provider": "AWS",
47
+ "access_key_id": "_id_",
48
+ "secret_access_key": "_secret_",
49
+ "region": "eu-north-1"
50
+ }
51
+ }
52
+ },
53
+ {
54
+ "path": "b2drop",
55
+ "options": {
56
+ "displayName": "B2Drop",
57
+ "template": "b2drop",
58
+ "readonly": true,
59
+ "config": {
60
+ "remotepath": "/",
61
+ "type": "webdav",
62
+ "url": "https://b2drop.eudat.eu/remote.php/webdav/",
63
+ "vendor": "nextcloud",
64
+ "user": "_user_",
65
+ "obscure_pass": "_password_"
66
+ }
67
+ }
68
+ }
69
+ ]
70
+ """,
71
+ ).tag(config=True)
72
+
73
+ def get_env(self):
74
+ env = super().get_env()
75
+ env["JUPYTERLAB_DATA_MOUNT_ENABLED"] = "true"
76
+ env["JUPYTERLAB_DATA_MOUNT_DIR"] = self.data_mount_path
77
+ templates = self.get_templates()
78
+ self.log.info("ABC")
79
+ self.log.info(templates)
80
+ if templates:
81
+ env["JUPYTERLAB_DATA_MOUNT_TEMPLATES"] = ",".join(templates)
82
+ return env
83
+
84
+ def get_default_volumes(self):
85
+ ret = [
86
+ {"name": "data-mounts", "emptyDir": {}},
87
+ {"name": "init-mounts", "emptyDir": {}},
88
+ ]
89
+ return ret
90
+
91
+ @default("volumes")
92
+ def _default_volumes(self):
93
+ """Provide default volumes when none are set."""
94
+ return self.get_default_volumes()
95
+
96
+ @observe("volumes")
97
+ def _ensure_default_volumes(self, change):
98
+ new_volumes = change["new"]
99
+
100
+ try:
101
+ if isinstance(new_volumes, dict):
102
+ new_volumes = [new_volumes]
103
+
104
+ default_volumes = self.get_default_volumes()
105
+ if default_volumes and default_volumes not in new_volumes:
106
+ new_volumes.extend(default_volumes)
107
+
108
+ self.log.info(f"volumes: {new_volumes}")
109
+ self.volumes = new_volumes
110
+ except:
111
+ self.log.exception("wtf")
112
+
113
+ data_mount_path = Unicode(
114
+ "/home/jovyan/data_mounts",
115
+ config=True,
116
+ help="Path to mount data in the notebook container",
117
+ )
118
+
119
+ def get_default_volume_mounts(self):
120
+ return {
121
+ "name": "data-mounts",
122
+ "mountPath": self.data_mount_path,
123
+ "mountPropagation": "HostToContainer",
124
+ }
125
+
126
+ @default("volume_mounts")
127
+ def _default_volumes_mounts(self):
128
+ """Provide default volumes when none are set."""
129
+ return [self.get_default_volume_mounts()]
130
+
131
+ @observe("volume_mounts")
132
+ def _ensure_default_volume_mounts(self, change):
133
+ new_volume_mounts = change["new"]
134
+
135
+ if isinstance(new_volume_mounts, dict):
136
+ new_volume_mounts = [new_volume_mounts]
137
+
138
+ default_volume_mounts = self.get_default_volume_mounts()
139
+
140
+ if default_volume_mounts and default_volume_mounts not in new_volume_mounts:
141
+ new_volume_mounts.append(default_volume_mounts)
142
+
143
+ # self.log.info(f"volume_mounts: {new_volume_mounts}")
144
+ self.volume_mounts = new_volume_mounts
145
+
146
+ data_mounts_image = Unicode(
147
+ "jupyterjsc/jupyterlab-data-mount-api:v0.1.4",
148
+ config=True,
149
+ help="Image to use for the data mount container",
150
+ )
151
+
152
+ def _get_extra_data_mount_init_container(self):
153
+ if not self.init_mounts:
154
+ return None
155
+ try:
156
+ mounts_b64 = base64.b64encode(
157
+ json.dumps(self.init_mounts).encode()
158
+ ).decode()
159
+ return {
160
+ "image": "alpine:latest",
161
+ "imagePullPolicy": "Always",
162
+ "name": "init-mounts",
163
+ "volumeMounts": [
164
+ {
165
+ "name": "init-mounts",
166
+ "mountPath": "/mnt/init_mounts",
167
+ }
168
+ ],
169
+ "command": [
170
+ "sh",
171
+ "-c",
172
+ "apk add --no-cache coreutils && echo '%s' | base64 -d > /mnt/init_mounts/mounts.json"
173
+ % mounts_b64,
174
+ ],
175
+ }
176
+ except Exception as e:
177
+ self.log.exception("wtf")
178
+ return None
179
+
180
+ @default("init_containers")
181
+ def _default_init_containers(self):
182
+ """Provide default volumes when none are set."""
183
+ ret = self._get_extra_data_mount_init_container()
184
+ if ret:
185
+ return [ret]
186
+ else:
187
+ return []
188
+
189
+ @observe("init_containers")
190
+ def _ensure_default_init_containers(self, change):
191
+ new_init_containers = change["new"]
192
+
193
+ if isinstance(new_init_containers, dict):
194
+ new_init_containers = [new_init_containers]
195
+
196
+ extra_data_mount_init_container = self._get_extra_data_mount_init_container()
197
+
198
+ if (
199
+ extra_data_mount_init_container
200
+ and extra_data_mount_init_container not in new_init_containers
201
+ ):
202
+ new_init_containers.append(extra_data_mount_init_container)
203
+
204
+ # self.log.info(f"init_containers: {new_init_containers}")
205
+ self.init_containers = new_init_containers
206
+
207
+ def _get_extra_data_mount_container(self):
208
+ volume_mounts = [
209
+ {
210
+ "name": "data-mounts",
211
+ "mountPath": "/mnt/data_mounts",
212
+ "mountPropagation": "Bidirectional",
213
+ }
214
+ ]
215
+ if self.init_mounts:
216
+ volume_mounts.append(
217
+ {
218
+ "name": "init-mounts",
219
+ "mountPath": "/mnt/init_mounts/mounts.json",
220
+ "subPath": "mounts.json",
221
+ }
222
+ )
223
+
224
+ extra_data_mount_container = {
225
+ "image": self.data_mounts_image,
226
+ "imagePullPolicy": "Always",
227
+ "name": "data-mounts",
228
+ "volumeMounts": volume_mounts,
229
+ "securityContext": {
230
+ "capabilities": {"add": ["SYS_ADMIN", "MKNOD", "SETFCAP"]},
231
+ "privileged": True,
232
+ "allowPrivilegeEscalation": True,
233
+ },
234
+ }
235
+ return extra_data_mount_container
236
+
237
+ @default("extra_containers")
238
+ def _default_extra_containers(self):
239
+ """Provide default volumes when none are set."""
240
+ return [self._get_extra_data_mount_container()]
241
+
242
+ @observe("extra_containers")
243
+ def _ensure_default_extra_containers(self, change):
244
+ new_extra_containers = change["new"]
245
+
246
+ if isinstance(new_extra_containers, dict):
247
+ new_extra_containers = [new_extra_containers]
248
+
249
+ extra_data_mount_container = self._get_extra_data_mount_container()
250
+
251
+ if (
252
+ extra_data_mount_container
253
+ and extra_data_mount_container not in new_extra_containers
254
+ ):
255
+ new_extra_containers.append(extra_data_mount_container)
256
+
257
+ # self.log.info(f"extra_containers: {new_extra_containers}")
258
+ self.extra_containers = new_extra_containers
259
+
260
+ @default("cmd")
261
+ def _default_cmd(self):
262
+ """Set the default command if none is provided."""
263
+ base_cmd = [
264
+ "sh",
265
+ "-c",
266
+ "pip install --user jupyterlab-data-mount==0.1.5 && "
267
+ "if command -v start-singleuser.sh > /dev/null; then exec start-singleuser.sh; else exec jupyterhub-singleuser; fi",
268
+ ]
269
+
270
+ return base_cmd
271
+
272
+ _setting_default_cmd = False
273
+
274
+ @observe("cmd")
275
+ def _ensure_pip_first(self, change):
276
+ """Ensure 'pip install --user jupyterlab-data-mount' is always prepended."""
277
+ # Skip recursion if we are setting the default command
278
+ if self._setting_default_cmd:
279
+ return
280
+
281
+ self._setting_default_cmd = True
282
+ new_cmd = change["new"]
283
+
284
+ # self.log.info(f"cmd: {new_cmd}")
285
+ if new_cmd is None or not isinstance(new_cmd, list) or len(new_cmd) == 0:
286
+ # Apply default if cmd is unset or empty
287
+ self.cmd = self._default_cmd()
288
+ else:
289
+ # Otherwise, modify the existing command
290
+ self.cmd = [
291
+ "sh",
292
+ "-c",
293
+ f"pip install --user jupyterlab-data-mount && ({' '.join(new_cmd)})",
294
+ ]
295
+
296
+
297
+ # Implementation with the same name as the original class
298
+ # Allows for easier integration into the JupyterHub HelmChart
299
+ class KubeSpawner(DataMountKubeSpawner):
300
+ pass
@@ -0,0 +1,85 @@
1
+
2
+ [build-system]
3
+ requires = ["hatchling"]
4
+ build-backend = "hatchling.build"
5
+
6
+ [tool.hatch.build.targets.wheel]
7
+ packages = ["datamountspawner"]
8
+ include = ["*.py"]
9
+ exclude = ["test*"]
10
+
11
+ [project]
12
+ name = "jupyterhub-datamountspawner"
13
+ description = "JupyterHub Spawner for JupyterLab DataMount Extension Integration"
14
+ readme = "README.md"
15
+ requires-python = ">=3.9"
16
+ license = {file = "LICENSE"}
17
+ keywords = ["jupyterhub", "spawner", "storage", "data"]
18
+ authors = [
19
+ {name = "Tim Kreuzer", email = "t.kreuzer@fz-juelich.de"},
20
+ ]
21
+
22
+ dependencies = [
23
+ "escapism",
24
+ "jinja2",
25
+ "jupyterhub>=4.0.0",
26
+ "traitlets",
27
+ "urllib3",
28
+ ]
29
+ dynamic = ["version"]
30
+
31
+ [tool.black]
32
+ target_version = [
33
+ "py39",
34
+ "py310",
35
+ "py311",
36
+ ]
37
+
38
+ [tool.hatch.version]
39
+ path = "datamountspawner/_version.py"
40
+
41
+ [tool.isort]
42
+ profile = "black"
43
+
44
+ [tool.tbump]
45
+ # Uncomment this if your project is hosted on GitHub:
46
+ github_url = "https://github.com/jsc-jupyter/jupyterhub-data-mount-kubespawner"
47
+
48
+ [tool.tbump.version]
49
+ current = "1.1.1"
50
+ regex = '''
51
+ (?P<major>\d+)
52
+ \.
53
+ (?P<minor>\d+)
54
+ \.
55
+ (?P<patch>\d+)
56
+ (?P<pre>((a|b|rc)\d+)|)
57
+ \.?
58
+ (?P<post>((post)\d+)|)
59
+ (?P<dev>(?<=\.)dev\d*|)
60
+ '''
61
+
62
+ [tool.tbump.git]
63
+ message_template = "Bump to {new_version}"
64
+ tag_template = "{new_version}"
65
+
66
+ # For each file to patch, add a [[tool.tbump.file]] config
67
+ # section containing the path of the file, relative to the
68
+ # pyproject.toml location.
69
+
70
+ #[[tool.tbump.file]]
71
+ #src = "pyproject.toml"
72
+ #search = 'version = "{current_version}"'
73
+
74
+ [[tool.tbump.file]]
75
+ src = "datamountspawner/_version.py"
76
+ #version_template = '({major}, {minor}, {patch}, "{pre}", "{dev}")'
77
+ #search = "version_info = {current_version}"
78
+
79
+ #[[tool.tbump.file]]
80
+ #src = "docs/source/_static/rest-api.yml"
81
+ #search = "version: {current_version}"
82
+
83
+ [tool.djlint]
84
+ indent = 2
85
+ profile = "jinja"