xai-review 0.25.0__py3-none-any.whl → 0.26.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.

Potentially problematic release.


This version of xai-review might be problematic. Click here for more details.

File without changes
@@ -0,0 +1,31 @@
1
+ from ai_review.clients.bitbucket.pr.client import BitbucketPullRequestsHTTPClient
2
+ from httpx import AsyncClient, AsyncHTTPTransport
3
+
4
+ from ai_review.config import settings
5
+ from ai_review.libs.http.event_hooks.logger import LoggerEventHook
6
+ from ai_review.libs.http.transports.retry import RetryTransport
7
+ from ai_review.libs.logger import get_logger
8
+
9
+
10
+ class BitbucketHTTPClient:
11
+ def __init__(self, client: AsyncClient):
12
+ self.pr = BitbucketPullRequestsHTTPClient(client)
13
+
14
+
15
+ def get_bitbucket_http_client() -> BitbucketHTTPClient:
16
+ logger = get_logger("BITBUCKET_HTTP_CLIENT")
17
+ logger_event_hook = LoggerEventHook(logger=logger)
18
+ retry_transport = RetryTransport(logger=logger, transport=AsyncHTTPTransport())
19
+
20
+ client = AsyncClient(
21
+ timeout=settings.llm.http_client.timeout,
22
+ headers={"Authorization": f"Bearer {settings.vcs.http_client.api_token_value}"},
23
+ base_url=settings.vcs.http_client.api_url_value,
24
+ transport=retry_transport,
25
+ event_hooks={
26
+ "request": [logger_event_hook.request],
27
+ "response": [logger_event_hook.response],
28
+ }
29
+ )
30
+
31
+ return BitbucketHTTPClient(client=client)
File without changes
@@ -0,0 +1,104 @@
1
+ from httpx import Response, QueryParams
2
+
3
+ from ai_review.clients.bitbucket.pr.schema.comments import (
4
+ BitbucketGetPRCommentsQuerySchema,
5
+ BitbucketGetPRCommentsResponseSchema,
6
+ BitbucketCreatePRCommentRequestSchema,
7
+ BitbucketCreatePRCommentResponseSchema,
8
+ )
9
+ from ai_review.clients.bitbucket.pr.schema.files import (
10
+ BitbucketGetPRFilesQuerySchema,
11
+ BitbucketGetPRFilesResponseSchema,
12
+ )
13
+ from ai_review.clients.bitbucket.pr.schema.pull_request import BitbucketGetPRResponseSchema
14
+ from ai_review.clients.bitbucket.pr.types import BitbucketPullRequestsHTTPClientProtocol
15
+ from ai_review.libs.http.client import HTTPClient
16
+ from ai_review.libs.http.handlers import handle_http_error, HTTPClientError
17
+
18
+
19
+ class BitbucketPullRequestsHTTPClientError(HTTPClientError):
20
+ pass
21
+
22
+
23
+ class BitbucketPullRequestsHTTPClient(HTTPClient, BitbucketPullRequestsHTTPClientProtocol):
24
+ @handle_http_error(client="BitbucketPullRequestsHTTPClient", exception=BitbucketPullRequestsHTTPClientError)
25
+ async def get_pull_request_api(self, workspace: str, repo_slug: str, pull_request_id: str) -> Response:
26
+ return await self.get(f"/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}")
27
+
28
+ @handle_http_error(client="BitbucketPullRequestsHTTPClient", exception=BitbucketPullRequestsHTTPClientError)
29
+ async def get_diffstat_api(
30
+ self,
31
+ workspace: str,
32
+ repo_slug: str,
33
+ pull_request_id: str,
34
+ query: BitbucketGetPRFilesQuerySchema,
35
+ ) -> Response:
36
+ return await self.get(
37
+ f"/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/diffstat",
38
+ query=QueryParams(**query.model_dump()),
39
+ )
40
+
41
+ @handle_http_error(client="BitbucketPullRequestsHTTPClient", exception=BitbucketPullRequestsHTTPClientError)
42
+ async def get_comments_api(
43
+ self,
44
+ workspace: str,
45
+ repo_slug: str,
46
+ pull_request_id: str,
47
+ query: BitbucketGetPRCommentsQuerySchema,
48
+ ) -> Response:
49
+ return await self.get(
50
+ f"/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/comments",
51
+ query=QueryParams(**query.model_dump()),
52
+ )
53
+
54
+ @handle_http_error(client="BitbucketPullRequestsHTTPClient", exception=BitbucketPullRequestsHTTPClientError)
55
+ async def create_comment_api(
56
+ self,
57
+ workspace: str,
58
+ repo_slug: str,
59
+ pull_request_id: str,
60
+ request: BitbucketCreatePRCommentRequestSchema,
61
+ ) -> Response:
62
+ return await self.post(
63
+ f"/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/comments",
64
+ json=request.model_dump(by_alias=True),
65
+ )
66
+
67
+ async def get_pull_request(
68
+ self,
69
+ workspace: str,
70
+ repo_slug: str,
71
+ pull_request_id: str
72
+ ) -> BitbucketGetPRResponseSchema:
73
+ resp = await self.get_pull_request_api(workspace, repo_slug, pull_request_id)
74
+ return BitbucketGetPRResponseSchema.model_validate_json(resp.text)
75
+
76
+ async def get_files(
77
+ self,
78
+ workspace: str,
79
+ repo_slug: str,
80
+ pull_request_id: str
81
+ ) -> BitbucketGetPRFilesResponseSchema:
82
+ query = BitbucketGetPRFilesQuerySchema(pagelen=100)
83
+ resp = await self.get_diffstat_api(workspace, repo_slug, pull_request_id, query)
84
+ return BitbucketGetPRFilesResponseSchema.model_validate_json(resp.text)
85
+
86
+ async def get_comments(
87
+ self,
88
+ workspace: str,
89
+ repo_slug: str,
90
+ pull_request_id: str
91
+ ) -> BitbucketGetPRCommentsResponseSchema:
92
+ query = BitbucketGetPRCommentsQuerySchema(pagelen=100)
93
+ response = await self.get_comments_api(workspace, repo_slug, pull_request_id, query)
94
+ return BitbucketGetPRCommentsResponseSchema.model_validate_json(response.text)
95
+
96
+ async def create_comment(
97
+ self,
98
+ workspace: str,
99
+ repo_slug: str,
100
+ pull_request_id: str,
101
+ request: BitbucketCreatePRCommentRequestSchema
102
+ ) -> BitbucketCreatePRCommentResponseSchema:
103
+ response = await self.create_comment_api(workspace, repo_slug, pull_request_id, request)
104
+ return BitbucketCreatePRCommentResponseSchema.model_validate_json(response.text)
File without changes
@@ -0,0 +1,44 @@
1
+ from pydantic import BaseModel, Field, ConfigDict
2
+
3
+
4
+ class BitbucketCommentContentSchema(BaseModel):
5
+ raw: str
6
+ html: str | None = None
7
+ markup: str | None = None
8
+
9
+
10
+ class BitbucketCommentInlineSchema(BaseModel):
11
+ model_config = ConfigDict(populate_by_name=True)
12
+
13
+ path: str
14
+ to_line: int | None = Field(alias="to", default=None)
15
+ from_line: int | None = Field(alias="from", default=None)
16
+
17
+
18
+ class BitbucketPRCommentSchema(BaseModel):
19
+ id: int
20
+ inline: BitbucketCommentInlineSchema | None = None
21
+ content: BitbucketCommentContentSchema
22
+
23
+
24
+ class BitbucketGetPRCommentsQuerySchema(BaseModel):
25
+ pagelen: int = 100
26
+
27
+
28
+ class BitbucketGetPRCommentsResponseSchema(BaseModel):
29
+ size: int
30
+ page: int | None = None
31
+ next: str | None = None
32
+ values: list[BitbucketPRCommentSchema]
33
+ pagelen: int
34
+
35
+
36
+ class BitbucketCreatePRCommentRequestSchema(BaseModel):
37
+ inline: BitbucketCommentInlineSchema | None = None
38
+ content: BitbucketCommentContentSchema
39
+
40
+
41
+ class BitbucketCreatePRCommentResponseSchema(BaseModel):
42
+ id: int
43
+ inline: BitbucketCommentInlineSchema | None = None
44
+ content: BitbucketCommentContentSchema
@@ -0,0 +1,25 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class BitbucketPRFilePathSchema(BaseModel):
5
+ path: str
6
+
7
+
8
+ class BitbucketPRFileSchema(BaseModel):
9
+ new: BitbucketPRFilePathSchema | None = None
10
+ old: BitbucketPRFilePathSchema | None = None
11
+ status: str
12
+ lines_added: int
13
+ lines_removed: int
14
+
15
+
16
+ class BitbucketGetPRFilesQuerySchema(BaseModel):
17
+ pagelen: int = 100
18
+
19
+
20
+ class BitbucketGetPRFilesResponseSchema(BaseModel):
21
+ size: int
22
+ page: int | None = None
23
+ next: str | None = None
24
+ values: list[BitbucketPRFileSchema]
25
+ pagelen: int
@@ -0,0 +1,38 @@
1
+ from pydantic import BaseModel, Field
2
+
3
+
4
+ class BitbucketUserSchema(BaseModel):
5
+ uuid: str
6
+ nickname: str
7
+ display_name: str
8
+
9
+
10
+ class BitbucketBranchSchema(BaseModel):
11
+ name: str
12
+
13
+
14
+ class BitbucketCommitSchema(BaseModel):
15
+ hash: str
16
+
17
+
18
+ class BitbucketRepositorySchema(BaseModel):
19
+ uuid: str
20
+ full_name: str
21
+
22
+
23
+ class BitbucketPRLocationSchema(BaseModel):
24
+ branch: BitbucketBranchSchema
25
+ commit: BitbucketCommitSchema
26
+ repository: BitbucketRepositorySchema
27
+
28
+
29
+ class BitbucketGetPRResponseSchema(BaseModel):
30
+ id: int
31
+ title: str
32
+ description: str | None = None
33
+ state: str
34
+ author: BitbucketUserSchema
35
+ source: BitbucketPRLocationSchema
36
+ destination: BitbucketPRLocationSchema
37
+ reviewers: list[BitbucketUserSchema] = Field(default_factory=list)
38
+ participants: list[BitbucketUserSchema] = Field(default_factory=list)
@@ -0,0 +1,44 @@
1
+ from typing import Protocol
2
+
3
+ from ai_review.clients.bitbucket.pr.schema.comments import (
4
+ BitbucketGetPRCommentsResponseSchema,
5
+ BitbucketCreatePRCommentRequestSchema,
6
+ BitbucketCreatePRCommentResponseSchema,
7
+ )
8
+ from ai_review.clients.bitbucket.pr.schema.files import BitbucketGetPRFilesResponseSchema
9
+ from ai_review.clients.bitbucket.pr.schema.pull_request import BitbucketGetPRResponseSchema
10
+
11
+
12
+ class BitbucketPullRequestsHTTPClientProtocol(Protocol):
13
+ async def get_pull_request(
14
+ self,
15
+ workspace: str,
16
+ repo_slug: str,
17
+ pull_request_id: str
18
+ ) -> BitbucketGetPRResponseSchema:
19
+ ...
20
+
21
+ async def get_files(
22
+ self,
23
+ workspace: str,
24
+ repo_slug: str,
25
+ pull_request_id: str
26
+ ) -> BitbucketGetPRFilesResponseSchema:
27
+ ...
28
+
29
+ async def get_comments(
30
+ self,
31
+ workspace: str,
32
+ repo_slug: str,
33
+ pull_request_id: str
34
+ ) -> BitbucketGetPRCommentsResponseSchema:
35
+ ...
36
+
37
+ async def create_comment(
38
+ self,
39
+ workspace: str,
40
+ repo_slug: str,
41
+ pull_request_id: str,
42
+ request: BitbucketCreatePRCommentRequestSchema,
43
+ ) -> BitbucketCreatePRCommentResponseSchema:
44
+ ...
@@ -2,6 +2,7 @@ from typing import Annotated, Literal
2
2
 
