├── .pdd ├── .ecrc ├── tests ├── fixtures │ ├── fake_diff_out.sh │ ├── actual_violations.txt │ ├── test-repo.yaml │ └── diff.patch ├── __init__.py ├── helpers │ ├── __init__.py │ └── define_repo.py ├── conftest.py ├── it │ ├── test_main.py │ └── test_app.py └── unit │ ├── test_entry.py │ └── _internal │ ├── test_define_changed_lines.py │ └── test_filter_out_violations.py ├── renovate.json ├── .editorconfig ├── cspell.json ├── cspell_custom_words.txt ├── LICENSE ├── ondivi ├── _internal │ ├── __init__.py │ ├── ondivi_types.py │ ├── define_changed_lines.py │ └── filter_out_violations.py ├── __init__.py └── entry.py ├── .gotemir.yaml ├── .github └── workflows │ ├── mutation-coverage.yml │ ├── release.yml │ └── pr-check.yml ├── Dockerfile ├── Taskfile.yml ├── setup.cfg ├── pyproject.toml ├── .gitignore ├── CHANGELOG.md ├── README.md └── poetry.lock /.pdd: -------------------------------------------------------------------------------- 1 | --source=. 2 | --exclude=tests/fixtures/violations.txt 3 | -------------------------------------------------------------------------------- /.ecrc: -------------------------------------------------------------------------------- 1 | { 2 | "Exclude": [ 3 | "README.md" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /tests/fixtures/fake_diff_out.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo "Fake diff output" 3 | exit 0 4 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ], 6 | "packageRules": [ 7 | { 8 | "matchUpdateTypes": [ 9 | "minor", 10 | "patch", 11 | "pin", 12 | "digest" 13 | ], 14 | "automerge": true 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Check http://editorconfig.org for more information 2 | # This is the main config file for this project: 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | end_of_line = lf 9 | indent_style = space 10 | insert_final_newline = true 11 | indent_size = 2 12 | 13 | [*.py] 14 | indent_size = 4 15 | 16 | [Makefile] 17 | indent_style = tab 18 | indent_size = 4 19 | -------------------------------------------------------------------------------- /cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2", 3 | "language": "en", 4 | "dictionaryDefinitions": [ 5 | { 6 | "name": "cspell-custom-words", 7 | "path": "./cspell_custom_words.txt", 8 | "addWords": true 9 | } 10 | ], 11 | "dictionaries": [ 12 | "cspell-custom-words", 13 | "python", 14 | "html" 15 | ], 16 | "import": [], 17 | "ignorePaths": [] 18 | } 19 | -------------------------------------------------------------------------------- /cspell_custom_words.txt: -------------------------------------------------------------------------------- 1 | addopts 2 | Almaz 3 | apdisk 4 | Availabe 5 | backreferencing 6 | blablatdinov 7 | booleanfield 8 | celerybeat 9 | Connor 10 | constaint 11 | creatordate 12 | deltaver 13 | dmypy 14 | donotpresent 15 | fexpr 16 | flakeheaven 17 | flakehell 18 | foreignobject 19 | fromfile 20 | fseventsd 21 | gotemir 22 | htmlcov 23 | icns 24 | Ilaletdinov 25 | ipynb 26 | isnull 27 | joinpromoter 28 | klass 29 | legacypath 30 | multivalued 31 | mutmut 32 | netrwhist 33 | nonfield 34 | noprefix 35 | nosetests 36 | ondivi 37 | optimised 38 | outerref 39 | pnorton 40 | prsd 41 | pybuilder 42 | pyflow 43 | pypa 44 | pypackages 45 | pyrightconfig 46 | pytype 47 | queryset 48 | refcounts 49 | ropeproject 50 | Rubics 51 | Rubocop 52 | Sessionx 53 | snok 54 | Spyder 55 | spyderproject 56 | spyproject 57 | staticmethods 58 | styleguide 59 | Taskfile 60 | testpaths 61 | testyear 62 | timemachine 63 | unref 64 | usefixtures 65 | webassets 66 | wemake 67 | wookkl 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024-2025 Almaz Ilaletdinov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # 3 | # Copyright (c) 2024-2025 Almaz Ilaletdinov 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | # OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | """Ondivi tests package.""" 24 | -------------------------------------------------------------------------------- /tests/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # 3 | # Copyright (c) 2024-2025 Almaz Ilaletdinov 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | # OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | """Ondivi test helpers.""" 24 | -------------------------------------------------------------------------------- /ondivi/_internal/__init__.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # 3 | # Copyright (c) 2024-2025 Almaz Ilaletdinov 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | # OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | """Internal logic. 24 | 25 | Protected from import 26 | """ 27 | -------------------------------------------------------------------------------- /ondivi/__init__.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # 3 | # Copyright (c) 2024-2025 Almaz Ilaletdinov 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | # OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | """Ondivi (Only diff violations). 24 | 25 | Python script filtering coding violations, identified by static analysis, 26 | only for changed lines in a Git repo. 27 | """ 28 | -------------------------------------------------------------------------------- /.gotemir.yaml: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # 3 | # Copyright (c) 2024-2025 Almaz Ilaletdinov 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | # OR OTHER DEALINGS IN THE SOFTWARE. 22 | --- 23 | test-free-files: 24 | # __init__ files for define package 25 | - .*__init__.py 26 | # File contain type aliases 27 | - ondivi/_internal/ondivi_types.py 28 | # entry.py test by integration tests 29 | - ondivi/entry.py 30 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # 3 | # Copyright (c) 2024-2025 Almaz Ilaletdinov 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | # OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | """Fixtures.""" 24 | 25 | from pathlib import Path 26 | from typing import Callable 27 | 28 | import pytest 29 | 30 | 31 | @pytest.fixture 32 | def localize_violation_path() -> Callable[[str], str]: 33 | """Localize violation path. 34 | 35 | Violation files contain strings with unix paths, but we need to convert them to windows paths. 36 | """ 37 | def _localize_violation_path(violation: str) -> str: # noqa: WPS430 38 | parts = violation.split(':') 39 | parts[0] = str(Path(parts[0])) 40 | return ':'.join(parts) 41 | return _localize_violation_path 42 | -------------------------------------------------------------------------------- /tests/fixtures/actual_violations.txt: -------------------------------------------------------------------------------- 1 | django/db/models/sql/query.py:1404:9: ANN201 Missing return type annotation for public function `try_transform` 2 | django/db/models/sql/query.py:1404:29: ANN001 Missing type annotation for function argument `lhs` 3 | django/db/models/sql/query.py:1404:34: ANN001 Missing type annotation for function argument `name` 4 | django/db/models/sql/query.py:1404:40: ANN001 Missing type annotation for function argument `lookups` 5 | django/db/models/sql/query.py:1405:9: D205 1 blank line required between summary line and description 6 | django/db/models/sql/query.py:1405:9: D212 [*] Multi-line docstring summary should start at the first line 7 | django/db/models/sql/query.py:1405:9: D401 First line of docstring should be in imperative mood: "Helper method for build_lookup(). Try to fetch and initialize" 8 | django/db/models/sql/query.py:1418:30: UP031 Use format specifiers instead of percent format 9 | django/db/models/sql/query.py:1427:17: UP031 Use format specifiers instead of percent format 10 | django/db/models/sql/query.py:1428:88: COM812 [*] Trailing comma missing 11 | django/db/models/sql/query.py:1431:9: C901 `build_filter` is too complex (20 > 10) 12 | django/db/models/sql/query.py:1431:9: PLR0913 Too many arguments in function definition (10 > 5) 13 | django/db/models/sql/query.py:1431:9: PLR0912 Too many branches (21 > 12) 14 | django/db/models/sql/query.py:1431:9: PLR0915 Too many statements (60 > 50) 15 | django/db/models/sql/query.py:1431:9: ANN201 Missing return type annotation for public function `build_filter` 16 | tests/custom_lookups/tests.py:623:13: PT009 Use a regular `assert` instead of unittest-style `assertEqual` 17 | tests/lookup/tests.py:843:9: ANN201 Missing return type annotation for public function `test_unsupported_lookups_custom_lookups` 18 | tests/lookup/tests.py:843:9: D102 Missing docstring in public method 19 | tests/lookup/tests.py:844:22: SLF001 Private member accessed: `_meta` 20 | tests/lookup/tests.py:853:9: ANN201 Missing return type annotation for public function `test_relation_nested_lookup_error` 21 | tests/lookup/tests.py:853:9: D102 Missing docstring in public method 22 | -------------------------------------------------------------------------------- /tests/it/test_main.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # 3 | # Copyright (c) 2024-2025 Almaz Ilaletdinov 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | # OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | """Tests for ondivi.""" 24 | 25 | from pathlib import Path 26 | from typing import Callable 27 | 28 | from ondivi.entry import controller 29 | 30 | 31 | def test_controller(localize_violation_path: Callable[[str], str]) -> None: 32 | """Testing script output with diff and violations list.""" 33 | got, found = controller( 34 | Path('tests/fixtures/diff.patch').read_text(encoding='utf-8'), 35 | [ 36 | localize_violation_path(line) 37 | for line in Path('tests/fixtures/violations.txt').read_text(encoding='utf-8').splitlines() 38 | ], 39 | '{filename}:{line_num:d}:{col_num:d}: {message}', 40 | only_violations=False, 41 | ) 42 | 43 | assert got == [ 44 | localize_violation_path(line) 45 | for line in Path('tests/fixtures/actual_violations.txt').read_text(encoding='utf-8').splitlines() 46 | ] 47 | assert found 48 | -------------------------------------------------------------------------------- /tests/helpers/define_repo.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # 3 | # Copyright (c) 2024-2025 Almaz Ilaletdinov 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | # OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | """Define repo from spec file.""" 24 | 25 | from pathlib import Path 26 | 27 | import yaml 28 | from git import Repo 29 | 30 | 31 | def define_repo(spec: str, repo_path: Path) -> None: 32 | """Define repo from spec file.""" 33 | repo = Repo.init(repo_path) 34 | changes_definition = yaml.safe_load(Path('tests/fixtures/test-repo.yaml').read_text()) 35 | for change in changes_definition['changes']: 36 | file_path = Path(repo_path / change['path']) 37 | file_path.parents[0].mkdir(exist_ok=True) 38 | Path(repo_path / change['path']).write_text(change['content']) 39 | if change.get('commit'): 40 | repo.index.add([change['path']]) 41 | repo.index.commit(change['commit']['message']) 42 | 43 | 44 | if __name__ == '__main__': 45 | define_repo( 46 | Path('tests/fixtures/test-repo.yaml').read_text(), 47 | Path('ondivi-test-repo'), 48 | ) 49 | -------------------------------------------------------------------------------- /tests/unit/test_entry.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # 3 | # Copyright (c) 2024-2025 Almaz Ilaletdinov 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | # OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | """Tests for ondivi.""" 24 | 25 | from typing import Callable 26 | 27 | from ondivi.entry import controller 28 | 29 | 30 | def test_controller(localize_violation_path: Callable[[str], str]) -> None: 31 | """Testing script output with diff and violations list.""" 32 | got, found = controller( 33 | '\n'.join([ 34 | 'diff --git a/ondivi/__main__.py b/ondivi/__main__.py', 35 | 'index 669d0ff..7a518fa 100644', 36 | '--- a/ondivi/__main__.py', 37 | '+++ b/ondivi/__main__.py', 38 | '@@ -26,0 +27,1 @@ from git import Repo', 39 | '+Diff = str', 40 | ]), 41 | [localize_violation_path('ondivi/__main__.py:27:1: Error message')], 42 | '{filename}:{line_num:d}:{col_num:d}: {message}', 43 | only_violations=False, 44 | ) 45 | 46 | assert got == [localize_violation_path('ondivi/__main__.py:27:1: Error message')] 47 | assert found 48 | -------------------------------------------------------------------------------- /tests/unit/_internal/test_define_changed_lines.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # 3 | # Copyright (c) 2024-2025 Almaz Ilaletdinov 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | # OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | """Tests for ondivi.""" 24 | 25 | from pathlib import Path 26 | from typing import Callable 27 | 28 | from ondivi._internal.define_changed_lines import define_changed_lines # noqa: WPS436. _internal allow into ondivi app 29 | 30 | 31 | def test_define_changed_files(localize_violation_path: Callable[[str], str]) -> None: 32 | """Testing search changed lines.""" 33 | got = define_changed_lines( 34 | Path('tests/fixtures/diff.patch').read_text(encoding='utf-8'), 35 | ) 36 | 37 | assert got == { 38 | localize_violation_path('django/db/models/sql/query.py'): [ 39 | *range(1367, 1374), 40 | *range(1401, 1408), 41 | *range(1418, 1432), 42 | ], 43 | localize_violation_path('tests/custom_lookups/tests.py'): list(range(614, 624)), 44 | localize_violation_path('tests/lookup/tests.py'): [ 45 | *range(812, 846), 46 | *range(853, 860), 47 | *range(1087, 1097), 48 | ], 49 | } 50 | 51 | 52 | def test_define_filename() -> None: 53 | """Test define filename. 54 | 55 | Created for kill mutant 56 | """ 57 | got = define_changed_lines( 58 | 'diff --git XX b/ XX b/ file.py', 59 | ) 60 | 61 | assert got == {'file.py': []} 62 | -------------------------------------------------------------------------------- /.github/workflows/mutation-coverage.yml: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # 3 | # Copyright (c) 2024-2025 Almaz Ilaletdinov 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | # OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | name: Mutation coverage 24 | 25 | on: 26 | schedule: 27 | - cron: '17 13 * * 3' 28 | workflow_dispatch: 29 | 30 | permissions: 31 | contents: read 32 | 33 | jobs: 34 | mutation-coverage: 35 | runs-on: ubuntu-24.04 36 | steps: 37 | - uses: actions/checkout@v6 38 | - name: Set up Python 39 | uses: actions/setup-python@v6 40 | with: 41 | python-version: 3.14 42 | - name: Install Poetry 43 | uses: snok/install-poetry@v1.4.1 44 | with: 45 | virtualenvs-create: true 46 | virtualenvs-in-project: true 47 | installer-parallel: true 48 | - name: Load cached venv 49 | id: cached-poetry-dependencies 50 | uses: actions/cache@v5 51 | with: 52 | path: .venv 53 | key: venv-${{ matrix.python_version }}-${{ hashFiles('**/poetry.lock') }} 54 | - name: Install dependencies 55 | if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' 56 | run: poetry install --no-interaction 57 | - name: Run mutmut 58 | run: | 59 | poetry run mutmut run --CI --no-progress 60 | mutant_ids=$(poetry run mutmut result-ids survived | tr ' ' '\n') 61 | count=1 62 | for id in $mutant_ids 63 | do 64 | printf "############### Mutant %d: ###############\n\n" "$count" 65 | poetry run mutmut show $id 66 | (( count++ )) 67 | done 68 | -------------------------------------------------------------------------------- /tests/fixtures/test-repo.yaml: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # 3 | # Copyright (c) 2024-2025 Almaz Ilaletdinov 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | # OR OTHER DEALINGS IN THE SOFTWARE. 22 | --- 23 | changes: 24 | 25 | - path: inner/file.py 26 | commit: 27 | message: Initial commit 28 | content: | 29 | def greet(name): 30 | print('Long string in initial commit ################################################################################') 31 | print(f'Hello, {name}!') 32 | 33 | if __name__ == '__main__': 34 | greet(input()) 35 | 36 | - path: inner/file.py 37 | commit: 38 | message: Second commit 39 | content: | 40 | from dataclasses import dataclass 41 | 42 | @dataclass 43 | class User(object): 44 | 45 | name: str 46 | age: int 47 | 48 | def greet(user: User): 49 | print('Long string in initial commit ################################################################################') 50 | print(f'Hello, {user.name}!') 51 | 52 | if __name__ == '__main__': 53 | greet(User(345, 23)) 54 | 55 | - path: inner/file.py 56 | content: | 57 | from dataclasses import dataclass 58 | 59 | @dataclass 60 | class User(object): 61 | 62 | name: str 63 | age: int 64 | 65 | def greet(user: User): 66 | print('Long string in initial commit ################################################################################') 67 | print(f'Hello, {user.name}!') 68 | print('Long string in new commit ################################################################################') 69 | 70 | if __name__ == '__main__': 71 | greet(User(345, 23)) 72 | greet(User('Bob', '23')) 73 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # 3 | # Copyright (c) 2024-2025 Almaz Ilaletdinov 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | # OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | FROM python:3.14.2 24 | ENV PIP_DISABLE_PIP_VERSION_CHECK=1 25 | ENV POETRY_VIRTUALENVS_IN_PROJECT=true 26 | ENV EC_VERSION="v3.0.3" 27 | ENV POETRY_NO_INTERACTION=1 28 | ENV PATH="/root/.local/bin:$PATH" 29 | 30 | RUN pip install poetry==2.1.3 31 | RUN sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b /bin 32 | 33 | ENV EC_VERSION="v3.0.3" 34 | RUN curl -O -L -C - https://github.com/editorconfig-checker/editorconfig-checker/releases/download/$EC_VERSION/ec-linux-amd64.tar.gz && \ 35 | tar xzf ec-linux-amd64.tar.gz -C /tmp && \ 36 | mkdir -p /root/.local/bin && \ 37 | mv /tmp/bin/ec-linux-amd64 /root/.local/bin/ec && \ 38 | rm ec-linux-amd64.tar.gz 39 | 40 | WORKDIR /app 41 | 42 | COPY poetry.lock pyproject.toml README.md ./ 43 | 44 | COPY . . 45 | RUN poetry install 46 | 47 | RUN ln -sf /app/.venv/bin/python /usr/local/bin/poetry-python && \ 48 | ln -sf /app/.venv/bin/pip /usr/local/bin/poetry-pip && \ 49 | ln -sf /app/lint-venv/bin/python /usr/local/bin/lint-python && \ 50 | ln -sf /app/lint-venv/bin/pip /usr/local/bin/lint-pip 51 | 52 | ENV VIRTUAL_ENV=/app/.venv 53 | ENV PATH="/app/.venv/bin:$PATH" 54 | ENV LINT_VENV=/app/lint-venv 55 | ENV LINT_PATH="/app/lint-venv/bin:$PATH" 56 | 57 | RUN echo '#!/bin/bash\n\ 58 | export VIRTUAL_ENV=/app/.venv\n\ 59 | export PATH="/app/.venv/bin:$PATH"\n\ 60 | export LINT_VENV=/app/lint-venv\n\ 61 | export LINT_PATH="/app/lint-venv/bin:$PATH"\n\ 62 | exec "$@"' > /usr/local/bin/activate-env && \ 63 | chmod +x /usr/local/bin/activate-env 64 | 65 | RUN useradd --create-home --shell /bin/bash developer && \ 66 | chown -R developer:developer /app 67 | 68 | USER developer 69 | WORKDIR /app 70 | 71 | ENV VIRTUAL_ENV=/app/.venv 72 | ENV PATH="/app/.venv/bin:$PATH" 73 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # 3 | # Copyright (c) 2024-2025 Almaz Ilaletdinov 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | # OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | name: Release 24 | 25 | on: 26 | push: 27 | tags: "*" 28 | 29 | jobs: 30 | tests: 31 | strategy: 32 | matrix: 33 | python_version: ["3.10", "3.14"] 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v6 37 | - name: Set up Python 38 | uses: actions/setup-python@v6 39 | with: 40 | python-version: ${{ matrix.python_version }} 41 | - name: Install Poetry 42 | uses: snok/install-poetry@v1.4.1 43 | with: 44 | virtualenvs-create: true 45 | virtualenvs-in-project: true 46 | installer-parallel: true 47 | - name: Load cached venv 48 | id: cached-poetry-dependencies 49 | uses: actions/cache@v5 50 | with: 51 | path: .venv 52 | key: venv-${{ matrix.python_version }}-${{ hashFiles('**/poetry.lock') }} 53 | - name: Install dependencies 54 | if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' 55 | run: poetry install --no-interaction 56 | - name: Run tests via pytest 57 | run: poetry run pytest --cov=ondivi --cov-report=term-missing:skip-covered -s -vv 58 | 59 | publish-pypi: 60 | needs: tests 61 | runs-on: ubuntu-latest 62 | steps: 63 | - uses: actions/checkout@v6 64 | - run: | 65 | git fetch --tags --force && \ 66 | latest=$(git tag --sort=creatordate | tail -1) && \ 67 | sed -i '/^version = "/s/"[^"]*"/"'"$latest"'"/' pyproject.toml 68 | - name: Build and publish to pypi 69 | uses: JRubics/poetry-publish@v2.1 70 | with: 71 | pypi_token: ${{ secrets.PYPI_API_TOKEN }} 72 | - uses: peter-evans/create-pull-request@v8 73 | with: 74 | branch: version-up 75 | commit-message: 'new version in pyproject.toml' 76 | delete-branch: true 77 | title: 'Up version in pyproject.toml' 78 | assignees: blablatdinov 79 | base: master 80 | -------------------------------------------------------------------------------- /Taskfile.yml: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # 3 | # Copyright (c) 2024-2025 Almaz Ilaletdinov 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | # OR OTHER DEALINGS IN THE SOFTWARE. 22 | --- 23 | version: '3' 24 | 25 | tasks: 26 | 27 | docker-setup: 28 | cmds: 29 | - docker build -t ondivi-dev . 30 | 31 | docker-bash: 32 | cmds: 33 | - docker run -it --rm -v $(pwd):/app --name ondivi-dev ondivi-dev bash 34 | 35 | default: 36 | desc: "Build pipeline" 37 | deps: [install-deps,unit-tests,integration-tests,ec,lint,build] 38 | 39 | build: 40 | desc: "Build the app" 41 | deps: [install-deps] 42 | cmds: 43 | - poetry build 44 | 45 | install-deps: 46 | desc: "Install dependencies" 47 | cmds: 48 | - poetry install 49 | 50 | syntax-check: 51 | cmds: 52 | - poetry run ruff check ondivi tests --select=F --ignore=F401,F841 53 | 54 | type-check: 55 | cmds: 56 | - poetry run mypy ondivi tests --strict 57 | 58 | test: 59 | deps: [syntax-check,type-check] 60 | cmds: 61 | - poetry run pytest {{.CLI_ARGS}} -s -vv 62 | 63 | unit-tests: 64 | desc: "Run unit tests" 65 | cmds: 66 | - poetry run pytest tests/unit 67 | 68 | integration-tests: 69 | desc: "Run integration tests" 70 | cmds: 71 | - poetry run pytest tests/it 72 | 73 | fmt: 74 | desc: "Run formatters" 75 | cmds: 76 | - poetry run ruff check ondivi tests --fix --fix-only 77 | 78 | lint: 79 | desc: "Run linters" 80 | cmds: 81 | - poetry run ruff check ondivi tests 82 | - poetry run flake8 ondivi tests 83 | - poetry run mypy ondivi tests --strict 84 | 85 | cspell-baseline: 86 | desc: "Generate cspell baseline" 87 | cmds: 88 | - git ls-files | xargs cspell --words-only | sort -uf > cspell_custom_words.txt 89 | 90 | ec: 91 | cmds: 92 | - git ls-files | xargs ec 93 | 94 | test-repo: 95 | cmds: 96 | - poetry run python tests/helpers/define_repo.py 97 | 98 | clean: 99 | desc: "Clean caches" 100 | cmds: 101 | - git clean -f -d -x 102 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # 3 | # Copyright (c) 2024-2025 Almaz Ilaletdinov 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | # OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | [flake8] 24 | max-line-length = 120 25 | docstring_style=sphinx 26 | max-arguments = 6 27 | exps-for-one-empty-line = 0 28 | copyright-check = True 29 | exclude = src/db/schema_migration.py 30 | ignore = 31 | # Missing docstring in public module 32 | D100, 33 | # Missing docstring in public package 34 | D104, 35 | # First line should be in imperative mood 36 | D401, 37 | # Found upper-case constant in a class 38 | WPS115, 39 | # Found module with too many imports 40 | WPS201, 41 | # line break before binary operator 42 | W503, 43 | # Found a line that starts with a dot 44 | WPS348, 45 | # Check by ruff PLR0913 46 | WPS211, 47 | # RST 48 | RST, 49 | # Found executable mismatch: file is executable but no shebang is present 50 | WPS453, 51 | 52 | per-file-ignores = 53 | tests/*: 54 | # Missing docstring in public class 55 | D101, 56 | # Missing docstring in public method 57 | D102, 58 | # Missing docstring in public function 59 | D103, 60 | # Missing docstring in magic method 61 | D105, 62 | # Missing docstring in __init__ 63 | D107, 64 | # Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. 65 | S101, 66 | # Found magic number 67 | WPS432, 68 | # Found wrong keyword: pass 69 | WPS420, 70 | # Found incorrect node inside `class` body 71 | WPS604, 72 | # Found outer scope names shadowing: message_update 73 | WPS442, 74 | # Found comparison with float or complex number 75 | WPS459, 76 | # split between test action and assert 77 | WPS473, 78 | # Found compare with falsy constant 79 | WPS520, 80 | # Found string literal over-use 81 | WPS226, 82 | # Found overused expression 83 | WPS204, 84 | # Missing parameter(s) in Docstring 85 | DAR101, 86 | # Not use rst format 87 | RST, 88 | -------------------------------------------------------------------------------- /ondivi/_internal/ondivi_types.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # 3 | # Copyright (c) 2024-2025 Almaz Ilaletdinov 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | # OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | """Ondivi app types.""" 24 | 25 | from typing import TypedDict 26 | 27 | DiffStr = str 28 | # Diff str is out of `git diff` command 29 | # Example: 30 | # +---------------------------------------------------------------------+ 31 | # │ diff --git a/ondivi/__main__.py b/ondivi/__main__.py | 32 | # | index 669d0ff..7a518fa 100644 | 33 | # | --- a/ondivi/__main__.py | 34 | # | +++ b/ondivi/__main__.py | 35 | # | @@ -26,0 +27,2 @@ from git import Repo | 36 | # | +Diff = str | 37 | # | +FileName = str | 38 | # | @@ -28 +30,2 @@ from git import Repo | 39 | # | -def define_changed_lines(diff): | 40 | # | + | 41 | # | +def define_changed_lines(diff: Diff) -> dict[FileName, list[int]]: | 42 | # +---------------------------------------------------------------------+ 43 | 44 | FileNameStr = str 45 | # Filename with path to file 46 | # This name must be equal in git diff and linter out 47 | 48 | ViolationStr = str 49 | # One line of violation output 50 | # Example: 51 | # "src/app_types/listable.py:23:1: UP035 Import from `collections.abc` instead: `Sequence`"" 52 | 53 | LinterAdditionalMessageStr = str 54 | # Some additional messages from checkers which not contain code violation 55 | # For example ruff write to stdout "Found 17 errors." 56 | # This line not needed for ondivi, but we must send it to user in some cases 57 | # See: "--only-violations" option 58 | 59 | ActualViolationsListStr = list[str] 60 | # List of violations filtered by ondivi 61 | 62 | ViolationFormatStr = str 63 | # Template for parsing linter messages. The template should include the following named parts:', 64 | # {filename} The name of the file with the error/warning', 65 | # {line_num} The line number with the error/warning (integer)', 66 | # See "--format" option, https://github.com/r1chardj0n3s/parse 67 | 68 | BaselineStr = str 69 | # Branch name or commit hash 70 | 71 | FromFilePathStr = str 72 | # Path to file with violations in utf-8 encoding 73 | 74 | 75 | class ParsedViolation(TypedDict): 76 | """Parsed violation. 77 | 78 | After parsing parse.parse takes dict type 79 | """ 80 | 81 | filename: FileNameStr 82 | line_num: int 83 | -------------------------------------------------------------------------------- /tests/unit/_internal/test_filter_out_violations.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # 3 | # Copyright (c) 2024-2025 Almaz Ilaletdinov 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | # OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | """Tests for ondivi.""" 24 | 25 | # _internal allow into ondivi app 26 | from ondivi._internal.filter_out_violations import filter_out_violations # noqa: WPS436 27 | 28 | 29 | def test_without_violation() -> None: 30 | """Test filtering without violations.""" 31 | violations, found = filter_out_violations( 32 | {}, 33 | ['All checks passed'], 34 | '{filename}:{line_num:d}:{col_num:d}: {message}', 35 | only_violations=False, 36 | ) 37 | 38 | assert violations == ['All checks passed'] 39 | assert not found 40 | 41 | 42 | def test_custom_format() -> None: 43 | """Test custom violation format.""" 44 | violations, found = filter_out_violations( 45 | {'file.py': [12]}, 46 | ['line=12 file=file.py message=`print` found'], 47 | 'line={line_num:d} file={filename} {other}', 48 | only_violations=False, 49 | ) 50 | 51 | assert violations == ['line=12 file=file.py message=`print` found'] 52 | assert found 53 | 54 | 55 | def test_not_target_violation() -> None: 56 | """Test not target violation.""" 57 | violations, found = filter_out_violations( 58 | {'file.py': [1, 2]}, 59 | ['file.py:3:1: line too long'], 60 | '{filename}:{line_num:d}:{col_num:d}: {message}', 61 | only_violations=False, 62 | ) 63 | 64 | assert not violations 65 | assert not found 66 | 67 | 68 | def test_file_without_diff() -> None: 69 | """Test file without diff.""" 70 | violations, found = filter_out_violations( 71 | {'file.py': [1, 2]}, 72 | ['foo.py:3:1: line too long'], 73 | '{filename}:{line_num:d}:{col_num:d}: {message}', 74 | only_violations=False, 75 | ) 76 | 77 | assert not violations 78 | assert not found 79 | 80 | 81 | def test_only_violations() -> None: 82 | """Test only violations.""" 83 | violations, found = filter_out_violations( 84 | {'file.py': [3]}, 85 | [ 86 | 'file.py:3:1: line too long', 87 | 'Info message', 88 | ], 89 | '{filename}:{line_num:d}:{col_num:d}: {message}', 90 | only_violations=True, 91 | ) 92 | 93 | assert violations == ['file.py:3:1: line too long'] 94 | assert found 95 | 96 | 97 | def test_path_starts_with_dot() -> None: 98 | """Test path starts with dot.""" 99 | violations, found = filter_out_violations( 100 | {'file.py': [3]}, 101 | ['./file.py:3:1: line too long'], 102 | '{filename}:{line_num:d}:{col_num:d}: {message}', 103 | only_violations=False, 104 | ) 105 | 106 | assert violations == ['./file.py:3:1: line too long'] 107 | assert found 108 | -------------------------------------------------------------------------------- /ondivi/_internal/define_changed_lines.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # 3 | # Copyright (c) 2024-2025 Almaz Ilaletdinov 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | # OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | """Define changed lines in file.""" 24 | 25 | from pathlib import Path 26 | 27 | from ondivi._internal.ondivi_types import DiffStr, FileNameStr # noqa: WPS436. _internal allow into ondivi app 28 | 29 | 30 | def define_changed_lines(diff: DiffStr) -> dict[FileNameStr, list[int]]: 31 | """Define changed lines in file. 32 | 33 | Example of diff: 34 | 35 | +---------------------------------------------------------------------+ 36 | │ diff --git a/ondivi/__main__.py b/ondivi/__main__.py | <- filename 37 | | index 669d0ff..7a518fa 100644 | 38 | | --- a/ondivi/__main__.py | 39 | | +++ b/ondivi/__main__.py | 40 | | @@ -26,0 +27,2 @@ from git import Repo | <- Changed lines = [27, 28] 41 | | +Diff = str | 42 | | +FileName = str | 43 | | @@ -28 +30,2 @@ from git import Repo | <- Changed lines = [27, 28, 30, 31] 44 | | -def define_changed_lines(diff): | 45 | | + | 46 | | +def define_changed_lines(diff: Diff) -> dict[FileName, list[int]]: | 47 | +---------------------------------------------------------------------+ 48 | 49 | :param diff: DiffStr 50 | :return: dict[FileNameStr, list[int]] 51 | """ 52 | changed_lines: dict[FileNameStr, list[int]] = {} 53 | current_file = '' 54 | for line in diff.splitlines(): 55 | if _line_contain_filename(line): 56 | current_file = str( 57 | Path(line.split(' b/')[-1].strip()), 58 | ) 59 | changed_lines[current_file] = [] 60 | elif _diff_line_contain_changed_lines(line): 61 | changed_lines[current_file].extend(_changed_lines(line)) 62 | return changed_lines 63 | 64 | 65 | def _line_contain_filename(diff_line: str) -> bool: 66 | return diff_line.startswith('diff --git') 67 | 68 | 69 | def _diff_line_contain_changed_lines(diff_line: str) -> bool: 70 | return diff_line.startswith('@@') 71 | 72 | 73 | def _changed_lines(diff_line: str) -> list[int]: 74 | """Changed lines. 75 | 76 | >>> _changed_lines('@@ -28 +30,2 @@ from git import Repo') 77 | [30, 31] 78 | 79 | :param diff_line: str 80 | :return: list[int] 81 | """ 82 | splitted_line = diff_line.split('@@')[1].strip() 83 | added_lines = splitted_line.split('+')[1] 84 | start_line = int( 85 | added_lines.split(',')[0], 86 | ) 87 | num_lines = 0 88 | if ',' in added_lines: 89 | num_lines = int(added_lines.split(',')[1]) - 1 90 | return list(range( 91 | start_line, start_line + num_lines + 1, 92 | )) 93 | -------------------------------------------------------------------------------- /ondivi/_internal/filter_out_violations.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # 3 | # Copyright (c) 2024-2025 Almaz Ilaletdinov 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | # OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | """Collect target violations.""" 24 | 25 | from __future__ import annotations 26 | 27 | from dataclasses import dataclass 28 | from pathlib import Path 29 | 30 | from parse import parse as parse_from_pattern # type: ignore [import-untyped] 31 | 32 | from ondivi._internal.ondivi_types import ( # noqa: WPS436. _internal allow into ondivi app 33 | ActualViolationsListStr, 34 | FileNameStr, 35 | LinterAdditionalMessageStr, 36 | ParsedViolation, 37 | ViolationFormatStr, 38 | ViolationStr, 39 | ) 40 | 41 | 42 | @dataclass(frozen=True) 43 | class LinterOutLine: 44 | 45 | _raw_line: ViolationStr | LinterAdditionalMessageStr 46 | _violation_format: ViolationFormatStr 47 | 48 | def line_num(self) -> int: 49 | return self._parse()['line_num'] 50 | 51 | def filename(self) -> str: 52 | return str(Path( 53 | self._parse()['filename'] 54 | .replace('./', '') 55 | .replace('\\', '/'), 56 | )) 57 | 58 | def violation_exist(self) -> bool: 59 | return bool(self._parse()) 60 | 61 | def _parse(self) -> ParsedViolation: 62 | prsd: ParsedViolation = parse_from_pattern(self._violation_format, self._raw_line) 63 | return prsd 64 | 65 | 66 | def filter_out_violations( 67 | changed_lines: dict[FileNameStr, list[int]], 68 | linter_out: list[ViolationStr | LinterAdditionalMessageStr], 69 | violation_format: ViolationFormatStr, 70 | only_violations: bool, 71 | ) -> tuple[ActualViolationsListStr, bool]: 72 | """Collect target violations. 73 | 74 | :param changed_lines: dict[FileName, list[int]], violations: list[str] 75 | :param linter_out: list[ViolationStr | LinterAdditionalMessageStr] 76 | :param violation_format: ViolationFormatStr 77 | :param only_violations: bool 78 | :return: tuple[ActualViolationsListStr, bool] 79 | """ 80 | filtered_violations = [] 81 | violation_found = False 82 | for linter_out_line in linter_out: 83 | line_for_out, is_violation = _is_line_for_out( 84 | changed_lines, 85 | LinterOutLine(linter_out_line, violation_format), 86 | ) 87 | violation_found = violation_found or is_violation 88 | if is_violation or (line_for_out and not only_violations): 89 | filtered_violations.append(linter_out_line) 90 | return filtered_violations, violation_found 91 | 92 | 93 | def _is_line_for_out( 94 | changed_lines: dict[FileNameStr, list[int]], 95 | linter_out_line: LinterOutLine, 96 | ) -> tuple[bool, bool]: 97 | line_for_out, is_violation = True, True 98 | if not linter_out_line.violation_exist(): 99 | line_for_out = True 100 | is_violation = False 101 | elif not _is_target_violation(changed_lines, linter_out_line): 102 | line_for_out = False 103 | is_violation = False 104 | return line_for_out, is_violation 105 | 106 | 107 | def _is_target_violation(changed_lines: dict[FileNameStr, list[int]], linter_out_line: LinterOutLine) -> bool: 108 | violation_file = linter_out_line.filename() 109 | is_target_file = violation_file in changed_lines 110 | try: 111 | violation_on_changed_line = linter_out_line.line_num() in changed_lines[violation_file] 112 | except KeyError: 113 | violation_on_changed_line = False 114 | return is_target_file and violation_on_changed_line 115 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # 3 | # Copyright (c) 2024-2025 Almaz Ilaletdinov 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | # OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | [tool.poetry] 24 | version = "0.7.3" 25 | requires-poetry = ">=2.0" 26 | 27 | [project] 28 | name = "ondivi" 29 | description = "" 30 | dynamic = ["version"] 31 | readme = "README.md" 32 | authors = [{name = "Almaz Ilaletdinov", email = "a.ilaletdinov@yandex.ru"}] 33 | requires-python = ">=3.10,<4.0" 34 | dependencies = [ 35 | "gitpython (>=2.1.15)", 36 | "parse (>=1.4,<2.0)", 37 | "click (>=0.2)", 38 | ] 39 | 40 | [project.scripts] 41 | ondivi = "ondivi.entry:main" 42 | 43 | [tool.poetry.group.dev.dependencies] 44 | ruff = "0.14.10" 45 | pytest = "9.0.2" 46 | pytest-cov = "7.0.0" 47 | mypy = "1.19.1" 48 | deltaver = "1.0.0" 49 | mutmut = "2.5.1" 50 | junit-xml = "1.9" # lock for mutmut 51 | pytest-randomly = "4.0.1" 52 | tomli = "2.3.0" 53 | pyyaml = "6.0.3" 54 | types-pyyaml = "6.0.12.20250915" 55 | wemake-python-styleguide = "1.4.0" 56 | 57 | [tool.isort] 58 | line_length = 120 59 | multi_line_output = 3 60 | include_trailing_comma = true 61 | 62 | [tool.ruff.lint] 63 | select = ["ALL"] 64 | fixable = [ 65 | "F401", # Unused import 66 | "I001", # sort imports 67 | "Q000", # quotes 68 | ] 69 | ignore = [ 70 | "ARG001", # Unused function argument 71 | "ARG002", # Unused method argument 72 | "D203", # no-blank-line-before-class 73 | "D213", # multi-line-summary-first-line 74 | "D401", # First line of docstring should be in imperative mood 75 | "D418", # Function decorated with `@overload` shouldn't contain a docstring 76 | "FBT001", # Boolean-typed positional argument in function definition 77 | "FBT002", # Boolean-typed positional argument in function definition 78 | "FIX002", # Line contains T0DO, consider resolving the issue 79 | "FLY002", # We not use f-strings 80 | "RUF100", # WPS primary linter 81 | "RUF001", # Project contain cyrillic symbols 82 | "RUF002", # Project contain cyrillic symbols 83 | "RET505", # Unnecessary `elif` after `return` statement 84 | "RET506", # Unnecessary `elif` after `raise` statement 85 | "UP030", # We use explicit references 86 | "UP032", # We not use f-strings 87 | "UP004", # Class `PrayerReaction` inherits from `object` 88 | "TD", # "t0do" formats 89 | "PLR630", # We disrespect staticmethods 90 | "TC003", # Move standard library import `...` into a type-checking block 91 | "TC001", # Move application import into a type-checking block 92 | ] 93 | 94 | [tool.ruff] 95 | line-length = 120 96 | target-version = "py39" 97 | 98 | [tool.ruff.lint.flake8-quotes] 99 | docstring-quotes = "double" 100 | inline-quotes = "single" 101 | multiline-quotes = "double" 102 | 103 | [tool.ruff.lint.per-file-ignores] 104 | "tests/*" = [ 105 | "S101", # use of `assert` detected 106 | "PLR2004", # Magic value 107 | "PLR0913", # Too many arguments to function call 108 | "INP001", # Add an `__init__.py`. Tests is closed to import 109 | ] 110 | 111 | [build-system] 112 | requires = ["poetry-core (>=2.0)"] 113 | build-backend = "poetry.core.masonry.api" 114 | 115 | [tool.pytest.ini_options] 116 | testpaths = [ 117 | "tests/*", 118 | ] 119 | 120 | [tool.deltaver] 121 | fail_on_avg = 50 122 | fail_on_max = 360 123 | excluded = [ 124 | "mutmut", 125 | "smmap", # https://github.com/gitpython-developers/gitdb 126 | # gitpython >=2.1.15 127 | # └── gitdb >=4.0.1,<5 128 | # └── smmap >=3.0.1,<6 129 | "junit-xml", 130 | # mutmut 3.1.0 mutation testing for Python 3 131 | # └── junit-xml 1.8 132 | ] 133 | 134 | [tool.mutmut] 135 | paths_to_mutate = "ondivi" 136 | runner = "pytest tests/unit -x -q --tb=no -o addopts=''" 137 | tests_dir = "tests" 138 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/python,vim,macos 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=python,vim,macos 3 | 4 | ### macOS ### 5 | # General 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # Icon must end with two \r 11 | Icon 12 | 13 | 14 | # Thumbnails 15 | ._* 16 | 17 | # Files that might appear in the root of a volume 18 | .DocumentRevisions-V100 19 | .fseventsd 20 | .Spotlight-V100 21 | .TemporaryItems 22 | .Trashes 23 | .VolumeIcon.icns 24 | .com.apple.timemachine.donotpresent 25 | 26 | # Directories potentially created on remote AFP share 27 | .AppleDB 28 | .AppleDesktop 29 | Network Trash Folder 30 | Temporary Items 31 | .apdisk 32 | 33 | ### macOS Patch ### 34 | # iCloud generated files 35 | *.icloud 36 | 37 | ### Python ### 38 | # Byte-compiled / optimized / DLL files 39 | __pycache__/ 40 | *.py[cod] 41 | *$py.class 42 | 43 | # C extensions 44 | *.so 45 | 46 | # Distribution / packaging 47 | .Python 48 | build/ 49 | develop-eggs/ 50 | dist/ 51 | downloads/ 52 | eggs/ 53 | .eggs/ 54 | lib/ 55 | lib64/ 56 | parts/ 57 | sdist/ 58 | var/ 59 | wheels/ 60 | share/python-wheels/ 61 | *.egg-info/ 62 | .installed.cfg 63 | *.egg 64 | MANIFEST 65 | 66 | # PyInstaller 67 | # Usually these files are written by a python script from a template 68 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 69 | *.manifest 70 | *.spec 71 | 72 | # Installer logs 73 | pip-log.txt 74 | pip-delete-this-directory.txt 75 | 76 | # Unit test / coverage reports 77 | htmlcov/ 78 | .tox/ 79 | .nox/ 80 | .coverage 81 | .coverage.* 82 | .cache 83 | nosetests.xml 84 | coverage.xml 85 | *.cover 86 | *.py,cover 87 | .hypothesis/ 88 | .pytest_cache/ 89 | cover/ 90 | 91 | # Translations 92 | *.mo 93 | *.pot 94 | 95 | # Django stuff: 96 | *.log 97 | local_settings.py 98 | db.sqlite3 99 | db.sqlite3-journal 100 | 101 | # Flask stuff: 102 | instance/ 103 | .webassets-cache 104 | 105 | # Scrapy stuff: 106 | .scrapy 107 | 108 | # Sphinx documentation 109 | docs/_build/ 110 | 111 | # PyBuilder 112 | .pybuilder/ 113 | target/ 114 | 115 | # Jupyter Notebook 116 | .ipynb_checkpoints 117 | 118 | # IPython 119 | profile_default/ 120 | ipython_config.py 121 | 122 | # pyenv 123 | # For a library or package, you might want to ignore these files since the code is 124 | # intended to run in multiple environments; otherwise, check them in: 125 | # .python-version 126 | 127 | # pipenv 128 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 129 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 130 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 131 | # install all needed dependencies. 132 | #Pipfile.lock 133 | 134 | # poetry 135 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 136 | # This is especially recommended for binary packages to ensure reproducibility, and is more 137 | # commonly ignored for libraries. 138 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 139 | #poetry.lock 140 | 141 | # pdm 142 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 143 | #pdm.lock 144 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 145 | # in version control. 146 | # https://pdm.fming.dev/#use-with-ide 147 | .pdm.toml 148 | 149 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 150 | __pypackages__/ 151 | 152 | # Celery stuff 153 | celerybeat-schedule 154 | celerybeat.pid 155 | 156 | # SageMath parsed files 157 | *.sage.py 158 | 159 | # Environments 160 | .env 161 | .venv 162 | env/ 163 | venv/ 164 | ENV/ 165 | env.bak/ 166 | venv.bak/ 167 | 168 | # Spyder project settings 169 | .spyderproject 170 | .spyproject 171 | 172 | # Rope project settings 173 | .ropeproject 174 | 175 | # mkdocs documentation 176 | /site 177 | 178 | # mypy 179 | .mypy_cache/ 180 | .dmypy.json 181 | dmypy.json 182 | 183 | # Pyre type checker 184 | .pyre/ 185 | 186 | # pytype static type analyzer 187 | .pytype/ 188 | 189 | # Cython debug symbols 190 | cython_debug/ 191 | 192 | # PyCharm 193 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 194 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 195 | # and can be added to the global gitignore or merged into this file. For a more nuclear 196 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 197 | .idea/ 198 | 199 | ### Python Patch ### 200 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration 201 | poetry.toml 202 | 203 | # ruff 204 | .ruff_cache/ 205 | 206 | # LSP config files 207 | pyrightconfig.json 208 | 209 | ### Vim ### 210 | # Swap 211 | [._]*.s[a-v][a-z] 212 | !*.svg # comment out if you don't need vector files 213 | [._]*.sw[a-p] 214 | [._]s[a-rt-v][a-z] 215 | [._]ss[a-gi-z] 216 | [._]sw[a-p] 217 | 218 | # Session 219 | Session.vim 220 | Sessionx.vim 221 | 222 | # Temporary 223 | .netrwhist 224 | *~ 225 | # Auto-generated tag files 226 | tags 227 | # Persistent undo 228 | [._]*.un~ 229 | 230 | # End of https://www.toptal.com/developers/gitignore/api/python,vim,macos 231 | 232 | .deltaver_cache 233 | 234 | .mutmut-cache 235 | 236 | ondivi-test-repo 237 | 238 | lint-venv 239 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 24 | # Changelog 25 | 26 | All notable changes to this project will be documented in this file. 27 | 28 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 29 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 30 | 31 | ## [Unreleased] 32 | 33 | ## [0.7.3] - 2025-12-19 34 | 35 | ### Fixed 36 | 37 | - Ensure ondivi works correctly when users have diff.mnemonicPrefix or diff.noprefix set in their Git config. (#294) 38 | 39 | ## [0.7.2] - 2025-05-12 40 | 41 | ### Fixed 42 | 43 | - Mixed windows path handling (#199) 44 | 45 | ## [0.7.1] - 2025-05-10 46 | 47 | ### Fixed 48 | 49 | - Remove new line without violation case (#179) 50 | - Fix issues when user has custom diff tool installed (#203) 51 | 52 | ## [0.7.0] - 2025-02-18 53 | 54 | ### Added 55 | 56 | - `Dockerfile` for development (#145) 57 | - Windows support (#173) 58 | 59 | ### Changed 60 | 61 | - Replace `Makefile` -> `Taskfile.yml` (#137) 62 | - Update license year to 2025 (#143) 63 | - Separate lint requirements, use ruff (#143) 64 | - Updated dev dependencies 65 | - Replace zip archive for integration test to create repo from `yaml` file (#174) 66 | 67 | ### Fixed 68 | 69 | - Bug with filtering violations starts with "./" (#175) 70 | - Tests for windows (#176) 71 | 72 | ### Removed 73 | 74 | - isort 75 | 76 | ## [0.6.0] - 2024-10-05 77 | 78 | ### Added 79 | 80 | - Reading violations from file. `--fromfile` flag (#109) 81 | - Handle "revision not found" (#125) 82 | 83 | ### Changed 84 | 85 | - `--help` output (#43) 86 | 87 | ## [0.5.0] - 2024-06-21 88 | 89 | ### Added 90 | 91 | - Lint via pylint (#39) 92 | - Handle exception (#42) 93 | - `click` for CLI (#45) 94 | - Show only violations in output. `--only-violations` flag (#48) 95 | 96 | ## [0.4.1] - 2024-06-14 97 | 98 | ### Changed 99 | 100 | - Availabe python version (#36) 101 | 102 | ### Fixed 103 | 104 | - Test custom violation format (#34) 105 | 106 | ## [0.4.0] - 2024-06-13 107 | 108 | ### Added 109 | 110 | - Test `mypy` violations 111 | - Tests for `python3.13` 112 | - `--format` cli parameter for custom error format 113 | 114 | ## [0.3.1] - 2024-05-28 115 | 116 | ### Added 117 | 118 | - Test exit code without violations (#19) 119 | 120 | ### Fixed 121 | 122 | - Out with info messages (#20) 123 | 124 | ## [0.3.0] - 2024-05-28 125 | 126 | ### Added 127 | 128 | - Test for `ruff` violations 129 | 130 | ### Changed 131 | 132 | - Extend `gitpython` version (#14) 133 | - Simplify script (#15) 134 | - Fail on exist violations (#17) 135 | 136 | ## [0.2.1] - 2024-05-27 137 | 138 | ### Fixed 139 | 140 | - Release workflow 141 | 142 | ## [0.2.0] - 2024-05-27 143 | 144 | ### Added 145 | 146 | - Deltaver (#9) 147 | - Mutation coverage workflow (#10) 148 | - `baseline` cli parameter (#11) 149 | 150 | ## [0.1.0] - 2024-05-16 151 | 152 | ### Added 153 | 154 | - Renovate (#1) 155 | 156 | ### Changed 157 | 158 | - Lint via ruff, mypy (#4) 159 | 160 | ## [0.0.1a6] - 2024-05-16 161 | 162 | ## [0.0.1a5] - 2024-05-16 163 | 164 | ### Removed 165 | 166 | - Old test 167 | 168 | ## [0.0.1a4] - 2024-05-16 169 | 170 | ### Fixed 171 | 172 | - Tests path 173 | 174 | ## [0.0.1a3] - 2024-05-16 175 | 176 | ### Added 177 | 178 | - Initial version 179 | 180 | [unreleased]: https://github.com/blablatdinov/ondivi/compare/0.7.3...HEAD 181 | [0.7.3]: https://github.com/blablatdinov/ondivi/compare/0.7.2...0.7.3 182 | [0.7.2]: https://github.com/blablatdinov/ondivi/compare/0.7.1...0.7.2 183 | [0.7.1]: https://github.com/blablatdinov/ondivi/compare/0.7.0...0.7.1 184 | [0.7.0]: https://github.com/blablatdinov/ondivi/compare/0.6.0...0.7.0 185 | [0.6.0]: https://github.com/blablatdinov/ondivi/compare/0.5.0...0.6.0 186 | [0.5.0]: https://github.com/blablatdinov/ondivi/compare/0.4.1...0.5.0 187 | [0.4.1]: https://github.com/blablatdinov/ondivi/compare/0.4.0...0.4.1 188 | [0.4.0]: https://github.com/blablatdinov/ondivi/compare/0.3.1...0.4.0 189 | [0.3.1]: https://github.com/blablatdinov/ondivi/compare/0.3.0...0.3.1 190 | [0.3.0]: https://github.com/blablatdinov/ondivi/compare/0.2.1...0.3.0 191 | [0.2.1]: https://github.com/blablatdinov/ondivi/compare/0.2.0...0.2.1 192 | [0.2.0]: https://github.com/blablatdinov/ondivi/compare/0.1.0...0.2.0 193 | [0.1.0]: https://github.com/blablatdinov/ondivi/compare/0.0.1a6...0.1.0 194 | [0.0.1a6]: https://github.com/blablatdinov/ondivi/compare/0.0.1a5...0.0.1a6 195 | [0.0.1a5]: https://github.com/blablatdinov/ondivi/compare/0.0.1a4...0.0.1a5 196 | [0.0.1a4]: https://github.com/blablatdinov/ondivi/compare/0.0.1a3...0.0.1a4 197 | [0.0.1a3]: https://github.com/blablatdinov/ondivi/releases/tag/0.0.1a3 198 | -------------------------------------------------------------------------------- /tests/fixtures/diff.patch: -------------------------------------------------------------------------------- 1 | From 759abc4dafef6860edb830c30754e6364ed9ea11 Mon Sep 17 00:00:00 2001 2 | From: wookkl 3 | Date: Sun, 7 Jul 2024 16:17:58 +0900 4 | Subject: [PATCH] Fixed #35413 -- Made unsupported lookup error message more 5 | specific. 6 | 7 | --- 8 | django/db/models/sql/query.py | 11 ++++++++--- 9 | tests/custom_lookups/tests.py | 4 ++++ 10 | tests/lookup/tests.py | 34 +++++++++++++++++++++++++++++++++- 11 | 3 files changed, 45 insertions(+), 4 deletions(-) 12 | 13 | diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py 14 | index 438bb5ddbd76..f00eb1e5a5ec 100644 15 | --- a/django/db/models/sql/query.py 16 | +++ b/django/db/models/sql/query.py 17 | @@ -1367,7 +1367,7 @@ def build_lookup(self, lookups, lhs, rhs): 18 | # __exact is the default lookup if one isn't given. 19 | *transforms, lookup_name = lookups or ["exact"] 20 | for name in transforms: 21 | - lhs = self.try_transform(lhs, name) 22 | + lhs = self.try_transform(lhs, name, lookups) 23 | # First try get_lookup() so that the lookup takes precedence if the lhs 24 | # supports both transform and lookup for the name. 25 | lookup_class = lhs.get_lookup(lookup_name) 26 | @@ -1401,7 +1401,7 @@ def build_lookup(self, lookups, lhs, rhs): 27 | 28 | return lookup 29 | 30 | - def try_transform(self, lhs, name): 31 | + def try_transform(self, lhs, name, lookups=None): 32 | """ 33 | Helper method for build_lookup(). Try to fetch and initialize 34 | a transform for name parameter from lhs. 35 | @@ -1418,9 +1418,14 @@ def try_transform(self, lhs, name): 36 | suggestion = ", perhaps you meant %s?" % " or ".join(suggested_lookups) 37 | else: 38 | suggestion = "." 39 | + if lookups is not None: 40 | + name_index = lookups.index(name) 41 | + unsupported_lookup = LOOKUP_SEP.join(lookups[name_index:]) 42 | + else: 43 | + unsupported_lookup = name 44 | raise FieldError( 45 | "Unsupported lookup '%s' for %s or join on the field not " 46 | - "permitted%s" % (name, output_field.__name__, suggestion) 47 | + "permitted%s" % (unsupported_lookup, output_field.__name__, suggestion) 48 | ) 49 | 50 | def build_filter( 51 | diff --git a/tests/custom_lookups/tests.py b/tests/custom_lookups/tests.py 52 | index f107c5320a2a..2f4ea0a9a024 100644 53 | --- a/tests/custom_lookups/tests.py 54 | +++ b/tests/custom_lookups/tests.py 55 | @@ -614,6 +614,10 @@ def test_call_order(self): 56 | ) 57 | TrackCallsYearTransform.call_order = [] 58 | # junk transform - tries transform only, then fails 59 | + msg = ( 60 | + "Unsupported lookup 'junk__more_junk' for IntegerField or join" 61 | + " on the field not permitted." 62 | + ) 63 | with self.assertRaisesMessage(FieldError, msg): 64 | Author.objects.filter(birthdate__testyear__junk__more_junk=2012) 65 | self.assertEqual(TrackCallsYearTransform.call_order, ["transform"]) 66 | diff --git a/tests/lookup/tests.py b/tests/lookup/tests.py 67 | index ebdaa21e3d31..28acd72874d1 100644 68 | --- a/tests/lookup/tests.py 69 | +++ b/tests/lookup/tests.py 70 | @@ -812,6 +812,34 @@ def test_unsupported_lookups(self): 71 | ): 72 | Article.objects.filter(pub_date__gobbledygook="blahblah") 73 | 74 | + with self.assertRaisesMessage( 75 | + FieldError, 76 | + "Unsupported lookup 'gt__foo' for DateTimeField or join on the field " 77 | + "not permitted, perhaps you meant gt or gte?", 78 | + ): 79 | + Article.objects.filter(pub_date__gt__foo="blahblah") 80 | + 81 | + with self.assertRaisesMessage( 82 | + FieldError, 83 | + "Unsupported lookup 'gt__' for DateTimeField or join on the field " 84 | + "not permitted, perhaps you meant gt or gte?", 85 | + ): 86 | + Article.objects.filter(pub_date__gt__="blahblah") 87 | + 88 | + with self.assertRaisesMessage( 89 | + FieldError, 90 | + "Unsupported lookup 'gt__lt' for DateTimeField or join on the field " 91 | + "not permitted, perhaps you meant gt or gte?", 92 | + ): 93 | + Article.objects.filter(pub_date__gt__lt="blahblah") 94 | + 95 | + with self.assertRaisesMessage( 96 | + FieldError, 97 | + "Unsupported lookup 'gt__lt__foo' for DateTimeField or join" 98 | + " on the field not permitted, perhaps you meant gt or gte?", 99 | + ): 100 | + Article.objects.filter(pub_date__gt__lt__foo="blahblah") 101 | + 102 | def test_unsupported_lookups_custom_lookups(self): 103 | slug_field = Article._meta.get_field("slug") 104 | msg = ( 105 | @@ -825,7 +853,7 @@ def test_unsupported_lookups_custom_lookups(self): 106 | def test_relation_nested_lookup_error(self): 107 | # An invalid nested lookup on a related field raises a useful error. 108 | msg = ( 109 | - "Unsupported lookup 'editor' for ForeignKey or join on the field not " 110 | + "Unsupported lookup 'editor__name' for ForeignKey or join on the field not " 111 | "permitted." 112 | ) 113 | with self.assertRaisesMessage(FieldError, msg): 114 | @@ -1059,6 +1087,10 @@ def test_nonfield_lookups(self): 115 | ) 116 | with self.assertRaisesMessage(FieldError, msg): 117 | Article.objects.filter(headline__blahblah=99) 118 | + msg = ( 119 | + "Unsupported lookup 'blahblah__exact' for CharField or join " 120 | + "on the field not permitted." 121 | + ) 122 | with self.assertRaisesMessage(FieldError, msg): 123 | Article.objects.filter(headline__blahblah__exact=99) 124 | msg = ( 125 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ondivi (Only diff violations) 2 | 3 | [![wemake-python-styleguide](https://img.shields.io/badge/style-wemake-000000.svg)](https://github.com/wemake-services/wemake-python-styleguide) 4 | [![PyPI version](https://badge.fury.io/py/ondivi.svg)](https://badge.fury.io/py/ondivi) 5 | ![CI status](https://github.com/blablatdinov/ondivi/actions/workflows/pr-check.yml/badge.svg?branch=master) 6 | [![Lines of code](https://tokei.rs/b1/github/blablatdinov/ondivi)](https://github.com/XAMPPRocky/tokei_rs) 7 | [![Hits-of-Code](https://hitsofcode.com/github/blablatdinov/ondivi)](https://hitsofcode.com/github/blablatdinov/quranbot-aiogram/view) 8 | 9 | Tired of 10,000 lint errors blocking your team from adopting code quality tools? 10 | Ondivi lets you enforce coding standards ONLY on new changes, 11 | making linter adoption possible in any legacy project. 12 | 13 | This tool works with any linter or static code analyzer, including but not limited to: 14 | 15 | - [Flake8](https://github.com/PyCQA/flake8) 16 | - [Ruff](https://github.com/astral-sh/ruff) 17 | - [Pylint](https://github.com/pylint-dev/pylint) 18 | - [Mypy](https://github.com/python/mypy) 19 | - [Eslint](https://github.com/eslint/eslint) 20 | - [Rubocop](https://github.com/rubocop/rubocop) 21 | - [Stylelint](https://github.com/stylelint/stylelint) 22 | 23 | ## Adopting Linters in Legacy Code 24 | 25 | **The Problem**: 26 | - Your 200K LOC project has 5,000+ lint violations 27 | - Enforcing linters would block all development 28 | - Technical debt keeps accumulating 29 | 30 | **The Solution with Ondivi**: 31 | 1. Run your linter as usual: `flake8 .` 32 | 2. Pipe to ondivi: `flake8 . | ondivi --baseline=main` 33 | 3. CI fails ONLY if new changes introduce violations 34 | 4. Old violations are ignored (for now) 35 | 36 | **Result**: Clean new code, legacy gradually refactored. 37 | 38 | ## Prerequisites: 39 | 40 | - [Python](https://python.org) 3.9 or higher 41 | - [Git](https://git-scm.com/) 42 | 43 | ## Installation 44 | 45 | ```bash 46 | pip install ondivi 47 | ``` 48 | 49 | ## Usage 50 | 51 | Ensure you are in the root directory of your Git repository. 52 | 53 | Run the script: 54 | 55 | ```bash 56 | flake8 script.py | ondivi 57 | # with ruff: 58 | ruff check file.py --output-format=concise | ondivi 59 | ``` 60 | 61 | or: 62 | 63 | ```bash 64 | flake8 script.py > violations.txt 65 | ondivi --fromfile=violations.txt 66 | ``` 67 | 68 | ``` 69 | $ ondivi --help 70 | Usage: ondivi [OPTIONS] 71 | 72 | Ondivi (Only diff violations). 73 | 74 | Python script filtering coding violations, identified by static analysis, 75 | only for changed lines in a Git repo. Usage example: 76 | 77 | flake8 script.py | ondivi 78 | 79 | Options: 80 | --baseline TEXT Commit or branch which will contain legacy code. Program 81 | filter out violations on baseline (default: "master") 82 | --fromfile TEXT Path to file with violations. Expected "utf-8" encoding 83 | --format TEXT Template for parsing linter messages. The template should 84 | include the following named parts: 85 | 86 | {filename} The name of the file with the error/warning 87 | {line_num} The line number with the error/warning 88 | (integer) 89 | 90 | Example usage: 91 | 92 | --format "{filename}:{line_num:d}{other}" 93 | 94 | In this example, the linter message 95 | 96 | "src/app_types/listable.py:23:1: UP035 Import from 97 | collections.abc instead: Sequence" 98 | 99 | will be recognized and parsed into the following 100 | components: 101 | 102 | - filename: "src/app_types/listable.py" 103 | - line_num: 23 104 | - other: :1: "UP035 Import from collections.abc instead: 105 | Sequence" 106 | 107 | Ensure that the template matches the format of the 108 | messages generated by your linter. 109 | (default: "{filename}:{line_num:d}{other}") 110 | --only-violations Show only violations 111 | --help Show this message and exit. 112 | ``` 113 | 114 | ## How it works 115 | 116 | The script parses the Git diff output to identify the changed lines in each file. 117 | 118 | It then filters the given coding violations to include only those violations that correspond to the changed lines. 119 | 120 | [flakeheaven](https://github.com/flakeheaven/flakeheaven) and [flakehell](https://github.com/flakehell/flakehell) 121 | are not supported because they rely on internal flake8 API, which can lead to compatibility issues as flake8 122 | evolves. In contrast, ondivi uses only the text output of violations and the state of Git repository, making 123 | it more robust and easier to maintain. 124 | 125 | Flake8 on file: 126 | 127 | ```bash 128 | $ flake8 file.py 129 | file.py:3:1: E302 expected 2 blank lines, found 1 130 | file.py:9:1: E302 expected 2 blank lines, found 1 131 | file.py:10:121: E501 line too long (123 > 120 characters) 132 | file.py:14:1: E305 expected 2 blank lines after class or function definition, found 1 133 | ``` 134 | 135 | Example of changes: 136 | 137 | ```diff 138 | from dataclasses import dataclass 139 | 140 | @dataclass 141 | class User(object): 142 | 143 | name: str 144 | age: int 145 | 146 | def greet(user: User): 147 | print('Long string in initial commit ################################################################################') 148 | print(f'Hello, {user.name}!') 149 | + print('Long string in new commit ################################################################################') 150 | 151 | if __name__ == '__main__': 152 | greet(User(345, 23)) 153 | + greet(User('Bob', '23')) 154 | ``` 155 | 156 | By git diff we see, that two new lines were appended (12 and 16): 157 | 158 | Ondivi filters out violations and shows only one for line 12: 159 | 160 | ```bash 161 | $ flake8 script.py | ondivi 162 | file.py:12:80: E501 line too long (119 > 79 characters) 163 | ``` 164 | 165 | ## License 166 | 167 | This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for details. 168 | -------------------------------------------------------------------------------- /.github/workflows/pr-check.yml: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # 3 | # Copyright (c) 2024-2025 Almaz Ilaletdinov 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | # OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | name: CI 24 | 25 | on: 26 | pull_request: 27 | branches: [ "master" ] 28 | push: 29 | branches: [ "master" ] 30 | 31 | permissions: read-all 32 | 33 | jobs: 34 | 35 | tests: 36 | strategy: 37 | matrix: 38 | # https://github.com/actions/python-versions/blob/main/versions-manifest.json 39 | python_version: 40 | - "3.10" 41 | - "3.14" 42 | # - pypy3.10 43 | runs-on: ubuntu-latest 44 | steps: 45 | - uses: actions/checkout@v6 46 | - name: Set up Python 47 | uses: actions/setup-python@v6 48 | with: 49 | python-version: ${{ matrix.python_version }} 50 | - name: Install Poetry 51 | uses: snok/install-poetry@v1.4.1 52 | with: 53 | virtualenvs-create: true 54 | virtualenvs-in-project: true 55 | installer-parallel: true 56 | - name: Install dependencies 57 | if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' 58 | run: poetry install --no-interaction 59 | - name: Run tests via pytest 60 | run: poetry run pytest --cov=ondivi --cov-report=term-missing:skip-covered -s -vv --cov-fail-under=100 61 | 62 | dependency-relevance: 63 | runs-on: ubuntu-latest 64 | steps: 65 | - uses: actions/checkout@v6 66 | - name: Set up Python 67 | uses: actions/setup-python@v6 68 | with: 69 | python-version: 3.14 70 | - name: Install Poetry 71 | uses: snok/install-poetry@v1.4.1 72 | with: 73 | virtualenvs-create: true 74 | virtualenvs-in-project: true 75 | installer-parallel: true 76 | - name: Install dependencies 77 | if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' 78 | run: poetry install --no-interaction 79 | - name: Check dependencies relevance 80 | run: poetry run deltaver poetry.lock --format poetry-lock 81 | 82 | win-test: 83 | runs-on: windows-latest 84 | steps: 85 | - name: Checkout repository 86 | uses: actions/checkout@v6 87 | - name: Set up Python 88 | uses: actions/setup-python@v6 89 | with: 90 | python-version: "3.14" 91 | - name: Install Poetry 92 | run: | 93 | pip install poetry 94 | - name: Configure Poetry 95 | run: | 96 | poetry config virtualenvs.in-project true 97 | - name: Install dependencies 98 | run: | 99 | poetry install --no-interaction --no-root 100 | - name: Run tests 101 | run: | 102 | poetry run pytest 103 | 104 | lint: 105 | runs-on: ubuntu-latest 106 | steps: 107 | - uses: actions/checkout@v6 108 | - name: Set up Python 109 | uses: actions/setup-python@v6 110 | with: 111 | python-version: "3.14" 112 | - name: Install Poetry 113 | uses: snok/install-poetry@v1.4.1 114 | with: 115 | virtualenvs-create: true 116 | virtualenvs-in-project: true 117 | installer-parallel: true 118 | - name: Install dependencies 119 | if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' 120 | run: poetry install --no-interaction 121 | - name: Setup go-task 122 | uses: pnorton5432/setup-task@v1 123 | with: 124 | task-version: 3.29.1 125 | - name: Lint 126 | run: task lint 127 | - name: Checking files for compliance with editorconfig 128 | run: | 129 | VERSION="v3.0.3" 130 | curl -O -L -C - https://github.com/editorconfig-checker/editorconfig-checker/releases/download/$VERSION/ec-linux-amd64.tar.gz 131 | tar xzf ec-linux-amd64.tar.gz 132 | git ls-files | xargs bin/ec-linux-amd64 -v 133 | 134 | check-duplicates: 135 | runs-on: ubuntu-24.04 136 | name: Check duplicated code 137 | steps: 138 | - name: Checkout 139 | uses: actions/checkout@v6 140 | - uses: actions/setup-node@v6 141 | - name: Install jscpd 142 | run: npm i jscpd@4.0.5 -g 143 | - name: Run jscpd 144 | run: jscpd -p '**/*.py' -t 0 145 | 146 | check-spelling: 147 | runs-on: ubuntu-24.04 148 | name: Check spelling 149 | steps: 150 | - name: Checkout 151 | uses: actions/checkout@v6 152 | - uses: actions/setup-node@v6 153 | - name: Install cspell 154 | run: npm i cspell@8.15.4 @cspell/dict-ru_ru@2.2.4 -g 155 | - name: Run cspell 156 | run: cspell . no-progress --show-suggestions --show-context 157 | 158 | gotemir: 159 | runs-on: ubuntu-24.04 160 | name: Src and tests structure check 161 | steps: 162 | - name: Checkout 163 | uses: actions/checkout@v6 164 | - name: Run gotemir 165 | run: | 166 | VERSION="0.0.3" 167 | curl -O -L -C - https://github.com/blablatdinov/gotemir/releases/download/$VERSION/gotemir-linux-amd64.tar 168 | tar xzf gotemir-linux-amd64.tar 169 | ./gotemir-linux-amd64 --ext .py ondivi tests/unit 170 | shell: bash 171 | -------------------------------------------------------------------------------- /ondivi/entry.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # 3 | # Copyright (c) 2024-2025 Almaz Ilaletdinov 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | # OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | """Ondivi (Only diff violations). 24 | 25 | Python script filtering coding violations, identified by static analysis, 26 | only for changed lines in a Git repo. 27 | """ 28 | 29 | from __future__ import annotations 30 | 31 | import sys 32 | import traceback 33 | from pathlib import Path 34 | 35 | import click 36 | from git import Repo 37 | from git.exc import GitCommandError 38 | 39 | from ondivi._internal.define_changed_lines import define_changed_lines 40 | from ondivi._internal.filter_out_violations import filter_out_violations 41 | from ondivi._internal.ondivi_types import ( 42 | ActualViolationsListStr, 43 | BaselineStr, 44 | DiffStr, 45 | FromFilePathStr, 46 | LinterAdditionalMessageStr, 47 | ViolationFormatStr, 48 | ViolationStr, 49 | ) 50 | 51 | 52 | def controller( 53 | diff: DiffStr, 54 | linter_out: list[ViolationStr | LinterAdditionalMessageStr], 55 | violation_format: ViolationFormatStr, 56 | only_violations: bool, 57 | ) -> tuple[ActualViolationsListStr, bool]: 58 | """Entrypoint. 59 | 60 | :param diff: Diff 61 | :param linter_out: list[str] 62 | :param violation_format: ViolationFormatStr 63 | :param only_violations: bool 64 | :return: tuple[ActualViolationsListStr, bool] 65 | """ 66 | return filter_out_violations( 67 | define_changed_lines(diff), 68 | linter_out, 69 | violation_format, 70 | only_violations, 71 | ) 72 | 73 | 74 | def _linter_output_from_file(file_path: FromFilePathStr) -> list[str]: 75 | if not Path(file_path).exists(): 76 | sys.stdout.write('File with violations "{0}" not found\n'.format(file_path)) 77 | sys.exit(1) 78 | return Path(file_path).read_text(encoding='utf-8').strip().splitlines() 79 | 80 | 81 | def cli( 82 | baseline: BaselineStr, 83 | fromfile: FromFilePathStr | None, 84 | violation_format: ViolationFormatStr, 85 | only_violations: bool, 86 | ) -> None: 87 | """Controller with CLI side effects. 88 | 89 | :param baseline: BaselineStr 90 | :param fromfile: FromFilePathStr | None 91 | :param violation_format: ViolationFormatStr 92 | :param only_violations: bool 93 | """ 94 | linter_output = ( 95 | _linter_output_from_file(fromfile) 96 | if fromfile 97 | else sys.stdin.read().strip().splitlines() 98 | ) 99 | try: 100 | diff = Repo('.').git.diff('--unified=0', '--no-ext-diff', '--src-prefix=a/', '--dst-prefix=b/', baseline) 101 | except GitCommandError: 102 | sys.stdout.write('Revision "{0}" not found'.format(baseline)) 103 | sys.exit(1) 104 | filtered_lines, violation_found = controller( 105 | diff, 106 | linter_output, 107 | violation_format, 108 | only_violations, 109 | ) 110 | if filtered_lines: 111 | sys.stdout.write( 112 | '{0}\n'.format( 113 | '\n'.join(filtered_lines), 114 | ), 115 | ) 116 | if violation_found: 117 | sys.exit(1) 118 | 119 | 120 | @click.command() 121 | @click.option( 122 | '--baseline', 123 | default='master', 124 | help=' '.join([ 125 | 'Commit or branch which will contain legacy code.', 126 | 'Program filter out violations on baseline', 127 | '(default: "master")', 128 | ]), 129 | ) 130 | @click.option( 131 | '--fromfile', 132 | default=None, 133 | help='Path to file with violations. Expected "utf-8" encoding', 134 | ) 135 | @click.option( 136 | '--format', 137 | 'violation_format', 138 | default='{filename}:{line_num:d}{other}', 139 | help=''.join([ 140 | 'Template for parsing linter messages. The template should include the following named parts:\n\n', 141 | '{filename} The name of the file with the error/warning\n', 142 | '{line_num} The line number with the error/warning (integer)\n\n', 143 | 'Example usage:\n\n', 144 | '--format "{filename}:{line_num:d}{other}"\n\n', 145 | 'In this example, the linter message\n\n', 146 | '"src/app_types/listable.py:23:1: UP035 Import from collections.abc instead: Sequence"\n\n', 147 | 'will be recognized and parsed into the following components:\n\n', 148 | ' - filename: "src/app_types/listable.py"\n\t\t\t\t', 149 | ' - line_num: 23\n\t\t\t\t\t', 150 | ' - other: :1: "UP035 Import from collections.abc instead: Sequence"\n\n', 151 | 'Ensure that the template matches the format of the messages generated by your linter.\n\t\t\t\t', 152 | '(default: "{filename}:{line_num:d}{other}")', 153 | ]), 154 | ) 155 | @click.option( 156 | '--only-violations', 157 | default=False, 158 | help='Show only violations', 159 | is_flag=True, 160 | ) 161 | def main(baseline: str, fromfile: str | None, violation_format: str, only_violations: bool) -> None: 162 | """Ondivi (Only diff violations). 163 | 164 | Python script filtering coding violations, identified by static analysis, 165 | only for changed lines in a Git repo. 166 | Usage example: 167 | 168 | flake8 script.py | ondivi 169 | """ 170 | try: 171 | cli(baseline, fromfile, violation_format, only_violations) 172 | except Exception as err: # noqa: BLE001 . Application entrypoint 173 | sys.stdout.write('\n'.join([ 174 | 'Ondivi fail with: "{0}"'.format(err), 175 | 'Please submit it to https://github.com/blablatdinov/ondivi/issues', 176 | 'Copy and paste this stack trace to GitHub:', 177 | '========================================', 178 | traceback.format_exc(), 179 | ])) 180 | sys.exit(1) 181 | -------------------------------------------------------------------------------- /tests/it/test_app.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT). 2 | # 3 | # Copyright (c) 2024-2025 Almaz Ilaletdinov 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | # OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | # flake8: noqa: WPS202 24 | 25 | """Integration test with installing and check on real git repo.""" 26 | 27 | import os 28 | import subprocess 29 | import sys 30 | from collections.abc import Generator 31 | from pathlib import Path 32 | from typing import Callable 33 | from unittest.mock import patch 34 | 35 | import pytest 36 | import tomli 37 | from _pytest.legacypath import TempdirFactory 38 | from click.testing import CliRunner 39 | from git import Repo 40 | from typing_extensions import TypeAlias 41 | 42 | from ondivi.entry import main 43 | from tests.helpers.define_repo import define_repo 44 | 45 | _RUN_SHELL_T: TypeAlias = Callable[ 46 | [list[str], list[str]], 47 | subprocess.CompletedProcess[bytes], 48 | ] 49 | 50 | 51 | @pytest.fixture(scope='module') 52 | def current_dir() -> Path: 53 | """Current directory for installing actual ondivi.""" 54 | return Path().absolute() 55 | 56 | 57 | def _version_from_lock(package_name: str) -> str: 58 | return '{0}=={1}'.format( 59 | package_name, 60 | next( 61 | package 62 | for package in tomli.loads(Path('poetry.lock').read_text(encoding='utf-8'))['package'] 63 | if package['name'] == package_name 64 | )['version'], 65 | ) 66 | 67 | 68 | @pytest.fixture 69 | def bin_dir() -> Path: 70 | """Directory with binaries for run.""" 71 | if os.name == 'nt': 72 | return Path('venv/Scripts') 73 | else: 74 | return Path('venv/bin') 75 | 76 | 77 | # ruff: noqa: S603, S607 Not a production code 78 | @pytest.fixture(scope='module') 79 | def test_repo( # noqa: WPS213 80 | tmpdir_factory: TempdirFactory, 81 | current_dir: str, 82 | ) -> Generator[Path, None, None]: 83 | """Real git repository.""" 84 | tmp_path = tmpdir_factory.mktemp('test') 85 | repo_path = tmp_path / 'ondivi-test-repo' 86 | repo_path.mkdir() 87 | define_repo(Path('tests/fixtures/test-repo.yaml').read_text(), repo_path) 88 | os.chdir(repo_path) 89 | subprocess.run(['python', '-m', 'venv', 'venv'], check=True) 90 | is_windows = os.name == 'nt' 91 | pip_path = Path('venv/Scripts/pip') if is_windows else Path('venv/bin/pip') 92 | if is_windows: 93 | subprocess.run( 94 | [str(Path('venv/Scripts/python')), '-m', 'pip', 'install', 'pip', '-U'], 95 | check=True, 96 | ) 97 | else: 98 | subprocess.run([str(pip_path), 'install', 'pip', '-U'], check=True) 99 | subprocess.run( 100 | [str(pip_path), 'install', 'flake8', 'ruff', 'mypy', str(current_dir)], 101 | check=True, 102 | ) 103 | yield tmp_path 104 | os.chdir(current_dir) 105 | 106 | 107 | @pytest.fixture 108 | def revisions(test_repo: Path) -> tuple[str, ...]: 109 | """List of commit hashes.""" 110 | return tuple( 111 | str(commit) for commit in Repo(test_repo / 'ondivi-test-repo').iter_commits() 112 | ) 113 | 114 | 115 | @pytest.fixture 116 | def run_shell() -> _RUN_SHELL_T: 117 | """Run commands with pipe in shell.""" 118 | def _exec(lint_cmd: list[str], ondivi_cmd: list[str]) -> subprocess.CompletedProcess[bytes]: # noqa: WPS430 119 | with subprocess.Popen(lint_cmd, stdout=subprocess.PIPE) as lint_proc: 120 | return subprocess.run( 121 | ondivi_cmd, 122 | stdin=lint_proc.stdout, 123 | stdout=subprocess.PIPE, 124 | check=False, 125 | ) 126 | return _exec 127 | 128 | 129 | @pytest.fixture 130 | def file_with_violations(test_repo: Path) -> Path: 131 | """File contain violations from linter.""" 132 | violations_file = test_repo / 'violations.txt' 133 | violations_file.write_text( 134 | '\n'.join([ 135 | '{0}:3:1: E302 expected 2 blank lines, found 1', 136 | '{0}:9:1: E302 expected 2 blank lines, found 1', 137 | '{0}:10:80: E501 line too long (123 > 79 characters)', 138 | '{0}:12:80: E501 line too long (119 > 79 characters)', 139 | '{0}:14:1: E305 expected 2 blank lines after class or function definition, found 1', 140 | ]).format(Path('inner/file.py')), 141 | encoding='utf-8', 142 | ) 143 | return violations_file 144 | 145 | 146 | @pytest.mark.usefixtures('test_repo') 147 | @pytest.mark.parametrize('version', [ 148 | ('gitpython==2.1.15',), 149 | (_version_from_lock('gitpython'),), 150 | ('gitpython', '-U'), 151 | ('parse==1.4',), 152 | (_version_from_lock('parse'),), 153 | ('parse', '-U'), 154 | ('click==0.2',), 155 | (_version_from_lock('click'),), 156 | ('click', '-U'), 157 | ]) 158 | def test_dependency_versions(version: tuple[str], run_shell: _RUN_SHELL_T, bin_dir: Path) -> None: 159 | """Test script with different dependency versions.""" 160 | subprocess.run( 161 | [str(bin_dir / 'pip'), 'install', *version], 162 | check=True, 163 | ) 164 | got = run_shell( 165 | [ 166 | str(bin_dir / 'flake8'), 167 | str(Path('inner/file.py')), 168 | ], 169 | [str(bin_dir / 'ondivi')], 170 | ) 171 | 172 | assert got.stdout.decode('utf-8').strip() == '{0}:12:80: E501 line too long (119 > 79 characters)'.format( 173 | Path('inner/file.py'), 174 | ) 175 | assert got.returncode == 1 176 | 177 | 178 | @pytest.mark.usefixtures('test_repo') 179 | def test(run_shell: _RUN_SHELL_T, revisions: tuple[str, ...], bin_dir: Path) -> None: 180 | """Test script with real git repo.""" 181 | got = run_shell( 182 | [str(bin_dir / 'flake8'), str(Path('inner/file.py'))], 183 | [str(bin_dir / 'ondivi'), '--baseline', revisions[-1]], 184 | ) 185 | 186 | assert got.stdout.decode('utf-8').strip().splitlines() == [ 187 | '{0}:3:1: E302 expected 2 blank lines, found 1'.format(Path('inner/file.py')), 188 | '{0}:9:1: E302 expected 2 blank lines, found 1'.format(Path('inner/file.py')), 189 | '{0}:12:80: E501 line too long (119 > 79 characters)'.format(Path('inner/file.py')), 190 | ] 191 | assert got.returncode == 1 192 | 193 | 194 | @pytest.mark.usefixtures('test_repo') 195 | def test_baseline_default(run_shell: _RUN_SHELL_T, bin_dir: Path) -> None: 196 | """Test baseline default.""" 197 | got = run_shell( 198 | [str(bin_dir / 'flake8'), str(Path('inner/file.py'))], 199 | [str(bin_dir / 'ondivi')], 200 | ) 201 | 202 | assert ( 203 | got.stdout.decode('utf-8').strip() 204 | == '{0}:12:80: E501 line too long (119 > 79 characters)'.format(Path('inner/file.py')) 205 | ) 206 | assert got.returncode == 1 207 | 208 | 209 | @pytest.mark.usefixtures('test_repo') 210 | def test_ruff(run_shell: _RUN_SHELL_T, bin_dir: Path) -> None: 211 | """Test ruff.""" 212 | got = run_shell( 213 | [str(bin_dir / 'ruff'), 'check', '--select=ALL', str(Path('inner/file.py')), '--output-format=concise'], 214 | [str(bin_dir / 'ondivi')], 215 | ) 216 | 217 | assert got.stdout.decode('utf-8').strip().splitlines() == [ 218 | '{0}:12:5: T201 `print` found'.format(Path('inner/file.py')), 219 | '{0}:12:11: Q000 [*] Single quotes found but double quotes preferred'.format(Path('inner/file.py')), 220 | '{0}:12:89: E501 Line too long (119 > 88)'.format(Path('inner/file.py')), 221 | '{0}:16:16: Q000 [*] Single quotes found but double quotes preferred'.format(Path('inner/file.py')), 222 | '{0}:16:23: Q000 [*] Single quotes found but double quotes preferred'.format(Path('inner/file.py')), 223 | 'Found 18 errors.', 224 | '[*] 8 fixable with the `--fix` option (4 hidden fixes can be enabled with the `--unsafe-fixes` option).', 225 | ] 226 | assert got.returncode == 1 227 | 228 | 229 | @pytest.mark.usefixtures('test_repo') 230 | def test_mypy(run_shell: _RUN_SHELL_T, bin_dir: Path) -> None: 231 | """Test mypy.""" 232 | got = run_shell( 233 | [str(bin_dir / 'mypy'), str(Path('inner/file.py'))], 234 | [str(bin_dir / 'ondivi')], 235 | ) 236 | 237 | assert got.stdout.decode('utf-8').strip().splitlines() == [ 238 | '{0}:16: error: Argument 2 to "User" has incompatible type "str"; expected "int" [arg-type]'.format( 239 | Path('inner/file.py'), 240 | ), 241 | 'Found 2 errors in 1 file (checked 1 source file)', 242 | ] 243 | assert got.returncode == 1 244 | 245 | 246 | @pytest.mark.usefixtures('test_repo') 247 | @pytest.mark.skipif(sys.platform.startswith('win'), reason='win not support "echo"') 248 | def test_without_violations(run_shell: _RUN_SHELL_T, bin_dir: Path) -> None: 249 | """Test exit without violations.""" 250 | got = run_shell(['echo', ''], [str(bin_dir / 'ondivi')]) 251 | 252 | assert got.returncode == 0 253 | 254 | 255 | @pytest.mark.usefixtures('test_repo') 256 | @pytest.mark.skipif(sys.platform.startswith('win'), reason='win not support "echo"') 257 | def test_info_message(run_shell: _RUN_SHELL_T, bin_dir: Path) -> None: 258 | """Test exit with info message.""" 259 | got = run_shell(['echo', 'All files correct!'], [str(bin_dir / 'ondivi')]) 260 | 261 | assert got.stdout.decode('utf-8').strip() == 'All files correct!' 262 | assert got.returncode == 0 263 | 264 | 265 | @pytest.mark.usefixtures('test_repo') 266 | @pytest.mark.skipif(sys.platform.startswith('win'), reason='win not support "echo"') 267 | def test_format(run_shell: _RUN_SHELL_T, bin_dir: Path) -> None: 268 | """Test with custom format.""" 269 | got = run_shell( 270 | ['echo', 'line=12 file={0} message=`print` found'.format(Path('inner/file.py'))], 271 | [str(bin_dir / 'ondivi'), '--format', 'line={line_num:d} file={filename} {other}'], 272 | ) 273 | 274 | assert got.stdout.decode('utf-8').strip() == 'line=12 file={0} message=`print` found'.format( 275 | Path('inner/file.py'), 276 | ) 277 | assert got.returncode == 1 278 | 279 | 280 | @pytest.mark.usefixtures('test_repo') 281 | def test_click_app() -> None: 282 | """Test click app.""" 283 | got = CliRunner().invoke( 284 | main, 285 | input='\n'.join([ 286 | '{0}:3:1: E302 expected 2 blank lines, found 1', 287 | '{0}:9:1: E302 expected 2 blank lines, found 1', 288 | '{0}:10:80: E501 line too long (123 > 79 characters)', 289 | '{0}:12:80: E501 line too long (119 > 79 characters)', 290 | '{0}:14:1: E305 expected 2 blank lines after class or function definition, found 1', 291 | ]).format(Path('inner/file.py')), 292 | ) 293 | 294 | assert got.exit_code == 1 295 | assert got.stdout.strip() == '{0}:12:80: E501 line too long (119 > 79 characters)'.format(Path('inner/file.py')) 296 | 297 | 298 | @pytest.fixture 299 | def _broke_cli() -> Generator[None, None, None]: 300 | with patch('ondivi.entry.cli') as cli_patch: 301 | cli_patch.side_effect = ValueError('Fail') 302 | yield 303 | 304 | 305 | @pytest.mark.usefixtures('_broke_cli') 306 | def test_handle_exception() -> None: 307 | """Test handle exception.""" 308 | got = CliRunner().invoke(main, input='') 309 | 310 | assert got.exit_code == 1 311 | assert len(got.stdout.strip().splitlines()) > 10 312 | assert got.stdout.strip().splitlines()[:5] == [ 313 | 'Ondivi fail with: "Fail"', 314 | 'Please submit it to https://github.com/blablatdinov/ondivi/issues', 315 | 'Copy and paste this stack trace to GitHub:', 316 | '========================================', 317 | 'Traceback (most recent call last):', 318 | ] 319 | assert got.stdout.strip().splitlines()[-1] == 'ValueError: Fail' 320 | 321 | 322 | @pytest.mark.usefixtures('test_repo') 323 | def test_only_violations(run_shell: _RUN_SHELL_T, bin_dir: Path, localize_violation_path: Callable[[str], str]) -> None: 324 | """Test only violations.""" 325 | got = run_shell( 326 | [str(bin_dir / 'ruff'), 'check', '--select=ALL', str(Path('inner/file.py')), '--output-format=concise'], 327 | [str(bin_dir / 'ondivi'), '--only-violations'], 328 | ) 329 | 330 | assert got.stdout.decode('utf-8').strip().splitlines() == [ 331 | localize_violation_path('inner/file.py:12:5: T201 `print` found'), 332 | localize_violation_path('inner/file.py:12:11: Q000 [*] Single quotes found but double quotes preferred'), 333 | localize_violation_path('inner/file.py:12:89: E501 Line too long (119 > 88)'), 334 | localize_violation_path('inner/file.py:16:16: Q000 [*] Single quotes found but double quotes preferred'), 335 | localize_violation_path('inner/file.py:16:23: Q000 [*] Single quotes found but double quotes preferred'), 336 | ] 337 | assert got.returncode == 1 338 | 339 | 340 | @pytest.mark.usefixtures('test_repo') 341 | def test_fromfile(file_with_violations: Path, bin_dir: Path) -> None: 342 | """Test script with violations from file.""" 343 | got = subprocess.run( 344 | [str(bin_dir / 'ondivi'), '--fromfile', str(file_with_violations)], 345 | stdout=subprocess.PIPE, 346 | check=False, 347 | ) 348 | 349 | assert got.stdout.decode('utf-8').strip() == '{0}:12:80: E501 line too long (119 > 79 characters)'.format( 350 | Path('inner/file.py'), 351 | ) 352 | assert got.returncode == 1 353 | 354 | 355 | @pytest.mark.usefixtures('test_repo') 356 | def test_fromfile_via_cli_runner(file_with_violations: Path) -> None: 357 | """Test script with violations from file via CliRunner.""" 358 | got = CliRunner().invoke(main, ['--fromfile', str(file_with_violations)], input='') 359 | 360 | assert got.stdout.strip() == '{0}:12:80: E501 line too long (119 > 79 characters)'.format(Path('inner/file.py')) 361 | assert got.exit_code == 1 362 | 363 | 364 | @pytest.mark.usefixtures('test_repo') 365 | def test_fromfile_not_found(bin_dir: Path) -> None: 366 | """Test script with violations from file.""" 367 | got = subprocess.run( 368 | [str(bin_dir / 'ondivi'), '--fromfile', 'undefined.txt'], 369 | stdout=subprocess.PIPE, 370 | check=False, 371 | ) 372 | 373 | assert got.stdout.decode('utf-8').strip() == 'File with violations "undefined.txt" not found' 374 | assert got.returncode == 1 375 | 376 | 377 | @pytest.mark.usefixtures('test_repo') 378 | def test_fromfile_not_found_via_cli_runner() -> None: 379 | """Test script with violations from file via CliRunner.""" 380 | got = CliRunner().invoke(main, ['--fromfile', 'undefined.txt'], input='') 381 | 382 | assert got.stdout == 'File with violations "undefined.txt" not found\n' 383 | assert got.exit_code == 1 384 | 385 | 386 | @pytest.mark.usefixtures('test_repo') 387 | def test_commit_not_found() -> None: 388 | """Test commit not found.""" 389 | got = CliRunner().invoke(main, ['--baseline', 'fakeHash'], input='') 390 | 391 | assert got.stdout == 'Revision "fakeHash" not found' 392 | assert got.exit_code == 1 393 | 394 | 395 | @pytest.mark.usefixtures('test_repo') 396 | def test_last_symbol(run_shell: _RUN_SHELL_T, bin_dir: Path) -> None: 397 | """Test last symbol with violations.""" 398 | got = run_shell( 399 | [str(bin_dir / 'flake8'), str(Path('inner/file.py'))], 400 | [str(bin_dir / 'ondivi')], 401 | ) 402 | 403 | assert got.returncode == 1 404 | assert got.stdout.decode('utf-8').count('\n') == 1 405 | 406 | 407 | @pytest.mark.usefixtures('test_repo') 408 | def test_last_symbol_without_violations(run_shell: _RUN_SHELL_T, bin_dir: Path) -> None: 409 | """Test last symbol without violations. 410 | 411 | Before fix: 412 | 413 | bash-3.2$ echo '' | ondivi 414 | 415 | bash-3.2$ 416 | """ 417 | got = run_shell( 418 | ['echo', ''], 419 | [str(bin_dir / 'ondivi')], 420 | ) 421 | 422 | assert got.returncode == 0 423 | assert got.stdout.decode('utf-8').count('\n') == 0 424 | 425 | 426 | @pytest.mark.usefixtures('test_repo') 427 | @pytest.mark.parametrize('git_config', [ 428 | pytest.param({'diff.external': str(Path('tests/fixtures/fake_diff_out.sh'))}, id='diff.external'), 429 | pytest.param({'diff.mnemonicPrefix': 'true'}, id='diff.mnemonicPrefix'), 430 | ]) 431 | def test_git_with_custom_user_config( 432 | run_shell: _RUN_SHELL_T, 433 | bin_dir: Path, 434 | git_config: dict[str, str], 435 | monkeypatch: pytest.MonkeyPatch, 436 | ) -> None: 437 | """Test that script works correctly when user has custom config for git.""" 438 | monkeypatch.setenv('GIT_CONFIG_COUNT', str(len(git_config))) 439 | for i, (k, v) in enumerate(git_config.items()): 440 | monkeypatch.setenv(f'GIT_CONFIG_KEY_{i}', k) 441 | monkeypatch.setenv(f'GIT_CONFIG_VALUE_{i}', v) 442 | 443 | got = run_shell( 444 | [str(bin_dir / 'flake8'), str(Path('inner/file.py'))], 445 | [str(bin_dir / 'ondivi')], 446 | ) 447 | 448 | assert got.stdout.decode('utf-8').strip() == '{0}:12:80: E501 line too long (119 > 79 characters)'.format( 449 | Path('inner/file.py'), 450 | ) 451 | assert got.returncode == 1 452 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "anyio" 5 | version = "4.11.0" 6 | description = "High-level concurrency and networking framework on top of asyncio or Trio" 7 | optional = false 8 | python-versions = ">=3.9" 9 | groups = ["dev"] 10 | files = [ 11 | {file = "anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc"}, 12 | {file = "anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4"}, 13 | ] 14 | 15 | [package.dependencies] 16 | exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} 17 | idna = ">=2.8" 18 | sniffio = ">=1.1" 19 | typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} 20 | 21 | [package.extras] 22 | trio = ["trio (>=0.31.0)"] 23 | 24 | [[package]] 25 | name = "attrs" 26 | version = "25.4.0" 27 | description = "Classes Without Boilerplate" 28 | optional = false 29 | python-versions = ">=3.9" 30 | groups = ["dev"] 31 | files = [ 32 | {file = "attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"}, 33 | {file = "attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11"}, 34 | ] 35 | 36 | [[package]] 37 | name = "certifi" 38 | version = "2025.10.5" 39 | description = "Python package for providing Mozilla's CA Bundle." 40 | optional = false 41 | python-versions = ">=3.7" 42 | groups = ["dev"] 43 | files = [ 44 | {file = "certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de"}, 45 | {file = "certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43"}, 46 | ] 47 | 48 | [[package]] 49 | name = "click" 50 | version = "8.3.1" 51 | description = "Composable command line interface toolkit" 52 | optional = false 53 | python-versions = ">=3.10" 54 | groups = ["main", "dev"] 55 | files = [ 56 | {file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"}, 57 | {file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"}, 58 | ] 59 | 60 | [package.dependencies] 61 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 62 | 63 | [[package]] 64 | name = "colorama" 65 | version = "0.4.6" 66 | description = "Cross-platform colored terminal text." 67 | optional = false 68 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 69 | groups = ["main", "dev"] 70 | files = [ 71 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 72 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 73 | ] 74 | markers = {main = "platform_system == \"Windows\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\""} 75 | 76 | [[package]] 77 | name = "coverage" 78 | version = "7.11.0" 79 | description = "Code coverage measurement for Python" 80 | optional = false 81 | python-versions = ">=3.10" 82 | groups = ["dev"] 83 | files = [ 84 | {file = "coverage-7.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eb53f1e8adeeb2e78962bade0c08bfdc461853c7969706ed901821e009b35e31"}, 85 | {file = "coverage-7.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d9a03ec6cb9f40a5c360f138b88266fd8f58408d71e89f536b4f91d85721d075"}, 86 | {file = "coverage-7.11.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0d7f0616c557cbc3d1c2090334eddcbb70e1ae3a40b07222d62b3aa47f608fab"}, 87 | {file = "coverage-7.11.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e44a86a47bbdf83b0a3ea4d7df5410d6b1a0de984fbd805fa5101f3624b9abe0"}, 88 | {file = "coverage-7.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:596763d2f9a0ee7eec6e643e29660def2eef297e1de0d334c78c08706f1cb785"}, 89 | {file = "coverage-7.11.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ef55537ff511b5e0a43edb4c50a7bf7ba1c3eea20b4f49b1490f1e8e0e42c591"}, 90 | {file = "coverage-7.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9cbabd8f4d0d3dc571d77ae5bdbfa6afe5061e679a9d74b6797c48d143307088"}, 91 | {file = "coverage-7.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e24045453384e0ae2a587d562df2a04d852672eb63051d16096d3f08aa4c7c2f"}, 92 | {file = "coverage-7.11.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:7161edd3426c8d19bdccde7d49e6f27f748f3c31cc350c5de7c633fea445d866"}, 93 | {file = "coverage-7.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d4ed4de17e692ba6415b0587bc7f12bc80915031fc9db46a23ce70fc88c9841"}, 94 | {file = "coverage-7.11.0-cp310-cp310-win32.whl", hash = "sha256:765c0bc8fe46f48e341ef737c91c715bd2a53a12792592296a095f0c237e09cf"}, 95 | {file = "coverage-7.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:24d6f3128f1b2d20d84b24f4074475457faedc3d4613a7e66b5e769939c7d969"}, 96 | {file = "coverage-7.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d58ecaa865c5b9fa56e35efc51d1014d4c0d22838815b9fce57a27dd9576847"}, 97 | {file = "coverage-7.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b679e171f1c104a5668550ada700e3c4937110dbdd153b7ef9055c4f1a1ee3cc"}, 98 | {file = "coverage-7.11.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ca61691ba8c5b6797deb221a0d09d7470364733ea9c69425a640f1f01b7c5bf0"}, 99 | {file = "coverage-7.11.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:aef1747ede4bd8ca9cfc04cc3011516500c6891f1b33a94add3253f6f876b7b7"}, 100 | {file = "coverage-7.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1839d08406e4cba2953dcc0ffb312252f14d7c4c96919f70167611f4dee2623"}, 101 | {file = "coverage-7.11.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e0eb0a2dcc62478eb5b4cbb80b97bdee852d7e280b90e81f11b407d0b81c4287"}, 102 | {file = "coverage-7.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bc1fbea96343b53f65d5351d8fd3b34fd415a2670d7c300b06d3e14a5af4f552"}, 103 | {file = "coverage-7.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:214b622259dd0cf435f10241f1333d32caa64dbc27f8790ab693428a141723de"}, 104 | {file = "coverage-7.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:258d9967520cca899695d4eb7ea38be03f06951d6ca2f21fb48b1235f791e601"}, 105 | {file = "coverage-7.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cf9e6ff4ca908ca15c157c409d608da77a56a09877b97c889b98fb2c32b6465e"}, 106 | {file = "coverage-7.11.0-cp311-cp311-win32.whl", hash = "sha256:fcc15fc462707b0680cff6242c48625da7f9a16a28a41bb8fd7a4280920e676c"}, 107 | {file = "coverage-7.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:865965bf955d92790f1facd64fe7ff73551bd2c1e7e6b26443934e9701ba30b9"}, 108 | {file = "coverage-7.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:5693e57a065760dcbeb292d60cc4d0231a6d4b6b6f6a3191561e1d5e8820b745"}, 109 | {file = "coverage-7.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9c49e77811cf9d024b95faf86c3f059b11c0c9be0b0d61bc598f453703bd6fd1"}, 110 | {file = "coverage-7.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a61e37a403a778e2cda2a6a39abcc895f1d984071942a41074b5c7ee31642007"}, 111 | {file = "coverage-7.11.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c79cae102bb3b1801e2ef1511fb50e91ec83a1ce466b2c7c25010d884336de46"}, 112 | {file = "coverage-7.11.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:16ce17ceb5d211f320b62df002fa7016b7442ea0fd260c11cec8ce7730954893"}, 113 | {file = "coverage-7.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80027673e9d0bd6aef86134b0771845e2da85755cf686e7c7c59566cf5a89115"}, 114 | {file = "coverage-7.11.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d3ffa07a08657306cd2215b0da53761c4d73cb54d9143b9303a6481ec0cd415"}, 115 | {file = "coverage-7.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a3b6a5f8b2524fd6c1066bc85bfd97e78709bb5e37b5b94911a6506b65f47186"}, 116 | {file = "coverage-7.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fcc0a4aa589de34bc56e1a80a740ee0f8c47611bdfb28cd1849de60660f3799d"}, 117 | {file = "coverage-7.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dba82204769d78c3fd31b35c3d5f46e06511936c5019c39f98320e05b08f794d"}, 118 | {file = "coverage-7.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:81b335f03ba67309a95210caf3eb43bd6fe75a4e22ba653ef97b4696c56c7ec2"}, 119 | {file = "coverage-7.11.0-cp312-cp312-win32.whl", hash = "sha256:037b2d064c2f8cc8716fe4d39cb705779af3fbf1ba318dc96a1af858888c7bb5"}, 120 | {file = "coverage-7.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:d66c0104aec3b75e5fd897e7940188ea1892ca1d0235316bf89286d6a22568c0"}, 121 | {file = "coverage-7.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:d91ebeac603812a09cf6a886ba6e464f3bbb367411904ae3790dfe28311b15ad"}, 122 | {file = "coverage-7.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cc3f49e65ea6e0d5d9bd60368684fe52a704d46f9e7fc413918f18d046ec40e1"}, 123 | {file = "coverage-7.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f39ae2f63f37472c17b4990f794035c9890418b1b8cca75c01193f3c8d3e01be"}, 124 | {file = "coverage-7.11.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7db53b5cdd2917b6eaadd0b1251cf4e7d96f4a8d24e174bdbdf2f65b5ea7994d"}, 125 | {file = "coverage-7.11.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10ad04ac3a122048688387828b4537bc9cf60c0bf4869c1e9989c46e45690b82"}, 126 | {file = "coverage-7.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4036cc9c7983a2b1f2556d574d2eb2154ac6ed55114761685657e38782b23f52"}, 127 | {file = "coverage-7.11.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ab934dd13b1c5e94b692b1e01bd87e4488cb746e3a50f798cb9464fd128374b"}, 128 | {file = "coverage-7.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59a6e5a265f7cfc05f76e3bb53eca2e0dfe90f05e07e849930fecd6abb8f40b4"}, 129 | {file = "coverage-7.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:df01d6c4c81e15a7c88337b795bb7595a8596e92310266b5072c7e301168efbd"}, 130 | {file = "coverage-7.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8c934bd088eed6174210942761e38ee81d28c46de0132ebb1801dbe36a390dcc"}, 131 | {file = "coverage-7.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a03eaf7ec24078ad64a07f02e30060aaf22b91dedf31a6b24d0d98d2bba7f48"}, 132 | {file = "coverage-7.11.0-cp313-cp313-win32.whl", hash = "sha256:695340f698a5f56f795b2836abe6fb576e7c53d48cd155ad2f80fd24bc63a040"}, 133 | {file = "coverage-7.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:2727d47fce3ee2bac648528e41455d1b0c46395a087a229deac75e9f88ba5a05"}, 134 | {file = "coverage-7.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:0efa742f431529699712b92ecdf22de8ff198df41e43aeaaadf69973eb93f17a"}, 135 | {file = "coverage-7.11.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:587c38849b853b157706407e9ebdca8fd12f45869edb56defbef2daa5fb0812b"}, 136 | {file = "coverage-7.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b971bdefdd75096163dd4261c74be813c4508477e39ff7b92191dea19f24cd37"}, 137 | {file = "coverage-7.11.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:269bfe913b7d5be12ab13a95f3a76da23cf147be7fa043933320ba5625f0a8de"}, 138 | {file = "coverage-7.11.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dadbcce51a10c07b7c72b0ce4a25e4b6dcb0c0372846afb8e5b6307a121eb99f"}, 139 | {file = "coverage-7.11.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ed43fa22c6436f7957df036331f8fe4efa7af132054e1844918866cd228af6c"}, 140 | {file = "coverage-7.11.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9516add7256b6713ec08359b7b05aeff8850c98d357784c7205b2e60aa2513fa"}, 141 | {file = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb92e47c92fcbcdc692f428da67db33337fa213756f7adb6a011f7b5a7a20740"}, 142 | {file = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d06f4fc7acf3cabd6d74941d53329e06bab00a8fe10e4df2714f0b134bfc64ef"}, 143 | {file = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:6fbcee1a8f056af07ecd344482f711f563a9eb1c2cad192e87df00338ec3cdb0"}, 144 | {file = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dbbf012be5f32533a490709ad597ad8a8ff80c582a95adc8d62af664e532f9ca"}, 145 | {file = "coverage-7.11.0-cp313-cp313t-win32.whl", hash = "sha256:cee6291bb4fed184f1c2b663606a115c743df98a537c969c3c64b49989da96c2"}, 146 | {file = "coverage-7.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a386c1061bf98e7ea4758e4313c0ab5ecf57af341ef0f43a0bf26c2477b5c268"}, 147 | {file = "coverage-7.11.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f9ea02ef40bb83823b2b04964459d281688fe173e20643870bb5d2edf68bc836"}, 148 | {file = "coverage-7.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c770885b28fb399aaf2a65bbd1c12bf6f307ffd112d6a76c5231a94276f0c497"}, 149 | {file = "coverage-7.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a3d0e2087dba64c86a6b254f43e12d264b636a39e88c5cc0a01a7c71bcfdab7e"}, 150 | {file = "coverage-7.11.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73feb83bb41c32811973b8565f3705caf01d928d972b72042b44e97c71fd70d1"}, 151 | {file = "coverage-7.11.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c6f31f281012235ad08f9a560976cc2fc9c95c17604ff3ab20120fe480169bca"}, 152 | {file = "coverage-7.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9570ad567f880ef675673992222746a124b9595506826b210fbe0ce3f0499cd"}, 153 | {file = "coverage-7.11.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8badf70446042553a773547a61fecaa734b55dc738cacf20c56ab04b77425e43"}, 154 | {file = "coverage-7.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a09c1211959903a479e389685b7feb8a17f59ec5a4ef9afde7650bd5eabc2777"}, 155 | {file = "coverage-7.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:5ef83b107f50db3f9ae40f69e34b3bd9337456c5a7fe3461c7abf8b75dd666a2"}, 156 | {file = "coverage-7.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f91f927a3215b8907e214af77200250bb6aae36eca3f760f89780d13e495388d"}, 157 | {file = "coverage-7.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbcd376716d6b7fbfeedd687a6c4be019c5a5671b35f804ba76a4c0a778cba4"}, 158 | {file = "coverage-7.11.0-cp314-cp314-win32.whl", hash = "sha256:bab7ec4bb501743edc63609320aaec8cd9188b396354f482f4de4d40a9d10721"}, 159 | {file = "coverage-7.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:3d4ba9a449e9364a936a27322b20d32d8b166553bfe63059bd21527e681e2fad"}, 160 | {file = "coverage-7.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:ce37f215223af94ef0f75ac68ea096f9f8e8c8ec7d6e8c346ee45c0d363f0479"}, 161 | {file = "coverage-7.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f413ce6e07e0d0dc9c433228727b619871532674b45165abafe201f200cc215f"}, 162 | {file = "coverage-7.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:05791e528a18f7072bf5998ba772fe29db4da1234c45c2087866b5ba4dea710e"}, 163 | {file = "coverage-7.11.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cacb29f420cfeb9283b803263c3b9a068924474ff19ca126ba9103e1278dfa44"}, 164 | {file = "coverage-7.11.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314c24e700d7027ae3ab0d95fbf8d53544fca1f20345fd30cd219b737c6e58d3"}, 165 | {file = "coverage-7.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:630d0bd7a293ad2fc8b4b94e5758c8b2536fdf36c05f1681270203e463cbfa9b"}, 166 | {file = "coverage-7.11.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e89641f5175d65e2dbb44db15fe4ea48fade5d5bbb9868fdc2b4fce22f4a469d"}, 167 | {file = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c9f08ea03114a637dab06cedb2e914da9dc67fa52c6015c018ff43fdde25b9c2"}, 168 | {file = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce9f3bde4e9b031eaf1eb61df95c1401427029ea1bfddb8621c1161dcb0fa02e"}, 169 | {file = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:e4dc07e95495923d6fd4d6c27bf70769425b71c89053083843fd78f378558996"}, 170 | {file = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:424538266794db2861db4922b05d729ade0940ee69dcf0591ce8f69784db0e11"}, 171 | {file = "coverage-7.11.0-cp314-cp314t-win32.whl", hash = "sha256:4c1eeb3fb8eb9e0190bebafd0462936f75717687117339f708f395fe455acc73"}, 172 | {file = "coverage-7.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b56efee146c98dbf2cf5cffc61b9829d1e94442df4d7398b26892a53992d3547"}, 173 | {file = "coverage-7.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b5c2705afa83f49bd91962a4094b6b082f94aef7626365ab3f8f4bd159c5acf3"}, 174 | {file = "coverage-7.11.0-py3-none-any.whl", hash = "sha256:4b7589765348d78fb4e5fb6ea35d07564e387da2fc5efff62e0222971f155f68"}, 175 | {file = "coverage-7.11.0.tar.gz", hash = "sha256:167bd504ac1ca2af7ff3b81d245dfea0292c5032ebef9d66cc08a7d28c1b8050"}, 176 | ] 177 | 178 | [package.dependencies] 179 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 180 | 181 | [package.extras] 182 | toml = ["tomli ; python_full_version <= \"3.11.0a6\""] 183 | 184 | [[package]] 185 | name = "deltaver" 186 | version = "1.0.0" 187 | description = "" 188 | optional = false 189 | python-versions = "<4.0,>=3.10" 190 | groups = ["dev"] 191 | files = [ 192 | {file = "deltaver-1.0.0-py3-none-any.whl", hash = "sha256:8760b491609aac0da4d87ce346d3bce584b7c9f2c9c06dab9f31f068a31fa60c"}, 193 | {file = "deltaver-1.0.0.tar.gz", hash = "sha256:00e3454aa109dfc7c98c1a1622398d4681e7711fe6c978e914b14215e770f550"}, 194 | ] 195 | 196 | [package.dependencies] 197 | attrs = ">=23.0" 198 | httpx = ">=0,<1" 199 | packaging = ">=23" 200 | pytz = ">=2018.4" 201 | rich = ">=14.0.0,<15.0.0" 202 | toml = ">=0.10.2,<0.11.0" 203 | typer = ">=0.13" 204 | typing-extensions = ">=4.9,<5.0" 205 | 206 | [[package]] 207 | name = "exceptiongroup" 208 | version = "1.3.0" 209 | description = "Backport of PEP 654 (exception groups)" 210 | optional = false 211 | python-versions = ">=3.7" 212 | groups = ["dev"] 213 | markers = "python_version == \"3.10\"" 214 | files = [ 215 | {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, 216 | {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, 217 | ] 218 | 219 | [package.dependencies] 220 | typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} 221 | 222 | [package.extras] 223 | test = ["pytest (>=6)"] 224 | 225 | [[package]] 226 | name = "flake8" 227 | version = "7.3.0" 228 | description = "the modular source code checker: pep8 pyflakes and co" 229 | optional = false 230 | python-versions = ">=3.9" 231 | groups = ["dev"] 232 | files = [ 233 | {file = "flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e"}, 234 | {file = "flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872"}, 235 | ] 236 | 237 | [package.dependencies] 238 | mccabe = ">=0.7.0,<0.8.0" 239 | pycodestyle = ">=2.14.0,<2.15.0" 240 | pyflakes = ">=3.4.0,<3.5.0" 241 | 242 | [[package]] 243 | name = "gitdb" 244 | version = "4.0.12" 245 | description = "Git Object Database" 246 | optional = false 247 | python-versions = ">=3.7" 248 | groups = ["main"] 249 | files = [ 250 | {file = "gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf"}, 251 | {file = "gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571"}, 252 | ] 253 | 254 | [package.dependencies] 255 | smmap = ">=3.0.1,<6" 256 | 257 | [[package]] 258 | name = "gitpython" 259 | version = "3.1.45" 260 | description = "GitPython is a Python library used to interact with Git repositories" 261 | optional = false 262 | python-versions = ">=3.7" 263 | groups = ["main"] 264 | files = [ 265 | {file = "gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77"}, 266 | {file = "gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c"}, 267 | ] 268 | 269 | [package.dependencies] 270 | gitdb = ">=4.0.1,<5" 271 | 272 | [package.extras] 273 | doc = ["sphinx (>=7.1.2,<7.2)", "sphinx-autodoc-typehints", "sphinx_rtd_theme"] 274 | test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock ; python_version < \"3.8\"", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions ; python_version < \"3.11\""] 275 | 276 | [[package]] 277 | name = "glob2" 278 | version = "0.7" 279 | description = "Version of the glob module that can capture patterns and supports recursive wildcards" 280 | optional = false 281 | python-versions = "*" 282 | groups = ["dev"] 283 | files = [ 284 | {file = "glob2-0.7.tar.gz", hash = "sha256:85c3dbd07c8aa26d63d7aacee34fa86e9a91a3873bc30bf62ec46e531f92ab8c"}, 285 | ] 286 | 287 | [[package]] 288 | name = "h11" 289 | version = "0.16.0" 290 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 291 | optional = false 292 | python-versions = ">=3.8" 293 | groups = ["dev"] 294 | files = [ 295 | {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, 296 | {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, 297 | ] 298 | 299 | [[package]] 300 | name = "httpcore" 301 | version = "1.0.9" 302 | description = "A minimal low-level HTTP client." 303 | optional = false 304 | python-versions = ">=3.8" 305 | groups = ["dev"] 306 | files = [ 307 | {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, 308 | {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, 309 | ] 310 | 311 | [package.dependencies] 312 | certifi = "*" 313 | h11 = ">=0.16" 314 | 315 | [package.extras] 316 | asyncio = ["anyio (>=4.0,<5.0)"] 317 | http2 = ["h2 (>=3,<5)"] 318 | socks = ["socksio (==1.*)"] 319 | trio = ["trio (>=0.22.0,<1.0)"] 320 | 321 | [[package]] 322 | name = "httpx" 323 | version = "0.28.1" 324 | description = "The next generation HTTP client." 325 | optional = false 326 | python-versions = ">=3.8" 327 | groups = ["dev"] 328 | files = [ 329 | {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, 330 | {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, 331 | ] 332 | 333 | [package.dependencies] 334 | anyio = "*" 335 | certifi = "*" 336 | httpcore = "==1.*" 337 | idna = "*" 338 | 339 | [package.extras] 340 | brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] 341 | cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] 342 | http2 = ["h2 (>=3,<5)"] 343 | socks = ["socksio (==1.*)"] 344 | zstd = ["zstandard (>=0.18.0)"] 345 | 346 | [[package]] 347 | name = "idna" 348 | version = "3.11" 349 | description = "Internationalized Domain Names in Applications (IDNA)" 350 | optional = false 351 | python-versions = ">=3.8" 352 | groups = ["dev"] 353 | files = [ 354 | {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, 355 | {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, 356 | ] 357 | 358 | [package.extras] 359 | all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] 360 | 361 | [[package]] 362 | name = "iniconfig" 363 | version = "2.3.0" 364 | description = "brain-dead simple config-ini parsing" 365 | optional = false 366 | python-versions = ">=3.10" 367 | groups = ["dev"] 368 | files = [ 369 | {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, 370 | {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, 371 | ] 372 | 373 | [[package]] 374 | name = "junit-xml" 375 | version = "1.9" 376 | description = "Creates JUnit XML test result documents that can be read by tools such as Jenkins" 377 | optional = false 378 | python-versions = "*" 379 | groups = ["dev"] 380 | files = [ 381 | {file = "junit-xml-1.9.tar.gz", hash = "sha256:de16a051990d4e25a3982b2dd9e89d671067548718866416faec14d9de56db9f"}, 382 | {file = "junit_xml-1.9-py2.py3-none-any.whl", hash = "sha256:ec5ca1a55aefdd76d28fcc0b135251d156c7106fa979686a4b48d62b761b4732"}, 383 | ] 384 | 385 | [package.dependencies] 386 | six = "*" 387 | 388 | [[package]] 389 | name = "librt" 390 | version = "0.6.3" 391 | description = "Mypyc runtime library" 392 | optional = false 393 | python-versions = ">=3.9" 394 | groups = ["dev"] 395 | markers = "platform_python_implementation != \"PyPy\"" 396 | files = [ 397 | {file = "librt-0.6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:45660d26569cc22ed30adf583389d8a0d1b468f8b5e518fcf9bfe2cd298f9dd1"}, 398 | {file = "librt-0.6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:54f3b2177fb892d47f8016f1087d21654b44f7fc4cf6571c1c6b3ea531ab0fcf"}, 399 | {file = "librt-0.6.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c5b31bed2c2f2fa1fcb4815b75f931121ae210dc89a3d607fb1725f5907f1437"}, 400 | {file = "librt-0.6.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f8ed5053ef9fb08d34f1fd80ff093ccbd1f67f147633a84cf4a7d9b09c0f089"}, 401 | {file = "librt-0.6.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3f0e4bd9bcb0ee34fa3dbedb05570da50b285f49e52c07a241da967840432513"}, 402 | {file = "librt-0.6.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d8f89c8d20dfa648a3f0a56861946eb00e5b00d6b00eea14bc5532b2fcfa8ef1"}, 403 | {file = "librt-0.6.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ecc2c526547eacd20cb9fbba19a5268611dbc70c346499656d6cf30fae328977"}, 404 | {file = "librt-0.6.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fbedeb9b48614d662822ee514567d2d49a8012037fc7b4cd63f282642c2f4b7d"}, 405 | {file = "librt-0.6.3-cp310-cp310-win32.whl", hash = "sha256:0765b0fe0927d189ee14b087cd595ae636bef04992e03fe6dfdaa383866c8a46"}, 406 | {file = "librt-0.6.3-cp310-cp310-win_amd64.whl", hash = "sha256:8c659f9fb8a2f16dc4131b803fa0144c1dadcb3ab24bb7914d01a6da58ae2457"}, 407 | {file = "librt-0.6.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:61348cc488b18d1b1ff9f3e5fcd5ac43ed22d3e13e862489d2267c2337285c08"}, 408 | {file = "librt-0.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:64645b757d617ad5f98c08e07620bc488d4bced9ced91c6279cec418f16056fa"}, 409 | {file = "librt-0.6.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:26b8026393920320bb9a811b691d73c5981385d537ffc5b6e22e53f7b65d4122"}, 410 | {file = "librt-0.6.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d998b432ed9ffccc49b820e913c8f327a82026349e9c34fa3690116f6b70770f"}, 411 | {file = "librt-0.6.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e18875e17ef69ba7dfa9623f2f95f3eda6f70b536079ee6d5763ecdfe6cc9040"}, 412 | {file = "librt-0.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a218f85081fc3f70cddaed694323a1ad7db5ca028c379c214e3a7c11c0850523"}, 413 | {file = "librt-0.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1ef42ff4edd369e84433ce9b188a64df0837f4f69e3d34d3b34d4955c599d03f"}, 414 | {file = "librt-0.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e0f2b79993fec23a685b3e8107ba5f8675eeae286675a216da0b09574fa1e47"}, 415 | {file = "librt-0.6.3-cp311-cp311-win32.whl", hash = "sha256:fd98cacf4e0fabcd4005c452cb8a31750258a85cab9a59fb3559e8078da408d7"}, 416 | {file = "librt-0.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:e17b5b42c8045867ca9d1f54af00cc2275198d38de18545edaa7833d7e9e4ac8"}, 417 | {file = "librt-0.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:87597e3d57ec0120a3e1d857a708f80c02c42ea6b00227c728efbc860f067c45"}, 418 | {file = "librt-0.6.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74418f718083009108dc9a42c21bf2e4802d49638a1249e13677585fcc9ca176"}, 419 | {file = "librt-0.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:514f3f363d1ebc423357d36222c37e5c8e6674b6eae8d7195ac9a64903722057"}, 420 | {file = "librt-0.6.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cf1115207a5049d1f4b7b4b72de0e52f228d6c696803d94843907111cbf80610"}, 421 | {file = "librt-0.6.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad8ba80cdcea04bea7b78fcd4925bfbf408961e9d8397d2ee5d3ec121e20c08c"}, 422 | {file = "librt-0.6.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4018904c83eab49c814e2494b4e22501a93cdb6c9f9425533fe693c3117126f9"}, 423 | {file = "librt-0.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8983c5c06ac9c990eac5eb97a9f03fe41dc7e9d7993df74d9e8682a1056f596c"}, 424 | {file = "librt-0.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7769c579663a6f8dbf34878969ac71befa42067ce6bf78e6370bf0d1194997c"}, 425 | {file = "librt-0.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d3c9a07eafdc70556f8c220da4a538e715668c0c63cabcc436a026e4e89950bf"}, 426 | {file = "librt-0.6.3-cp312-cp312-win32.whl", hash = "sha256:38320386a48a15033da295df276aea93a92dfa94a862e06893f75ea1d8bbe89d"}, 427 | {file = "librt-0.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:c0ecf4786ad0404b072196b5df774b1bb23c8aacdcacb6c10b4128bc7b00bd01"}, 428 | {file = "librt-0.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:9f2a6623057989ebc469cd9cc8fe436c40117a0147627568d03f84aef7854c55"}, 429 | {file = "librt-0.6.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9e716f9012148a81f02f46a04fc4c663420c6fbfeacfac0b5e128cf43b4413d3"}, 430 | {file = "librt-0.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:669ff2495728009a96339c5ad2612569c6d8be4474e68f3f3ac85d7c3261f5f5"}, 431 | {file = "librt-0.6.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:349b6873ebccfc24c9efd244e49da9f8a5c10f60f07575e248921aae2123fc42"}, 432 | {file = "librt-0.6.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c74c26736008481c9f6d0adf1aedb5a52aff7361fea98276d1f965c0256ee70"}, 433 | {file = "librt-0.6.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:408a36ddc75e91918cb15b03460bdc8a015885025d67e68c6f78f08c3a88f522"}, 434 | {file = "librt-0.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e61ab234624c9ffca0248a707feffe6fac2343758a36725d8eb8a6efef0f8c30"}, 435 | {file = "librt-0.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:324462fe7e3896d592b967196512491ec60ca6e49c446fe59f40743d08c97917"}, 436 | {file = "librt-0.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:36b2ec8c15030002c7f688b4863e7be42820d7c62d9c6eece3db54a2400f0530"}, 437 | {file = "librt-0.6.3-cp313-cp313-win32.whl", hash = "sha256:25b1b60cb059471c0c0c803e07d0dfdc79e41a0a122f288b819219ed162672a3"}, 438 | {file = "librt-0.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:10a95ad074e2a98c9e4abc7f5b7d40e5ecbfa84c04c6ab8a70fabf59bd429b88"}, 439 | {file = "librt-0.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:17000df14f552e86877d67e4ab7966912224efc9368e998c96a6974a8d609bf9"}, 440 | {file = "librt-0.6.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8e695f25d1a425ad7a272902af8ab8c8d66c1998b177e4b5f5e7b4e215d0c88a"}, 441 | {file = "librt-0.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3e84a4121a7ae360ca4da436548a9c1ca8ca134a5ced76c893cc5944426164bd"}, 442 | {file = "librt-0.6.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:05f385a414de3f950886ea0aad8f109650d4b712cf9cc14cc17f5f62a9ab240b"}, 443 | {file = "librt-0.6.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36a8e337461150b05ca2c7bdedb9e591dfc262c5230422cea398e89d0c746cdc"}, 444 | {file = "librt-0.6.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dcbe48f6a03979384f27086484dc2a14959be1613cb173458bd58f714f2c48f3"}, 445 | {file = "librt-0.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4bca9e4c260233fba37b15c4ec2f78aa99c1a79fbf902d19dd4a763c5c3fb751"}, 446 | {file = "librt-0.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:760c25ed6ac968e24803eb5f7deb17ce026902d39865e83036bacbf5cf242aa8"}, 447 | {file = "librt-0.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4aa4a93a353ccff20df6e34fa855ae8fd788832c88f40a9070e3ddd3356a9f0e"}, 448 | {file = "librt-0.6.3-cp314-cp314-win32.whl", hash = "sha256:cb92741c2b4ea63c09609b064b26f7f5d9032b61ae222558c55832ec3ad0bcaf"}, 449 | {file = "librt-0.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:fdcd095b1b812d756fa5452aca93b962cf620694c0cadb192cec2bb77dcca9a2"}, 450 | {file = "librt-0.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:822ca79e28720a76a935c228d37da6579edef048a17cd98d406a2484d10eda78"}, 451 | {file = "librt-0.6.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:078cd77064d1640cb7b0650871a772956066174d92c8aeda188a489b58495179"}, 452 | {file = "librt-0.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5cc22f7f5c0cc50ed69f4b15b9c51d602aabc4500b433aaa2ddd29e578f452f7"}, 453 | {file = "librt-0.6.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:14b345eb7afb61b9fdcdfda6738946bd11b8e0f6be258666b0646af3b9bb5916"}, 454 | {file = "librt-0.6.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d46aa46aa29b067f0b8b84f448fd9719aaf5f4c621cc279164d76a9dc9ab3e8"}, 455 | {file = "librt-0.6.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1b51ba7d9d5d9001494769eca8c0988adce25d0a970c3ba3f2eb9df9d08036fc"}, 456 | {file = "librt-0.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ced0925a18fddcff289ef54386b2fc230c5af3c83b11558571124bfc485b8c07"}, 457 | {file = "librt-0.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6bac97e51f66da2ca012adddbe9fd656b17f7368d439de30898f24b39512f40f"}, 458 | {file = "librt-0.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b2922a0e8fa97395553c304edc3bd36168d8eeec26b92478e292e5d4445c1ef0"}, 459 | {file = "librt-0.6.3-cp314-cp314t-win32.whl", hash = "sha256:f33462b19503ba68d80dac8a1354402675849259fb3ebf53b67de86421735a3a"}, 460 | {file = "librt-0.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:04f8ce401d4f6380cfc42af0f4e67342bf34c820dae01343f58f472dbac75dcf"}, 461 | {file = "librt-0.6.3-cp314-cp314t-win_arm64.whl", hash = "sha256:afb39550205cc5e5c935762c6bf6a2bb34f7d21a68eadb25e2db7bf3593fecc0"}, 462 | {file = "librt-0.6.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:09262cb2445b6f15d09141af20b95bb7030c6f13b00e876ad8fdd1a9045d6aa5"}, 463 | {file = "librt-0.6.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:57705e8eec76c5b77130d729c0f70190a9773366c555c5457c51eace80afd873"}, 464 | {file = "librt-0.6.3-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3ac2a7835434b31def8ed5355dd9b895bbf41642d61967522646d1d8b9681106"}, 465 | {file = "librt-0.6.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71f0a5918aebbea1e7db2179a8fe87e8a8732340d9e8b8107401fb407eda446e"}, 466 | {file = "librt-0.6.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa346e202e6e1ebc01fe1c69509cffe486425884b96cb9ce155c99da1ecbe0e9"}, 467 | {file = "librt-0.6.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:92267f865c7bbd12327a0d394666948b9bf4b51308b52947c0cc453bfa812f5d"}, 468 | {file = "librt-0.6.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:86605d5bac340beb030cbc35859325982a79047ebdfba1e553719c7126a2389d"}, 469 | {file = "librt-0.6.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:98e4bbecbef8d2a60ecf731d735602feee5ac0b32117dbbc765e28b054bac912"}, 470 | {file = "librt-0.6.3-cp39-cp39-win32.whl", hash = "sha256:3caa0634c02d5ff0b2ae4a28052e0d8c5f20d497623dc13f629bd4a9e2a6efad"}, 471 | {file = "librt-0.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:b47395091e7e0ece1e6ebac9b98bf0c9084d1e3d3b2739aa566be7e56e3f7bf2"}, 472 | {file = "librt-0.6.3.tar.gz", hash = "sha256:c724a884e642aa2bbad52bb0203ea40406ad742368a5f90da1b220e970384aae"}, 473 | ] 474 | 475 | [[package]] 476 | name = "markdown-it-py" 477 | version = "4.0.0" 478 | description = "Python port of markdown-it. Markdown parsing, done right!" 479 | optional = false 480 | python-versions = ">=3.10" 481 | groups = ["dev"] 482 | files = [ 483 | {file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"}, 484 | {file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"}, 485 | ] 486 | 487 | [package.dependencies] 488 | mdurl = ">=0.1,<1.0" 489 | 490 | [package.extras] 491 | benchmarking = ["psutil", "pytest", "pytest-benchmark"] 492 | compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "markdown-it-pyrs", "mistletoe (>=1.0,<2.0)", "mistune (>=3.0,<4.0)", "panflute (>=2.3,<3.0)"] 493 | linkify = ["linkify-it-py (>=1,<3)"] 494 | plugins = ["mdit-py-plugins (>=0.5.0)"] 495 | profiling = ["gprof2dot"] 496 | rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"] 497 | testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "requests"] 498 | 499 | [[package]] 500 | name = "mccabe" 501 | version = "0.7.0" 502 | description = "McCabe checker, plugin for flake8" 503 | optional = false 504 | python-versions = ">=3.6" 505 | groups = ["dev"] 506 | files = [ 507 | {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, 508 | {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, 509 | ] 510 | 511 | [[package]] 512 | name = "mdurl" 513 | version = "0.1.2" 514 | description = "Markdown URL utilities" 515 | optional = false 516 | python-versions = ">=3.7" 517 | groups = ["dev"] 518 | files = [ 519 | {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, 520 | {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, 521 | ] 522 | 523 | [[package]] 524 | name = "mutmut" 525 | version = "2.5.1" 526 | description = "mutation testing for Python 3" 527 | optional = false 528 | python-versions = ">=3.7" 529 | groups = ["dev"] 530 | files = [ 531 | {file = "mutmut-2.5.1.tar.gz", hash = "sha256:d8fea2538805277f6290922e88881ad045002fc284d5a53c2b3915298b77f79d"}, 532 | ] 533 | 534 | [package.dependencies] 535 | click = "*" 536 | glob2 = "*" 537 | junit-xml = ">=1.8,<2" 538 | parso = "*" 539 | pony = "*" 540 | toml = "*" 541 | 542 | [package.extras] 543 | coverage = ["coverage"] 544 | patch = ["whatthepatch (==0.0.6)"] 545 | pytest = ["pytest", "pytest-cov"] 546 | 547 | [[package]] 548 | name = "mypy" 549 | version = "1.19.1" 550 | description = "Optional static typing for Python" 551 | optional = false 552 | python-versions = ">=3.9" 553 | groups = ["dev"] 554 | files = [ 555 | {file = "mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec"}, 556 | {file = "mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b"}, 557 | {file = "mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6"}, 558 | {file = "mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74"}, 559 | {file = "mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1"}, 560 | {file = "mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac"}, 561 | {file = "mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288"}, 562 | {file = "mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab"}, 563 | {file = "mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6"}, 564 | {file = "mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331"}, 565 | {file = "mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925"}, 566 | {file = "mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042"}, 567 | {file = "mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1"}, 568 | {file = "mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e"}, 569 | {file = "mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2"}, 570 | {file = "mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8"}, 571 | {file = "mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a"}, 572 | {file = "mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13"}, 573 | {file = "mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250"}, 574 | {file = "mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b"}, 575 | {file = "mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e"}, 576 | {file = "mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef"}, 577 | {file = "mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75"}, 578 | {file = "mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd"}, 579 | {file = "mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1"}, 580 | {file = "mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718"}, 581 | {file = "mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b"}, 582 | {file = "mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045"}, 583 | {file = "mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957"}, 584 | {file = "mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f"}, 585 | {file = "mypy-1.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3"}, 586 | {file = "mypy-1.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a"}, 587 | {file = "mypy-1.19.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67"}, 588 | {file = "mypy-1.19.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e"}, 589 | {file = "mypy-1.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376"}, 590 | {file = "mypy-1.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24"}, 591 | {file = "mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247"}, 592 | {file = "mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba"}, 593 | ] 594 | 595 | [package.dependencies] 596 | librt = {version = ">=0.6.2", markers = "platform_python_implementation != \"PyPy\""} 597 | mypy_extensions = ">=1.0.0" 598 | pathspec = ">=0.9.0" 599 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 600 | typing_extensions = ">=4.6.0" 601 | 602 | [package.extras] 603 | dmypy = ["psutil (>=4.0)"] 604 | faster-cache = ["orjson"] 605 | install-types = ["pip"] 606 | mypyc = ["setuptools (>=50)"] 607 | reports = ["lxml"] 608 | 609 | [[package]] 610 | name = "mypy-extensions" 611 | version = "1.1.0" 612 | description = "Type system extensions for programs checked with the mypy type checker." 613 | optional = false 614 | python-versions = ">=3.8" 615 | groups = ["dev"] 616 | files = [ 617 | {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, 618 | {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, 619 | ] 620 | 621 | [[package]] 622 | name = "packaging" 623 | version = "25.0" 624 | description = "Core utilities for Python packages" 625 | optional = false 626 | python-versions = ">=3.8" 627 | groups = ["dev"] 628 | files = [ 629 | {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, 630 | {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, 631 | ] 632 | 633 | [[package]] 634 | name = "parse" 635 | version = "1.20.2" 636 | description = "parse() is the opposite of format()" 637 | optional = false 638 | python-versions = "*" 639 | groups = ["main"] 640 | files = [ 641 | {file = "parse-1.20.2-py2.py3-none-any.whl", hash = "sha256:967095588cb802add9177d0c0b6133b5ba33b1ea9007ca800e526f42a85af558"}, 642 | {file = "parse-1.20.2.tar.gz", hash = "sha256:b41d604d16503c79d81af5165155c0b20f6c8d6c559efa66b4b695c3e5a0a0ce"}, 643 | ] 644 | 645 | [[package]] 646 | name = "parso" 647 | version = "0.8.5" 648 | description = "A Python Parser" 649 | optional = false 650 | python-versions = ">=3.6" 651 | groups = ["dev"] 652 | files = [ 653 | {file = "parso-0.8.5-py2.py3-none-any.whl", hash = "sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887"}, 654 | {file = "parso-0.8.5.tar.gz", hash = "sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a"}, 655 | ] 656 | 657 | [package.extras] 658 | qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] 659 | testing = ["docopt", "pytest"] 660 | 661 | [[package]] 662 | name = "pathspec" 663 | version = "0.12.1" 664 | description = "Utility library for gitignore style pattern matching of file paths." 665 | optional = false 666 | python-versions = ">=3.8" 667 | groups = ["dev"] 668 | files = [ 669 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, 670 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, 671 | ] 672 | 673 | [[package]] 674 | name = "pluggy" 675 | version = "1.6.0" 676 | description = "plugin and hook calling mechanisms for python" 677 | optional = false 678 | python-versions = ">=3.9" 679 | groups = ["dev"] 680 | files = [ 681 | {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, 682 | {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, 683 | ] 684 | 685 | [package.extras] 686 | dev = ["pre-commit", "tox"] 687 | testing = ["coverage", "pytest", "pytest-benchmark"] 688 | 689 | [[package]] 690 | name = "pony" 691 | version = "0.7.19" 692 | description = "Pony Object-Relational Mapper" 693 | optional = false 694 | python-versions = "*" 695 | groups = ["dev"] 696 | files = [ 697 | {file = "pony-0.7.19-py3-none-any.whl", hash = "sha256:5112b4cf40d3f24e93ae66dc5ab7dc6813388efa870e750928d60dc699873cf5"}, 698 | {file = "pony-0.7.19.tar.gz", hash = "sha256:f7f83b2981893e49f7f18e8def52ad8fa8f8e6c5f9583b9aaed62d4d85036a0f"}, 699 | ] 700 | 701 | [[package]] 702 | name = "pycodestyle" 703 | version = "2.14.0" 704 | description = "Python style guide checker" 705 | optional = false 706 | python-versions = ">=3.9" 707 | groups = ["dev"] 708 | files = [ 709 | {file = "pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d"}, 710 | {file = "pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783"}, 711 | ] 712 | 713 | [[package]] 714 | name = "pyflakes" 715 | version = "3.4.0" 716 | description = "passive checker of Python programs" 717 | optional = false 718 | python-versions = ">=3.9" 719 | groups = ["dev"] 720 | files = [ 721 | {file = "pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f"}, 722 | {file = "pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58"}, 723 | ] 724 | 725 | [[package]] 726 | name = "pygments" 727 | version = "2.19.2" 728 | description = "Pygments is a syntax highlighting package written in Python." 729 | optional = false 730 | python-versions = ">=3.8" 731 | groups = ["dev"] 732 | files = [ 733 | {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, 734 | {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, 735 | ] 736 | 737 | [package.extras] 738 | windows-terminal = ["colorama (>=0.4.6)"] 739 | 740 | [[package]] 741 | name = "pytest" 742 | version = "9.0.2" 743 | description = "pytest: simple powerful testing with Python" 744 | optional = false 745 | python-versions = ">=3.10" 746 | groups = ["dev"] 747 | files = [ 748 | {file = "pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b"}, 749 | {file = "pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11"}, 750 | ] 751 | 752 | [package.dependencies] 753 | colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} 754 | exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} 755 | iniconfig = ">=1.0.1" 756 | packaging = ">=22" 757 | pluggy = ">=1.5,<2" 758 | pygments = ">=2.7.2" 759 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 760 | 761 | [package.extras] 762 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] 763 | 764 | [[package]] 765 | name = "pytest-cov" 766 | version = "7.0.0" 767 | description = "Pytest plugin for measuring coverage." 768 | optional = false 769 | python-versions = ">=3.9" 770 | groups = ["dev"] 771 | files = [ 772 | {file = "pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861"}, 773 | {file = "pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1"}, 774 | ] 775 | 776 | [package.dependencies] 777 | coverage = {version = ">=7.10.6", extras = ["toml"]} 778 | pluggy = ">=1.2" 779 | pytest = ">=7" 780 | 781 | [package.extras] 782 | testing = ["process-tests", "pytest-xdist", "virtualenv"] 783 | 784 | [[package]] 785 | name = "pytest-randomly" 786 | version = "4.0.1" 787 | description = "Pytest plugin to randomly order tests and control random.seed." 788 | optional = false 789 | python-versions = ">=3.9" 790 | groups = ["dev"] 791 | files = [ 792 | {file = "pytest_randomly-4.0.1-py3-none-any.whl", hash = "sha256:e0dfad2fd4f35e07beff1e47c17fbafcf98f9bf4531fd369d9260e2f858bfcb7"}, 793 | {file = "pytest_randomly-4.0.1.tar.gz", hash = "sha256:174e57bb12ac2c26f3578188490bd333f0e80620c3f47340158a86eca0593cd8"}, 794 | ] 795 | 796 | [package.dependencies] 797 | pytest = "*" 798 | 799 | [[package]] 800 | name = "pytz" 801 | version = "2025.2" 802 | description = "World timezone definitions, modern and historical" 803 | optional = false 804 | python-versions = "*" 805 | groups = ["dev"] 806 | files = [ 807 | {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, 808 | {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, 809 | ] 810 | 811 | [[package]] 812 | name = "pyyaml" 813 | version = "6.0.3" 814 | description = "YAML parser and emitter for Python" 815 | optional = false 816 | python-versions = ">=3.8" 817 | groups = ["dev"] 818 | files = [ 819 | {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, 820 | {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, 821 | {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"}, 822 | {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"}, 823 | {file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"}, 824 | {file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"}, 825 | {file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"}, 826 | {file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}, 827 | {file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}, 828 | {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"}, 829 | {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"}, 830 | {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"}, 831 | {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"}, 832 | {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"}, 833 | {file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"}, 834 | {file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"}, 835 | {file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"}, 836 | {file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"}, 837 | {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"}, 838 | {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"}, 839 | {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"}, 840 | {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"}, 841 | {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"}, 842 | {file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"}, 843 | {file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"}, 844 | {file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"}, 845 | {file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"}, 846 | {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"}, 847 | {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"}, 848 | {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"}, 849 | {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"}, 850 | {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"}, 851 | {file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"}, 852 | {file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"}, 853 | {file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"}, 854 | {file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"}, 855 | {file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"}, 856 | {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"}, 857 | {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"}, 858 | {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"}, 859 | {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"}, 860 | {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"}, 861 | {file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"}, 862 | {file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"}, 863 | {file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"}, 864 | {file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"}, 865 | {file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"}, 866 | {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"}, 867 | {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"}, 868 | {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"}, 869 | {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"}, 870 | {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"}, 871 | {file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"}, 872 | {file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"}, 873 | {file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"}, 874 | {file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"}, 875 | {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"}, 876 | {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"}, 877 | {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"}, 878 | {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"}, 879 | {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"}, 880 | {file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"}, 881 | {file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"}, 882 | {file = "pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da"}, 883 | {file = "pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917"}, 884 | {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9"}, 885 | {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5"}, 886 | {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a"}, 887 | {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926"}, 888 | {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7"}, 889 | {file = "pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0"}, 890 | {file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"}, 891 | {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, 892 | ] 893 | 894 | [[package]] 895 | name = "rich" 896 | version = "14.2.0" 897 | description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" 898 | optional = false 899 | python-versions = ">=3.8.0" 900 | groups = ["dev"] 901 | files = [ 902 | {file = "rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd"}, 903 | {file = "rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4"}, 904 | ] 905 | 906 | [package.dependencies] 907 | markdown-it-py = ">=2.2.0" 908 | pygments = ">=2.13.0,<3.0.0" 909 | 910 | [package.extras] 911 | jupyter = ["ipywidgets (>=7.5.1,<9)"] 912 | 913 | [[package]] 914 | name = "ruff" 915 | version = "0.14.10" 916 | description = "An extremely fast Python linter and code formatter, written in Rust." 917 | optional = false 918 | python-versions = ">=3.7" 919 | groups = ["dev"] 920 | files = [ 921 | {file = "ruff-0.14.10-py3-none-linux_armv6l.whl", hash = "sha256:7a3ce585f2ade3e1f29ec1b92df13e3da262178df8c8bdf876f48fa0e8316c49"}, 922 | {file = "ruff-0.14.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:674f9be9372907f7257c51f1d4fc902cb7cf014b9980152b802794317941f08f"}, 923 | {file = "ruff-0.14.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d85713d522348837ef9df8efca33ccb8bd6fcfc86a2cde3ccb4bc9d28a18003d"}, 924 | {file = "ruff-0.14.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6987ebe0501ae4f4308d7d24e2d0fe3d7a98430f5adfd0f1fead050a740a3a77"}, 925 | {file = "ruff-0.14.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16a01dfb7b9e4eee556fbfd5392806b1b8550c9b4a9f6acd3dbe6812b193c70a"}, 926 | {file = "ruff-0.14.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7165d31a925b7a294465fa81be8c12a0e9b60fb02bf177e79067c867e71f8b1f"}, 927 | {file = "ruff-0.14.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c561695675b972effb0c0a45db233f2c816ff3da8dcfbe7dfc7eed625f218935"}, 928 | {file = "ruff-0.14.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bb98fcbbc61725968893682fd4df8966a34611239c9fd07a1f6a07e7103d08e"}, 929 | {file = "ruff-0.14.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f24b47993a9d8cb858429e97bdf8544c78029f09b520af615c1d261bf827001d"}, 930 | {file = "ruff-0.14.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59aabd2e2c4fd614d2862e7939c34a532c04f1084476d6833dddef4afab87e9f"}, 931 | {file = "ruff-0.14.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:213db2b2e44be8625002dbea33bb9c60c66ea2c07c084a00d55732689d697a7f"}, 932 | {file = "ruff-0.14.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b914c40ab64865a17a9a5b67911d14df72346a634527240039eb3bd650e5979d"}, 933 | {file = "ruff-0.14.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1484983559f026788e3a5c07c81ef7d1e97c1c78ed03041a18f75df104c45405"}, 934 | {file = "ruff-0.14.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c70427132db492d25f982fffc8d6c7535cc2fd2c83fc8888f05caaa248521e60"}, 935 | {file = "ruff-0.14.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5bcf45b681e9f1ee6445d317ce1fa9d6cba9a6049542d1c3d5b5958986be8830"}, 936 | {file = "ruff-0.14.10-py3-none-win32.whl", hash = "sha256:104c49fc7ab73f3f3a758039adea978869a918f31b73280db175b43a2d9b51d6"}, 937 | {file = "ruff-0.14.10-py3-none-win_amd64.whl", hash = "sha256:466297bd73638c6bdf06485683e812db1c00c7ac96d4ddd0294a338c62fdc154"}, 938 | {file = "ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6"}, 939 | {file = "ruff-0.14.10.tar.gz", hash = "sha256:9a2e830f075d1a42cd28420d7809ace390832a490ed0966fe373ba288e77aaf4"}, 940 | ] 941 | 942 | [[package]] 943 | name = "shellingham" 944 | version = "1.5.4" 945 | description = "Tool to Detect Surrounding Shell" 946 | optional = false 947 | python-versions = ">=3.7" 948 | groups = ["dev"] 949 | files = [ 950 | {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, 951 | {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, 952 | ] 953 | 954 | [[package]] 955 | name = "six" 956 | version = "1.17.0" 957 | description = "Python 2 and 3 compatibility utilities" 958 | optional = false 959 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 960 | groups = ["dev"] 961 | files = [ 962 | {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, 963 | {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, 964 | ] 965 | 966 | [[package]] 967 | name = "smmap" 968 | version = "5.0.2" 969 | description = "A pure Python implementation of a sliding window memory map manager" 970 | optional = false 971 | python-versions = ">=3.7" 972 | groups = ["main"] 973 | files = [ 974 | {file = "smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e"}, 975 | {file = "smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5"}, 976 | ] 977 | 978 | [[package]] 979 | name = "sniffio" 980 | version = "1.3.1" 981 | description = "Sniff out which async library your code is running under" 982 | optional = false 983 | python-versions = ">=3.7" 984 | groups = ["dev"] 985 | files = [ 986 | {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, 987 | {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, 988 | ] 989 | 990 | [[package]] 991 | name = "toml" 992 | version = "0.10.2" 993 | description = "Python Library for Tom's Obvious, Minimal Language" 994 | optional = false 995 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 996 | groups = ["dev"] 997 | files = [ 998 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 999 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 1000 | ] 1001 | 1002 | [[package]] 1003 | name = "tomli" 1004 | version = "2.3.0" 1005 | description = "A lil' TOML parser" 1006 | optional = false 1007 | python-versions = ">=3.8" 1008 | groups = ["dev"] 1009 | files = [ 1010 | {file = "tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45"}, 1011 | {file = "tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba"}, 1012 | {file = "tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf"}, 1013 | {file = "tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441"}, 1014 | {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845"}, 1015 | {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c"}, 1016 | {file = "tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456"}, 1017 | {file = "tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be"}, 1018 | {file = "tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac"}, 1019 | {file = "tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22"}, 1020 | {file = "tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f"}, 1021 | {file = "tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52"}, 1022 | {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8"}, 1023 | {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6"}, 1024 | {file = "tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876"}, 1025 | {file = "tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878"}, 1026 | {file = "tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b"}, 1027 | {file = "tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae"}, 1028 | {file = "tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b"}, 1029 | {file = "tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf"}, 1030 | {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f"}, 1031 | {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05"}, 1032 | {file = "tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606"}, 1033 | {file = "tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999"}, 1034 | {file = "tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e"}, 1035 | {file = "tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3"}, 1036 | {file = "tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc"}, 1037 | {file = "tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0"}, 1038 | {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879"}, 1039 | {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005"}, 1040 | {file = "tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463"}, 1041 | {file = "tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8"}, 1042 | {file = "tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77"}, 1043 | {file = "tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf"}, 1044 | {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530"}, 1045 | {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b"}, 1046 | {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67"}, 1047 | {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f"}, 1048 | {file = "tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0"}, 1049 | {file = "tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba"}, 1050 | {file = "tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b"}, 1051 | {file = "tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549"}, 1052 | ] 1053 | 1054 | [[package]] 1055 | name = "typer" 1056 | version = "0.20.0" 1057 | description = "Typer, build great CLIs. Easy to code. Based on Python type hints." 1058 | optional = false 1059 | python-versions = ">=3.8" 1060 | groups = ["dev"] 1061 | files = [ 1062 | {file = "typer-0.20.0-py3-none-any.whl", hash = "sha256:5b463df6793ec1dca6213a3cf4c0f03bc6e322ac5e16e13ddd622a889489784a"}, 1063 | {file = "typer-0.20.0.tar.gz", hash = "sha256:1aaf6494031793e4876fb0bacfa6a912b551cf43c1e63c800df8b1a866720c37"}, 1064 | ] 1065 | 1066 | [package.dependencies] 1067 | click = ">=8.0.0" 1068 | rich = ">=10.11.0" 1069 | shellingham = ">=1.3.0" 1070 | typing-extensions = ">=3.7.4.3" 1071 | 1072 | [[package]] 1073 | name = "types-pyyaml" 1074 | version = "6.0.12.20250915" 1075 | description = "Typing stubs for PyYAML" 1076 | optional = false 1077 | python-versions = ">=3.9" 1078 | groups = ["dev"] 1079 | files = [ 1080 | {file = "types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = "sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6"}, 1081 | {file = "types_pyyaml-6.0.12.20250915.tar.gz", hash = "sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3"}, 1082 | ] 1083 | 1084 | [[package]] 1085 | name = "typing-extensions" 1086 | version = "4.15.0" 1087 | description = "Backported and Experimental Type Hints for Python 3.9+" 1088 | optional = false 1089 | python-versions = ">=3.9" 1090 | groups = ["dev"] 1091 | files = [ 1092 | {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, 1093 | {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, 1094 | ] 1095 | 1096 | [[package]] 1097 | name = "wemake-python-styleguide" 1098 | version = "1.4.0" 1099 | description = "The strictest and most opinionated python linter ever" 1100 | optional = false 1101 | python-versions = "<4.0,>=3.10" 1102 | groups = ["dev"] 1103 | files = [ 1104 | {file = "wemake_python_styleguide-1.4.0-py3-none-any.whl", hash = "sha256:c0727475a20a1b7d59f1d806040e84768bdb0935d1147023453aa44c14b65c95"}, 1105 | {file = "wemake_python_styleguide-1.4.0.tar.gz", hash = "sha256:0964cf40ac4d3f1c89dd79aee4b6edba9a1806fb395836c73e746fe287dbae3e"}, 1106 | ] 1107 | 1108 | [package.dependencies] 1109 | attrs = "*" 1110 | flake8 = ">=7.3,<8.0" 1111 | pygments = ">=2.19,<3.0" 1112 | 1113 | [metadata] 1114 | lock-version = "2.1" 1115 | python-versions = ">=3.10,<4.0" 1116 | content-hash = "364ffa5d91cd2eccd31351a90a53322bf983a0e2506e8efcb1a9b6afcb6e0579" 1117 | --------------------------------------------------------------------------------