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

Files changed (29) hide show
  1. ai_review/clients/bitbucket/pr/client.py +45 -8
  2. ai_review/clients/bitbucket/pr/schema/comments.py +7 -2
  3. ai_review/clients/bitbucket/pr/schema/files.py +8 -3
  4. ai_review/clients/bitbucket/tools.py +6 -0
  5. ai_review/clients/github/pr/client.py +66 -12
  6. ai_review/clients/github/pr/schema/comments.py +2 -1
  7. ai_review/clients/github/pr/schema/files.py +2 -1
  8. ai_review/clients/github/pr/schema/reviews.py +2 -1
  9. ai_review/clients/github/tools.py +6 -0
  10. ai_review/clients/gitlab/mr/client.py +35 -6
  11. ai_review/clients/gitlab/mr/schema/discussions.py +2 -1
  12. ai_review/clients/gitlab/mr/schema/notes.py +2 -1
  13. ai_review/clients/gitlab/tools.py +5 -0
  14. ai_review/libs/config/vcs/base.py +2 -0
  15. ai_review/libs/config/vcs/pagination.py +6 -0
  16. ai_review/libs/http/paginate.py +43 -0
  17. ai_review/tests/suites/clients/bitbucket/__init__.py +0 -0
  18. ai_review/tests/suites/clients/bitbucket/test_client.py +14 -0
  19. ai_review/tests/suites/clients/bitbucket/test_tools.py +31 -0
  20. ai_review/tests/suites/clients/github/test_tools.py +31 -0
  21. ai_review/tests/suites/clients/gitlab/test_tools.py +26 -0
  22. ai_review/tests/suites/libs/http/__init__.py +0 -0
  23. ai_review/tests/suites/libs/http/test_paginate.py +95 -0
  24. {xai_review-0.26.0.dist-info → xai_review-0.27.0.dist-info}/METADATA +2 -2
  25. {xai_review-0.26.0.dist-info → xai_review-0.27.0.dist-info}/RECORD +29 -17
  26. {xai_review-0.26.0.dist-info → xai_review-0.27.0.dist-info}/WHEEL +0 -0
  27. {xai_review-0.26.0.dist-info → xai_review-0.27.0.dist-info}/entry_points.txt +0 -0
  28. {xai_review-0.26.0.dist-info → xai_review-0.27.0.dist-info}/licenses/LICENSE +0 -0
  29. {xai_review-0.26.0.dist-info → xai_review-0.27.0.dist-info}/top_level.txt +0 -0
@@ -1,19 +1,24 @@
1
1
  from httpx import Response, QueryParams
2
2
 
3
3
  from ai_review.clients.bitbucket.pr.schema.comments import (
4
+ BitbucketPRCommentSchema,
4
5
  BitbucketGetPRCommentsQuerySchema,
5
6
  BitbucketGetPRCommentsResponseSchema,
6
7
  BitbucketCreatePRCommentRequestSchema,
7
8
  BitbucketCreatePRCommentResponseSchema,
8
9
  )
9
10
  from ai_review.clients.bitbucket.pr.schema.files import (
11
+ BitbucketPRFileSchema,
10
12
  BitbucketGetPRFilesQuerySchema,
11
13
  BitbucketGetPRFilesResponseSchema,
12
14
  )
13
15
  from ai_review.clients.bitbucket.pr.schema.pull_request import BitbucketGetPRResponseSchema
14
16
  from ai_review.clients.bitbucket.pr.types import BitbucketPullRequestsHTTPClientProtocol
17
+ from ai_review.clients.bitbucket.tools import bitbucket_has_next_page
18
+ from ai_review.config import settings
15
19
  from ai_review.libs.http.client import HTTPClient
16
20
  from ai_review.libs.http.handlers import handle_http_error, HTTPClientError
21
+ from ai_review.libs.http.paginate import paginate
17
22
 
18
23
 
19
24
  class BitbucketPullRequestsHTTPClientError(HTTPClientError):
@@ -35,7 +40,7 @@ class BitbucketPullRequestsHTTPClient(HTTPClient, BitbucketPullRequestsHTTPClien
35
40
  ) -> Response:
36
41
  return await self.get(
37
42
  f"/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/diffstat",
38
- query=QueryParams(**query.model_dump()),
43
+ query=QueryParams(**query.model_dump(by_alias=True)),
39
44
  )
40
45
 
41
46
  @handle_http_error(client="BitbucketPullRequestsHTTPClient", exception=BitbucketPullRequestsHTTPClientError)
@@ -48,7 +53,7 @@ class BitbucketPullRequestsHTTPClient(HTTPClient, BitbucketPullRequestsHTTPClien
48
53
  ) -> Response:
49
54
  return await self.get(
50
55
  f"/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/comments",
51
- query=QueryParams(**query.model_dump()),
56
+ query=QueryParams(**query.model_dump(by_alias=True)),
52
57
  )
53
58
 
54
59
  @handle_http_error(client="BitbucketPullRequestsHTTPClient", exception=BitbucketPullRequestsHTTPClientError)
@@ -79,9 +84,25 @@ class BitbucketPullRequestsHTTPClient(HTTPClient, BitbucketPullRequestsHTTPClien
79
84
  repo_slug: str,
80
85
  pull_request_id: str
81
86
  ) -> 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)
87
+ async def fetch_page(page: int) -> Response:
88
+ query = BitbucketGetPRFilesQuerySchema(page=page, page_len=settings.vcs.pagination.per_page)
89
+ return await self.get_diffstat_api(workspace, repo_slug, pull_request_id, query)
90
+
91
+ def extract_items(response: Response) -> list[BitbucketPRFileSchema]:
92
+ result = BitbucketGetPRFilesResponseSchema.model_validate_json(response.text)
93
+ return result.values
94
+
95
+ items = await paginate(
96
+ max_pages=settings.vcs.pagination.max_pages,
97
+ fetch_page=fetch_page,
98
+ extract_items=extract_items,
99
+ has_next_page=bitbucket_has_next_page
100
+ )
101
+ return BitbucketGetPRFilesResponseSchema(
102
+ size=len(items),
103
+ values=items,
104
+ page_len=settings.vcs.pagination.per_page
105
+ )
85
106
 