3
3
  from pydantic import BaseModel, Field
4
4
 
5
+ from ai_review.libs.config.vcs.bitbucket import BitbucketPipelineConfig, BitbucketHTTPClientConfig
5
6
  from ai_review.libs.config.vcs.github import GitHubPipelineConfig, GitHubHTTPClientConfig
6
7
  from ai_review.libs.config.vcs.gitlab import GitLabPipelineConfig, GitLabHTTPClientConfig
7
8
  from ai_review.libs.constants.vcs_provider import VCSProvider
@@ -23,4 +24,13 @@ class GitHubVCSConfig(VCSConfigBase):
23
24
  http_client: GitHubHTTPClientConfig
24
25
 
25
26
 
26
- VCSConfig = Annotated[GitLabVCSConfig | GitHubVCSConfig, Field(discriminator="provider")]
27
+ class BitbucketVCSConfig(VCSConfigBase):
28
+ provider: Literal[VCSProvider.BITBUCKET]
29
+ pipeline: BitbucketPipelineConfig
30
+ http_client: BitbucketHTTPClientConfig
31
+
32
+
33
+ VCSConfig = Annotated[
34
+ GitLabVCSConfig | GitHubVCSConfig | BitbucketVCSConfig,
35
+ Field(discriminator="provider")
36
+ ]
@@ -0,0 +1,13 @@
1
+ from pydantic import BaseModel
2
+
3
+ from ai_review.libs.config.http import HTTPClientWithTokenConfig
4
+
5
+
6
+ class BitbucketPipelineConfig(BaseModel):
7
+ workspace: str
8
+ repo_slug: str
9
+ pull_request_id: str
10
+
11
+
12
+ class BitbucketHTTPClientConfig(HTTPClientWithTokenConfig):
13
+ pass
@@ -4,3 +4,4 @@ from enum import StrEnum
4
4
  class VCSProvider(StrEnum):
