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.
- codespector/__init__.py +0 -0
- codespector/clients/__init__.py +0 -0
- codespector/clients/codestral.py +5 -0
- codespector/controller.py +44 -0
- codespector/local/__init__.py +1 -0
- codespector/local/main.py +35 -0
- codespector/local/prepare.py +81 -0
- codespector/local/reviewer.py +109 -0
- codespector/main.py +81 -0
- codespector-0.1.0.dist-info/METADATA +97 -0
- codespector-0.1.0.dist-info/RECORD +14 -0
- codespector-0.1.0.dist-info/WHEEL +4 -0
- codespector-0.1.0.dist-info/entry_points.txt +2 -0
- codespector-0.1.0.dist-info/licenses/LICENSE +21 -0
codespector/__init__.py
ADDED
File without changes
|
File without changes
|
@@ -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,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.
|