YoloDatasetCustomizer 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 @@
1
+ *.jpg filter=lfs diff=lfs merge=lfs -text
@@ -0,0 +1,218 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
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
+ # poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ # pdm.lock
116
+ # pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ # pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # Redis
135
+ *.rdb
136
+ *.aof
137
+ *.pid
138
+
139
+ # RabbitMQ
140
+ mnesia/
141
+ rabbitmq/
142
+ rabbitmq-data/
143
+
144
+ # ActiveMQ
145
+ activemq-data/
146
+
147
+ # SageMath parsed files
148
+ *.sage.py
149
+
150
+ # Environments
151
+ .env
152
+ .envrc
153
+ .venv
154
+ env/
155
+ venv/
156
+ ENV/
157
+ env.bak/
158
+ venv.bak/
159
+
160
+ # Spyder project settings
161
+ .spyderproject
162
+ .spyproject
163
+
164
+ # Rope project settings
165
+ .ropeproject
166
+
167
+ # mkdocs documentation
168
+ /site
169
+
170
+ # mypy
171
+ .mypy_cache/
172
+ .dmypy.json
173
+ dmypy.json
174
+
175
+ # Pyre type checker
176
+ .pyre/
177
+
178
+ # pytype static type analyzer
179
+ .pytype/
180
+
181
+ # Cython debug symbols
182
+ cython_debug/
183
+
184
+ # PyCharm
185
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
186
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
187
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
188
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
189
+ # .idea/
190
+
191
+ # Abstra
192
+ # Abstra is an AI-powered process automation framework.
193
+ # Ignore directories containing user credentials, local state, and settings.
194
+ # Learn more at https://abstra.io/docs
195
+ .abstra/
196
+
197
+ # Visual Studio Code
198
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
199
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
200
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
201
+ # you could uncomment the following to ignore the entire vscode folder
202
+ # .vscode/
203
+ # Temporary file for partial code execution
204
+ tempCodeRunnerFile.py
205
+
206
+ # Ruff stuff:
207
+ .ruff_cache/
208
+
209
+ # PyPI configuration file
210
+ .pypirc
211
+
212
+ # Marimo
213
+ marimo/_static/
214
+ marimo/_lsp/
215
+ __marimo__/
216
+
217
+ # Streamlit
218
+ .streamlit/secrets.toml
@@ -0,0 +1 @@
1
+ 3.14
@@ -0,0 +1,17 @@
1
+ GNU GENERAL PUBLIC LICENSE
2
+ Version 3, 29 June 2007
3
+
4
+ Copyright (C) 2024 Luca Mazzon
5
+
6
+ This program is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU General Public License as published by
8
+ the Free Software Foundation, either version 3 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU General Public License for more details.
15
+
16
+ You should have received a copy of the GNU General Public License
17
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
@@ -0,0 +1,70 @@
1
+ Metadata-Version: 2.5
2
+ Name: YoloDatasetCustomizer
3
+ Version: 0.1.0
4
+ Summary: Load one or multiple datasets. Reduce, mix or merge into a custom dataset.
5
+ Project-URL: Homepage, https://github.com/mLuca/YoloDatasetCustomizer
6
+ Project-URL: Repository, https://github.com/mLuca/YoloDatasetCustomizer
7
+ Project-URL: Issue Tracker, https://github.com/mLuca/YoloDatasetCustomizer/issues
8
+ Author-email: Luca Mazzon <mazzon.luca@gmail.com>
9
+ License: GPL-3.0-only
10
+ License-File: LICENSE
11
+ Keywords: computer-vision,customization,dataset,machine-learning,ultralytics,yolo
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
23
+ Classifier: Topic :: Scientific/Engineering :: Image Recognition
24
+ Requires-Python: >=3.10
25
+ Requires-Dist: pyyaml>=6.0.3
26
+ Description-Content-Type: text/markdown
27
+
28
+ # Intro
29
+
30
+ This tool is meant to help create a custom dataset by picking and choosing class names from pre-existing datasets.
31
+
32
+ # Limitations
33
+
34
+ - The dataset YAML file is expected to be called _data.yaml_
35
+ - The 'path' variable in data.yaml is not processed.
36
+ It is assumed that the dataset root path is the directory where _data.yaml_ resides and all data is present already.
37
+ - The _script_ variable in _data.yaml_ is not processed. Make sure all data is present.
38
+
39
+ # Usage
40
+
41
+ If a path ends on _data.yaml_ it will take that file into account.
42
+ If a path doesn't end on _/data.yaml_ it will recursevly look for all _data.yaml_ files from that directory downards.
43
+ e.g.
44
+
45
+ from YoloDatasetCustomizer import YoloDatasetCustomizer
46
+
47
+ ydc = YoloDatasetCustomizer(list_to_existing_datasets)
48
+ ydc.add([one_more_existing_dataset])
49
+
50
+ if ydc.create_new_dataset_for_class_names(set_of_wanted_class_names, optional_custom_path, optional_custom_name):
51
+ print("All good")
52
+ else:
53
+ print("Oh no.")
54
+
55
+ # Run tests
56
+
57
+ The project uses uv. Install dependencies and run tests with:
58
+
59
+ uv run pytest
60
+
61
+ # Build project
62
+
63
+ Build the package
64
+
65
+ uv build
66
+
67
+ Publish to PyPI (still need twine)
68
+
69
+ uv pip install twine
70
+ uv run twine upload dist/\*
@@ -0,0 +1,43 @@
1
+ # Intro
2
+
3
+ This tool is meant to help create a custom dataset by picking and choosing class names from pre-existing datasets.
4
+
5
+ # Limitations
6
+
7
+ - The dataset YAML file is expected to be called _data.yaml_
8
+ - The 'path' variable in data.yaml is not processed.
9
+ It is assumed that the dataset root path is the directory where _data.yaml_ resides and all data is present already.
10
+ - The _script_ variable in _data.yaml_ is not processed. Make sure all data is present.
11
+
12
+ # Usage
13
+
14
+ If a path ends on _data.yaml_ it will take that file into account.
15
+ If a path doesn't end on _/data.yaml_ it will recursevly look for all _data.yaml_ files from that directory downards.
16
+ e.g.
17
+
18
+ from YoloDatasetCustomizer import YoloDatasetCustomizer
19
+
20
+ ydc = YoloDatasetCustomizer(list_to_existing_datasets)
21
+ ydc.add([one_more_existing_dataset])
22
+
23
+ if ydc.create_new_dataset_for_class_names(set_of_wanted_class_names, optional_custom_path, optional_custom_name):
24
+ print("All good")
25
+ else:
26
+ print("Oh no.")
27
+
28
+ # Run tests
29
+
30
+ The project uses uv. Install dependencies and run tests with:
31
+
32
+ uv run pytest
33
+
34
+ # Build project
35
+
36
+ Build the package
37
+
38
+ uv build
39
+
40
+ Publish to PyPI (still need twine)
41
+
42
+ uv pip install twine
43
+ uv run twine upload dist/\*
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "YoloDatasetCustomizer"
7
+ version = "0.1.0"
8
+ description = "Load one or multiple datasets. Reduce, mix or merge into a custom dataset."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = {text = "GPL-3.0-only"}
12
+ authors = [
13
+ {name = "Luca Mazzon", email = "mazzon.luca@gmail.com"},
14
+ ]
15
+ keywords = ["ultralytics", "yolo", "dataset", "machine-learning", "computer-vision", "customization"]
16
+ classifiers = [
17
+ "Development Status :: 5 - Production/Stable",
18
+ "Intended Audience :: Developers",
19
+ "Intended Audience :: Science/Research",
20
+ "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Programming Language :: Python :: 3.14",
27
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
28
+ "Topic :: Scientific/Engineering :: Image Recognition",
29
+ ]
30
+
31
+ dependencies = [
32
+ "pyyaml>=6.0.3",
33
+ ]
34
+
35
+ [project.urls]
36
+ Homepage = "https://github.com/mLuca/YoloDatasetCustomizer"
37
+ Repository = "https://github.com/mLuca/YoloDatasetCustomizer"
38
+ "Issue Tracker" = "https://github.com/mLuca/YoloDatasetCustomizer/issues"
39
+
40
+ [dependency-groups]
41
+ dev = [
42
+ "pytest>=9.1.1",
43
+ ]
@@ -0,0 +1,351 @@
1
+ import yaml
2
+ import glob
3
+ import os
4
+ import shutil
5
+ import re
6
+ from typing import Any
7
+
8
+ _SPLIT_NAMES =["train", "val", "test"]
9
+
10
+ def _get_unique_path(path:str) -> str:
11
+ """Return an unused file path by appending a numeric suffix when needed."""
12
+ path = os.path.abspath(path)
13
+ base, ext = os.path.splitext(path)
14
+ unique_path = path
15
+ index = 1
16
+ while True:
17
+ if os.path.exists(unique_path):
18
+ unique_path = "".join([base, f"_{index}", ext])
19
+ index += 1
20
+ else:
21
+ return unique_path
22
+
23
+ class YoloDataFileWriter():
24
+ """Create and write a YOLO dataset YAML file for a generated dataset."""
25
+ def __init__(self, data_set_dir: str = "./", class_names: list[str] = [], train_split: str = "./train", val_split: str = "./valid", test_split: str = "./test") -> None:
26
+ """Initialize writer with dataset directory, class names, and split locations."""
27
+ self.data_set_dir = os.path.abspath(data_set_dir)
28
+ self.class_names = class_names
29
+ self.train_split = train_split
30
+ self.val_split = val_split
31
+ self.test_split = test_split
32
+ self.FILE_NAME = "data.yaml"
33
+
34
+ def write(self):
35
+ """Write the YOLO data.yaml file with train/val/test paths and class names."""
36
+ classes = {}
37
+ for i, class_name in enumerate(self.class_names):
38
+ classes[i] = class_name
39
+ data = {
40
+ "train": f"{self.train_split}/images",
41
+ "val": f"{self.val_split}/images",
42
+ "test": f"{self.test_split}/images",
43
+ "names": classes,
44
+ "nc": len(self.class_names),
45
+ }
46
+
47
+ with open(os.path.join(self.data_set_dir, self.FILE_NAME), "w") as f:
48
+ yaml.dump(data,f, default_flow_style=False)
49
+
50
+
51
+
52
+
53
+
54
+ class YoloDataFileReader():
55
+ """Read a YOLO dataset YAML file and expose its split paths and class names."""
56
+ def __init__(self, file_path: str):
57
+ """
58
+ Args:
59
+ file_path: Valid path to the data set YAML file
60
+ Raises:
61
+ yaml.YAMLError: When there is an issue with reading the data-set YAML file
62
+ FileNotFoundError: When the data-set YAML file does not exist
63
+ ValueError: When a value in the YAML file is missing or is faulty
64
+ TypeError: When a value comes in an unexpected type
65
+ """
66
+ self.__file_path = os.path.abspath(file_path)
67
+ self.__file_dir = os.path.dirname(self.__file_path)
68
+ self.__splits_paths: dict[str,list[str]] = {}
69
+ self.__class_names: list[str] = []
70
+
71
+ self.sync_content()
72
+
73
+ def get_split_paths_for_split_name(self, split_name:str) -> list[str]:
74
+ """Return a list of dataset split paths for the requested split name."""
75
+ return self.__splits_paths.get(split_name,[])
76
+
77
+ def get_class_names(self) -> list[str]:
78
+ """Return the class names defined in the dataset YAML."""
79
+ return self.__class_names
80
+
81
+ def get_file_path(self) -> str:
82
+ """Return the absolute path to the loaded YAML file."""
83
+ return self.__file_path
84
+
85
+ def get_file_dir(self) -> str:
86
+ """Return the directory containing the loaded dataset YAML file."""
87
+ return self.__file_dir
88
+
89
+ def get_indices_for_names(self, class_names: set[str]) -> set[str]:
90
+ """Map a set of class names to their corresponding numeric label indices."""
91
+ ret: set[str] = set()
92
+ for class_name in class_names:
93
+ try:
94
+ idx = self.__class_names.index(class_name)
95
+ ret.add(str(idx))
96
+ except:
97
+ pass
98
+ return ret
99
+
100
+ def sync_content(self):
101
+ """Reload YAML content and refresh split and name mappings."""
102
+ with open(self.__file_path) as f:
103
+ content = yaml.safe_load(f)
104
+ for split_name in _SPLIT_NAMES:
105
+ self.__splits_paths[split_name] = self.__read_yaml_entry(content, split_name)
106
+ self.__class_names = self.__read_yaml_entry(content, "names")
107
+
108
+
109
+ def __read_yaml_entry(self, yaml_content: Any, entry_name: str, missing_ok:bool = True) -> list[str]:
110
+ """Normalize YAML entry values into a list of paths or names."""
111
+ entry = yaml_content.get(entry_name)
112
+ ret: list[str] = []
113
+ if entry:
114
+ if isinstance(entry, str):
115
+ ret.append(entry)
116
+ elif isinstance(entry, dict):
117
+ ret.extend(entry.values())
118
+ elif isinstance(entry, list):
119
+ ret.extend(entry)
120
+ else:
121
+ raise TypeError(f"Unexpected type for {entry_name} in: {self.__file_path}")
122
+ else:
123
+ if not missing_ok:
124
+ raise ValueError(f"{entry_name} not defined in: {self.__file_path}")
125
+
126
+ return ret
127
+
128
+
129
+ def __repr__(self) -> str:
130
+ return f"\nYoloDatasetFile(\n path: {self.__file_path}\n training_split_path: {self.__training_split_paths}\n validation_split_path: {self.__validation_split_paths}\n testing_split_path: {self.__testing_split_paths}\n class_names: {self.__class_names}\n)\n"
131
+
132
+ class LabelFile():
133
+ """Read YOLO label files and copy label lines with remapped class indices."""
134
+ def __init__(self, file_path: str):
135
+ """
136
+ Args:
137
+ file_path: Valid path to the label file
138
+ Raises:
139
+ FileNotFoundError: When the lablelfile does not exist
140
+ """
141
+ self.__class_indeces: set[str] = set()
142
+ self.file_path = os.path.abspath(file_path)
143
+ if not os.path.exists(self.file_path) or not os.path.isfile(self.file_path):
144
+ raise FileNotFoundError(f"No label file found at: {self.file_path}")
145
+
146
+ self.read_label_file()
147
+
148
+ def read_label_file(self):
149
+ """Parse the label file and collect all class indices referenced in it."""
150
+ with open(self.file_path) as f:
151
+ for i, line in enumerate(f):
152
+ match = re.match(r"^(\d+) ", line)
153
+ if not match:
154
+ print(f"WARNING: No class found on line {i} in label file {self.file_path}")
155
+ else:
156
+ self.__class_indeces.add(match.group(1))
157
+
158
+ def get_class_indices(self) -> set[str]:
159
+ """Return the set of class indices in the loaded label file."""
160
+ return self.__class_indeces
161
+
162
+ def copy_by_class_indeces(self, old_to_new_index_match: dict[str,str], dst_path: str, file_name: str = "") -> str|None:
163
+ """Copy the label file to the destination with indices remapped for the new dataset."""
164
+
165
+ common_class_indeces = self.__class_indeces & set(old_to_new_index_match.keys())
166
+ if not common_class_indeces:
167
+ return None
168
+
169
+ dst_path = os.path.abspath(dst_path)
170
+ if not os.path.basename(dst_path) == "labels":
171
+ dst_path = os.path.join(dst_path,"labels")
172
+
173
+ if os.path.exists(dst_path) and os.path.isdir(dst_path):
174
+ new_content: list[str] = []
175
+ with open(self.file_path) as f:
176
+ for line in f:
177
+ match = re.match(r"^(\d+) ", line)
178
+ if not match:
179
+ continue
180
+ elif match.group(1) in common_class_indeces:
181
+ old_index = match.group(1)
182
+ new_index = old_to_new_index_match[old_index]
183
+ if new_index != old_index:
184
+ line = line.replace(old_index, new_index, 1)
185
+ new_content.append(line)
186
+
187
+ if file_name == "":
188
+ file_name = os.path.basename(self.file_path)
189
+ dst_file_path = _get_unique_path("/".join([dst_path, file_name]))
190
+
191
+ with open(dst_file_path, "w") as f:
192
+ f.writelines(new_content)
193
+ else:
194
+ raise FileNotFoundError(f"Directory not found: {dst_path}")
195
+
196
+ return dst_file_path
197
+
198
+
199
+
200
+
201
+ class YoloDatasetCustomizer():
202
+ """Find and customize YOLO datasets across one or more directory paths."""
203
+ def __init__(self, data_set_paths: list[str]):
204
+ """Initialize with YAML dataset paths or directories to scan for YOLO datasets."""
205
+ self.__found_data_file_paths: set[str] = set()
206
+ self.__data_sets: list[YoloDataFileReader] = []
207
+
208
+ self.add_data_sets(data_set_paths)
209
+ self.SUPPORTED_IMAGE_FORMATS = [".avif", ".bmp", ".dng", ".heic", ".jp2", ".jpeg", ".jpg", ".mpo", ".png", ".tif", ".tiff", ".webp"]
210
+
211
+ def add_data_sets(self, data_set_paths: list[str]):
212
+ """Discover YOLO data.yaml files from explicit paths or directory trees."""
213
+ data_set_paths = list(map(lambda p: os.path.abspath(p), data_set_paths))
214
+
215
+ self.__found_data_file_paths.update([path for path in data_set_paths if path.endswith("data.yaml")])
216
+
217
+ paths_without_file_ending = [path for path in data_set_paths if not path.endswith("data.yaml")]
218
+ for path in paths_without_file_ending:
219
+ self.__found_data_file_paths.update(glob.glob(path + "/**/data.yaml", recursive=True))
220
+
221
+ for path in self.__found_data_file_paths:
222
+ try:
223
+ ydfr = YoloDataFileReader(path)
224
+ self.__data_sets.append(ydfr)
225
+ except (yaml.YAMLError, TypeError, ValueError) as e:
226
+ print(f"ERROR: Issue loading YAML file at {path}: {e}")
227
+ except FileNotFoundError:
228
+ print(f"ERROR: Yaml file does not exist at {path}")
229
+
230
+
231
+
232
+ def get_found_data_file_paths(self) -> set[str]:
233
+ """Return all discovered YOLO dataset YAML file paths."""
234
+ return self.__found_data_file_paths
235
+
236
+ def get_found_class_names(self) -> set[str]:
237
+ """Return the union of all class names from discovered datasets."""
238
+ found_classes: set[str] = set()
239
+ for ydf in self.__data_sets:
240
+ found_classes.update(ydf.get_class_names())
241
+ return found_classes
242
+
243
+ def create_new_dataset_for_class_names(self, class_names: set[str], dst_path: str = ".", data_set_name: str = "new_dataset", ignore_img_formats: bool = False) -> bool:
244
+ """Create a filtered dataset containing only the requested classes."""
245
+ if len(class_names) == 0:
246
+ print("ERROR: No classes selected for new dataset.")
247
+ return False
248
+
249
+ # Build a unique target folder for the new dataset
250
+ new_dataset_path = "/".join([os.path.abspath(dst_path), data_set_name])
251
+ new_dataset_path = _get_unique_path(new_dataset_path)
252
+
253
+ # Create a new label index mapping for the selected classes
254
+ new_class_indeces = self.__generate_new_class_indeces(class_names)
255
+ print(f"INFO: New class indeces: {new_class_indeces}")
256
+
257
+ # Iterate over all loaded datasets and keep only objects for selected classes
258
+ for data_set in self.__data_sets:
259
+ common_class_names = set(data_set.get_class_names()) & class_names
260
+ if not common_class_names:
261
+ continue
262
+ else:
263
+ # Map old indices from the source dataset to new indices for the target dataset
264
+ old_to_new_index_match: dict[str, str] = {}
265
+ for class_name in common_class_names:
266
+ old_index = str(data_set.get_class_names().index(class_name))
267
+ new_index = new_class_indeces[class_name]
268
+ old_to_new_index_match[old_index] = new_index
269
+
270
+
271
+ # Copy files from each split (train/val/test) if they contain selected classes
272
+ for split_name in _SPLIT_NAMES:
273
+ for split_path in data_set.get_split_paths_for_split_name(split_name):
274
+ old_images_path = os.path.join(os.path.abspath(data_set.get_file_dir()), split_path)
275
+ old_labels_path = self.__generate_label_path_from_img_path(old_images_path)
276
+
277
+ # Skip missing split directories and continue with the next path
278
+ for path in [old_labels_path, old_images_path]:
279
+ if not os.path.exists(path):
280
+ print(f"WARNING: Path '{path}' does not exist! Skipping it.")
281
+ continue
282
+
283
+ new_labels_path = "/".join([new_dataset_path, split_name, "labels"])
284
+ new_images_path = "/".join([new_dataset_path, split_name, "images"])
285
+ os.makedirs(new_labels_path, exist_ok=True)
286
+ os.makedirs(new_images_path, exist_ok=True)
287
+
288
+ # Process every label file in the source labels directory
289
+ for original_label_file_path in glob.glob(f"{old_labels_path}/*.txt"):
290
+ label_file = LabelFile(original_label_file_path)
291
+
292
+ # Copy only labels that belong to selected classes, remapping indices, renaming file if name collison occurs
293
+ new_label_file_path = label_file.copy_by_class_indeces(old_to_new_index_match, new_labels_path)
294
+ if new_label_file_path:
295
+ # Find images belonging to copied label file and copy them with renaming if neccessary
296
+ file_name = os.path.basename(original_label_file_path)
297
+ file_name_no_ext = file_name[:file_name.rfind('.')]
298
+ corresponding_image_files = glob.glob(f"{old_images_path}/{file_name_no_ext}.*")
299
+ for img in corresponding_image_files:
300
+ try:
301
+ _ , ext = os.path.splitext(img)
302
+ except:
303
+ print(f"ERROR: Not a valid image, extension missing for: {img}")
304
+ return False
305
+
306
+ # Validate image format before copying
307
+ if not ignore_img_formats and ext.lower() not in self.SUPPORTED_IMAGE_FORMATS:
308
+ print(f"ERROR: Image format not supported by yolo: {img}\nSupported formats are: {self.SUPPORTED_IMAGE_FORMATS}")
309
+ return False
310
+
311
+ # Preserve the label filename and copy the image alongside the new label
312
+ new_image_name,_ = os.path.splitext(os.path.basename(new_label_file_path))
313
+ new_image_name = new_image_name + ext
314
+ destination = "/".join([new_images_path, new_image_name])
315
+ if not shutil.copyfile(img, destination):
316
+ print(f"ERROR: Image could not be copied:\nSource: {img}\nDestination: {destination}")
317
+ return False
318
+
319
+ # Write the new dataset YAML once file copying is complete
320
+ data_file_writer = YoloDataFileWriter(new_dataset_path, list(new_class_indeces.keys()))
321
+ try:
322
+ data_file_writer.write()
323
+ except Exception as e:
324
+ print(f"ERROR: Could not write new data.yaml to {new_dataset_path}. Generation of {data_set_name} is incomplete!")
325
+ print(f"Exception was: {e}")
326
+ return False
327
+
328
+
329
+ return True
330
+
331
+ def __generate_new_class_indeces(self, class_names:set[str])-> dict[str, str]:
332
+ """Generate a fresh label index mapping for the selected class names."""
333
+ ret: dict[str, str] = {}
334
+ for i, name in enumerate(class_names):
335
+ ret[name] = str(i)
336
+ return ret
337
+
338
+ def __generate_label_path_from_img_path(self, img_path: str) -> str:
339
+ """Convert an images directory path into its corresponding labels directory path."""
340
+ img_path = "".join([img_path, "/"])
341
+ return img_path.replace("/images/", "/labels/")
342
+
343
+
344
+
345
+
346
+
347
+
348
+
349
+
350
+
351
+
@@ -0,0 +1,9 @@
1
+ """YOLO Dataset Customizer - Load, mix, merge, and customize YOLO datasets."""
2
+
3
+ from .YoloDatasetCustomizer import YoloDatasetCustomizer
4
+
5
+ __version__ = "0.1.0"
6
+ __author__ = "Luca Mazzon"
7
+ __email__ = "mazzon.luca@gmail.com"
8
+
9
+ __all__ = ["YoloDatasetCustomizer"]
@@ -0,0 +1,224 @@
1
+ import os
2
+ import sys
3
+ import tempfile
4
+ import shutil
5
+ import unittest
6
+ import yaml
7
+
8
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
9
+
10
+ from YoloDatasetCustomizer.YoloDatasetCustomizer import (
11
+ YoloDataFileWriter,
12
+ YoloDataFileReader,
13
+ LabelFile,
14
+ YoloDatasetCustomizer,
15
+ )
16
+
17
+
18
+ class TestYoloDatasetCustomizer(unittest.TestCase):
19
+ @classmethod
20
+ def setUpClass(cls):
21
+ cls.workspace_root = tempfile.mkdtemp(prefix="yolodatasetcustomizer_test_")
22
+
23
+ # Good dataset used for positive tests
24
+ cls.good_dataset_dir = os.path.join(cls.workspace_root, "good_dataset")
25
+ os.makedirs(cls.good_dataset_dir, exist_ok=True)
26
+ writer = YoloDataFileWriter(
27
+ data_set_dir=cls.good_dataset_dir,
28
+ class_names=["person", "car"],
29
+ train_split="train",
30
+ val_split="valid",
31
+ test_split="test",
32
+ )
33
+ writer.write()
34
+ for split_name in ("train", "valid", "test"):
35
+ images_dir = os.path.join(cls.good_dataset_dir, split_name, "images")
36
+ labels_dir = os.path.join(cls.good_dataset_dir, split_name, "labels")
37
+ os.makedirs(images_dir, exist_ok=True)
38
+ os.makedirs(labels_dir, exist_ok=True)
39
+
40
+ cls._create_dummy_image_file(os.path.join(cls.good_dataset_dir, "train", "images", "person_car.jpg"))
41
+ cls._write_text_file(
42
+ os.path.join(cls.good_dataset_dir, "train", "labels", "person_car.txt"),
43
+ "0 0.1 0.2 0.3 0.4\n1 0.5 0.6 0.7 0.8\n",
44
+ )
45
+ cls._create_dummy_image_file(os.path.join(cls.good_dataset_dir, "valid", "images", "car.png"))
46
+ cls._write_text_file(
47
+ os.path.join(cls.good_dataset_dir, "valid", "labels", "car.txt"),
48
+ "1 0.2 0.3 0.4 0.5\n",
49
+ )
50
+ cls._create_dummy_image_file(os.path.join(cls.good_dataset_dir, "test", "images", "person.jpg"))
51
+ cls._write_text_file(
52
+ os.path.join(cls.good_dataset_dir, "test", "labels", "person.txt"),
53
+ "0 0.4 0.5 0.6 0.7\n",
54
+ )
55
+
56
+ # Dataset with invalid YAML entry type for names
57
+ cls.invalid_yaml_dataset_dir = os.path.join(cls.workspace_root, "invalid_yaml_dataset")
58
+ os.makedirs(cls.invalid_yaml_dataset_dir, exist_ok=True)
59
+ invalid_yaml_content = """
60
+ train: train/images
61
+ val: valid/images
62
+ test: test/images
63
+ names: !!set {person: null}
64
+ nc: 1
65
+ """
66
+ cls._write_text_file(
67
+ os.path.join(cls.invalid_yaml_dataset_dir, "data.yaml"),
68
+ invalid_yaml_content,
69
+ )
70
+ cls._create_dummy_image_file(os.path.join(cls.invalid_yaml_dataset_dir, "train", "images", "person_car.jpg"))
71
+ os.makedirs(os.path.join(cls.invalid_yaml_dataset_dir, "train", "labels"), exist_ok=True)
72
+ cls._write_text_file(
73
+ os.path.join(cls.invalid_yaml_dataset_dir, "train", "labels", "person_car.txt"),
74
+ "0 0.1 0.2 0.3 0.4\n1 0.5 0.6 0.7 0.8\n",
75
+ )
76
+
77
+ # Dataset with unsupported image format for the customizer
78
+ cls.unsupported_image_dataset_dir = os.path.join(cls.workspace_root, "unsupported_image_dataset")
79
+ os.makedirs(cls.unsupported_image_dataset_dir, exist_ok=True)
80
+ writer = YoloDataFileWriter(
81
+ data_set_dir=cls.unsupported_image_dataset_dir,
82
+ class_names=["person", "car"],
83
+ train_split="train",
84
+ val_split="valid",
85
+ test_split="test",
86
+ )
87
+ writer.write()
88
+ os.makedirs(os.path.join(cls.unsupported_image_dataset_dir, "train", "images"), exist_ok=True)
89
+ os.makedirs(os.path.join(cls.unsupported_image_dataset_dir, "train", "labels"), exist_ok=True)
90
+ cls._create_dummy_image_file(os.path.join(cls.unsupported_image_dataset_dir, "train", "images", "person_car.gif"))
91
+ cls._write_text_file(
92
+ os.path.join(cls.unsupported_image_dataset_dir, "train", "labels", "person_car.txt"),
93
+ "0 0.1 0.2 0.3 0.4\n1 0.5 0.6 0.7 0.8\n",
94
+ )
95
+
96
+ @classmethod
97
+ def tearDownClass(cls):
98
+ shutil.rmtree(cls.workspace_root, ignore_errors=True)
99
+
100
+ @staticmethod
101
+ def _write_text_file(path: str, content: str) -> None:
102
+ os.makedirs(os.path.dirname(path), exist_ok=True)
103
+ with open(path, "w", encoding="utf-8") as f:
104
+ f.write(content)
105
+
106
+ @staticmethod
107
+ def _create_dummy_image_file(path: str) -> None:
108
+ os.makedirs(os.path.dirname(path), exist_ok=True)
109
+ extension = os.path.splitext(path)[1].lower()
110
+ if extension == ".png":
111
+ header = b"\x89PNG\r\n\x1a\n"
112
+ elif extension in (".jpg", ".jpeg"):
113
+ header = b"\xff\xd8\xff\xe0\x00\x10JFIF\x00"
114
+ elif extension == ".gif":
115
+ header = b"GIF89a"
116
+ else:
117
+ header = b"\x00"
118
+
119
+ with open(path, "wb") as f:
120
+ f.write(header)
121
+
122
+ def test_yolo_data_file_writer_writes_yaml(self):
123
+ yaml_path = os.path.join(self.good_dataset_dir, "data.yaml")
124
+ self.assertTrue(os.path.exists(yaml_path))
125
+ with open(yaml_path, "r", encoding="utf-8") as f:
126
+ loaded = yaml.safe_load(f)
127
+ self.assertEqual(loaded["train"], "train/images")
128
+ self.assertEqual(loaded["val"], "valid/images")
129
+ self.assertEqual(loaded["test"], "test/images")
130
+ self.assertEqual(loaded["names"], {0: "person", 1: "car"})
131
+ self.assertEqual(loaded["nc"], 2)
132
+
133
+ def test_yolo_data_file_reader_reads_splits_names_and_indices(self):
134
+ reader = YoloDataFileReader(os.path.join(self.good_dataset_dir, "data.yaml"))
135
+ self.assertEqual(reader.get_file_path(), os.path.abspath(os.path.join(self.good_dataset_dir, "data.yaml")))
136
+ self.assertEqual(reader.get_split_paths_for_split_name("train"), ["train/images"])
137
+ self.assertEqual(reader.get_split_paths_for_split_name("val"), ["valid/images"])
138
+ self.assertEqual(reader.get_class_names(), ["person", "car"])
139
+ self.assertEqual(reader.get_indices_for_names({"person"}), {"0"})
140
+ self.assertEqual(reader.get_indices_for_names({"unknown"}), set())
141
+
142
+ def test_yolo_data_file_reader_raises_file_not_found(self):
143
+ with self.assertRaises(FileNotFoundError):
144
+ YoloDataFileReader(os.path.join(self.workspace_root, "does_not_exist.yaml"))
145
+
146
+ def test_yolo_data_file_reader_raises_type_error_for_invalid_names_entry(self):
147
+ with self.assertRaises(TypeError):
148
+ YoloDataFileReader(os.path.join(self.invalid_yaml_dataset_dir, "data.yaml"))
149
+
150
+ def test_label_file_parses_class_indices_and_copy_by_class_indices(self):
151
+ label_path = os.path.join(self.good_dataset_dir, "train", "labels", "person_car.txt")
152
+ label_file = LabelFile(label_path)
153
+ self.assertEqual(label_file.get_class_indices(), {"0", "1"})
154
+
155
+ output_labels_dir = os.path.join(self.workspace_root, "copied_labels")
156
+ os.makedirs(os.path.join(output_labels_dir, "labels"), exist_ok=True)
157
+ copied_path = label_file.copy_by_class_indeces({"1": "0"}, output_labels_dir)
158
+ self.assertIsNotNone(copied_path)
159
+ self.assertTrue(os.path.exists(copied_path))
160
+ with open(copied_path, "r", encoding="utf-8") as f:
161
+ content = f.read()
162
+ self.assertIn("0 0.5 0.6 0.7 0.8", content)
163
+ self.assertNotIn("1 0.5 0.6 0.7 0.8", content)
164
+ self.assertNotIn("0.1 0.2 0.3 0.4", content)
165
+
166
+ def test_label_file_copy_by_class_indeces_returns_none_when_missing_indices(self):
167
+ label_path = os.path.join(self.good_dataset_dir, "train", "labels", "person_car.txt")
168
+ label_file = LabelFile(label_path)
169
+ self.assertIsNone(label_file.copy_by_class_indeces({"2": "0"}, os.path.join(self.workspace_root, "labels")))
170
+
171
+ def test_label_file_copy_with_missing_destination_directory_raises(self):
172
+ label_path = os.path.join(self.good_dataset_dir, "train", "labels", "person_car.txt")
173
+ label_file = LabelFile(label_path)
174
+ missing_destination = os.path.join(self.workspace_root, "missing", "labels")
175
+ with self.assertRaises(FileNotFoundError):
176
+ label_file.copy_by_class_indeces({"0": "0"}, missing_destination)
177
+
178
+ def test_yolo_dataset_customizer_discovers_all_data_yaml_files(self):
179
+ customizer = YoloDatasetCustomizer([self.workspace_root])
180
+ found_paths = customizer.get_found_data_file_paths()
181
+ self.assertGreaterEqual(len(found_paths), 3)
182
+ self.assertIn(os.path.abspath(os.path.join(self.good_dataset_dir, "data.yaml")), found_paths)
183
+ self.assertIn(os.path.abspath(os.path.join(self.invalid_yaml_dataset_dir, "data.yaml")), found_paths)
184
+ self.assertIn(os.path.abspath(os.path.join(self.unsupported_image_dataset_dir, "data.yaml")), found_paths)
185
+ self.assertEqual(customizer.get_found_class_names(), {"person", "car"})
186
+
187
+ def test_yolo_dataset_customizer_creates_filtered_dataset(self):
188
+ customizer = YoloDatasetCustomizer([self.good_dataset_dir])
189
+ success = customizer.create_new_dataset_for_class_names({"car"}, dst_path=self.workspace_root, data_set_name="filtered_car_dataset")
190
+ self.assertTrue(success)
191
+
192
+ new_dataset_path = os.path.join(self.workspace_root, "filtered_car_dataset")
193
+ self.assertTrue(os.path.isdir(new_dataset_path))
194
+ new_yaml_path = os.path.join(new_dataset_path, "data.yaml")
195
+ self.assertTrue(os.path.exists(new_yaml_path))
196
+ with open(new_yaml_path, "r", encoding="utf-8") as f:
197
+ new_data = yaml.safe_load(f)
198
+ self.assertEqual(new_data["names"], {0: "car"})
199
+
200
+ new_label_path = os.path.join(new_dataset_path, "train", "labels", "person_car.txt")
201
+ self.assertTrue(os.path.exists(new_label_path))
202
+ with open(new_label_path, "r", encoding="utf-8") as f:
203
+ copied_label = f.read()
204
+ self.assertIn("0 0.5 0.6 0.7 0.8", copied_label)
205
+ self.assertNotIn("1 0.5 0.6 0.7 0.8", copied_label)
206
+ self.assertNotIn("0.1 0.2 0.3 0.4", copied_label)
207
+
208
+ new_image_path = os.path.join(new_dataset_path, "train", "images", "person_car.jpg")
209
+ self.assertTrue(os.path.exists(new_image_path))
210
+
211
+ def test_yolo_dataset_customizer_returns_false_for_empty_class_names(self):
212
+ customizer = YoloDatasetCustomizer([self.good_dataset_dir])
213
+ self.assertFalse(customizer.create_new_dataset_for_class_names(set(), dst_path=self.workspace_root, data_set_name="empty_selection"))
214
+
215
+ def test_yolo_dataset_customizer_returns_false_for_unsupported_image_format(self):
216
+ customizer = YoloDatasetCustomizer([self.unsupported_image_dataset_dir])
217
+ destination_name = "unsupported_image_dataset_filtered"
218
+ success = customizer.create_new_dataset_for_class_names({"person"}, dst_path=self.workspace_root, data_set_name=destination_name)
219
+ self.assertFalse(success)
220
+ self.assertFalse(os.path.exists(os.path.join(self.workspace_root, destination_name, "data.yaml")))
221
+
222
+
223
+ if __name__ == "__main__":
224
+ unittest.main(verbosity=2)
@@ -0,0 +1,224 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.10"
4
+
5
+ [[package]]
6
+ name = "colorama"
7
+ version = "0.4.6"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
10
+ wheels = [
11
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
12
+ ]
13
+
14
+ [[package]]
15
+ name = "exceptiongroup"
16
+ version = "1.3.1"
17
+ source = { registry = "https://pypi.org/simple" }
18
+ dependencies = [
19
+ { name = "typing-extensions" },
20
+ ]
21
+ sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
22
+ wheels = [
23
+ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
24
+ ]
25
+
26
+ [[package]]
27
+ name = "iniconfig"
28
+ version = "2.3.0"
29
+ source = { registry = "https://pypi.org/simple" }
30
+ sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
31
+ wheels = [
32
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
33
+ ]
34
+
35
+ [[package]]
36
+ name = "packaging"
37
+ version = "26.3"
38
+ source = { registry = "https://pypi.org/simple" }
39
+ sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
40
+ wheels = [
41
+ { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
42
+ ]
43
+
44
+ [[package]]
45
+ name = "pluggy"
46
+ version = "1.6.0"
47
+ source = { registry = "https://pypi.org/simple" }
48
+ sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
49
+ wheels = [
50
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
51
+ ]
52
+
53
+ [[package]]
54
+ name = "pygments"
55
+ version = "2.20.0"
56
+ source = { registry = "https://pypi.org/simple" }
57
+ sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
58
+ wheels = [
59
+ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
60
+ ]
61
+
62
+ [[package]]
63
+ name = "pytest"
64
+ version = "9.1.1"
65
+ source = { registry = "https://pypi.org/simple" }
66
+ dependencies = [
67
+ { name = "colorama", marker = "sys_platform == 'win32'" },
68
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
69
+ { name = "iniconfig" },
70
+ { name = "packaging" },
71
+ { name = "pluggy" },
72
+ { name = "pygments" },
73
+ { name = "tomli", marker = "python_full_version < '3.11'" },
74
+ ]
75
+ sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
76
+ wheels = [
77
+ { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
78
+ ]
79
+
80
+ [[package]]
81
+ name = "pyyaml"
82
+ version = "6.0.3"
83
+ source = { registry = "https://pypi.org/simple" }
84
+ sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
85
+ wheels = [
86
+ { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" },
87
+ { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" },
88
+ { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" },
89
+ { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" },
90
+ { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" },
91
+ { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" },
92
+ { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" },
93
+ { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" },
94
+ { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" },
95
+ { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
96
+ { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
97
+ { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
98
+ { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
99
+ { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
100
+ { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
101
+ { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
102
+ { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
103
+ { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
104
+ { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
105
+ { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
106
+ { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
107
+ { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
108
+ { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
109
+ { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
110
+ { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
111
+ { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
112
+ { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
113
+ { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
114
+ { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
115
+ { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
116
+ { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
117
+ { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
118
+ { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
119
+ { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
120
+ { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
121
+ { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
122
+ { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
123
+ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
124
+ { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
125
+ { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
126
+ { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
127
+ { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
128
+ { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
129
+ { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
130
+ { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
131
+ { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
132
+ { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
133
+ { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
134
+ { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
135
+ { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
136
+ { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
137
+ { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
138
+ { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
139
+ { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
140
+ { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
141
+ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
142
+ ]
143
+
144
+ [[package]]
145
+ name = "tomli"
146
+ version = "2.4.1"
147
+ source = { registry = "https://pypi.org/simple" }
148
+ sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" }
149
+ wheels = [
150
+ { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" },
151
+ { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" },
152
+ { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" },
153
+ { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" },
154
+ { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" },
155
+ { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" },
156
+ { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" },
157
+ { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" },
158
+ { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" },
159
+ { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" },
160
+ { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" },
161
+ { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" },
162
+ { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" },
163
+ { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" },
164
+ { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" },
165
+ { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" },
166
+ { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" },
167
+ { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" },
168
+ { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" },
169
+ { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" },
170
+ { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" },
171
+ { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" },
172
+ { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" },
173
+ { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" },
174
+ { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" },
175
+ { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" },
176
+ { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" },
177
+ { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" },
178
+ { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" },
179
+ { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" },
180
+ { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" },
181
+ { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" },
182
+ { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" },
183
+ { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" },
184
+ { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" },
185
+ { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" },
186
+ { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" },
187
+ { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" },
188
+ { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" },
189
+ { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" },
190
+ { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" },
191
+ { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" },
192
+ { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" },
193
+ { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" },
194
+ { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" },
195
+ { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
196
+ ]
197
+
198
+ [[package]]
199
+ name = "typing-extensions"
200
+ version = "4.16.0"
201
+ source = { registry = "https://pypi.org/simple" }
202
+ sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
203
+ wheels = [
204
+ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
205
+ ]
206
+
207
+ [[package]]
208
+ name = "yolodatasetcustomizer"
209
+ version = "0.1.0"
210
+ source = { editable = "." }
211
+ dependencies = [
212
+ { name = "pyyaml" },
213
+ ]
214
+
215
+ [package.dev-dependencies]
216
+ dev = [
217
+ { name = "pytest" },
218
+ ]
219
+
220
+ [package.metadata]
221
+ requires-dist = [{ name = "pyyaml", specifier = ">=6.0.3" }]
222
+
223
+ [package.metadata.requires-dev]
224
+ dev = [{ name = "pytest", specifier = ">=9.1.1" }]