5
5
  GITHUB = "GITHUB"
6
6
  GITLAB = "GITLAB"
7
+ BITBUCKET = "BITBUCKET"
File without changes
@@ -0,0 +1,185 @@
1
+ from ai_review.clients.bitbucket.client import get_bitbucket_http_client
2
+ from ai_review.clients.bitbucket.pr.schema.comments import (
3
+ BitbucketCommentInlineSchema,
4
+ BitbucketCommentContentSchema,
5
+ BitbucketCreatePRCommentRequestSchema,
6
+ )
7
+ from ai_review.config import settings
8
+ from ai_review.libs.logger import get_logger
9
+ from ai_review.services.vcs.types import (
10
+ VCSClientProtocol,
11
+ UserSchema,
12
+ BranchRefSchema,
13
+ ReviewInfoSchema,
14
+ ReviewCommentSchema,
15
+ )
16
+
17
+ logger = get_logger("BITBUCKET_VCS_CLIENT")
18
+
19
+
20
+ class BitbucketVCSClient(VCSClientProtocol):
21
+ def __init__(self):
22
+ self.http_client = get_bitbucket_http_client()
23
+ self.workspace = settings.vcs.pipeline.workspace
24
+ self.repo_slug = settings.vcs.pipeline.repo_slug
25
+ self.pull_request_id = settings.vcs.pipeline.pull_request_id
26
+
27
+ async def get_review_info(self) -> ReviewInfoSchema:
28
+ try:
29
+ pr = await self.http_client.pr.get_pull_request(
30
+ workspace=self.workspace,
31
+ repo_slug=self.repo_slug,
32
+ pull_request_id=self.pull_request_id,
33
+ )
34
+ files = await self.http_client.pr.get_files(
35
+ workspace=self.workspace,
36
+ repo_slug=self.repo_slug,
37
+ pull_request_id=self.pull_request_id,
38
+ )
39
+
40
+ logger.info(f"Fetched PR info for {self.workspace}/{self.repo_slug}#{self.pull_request_id}")
41
+
42
+ return ReviewInfoSchema(
43
+ id=pr.id,
44
+ title=pr.title,
45
+ description=pr.description or "",
46
+ author=UserSchema(
47
+ id=pr.author.uuid,
48
+ name=pr.author.display_name,
49
+ username=pr.author.nickname,
50
+ ),
51
+ labels=[],
52
+ base_sha=pr.destination.commit.hash,
53
+ head_sha=pr.source.commit.hash,
54
+ assignees=[
55
+ UserSchema(
56
+ id=user.uuid,
57
+ name=user.display_name,
58
+ username=user.nickname,
59
+ )
60
+ for user in pr.participants
61
+ ],
62
+ reviewers=[
63
+ UserSchema(
64
+ id=user.uuid,
65
+ name=user.display_name,
66
+ username=user.nickname,
67
+ )
68
+ for user in pr.reviewers
69
+ ],
70
+ source_branch=BranchRefSchema(
71
+ ref=pr.source.branch.name,
72
+ sha=pr.source.commit.hash,
73
+ ),
74
+ target_branch=BranchRefSchema(
75
+ ref=pr.destination.branch.name,
76
+ sha=pr.destination.commit.hash,
77
+ ),
78
+ changed_files=[
79
+ file.new.path if file.new else file.old.path
80
+ for file in files.values
81
+ ],
82
+ )
83
+ except Exception as error:
84
+ logger.exception(
85
+ f"Failed to fetch PR info {self.workspace}/{self.repo_slug}#{self.pull_request_id}: {error}"
86
+ )
87
+ return ReviewInfoSchema()
88
+
89
+ async def get_general_comments(self) -> list[ReviewCommentSchema]:
90
+ try:
91
+ response = await self.http_client.pr.get_comments(
92
+ workspace=self.workspace,
93
+ repo_slug=self.repo_slug,
94
+ pull_request_id=self.pull_request_id,
95
+ )
96
+ logger.info(f"Fetched general comments for {self.workspace}/{self.repo_slug}#{self.pull_request_id}")
97
+
98
+ return [
99
+ ReviewCommentSchema(id=comment.id, body=comment.content.raw)
100
+ for comment in response.values
101
+ if comment.inline is None
102
+ ]
103
+ except Exception as error:
104
+ logger.exception(
105
+ f"Failed to fetch general comments for "
106
+ f"{self.workspace}/{self.repo_slug}#{self.pull_request_id}: {error}"
107
+ )
108
+ return []
109
+
110
+ async def get_inline_comments(self) -> list[ReviewCommentSchema]:
111
+ try:
112
+ response = await self.http_client.pr.get_comments(
113
+ workspace=self.workspace,
114
+ repo_slug=self.repo_slug,
115
+ pull_request_id=self.pull_request_id,
116
+ )
117
+ logger.info(f"Fetched inline comments for {self.workspace}/{self.repo_slug}#{self.pull_request_id}")
118
+
119
+ return [
120
+ ReviewCommentSchema(
121
+ id=comment.id,
122
+ body=comment.content.raw,
123
+ file=comment.inline.path,
124
+ line=comment.inline.to_line,
125
+ )
126
+ for comment in response.values
127
+ if comment.inline is not None
128
+ ]
129
+ except Exception as error:
130
+ logger.exception(
131
+ f"Failed to fetch inline comments for "
132
+ f"{self.workspace}/{self.repo_slug}#{self.pull_request_id}: {error}"
133
+ )
134
+ return []
135
+
136
+ async def create_general_comment(self, message: str) -> None:
137
+ try:
138
+ logger.info(
139
+ f"Posting general comment to PR {self.workspace}/{self.repo_slug}#{self.pull_request_id}: {message}"
140
+ )
141
+ request = BitbucketCreatePRCommentRequestSchema(
142
+ content=BitbucketCommentContentSchema(raw=message)
143
+ )
144
+ await self.http_client.pr.create_comment(
145
+ workspace=self.workspace,
146
+ repo_slug=self.repo_slug,
147
+ pull_request_id=self.pull_request_id,
148
+ request=request,
149
+ )
150
+ logger.info(
151
+ f"Created general comment in PR {self.workspace}/{self.repo_slug}#{self.pull_request_id}"
152
+ )
153
+ except Exception as error:
154
+ logger.exception(
155
+ f"Failed to create general comment in PR "
156
+ f"{self.workspace}/{self.repo_slug}#{self.pull_request_id}: {error}"
157
+ )
158
+ raise
159
+
160
+ async def create_inline_comment(self, file: str, line: int, message: str) -> None:
161
+ try:
162
+ logger.info(
163
+ f"Posting inline comment in {self.workspace}/{self.repo_slug}#{self.pull_request_id} "
164
+ f"at {file}:{line}: {message}"
165
+ )
166
+ request = BitbucketCreatePRCommentRequestSchema(
167
+ content=BitbucketCommentContentSchema(raw=message),
168
+ inline=BitbucketCommentInlineSchema(path=file, to_line=line),
169
+ )
170
+ await self.http_client.pr.create_comment(
171
+ workspace=self.workspace,
172
+ repo_slug=self.repo_slug,
173
+ pull_request_id=self.pull_request_id,
174
+ request=request,
175
+ )
176
+ logger.info(
177
+ f"Created inline comment in {self.workspace}/{self.repo_slug}#{self.pull_request_id} "
178
+ f"at {file}:{line}"
179
+ )
180
+ except Exception as error:
181
+ logger.exception(
182
+ f"Failed to create inline comment in {self.workspace}/{self.repo_slug}#{self.pull_request_id} "
183
+ f"at {file}:{line}: {error}"
184
+ )
185
+ raise
@@ -1,5 +1,6 @@
1
1
  from ai_review.config import settings
