ai-cr 2.0.0.dev2__py3-none-any.whl → 2.0.1__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.
@@ -1,157 +1,194 @@
1
- """
2
- Fix issues from code review report
3
- """
4
-
5
- import logging
6
- import os
7
- import re
8
- from pathlib import Path
9
- from typing import Optional
10
- import zipfile
11
-
12
- import requests
13
- import typer
14
- from fastcore.basics import AttrDict
15
- from gito.project_config import ProjectConfig
16
- from gito.utils import extract_gh_owner_repo
17
- from microcore import ui
18
- from ghapi.all import GhApi
19
- import git
20
-
21
- from ..bootstrap import app
22
- from ..constants import JSON_REPORT_FILE_NAME
23
- from .fix import fix
24
- from ..utils import is_running_in_github_action
25
-
26
-
27
- @app.command()
28
- def react_to_comment(
29
- comment_id: int = typer.Argument(),
30
- gh_token: str = typer.Option(
31
- "",
32
- "--gh-token",
33
- "--token",
34
- "-t",
35
- "--github-token",
36
- help="GitHub token for authentication",
37
- ),
38
- dry_run: bool = typer.Option(
39
- False, "--dry-run", "-d", help="Only print changes without applying them"
40
- ),
41
- ):
42
- repo = git.Repo(".") # Current directory
43
- owner, repo_name = extract_gh_owner_repo(repo)
44
- logging.info(f"Using repository: {ui.yellow}{owner}/{repo_name}{ui.reset}")
45
- gh_token = (
46
- gh_token or os.getenv("GITHUB_TOKEN", None) or os.getenv("GH_TOKEN", None)
47
- )
48
- api = GhApi(owner=owner, repo=repo_name, token=gh_token)
49
- comment = api.issues.get_comment(comment_id=comment_id)
50
- logging.info(
51
- f"Comment by {ui.yellow('@' + comment.user.login)}: "
52
- f"{ui.green(comment.body)}\n"
53
- f"url: {comment.html_url}"
54
- )
55
-
56
- cfg = ProjectConfig.load_for_repo(repo)
57
- if not any(
58
- trigger.lower() in comment.body.lower() for trigger in cfg.mention_triggers
59
- ):
60
- ui.error("No mention trigger found in comment, no reaction added.")
61
- return
62
- if not is_running_in_github_action():
63
- # @todo: need service account to react to comments
64
- logging.info("Comment contains mention trigger, reacting with 'eyes'.")
65
- api.reactions.create_for_issue_comment(comment_id=comment_id, content="eyes")
66
-
67
- pr = int(comment.issue_url.split("/")[-1])
68
- print(f"Processing comment for PR #{pr}...")
69
- out_folder = "artifact"
70
- download_latest_code_review_artifact(
71
- api, pr_number=pr, gh_token=gh_token, out_folder=out_folder
72
- )
73
-
74
- issue_ids = extract_fix_args(comment.body)
75
- if not issue_ids:
76
- ui.error("Can't identify target command in the text.")
77
- return
78
- logging.info(f"Extracted issue IDs: {ui.yellow(str(issue_ids))}")
79
-
80
- fix(
81
- issue_ids[0], # @todo: support multiple IDs
82
- report_path=Path(out_folder) / JSON_REPORT_FILE_NAME,
83
- dry_run=dry_run,
84
- commit=not dry_run,
85
- push=not dry_run,
86
- )
87
- logging.info("Fix applied successfully.")
88
-
89
-
90
- def last_code_review_run(api: GhApi, pr_number: int) -> AttrDict | None:
91
- pr = api.pulls.get(pr_number)
92
- sha = pr["head"]["sha"] # noqa
93
- branch = pr["head"]["ref"]
94
-
95
- runs = api.actions.list_workflow_runs_for_repo(branch=branch)["workflow_runs"]
96
- # Find the run for this SHA
97
- run = next(
98
- (
99
- r
100
- for r in runs # r['head_sha'] == sha and
101
- if (
102
- any(
103
- marker in r["path"].lower()
104
- for marker in ["code-review", "code_review", "cr"]
105
- )
106
- or "gito.yml" in r["name"].lower()
107
- )
108
- and r["status"] == "completed"
109
- ),
110
- None,
111
- )
112
- return run
113
-
114
-
115
- def download_latest_code_review_artifact(
116
- api: GhApi, pr_number: int, gh_token: str, out_folder: Optional[str] = "artifact"
117
- ) -> tuple[str, dict] | None:
118
- run = last_code_review_run(api, pr_number)
119
- if not run:
120
- raise Exception("No workflow run found for this PR/SHA")
121
-
122
- artifacts = api.actions.list_workflow_run_artifacts(run["id"])["artifacts"]
123
- if not artifacts:
124
- raise Exception("No artifacts found for this workflow run")
125
-
126
- latest_artifact = artifacts[0]
127
- url = latest_artifact["archive_download_url"]
128
- print(f"Artifact: {latest_artifact['name']}, Download URL: {url}")
129
- headers = {"Authorization": f"token {gh_token}"} if gh_token else {}
130
- zip_path = "artifact.zip"
131
- try:
132
- with requests.get(url, headers=headers, stream=True) as r:
133
- r.raise_for_status()
134
- with open(zip_path, "wb") as f:
135
- for chunk in r.iter_content(chunk_size=8192):
136
- f.write(chunk)
137
-
138
- # Unpack to ./artifact
139
- os.makedirs("artifact", exist_ok=True)
140
- with zipfile.ZipFile(zip_path, "r") as zip_ref:
141
- zip_ref.extractall("artifact")
142
- finally:
143
- if os.path.exists(zip_path):
144
- os.remove(zip_path)
145
-
146
- print("Artifact unpacked to ./artifact")
147
-
148
-
149
- def extract_fix_args(text: str) -> list[int]:
150
- pattern1 = r"fix\s+(?:issues?)?(?:\s+)?#?(\d+(?:\s*,\s*#?\d+)*)"
151
- match = re.search(pattern1, text)
152
- if match:
153
- numbers_str = match.group(1)
154
- numbers = re.findall(r"\d+", numbers_str)
155
- issue_numbers = [int(num) for num in numbers]
156
- return issue_numbers
157
- return []
1
+ """
2
+ Fix issues from code review report
3
+ """
4
+
5
+ import logging
6
+ import os
7
+ import re
8
+ from pathlib import Path
9
+ from typing import Optional
10
+ import zipfile
11
+
12
+ import requests
13
+ import typer
14
+ from fastcore.basics import AttrDict
15
+ from microcore import ui
16
+ from ghapi.all import GhApi
17
+ import git
18
+
19
+ from ..bootstrap import app
20
+ from ..constants import JSON_REPORT_FILE_NAME, HTML_TEXT_ICON
21
+ from ..core import answer
22
+ from ..gh_api import post_gh_comment
23
+ from ..project_config import ProjectConfig
24
+ from ..utils import extract_gh_owner_repo
25
+ from .fix import fix
26
+
27
+
28
+ @app.command(hidden=True)
29
+ def react_to_comment(
30
+ comment_id: int = typer.Argument(),
31
+ gh_token: str = typer.Option(
32
+ "",
33
+ "--gh-token",
34
+ "--token",
35
+ "-t",
36
+ "--github-token",
37
+ help="GitHub token for authentication",
38
+ ),
39
+ dry_run: bool = typer.Option(
40
+ False, "--dry-run", "-d", help="Only print changes without applying them"
41
+ ),
42
+ ):
43
+ """
44
+ Handles direct agent instructions from pull request comments.
45
+
46
+ Note: Not for local usage. Designed for execution within GitHub Actions workflows.
47
+
48
+ Fetches the PR comment by ID, parses agent directives, and executes the requested
49
+ actions automatically to enable seamless code review workflow integration.
50
+ """
51
+ repo = git.Repo(".") # Current directory
52
+ owner, repo_name = extract_gh_owner_repo(repo)
53
+ logging.info(f"Using repository: {ui.yellow}{owner}/{repo_name}{ui.reset}")
54
+ gh_token = (
55
+ gh_token or os.getenv("GITHUB_TOKEN", None) or os.getenv("GH_TOKEN", None)
56
+ )
57
+ api = GhApi(owner=owner, repo=repo_name, token=gh_token)
58
+ comment = api.issues.get_comment(comment_id=comment_id)
59
+ logging.info(
60
+ f"Comment by {ui.yellow('@' + comment.user.login)}: "
61
+ f"{ui.green(comment.body)}\n"
62
+ f"url: {comment.html_url}"
63
+ )
64
+
65
+ cfg = ProjectConfig.load_for_repo(repo)
66
+ if not any(
67
+ trigger.lower() in comment.body.lower() for trigger in cfg.mention_triggers
68
+ ):
69
+ ui.error("No mention trigger found in comment, no reaction added.")
70
+ return
71
+ try:
72
+ logging.info("Comment contains mention trigger, reacting with 'eyes'.")
73
+ api.reactions.create_for_issue_comment(comment_id=comment_id, content="eyes")
74
+ except Exception as e:
75
+ logging.error("Error reacting to comment with emoji: %s", str(e))
76
+ pr = int(comment.issue_url.split("/")[-1])
77
+ print(f"Processing comment for PR #{pr}...")
78
+
79
+ issue_ids = extract_fix_args(comment.body)
80
+ if issue_ids:
81
+ logging.info(f"Extracted issue IDs: {ui.yellow(str(issue_ids))}")
82
+ out_folder = "artifact"
83
+ download_latest_code_review_artifact(
84
+ api, pr_number=pr, gh_token=gh_token, out_folder=out_folder
85
+ )
86
+ fix(
87
+ issue_ids[0], # @todo: support multiple IDs
88
+ report_path=Path(out_folder) / JSON_REPORT_FILE_NAME,
89
+ dry_run=dry_run,
90
+ commit=not dry_run,
91
+ push=not dry_run,
92
+ )
93
+ logging.info("Fix applied successfully.")
94
+ elif is_review_request(comment.body):
95
+ ref = repo.active_branch.name
96
+ logging.info(f"Triggering code-review workflow, ref='{ref}'")
97
+ api.actions.create_workflow_dispatch(
98
+ workflow_id="gito-code-review.yml",
99
+ ref=ref,
100
+ inputs={"pr_number": str(pr)},
101
+ )
102
+ else:
103
+ if cfg.answer_github_comments:
104
+ response = answer(comment.body, repo=repo)
105
+ post_gh_comment(
106
+ gh_repository=f"{owner}/{repo_name}",
107
+ pr_or_issue_number=pr,
108
+ gh_token=gh_token,
109
+ text=HTML_TEXT_ICON+response,
110
+ )
111
+ else:
112
+ ui.error("Can't identify target command in the text.")
113
+ return
114
+
115
+
116
+ def last_code_review_run(api: GhApi, pr_number: int) -> AttrDict | None:
117
+ pr = api.pulls.get(pr_number)
118
+ sha = pr["head"]["sha"] # noqa
119
+ branch = pr["head"]["ref"]
120
+
121
+ runs = api.actions.list_workflow_runs_for_repo(branch=branch)["workflow_runs"]
122
+ # Find the run for this SHA
123
+ run = next(
124
+ (
125
+ r
126
+ for r in runs # r['head_sha'] == sha and
127
+ if (
128
+ any(
129
+ marker in r["path"].lower()
130
+ for marker in ["code-review", "code_review", "cr"]
131
+ )
132
+ or "gito.yml" in r["name"].lower()
133
+ )
134
+ and r["status"] == "completed"
135
+ ),
136
+ None,
137
+ )
138
+ return run
139
+
140
+
141
+ def download_latest_code_review_artifact(
142
+ api: GhApi, pr_number: int, gh_token: str, out_folder: Optional[str] = "artifact"
143
+ ) -> tuple[str, dict] | None:
144
+ run = last_code_review_run(api, pr_number)
145
+ if not run:
146
+ raise Exception("No workflow run found for this PR/SHA")
147
+
148
+ artifacts = api.actions.list_workflow_run_artifacts(run["id"])["artifacts"]
149
+ if not artifacts:
150
+ raise Exception("No artifacts found for this workflow run")
151
+
152
+ latest_artifact = artifacts[0]
153
+ url = latest_artifact["archive_download_url"]
154
+ print(f"Artifact: {latest_artifact['name']}, Download URL: {url}")
155
+ headers = {"Authorization": f"token {gh_token}"} if gh_token else {}
156
+ zip_path = "artifact.zip"
157
+ try:
158
+ with requests.get(url, headers=headers, stream=True) as r:
159
+ r.raise_for_status()
160
+ with open(zip_path, "wb") as f:
161
+ for chunk in r.iter_content(chunk_size=8192):
162
+ f.write(chunk)
163
+
164
+ # Unpack to ./artifact
165
+ os.makedirs("artifact", exist_ok=True)
166
+ with zipfile.ZipFile(zip_path, "r") as zip_ref:
167
+ zip_ref.extractall("artifact")
168
+ finally:
169
+ if os.path.exists(zip_path):
170
+ os.remove(zip_path)
171
+
172
+ print("Artifact unpacked to ./artifact")
173
+
174
+
175
+ def extract_fix_args(text: str) -> list[int]:
176
+ pattern1 = r"fix\s+(?:issues?)?(?:\s+)?#?(\d+(?:\s*,\s*#?\d+)*)"
177
+ match = re.search(pattern1, text)
178
+ if match:
179
+ numbers_str = match.group(1)
180
+ numbers = re.findall(r"\d+", numbers_str)
181
+ issue_numbers = [int(num) for num in numbers]
182
+ return issue_numbers
183
+ return []
184
+
185
+
186
+ def is_review_request(text: str) -> bool:
187
+ text = text.lower().strip()
188
+ trigger_words = ['review', 'run', 'code-review']
189
+ if any(f"/{word}" in text for word in trigger_words):
190
+ return True
191
+ parts = text.split()
192
+ if len(parts) == 2 and parts[1] in trigger_words:
193
+ return True
194
+ return False
gito/commands/repl.py CHANGED
@@ -4,8 +4,7 @@ Python REPL
4
4
  # flake8: noqa: F401
5
5
  import code
6
6
 
7
-
8
- # Imports for usage in REPL
7
+ # Wildcard imports are preferred to capture most of functionality for usage in REPL
9
8
  import os
10
9
  import sys
11
10
  from dataclasses import dataclass
@@ -18,6 +17,10 @@ import microcore as mc
18
17
  from microcore import ui
19
18
 
20
19
  from ..cli import app
20
+ from ..constants import *
21
+ from ..core import *
22
+ from ..utils import *
23
+
21
24
 
22
25
  @app.command(help="python REPL")
23
26
  def repl():