86
107
  async def get_comments(
87
108
  self,
@@ -89,9 +110,25 @@ class BitbucketPullRequestsHTTPClient(HTTPClient, BitbucketPullRequestsHTTPClien
89
110
  repo_slug: str,
90
111
  pull_request_id: str
91
112
  ) -> 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)
113
+ async def fetch_page(page: int) -> Response:
114
+ query = BitbucketGetPRCommentsQuerySchema(page=page, page_len=settings.vcs.pagination.per_page)
115
+ return await self.get_comments_api(workspace, repo_slug, pull_request_id, query)
116
+
117
+ def extract_items(response: Response) -> list[BitbucketPRCommentSchema]:
118
+ result = BitbucketGetPRCommentsResponseSchema.model_validate_json(response.text)
119
+ return result.values
120
+
121
+ items = await paginate(
122
+ max_pages=settings.vcs.pagination.max_pages,
123
+ fetch_page=fetch_page,
124
+ extract_items=extract_items,
125
+ has_next_page=bitbucket_has_next_page
126
+ )
127
+ return BitbucketGetPRCommentsResponseSchema(
128
+ size=len(items),
129
+ values=items,
130
+ page_len=settings.vcs.pagination.per_page
131
+ )
95
132
 
96
133
  async def create_comment(
97
134
  self,
@@ -22,15 +22,20 @@ class BitbucketPRCommentSchema(BaseModel):
22
22
 
23
23
 
24
24
  class BitbucketGetPRCommentsQuerySchema(BaseModel):
25
- pagelen: int = 100
25
+ model_config = ConfigDict(populate_by_name=True)
26
+
27
+ page: int = 1
28
+ page_len: int = Field(alias="pagelen", default=100)
26
29
 
27
30
 
28
31
  class BitbucketGetPRCommentsResponseSchema(BaseModel):
32
+ model_config = ConfigDict(populate_by_name=True)
33
+
29
34
  size: int
30
35
  page: int | None = None
31
36
  next: str | None = None
32
37
  values: list[BitbucketPRCommentSchema]
33
- pagelen: int
38
+ page_len: int = Field(alias="pagelen")
34
39
 
35
40
 
36
41
  class BitbucketCreatePRCommentRequestSchema(BaseModel):
@@ -1,4 +1,4 @@
1
- from pydantic import BaseModel
1
+ from pydantic import BaseModel, Field, ConfigDict
2
2
 
3
3
 
4
4
  class BitbucketPRFilePathSchema(BaseModel):
@@ -14,12 +14,17 @@ class BitbucketPRFileSchema(BaseModel):
14
14
 
15
15
 
16
16
  class BitbucketGetPRFilesQuerySchema(BaseModel):
17
- pagelen: int = 100
17
+ model_config = ConfigDict(populate_by_name=True)
18
+
19
+ page: int = 1
20
+ page_len: int = Field(alias="pagelen", default=100)
18
21
 
19
22
 
20
23
  class BitbucketGetPRFilesResponseSchema(BaseModel):
24
+ model_config = ConfigDict(populate_by_name=True)
25
+
21
26
  size: int
22
27
  page: int | None = None
23
28
  next: str | None = None
24
29
  values: list[BitbucketPRFileSchema]
25
- pagelen: int
30
+ page_len: int = Field(alias="pagelen")
@@ -0,0 +1,6 @@
1
+ from httpx import Response
2
+
3
+
4
+ def bitbucket_has_next_page(response: Response) -> bool:
5
+ data = response.json()
6
+ return bool(data.get("next"))
@@ -1,6 +1,7 @@
1
1
  from httpx import Response, QueryParams
2
2
 
3
3
  from ai_review.clients.github.pr.schema.comments import (
4
+ GitHubPRCommentSchema,
4
5
  GitHubGetPRCommentsQuerySchema,
5
6
  GitHubGetPRCommentsResponseSchema,
6
7
  GitHubCreateIssueCommentRequestSchema,
@@ -9,17 +10,22 @@ from ai_review.clients.github.pr.schema.comments import (
9
10
  GitHubCreateReviewCommentResponseSchema
10
11
  )
11
12
  from ai_review.clients.github.pr.schema.files import (
13
+ GitHubPRFileSchema,
12
14
  GitHubGetPRFilesQuerySchema,
13
15
  GitHubGetPRFilesResponseSchema
14
16
  )
15
17
  from ai_review.clients.github.pr.schema.pull_request import GitHubGetPRResponseSchema
16
18
  from ai_review.clients.github.pr.schema.reviews import (
19
+ GitHubPRReviewSchema,
17
20
  GitHubGetPRReviewsQuerySchema,
18
21
  GitHubGetPRReviewsResponseSchema
19
22
  )
20
23
  from ai_review.clients.github.pr.types import GitHubPullRequestsHTTPClientProtocol
24
+ from ai_review.clients.github.tools import github_has_next_page
25
+ from ai_review.config import settings
21
26
  from ai_review.libs.http.client import HTTPClient
22
27
  from ai_review.libs.http.handlers import HTTPClientError, handle_http_error
28
+ from ai_review.libs.http.paginate import paginate
23
29
 
24
30
 
25
31
  class GitHubPullRequestsHTTPClientError(HTTPClientError):
@@ -114,24 +120,72 @@ class GitHubPullRequestsHTTPClient(HTTPClient, GitHubPullRequestsHTTPClientProto
114
120
  return GitHubGetPRResponseSchema.model_validate_json(response.text)
115
121
 
116
122
  async def get_files(self, owner: str, repo: str, pull_number: str) -> GitHubGetPRFilesResponseSchema:
117
- query = GitHubGetPRFilesQuerySchema(per_page=100)
118
- response = await self.get_files_api(owner, repo, pull_number, query)
119
- return GitHubGetPRFilesResponseSchema.model_validate_json(response.text)
123
+ async def fetch_page(page: int) -> Response:
124
+ query = GitHubGetPRFilesQuerySchema(page=page, per_page=settings.vcs.pagination.per_page)
125
+ return await self.get_files_api(owner, repo, pull_number, query)
126
+
127
+ def extract_items(response: Response) -> list[GitHubPRFileSchema]:
128
+ result = GitHubGetPRFilesResponseSchema.model_validate_json(response.text)
129
+ return result.root
130
+
131
+ items = await paginate(
132
+ max_pages=settings.vcs.pagination.max_pages,
133
+ fetch_page=fetch_page,
134
+ extract_items=extract_items,
135
+ has_next_page=github_has_next_page
136
+ )
137
+ return GitHubGetPRFilesResponseSchema(root=items)
120
138
 
121
139
  async def get_issue_comments(self, owner: str, repo: str, issue_number: str) -> GitHubGetPRCommentsResponseSchema:
122
- query = GitHubGetPRCommentsQuerySchema(per_page=100)
123
- response = await self.get_issue_comments_api(owner, repo, issue_number, query)
124
- return GitHubGetPRCommentsResponseSchema.model_validate_json(response.text)
140
+ async def fetch_page(page: int) -> Response:
141
+ query = GitHubGetPRCommentsQuerySchema(page=page, per_page=settings.vcs.pagination.per_page)
142
+ return await self.get_issue_comments_api(owner, repo, issue_number, query)
143
+
144
+ def extract_items(response: Response) -> list[GitHubPRCommentSchema]:
145
+ result = GitHubGetPRCommentsResponseSchema.model_validate_json(response.text)
146
+ return result.root
147
+
148
+ items = await paginate(
149
+ max_pages=settings.vcs.pagination.max_pages,
150
+ fetch_page=fetch_page,
151
+ extract_items=extract_items,
152
+ has_next_page=github_has_next_page
153
+ )
154
+ return GitHubGetPRCommentsResponseSchema(root=items)
125
155
 
126
156
  async def get_review_comments(self, owner: str, repo: str, pull_number: str) -> GitHubGetPRCommentsResponseSchema:
127
- query = GitHubGetPRCommentsQuerySchema(per_page=100)
128
- response = await self.get_review_comments_api(owner, repo, pull_number, query)
129
- return GitHubGetPRCommentsResponseSchema.model_validate_json(response.text)
157
+ async def fetch_page(page: int) -> Response:
158
+ query = GitHubGetPRCommentsQuerySchema(page=page, per_page=settings.vcs.pagination.per_page)
159
+ return await self.get_review_comments_api(owner, repo, pull_number, query)
160
+
161
+ def extract_items(response: Response) -> list[GitHubPRCommentSchema]:
162
+ result = GitHubGetPRCommentsResponseSchema.model_validate_json(response.text)
163
+ return result.root
164
+
165
+ items = await paginate(
166
+ max_pages=settings.vcs.pagination.max_pages,
167
+ fetch_page=fetch_page,
168
+ extract_items=extract_items,
169
+ has_next_page=github_has_next_page
170
+ )
171
+ return GitHubGetPRCommentsResponseSchema(root=items)
130
172
 
131
173
  async def get_reviews(self, owner: str, repo: str, pull_number: str) -> GitHubGetPRReviewsResponseSchema:
132
- query = GitHubGetPRReviewsQuerySchema(per_page=100)
133
- response = await self.get_reviews_api(owner, repo, pull_number, query)
134
- return GitHubGetPRReviewsResponseSchema.model_validate_json(response.text)
174
+ async def fetch_page(page: int) -> Response:
175
+ query = GitHubGetPRReviewsQuerySchema(page=page, per_page=settings.vcs.pagination.per_page)
176
+ return await self.get_reviews_api(owner, repo, pull_number, query)
177
+
178
+ def extract_items(response: Response) -> list[GitHubPRReviewSchema]:
179
+ result = GitHubGetPRReviewsResponseSchema.model_validate_json(response.text)
180
+ return result.root
181
+
182
+ items = await paginate(
183
+ max_pages=settings.vcs.pagination.max_pages,
184
+ fetch_page=fetch_page,
185
+ extract_items=extract_items,
186
+ has_next_page=github_has_next_page
187
+ )
188
+ return GitHubGetPRReviewsResponseSchema(root=items)
135
189
 
136
190
  async def create_review_comment(
137
191
  self,
@@ -9,7 +9,8 @@ class GitHubPRCommentSchema(BaseModel):
9
9
 
10
10
 
11
11
  class GitHubGetPRCommentsQuerySchema(BaseModel):
12
- per_page: int
12
+ page: int = 1
13
+ per_page: int = 100
13
14
 
14
15
 
15
16
  class GitHubGetPRCommentsResponseSchema(RootModel[list[GitHubPRCommentSchema]]):
@@ -9,7 +9,8 @@ class GitHubPRFileSchema(BaseModel):
9
9
 
10
10
 
11
11
  class GitHubGetPRFilesQuerySchema(BaseModel):
12
- per_page: int
12
+ page: int = 1
13
+ per_page: int = 100
13
14
 
14
15
 
15
16
  class GitHubGetPRFilesResponseSchema(RootModel[list[GitHubPRFileSchema]]):
@@ -10,7 +10,8 @@ class GitHubPRReviewSchema(BaseModel):
10
10
 
11
11
 
12
12
  class GitHubGetPRReviewsQuerySchema(BaseModel):
13
- per_page: int
13
+ page: int = 1
14
+ per_page: int = 100
14
15
 
15
16
 
16
17
  class GitHubGetPRReviewsResponseSchema(RootModel[list[GitHubPRReviewSchema]]):
@@ -0,0 +1,6 @@
1
+ from httpx import Response
2
+
3
+
4
+ def github_has_next_page(response: Response) -> bool:
5
+ link_header = response.headers.get("Link")
6
+ return (link_header is not None) and ('rel="next"' in link_header)
@@ -2,20 +2,25 @@ from httpx import Response, QueryParams
2
2
 
3
3
  from ai_review.clients.gitlab.mr.schema.changes import GitLabGetMRChangesResponseSchema
4
4
  from ai_review.clients.gitlab.mr.schema.discussions import (
5
+ GitLabDiscussionSchema,
5
6
  GitLabGetMRDiscussionsQuerySchema,
6
7
  GitLabGetMRDiscussionsResponseSchema,
7
8
  GitLabCreateMRDiscussionRequestSchema,
8
9
  GitLabCreateMRDiscussionResponseSchema
9
10
  )
10
11
  from ai_review.clients.gitlab.mr.schema.notes import (
12
+ GitLabNoteSchema,
11
13
  GitLabGetMRNotesQuerySchema,
12
14
  GitLabGetMRNotesResponseSchema,
13
15
  GitLabCreateMRNoteRequestSchema,
14
16
  GitLabCreateMRNoteResponseSchema,
15
17
  )
16
18
  from ai_review.clients.gitlab.mr.types import GitLabMergeRequestsHTTPClientProtocol
19
+ from ai_review.clients.gitlab.tools import gitlab_has_next_page
20
+ from ai_review.config import settings
17
21
  from ai_review.libs.http.client import HTTPClient
18
22
  from ai_review.libs.http.handlers import handle_http_error, HTTPClientError
23
+ from ai_review.libs.http.paginate import paginate
19
24
 
20
25
 
21
26
  class GitLabMergeRequestsHTTPClientError(HTTPClientError):
@@ -86,18 +91,42 @@ class GitLabMergeRequestsHTTPClient(HTTPClient, GitLabMergeRequestsHTTPClientPro
86
91
  project_id: str,
87
92
  merge_request_id: str
88
93
  ) -> GitLabGetMRNotesResponseSchema:
89
- query = GitLabGetMRNotesQuerySchema(per_page=100)
90
- response = await self.get_notes_api(project_id, merge_request_id, query)
91
- return GitLabGetMRNotesResponseSchema.model_validate_json(response.text)
94
+ async def fetch_page(page: int) -> Response:
95
+ query = GitLabGetMRNotesQuerySchema(page=page, per_page=settings.vcs.pagination.per_page)
96
+ return await self.get_notes_api(project_id, merge_request_id, query)
97
+
98
+ def extract_items(response: Response) -> list[GitLabNoteSchema]:
99
+ result = GitLabGetMRNotesResponseSchema.model_validate_json(response.text)
100
+ return result.root
101
+
102
+ items = await paginate(
103
+ max_pages=settings.vcs.pagination.max_pages,
104
+ fetch_page=fetch_page,
105
+ extract_items=extract_items,
106
+ has_next_page=gitlab_has_next_page
107
+ )
108
+ return GitLabGetMRNotesResponseSchema(root=items)
92
109
 
93
110
  async def get_discussions(
94
111
  self,
95
112
  project_id: str,
96
113
  merge_request_id: str
97
114
  ) -> GitLabGetMRDiscussionsResponseSchema:
98
- query = GitLabGetMRDiscussionsQuerySchema(per_page=100)
99
- response = await self.get_discussions_api(project_id, merge_request_id, query)
100
- return GitLabGetMRDiscussionsResponseSchema.model_validate_json(response.text)
115
+ async def fetch_page(page: int) -> Response:
116
+ query = GitLabGetMRDiscussionsQuerySchema(page=page, per_page=settings.vcs.pagination.per_page)
117
+ return await self.get_discussions_api(project_id, merge_request_id, query)
118
+
119
+ def extract_items(response: Response) -> list[GitLabDiscussionSchema]:
120
+ result = GitLabGetMRDiscussionsResponseSchema.model_validate_json(response.text)
121
+ return result.root
122
+
123
+ items = await paginate(
124
+ max_pages=settings.vcs.pagination.max_pages,
125
+ fetch_page=fetch_page,
126
+ extract_items=extract_items,
127
+ has_next_page=gitlab_has_next_page
128
+ )
129
+ return GitLabGetMRDiscussionsResponseSchema(root=items)
101
130
 
102
131
  async def create_note(
103
132
  self,
@@ -18,7 +18,8 @@ class GitLabDiscussionPositionSchema(BaseModel):
18
18
 
19
19
 
20
20
  class GitLabGetMRDiscussionsQuerySchema(BaseModel):
21
- per_page: int
21
+ page: int = 1
22
+ per_page: int = 100
22
23
 
23
24
 
24
25
  class GitLabGetMRDiscussionsResponseSchema(RootModel[list[GitLabDiscussionSchema]]):
@@ -7,7 +7,8 @@ class GitLabNoteSchema(BaseModel):
7
7
 
8
8
 
9
9
  class GitLabGetMRNotesQuerySchema(BaseModel):
10
- per_page: int
10
+ page: int = 1
11
+ per_page: int = 100
11
12
 
12
13
 
13
14
  class GitLabGetMRNotesResponseSchema(RootModel[list[GitLabNoteSchema]]):
@@ -0,0 +1,5 @@
1
+ from httpx import Response
2
+
3
+
4
+ def gitlab_has_next_page(response: Response) -> bool:
5
+ return bool(response.headers.get("X-Next-Page"))
@@ -5,11 +5,13 @@ from pydantic import BaseModel, Field
5
5
  from ai_review.libs.config.vcs.bitbucket import BitbucketPipelineConfig, BitbucketHTTPClientConfig
6
6
  from ai_review.libs.config.vcs.github import GitHubPipelineConfig, GitHubHTTPClientConfig
7
7
  from ai_review.libs.config.vcs.gitlab import GitLabPipelineConfig, GitLabHTTPClientConfig
8
+ from ai_review.libs.config.vcs.pagination import VCSPaginationConfig
8
9
  from ai_review.libs.constants.vcs_provider import VCSProvider
9
10
 
10
11
 
11
12
  class VCSConfigBase(BaseModel):
12
13
  provider: VCSProvider
14
+ pagination: VCSPaginationConfig = VCSPaginationConfig()
13
15
 
14
16
 
15
17
  class GitLabVCSConfig(VCSConfigBase):
@@ -0,0 +1,6 @@
1
+ from pydantic import BaseModel, Field
2
+
3
+
4
+ class VCSPaginationConfig(BaseModel):
5
+ per_page: int = Field(default=100, ge=1, le=100)
6
+ max_pages: int = Field(default=5, ge=1)
@@ -0,0 +1,43 @@
1
+ from typing import Awaitable, Callable, TypeVar
2
+
3
+ from httpx import Response
4
+ from pydantic import BaseModel
5
+
6
+ from ai_review.libs.logger import get_logger
7
+
8
+ T = TypeVar("T", bound=BaseModel)
9
+
10
+ logger = get_logger("PAGINATE")
11
+
12
+
13
+ async def paginate(
14
+ fetch_page: Callable[[int], Awaitable[Response]],
15
+ extract_items: Callable[[Response], list[T]],
16
+ has_next_page: Callable[[Response], bool],
17
+ max_pages: int | None = None,
18
+ ) -> list[T]:
19
+ page = 1
20
+ items: list[T] = []
21
+
22
+ while True:
23
+ response = await fetch_page(page)
24
+
25
+ try:
26
+ extracted = extract_items(response)
27
+ except Exception as error:
28
+ logger.error(f"Failed to extract items on {page=}")
29
+ raise RuntimeError(f"Failed to extract items on {page=}") from error
30
+
31
+ logger.debug(f"Page {page}: extracted {len(extracted)} items (total={len(items) + len(extracted)})")
32
+ items.extend(extracted)
33
+
34
+ if not has_next_page(response):
35
+ logger.debug(f"Pagination finished after {page} page(s), total items={len(items)}")
36
+ break
37
+
38
+ page += 1
39
+ if max_pages and (page > max_pages):
40
+ logger.error(f"Pagination exceeded {max_pages=}")
41
+ raise RuntimeError(f"Pagination exceeded {max_pages=}")
42
+
43
+ return items
File without changes
@@ -0,0 +1,14 @@
1
+ import pytest
2
+ from httpx import AsyncClient
3
+
4
+ from ai_review.clients.bitbucket.client import get_bitbucket_http_client, BitbucketHTTPClient
5
+ from ai_review.clients.bitbucket.pr.client import BitbucketPullRequestsHTTPClient
6
+
7
+
8
+ @pytest.mark.usefixtures("bitbucket_http_client_config")
9
+ def test_get_bitbucket_http_client_builds_ok():
10
+ bitbucket_http_client = get_bitbucket_http_client()
11
+
12
+ assert isinstance(bitbucket_http_client, BitbucketHTTPClient)
13
+ assert isinstance(bitbucket_http_client.pr, BitbucketPullRequestsHTTPClient)
14
+ assert isinstance(bitbucket_http_client.pr.client, AsyncClient)
@@ -0,0 +1,31 @@
1
+ from httpx import Response, Request
2
+
3
+ from ai_review.clients.bitbucket.tools import bitbucket_has_next_page
4
+
5
+
6
+ def make_response(data: dict) -> Response:
7
+ return Response(
8
+ json=data,
9
+ request=Request("GET", "http://bitbucket.test"),
10
+ status_code=200,
11
+ )
12
+
13
+
14
+ def test_bitbucket_has_next_page_true():
15
+ resp = make_response({"next": "https://api.bitbucket.org/2.0/repositories/test/repo?page=2"})
16
+ assert bitbucket_has_next_page(resp) is True
17
+
18
+
19
+ def test_bitbucket_has_next_page_false_none():
20
+ resp = make_response({"next": None})
21
+ assert bitbucket_has_next_page(resp) is False
22
+
23
+
24
+ def test_bitbucket_has_next_page_false_missing():
25
+ resp = make_response({})
26
+ assert bitbucket_has_next_page(resp) is False
27
+
28
+
29
+ def test_bitbucket_has_next_page_false_empty_string():
30
+ resp = make_response({"next": ""})
31
+ assert bitbucket_has_next_page(resp) is False
@@ -0,0 +1,31 @@
1
+ from httpx import Response, Request
2
+
3
+ from ai_review.clients.github.tools import github_has_next_page
4
+
5
+
6
+ def make_response(headers: dict) -> Response:
7
+ return Response(
8
+ request=Request("GET", "http://test"),
9
+ headers=headers,
10
+ status_code=200,
11
+ )
12
+
13
+
14
+ def test_github_has_next_page_true():
15
+ response = make_response({
16
+ "Link": '<https://api.github.com/resource?page=2>; rel="next", '
17
+ '<https://api.github.com/resource?page=5>; rel="last"'
18
+ })
19
+ assert github_has_next_page(response) is True
20
+
21
+
22
+ def test_github_has_next_page_false_no_next():
23
+ response = make_response({
24
+ "Link": '<https://api.github.com/resource?page=5>; rel="last"'
25
+ })
26
+ assert github_has_next_page(response) is False
27
+
28
+
29
+ def test_github_has_next_page_false_no_header():
30
+ resp = make_response({})
31
+ assert github_has_next_page(resp) is False
@@ -0,0 +1,26 @@
1
+ from httpx import Response, Request
2
+
3
+ from ai_review.clients.gitlab.tools import gitlab_has_next_page
4
+
5
+
6
+ def make_response(headers: dict) -> Response:
7
+ return Response(
8
+ request=Request("GET", "http://gitlab.test"),
9
+ headers=headers,
10
+ status_code=200,
11
+ )
12
+
13
+
14
+ def test_gitlab_has_next_page_true():
15
+ resp = make_response({"X-Next-Page": "2"})
16
+ assert gitlab_has_next_page(resp) is True
17
+
18
+
19
+ def test_gitlab_has_next_page_false_empty():
20
+ resp = make_response({"X-Next-Page": ""})
21
+ assert gitlab_has_next_page(resp) is False
22
+
23
+
24
+ def test_gitlab_has_next_page_false_missing():
25
+ resp = make_response({})
26
+ assert gitlab_has_next_page(resp) is False
File without changes
@@ -0,0 +1,95 @@
1
+ import pytest
2
+ from httpx import Response, Request
3
+ from pydantic import BaseModel
4
+
5
+ from ai_review.libs.http.paginate import paginate
6
+
7
+
8
+ class DummySchema(BaseModel):
9
+ value: int
10
+
11
+
12
+ def make_response(data: dict) -> Response:
13
+ return Response(
14
+ json=data,
15
+ request=Request("GET", "http://test"),
16
+ status_code=200,
17
+ )
18
+
19
+
20
+ @pytest.mark.asyncio
21
+ async def test_single_page():
22
+ async def fetch_page(_: int) -> Response:
23
+ return make_response({"items": [1, 2, 3]})
24
+
25
+ def extract_items(response: Response) -> list[DummySchema]:
26
+ return [DummySchema(value=value) for value in response.json()["items"]]
27
+
28
+ def has_next_page(_: Response) -> bool:
29
+ return False
30
+
31
+ items = await paginate(fetch_page, extract_items, has_next_page)
32
+ assert len(items) == 3
33
+ assert [item.value for item in items] == [1, 2, 3]
34
+
35
+
36
+ @pytest.mark.asyncio
37
+ async def test_multiple_pages():
38
+ async def fetch_page(page: int) -> Response:
39
+ return make_response({"items": [page]})
40
+
41
+ def extract_items(response: Response):
42
+ return [DummySchema(value=value) for value in response.json()["items"]]
43
+
44
+ def has_next_page(response: Response) -> bool:
45
+ return response.json()["items"][0] < 3
46
+
47
+ items = await paginate(fetch_page, extract_items, has_next_page)
48
+ assert [item.value for item in items] == [1, 2, 3]
49
+
50
+
51
+ @pytest.mark.asyncio
52
+ async def test_extract_items_error():
53
+ async def fetch_page(_: int) -> Response:
54
+ return make_response({"items": [1]})
55
+
56
+ def extract_items(_: Response):
57
+ raise ValueError("bad json")
58
+
59
+ def has_next_page(_: Response) -> bool:
60
+ return False
61
+
62
+ with pytest.raises(RuntimeError) as exc:
63
+ await paginate(fetch_page, extract_items, has_next_page)
64
+ assert "Failed to extract items" in str(exc.value)
65
+
66
+
67
+ @pytest.mark.asyncio
68
+ async def test_max_pages_exceeded():
69
+ async def fetch_page(page: int) -> Response:
70
+ return make_response({"items": [page]})
71
+
72
+ def extract_items(response: Response):
73
+ return [DummySchema(value=value) for value in response.json()["items"]]
74
+
75
+ def has_next_page(_: Response) -> bool:
76
+ return True
77
+
78
+ with pytest.raises(RuntimeError) as exc:
79
+ await paginate(fetch_page, extract_items, has_next_page, max_pages=2)
80
+ assert "Pagination exceeded" in str(exc.value)
81
+
82
+
83
+ @pytest.mark.asyncio
84
+ async def test_empty_items():
85
+ async def fetch_page(_: int) -> Response:
86
+ return make_response({"items": []})
87
+
88
+ def extract_items(_: Response):
89
+ return []
90
+
91
+ def has_next_page(_: Response) -> bool:
92
+ return False
93
+
94
+ result = await paginate(fetch_page, extract_items, has_next_page)
95
+ assert result == []
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xai-review
3
- Version: 0.26.0
3
+ Version: 0.27.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>
@@ -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.26.0
212
+ - uses: Nikita-Filonov/ai-review@v0.27.0
213
213
  with:
214
214
  review-command: ${{ inputs.review-command }}
215
215
  env:
@@ -10,12 +10,13 @@ ai_review/cli/commands/run_summary_review.py,sha256=NqjepGH5cbqczPzcuMEAxO4dI58F
10
10
  ai_review/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  ai_review/clients/bitbucket/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
12
  ai_review/clients/bitbucket/client.py,sha256=VaqaQ5USMPTOEeS5XPdr-RkMKsxUpJ2SBE6lcemkz-g,1174
13
+ ai_review/clients/bitbucket/tools.py,sha256=UGBCurb8MQECivWDJDNXr7ej7rwA5-_kXTT4zF8RXIQ,147
13
14
  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/client.py,sha256=YnA5GCw_qqrjraNH_LjtE7t7cCmrMTY9IrcxDT2uyZ0,5836
15
16
  ai_review/clients/bitbucket/pr/types.py,sha256=ZICV4ghYChj1Jl9Nlwyw1_kwmGybX51GhGdGzkRaLCk,1296
16
17
  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
18
+ ai_review/clients/bitbucket/pr/schema/comments.py,sha256=p3HQyi-M7DLXcD0RErVlqsnKhmo2JVqdA8ejVYF_Kzg,1337
19
+ ai_review/clients/bitbucket/pr/schema/files.py,sha256=qBDfIv370TDMKM8yoLlm1c-BTHNvEZRuhnBo18nkx9g,750
19
20
  ai_review/clients/bitbucket/pr/schema/pull_request.py,sha256=buGULgaCkxCUFSdiw0XTwaSIYP_p1rAEuKXUyJ_Mzi8,863
20
21
  ai_review/clients/claude/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
22
  ai_review/clients/claude/client.py,sha256=uEadbBNBJnzjHDczbxXiiw1V1H1PdUWKu-Gn-eIDEmw,1890
@@ -27,23 +28,25 @@ ai_review/clients/gemini/schema.py,sha256=5oVvbI-h_sw8bFreS4JUmMj-aXa_frvxK3H8sg
27
28
  ai_review/clients/gemini/types.py,sha256=D-P0THorrQ8yq5P-NKAC65zzhEYRa9HkiXTORG9QoIk,267
28
29
  ai_review/clients/github/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
30
  ai_review/clients/github/client.py,sha256=pprQcCYdrhRYtuqRsTFiCbj54Qb1Ll6_jmlm7AJg8pk,1149
31
+ ai_review/clients/github/tools.py,sha256=sD2eS_iNhzT4ZLgTRmO2uBLuId-fa2aSvbu6VFeKSlc,201
30
32
  ai_review/clients/github/pr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
- ai_review/clients/github/pr/client.py,sha256=YvGc4ClK615PgWslD8bL54pk1YakXHivabjnPZ8p6iI,6642
33
+ ai_review/clients/github/pr/client.py,sha256=HysL1u-WxLdhGKi-6HAx6fN-50XSk3yK55gZ9Nii72U,8851
32
34
  ai_review/clients/github/pr/types.py,sha256=mI5Vtlxc25iymOTPkNl55IQygXd4ti3B1Ydpx3q1fps,1726
33
35
  ai_review/clients/github/pr/schema/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
- ai_review/clients/github/pr/schema/comments.py,sha256=IjBtpcJABlql97_N7_CMAKuAozbVnSkaD16l3KueGqk,719
35
- ai_review/clients/github/pr/schema/files.py,sha256=mXfmjaJuVbbo2PPTppCyYlc4LSd04fb-_x_9aJkNW04,335
36
+ ai_review/clients/github/pr/schema/comments.py,sha256=dIlX6tYPseAtLquk-UIie7jxBxrCbmPbDyB8bC9OFPo,743
37
+ ai_review/clients/github/pr/schema/files.py,sha256=ARirokToZ1zqMWh2jpDtVIs4qw5rlL7xfoRDJBJF71o,359
36
38
  ai_review/clients/github/pr/schema/pull_request.py,sha256=EwOworYQY4kCmL6QFKEXK7r2fpINK8o-4-FEy9-nTpg,688
37
- ai_review/clients/github/pr/schema/reviews.py,sha256=tRHqGPzZ-LTbAZhtoVeBdl-LH34tgI0zPCtJTfFe2Mc,356
39
+ ai_review/clients/github/pr/schema/reviews.py,sha256=qRJpBu03CBwC_tPng9q9tkqT_u2WcdDELblAi-OzxWI,380
38
40
  ai_review/clients/gitlab/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
41
  ai_review/clients/gitlab/client.py,sha256=MIZed-V4JUzntkU9vJq7Fkp5VAjnzjvep_5QM5ni_Co,1151
42
+ ai_review/clients/gitlab/tools.py,sha256=ed8t2-dy2XtSRpYWhLxUobYvbBX_1zMZTPuzjbgPzTg,136
40
43
  ai_review/clients/gitlab/mr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
- ai_review/clients/gitlab/mr/client.py,sha256=WdaJCJdSHeZZUQ9Hz8172RCgACwm-_NmcC91LHT7YwU,5158
44
+ ai_review/clients/gitlab/mr/client.py,sha256=39EjS0bvvi8-kiBOkBa7N1q0wHb0qTgo_KvQise61js,6350
42
45
  ai_review/clients/gitlab/mr/types.py,sha256=tZ3rDDkzJM8v4zPt4gTyKCL30htiPQE9F_kVR6K5JHA,1280
43
46
  ai_review/clients/gitlab/mr/schema/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
44
47
  ai_review/clients/gitlab/mr/schema/changes.py,sha256=ZqSPb8zO0z_V8cEjxoTqnwbjRLxo6OTV4LeQEAg91cU,835
45
- ai_review/clients/gitlab/mr/schema/discussions.py,sha256=JgvxKfHoYxmp86aP4MpIczK-arU0hc-BZLASWDWBIRs,790
46
- ai_review/clients/gitlab/mr/schema/notes.py,sha256=yfnnRt69fALKfapzZpVtvCvNwPkq5jBFI7fbPMq1w1c,424
48
+ ai_review/clients/gitlab/mr/schema/discussions.py,sha256=owaWWD5lMG9GXkT8kgHiU_-khYTWGHanbWgSKDiJknA,814
49
+ ai_review/clients/gitlab/mr/schema/notes.py,sha256=DX9juurMYKcaYobh4n4M49dvHtUiLw5IzZKsGdBMxZg,448
47
50
  ai_review/clients/ollama/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
48
51
  ai_review/clients/ollama/client.py,sha256=KoJ9J5_Vfpv5XNJREshE_gA46uo9J0Z3qVC7wJPEcX8,1720
49
52
  ai_review/clients/ollama/schema.py,sha256=A6oKwkkEVrduyzMR_lhLnaLyvKXqlfsXjkMIF2eXaYw,1310
@@ -74,10 +77,11 @@ ai_review/libs/config/llm/meta.py,sha256=cEcAHOwy-mQBKo9_KJrQe0I7qppq6h99lSmoWX4
74
77
  ai_review/libs/config/llm/ollama.py,sha256=M6aiPb5GvYvkiGcgHTsh9bOw5JsBLqmfSKoIbHCejrU,372
75
78
  ai_review/libs/config/llm/openai.py,sha256=jGVL4gJ2wIacoKeK9Zc9LCgY95TxdeYOThdglVPErFU,262
76
79
  ai_review/libs/config/vcs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
77
- ai_review/libs/config/vcs/base.py,sha256=ks9lrSalkPUuG8ijlaw-8d-F-dv59GdSywHS2TsIKjs,1085
80
+ ai_review/libs/config/vcs/base.py,sha256=B0kKc6-A3mmV7dvHpo47u-1yTvdUCvLj_g4oCBo_NyY,1214
78
81
  ai_review/libs/config/vcs/bitbucket.py,sha256=on5sQaE57kM_zSmqdDUNrttVtTPGOzqLHM5s7eFN7DA,275
79
82
  ai_review/libs/config/vcs/github.py,sha256=hk-kuDLd8wecqtEb8PSqF7Yy_pkihplJhi6nB6FZID4,256
80
83
  ai_review/libs/config/vcs/gitlab.py,sha256=ecYfU158VgVlM6P5mgZn8FOqk3Xt60xx7gUqT5e22a4,252
84
+ ai_review/libs/config/vcs/pagination.py,sha256=S-XxWQYkIjhu3ffpHQ44d7UtRHH81fh9GaJ-xQXUUy4,175
81
85
  ai_review/libs/constants/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
82
86
  ai_review/libs/constants/llm_provider.py,sha256=k7GzctIZ-TDsRlhTPbpGYgym_CO2YKVFp_oXG9dTBW0,143
83
87
  ai_review/libs/constants/vcs_provider.py,sha256=xJpRdJIdAf05iH2x2f362d1MuviOlPVP7In-JvDVotE,127
@@ -88,6 +92,7 @@ ai_review/libs/diff/tools.py,sha256=CZWRDlOW2YS-b8h9gv_uP1MG194_FLkKzcKTwwZHocI,
88
92
  ai_review/libs/http/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
89
93
  ai_review/libs/http/client.py,sha256=JONzbhJWJKFz8Jy6p9pzq2hAMKlyJ2_WkBksuAlqW7k,453
90
94
  ai_review/libs/http/handlers.py,sha256=k1VvCIFjLzfH3qQ--aj4CZVgbU0oj78sYStMBrxo_Ek,1040
95
+ ai_review/libs/http/paginate.py,sha256=yAryDaUkQd-wojXOaak9HyicT-QZwx3L49AJlUEins4,1305
91
96
  ai_review/libs/http/event_hooks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
92
97
  ai_review/libs/http/event_hooks/base.py,sha256=cnSOOButTJYKeyb_OnGms1vXRfwfExP81L3ZfYWLufk,279
93
98
  ai_review/libs/http/event_hooks/logger.py,sha256=8_omfl6q3JijaBBIgzvzb4SayjNEDW-oxyck_Ky8wnI,603
@@ -191,6 +196,9 @@ ai_review/tests/fixtures/services/review/inline.py,sha256=k4IW6oy5JHMo9Kv0H97DLl
191
196
  ai_review/tests/fixtures/services/review/summary.py,sha256=Hkt8mq1ZmqMH5_mELrS1x0wtCoNPbBjOEQ9yIsMbRts,691
192
197
  ai_review/tests/suites/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
193
198
  ai_review/tests/suites/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
199
+ ai_review/tests/suites/clients/bitbucket/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
200
+ ai_review/tests/suites/clients/bitbucket/test_client.py,sha256=1qEkznC01hkh6TPpzChY9glhK6OTX-pfnSF43KNBtrY,600
201
+ ai_review/tests/suites/clients/bitbucket/test_tools.py,sha256=naHq0Xy7uP8pYdWR8JVrET5JJpNb0Z2-TEPkGHh2fFI,887
194
202
  ai_review/tests/suites/clients/claude/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
195
203
  ai_review/tests/suites/clients/claude/test_client.py,sha256=jLGqK7lzYc8LjJO-3HjBtLqCdg-ubw-xZ4EvFeFGRhY,404
196
204
  ai_review/tests/suites/clients/claude/test_schema.py,sha256=MUZXvEROgLNpUVHfCsH5D3ruJPQwTx0OgeT3_BRVjgI,1671
@@ -199,8 +207,10 @@ ai_review/tests/suites/clients/gemini/test_client.py,sha256=f2R7KisiENrzf8gaK26N
199
207
  ai_review/tests/suites/clients/gemini/test_schema.py,sha256=88dU28m7sEWvGx6tqYl7if7weWYuVc8erlkFkKKI3bk,3109
200
208
  ai_review/tests/suites/clients/github/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
201
209
  ai_review/tests/suites/clients/github/test_client.py,sha256=BiuLKCHIk83U1szYEZkB-n3vvyPgj6tAI5EqxKiT-CY,558
210
+ ai_review/tests/suites/clients/github/test_tools.py,sha256=_RKMWNgfeynnpbiDebRLg-1Qz91Kyevf5drl4hCngzU,881
202
211
  ai_review/tests/suites/clients/gitlab/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
203
212
  ai_review/tests/suites/clients/gitlab/test_client.py,sha256=5QOkNvgm0XRKHh79FNIY9CTonAqYPXqCCxcxeiAHYCA,560
213
+ ai_review/tests/suites/clients/gitlab/test_tools.py,sha256=-gS_kvZwNBkvYeYyYPld78F4ZuZPrpNORWVbg2eq5wM,678
204
214
  ai_review/tests/suites/clients/ollama/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
205
215
  ai_review/tests/suites/clients/ollama/test_client.py,sha256=XZ8NAd1bS_ltTuYZPgqlutPRA6kbvH3_3SKTCbNBTgA,404
206
216
  ai_review/tests/suites/clients/ollama/test_schema.py,sha256=A93wCmxwGdvudfbA97VCPYP3gT6u6EYMetAg5fgURRA,1836
@@ -217,6 +227,8 @@ ai_review/tests/suites/libs/diff/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRk
217
227
  ai_review/tests/suites/libs/diff/test_models.py,sha256=RBFQ97LWhU8TlupxXkJ97ryAvJrSuOHLtT9biUBUMXg,3321
218
228
  ai_review/tests/suites/libs/diff/test_parser.py,sha256=rvWEVGIdaLBlDAnSevjRY7I1Zikj12d5GOgMk9QyHQQ,3013
219
229
  ai_review/tests/suites/libs/diff/test_tools.py,sha256=XkHJZ-b5veFz5oLKO09P7npaLN8lOzCnGR7e83Zv_mg,1953
230
+ ai_review/tests/suites/libs/http/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
231
+ ai_review/tests/suites/libs/http/test_paginate.py,sha256=9PCn1mUQypPId9l8RToBQnLBJBV_WEYxBh785bqqmMA,2694
220
232
  ai_review/tests/suites/libs/template/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
221
233
  ai_review/tests/suites/libs/template/test_render.py,sha256=n-ss5bd_hwc-RzYmqWmFM6KSlP1zLSnlsW1Yki12Bpw,1890
222
234
  ai_review/tests/suites/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -262,9 +274,9 @@ ai_review/tests/suites/services/vcs/github/__init__.py,sha256=47DEQpj8HBSa-_TImW
262
274
  ai_review/tests/suites/services/vcs/github/test_service.py,sha256=c2sjecm4qzqYXuO9j6j35NQyJzqDpnXIJImRTcpkyHo,4378
263
275
  ai_review/tests/suites/services/vcs/gitlab/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
264
276
  ai_review/tests/suites/services/vcs/gitlab/test_service.py,sha256=0dqgL5whzjcP-AQ4adP_12QfkYm_ZtdtMotmYm8Se7Y,4449
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,,
277
+ xai_review-0.27.0.dist-info/licenses/LICENSE,sha256=p-v8m7Kmz4KKc7PcvsGiGEmCw9AiSXY4_ylOPy_u--Y,11343
278
+ xai_review-0.27.0.dist-info/METADATA,sha256=5SnK3Ip4vzfwvS110fYNDSDYD4OzrmA5G1eUV2oGMbE,11150
279
+ xai_review-0.27.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
280
+ xai_review-0.27.0.dist-info/entry_points.txt,sha256=JyC5URanMi5io5P_PXQf7H_I1OGIpk5cZQhaPQ0g4Zs,53
281
+ xai_review-0.27.0.dist-info/top_level.txt,sha256=sTsZbfzLoqvRZKdKa-BcxWvjlHdrpbeJ6DrGY0EuR0E,10
282
+ xai_review-0.27.0.dist-info/RECORD,,