2
2
  from ai_review.libs.constants.vcs_provider import VCSProvider
3
+ from ai_review.services.vcs.bitbucket.client import BitbucketVCSClient
3
4
  from ai_review.services.vcs.github.client import GitHubVCSClient
4
5
  from ai_review.services.vcs.gitlab.client import GitLabVCSClient
5
6
  from ai_review.services.vcs.types import VCSClientProtocol
@@ -11,5 +12,7 @@ def get_vcs_client() -> VCSClientProtocol:
11
12
  return GitLabVCSClient()
12
13
  case VCSProvider.GITHUB:
13
14
  return GitHubVCSClient()
15
+ case VCSProvider.BITBUCKET:
16
+ return BitbucketVCSClient()
14
17
  case _:
15
18
  raise ValueError(f"Unsupported VCS provider: {settings.vcs.provider}")
@@ -0,0 +1,204 @@
1
+ import pytest
2
+ from pydantic import HttpUrl, SecretStr
3
+
4
+ from ai_review.clients.bitbucket.pr.schema.comments import (
5
+ BitbucketPRCommentSchema,
6
+ BitbucketCommentContentSchema,
7
+ BitbucketCommentInlineSchema,
8
+ BitbucketGetPRCommentsResponseSchema,
9
+ BitbucketCreatePRCommentRequestSchema,
10
+ BitbucketCreatePRCommentResponseSchema,
11
+ )
12
+ from ai_review.clients.bitbucket.pr.schema.files import (
13
+ BitbucketGetPRFilesResponseSchema,
14
+ BitbucketPRFileSchema,
15
+ BitbucketPRFilePathSchema,
16
+ )
17
+ from ai_review.clients.bitbucket.pr.schema.pull_request import (
18
+ BitbucketUserSchema,
19
+ BitbucketBranchSchema,
20
+ BitbucketCommitSchema,
21
+ BitbucketRepositorySchema,
22
+ BitbucketPRLocationSchema,
23
+ BitbucketGetPRResponseSchema,
24
+ )
25
+ from ai_review.clients.bitbucket.pr.types import BitbucketPullRequestsHTTPClientProtocol
26
+ from ai_review.config import settings
27
+ from ai_review.libs.config.vcs.base import BitbucketVCSConfig
28
+ from ai_review.libs.config.vcs.bitbucket import BitbucketPipelineConfig, BitbucketHTTPClientConfig
29
+ from ai_review.libs.constants.vcs_provider import VCSProvider
30
+ from ai_review.services.vcs.bitbucket.client import BitbucketVCSClient
31
+
32
+
33
+ class FakeBitbucketPullRequestsHTTPClient(BitbucketPullRequestsHTTPClientProtocol):
34
+ def __init__(self):
35
+ self.calls: list[tuple[str, dict]] = []
36
+
37
+ async def get_pull_request(
38
+ self,
39
+ workspace: str,
40
+ repo_slug: str,
41
+ pull_request_id: str
42
+ ) -> BitbucketGetPRResponseSchema:
43
+ self.calls.append(
44
+ (
45
+ "get_pull_request",
46
+ {"workspace": workspace, "repo_slug": repo_slug, "pull_request_id": pull_request_id}
47
+ )
48
+ )
49
+ return BitbucketGetPRResponseSchema(
50
+ id=1,
51
+ title="Fake Bitbucket PR",
52
+ description="This is a fake PR for testing",
53
+ state="OPEN",
54
+ author=BitbucketUserSchema(uuid="u1", display_name="Tester", nickname="tester"),
55
+ source=BitbucketPRLocationSchema(
56
+ commit=BitbucketCommitSchema(hash="def456"),
57
+ branch=BitbucketBranchSchema(name="feature/test"),
58
+ repository=BitbucketRepositorySchema(uuid="r1", full_name="workspace/repo"),
59
+ ),
60
+ destination=BitbucketPRLocationSchema(
61
+ commit=BitbucketCommitSchema(hash="abc123"),
62
+ branch=BitbucketBranchSchema(name="main"),
63
+ repository=BitbucketRepositorySchema(uuid="r1", full_name="workspace/repo"),
64
+ ),
65
+ reviewers=[BitbucketUserSchema(uuid="u2", display_name="Reviewer", nickname="reviewer")],
66
+ participants=[BitbucketUserSchema(uuid="u3", display_name="Participant", nickname="participant")],
67
+ )
68
+
69
+ async def get_files(
70
+ self,
71
+ workspace: str,
72
+ repo_slug: str,
73
+ pull_request_id: str
74
+ ) -> BitbucketGetPRFilesResponseSchema:
75
+ self.calls.append(
76
+ (
77
+ "get_files",
78
+ {"workspace": workspace, "repo_slug": repo_slug, "pull_request_id": pull_request_id}
79
+ )
80
+ )
81
+ return BitbucketGetPRFilesResponseSchema(
82
+ size=2,
83
+ page=1,
84
+ pagelen=100,
85
+ next=None,
86
+ values=[
87
+ BitbucketPRFileSchema(
88
+ new=BitbucketPRFilePathSchema(path="app/main.py"),
89
+ old=None,
90
+ status="modified",
91
+ lines_added=10,
92
+ lines_removed=2,
93
+ ),
94
+ BitbucketPRFileSchema(
95
+ new=BitbucketPRFilePathSchema(path="utils/helper.py"),
96
+ old=None,
97
+ status="added",
98
+ lines_added=5,
99
+ lines_removed=0,
100
+ ),
101
+ ],
102
+ )
103
+
104
+ async def get_comments(
105
+ self,
106
+ workspace: str,
107
+ repo_slug: str,
108
+ pull_request_id: str
109
+ ) -> BitbucketGetPRCommentsResponseSchema:
110
+ self.calls.append(
111
+ (
112
+ "get_comments",
113
+ {"workspace": workspace, "repo_slug": repo_slug, "pull_request_id": pull_request_id}
114
+ )
115
+ )
116
+ return BitbucketGetPRCommentsResponseSchema(
117
+ size=2,
118
+ page=1,
119
+ next=None,
120
+ values=[
121
+ BitbucketPRCommentSchema(
122
+ id=1,
123
+ inline=None,
124
+ content=BitbucketCommentContentSchema(raw="General comment"),
125
+ ),
126
+ BitbucketPRCommentSchema(
127
+ id=2,
128
+ inline=BitbucketCommentInlineSchema(path="file.py", to_line=5),
129
+ content=BitbucketCommentContentSchema(raw="Inline comment"),
130
+ ),
131
+ ],
132
+ pagelen=100,
133
+ )
134
+
135
+ async def create_comment(
136
+ self,
137
+ workspace: str,
138
+ repo_slug: str,
139
+ pull_request_id: str,
140
+ request: BitbucketCreatePRCommentRequestSchema
141
+ ) -> BitbucketCreatePRCommentResponseSchema:
142
+ self.calls.append(
143
+ (
144
+ "create_comment",
145
+ {
146
+ "workspace": workspace,
147
+ "repo_slug": repo_slug,
148
+ "pull_request_id": pull_request_id,
149
+ **request.model_dump(by_alias=True)
150
+ }
151
+ )
152
+ )
153
+ return BitbucketCreatePRCommentResponseSchema(
154
+ id=10,
155
+ content=request.content,
156
+ inline=request.inline,
157
+ )
158
+
159
+
160
+ class FakeBitbucketHTTPClient:
161
+ def __init__(self, pull_requests_client: BitbucketPullRequestsHTTPClientProtocol):
162
+ self.pr = pull_requests_client
163
+
164
+
165
+ @pytest.fixture
166
+ def fake_bitbucket_pull_requests_http_client() -> FakeBitbucketPullRequestsHTTPClient:
167
+ return FakeBitbucketPullRequestsHTTPClient()
168
+
169
+
170
+ @pytest.fixture
171
+ def fake_bitbucket_http_client(
172
+ fake_bitbucket_pull_requests_http_client: FakeBitbucketPullRequestsHTTPClient
173
+ ) -> FakeBitbucketHTTPClient:
174
+ return FakeBitbucketHTTPClient(pull_requests_client=fake_bitbucket_pull_requests_http_client)
175
+
176
+
177
+ @pytest.fixture
178
+ def bitbucket_vcs_client(
179
+ monkeypatch: pytest.MonkeyPatch,
180
+ fake_bitbucket_http_client: FakeBitbucketHTTPClient
181
+ ) -> BitbucketVCSClient:
182
+ monkeypatch.setattr(
183
+ "ai_review.services.vcs.bitbucket.client.get_bitbucket_http_client",
184
+ lambda: fake_bitbucket_http_client,
185
+ )
186
+ return BitbucketVCSClient()
187
+
188
+
189
+ @pytest.fixture
190
+ def bitbucket_http_client_config(monkeypatch: pytest.MonkeyPatch):
191
+ fake_config = BitbucketVCSConfig(
192
+ provider=VCSProvider.BITBUCKET,
193
+ pipeline=BitbucketPipelineConfig(
194
+ workspace="workspace",
195
+ repo_slug="repo",
196
+ pull_request_id="123",
197
+ ),
198
+ http_client=BitbucketHTTPClientConfig(
199
+ timeout=10,
200
+ api_url=HttpUrl("https://api.bitbucket.org/2.0"),
201
+ api_token=SecretStr("fake-token"),
202
+ )
203
+ )
204
+ monkeypatch.setattr(settings, "vcs", fake_config)
@@ -0,0 +1,117 @@
1
+ import pytest
2
+
3
+ from ai_review.services.vcs.bitbucket.client import BitbucketVCSClient
4
+ from ai_review.services.vcs.types import ReviewInfoSchema, ReviewCommentSchema
5
+ from ai_review.tests.fixtures.clients.bitbucket import FakeBitbucketPullRequestsHTTPClient
6
+
7
+
8
+ @pytest.mark.asyncio
9
+ @pytest.mark.usefixtures("bitbucket_http_client_config")
10
+ async def test_get_review_info_returns_valid_schema(
11
+ bitbucket_vcs_client: BitbucketVCSClient,
12
+ fake_bitbucket_pull_requests_http_client: FakeBitbucketPullRequestsHTTPClient,
13
+ ):
14
+ """Should return detailed PR info with branches, author, reviewers, and files."""
15
+ info = await bitbucket_vcs_client.get_review_info()
16
+
17
+ assert isinstance(info, ReviewInfoSchema)
18
+ assert info.id == 1
19
+ assert info.title == "Fake Bitbucket PR"
20
+ assert info.description == "This is a fake PR for testing"
21
+
22
+ assert info.author.username == "tester"
23
+ assert {r.username for r in info.reviewers} == {"reviewer"}
24
+
25
+ assert info.source_branch.ref == "feature/test"
26
+ assert info.target_branch.ref == "main"
27
+ assert info.base_sha == "abc123"
28
+ assert info.head_sha == "def456"
29
+
30
+ assert "app/main.py" in info.changed_files
31
+ assert len(info.changed_files) == 2
32
+
33
+ called_methods = [name for name, _ in fake_bitbucket_pull_requests_http_client.calls]
34
+ assert called_methods == ["get_pull_request", "get_files"]
35
+
36
+
37
+ @pytest.mark.asyncio
38
+ @pytest.mark.usefixtures("bitbucket_http_client_config")
39
+ async def test_get_general_comments_filters_inline(
40
+ bitbucket_vcs_client: BitbucketVCSClient,
41
+ fake_bitbucket_pull_requests_http_client: FakeBitbucketPullRequestsHTTPClient,
42
+ ):
43
+ """Should return only general comments (without inline info)."""
44
+ comments = await bitbucket_vcs_client.get_general_comments()
45
+
46
+ assert all(isinstance(c, ReviewCommentSchema) for c in comments)
47
+ assert len(comments) == 1
48
+
49
+ first = comments[0]
50
+ assert first.body == "General comment"
51
+ assert first.file is None
52
+ assert first.line is None
53
+
54
+ called_methods = [name for name, _ in fake_bitbucket_pull_requests_http_client.calls]
55
+ assert called_methods == ["get_comments"]
56
+
57
+
58
+ @pytest.mark.asyncio
59
+ @pytest.mark.usefixtures("bitbucket_http_client_config")
60
+ async def test_get_inline_comments_filters_general(
61
+ bitbucket_vcs_client: BitbucketVCSClient,
62
+ fake_bitbucket_pull_requests_http_client: FakeBitbucketPullRequestsHTTPClient,
63
+ ):
64
+ """Should return only inline comments with file and line references."""
65
+ comments = await bitbucket_vcs_client.get_inline_comments()
66
+
67
+ assert all(isinstance(c, ReviewCommentSchema) for c in comments)
68
+ assert len(comments) == 1
69
+
70
+ first = comments[0]
71
+ assert first.body == "Inline comment"
72
+ assert first.file == "file.py"
73
+ assert first.line == 5
74
+
75
+ called_methods = [name for name, _ in fake_bitbucket_pull_requests_http_client.calls]
76
+ assert called_methods == ["get_comments"]
77
+
78
+
79
+ @pytest.mark.asyncio
80
+ @pytest.mark.usefixtures("bitbucket_http_client_config")
81
+ async def test_create_general_comment_posts_comment(
82
+ bitbucket_vcs_client: BitbucketVCSClient,
83
+ fake_bitbucket_pull_requests_http_client: FakeBitbucketPullRequestsHTTPClient,
84
+ ):
85
+ """Should post a general (non-inline) comment."""
86
+ message = "Hello from Bitbucket test!"
87
+
88
+ await bitbucket_vcs_client.create_general_comment(message)
89
+
90
+ calls = [args for name, args in fake_bitbucket_pull_requests_http_client.calls if name == "create_comment"]
91
+ assert len(calls) == 1
92
+ call_args = calls[0]
93
+ assert call_args["content"]["raw"] == message
94
+ assert call_args["workspace"] == "workspace"
95
+ assert call_args["repo_slug"] == "repo"
96
+
97
+
98
+ @pytest.mark.asyncio
99
+ @pytest.mark.usefixtures("bitbucket_http_client_config")
100
+ async def test_create_inline_comment_posts_comment(
101
+ bitbucket_vcs_client: BitbucketVCSClient,
102
+ fake_bitbucket_pull_requests_http_client: FakeBitbucketPullRequestsHTTPClient,
103
+ ):
104
+ """Should post an inline comment with correct file and line."""
105
+ file = "file.py"
106
+ line = 10
107
+ message = "Looks good"
108
+
109
+ await bitbucket_vcs_client.create_inline_comment(file, line, message)
110
+
111
+ calls = [args for name, args in fake_bitbucket_pull_requests_http_client.calls if name == "create_comment"]
112
+ assert len(calls) == 1
113
+
114
+ call_args = calls[0]
115
+ assert call_args["content"]["raw"] == message
116
+ assert call_args["inline"]["path"] == file
117
+ assert call_args["inline"]["to"] == line
@@ -1,5 +1,6 @@
1
1
  import pytest
