ai-cr 0.4.7__py3-none-any.whl → 0.5.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.
- ai_code_review/__main__.py +4 -0
- ai_code_review/bootstrap.py +31 -6
- ai_code_review/cli.py +55 -10
- ai_code_review/core.py +18 -7
- ai_code_review/utils.py +44 -3
- {ai_cr-0.4.7.dist-info → ai_cr-0.5.0.dist-info}/METADATA +7 -5
- ai_cr-0.5.0.dist-info/RECORD +15 -0
- ai_cr-0.5.0.dist-info/entry_points.txt +3 -0
- ai_cr-0.4.7.dist-info/RECORD +0 -14
- ai_cr-0.4.7.dist-info/entry_points.txt +0 -3
- {ai_cr-0.4.7.dist-info → ai_cr-0.5.0.dist-info}/LICENSE +0 -0
- {ai_cr-0.4.7.dist-info → ai_cr-0.5.0.dist-info}/WHEEL +0 -0
ai_code_review/bootstrap.py
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
import logging
|
2
|
+
import os
|
2
3
|
from datetime import datetime
|
3
4
|
|
4
5
|
import microcore as mc
|
6
|
+
from ai_code_review.utils import is_running_in_github_action
|
5
7
|
|
6
8
|
from .constants import ENV_CONFIG_FILE
|
7
9
|
|
@@ -28,10 +30,33 @@ def bootstrap():
|
|
28
30
|
"""Bootstrap the application with the environment configuration."""
|
29
31
|
setup_logging()
|
30
32
|
logging.info("Bootstrapping...")
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
33
|
+
try:
|
34
|
+
mc.configure(
|
35
|
+
DOT_ENV_FILE=ENV_CONFIG_FILE,
|
36
|
+
USE_LOGGING=True,
|
37
|
+
EMBEDDING_DB_TYPE=mc.EmbeddingDbType.NONE,
|
38
|
+
)
|
39
|
+
except mc.LLMConfigError as e:
|
40
|
+
msg = str(e)
|
41
|
+
if is_running_in_github_action():
|
42
|
+
ref = os.getenv("GITHUB_WORKFLOW_REF", "")
|
43
|
+
if ref:
|
44
|
+
# example value: 'owner/repo/.github/workflows/ai-code-review.yml@refs/pull/1/merge'
|
45
|
+
ref = ref.split("@")[0]
|
46
|
+
ref = ref.split(".github/workflows/")[-1]
|
47
|
+
ref = f" (.github/workflows/{ref})"
|
48
|
+
msg += (
|
49
|
+
f"\nPlease check your GitHub Action Secrets "
|
50
|
+
f"and `env` configuration section of the corresponding workflow step{ref}."
|
51
|
+
)
|
52
|
+
else:
|
53
|
+
msg += (
|
54
|
+
"\nPlease run 'ai-code-review setup' "
|
55
|
+
"to configure LLM API access (API keys, model, etc)."
|
56
|
+
)
|
57
|
+
print(mc.ui.red(msg))
|
58
|
+
raise SystemExit(2)
|
59
|
+
except Exception as e:
|
60
|
+
logging.error(f"Unexpected configuration error: {e}")
|
61
|
+
raise SystemExit(3)
|
37
62
|
mc.logging.LoggingConfig.STRIP_REQUEST_LINES = [100, 15]
|
ai_code_review/cli.py
CHANGED
@@ -3,34 +3,79 @@ import logging
|
|
3
3
|
import sys
|
4
4
|
import os
|
5
5
|
import shutil
|
6
|
+
import requests
|
6
7
|
|
7
8
|
import microcore as mc
|
8
9
|
import async_typer
|
9
10
|
import typer
|
10
|
-
from .
|
11
|
-
from .report_struct import Report
|
11
|
+
from ai_code_review.utils import parse_refs_pair
|
12
12
|
from git import Repo
|
13
|
-
import requests
|
14
13
|
|
14
|
+
from .core import review
|
15
|
+
from .report_struct import Report
|
15
16
|
from .constants import ENV_CONFIG_FILE
|
16
17
|
from .bootstrap import bootstrap
|
17
18
|
from .project_config import ProjectConfig
|
18
|
-
|
19
|
-
app = async_typer.AsyncTyper(
|
20
|
-
pretty_exceptions_show_locals=False,
|
21
|
-
)
|
19
|
+
from .utils import is_app_command_invocation
|
22
20
|
|
23
21
|
|
22
|
+
app = async_typer.AsyncTyper(pretty_exceptions_show_locals=False)
|
23
|
+
default_command_app = async_typer.AsyncTyper(pretty_exceptions_show_locals=False)
|
24
24
|
if sys.platform == "win32":
|
25
25
|
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
26
26
|
|
27
27
|
|
28
|
+
def main():
|
29
|
+
if is_app_command_invocation(app):
|
30
|
+
app()
|
31
|
+
else:
|
32
|
+
bootstrap()
|
33
|
+
default_command_app()
|
34
|
+
|
35
|
+
|
28
36
|
@app.callback(invoke_without_command=True)
|
29
|
-
def cli(ctx: typer.Context
|
37
|
+
def cli(ctx: typer.Context):
|
30
38
|
if ctx.invoked_subcommand != "setup":
|
31
39
|
bootstrap()
|
32
|
-
|
33
|
-
|
40
|
+
|
41
|
+
|
42
|
+
@default_command_app.async_command(name="review", help="Perform code review")
|
43
|
+
@app.async_command(name="review", help="Perform code review")
|
44
|
+
async def cmd_review(
|
45
|
+
refs: str = typer.Argument(
|
46
|
+
default=None,
|
47
|
+
help="Git refs to review, [what]..[against] e.g. 'HEAD..HEAD~1'"
|
48
|
+
),
|
49
|
+
what: str = typer.Option(None, "--what", "-w", help="Git ref to review"),
|
50
|
+
against: str = typer.Option(
|
51
|
+
None,
|
52
|
+
"--against", "-vs", "--vs",
|
53
|
+
help="Git ref to compare against"
|
54
|
+
),
|
55
|
+
filters: str = typer.Option(
|
56
|
+
"", "--filter", "-f", "--filters",
|
57
|
+
help="""
|
58
|
+
filter reviewed files by glob / fnmatch pattern(s),
|
59
|
+
e.g. 'src/**/*.py', may be comma-separated
|
60
|
+
""",
|
61
|
+
)
|
62
|
+
):
|
63
|
+
_what, _against = parse_refs_pair(refs)
|
64
|
+
if _what:
|
65
|
+
if what:
|
66
|
+
raise typer.BadParameter(
|
67
|
+
"You cannot specify both 'refs' <WHAT>..<AGAINST> and '--what'. Use one of them."
|
68
|
+
)
|
69
|
+
else:
|
70
|
+
_what = what
|
71
|
+
if _against:
|
72
|
+
if against:
|
73
|
+
raise typer.BadParameter(
|
74
|
+
"You cannot specify both 'refs' <WHAT>..<AGAINST> and '--against'. Use one of them."
|
75
|
+
)
|
76
|
+
else:
|
77
|
+
_against = against
|
78
|
+
await review(what=_what, against=_against, filters=filters)
|
34
79
|
|
35
80
|
|
36
81
|
@app.async_command(help="Configure LLM for local usage interactively")
|
ai_code_review/core.py
CHANGED
@@ -11,11 +11,18 @@ from .project_config import ProjectConfig
|
|
11
11
|
from .report_struct import Report
|
12
12
|
|
13
13
|
|
14
|
-
def get_diff(
|
14
|
+
def get_diff(
|
15
|
+
repo: Repo = None,
|
16
|
+
what: str = None,
|
17
|
+
against: str = None
|
18
|
+
) -> PatchSet | list[PatchedFile]:
|
15
19
|
repo = repo or Repo(".")
|
16
|
-
|
17
|
-
|
18
|
-
|
20
|
+
if not against:
|
21
|
+
against = repo.remotes.origin.refs.HEAD.reference.name # origin/main
|
22
|
+
if not what:
|
23
|
+
what = None # working copy
|
24
|
+
logging.info(f"Reviewing {mc.ui.green(what or 'working copy')} vs {mc.ui.yellow(against)}")
|
25
|
+
diff_content = repo.git.diff(against, what)
|
19
26
|
diff = PatchSet.from_string(diff_content)
|
20
27
|
return diff
|
21
28
|
|
@@ -62,10 +69,14 @@ def make_cr_summary(cfg: ProjectConfig, report: Report, diff):
|
|
62
69
|
).to_llm() if cfg.summary_prompt else ""
|
63
70
|
|
64
71
|
|
65
|
-
async def review(
|
72
|
+
async def review(
|
73
|
+
what: str = None,
|
74
|
+
against: str = None,
|
75
|
+
filters: str | list[str] = ""
|
76
|
+
):
|
66
77
|
cfg = ProjectConfig.load()
|
67
78
|
repo = Repo(".")
|
68
|
-
diff = get_diff(repo=repo, against=
|
79
|
+
diff = get_diff(repo=repo, what=what, against=against)
|
69
80
|
diff = filter_diff(diff, filters)
|
70
81
|
if not diff:
|
71
82
|
logging.error("Nothing to review")
|
@@ -78,7 +89,7 @@ async def review(filters: str | list[str] = ""):
|
|
78
89
|
cfg.max_code_tokens
|
79
90
|
- mc.tokenizing.num_tokens_from_string(str(file_diff)),
|
80
91
|
)
|
81
|
-
if file_diff.target_file != DEV_NULL
|
92
|
+
if file_diff.target_file != DEV_NULL and not file_diff.is_added_file
|
82
93
|
else ""
|
83
94
|
)
|
84
95
|
for file_diff in diff
|
ai_code_review/utils.py
CHANGED
@@ -1,4 +1,8 @@
|
|
1
|
+
import sys
|
2
|
+
import os
|
1
3
|
from pathlib import Path
|
4
|
+
import typer
|
5
|
+
|
2
6
|
|
3
7
|
_EXT_TO_HINT: dict[str, str] = {
|
4
8
|
# scripting & languages
|
@@ -61,11 +65,20 @@ _EXT_TO_HINT: dict[str, str] = {
|
|
61
65
|
}
|
62
66
|
|
63
67
|
|
64
|
-
def syntax_hint(
|
68
|
+
def syntax_hint(file_path: str | Path) -> str:
|
65
69
|
"""
|
66
|
-
|
70
|
+
Returns a syntax highlighting hint based on the file's extension or name.
|
71
|
+
|
72
|
+
This can be used to annotate code blocks for rendering with syntax highlighting,
|
73
|
+
e.g., using Markdown-style code blocks: ```<syntax_hint>\n<code>\n```.
|
74
|
+
|
75
|
+
Args:
|
76
|
+
file_path (str | Path): Path to the file.
|
77
|
+
|
78
|
+
Returns:
|
79
|
+
str: A syntax identifier suitable for code highlighting (e.g., 'python', 'json').
|
67
80
|
"""
|
68
|
-
p = Path(
|
81
|
+
p = Path(file_path)
|
69
82
|
ext = p.suffix.lower()
|
70
83
|
if not ext:
|
71
84
|
name = p.name.lower()
|
@@ -73,3 +86,31 @@ def syntax_hint(file_name: str) -> str:
|
|
73
86
|
return "dockerfile"
|
74
87
|
return ""
|
75
88
|
return _EXT_TO_HINT.get(ext, ext.lstrip("."))
|
89
|
+
|
90
|
+
|
91
|
+
def is_running_in_github_action():
|
92
|
+
return os.getenv("GITHUB_ACTIONS") == "true"
|
93
|
+
|
94
|
+
|
95
|
+
def is_app_command_invocation(app: typer.Typer) -> bool:
|
96
|
+
"""
|
97
|
+
Checks if the current script is being invoked as a command in a target Typer application.
|
98
|
+
"""
|
99
|
+
return (
|
100
|
+
(first_arg := next((a for a in sys.argv[1:] if not a.startswith('-')), None))
|
101
|
+
and first_arg in (
|
102
|
+
cmd.name or cmd.callback.__name__.replace('_', '-')
|
103
|
+
for cmd in app.registered_commands
|
104
|
+
)
|
105
|
+
or '--help' in sys.argv
|
106
|
+
)
|
107
|
+
|
108
|
+
|
109
|
+
def parse_refs_pair(refs: str):
|
110
|
+
SEPARATOR = '..'
|
111
|
+
if not refs:
|
112
|
+
return None, None
|
113
|
+
if SEPARATOR not in refs:
|
114
|
+
return refs, None
|
115
|
+
what, against = refs.split(SEPARATOR)
|
116
|
+
return what or None, against or None
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: ai-cr
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.5.0
|
4
4
|
Summary: LLM-agnostic GitHub AI Code Review Tool with integration to GitHub actions
|
5
5
|
License: MIT
|
6
6
|
Keywords: static code analysis,code review,code quality,ai,coding,assistant,llm,github,automation,devops,developer tools,github actions,workflows,git
|
@@ -16,8 +16,8 @@ Classifier: Programming Language :: Python :: 3.12
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.13
|
17
17
|
Classifier: Topic :: Software Development
|
18
18
|
Requires-Dist: GitPython (==3.1.44)
|
19
|
-
Requires-Dist: ai-microcore (==4.0.0.
|
20
|
-
Requires-Dist: anthropic (==0.
|
19
|
+
Requires-Dist: ai-microcore (==4.0.0.dev18)
|
20
|
+
Requires-Dist: anthropic (==0.52.2)
|
21
21
|
Requires-Dist: async-typer (==0.1.8)
|
22
22
|
Requires-Dist: google-generativeai (==0.8.5)
|
23
23
|
Requires-Dist: typer (==0.9.4)
|
@@ -70,7 +70,7 @@ jobs:
|
|
70
70
|
uses: actions/setup-python@v5
|
71
71
|
with: { python-version: "3.13" }
|
72
72
|
- name: Install AI Code Review tool
|
73
|
-
run: pip install ai-code-review==0.
|
73
|
+
run: pip install ai-code-review==0.5.0
|
74
74
|
- name: Run AI code analysis
|
75
75
|
env:
|
76
76
|
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
|
@@ -151,7 +151,9 @@ pytest
|
|
151
151
|
|
152
152
|
## 🤝 Contributing
|
153
153
|
|
154
|
-
|
154
|
+
**Looking for a specific feature or having trouble?**
|
155
|
+
Contributions are welcome! ❤️
|
156
|
+
See [CONTRIBUTING.md](https://github.com/Nayjest/ai-code-review/blob/main/CONTRIBUTING.md) for details.
|
155
157
|
|
156
158
|
## 📝 License
|
157
159
|
|
@@ -0,0 +1,15 @@
|
|
1
|
+
ai_code_review/.ai-code-review.toml,sha256=z-3iliCvkkpdAr9l_yUyM-IbgRm9hbAA3CIV6ocuurY,10378
|
2
|
+
ai_code_review/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
|
+
ai_code_review/__main__.py,sha256=MSmt_5Xg84uHqzTN38JwgseJK8rsJn_11A8WD99VtEo,61
|
4
|
+
ai_code_review/bootstrap.py,sha256=b9BlhRgliwsjmWRCCZcsTivsOw9AbXOf8_AZYjQTx-c,2336
|
5
|
+
ai_code_review/cli.py,sha256=L1utFMHs4D9HVcuJ1VC96PEvP6q26UHsfCz_Lq49VMg,4893
|
6
|
+
ai_code_review/constants.py,sha256=K9mNxTq9seTG3aVm__3r1lXb5oCOQjH24Cl3hfX9FsE,281
|
7
|
+
ai_code_review/core.py,sha256=WMGU2zwZTJ1P6PkUId-RJP7KHQbzB31GKSnI5wsedcA,4177
|
8
|
+
ai_code_review/project_config.py,sha256=RDbplmncALKw0zgqSG8POofi300z0DPvtF33wt7_1Sk,3651
|
9
|
+
ai_code_review/report_struct.py,sha256=N-EnNMwBY9LyJ9sdFHpUzn2fwBvxo5TZYYiJagBl8Po,3488
|
10
|
+
ai_code_review/utils.py,sha256=UzFp2ShNzQvsydxJ98bdr_j2rBUHcm62pPtl6hBuZD4,2914
|
11
|
+
ai_cr-0.5.0.dist-info/entry_points.txt,sha256=u0N5NroPYEGqmDGaGqFmiijJ5swzpe7EyKBupnkp1FY,49
|
12
|
+
ai_cr-0.5.0.dist-info/LICENSE,sha256=XATf3zv-CppUSJqI18KLhwnPEomUXEl5WbBzFyb9OSU,1096
|
13
|
+
ai_cr-0.5.0.dist-info/METADATA,sha256=k3FaB6nbVV0xbgZYPg_VQ6zMauB65Xy6CLWI4qFOMF0,5609
|
14
|
+
ai_cr-0.5.0.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
|
15
|
+
ai_cr-0.5.0.dist-info/RECORD,,
|
ai_cr-0.4.7.dist-info/RECORD
DELETED
@@ -1,14 +0,0 @@
|
|
1
|
-
ai_code_review/.ai-code-review.toml,sha256=z-3iliCvkkpdAr9l_yUyM-IbgRm9hbAA3CIV6ocuurY,10378
|
2
|
-
ai_code_review/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
|
-
ai_code_review/bootstrap.py,sha256=vhAWjrfhkyyA504YusXhBlndsXd_bg2RP9hQbfnvM1M,1280
|
4
|
-
ai_code_review/cli.py,sha256=lulkEEUmQ1Fn3j3aQ0ZXc6aPHPcQDsr4YqA_C6gFFH4,3409
|
5
|
-
ai_code_review/constants.py,sha256=K9mNxTq9seTG3aVm__3r1lXb5oCOQjH24Cl3hfX9FsE,281
|
6
|
-
ai_code_review/core.py,sha256=LhstEanCpMD0rVEYja0EQ4GuwB47RXGJJNOTkD6WqYk,3888
|
7
|
-
ai_code_review/project_config.py,sha256=RDbplmncALKw0zgqSG8POofi300z0DPvtF33wt7_1Sk,3651
|
8
|
-
ai_code_review/report_struct.py,sha256=N-EnNMwBY9LyJ9sdFHpUzn2fwBvxo5TZYYiJagBl8Po,3488
|
9
|
-
ai_code_review/utils.py,sha256=DLzX9kILl5ABAsuwqFJk_3kItjgzzeTUrgZ3RMnDgAY,1713
|
10
|
-
ai_cr-0.4.7.dist-info/entry_points.txt,sha256=fQYGUz6CVCClhei9kNCTtJUX6ia-dTca6p1sZ9pnst8,48
|
11
|
-
ai_cr-0.4.7.dist-info/LICENSE,sha256=XATf3zv-CppUSJqI18KLhwnPEomUXEl5WbBzFyb9OSU,1096
|
12
|
-
ai_cr-0.4.7.dist-info/METADATA,sha256=QCXyksLtb1v8Jl8K5DE3p3nU6Nh-IEZv9XhI4G5vCf0,5530
|
13
|
-
ai_cr-0.4.7.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
|
14
|
-
ai_cr-0.4.7.dist-info/RECORD,,
|
File without changes
|
File without changes
|