codespector 0.1.0__py3-none-any.whl

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.
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()
codespector/main.py ADDED
@@ -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,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,14 @@
1
+ codespector/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ codespector/controller.py,sha256=z7TznOY6SBXCirBFpRO4bSh0_XY3gjMjUmnlFDkkc7Q,1211
3
+ codespector/main.py,sha256=y_mij-Tj_LaFQ2DUn8wAKJLKYy60jvb162UBnI1QWGM,1950
4
+ codespector/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ codespector/clients/codestral.py,sha256=MV6_m9IMYzTHX5AS9UoPkhGUNHRoOXgIpTZ8uHfwZgQ,56
6
+ codespector/local/__init__.py,sha256=spdqmAUYVjE-6RJUNUQ63ymwBY3svwC1gQp5QHAZdwg,35
7
+ codespector/local/main.py,sha256=JQb8mBQEjxsjNnjpqCOP84ZF92T2DjTzCslRdHQLqlg,1136
8
+ codespector/local/prepare.py,sha256=uqQft4RDH_5Rms3fGROc_fzCCRVcu-BxDzCAVF16buo,2821
9
+ codespector/local/reviewer.py,sha256=3dyopW6Y79M3i5_vruzk6Kf_WfXK1Ac8PLGrPI8meTU,3611
10
+ codespector-0.1.0.dist-info/METADATA,sha256=4ml2ECi_NgJ0siZMd3S9pV_x3v68YszFjDO-fqMNG6U,3160
11
+ codespector-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
12
+ codespector-0.1.0.dist-info/entry_points.txt,sha256=QlHn96KY8vzY1sOweKIuZOAQrSVse6h3v57vkwmHmJg,54
13
+ codespector-0.1.0.dist-info/licenses/LICENSE,sha256=Eta34ENUL_dZWy-prVNucMkoA38WIqiO9pKTUYiuT_A,1070
14
+ codespector-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ codespector = codespector.main:main
@@ -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.