2
2
 
3
+ from ai_review.services.vcs.bitbucket.client import BitbucketVCSClient
3
4
  from ai_review.services.vcs.factory import get_vcs_client
4
5
  from ai_review.services.vcs.github.client import GitHubVCSClient
5
6
  from ai_review.services.vcs.gitlab.client import GitLabVCSClient
@@ -17,7 +18,13 @@ def test_get_vcs_client_returns_gitlab(monkeypatch: pytest.MonkeyPatch):
17
18
  assert isinstance(client, GitLabVCSClient)
18
19
 
19
20
 
21
+ @pytest.mark.usefixtures("bitbucket_http_client_config")
22
+ def test_get_vcs_client_returns_bitbucket(monkeypatch: pytest.MonkeyPatch):
23
+ client = get_vcs_client()
24
+ assert isinstance(client, BitbucketVCSClient)
25
+
26
+
20
27
  def test_get_vcs_client_unsupported_provider(monkeypatch: pytest.MonkeyPatch):
21
- monkeypatch.setattr("ai_review.services.vcs.factory.settings.vcs.provider", "BITBUCKET")
28
+ monkeypatch.setattr("ai_review.services.vcs.factory.settings.vcs.provider", "UNSUPPORTED")
22
29
  with pytest.raises(ValueError):
