ai-cr 2.0.0.dev1__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.
gito/pipeline.py ADDED
@@ -0,0 +1,82 @@
1
+ import logging
2
+ from enum import StrEnum
3
+ from dataclasses import dataclass, field
4
+
5
+ from gito.utils import is_running_in_github_action
6
+ from microcore import ui
7
+ from microcore.utils import resolve_callable
8
+
9
+
10
+ class PipelineEnv(StrEnum):
11
+ LOCAL = "local"
12
+ GH_ACTION = "gh-action"
13
+
14
+ @staticmethod
15
+ def all():
16
+ return [PipelineEnv.LOCAL, PipelineEnv.GH_ACTION]
17
+
18
+ @staticmethod
19
+ def current():
20
+ return (
21
+ PipelineEnv.GH_ACTION
22
+ if is_running_in_github_action()
23
+ else PipelineEnv.LOCAL
24
+ )
25
+
26
+
27
+ @dataclass
28
+ class PipelineStep:
29
+ call: str
30
+ envs: list[PipelineEnv] = field(default_factory=PipelineEnv.all)
31
+ enabled: bool = field(default=True)
32
+
33
+ def get_callable(self):
34
+ """
35
+ Resolve the callable from the string representation.
36
+ """
37
+ return resolve_callable(self.call)
38
+
39
+ def run(self, *args, **kwargs):
40
+ return self.get_callable()(*args, **kwargs)
41
+
42
+
43
+ @dataclass
44
+ class Pipeline:
45
+ ctx: dict = field(default_factory=dict)
46
+ steps: dict[str, PipelineStep] = field(default_factory=dict)
47
+ verbose: bool = False
48
+
49
+ @property
50
+ def enabled_steps(self):
51
+ return {
52
+ k: v for k, v in self.steps.items() if v.enabled
53
+ }
54
+
55
+ def run(self, *args, **kwargs):
56
+ cur_env = PipelineEnv.current()
57
+ logging.info("Running pipeline... [env: %s]", ui.yellow(cur_env))
58
+ self.ctx["pipeline_out"] = self.ctx.get("pipeline_out", {})
59
+ for step_name, step in self.enabled_steps.items():
60
+ if cur_env in step.envs:
61
+ logging.info(f"Running pipeline step: {step_name}")
62
+ try:
63
+ step_output = step.run(*args, **kwargs, **self.ctx)
64
+ if isinstance(step_output, dict):
65
+ self.ctx["pipeline_out"].update(step_output)
66
+ self.ctx["pipeline_out"][step_name] = step_output
67
+ if self.verbose and step_output:
68
+ logging.info(
69
+ f"Pipeline step {step_name} output: {repr(step_output)}"
70
+ )
71
+ if not step_output:
72
+ logging.warning(
73
+ f'Pipeline step "{step_name}" returned {repr(step_output)}.'
74
+ )
75
+ except Exception as e:
76
+ logging.error(f'Error in pipeline step "{step_name}": {e}')
77
+ else:
78
+ logging.info(
79
+ f"Skipping pipeline step: {step_name}"
80
+ f" [env: {ui.yellow(cur_env)} not in {step.envs}]"
81
+ )
82
+ return self.ctx["pipeline_out"]
File without changes
@@ -0,0 +1,57 @@
1
+ import logging
2
+ import os
3
+
4
+ import git
5
+ from jira import JIRA
6
+
7
+ from gito.issue_trackers import IssueTrackerIssue, resolve_issue_key
8
+
9
+
10
+ def fetch_issue(issue_key, jira_url, username, api_token) -> IssueTrackerIssue | None:
11
+ try:
12
+ jira = JIRA(jira_url, basic_auth=(username, api_token))
13
+ issue = jira.issue(issue_key)
14
+ return IssueTrackerIssue(
15
+ title=issue.fields.summary,
16
+ description=issue.fields.description or "",
17
+ url=f"{jira_url.rstrip('/')}/browse/{issue_key}"
18
+ )
19
+ except Exception as e:
20
+ logging.error(f"Failed to fetch Jira issue {issue_key}: {e}")
21
+ return None
22
+
23
+
24
+ def fetch_associated_issue(
25
+ repo: git.Repo,
26
+ jira_url=None,
27
+ jira_username=None,
28
+ jira_api_token=None,
29
+ **kwargs
30
+ ):
31
+ """
32
+ Pipeline step to fetch a Jira issue based on the current branch name.
33
+ """
34
+ jira_url = jira_url or os.getenv("JIRA_URL")
35
+ jira_username = (
36
+ jira_username
37
+ or os.getenv("JIRA_USERNAME")
38
+ or os.getenv("JIRA_USER")
39
+ or os.getenv("JIRA_EMAIL")
40
+ )
41
+ jira_token = (
42
+ jira_api_token
43
+ or os.getenv("JIRA_API_TOKEN")
44
+ or os.getenv("JIRA_API_KEY")
45
+ or os.getenv("JIRA_TOKEN")
46
+ )
47
+ try:
48
+ assert jira_url, "JIRA_URL is not set"
49
+ assert jira_username, "JIRA_USERNAME is not set"
50
+ assert jira_token, "JIRA_API_TOKEN is not set"
51
+ except AssertionError as e:
52
+ logging.error(f"Jira configuration error: {e}")
53
+ return None
54
+ issue_key = resolve_issue_key(repo)
55
+ return dict(
56
+ associated_issue=fetch_issue(issue_key, jira_url, jira_username, jira_token)
57
+ ) if issue_key else None
@@ -0,0 +1,84 @@
1
+ import logging
2
+ import os
3
+ import requests
4
+
5
+ import git
6
+
7
+ from gito.issue_trackers import IssueTrackerIssue, resolve_issue_key
8
+
9
+
10
+ def fetch_issue(issue_key, api_key) -> IssueTrackerIssue | None:
11
+ """
12
+ Fetch a Linear issue using GraphQL API.
13
+ """
14
+ try:
15
+ url = "https://api.linear.app/graphql"
16
+ headers = {
17
+ "Authorization": f"{api_key}",
18
+ "Content-Type": "application/json"
19
+ }
20
+
21
+ query = """
22
+ query Issues($teamKey: String!, $issueNumber: Float) {
23
+ issues(filter: {team: {key: {eq: $teamKey}}, number: {eq: $issueNumber}}) {
24
+ nodes {
25
+ id
26
+ identifier
27
+ title
28
+ description
29
+ url
30
+ }
31
+ }
32
+ }
33
+ """
34
+ team_key, issue_number = issue_key.split("-")
35
+ response = requests.post(
36
+ url,
37
+ json={
38
+ "query": query,
39
+ "variables": {'teamKey': team_key, 'issueNumber': int(issue_number)}
40
+ },
41
+ headers=headers
42
+ )
43
+ response.raise_for_status()
44
+ data = response.json()
45
+
46
+ if "errors" in data:
47
+ logging.error(f"Linear API error: {data['errors']}")
48
+ return None
49
+
50
+ nodes = data.get("data", {}).get("issues", {}).get("nodes", [])
51
+ if not nodes:
52
+ logging.error(f"Linear issue {issue_key} not found")
53
+ return None
54
+
55
+ issue = nodes[0]
56
+ return IssueTrackerIssue(
57
+ title=issue["title"],
58
+ description=issue.get("description") or "",
59
+ url=issue["url"]
60
+ )
61
+
62
+ except requests.HTTPError as e:
63
+ logging.error(f"Failed to fetch Linear issue {issue_key}: {e}")
64
+ logging.error(f"Response body: {response.text}")
65
+ return None
66
+
67
+
68
+ def fetch_associated_issue(
69
+ repo: git.Repo,
70
+ api_key=None,
71
+ **kwargs
72
+ ):
73
+ """
74
+ Pipeline step to fetch a Linear issue based on the current branch name.
75
+ """
76
+ api_key = api_key or os.getenv("LINEAR_API_KEY")
77
+ if not api_key:
78
+ logging.error("LINEAR_API_KEY environment variable is not set")
79
+ return
80
+
81
+ issue_key = resolve_issue_key(repo)
82
+ return dict(
83
+ associated_issue=fetch_issue(issue_key, api_key)
84
+ ) if issue_key else None
gito/project_config.py CHANGED
@@ -1,119 +1,73 @@
1
- import re
2
- import logging
3
- import tomllib
4
- from dataclasses import dataclass, field
5
- from pathlib import Path
6
-
7
- import microcore as mc
8
- from microcore import ui
9
- from git import Repo
10
-
11
- from .constants import PROJECT_CONFIG_BUNDLED_DEFAULTS_FILE, PROJECT_CONFIG_FILE_PATH
12
-
13
-
14
- def _detect_github_env() -> dict:
15
- """
16
- Try to detect GitHub repository/PR info from environment variables (for GitHub Actions).
17
- Returns a dict with github_repo, github_pr_sha, github_pr_number, github_ref, etc.
18
- """
19
- import os
20
-
21
- repo = os.environ.get("GITHUB_REPOSITORY", "")
22
- pr_sha = os.environ.get("GITHUB_SHA", "")
23
- pr_number = os.environ.get("GITHUB_REF", "")
24
- branch = ""
25
- ref = os.environ.get("GITHUB_REF", "")
26
- # Try to resolve PR head SHA if available.
27
- # On PRs, GITHUB_HEAD_REF/BASE_REF contain branch names.
28
- if "GITHUB_HEAD_REF" in os.environ:
29
- branch = os.environ["GITHUB_HEAD_REF"]
30
- elif ref.startswith("refs/heads/"):
31
- branch = ref[len("refs/heads/"):]
32
- elif ref.startswith("refs/pull/"):
33
- # for pull_request events
34
- branch = ref
35
-
36
- d = {
37
- "github_repo": repo,
38
- "github_pr_sha": pr_sha,
39
- "github_pr_number": pr_number,
40
- "github_branch": branch,
41
- "github_ref": ref,
42
- }
43
- # Fallback for local usage: try to get from git
44
- if not repo:
45
- git_repo = None
46
- try:
47
- git_repo = Repo(".", search_parent_directories=True)
48
- origin = git_repo.remotes.origin.url
49
- # e.g. git@github.com:Nayjest/ai-code-review.git -> Nayjest/ai-code-review
50
- match = re.search(r"[:/]([\w\-]+)/([\w\-\.]+?)(\.git)?$", origin)
51
- if match:
52
- d["github_repo"] = f"{match.group(1)}/{match.group(2)}"
53
- d["github_pr_sha"] = git_repo.head.commit.hexsha
54
- d["github_branch"] = (
55
- git_repo.active_branch.name if hasattr(git_repo, "active_branch") else ""
56
- )
57
- except Exception:
58
- pass
59
- finally:
60
- if git_repo:
61
- try:
62
- git_repo.close()
63
- except Exception:
64
- pass
65
- # If branch is not a commit SHA, prefer branch for links
66
- if d["github_branch"]:
67
- d["github_pr_sha_or_branch"] = d["github_branch"]
68
- elif d["github_pr_sha"]:
69
- d["github_pr_sha_or_branch"] = d["github_pr_sha"]
70
- else:
71
- d["github_pr_sha_or_branch"] = "main"
72
- return d
73
-
74
-
75
- @dataclass
76
- class ProjectConfig:
77
- prompt: str = ""
78
- summary_prompt: str = ""
79
- report_template_md: str = ""
80
- """Markdown report template"""
81
- report_template_cli: str = ""
82
- """Report template for CLI output"""
83
- post_process: str = ""
84
- retries: int = 3
85
- """LLM retries for one request"""
86
- max_code_tokens: int = 32000
87
- prompt_vars: dict = field(default_factory=dict)
88
-
89
- @staticmethod
90
- def _read_bundled_defaults() -> dict:
91
- with open(PROJECT_CONFIG_BUNDLED_DEFAULTS_FILE, "rb") as f:
92
- config = tomllib.load(f)
93
- return config
94
-
95
- @staticmethod
96
- def load_for_repo(repo: Repo):
97
- return ProjectConfig.load(Path(repo.working_tree_dir) / PROJECT_CONFIG_FILE_PATH)
98
-
99
- @staticmethod
100
- def load(config_path: str | Path | None = None) -> "ProjectConfig":
101
- config = ProjectConfig._read_bundled_defaults()
102
- github_env = _detect_github_env()
103
- config["prompt_vars"] |= github_env | dict(github_env=github_env)
104
-
105
- config_path = Path(config_path or PROJECT_CONFIG_FILE_PATH)
106
- if config_path.exists():
107
- logging.info(
108
- f"Loading project-specific configuration from {mc.utils.file_link(config_path)}...")
109
- default_prompt_vars = config["prompt_vars"]
110
- with open(config_path, "rb") as f:
111
- config.update(tomllib.load(f))
112
- # overriding prompt_vars config section will not empty default values
113
- config["prompt_vars"] = default_prompt_vars | config["prompt_vars"]
114
- else:
115
- logging.info(
116
- f"No project config found at {ui.blue(config_path)}, using defaults"
117
- )
118
-
119
- return ProjectConfig(**config)
1
+ import logging
2
+ import tomllib
3
+ from dataclasses import dataclass, field
4
+ from pathlib import Path
5
+
6
+ import microcore as mc
7
+ from gito.utils import detect_github_env
8
+ from microcore import ui
9
+ from git import Repo
10
+
11
+ from .constants import PROJECT_CONFIG_BUNDLED_DEFAULTS_FILE, PROJECT_CONFIG_FILE_PATH
12
+ from .pipeline import PipelineStep
13
+
14
+
15
+ @dataclass
16
+ class ProjectConfig:
17
+ prompt: str = ""
18
+ summary_prompt: str = ""
19
+ answer_prompt: str = ""
20
+ report_template_md: str = ""
21
+ """Markdown report template"""
22
+ report_template_cli: str = ""
23
+ """Report template for CLI output"""
24
+ post_process: str = ""
25
+ retries: int = 3
26
+ """LLM retries for one request"""
27
+ max_code_tokens: int = 32000
28
+ prompt_vars: dict = field(default_factory=dict)
29
+ mention_triggers: list[str] = field(default_factory=list)
30
+ answer_github_comments: bool = field(default=True)
31
+ """
32
+ Defines the keyword or mention tag that triggers bot actions
33
+ when referenced in code review comments.
34
+ """
35
+ pipeline_steps: dict[str, dict | PipelineStep] = field(default_factory=dict)
36
+
37
+ def __post_init__(self):
38
+ self.pipeline_steps = {
39
+ k: PipelineStep(**v) if isinstance(v, dict) else v
40
+ for k, v in self.pipeline_steps.items()
41
+ }
42
+
43
+ @staticmethod
44
+ def _read_bundled_defaults() -> dict:
45
+ with open(PROJECT_CONFIG_BUNDLED_DEFAULTS_FILE, "rb") as f:
46
+ config = tomllib.load(f)
47
+ return config
48
+
49
+ @staticmethod
50
+ def load_for_repo(repo: Repo):
51
+ return ProjectConfig.load(Path(repo.working_tree_dir) / PROJECT_CONFIG_FILE_PATH)
52
+
53
+ @staticmethod
54
+ def load(config_path: str | Path | None = None) -> "ProjectConfig":
55
+ config = ProjectConfig._read_bundled_defaults()
56
+ github_env = detect_github_env()
57
+ config["prompt_vars"] |= github_env | dict(github_env=github_env)
58
+
59
+ config_path = Path(config_path or PROJECT_CONFIG_FILE_PATH)
60
+ if config_path.exists():
61
+ logging.info(
62
+ f"Loading project-specific configuration from {mc.utils.file_link(config_path)}...")
63
+ default_prompt_vars = config["prompt_vars"]
64
+ with open(config_path, "rb") as f:
65
+ config.update(tomllib.load(f))
66
+ # overriding prompt_vars config section will not empty default values
67
+ config["prompt_vars"] = default_prompt_vars | config["prompt_vars"]
68
+ else:
69
+ logging.info(
70
+ f"No project config found at {ui.blue(config_path)}, using defaults"
71
+ )
72
+
73
+ return ProjectConfig(**config)