git-alternative 0.2.2__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.
@@ -0,0 +1,292 @@
1
+ """Issue resource manager."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime
6
+
7
+ from git_alternative.http import HttpClient
8
+ from git_alternative.models import Comment, Issue, IssueState, Label
9
+
10
+
11
+ def _parse_label(data: dict) -> Label:
12
+ return Label(
13
+ id=data["id"],
14
+ name=data["name"],
15
+ color=data["color"],
16
+ description=data.get("description"),
17
+ )
18
+
19
+
20
+ def _parse_issue(data: dict) -> Issue:
21
+ return Issue(
22
+ id=data["id"],
23
+ repo_id=data["repo_id"],
24
+ number=data["number"],
25
+ title=data["title"],
26
+ author_id=data["author_id"],
27
+ state=IssueState(data["state"]),
28
+ created_at=datetime.fromisoformat(data["created_at"]),
29
+ updated_at=datetime.fromisoformat(data["updated_at"]),
30
+ body=data.get("body"),
31
+ labels=[_parse_label(lb) for lb in data.get("labels", [])],
32
+ assignees=data.get("assignees", []),
33
+ milestone_id=data.get("milestone_id"),
34
+ closed_at=(
35
+ datetime.fromisoformat(data["closed_at"]) if data.get("closed_at") else None
36
+ ),
37
+ comment_count=data.get("comment_count", 0),
38
+ )
39
+
40
+
41
+ def _parse_comment(data: dict) -> Comment:
42
+ return Comment(
43
+ id=data["id"],
44
+ author_id=data["author_id"],
45
+ body=data["body"],
46
+ created_at=datetime.fromisoformat(data["created_at"]),
47
+ updated_at=datetime.fromisoformat(data["updated_at"]),
48
+ )
49
+
50
+
51
+ class IssueManager:
52
+ """Manages issue operations against the Forge API.
53
+
54
+ Args:
55
+ http: An :class:`~git_alternative.http.HttpClient` instance.
56
+ """
57
+
58
+ def __init__(self, http: HttpClient) -> None:
59
+ self._http = http
60
+
61
+ def list(
62
+ self,
63
+ owner: str,
64
+ repo: str,
65
+ state: str = "open",
66
+ labels: list[str] | None = None,
67
+ page: int = 1,
68
+ per_page: int = 30,
69
+ ) -> list[Issue]:
70
+ """List issues in a repository.
71
+
72
+ Args:
73
+ owner: Repository owner username.
74
+ repo: Repository name.
75
+ state: Filter by state: ``"open"``, ``"closed"``, or ``"all"``.
76
+ labels: Filter by label names.
77
+ page: Page number (1-based).
78
+ per_page: Items per page.
79
+
80
+ Returns:
81
+ List of :class:`~git_alternative.models.Issue` objects.
82
+ """
83
+ params: dict = {"state": state, "page": page, "per_page": per_page}
84
+ if labels:
85
+ params["labels"] = ",".join(labels)
86
+ data = self._http.get(f"/api/v1/repos/{owner}/{repo}/issues", params=params)
87
+ return [_parse_issue(i) for i in data]
88
+
89
+ def get(self, owner: str, repo: str, issue_number: int) -> Issue:
90
+ """Fetch a single issue by number.
91
+
92
+ Args:
93
+ owner: Repository owner username.
94
+ repo: Repository name.
95
+ issue_number: The issue number.
96
+
97
+ Returns:
98
+ An :class:`~git_alternative.models.Issue` instance.
99
+ """
100
+ data = self._http.get(f"/api/v1/repos/{owner}/{repo}/issues/{issue_number}")
101
+ return _parse_issue(data)
102
+
103
+ def create(
104
+ self,
105
+ owner: str,
106
+ repo: str,
107
+ title: str,
108
+ body: str | None = None,
109
+ labels: list[str] | None = None,
110
+ assignees: list[str] | None = None,
111
+ milestone_id: str | None = None,
112
+ ) -> Issue:
113
+ """Create a new issue.
114
+
115
+ Args:
116
+ owner: Repository owner username.
117
+ repo: Repository name.
118
+ title: Issue title.
119
+ body: Issue body in Markdown.
120
+ labels: Label names to attach.
121
+ assignees: Usernames to assign.
122
+ milestone_id: Milestone ID to associate.
123
+
124
+ Returns:
125
+ The newly created :class:`~git_alternative.models.Issue`.
126
+ """
127
+ payload: dict = {"title": title}
128
+ if body is not None:
129
+ payload["body"] = body
130
+ if labels:
131
+ payload["labels"] = labels
132
+ if assignees:
133
+ payload["assignees"] = assignees
134
+ if milestone_id:
135
+ payload["milestone_id"] = milestone_id
136
+ data = self._http.post(f"/api/v1/repos/{owner}/{repo}/issues", json=payload)
137
+ return _parse_issue(data)
138
+
139
+ def update(
140
+ self,
141
+ owner: str,
142
+ repo: str,
143
+ issue_number: int,
144
+ title: str | None = None,
145
+ body: str | None = None,
146
+ state: str | None = None,
147
+ ) -> Issue:
148
+ """Update an existing issue.
149
+
150
+ Args:
151
+ owner: Repository owner username.
152
+ repo: Repository name.
153
+ issue_number: The issue number.
154
+ title: New title (optional).
155
+ body: New body (optional).
156
+ state: New state: ``"open"`` or ``"closed"`` (optional).
157
+
158
+ Returns:
159
+ The updated :class:`~git_alternative.models.Issue`.
160
+ """
161
+ payload: dict = {}
162
+ if title is not None:
163
+ payload["title"] = title
164
+ if body is not None:
165
+ payload["body"] = body
166
+ if state is not None:
167
+ payload["state"] = state
168
+ data = self._http.patch(
169
+ f"/api/v1/repos/{owner}/{repo}/issues/{issue_number}", json=payload
170
+ )
171
+ return _parse_issue(data)
172
+
173
+ def delete(self, owner: str, repo: str, issue_number: int) -> None:
174
+ """Delete an issue.
175
+
176
+ Args:
177
+ owner: Repository owner username.
178
+ repo: Repository name.
179
+ issue_number: The issue number.
180
+ """
181
+ self._http.delete(f"/api/v1/repos/{owner}/{repo}/issues/{issue_number}")
182
+
183
+ def list_comments(self, owner: str, repo: str, issue_number: int) -> list[Comment]:
184
+ """List all comments on an issue.
185
+
186
+ Args:
187
+ owner: Repository owner username.
188
+ repo: Repository name.
189
+ issue_number: The issue number.
190
+
191
+ Returns:
192
+ List of :class:`~git_alternative.models.Comment` objects.
193
+ """
194
+ data = self._http.get(
195
+ f"/api/v1/repos/{owner}/{repo}/issues/{issue_number}/comments"
196
+ )
197
+ return [_parse_comment(c) for c in data]
198
+
199
+ def add_comment(
200
+ self, owner: str, repo: str, issue_number: int, body: str
201
+ ) -> Comment:
202
+ """Add a comment to an issue.
203
+
204
+ Args:
205
+ owner: Repository owner username.
206
+ repo: Repository name.
207
+ issue_number: The issue number.
208
+ body: Comment body in Markdown.
209
+
210
+ Returns:
211
+ The newly created :class:`~git_alternative.models.Comment`.
212
+ """
213
+ data = self._http.post(
214
+ f"/api/v1/repos/{owner}/{repo}/issues/{issue_number}/comments",
215
+ json={"body": body},
216
+ )
217
+ return _parse_comment(data)
218
+
219
+ def update_comment(
220
+ self, owner: str, repo: str, issue_number: int, comment_id: str, body: str
221
+ ) -> Comment:
222
+ """Update an existing comment.
223
+
224
+ Args:
225
+ owner: Repository owner username.
226
+ repo: Repository name.
227
+ issue_number: The issue number.
228
+ comment_id: The comment ID.
229
+ body: New comment body.
230
+
231
+ Returns:
232
+ The updated :class:`~git_alternative.models.Comment`.
233
+ """
234
+ data = self._http.patch(
235
+ f"/api/v1/repos/{owner}/{repo}/issues/{issue_number}/comments/{comment_id}",
236
+ json={"body": body},
237
+ )
238
+ return _parse_comment(data)
239
+
240
+ def delete_comment(
241
+ self, owner: str, repo: str, issue_number: int, comment_id: str
242
+ ) -> None:
243
+ """Delete a comment from an issue.
244
+
245
+ Args:
246
+ owner: Repository owner username.
247
+ repo: Repository name.
248
+ issue_number: The issue number.
249
+ comment_id: The comment ID.
250
+ """
251
+ self._http.delete(
252
+ f"/api/v1/repos/{owner}/{repo}/issues/{issue_number}/comments/{comment_id}"
253
+ )
254
+
255
+ def list_labels(self, owner: str, repo: str) -> list[Label]:
256
+ """List all labels defined in a repository.
257
+
258
+ Args:
259
+ owner: Repository owner username.
260
+ repo: Repository name.
261
+
262
+ Returns:
263
+ List of :class:`~git_alternative.models.Label` objects.
264
+ """
265
+ data = self._http.get(f"/api/v1/repos/{owner}/{repo}/labels")
266
+ return [_parse_label(lb) for lb in data]
267
+
268
+ def create_label(
269
+ self,
270
+ owner: str,
271
+ repo: str,
272
+ name: str,
273
+ color: str,
274
+ description: str | None = None,
275
+ ) -> Label:
276
+ """Create a new label in a repository.
277
+
278
+ Args:
279
+ owner: Repository owner username.
280
+ repo: Repository name.
281
+ name: Label name.
282
+ color: Hex color code (e.g. ``"#ff0000"``).
283
+ description: Optional label description.
284
+
285
+ Returns:
286
+ The newly created :class:`~git_alternative.models.Label`.
287
+ """
288
+ payload: dict = {"name": name, "color": color}
289
+ if description:
290
+ payload["description"] = description
291
+ data = self._http.post(f"/api/v1/repos/{owner}/{repo}/labels", json=payload)
292
+ return _parse_label(data)
@@ -0,0 +1,218 @@
1
+ """Repository resource manager."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime
6
+
7
+ from git_alternative.http import HttpClient
8
+ from git_alternative.models import (
9
+ Branch,
10
+ CommitSummary,
11
+ CreateRepoRequest,
12
+ PaginatedResponse,
13
+ Repository,
14
+ )
15
+ from git_alternative.pagination import paginate_commits
16
+
17
+
18
+ def _parse_repo(data: dict) -> Repository:
19
+ return Repository(
20
+ id=data["id"],
21
+ owner_id=data["owner_id"],
22
+ name=data["name"],
23
+ description=data.get("description"),
24
+ is_private=data.get("is_private", False),
25
+ is_fork=data.get("is_fork", False),
26
+ default_branch=data.get("default_branch", "main"),
27
+ created_at=datetime.fromisoformat(data["created_at"]),
28
+ updated_at=datetime.fromisoformat(data["updated_at"]),
29
+ star_count=data.get("star_count", 0),
30
+ fork_count=data.get("fork_count", 0),
31
+ fork_of=data.get("fork_of"),
32
+ )
33
+
34
+
35
+ def _parse_branch(data: dict) -> Branch:
36
+ return Branch(
37
+ name=data["name"],
38
+ sha=data["sha"],
39
+ is_default=data.get("is_default", False),
40
+ is_protected=data.get("is_protected", False),
41
+ )
42
+
43
+
44
+ def _parse_commit(data: dict) -> CommitSummary:
45
+ return CommitSummary(
46
+ sha=data["sha"],
47
+ message=data["message"],
48
+ author_name=data["author_name"],
49
+ author_email=data["author_email"],
50
+ authored_at=datetime.fromisoformat(data["authored_at"]),
51
+ committer_name=data["committer_name"],
52
+ committer_email=data["committer_email"],
53
+ committed_at=datetime.fromisoformat(data["committed_at"]),
54
+ parents=data.get("parents", []),
55
+ )
56
+
57
+
58
+ class RepoManager:
59
+ """Manages repository operations against the Forge API.
60
+
61
+ Args:
62
+ http: An :class:`~git_alternative.http.HttpClient` instance.
63
+ """
64
+
65
+ def __init__(self, http: HttpClient) -> None:
66
+ self._http = http
67
+
68
+ def list(self, owner: str) -> list[Repository]:
69
+ """List all repositories for a user.
70
+
71
+ Args:
72
+ owner: The username whose repositories to list.
73
+
74
+ Returns:
75
+ List of :class:`~git_alternative.models.Repository` objects.
76
+ """
77
+ data = self._http.get(f"/api/v1/users/{owner}/repos")
78
+ return [_parse_repo(r) for r in data]
79
+
80
+ def get(self, owner: str, repo: str) -> Repository:
81
+ """Fetch a single repository by owner and name.
82
+
83
+ Args:
84
+ owner: Repository owner username.
85
+ repo: Repository name.
86
+
87
+ Returns:
88
+ A :class:`~git_alternative.models.Repository` instance.
89
+ """
90
+ data = self._http.get(f"/api/v1/repos/{owner}/{repo}")
91
+ return _parse_repo(data)
92
+
93
+ def create(self, request: CreateRepoRequest) -> Repository:
94
+ """Create a new repository.
95
+
96
+ Args:
97
+ request: A :class:`~git_alternative.models.CreateRepoRequest` with
98
+ the desired repository settings.
99
+
100
+ Returns:
101
+ The newly created :class:`~git_alternative.models.Repository`.
102
+ """
103
+ payload = {
104
+ "name": request.name,
105
+ "description": request.description,
106
+ "is_private": request.is_private,
107
+ "default_branch": request.default_branch,
108
+ "auto_init": request.auto_init,
109
+ }
110
+ data = self._http.post("/api/v1/repos", json=payload)
111
+ return _parse_repo(data)
112
+
113
+ def delete(self, owner: str, repo: str) -> None:
114
+ """Delete a repository.
115
+
116
+ Args:
117
+ owner: Repository owner username.
118
+ repo: Repository name.
119
+ """
120
+ self._http.delete(f"/api/v1/repos/{owner}/{repo}")
121
+
122
+ def update(
123
+ self,
124
+ owner: str,
125
+ repo: str,
126
+ description: str | None = None,
127
+ is_private: bool | None = None,
128
+ default_branch: str | None = None,
129
+ ) -> Repository:
130
+ """Update repository settings.
131
+
132
+ Args:
133
+ owner: Repository owner username.
134
+ repo: Repository name.
135
+ description: New description (optional).
136
+ is_private: New visibility setting (optional).
137
+ default_branch: New default branch name (optional).
138
+
139
+ Returns:
140
+ The updated :class:`~git_alternative.models.Repository`.
141
+ """
142
+ payload: dict = {}
143
+ if description is not None:
144
+ payload["description"] = description
145
+ if is_private is not None:
146
+ payload["is_private"] = is_private
147
+ if default_branch is not None:
148
+ payload["default_branch"] = default_branch
149
+ data = self._http.patch(f"/api/v1/repos/{owner}/{repo}", json=payload)
150
+ return _parse_repo(data)
151
+
152
+ def list_branches(self, owner: str, repo: str) -> list[Branch]:
153
+ """List all branches in a repository.
154
+
155
+ Args:
156
+ owner: Repository owner username.
157
+ repo: Repository name.
158
+
159
+ Returns:
160
+ List of :class:`~git_alternative.models.Branch` objects.
161
+ """
162
+ data = self._http.get(f"/api/v1/repos/{owner}/{repo}/branches")
163
+ return [_parse_branch(b) for b in data]
164
+
165
+ def get_branch(self, owner: str, repo: str, branch: str) -> Branch:
166
+ """Fetch a single branch by name.
167
+
168
+ Args:
169
+ owner: Repository owner username.
170
+ repo: Repository name.
171
+ branch: Branch name.
172
+
173
+ Returns:
174
+ A :class:`~git_alternative.models.Branch` instance.
175
+ """
176
+ data = self._http.get(f"/api/v1/repos/{owner}/{repo}/branches/{branch}")
177
+ return _parse_branch(data)
178
+
179
+ def list_commits(
180
+ self,
181
+ owner: str,
182
+ repo: str,
183
+ page: int = 1,
184
+ per_page: int = 30,
185
+ ) -> PaginatedResponse[CommitSummary]:
186
+ """List commits in a repository with pagination.
187
+
188
+ Args:
189
+ owner: Repository owner username.
190
+ repo: Repository name.
191
+ page: Page number (1-based).
192
+ per_page: Items per page (max 100).
193
+
194
+ Returns:
195
+ A :class:`~git_alternative.models.PaginatedResponse` of
196
+ :class:`~git_alternative.models.CommitSummary` objects.
197
+ """
198
+ capped = min(per_page, 100)
199
+ data = self._http.get(
200
+ f"/api/v1/repos/{owner}/{repo}/commits",
201
+ params={"page": page, "per_page": capped + 1},
202
+ )
203
+ raw = [_parse_commit(c) for c in data]
204
+ return paginate_commits(raw, page=page, per_page=capped)
205
+
206
+ def get_commit(self, owner: str, repo: str, sha: str) -> CommitSummary:
207
+ """Fetch a single commit by SHA.
208
+
209
+ Args:
210
+ owner: Repository owner username.
211
+ repo: Repository name.
212
+ sha: Full or abbreviated commit SHA.
213
+
214
+ Returns:
215
+ A :class:`~git_alternative.models.CommitSummary` instance.
216
+ """
217
+ data = self._http.get(f"/api/v1/repos/{owner}/{repo}/commits/{sha}")
218
+ return _parse_commit(data)