23
30
  get_vcs_client()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xai-review
3
- Version: 0.25.0
3
+ Version: 0.26.0
4
4
  Summary: AI-powered code review tool
5
5
  Author-email: Nikita Filonov <nikita.filonov@example.com>
6
6
  Maintainer-email: Nikita Filonov <nikita.filonov@example.com>
@@ -67,7 +67,7 @@ improve code quality, enforce consistency, and speed up the review process.
67
67
  ✨ Key features:
68
68
 
69
69
  - **Multiple LLM providers** — choose between **OpenAI**, **Claude**, **Gemini**, or **Ollama**, and switch anytime.
70
- - **VCS integration** — works out of the box with GitLab, GitHub (more providers coming).
70
+ - **VCS integration** — works out of the box with **GitLab**, **GitHub**, and **Bitbucket**.
71
71
  - **Customizable prompts** — adapt inline, context, and summary reviews to match your team’s coding guidelines.
72
72
  - **Flexible configuration** — supports `YAML`, `JSON`, and `ENV`, with seamless overrides in CI/CD pipelines.
73
73
  - **AI Review runs fully client-side** — it never proxies or inspects your requests.
@@ -170,7 +170,7 @@ Key things you can customize:
170
170
 
171
171
  - **LLM provider** — OpenAI, Gemini, Claude, or Ollama
