codespector 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,54 @@
1
+ name: Build and publish package
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ release-build:
12
+ runs-on: ubuntu-latest
13
+
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: "3.12"
20
+
21
+ - name: Build release distributions
22
+ run: |
23
+ python -m pip install --upgrade pip
24
+ python -m pip install uv
25
+ uv build
26
+
27
+ - name: Upload distributions
28
+ uses: actions/upload-artifact@v4
29
+ with:
30
+ name: release-dists
31
+ path: dist/
32
+
33
+ pypi-publish:
34
+ runs-on: ubuntu-latest
35
+ needs:
36
+ - release-build
37
+ permissions:
38
+ id-token: write
39
+
40
+ environment:
41
+ name: pypi
42
+ url: https://pypi.org/p/codespector
43
+
44
+ steps:
45
+ - name: Retrieve release distributions
46
+ uses: actions/download-artifact@v4
47
+ with:
48
+ name: release-dists
49
+ path: dist/
50
+
51
+ - name: Publish release distributions to PyPI
52
+ uses: pypa/gh-action-pypi-publish@release/v1
53
+ with:
54
+ packages-dir: dist/
@@ -0,0 +1,36 @@
1
+ name: tests
2
+
3
+ on:
4
+ push:
5
+ branches: [ "master" ]
6
+ pull_request:
7
+ branches: [ "*" ]
8
+
9
+ jobs:
10
+ build:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ python-version: ["3.12"]
16
+
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - name: Set up Python ${{ matrix.python-version }}
20
+ uses: actions/setup-python@v3
21
+ with:
22
+ python-version: ${{ matrix.python-version }}
23
+ - name: Install dependencies
24
+ run: |
25
+ python -m pip install --upgrade pip
26
+ python -m pip install uv
27
+ uv venv
28
+ source .venv/bin/activate
29
+ uv sync --all-extras
30
+ - name: Lint with ruff
31
+ run: |
32
+ uvx ruff check . --fix
33
+ uvx ruff format . --check
34
+ - name: Test with pytest
35
+ run: |
36
+ uv run pytest -vv
@@ -0,0 +1,114 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ *.egg-info/
24
+ .installed.cfg
25
+ *.egg
26
+ MANIFEST
27
+
28
+ # PyInstaller
29
+ # Usually these files are written by a python script from a template
30
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
31
+ *.manifest
32
+ *.spec
33
+
34
+ # Installer logs
35
+ pip-log.txt
36
+ pip-delete-this-directory.txt
37
+
38
+ # Unit test / coverage reports
39
+ htmlcov/
40
+ .tox/
41
+ .coverage
42
+ .coverage.*
43
+ .cache
44
+ nosetests.xml
45
+ coverage.xml
46
+ *.cover
47
+ .hypothesis/
48
+ .pytest_cache/
49
+
50
+ # Translations
51
+ *.mo
52
+ *.pot
53
+
54
+ # Django stuff:
55
+ *.log
56
+ local_settings.py
57
+ db.sqlite3
58
+
59
+ # Flask stuff:
60
+ instance/
61
+ .webassets-cache
62
+
63
+ # Scrapy stuff:
64
+ .scrapy
65
+
66
+ # Sphinx documentation
67
+ docs/_build/
68
+
69
+ # PyBuilder
70
+ target/
71
+
72
+ # Jupyter Notebook
73
+ .ipynb_checkpoints
74
+
75
+ # pyenv
76
+ .python-version
77
+
78
+ # celery beat schedule file
79
+ celerybeat-schedule
80
+
81
+ # SageMath parsed files
82
+ *.sage.py
83
+
84
+ # Environments
85
+ .env
86
+ .venv
87
+ env/
88
+ venv/
89
+ ENV/
90
+ env.bak/
91
+ venv.bak/
92
+ venv_pp
93
+
94
+ # Spyder project settings
95
+ .spyderproject
96
+ .spyproject
97
+
98
+ #VS code
99
+ .vscode
100
+
101
+ # Rope project settings
102
+ .ropeproject
103
+
104
+ # mkdocs documentation
105
+ /site
106
+
107
+ # mypy
108
+ .mypy_cache/
109
+ .idea
110
+ tmp/
111
+
112
+ # linters
113
+ .flakeheaven_cache
114
+ pip.ini
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Vladimir-Titov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,13 @@
1
+ lint:
2
+ @ruff check . --fix
3
+ @ruff format --check .
4
+
5
+ format:
6
+ @ruff format .
7
+
8
+ fix:
9
+ @ruff check . --fix
10
+ @ruff format .
11
+
12
+ test:
13
+ @pytest tests -vv
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.4
2
+ Name: codespector
3
+ Version: 0.1.0
4
+ Summary: Assistant for reviewing your code
5
+ Project-URL: Repository, https://github.com/Vladimir-Titov/codespector
6
+ Project-URL: Issues, https://github.com/Vladimir-Titov/codespector/issues
7
+ Author-email: vtitov <v.v.titov94@gmail.com>
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Requires-Python: >=3.10
14
+ Requires-Dist: click>=8.1.8
15
+ Requires-Dist: environs>=14.1.1
16
+ Requires-Dist: loguru>=0.7.3
17
+ Requires-Dist: requests>=2.32.3
18
+ Requires-Dist: ujson>=5.10.0
19
+ Description-Content-Type: text/markdown
20
+
21
+ # CodeSpector
22
+
23
+ CodeSpector is a Python package designed to review code changes for quality and security issues using AI chat agents. It supports different chat agents like Codestral and ChatGPT.
24
+
25
+ ## Features
26
+
27
+ - Automated code review using AI chat agents.
28
+ - Supports multiple chat agents and models.
29
+ - Generates detailed review reports in markdown format.
30
+ - Configurable via environment variables and command-line options.
31
+
32
+ ## Installation
33
+
34
+ To install the package, use the following command:
35
+
36
+ ```sh
37
+ pip install codespector
38
+ ```
39
+
40
+ ```sh
41
+ uv add codespector
42
+ ```
43
+
44
+ ## Usage
45
+
46
+ ### Command-Line Interface
47
+
48
+ You can use the `codespector` command to start a code review. Below are the available options:
49
+
50
+ ```sh
51
+ Usage: codespector [OPTIONS]
52
+
53
+ Options:
54
+ --system-content TEXT Content which used in system field for agent
55
+ [default: Ты код ревьювер. Отвечай на русском языке.]
56
+ --output-dir TEXT Select the output directory [default: codespector]
57
+ -b, --compare-branch TEXT Select the branch to compare the current one with
58
+ [default: develop]
59
+ --chat-agent [codestral|chatgpt]
60
+ Choose the chat agent to use [default: codestral]
61
+ --chat-model TEXT Choose the chat model to use
62
+ --chat-token TEXT Chat agent token
63
+ --mode [local] Choose the mode of the application [default: local]
64
+ --version Show the version and exit.
65
+ --help Show this message and exit.
66
+ ```
67
+
68
+ ### Example
69
+
70
+ To run a code review, use the following command:
71
+
72
+ ```sh
73
+ codespector --chat-token YOUR_CHAT_TOKEN --chat-agent codestral --compare-branch develop
74
+ ```
75
+
76
+ ## Configuration
77
+
78
+ You can also configure CodeSpector using environment variables. Create a `.env` file in the root directory of your project with the following content:
79
+
80
+ ```
81
+ CODESPECTOR_SYSTEM_CONTENT=Ты код ревьювер. Отвечай на русском языке.
82
+ CODESPECTOR_OUTPUT_DIR=codespector
83
+ CODESPECTOR_CHAT_AGENT=codestral
84
+ CODESPECTOR_CHAT_MODEL=codestral-latest
85
+ CODESPECTOR_CHAT_TOKEN=YOUR_CHAT_TOKEN
86
+ ```
87
+
88
+ ## Makefile Commands
89
+
90
+ - `lint`: Run linting and formatting checks.
91
+ - `format`: Format the code.
92
+ - `fix`: Fix linting issues and format the code.
93
+ - `test`: Run the tests.
94
+
95
+ ## License
96
+
97
+ This project is licensed under the MIT License. See the `LICENSE` file for more details.
@@ -0,0 +1,77 @@
1
+ # CodeSpector
2
+
3
+ CodeSpector is a Python package designed to review code changes for quality and security issues using AI chat agents. It supports different chat agents like Codestral and ChatGPT.
4
+
5
+ ## Features
6
+
7
+ - Automated code review using AI chat agents.
8
+ - Supports multiple chat agents and models.
9
+ - Generates detailed review reports in markdown format.
10
+ - Configurable via environment variables and command-line options.
11
+
12
+ ## Installation
13
+
14
+ To install the package, use the following command:
15
+
16
+ ```sh
17
+ pip install codespector
18
+ ```
19
+
20
+ ```sh
21
+ uv add codespector
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ### Command-Line Interface
27
+
28
+ You can use the `codespector` command to start a code review. Below are the available options:
29
+
30
+ ```sh
31
+ Usage: codespector [OPTIONS]
32
+
33
+ Options:
34
+ --system-content TEXT Content which used in system field for agent
35
+ [default: Ты код ревьювер. Отвечай на русском языке.]
36
+ --output-dir TEXT Select the output directory [default: codespector]
37
+ -b, --compare-branch TEXT Select the branch to compare the current one with
38
+ [default: develop]
39
+ --chat-agent [codestral|chatgpt]
40
+ Choose the chat agent to use [default: codestral]
41
+ --chat-model TEXT Choose the chat model to use
42
+ --chat-token TEXT Chat agent token
43
+ --mode [local] Choose the mode of the application [default: local]
44
+ --version Show the version and exit.
45
+ --help Show this message and exit.
46
+ ```
47
+
48
+ ### Example
49
+
50
+ To run a code review, use the following command:
51
+
52
+ ```sh
53
+ codespector --chat-token YOUR_CHAT_TOKEN --chat-agent codestral --compare-branch develop
54
+ ```
55
+
56
+ ## Configuration
57
+
58
+ You can also configure CodeSpector using environment variables. Create a `.env` file in the root directory of your project with the following content:
59
+
60
+ ```
61
+ CODESPECTOR_SYSTEM_CONTENT=Ты код ревьювер. Отвечай на русском языке.
62
+ CODESPECTOR_OUTPUT_DIR=codespector
63
+ CODESPECTOR_CHAT_AGENT=codestral
64
+ CODESPECTOR_CHAT_MODEL=codestral-latest
65
+ CODESPECTOR_CHAT_TOKEN=YOUR_CHAT_TOKEN
66
+ ```
67
+
68
+ ## Makefile Commands
69
+
70
+ - `lint`: Run linting and formatting checks.
71
+ - `format`: Format the code.
72
+ - `fix`: Fix linting issues and format the code.
73
+ - `test`: Run the tests.
74
+
75
+ ## License
76
+
77
+ This project is licensed under the MIT License. See the `LICENSE` file for more details.
File without changes
File without changes
@@ -0,0 +1,5 @@
1
+ import ujson
2
+
3
+
4
+ def chat_completion(diff: str):
5
+ pass
@@ -0,0 +1,44 @@
1
+ from codespector import local
2
+ from loguru import logger
3
+
4
+
5
+ class CodeSpectorController:
6
+ __slots__ = (
7
+ 'mode',
8
+ 'chat_token',
9
+ 'chat_agent',
10
+ 'compare_branch',
11
+ 'output_dir',
12
+ 'system_content',
13
+ 'chat_model',
14
+ )
15
+
16
+ def __init__(
17
+ self,
18
+ mode: str,
19
+ chat_token: str,
20
+ chat_agent: str,
21
+ compare_branch: str,
22
+ output_dir: str,
23
+ system_content: str,
24
+ chat_model: str,
25
+ ):
26
+ self.mode = mode
27
+ self.chat_token = chat_token
28
+ self.chat_agent = chat_agent
29
+ self.compare_branch = compare_branch
30
+ self.output_dir = output_dir
31
+ self.system_content = system_content
32
+ self.chat_model = chat_model
33
+
34
+ def start(self):
35
+ codespector = local.LocalCodespector(
36
+ chat_token=self.chat_token,
37
+ chat_agent=self.chat_agent,
38
+ compare_branch=self.compare_branch,
39
+ output_dir=self.output_dir,
40
+ system_content=self.system_content,
41
+ chat_model=self.chat_model,
42
+ )
43
+ codespector.review()
44
+ logger.info('Review completed successfully.See result.txt in {} directory', self.output_dir)
@@ -0,0 +1 @@
1
+ from .main import LocalCodespector
@@ -0,0 +1,35 @@
1
+ from .prepare import CodeSpectorDataPreparer
2
+ from .reviewer import CodeSpectorReviewer
3
+
4
+
5
+ class LocalCodespector:
6
+ def __init__(
7
+ self,
8
+ chat_token: str,
9
+ chat_agent: str,
10
+ compare_branch: str,
11
+ output_dir: str,
12
+ system_content: str,
13
+ chat_model: str,
14
+ ):
15
+ self.chat_token = chat_token
16
+ self.chat_agent = chat_agent
17
+ self.compare_branch = compare_branch
18
+ self.output_dir = output_dir
19
+ self.system_content = system_content
20
+ self.chat_model = chat_model
21
+
22
+ self.data_preparer = CodeSpectorDataPreparer(output_dir=self.output_dir, compare_branch=self.compare_branch)
23
+ self.reviewer = CodeSpectorReviewer(
24
+ diff_file=self.data_preparer.combined_file,
25
+ chat_token=self.chat_token,
26
+ chat_agent=self.chat_agent,
27
+ system_content=self.system_content,
28
+ output_dir=self.output_dir,
29
+ chat_model=self.chat_model,
30
+ )
31
+ self.processes = [self.data_preparer, self.reviewer]
32
+
33
+ def review(self):
34
+ for process in self.processes:
35
+ process.start()
@@ -0,0 +1,81 @@
1
+ import ujson
2
+ import subprocess
3
+ import os
4
+
5
+
6
+ class CodeSpectorDataPreparer:
7
+ def __init__(
8
+ self,
9
+ output_dir: str,
10
+ compare_branch: str,
11
+ ):
12
+ self.output_dir = output_dir
13
+ self.compare_branch = compare_branch
14
+
15
+ self.original_files_tmp = 'original_files_tmp.json'
16
+ self.code_changes_only = 'code_changes_only.txt'
17
+ self.diff_file = 'diff.json'
18
+ self.combined_file = 'combined.json'
19
+
20
+ def _prepare_dir(self):
21
+ if not os.path.exists(self.output_dir):
22
+ os.makedirs(self.output_dir)
23
+
24
+ def _prepare_diff_file(self):
25
+ diff_output = subprocess.run(['git', 'diff', self.compare_branch], stdout=subprocess.PIPE, text=True).stdout
26
+
27
+ filtered_diff = [
28
+ line
29
+ for line in diff_output.splitlines()
30
+ if (line.startswith('+') or line.startswith('-'))
31
+ and not line.startswith('+++')
32
+ and not line.startswith('---')
33
+ ]
34
+
35
+ with open(os.path.join(self.output_dir, self.code_changes_only), 'w', encoding='utf-8') as f:
36
+ f.write('\n'.join(filtered_diff))
37
+
38
+ diff_json = {'diff': '\n'.join(filtered_diff)}
39
+ diff_filepath = os.path.join(self.output_dir, self.diff_file)
40
+ with open(diff_filepath, 'w', encoding='utf-8') as f:
41
+ ujson.dump(diff_json, f, indent=4, ensure_ascii=False)
42
+
43
+ with open(os.path.join(self.output_dir, self.original_files_tmp), 'r', encoding='utf-8') as f:
44
+ original_files_data = ujson.load(f)
45
+
46
+ with open(diff_filepath, 'r', encoding='utf-8') as f:
47
+ diff_data = ujson.load(f)
48
+
49
+ combined_data = {**original_files_data, **diff_data}
50
+
51
+ with open(os.path.join(self.output_dir, self.combined_file), 'w', encoding='utf-8') as f:
52
+ ujson.dump(combined_data, f, indent=4, ensure_ascii=False)
53
+
54
+ def _prepare_name_only_file(self):
55
+ changed_files = subprocess.run(
56
+ ['git', 'diff', '--name-only', self.compare_branch], stdout=subprocess.PIPE, text=True
57
+ ).stdout.splitlines()
58
+
59
+ result = {'original files': []}
60
+
61
+ for file in changed_files:
62
+ if not file.endswith('.py'):
63
+ continue
64
+
65
+ if os.path.isfile(file):
66
+ with open(file, 'r', encoding='utf-8') as f:
67
+ content = f.read()
68
+ result['original files'].append({'filename': file, 'content': content})
69
+
70
+ filepath = os.path.join(self.output_dir, self.original_files_tmp)
71
+
72
+ with open(filepath, 'w', encoding='utf-8') as f:
73
+ ujson.dump(result, f, indent=4, ensure_ascii=False)
74
+
75
+ def prepare_data(self) -> str:
76
+ self._prepare_dir()
77
+ self._prepare_name_only_file()
78
+ self._prepare_diff_file()
79
+
80
+ def start(self):
81
+ self.prepare_data()
@@ -0,0 +1,109 @@
1
+ import os.path
2
+ from dataclasses import dataclass
3
+ from typing import Self
4
+
5
+ import ujson
6
+ import requests
7
+
8
+ from loguru import logger
9
+
10
+ AGENT_URL_MAPPING = {
11
+ 'codestral': 'https://api.mistral.ai/v1/chat/completions',
12
+ 'chatgpt': 'https://api.openai.com/v1/chat/completions',
13
+ }
14
+
15
+ DEFAULT_AGENT_MODEL = {'codestral': 'codestral-latest', 'chatgpt': 'gpt-4o'}
16
+
17
+
18
+ @dataclass
19
+ class AgentInfo:
20
+ model: str
21
+ url: str
22
+ headers: dict
23
+
24
+ @classmethod
25
+ def create(cls, chat_agent: str, chat_token: str, chat_model: str | None = None) -> Self:
26
+ url = AGENT_URL_MAPPING[chat_agent]
27
+ model = chat_model if chat_model else DEFAULT_AGENT_MODEL[chat_agent]
28
+ headers = {'Authorization': f'Bearer {chat_token}'}
29
+ return cls(
30
+ url=url,
31
+ model=model,
32
+ headers=headers,
33
+ )
34
+
35
+
36
+ class CodeSpectorReviewer:
37
+ def __init__(
38
+ self,
39
+ diff_file: str,
40
+ chat_token: str,
41
+ chat_agent: str,
42
+ chat_model: str | None,
43
+ system_content: str,
44
+ output_dir: str,
45
+ ):
46
+ self.diff_file = diff_file
47
+ self.chat_token = chat_token
48
+ self.chat_agent = chat_agent
49
+ self.chat_model = chat_model
50
+ self.system_content = system_content
51
+ self.output_dir = output_dir
52
+
53
+ self.request_file = 'request.json'
54
+ self.response_file = 'response.json'
55
+ self.result_file = 'result.txt'
56
+
57
+ def _request_to_chat_agent(self, prompt: str):
58
+ agent_info = AgentInfo.create(self.chat_agent, self.chat_token, self.chat_model)
59
+ request_data = {
60
+ 'model': agent_info.model,
61
+ 'messages': [{'role': 'system', 'content': self.system_content}, {'role': 'user', 'content': prompt}],
62
+ }
63
+
64
+ with open(os.path.join(self.output_dir, self.request_file), 'w', encoding='utf-8') as f:
65
+ ujson.dump(request_data, f, indent=4, ensure_ascii=False)
66
+
67
+ response = requests.post(
68
+ agent_info.url,
69
+ json=request_data,
70
+ headers=agent_info.headers,
71
+ timeout=100,
72
+ )
73
+ response.raise_for_status()
74
+ return response
75
+
76
+ def send_to_review(self):
77
+ with open(os.path.join(self.output_dir, self.diff_file), 'r', encoding='utf-8') as f:
78
+ diff_data = ujson.load(f)
79
+
80
+ diff_content = diff_data.get('diff', '')
81
+ original_files = diff_data.get('original files', [])
82
+
83
+ original_files_str = ujson.dumps(original_files, indent=4, ensure_ascii=False)
84
+
85
+ prompt = (
86
+ 'Пожалуйста, проверь следующие изменения в коде на наличие очевидных проблем с качеством или безопасностью. '
87
+ 'Предоставь краткий отчет в формате markdown:\n\n'
88
+ 'DIFF:\n'
89
+ f'{diff_content}\n\n'
90
+ 'ORIGINAL FILES:\n'
91
+ f'{original_files_str}'
92
+ )
93
+ try:
94
+ response = self._request_to_chat_agent(prompt=prompt)
95
+ except Exception as e:
96
+ logger.error('Error while send request: {}', e)
97
+ raise e
98
+
99
+ with open(os.path.join(self.output_dir, self.response_file), 'w', encoding='utf-8') as f:
100
+ ujson.dump(response.json(), f, indent=4, ensure_ascii=False)
101
+
102
+ resp = response.json()
103
+ clear_response = resp['choices'][0]['message']['content']
104
+
105
+ with open(os.path.join(self.output_dir, self.result_file), 'w', encoding='utf-8') as f:
106
+ f.write(clear_response)
107
+
108
+ def start(self):
109
+ self.send_to_review()
@@ -0,0 +1,81 @@
1
+ from pathlib import Path
2
+
3
+ import click
4
+ from environs import Env
5
+
6
+ from .controller import CodeSpectorController
7
+ from loguru import logger
8
+
9
+ BASE_PATH = Path(__file__).parent.parent
10
+
11
+ env = Env()
12
+ env.read_env(path=str(BASE_PATH / '.env'))
13
+
14
+
15
+ @click.option(
16
+ '--system-content',
17
+ type=str,
18
+ default='Ты код ревьювер. Отвечай на русском языке.',
19
+ envvar='CODESPECTOR_SYSTEM_CONTENT',
20
+ show_envvar=True,
21
+ help='Content which used in system field for agent',
22
+ )
23
+ @click.option(
24
+ '--output-dir',
25
+ type=str,
26
+ default='codespector',
27
+ envvar='CODESPECTOR_OUTPUT_DIR',
28
+ show_envvar=True,
29
+ help='Select the output directory',
30
+ )
31
+ @click.option(
32
+ '-b',
33
+ '--compare-branch',
34
+ type=str,
35
+ default='develop',
36
+ help='Select the branch to compare the current one with',
37
+ )
38
+ @click.option(
39
+ '--chat-agent',
40
+ type=click.Choice(['codestral', 'chatgpt'], case_sensitive=False),
41
+ envvar='CODESPECTOR_CHAT_AGENT',
42
+ show_envvar=True,
43
+ default='codestral',
44
+ help='Choose the chat agent to use',
45
+ )
46
+ @click.option(
47
+ '--chat-model',
48
+ type=str,
49
+ envvar='CODESPECTOR_CHAT_MODEL',
50
+ show_envvar=True,
51
+ help='Choose the chat model to use',
52
+ )
53
+ @click.option(
54
+ '--chat-token',
55
+ type=str,
56
+ envvar='CODESPECTOR_CHAT_TOKEN',
57
+ show_envvar=True,
58
+ )
59
+ @click.option(
60
+ '--mode',
61
+ type=click.Choice(['local'], case_sensitive=False),
62
+ default='local',
63
+ help='Choose the mode of the application',
64
+ )
65
+ @click.version_option(message='%(version)s')
66
+ @click.command()
67
+ def main(*args, **kwargs):
68
+ return start(*args, **kwargs)
69
+
70
+
71
+ def start(*args, **kwargs):
72
+ codespector = CodeSpectorController(*args, **kwargs)
73
+ try:
74
+ codespector.start()
75
+ logger.info('Review completed successfully.See result.txt in {} directory', kwargs['output_dir'])
76
+ except Exception as e:
77
+ logger.error('Error while review: {}', e)
78
+
79
+
80
+ if __name__ == '__main__':
81
+ main()
@@ -0,0 +1,56 @@
1
+ [project]
2
+ name = "codespector"
3
+ version = "0.1.0"
4
+ authors = [
5
+ { name = "vtitov", email = "v.v.titov94@gmail.com" }
6
+ ]
7
+ description = "Assistant for reviewing your code"
8
+ readme = "README.md"
9
+ requires-python = ">=3.10"
10
+ dependencies = [
11
+ "click>=8.1.8",
12
+ "environs>=14.1.1",
13
+ "loguru>=0.7.3",
14
+ "requests>=2.32.3",
15
+ "ujson>=5.10.0",
16
+ ]
17
+
18
+ classifiers = [
19
+ "Development Status :: 5 - Production/Stable",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ ]
24
+
25
+ [build-system]
26
+ requires = ["hatchling"]
27
+ build-backend = "hatchling.build"
28
+
29
+ [project.urls]
30
+ Repository = "https://github.com/Vladimir-Titov/codespector"
31
+ Issues = "https://github.com/Vladimir-Titov/codespector/issues"
32
+
33
+
34
+ [project.scripts]
35
+ codespector = "codespector.main:main"
36
+
37
+
38
+ [tool.ruff]
39
+ line-length = 120
40
+ indent-width = 4
41
+
42
+ [tool.ruff.lint]
43
+ ignore = ["F401"]
44
+
45
+ [tool.ruff.format]
46
+ quote-style = "single"
47
+ indent-style = "space"
48
+ skip-magic-trailing-comma = false
49
+ line-ending = "auto"
50
+
51
+ [dependency-groups]
52
+ dev = [
53
+ "pytest>=8.3.5",
54
+ "ruff>=0.11.0",
55
+ ]
56
+
File without changes
File without changes
File without changes
@@ -0,0 +1,26 @@
1
+ import pytest
2
+ from unittest.mock import patch
3
+ from codespector.local.prepare import CodeSpectorDataPreparer
4
+
5
+
6
+ @pytest.fixture
7
+ def preparer():
8
+ return CodeSpectorDataPreparer(output_dir='test_output', compare_branch='develop')
9
+
10
+
11
+ def test_prepare_dir(preparer):
12
+ with patch('os.makedirs') as mock_makedirs, patch('os.path.exists', return_value=False):
13
+ preparer._prepare_dir()
14
+ mock_makedirs.assert_called_once_with('test_output')
15
+
16
+
17
+ def test_prepare_data(preparer):
18
+ with (
19
+ patch.object(preparer, '_prepare_dir') as mock_prepare_dir,
20
+ patch.object(preparer, '_prepare_name_only_file') as mock_prepare_name_only_file,
21
+ patch.object(preparer, '_prepare_diff_file') as mock_prepare_diff_file,
22
+ ):
23
+ preparer.prepare_data()
24
+ mock_prepare_dir.assert_called_once()
25
+ mock_prepare_name_only_file.assert_called_once()
26
+ mock_prepare_diff_file.assert_called_once()
@@ -0,0 +1,74 @@
1
+ import pytest
2
+ from unittest.mock import patch, mock_open
3
+ from codespector.local.reviewer import CodeSpectorReviewer
4
+ import os
5
+ import ujson
6
+ from unittest.mock import MagicMock
7
+
8
+
9
+ @pytest.fixture
10
+ def reviewer():
11
+ return CodeSpectorReviewer(
12
+ diff_file='diff.json',
13
+ chat_token='test_token',
14
+ chat_agent='codestral',
15
+ chat_model=None,
16
+ system_content='Test system content',
17
+ output_dir='test_output',
18
+ )
19
+
20
+
21
+ def test_request_to_chat_agent(reviewer):
22
+ prompt = 'Test prompt'
23
+ mock_response = MagicMock()
24
+ mock_response.json.return_value = {'choices': [{'message': {'content': 'Test response'}}]}
25
+
26
+ with (
27
+ patch('requests.post', return_value=mock_response) as mock_post,
28
+ patch('builtins.open', mock_open()) as mock_file,
29
+ ):
30
+ response = reviewer._request_to_chat_agent(prompt)
31
+
32
+ mock_post.assert_called_once_with(
33
+ 'https://api.mistral.ai/v1/chat/completions',
34
+ json={
35
+ 'model': 'codestral-latest',
36
+ 'messages': [{'role': 'system', 'content': 'Test system content'}, {'role': 'user', 'content': prompt}],
37
+ },
38
+ headers={'Authorization': 'Bearer test_token'},
39
+ timeout=100,
40
+ )
41
+ mock_file.assert_called_with(os.path.join('test_output', 'request.json'), 'w', encoding='utf-8')
42
+ assert response == mock_response
43
+
44
+
45
+ # tests/unit/test_reviewer.py
46
+ def test_send_to_review(reviewer):
47
+ diff_data = {'diff': 'Test diff', 'original files': ['file1.py', 'file2.py']}
48
+ mock_response = MagicMock()
49
+ mock_response.json.return_value = {'choices': [{'message': {'content': 'Test response'}}]}
50
+
51
+ with (
52
+ patch('builtins.open', mock_open(read_data=ujson.dumps(diff_data))) as mock_file,
53
+ patch.object(reviewer, '_request_to_chat_agent', return_value=mock_response) as mock_request,
54
+ patch('ujson.dump') as _,
55
+ ):
56
+ reviewer.send_to_review()
57
+
58
+ mock_file.assert_any_call(os.path.join('test_output', 'diff.json'), 'r', encoding='utf-8')
59
+ mock_request.assert_called_once_with(
60
+ prompt='Пожалуйста, проверь следующие изменения в коде на наличие очевидных проблем с качеством или безопасностью. '
61
+ 'Предоставь краткий отчет в формате markdown:\n\n'
62
+ 'DIFF:\n'
63
+ 'Test diff\n\n'
64
+ 'ORIGINAL FILES:\n'
65
+ '[\n "file1.py",\n "file2.py"\n]'
66
+ )
67
+ mock_file.assert_any_call(os.path.join('test_output', 'response.json'), 'w', encoding='utf-8')
68
+ mock_file.assert_any_call(os.path.join('test_output', 'result.txt'), 'w', encoding='utf-8')
69
+
70
+
71
+ def test_start(reviewer):
72
+ with patch.object(reviewer, 'send_to_review') as mock_send_to_review:
73
+ reviewer.start()
74
+ mock_send_to_review.assert_called_once()
@@ -0,0 +1,284 @@
1
+ version = 1
2
+ requires-python = ">=3.12"
3
+
4
+ [[package]]
5
+ name = "certifi"
6
+ version = "2025.1.31"
7
+ source = { registry = "https://pypi.org/simple" }
8
+ sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 }
9
+ wheels = [
10
+ { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 },
11
+ ]
12
+
13
+ [[package]]
14
+ name = "charset-normalizer"
15
+ version = "3.4.1"
16
+ source = { registry = "https://pypi.org/simple" }
17
+ sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 }
18
+ wheels = [
19
+ { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 },
20
+ { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 },
21
+ { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 },
22
+ { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 },
23
+ { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 },
24
+ { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 },
25
+ { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 },
26
+ { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 },
27
+ { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 },
28
+ { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 },
29
+ { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 },
30
+ { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 },
31
+ { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 },
32
+ { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 },
33
+ { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 },
34
+ { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 },
35
+ { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 },
36
+ { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 },
37
+ { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 },
38
+ { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 },
39
+ { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 },
40
+ { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 },
41
+ { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 },
42
+ { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 },
43
+ { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 },
44
+ { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 },
45
+ { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 },
46
+ ]
47
+
48
+ [[package]]
49
+ name = "click"
50
+ version = "8.1.8"
51
+ source = { registry = "https://pypi.org/simple" }
52
+ dependencies = [
53
+ { name = "colorama", marker = "sys_platform == 'win32'" },
54
+ ]
55
+ sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
56
+ wheels = [
57
+ { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 },
58
+ ]
59
+
60
+ [[package]]
61
+ name = "codespector"
62
+ version = "0.1.0"
63
+ source = { editable = "." }
64
+ dependencies = [
65
+ { name = "click" },
66
+ { name = "environs" },
67
+ { name = "loguru" },
68
+ { name = "requests" },
69
+ { name = "ujson" },
70
+ ]
71
+
72
+ [package.dev-dependencies]
73
+ dev = [
74
+ { name = "pytest" },
75
+ { name = "ruff" },
76
+ ]
77
+
78
+ [package.metadata]
79
+ requires-dist = [
80
+ { name = "click", specifier = ">=8.1.8" },
81
+ { name = "environs", specifier = ">=14.1.1" },
82
+ { name = "loguru", specifier = ">=0.7.3" },
83
+ { name = "requests", specifier = ">=2.32.3" },
84
+ { name = "ujson", specifier = ">=5.10.0" },
85
+ ]
86
+
87
+ [package.metadata.requires-dev]
88
+ dev = [
89
+ { name = "pytest", specifier = ">=8.3.5" },
90
+ { name = "ruff", specifier = ">=0.11.0" },
91
+ ]
92
+
93
+ [[package]]
94
+ name = "colorama"
95
+ version = "0.4.6"
96
+ source = { registry = "https://pypi.org/simple" }
97
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
98
+ wheels = [
99
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
100
+ ]
101
+
102
+ [[package]]
103
+ name = "environs"
104
+ version = "14.1.1"
105
+ source = { registry = "https://pypi.org/simple" }
106
+ dependencies = [
107
+ { name = "marshmallow" },
108
+ { name = "python-dotenv" },
109
+ ]
110
+ sdist = { url = "https://files.pythonhosted.org/packages/31/d3/e82bdbb8cc332e751f67a3f668c5d134d57f983497d9f3a59a375b6e8fd8/environs-14.1.1.tar.gz", hash = "sha256:03db7ee2d50ec697b68814cd175a3a05a7c7954804e4e419ca8b570dc5a835cf", size = 32050 }
111
+ wheels = [
112
+ { url = "https://files.pythonhosted.org/packages/f4/1c/ab9752f02d32d981d647c05822be9ff93809be8953dacea2da2bec9a9de9/environs-14.1.1-py3-none-any.whl", hash = "sha256:45bc56f1d53bbc59d8dd69bba97377dd88ec28b8229d81cedbd455b21789445b", size = 15566 },
113
+ ]
114
+
115
+ [[package]]
116
+ name = "idna"
117
+ version = "3.10"
118
+ source = { registry = "https://pypi.org/simple" }
119
+ sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
120
+ wheels = [
121
+ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
122
+ ]
123
+
124
+ [[package]]
125
+ name = "iniconfig"
126
+ version = "2.1.0"
127
+ source = { registry = "https://pypi.org/simple" }
128
+ sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 }
129
+ wheels = [
130
+ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 },
131
+ ]
132
+
133
+ [[package]]
134
+ name = "loguru"
135
+ version = "0.7.3"
136
+ source = { registry = "https://pypi.org/simple" }
137
+ dependencies = [
138
+ { name = "colorama", marker = "sys_platform == 'win32'" },
139
+ { name = "win32-setctime", marker = "sys_platform == 'win32'" },
140
+ ]
141
+ sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559 }
142
+ wheels = [
143
+ { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595 },
144
+ ]
145
+
146
+ [[package]]
147
+ name = "marshmallow"
148
+ version = "3.26.1"
149
+ source = { registry = "https://pypi.org/simple" }
150
+ dependencies = [
151
+ { name = "packaging" },
152
+ ]
153
+ sdist = { url = "https://files.pythonhosted.org/packages/ab/5e/5e53d26b42ab75491cda89b871dab9e97c840bf12c63ec58a1919710cd06/marshmallow-3.26.1.tar.gz", hash = "sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6", size = 221825 }
154
+ wheels = [
155
+ { url = "https://files.pythonhosted.org/packages/34/75/51952c7b2d3873b44a0028b1bd26a25078c18f92f256608e8d1dc61b39fd/marshmallow-3.26.1-py3-none-any.whl", hash = "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c", size = 50878 },
156
+ ]
157
+
158
+ [[package]]
159
+ name = "packaging"
160
+ version = "24.2"
161
+ source = { registry = "https://pypi.org/simple" }
162
+ sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
163
+ wheels = [
164
+ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
165
+ ]
166
+
167
+ [[package]]
168
+ name = "pluggy"
169
+ version = "1.5.0"
170
+ source = { registry = "https://pypi.org/simple" }
171
+ sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 }
172
+ wheels = [
173
+ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 },
174
+ ]
175
+
176
+ [[package]]
177
+ name = "pytest"
178
+ version = "8.3.5"
179
+ source = { registry = "https://pypi.org/simple" }
180
+ dependencies = [
181
+ { name = "colorama", marker = "sys_platform == 'win32'" },
182
+ { name = "iniconfig" },
183
+ { name = "packaging" },
184
+ { name = "pluggy" },
185
+ ]
186
+ sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 }
187
+ wheels = [
188
+ { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 },
189
+ ]
190
+
191
+ [[package]]
192
+ name = "python-dotenv"
193
+ version = "1.0.1"
194
+ source = { registry = "https://pypi.org/simple" }
195
+ sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 }
196
+ wheels = [
197
+ { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 },
198
+ ]
199
+
200
+ [[package]]
201
+ name = "requests"
202
+ version = "2.32.3"
203
+ source = { registry = "https://pypi.org/simple" }
204
+ dependencies = [
205
+ { name = "certifi" },
206
+ { name = "charset-normalizer" },
207
+ { name = "idna" },
208
+ { name = "urllib3" },
209
+ ]
210
+ sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 }
211
+ wheels = [
212
+ { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
213
+ ]
214
+
215
+ [[package]]
216
+ name = "ruff"
217
+ version = "0.11.0"
218
+ source = { registry = "https://pypi.org/simple" }
219
+ sdist = { url = "https://files.pythonhosted.org/packages/77/2b/7ca27e854d92df5e681e6527dc0f9254c9dc06c8408317893cf96c851cdd/ruff-0.11.0.tar.gz", hash = "sha256:e55c620690a4a7ee6f1cccb256ec2157dc597d109400ae75bbf944fc9d6462e2", size = 3799407 }
220
+ wheels = [
221
+ { url = "https://files.pythonhosted.org/packages/48/40/3d0340a9e5edc77d37852c0cd98c5985a5a8081fc3befaeb2ae90aaafd2b/ruff-0.11.0-py3-none-linux_armv6l.whl", hash = "sha256:dc67e32bc3b29557513eb7eeabb23efdb25753684b913bebb8a0c62495095acb", size = 10098158 },
222
+ { url = "https://files.pythonhosted.org/packages/ec/a9/d8f5abb3b87b973b007649ac7bf63665a05b2ae2b2af39217b09f52abbbf/ruff-0.11.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:38c23fd9bdec4eb437b4c1e3595905a0a8edfccd63a790f818b28c78fe345639", size = 10879071 },
223
+ { url = "https://files.pythonhosted.org/packages/ab/62/aaa198614c6211677913ec480415c5e6509586d7b796356cec73a2f8a3e6/ruff-0.11.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7c8661b0be91a38bd56db593e9331beaf9064a79028adee2d5f392674bbc5e88", size = 10247944 },
224
+ { url = "https://files.pythonhosted.org/packages/9f/52/59e0a9f2cf1ce5e6cbe336b6dd0144725c8ea3b97cac60688f4e7880bf13/ruff-0.11.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6c0e8d3d2db7e9f6efd884f44b8dc542d5b6b590fc4bb334fdbc624d93a29a2", size = 10421725 },
225
+ { url = "https://files.pythonhosted.org/packages/a6/c3/dcd71acc6dff72ce66d13f4be5bca1dbed4db678dff2f0f6f307b04e5c02/ruff-0.11.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c3156d3f4b42e57247275a0a7e15a851c165a4fc89c5e8fa30ea6da4f7407b8", size = 9954435 },
226
+ { url = "https://files.pythonhosted.org/packages/a6/9a/342d336c7c52dbd136dee97d4c7797e66c3f92df804f8f3b30da59b92e9c/ruff-0.11.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:490b1e147c1260545f6d041c4092483e3f6d8eba81dc2875eaebcf9140b53905", size = 11492664 },
227
+ { url = "https://files.pythonhosted.org/packages/84/35/6e7defd2d7ca95cc385ac1bd9f7f2e4a61b9cc35d60a263aebc8e590c462/ruff-0.11.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1bc09a7419e09662983b1312f6fa5dab829d6ab5d11f18c3760be7ca521c9329", size = 12207856 },
228
+ { url = "https://files.pythonhosted.org/packages/22/78/da669c8731bacf40001c880ada6d31bcfb81f89cc996230c3b80d319993e/ruff-0.11.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcfa478daf61ac8002214eb2ca5f3e9365048506a9d52b11bea3ecea822bb844", size = 11645156 },
229
+ { url = "https://files.pythonhosted.org/packages/ee/47/e27d17d83530a208f4a9ab2e94f758574a04c51e492aa58f91a3ed7cbbcb/ruff-0.11.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6fbb2aed66fe742a6a3a0075ed467a459b7cedc5ae01008340075909d819df1e", size = 13884167 },
230
+ { url = "https://files.pythonhosted.org/packages/9f/5e/42ffbb0a5d4b07bbc642b7d58357b4e19a0f4774275ca6ca7d1f7b5452cd/ruff-0.11.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92c0c1ff014351c0b0cdfdb1e35fa83b780f1e065667167bb9502d47ca41e6db", size = 11348311 },
231
+ { url = "https://files.pythonhosted.org/packages/c8/51/dc3ce0c5ce1a586727a3444a32f98b83ba99599bb1ebca29d9302886e87f/ruff-0.11.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e4fd5ff5de5f83e0458a138e8a869c7c5e907541aec32b707f57cf9a5e124445", size = 10305039 },
232
+ { url = "https://files.pythonhosted.org/packages/60/e0/475f0c2f26280f46f2d6d1df1ba96b3399e0234cf368cc4c88e6ad10dcd9/ruff-0.11.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:96bc89a5c5fd21a04939773f9e0e276308be0935de06845110f43fd5c2e4ead7", size = 9937939 },
233
+ { url = "https://files.pythonhosted.org/packages/e2/d3/3e61b7fd3e9cdd1e5b8c7ac188bec12975c824e51c5cd3d64caf81b0331e/ruff-0.11.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a9352b9d767889ec5df1483f94870564e8102d4d7e99da52ebf564b882cdc2c7", size = 10923259 },
234
+ { url = "https://files.pythonhosted.org/packages/30/32/cd74149ebb40b62ddd14bd2d1842149aeb7f74191fb0f49bd45c76909ff2/ruff-0.11.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:049a191969a10897fe052ef9cc7491b3ef6de79acd7790af7d7897b7a9bfbcb6", size = 11406212 },
235
+ { url = "https://files.pythonhosted.org/packages/00/ef/033022a6b104be32e899b00de704d7c6d1723a54d4c9e09d147368f14b62/ruff-0.11.0-py3-none-win32.whl", hash = "sha256:3191e9116b6b5bbe187447656f0c8526f0d36b6fd89ad78ccaad6bdc2fad7df2", size = 10310905 },
236
+ { url = "https://files.pythonhosted.org/packages/ed/8a/163f2e78c37757d035bd56cd60c8d96312904ca4a6deeab8442d7b3cbf89/ruff-0.11.0-py3-none-win_amd64.whl", hash = "sha256:c58bfa00e740ca0a6c43d41fb004cd22d165302f360aaa56f7126d544db31a21", size = 11411730 },
237
+ { url = "https://files.pythonhosted.org/packages/4e/f7/096f6efabe69b49d7ca61052fc70289c05d8d35735c137ef5ba5ef423662/ruff-0.11.0-py3-none-win_arm64.whl", hash = "sha256:868364fc23f5aa122b00c6f794211e85f7e78f5dffdf7c590ab90b8c4e69b657", size = 10538956 },
238
+ ]
239
+
240
+ [[package]]
241
+ name = "ujson"
242
+ version = "5.10.0"
243
+ source = { registry = "https://pypi.org/simple" }
244
+ sdist = { url = "https://files.pythonhosted.org/packages/f0/00/3110fd566786bfa542adb7932d62035e0c0ef662a8ff6544b6643b3d6fd7/ujson-5.10.0.tar.gz", hash = "sha256:b3cd8f3c5d8c7738257f1018880444f7b7d9b66232c64649f562d7ba86ad4bc1", size = 7154885 }
245
+ wheels = [
246
+ { url = "https://files.pythonhosted.org/packages/e8/a6/fd3f8bbd80842267e2d06c3583279555e8354c5986c952385199d57a5b6c/ujson-5.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98ba15d8cbc481ce55695beee9f063189dce91a4b08bc1d03e7f0152cd4bbdd5", size = 55642 },
247
+ { url = "https://files.pythonhosted.org/packages/a8/47/dd03fd2b5ae727e16d5d18919b383959c6d269c7b948a380fdd879518640/ujson-5.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9d2edbf1556e4f56e50fab7d8ff993dbad7f54bac68eacdd27a8f55f433578e", size = 51807 },
248
+ { url = "https://files.pythonhosted.org/packages/25/23/079a4cc6fd7e2655a473ed9e776ddbb7144e27f04e8fc484a0fb45fe6f71/ujson-5.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6627029ae4f52d0e1a2451768c2c37c0c814ffc04f796eb36244cf16b8e57043", size = 51972 },
249
+ { url = "https://files.pythonhosted.org/packages/04/81/668707e5f2177791869b624be4c06fb2473bf97ee33296b18d1cf3092af7/ujson-5.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8ccb77b3e40b151e20519c6ae6d89bfe3f4c14e8e210d910287f778368bb3d1", size = 53686 },
250
+ { url = "https://files.pythonhosted.org/packages/bd/50/056d518a386d80aaf4505ccf3cee1c40d312a46901ed494d5711dd939bc3/ujson-5.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3caf9cd64abfeb11a3b661329085c5e167abbe15256b3b68cb5d914ba7396f3", size = 58591 },
251
+ { url = "https://files.pythonhosted.org/packages/fc/d6/aeaf3e2d6fb1f4cfb6bf25f454d60490ed8146ddc0600fae44bfe7eb5a72/ujson-5.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6e32abdce572e3a8c3d02c886c704a38a1b015a1fb858004e03d20ca7cecbb21", size = 997853 },
252
+ { url = "https://files.pythonhosted.org/packages/f8/d5/1f2a5d2699f447f7d990334ca96e90065ea7f99b142ce96e85f26d7e78e2/ujson-5.10.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a65b6af4d903103ee7b6f4f5b85f1bfd0c90ba4eeac6421aae436c9988aa64a2", size = 1140689 },
253
+ { url = "https://files.pythonhosted.org/packages/f2/2c/6990f4ccb41ed93744aaaa3786394bca0875503f97690622f3cafc0adfde/ujson-5.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:604a046d966457b6cdcacc5aa2ec5314f0e8c42bae52842c1e6fa02ea4bda42e", size = 1043576 },
254
+ { url = "https://files.pythonhosted.org/packages/14/f5/a2368463dbb09fbdbf6a696062d0c0f62e4ae6fa65f38f829611da2e8fdd/ujson-5.10.0-cp312-cp312-win32.whl", hash = "sha256:6dea1c8b4fc921bf78a8ff00bbd2bfe166345f5536c510671bccececb187c80e", size = 38764 },
255
+ { url = "https://files.pythonhosted.org/packages/59/2d/691f741ffd72b6c84438a93749ac57bf1a3f217ac4b0ea4fd0e96119e118/ujson-5.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:38665e7d8290188b1e0d57d584eb8110951a9591363316dd41cf8686ab1d0abc", size = 42211 },
256
+ { url = "https://files.pythonhosted.org/packages/0d/69/b3e3f924bb0e8820bb46671979770c5be6a7d51c77a66324cdb09f1acddb/ujson-5.10.0-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:618efd84dc1acbd6bff8eaa736bb6c074bfa8b8a98f55b61c38d4ca2c1f7f287", size = 55646 },
257
+ { url = "https://files.pythonhosted.org/packages/32/8a/9b748eb543c6cabc54ebeaa1f28035b1bd09c0800235b08e85990734c41e/ujson-5.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38d5d36b4aedfe81dfe251f76c0467399d575d1395a1755de391e58985ab1c2e", size = 51806 },
258
+ { url = "https://files.pythonhosted.org/packages/39/50/4b53ea234413b710a18b305f465b328e306ba9592e13a791a6a6b378869b/ujson-5.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67079b1f9fb29ed9a2914acf4ef6c02844b3153913eb735d4bf287ee1db6e557", size = 51975 },
259
+ { url = "https://files.pythonhosted.org/packages/b4/9d/8061934f960cdb6dd55f0b3ceeff207fcc48c64f58b43403777ad5623d9e/ujson-5.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7d0e0ceeb8fe2468c70ec0c37b439dd554e2aa539a8a56365fd761edb418988", size = 53693 },
260
+ { url = "https://files.pythonhosted.org/packages/f5/be/7bfa84b28519ddbb67efc8410765ca7da55e6b93aba84d97764cd5794dbc/ujson-5.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:59e02cd37bc7c44d587a0ba45347cc815fb7a5fe48de16bf05caa5f7d0d2e816", size = 58594 },
261
+ { url = "https://files.pythonhosted.org/packages/48/eb/85d465abafb2c69d9699cfa5520e6e96561db787d36c677370e066c7e2e7/ujson-5.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2a890b706b64e0065f02577bf6d8ca3b66c11a5e81fb75d757233a38c07a1f20", size = 997853 },
262
+ { url = "https://files.pythonhosted.org/packages/9f/76/2a63409fc05d34dd7d929357b7a45e3a2c96f22b4225cd74becd2ba6c4cb/ujson-5.10.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:621e34b4632c740ecb491efc7f1fcb4f74b48ddb55e65221995e74e2d00bbff0", size = 1140694 },
263
+ { url = "https://files.pythonhosted.org/packages/45/ed/582c4daba0f3e1688d923b5cb914ada1f9defa702df38a1916c899f7c4d1/ujson-5.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9500e61fce0cfc86168b248104e954fead61f9be213087153d272e817ec7b4f", size = 1043580 },
264
+ { url = "https://files.pythonhosted.org/packages/d7/0c/9837fece153051e19c7bade9f88f9b409e026b9525927824cdf16293b43b/ujson-5.10.0-cp313-cp313-win32.whl", hash = "sha256:4c4fc16f11ac1612f05b6f5781b384716719547e142cfd67b65d035bd85af165", size = 38766 },
265
+ { url = "https://files.pythonhosted.org/packages/d7/72/6cb6728e2738c05bbe9bd522d6fc79f86b9a28402f38663e85a28fddd4a0/ujson-5.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:4573fd1695932d4f619928fd09d5d03d917274381649ade4328091ceca175539", size = 42212 },
266
+ ]
267
+
268
+ [[package]]
269
+ name = "urllib3"
270
+ version = "2.3.0"
271
+ source = { registry = "https://pypi.org/simple" }
272
+ sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 }
273
+ wheels = [
274
+ { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 },
275
+ ]
276
+
277
+ [[package]]
278
+ name = "win32-setctime"
279
+ version = "1.2.0"
280
+ source = { registry = "https://pypi.org/simple" }
281
+ sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867 }
282
+ wheels = [
283
+ { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083 },
284
+ ]