172
172
  - **Model settings** — model name, temperature, max tokens
173
- - **VCS integration** — works out of the box with **GitLab** and **GitHub**.
173
+ - **VCS integration** — works out of the box with **GitLab**, **GitHub**, and **Bitbucket**
174
174
  - **Review policy** — which files to include/exclude, review modes
175
175
  - **Prompts** — inline/context/summary prompt templates
176
176
 
@@ -209,7 +209,7 @@ jobs:
209
209
  runs-on: ubuntu-latest
210
210
  steps:
211
211
  - uses: actions/checkout@v4
212
- - uses: Nikita-Filonov/ai-review@v0.25.0
212
+ - uses: Nikita-Filonov/ai-review@v0.26.0
213
213
  with:
214
214
  review-command: ${{ inputs.review-command }}
215
215
  env:
@@ -8,6 +8,15 @@ ai_review/cli/commands/run_inline_review.py,sha256=u55K-Su0PR2-NcK7XI2rTCIi7HTEi
8
8
  ai_review/cli/commands/run_review.py,sha256=i39IYNDE_lAiQQnKLmxG71Ao8WAIOSn82L9EpdbPcsI,261
9
9
  ai_review/cli/commands/run_summary_review.py,sha256=NqjepGH5cbqczPzcuMEAxO4dI58FEUZl0b6uRVQ9SiA,224
10
10
  ai_review/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ ai_review/clients/bitbucket/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ ai_review/clients/bitbucket/client.py,sha256=VaqaQ5USMPTOEeS5XPdr-RkMKsxUpJ2SBE6lcemkz-g,1174
13
+ ai_review/clients/bitbucket/pr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ ai_review/clients/bitbucket/pr/client.py,sha256=9C6vXBz8o0Df76N9WW4hORN-Q39Vd8I575AaidyW_HM,4359
15
+ ai_review/clients/bitbucket/pr/types.py,sha256=ZICV4ghYChj1Jl9Nlwyw1_kwmGybX51GhGdGzkRaLCk,1296
16
+ ai_review/clients/bitbucket/pr/schema/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ ai_review/clients/bitbucket/pr/schema/comments.py,sha256=DLi3LhThXfHB9MJ5Akv7Yf_n-VttjvJAausSMoksHTY,1152
18
+ ai_review/clients/bitbucket/pr/schema/files.py,sha256=A-h9Cgi0iJ6e9pGr5TcbpgSb3y9SMTqNi5FxJ7ySxpk,546
19
+ ai_review/clients/bitbucket/pr/schema/pull_request.py,sha256=buGULgaCkxCUFSdiw0XTwaSIYP_p1rAEuKXUyJ_Mzi8,863
11
20
  ai_review/clients/claude/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
21
  ai_review/clients/claude/client.py,sha256=uEadbBNBJnzjHDczbxXiiw1V1H1PdUWKu-Gn-eIDEmw,1890
13
22
  ai_review/clients/claude/schema.py,sha256=LE6KCjJKDXqBGU2Cno5XL5R8vUfScgskE9MqvE0Pt2A,887
@@ -65,12 +74,13 @@ ai_review/libs/config/llm/meta.py,sha256=cEcAHOwy-mQBKo9_KJrQe0I7qppq6h99lSmoWX4
65
74
  ai_review/libs/config/llm/ollama.py,sha256=M6aiPb5GvYvkiGcgHTsh9bOw5JsBLqmfSKoIbHCejrU,372
66
75
  ai_review/libs/config/llm/openai.py,sha256=jGVL4gJ2wIacoKeK9Zc9LCgY95TxdeYOThdglVPErFU,262
67
76
  ai_review/libs/config/vcs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
68
- ai_review/libs/config/vcs/base.py,sha256=zkfqBnI9kF_wbU9Nan19CKciKwOpWvdcZEt57jHujbE,786
77
+ ai_review/libs/config/vcs/base.py,sha256=ks9lrSalkPUuG8ijlaw-8d-F-dv59GdSywHS2TsIKjs,1085
78
+ ai_review/libs/config/vcs/bitbucket.py,sha256=on5sQaE57kM_zSmqdDUNrttVtTPGOzqLHM5s7eFN7DA,275
69
79
  ai_review/libs/config/vcs/github.py,sha256=hk-kuDLd8wecqtEb8PSqF7Yy_pkihplJhi6nB6FZID4,256
70
80
  ai_review/libs/config/vcs/gitlab.py,sha256=ecYfU158VgVlM6P5mgZn8FOqk3Xt60xx7gUqT5e22a4,252
71
81
  ai_review/libs/constants/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
72
82
  ai_review/libs/constants/llm_provider.py,sha256=k7GzctIZ-TDsRlhTPbpGYgym_CO2YKVFp_oXG9dTBW0,143
73
- ai_review/libs/constants/vcs_provider.py,sha256=mZMC8DWIDWQ1YeUZh1a1jduX5enOAe1rWeza0RBmpTY,99
83
+ ai_review/libs/constants/vcs_provider.py,sha256=xJpRdJIdAf05iH2x2f362d1MuviOlPVP7In-JvDVotE,127
74
84
  ai_review/libs/diff/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
75
85
  ai_review/libs/diff/models.py,sha256=RT4YJboOPA-AjNJGRj_HIZaJLEmROOhOgMh1wIGpIwY,2344
76
86
  ai_review/libs/diff/parser.py,sha256=2BGxZnRN3SRjNnZK4qIOW28aM93Ij__1SltwclJrlno,3817
@@ -150,8 +160,10 @@ ai_review/services/review/summary/schema.py,sha256=GipVNWrEKtgZPkytNSrXwzvX9Zq8P
150
160
  ai_review/services/review/summary/service.py,sha256=F4diIESc0y7YSiUKbInHWiSOW5tW_eQ0rpf78wKxLAo,562
151
161
  ai_review/services/review/summary/types.py,sha256=iDsucvx9xJZ5Xb5FN70da3bub3YDtt4vpQeVEK532E8,235
152
162
  ai_review/services/vcs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
153
- ai_review/services/vcs/factory.py,sha256=yoZ5qCI_vq2bG-9lbunf70ojcxdoj8ms-4vY4c5BcJE,606
163
+ ai_review/services/vcs/factory.py,sha256=AfhpZjQ257BkLjb_7zUyw_EUnfEiCUHgTph7GGm-MY4,753
154
164
  ai_review/services/vcs/types.py,sha256=S49LhAGHVAd_0QwZUr4JMhfc6DR-HikHR6-T_ETlTus,1998
165
+ ai_review/services/vcs/bitbucket/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
166
+ ai_review/services/vcs/bitbucket/client.py,sha256=OceM48MBoiUVKGTh8ZrrpVt8a1fDczCvOMD9VlwoapY,7253
155
167
  ai_review/services/vcs/github/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
156
168
  ai_review/services/vcs/github/client.py,sha256=v6NV97xi_rtRQQi8atRdSrXKhSOQ7CeRHK7YjoyjU6Q,6353
157
169
  ai_review/services/vcs/gitlab/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -159,6 +171,7 @@ ai_review/services/vcs/gitlab/client.py,sha256=LK95m-uFSxhDEVU-cBGct61NTKjul-ieL
159
171
  ai_review/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
160
172
  ai_review/tests/fixtures/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
161
173
  ai_review/tests/fixtures/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
174
+ ai_review/tests/fixtures/clients/bitbucket.py,sha256=XJK1nU7Wir5PnmwCUJ_2uTlByA5a_CTEuXc2a-WmWio,7122
162
175
  ai_review/tests/fixtures/clients/claude.py,sha256=6ldJlSSea0zsZV0hRDMi9mqWm0hWT3mp_ROwG_sVU1c,2203
163
176
  ai_review/tests/fixtures/clients/gemini.py,sha256=zhLJhm49keKEBCPOf_pLu8_zCatsKKAWM4-gXOhaXeM,2429
164
177
  ai_review/tests/fixtures/clients/github.py,sha256=Mzr8LcvVlYLhimzDMG4tEOQwj_6E6kTvYvSrq04R3YI,6865
@@ -242,14 +255,16 @@ ai_review/tests/suites/services/review/summary/__init__.py,sha256=47DEQpj8HBSa-_
242
255
  ai_review/tests/suites/services/review/summary/test_schema.py,sha256=HUbSDbQzBp-iTsGLs7hJfu-sz6sq9xLO0woGmZPWyx0,735
243
256
  ai_review/tests/suites/services/review/summary/test_service.py,sha256=ibiYOWQMZuQKRutIT_EKGq7DEPQvp62YhscNHeSWFVQ,588
244
257
  ai_review/tests/suites/services/vcs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
245
- ai_review/tests/suites/services/vcs/test_factory.py,sha256=RLxKRSISJQGH9_h2QY7zLvMyAVF4shYoEmKrrmUySME,850
258
+ ai_review/tests/suites/services/vcs/test_factory.py,sha256=EergKSHW4b7RZg9vJJ5Cj0XfPsDTLEclV1kq2_9greA,1138
259
+ ai_review/tests/suites/services/vcs/bitbucket/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
260
+ ai_review/tests/suites/services/vcs/bitbucket/test_service.py,sha256=JnG5BYTgGMb-doNjis2BOeI8JrMmvqwv82UFD5f92kg,4448
246
261
  ai_review/tests/suites/services/vcs/github/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
247
262
  ai_review/tests/suites/services/vcs/github/test_service.py,sha256=c2sjecm4qzqYXuO9j6j35NQyJzqDpnXIJImRTcpkyHo,4378
248
263
  ai_review/tests/suites/services/vcs/gitlab/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
249
264
  ai_review/tests/suites/services/vcs/gitlab/test_service.py,sha256=0dqgL5whzjcP-AQ4adP_12QfkYm_ZtdtMotmYm8Se7Y,4449
250
- xai_review-0.25.0.dist-info/licenses/LICENSE,sha256=p-v8m7Kmz4KKc7PcvsGiGEmCw9AiSXY4_ylOPy_u--Y,11343
251
- xai_review-0.25.0.dist-info/METADATA,sha256=-Ogp0iZijYiluzi9D_yO21Ur9N9Z4qJclmSeHrddU3A,11132
252
- xai_review-0.25.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
253
- xai_review-0.25.0.dist-info/entry_points.txt,sha256=JyC5URanMi5io5P_PXQf7H_I1OGIpk5cZQhaPQ0g4Zs,53
254
- xai_review-0.25.0.dist-info/top_level.txt,sha256=sTsZbfzLoqvRZKdKa-BcxWvjlHdrpbeJ6DrGY0EuR0E,10
255
- xai_review-0.25.0.dist-info/RECORD,,
265
+ xai_review-0.26.0.dist-info/licenses/LICENSE,sha256=p-v8m7Kmz4KKc7PcvsGiGEmCw9AiSXY4_ylOPy_u--Y,11343
266
+ xai_review-0.26.0.dist-info/METADATA,sha256=RXLUNKPnkpxjCcXWbKwFI8RN0UHvVjfWwtqGpVsukIs,11150
267
+ xai_review-0.26.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
268
+ xai_review-0.26.0.dist-info/entry_points.txt,sha256=JyC5URanMi5io5P_PXQf7H_I1OGIpk5cZQhaPQ0g4Zs,53
269
+ xai_review-0.26.0.dist-info/top_level.txt,sha256=sTsZbfzLoqvRZKdKa-BcxWvjlHdrpbeJ6DrGY0EuR0E,10
270
+ xai_review-0.26.0.dist-info/RECORD,,