pierre-storage 0.10.0__py3-none-any.whl → 0.12.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.
- pierre_storage/__init__.py +8 -0
- pierre_storage/client.py +116 -17
- pierre_storage/repo.py +235 -0
- pierre_storage/types.py +110 -1
- {pierre_storage-0.10.0.dist-info → pierre_storage-0.12.0.dist-info}/METADATA +65 -5
- pierre_storage-0.12.0.dist-info/RECORD +15 -0
- {pierre_storage-0.10.0.dist-info → pierre_storage-0.12.0.dist-info}/WHEEL +1 -1
- pierre_storage-0.10.0.dist-info/RECORD +0 -15
- {pierre_storage-0.10.0.dist-info → pierre_storage-0.12.0.dist-info}/licenses/LICENSE +0 -0
- {pierre_storage-0.10.0.dist-info → pierre_storage-0.12.0.dist-info}/top_level.txt +0 -0
pierre_storage/__init__.py
CHANGED
|
@@ -27,8 +27,12 @@ from pierre_storage.types import (
|
|
|
27
27
|
ListBranchesResult,
|
|
28
28
|
ListCommitsResult,
|
|
29
29
|
ListFilesResult,
|
|
30
|
+
ListReposResult,
|
|
31
|
+
NoteReadResult,
|
|
32
|
+
NoteWriteResult,
|
|
30
33
|
RefUpdate,
|
|
31
34
|
Repo,
|
|
35
|
+
RepoInfo,
|
|
32
36
|
RestoreCommitResult,
|
|
33
37
|
)
|
|
34
38
|
from pierre_storage.version import PACKAGE_VERSION
|
|
@@ -71,7 +75,11 @@ __all__ = [
|
|
|
71
75
|
"ListBranchesResult",
|
|
72
76
|
"ListCommitsResult",
|
|
73
77
|
"ListFilesResult",
|
|
78
|
+
"ListReposResult",
|
|
79
|
+
"NoteReadResult",
|
|
80
|
+
"NoteWriteResult",
|
|
74
81
|
"RefUpdate",
|
|
82
|
+
"RepoInfo",
|
|
75
83
|
"Repo",
|
|
76
84
|
"RestoreCommitResult",
|
|
77
85
|
# Webhook
|
pierre_storage/client.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"""Main client for Pierre Git Storage SDK."""
|
|
2
2
|
|
|
3
3
|
import uuid
|
|
4
|
-
from typing import Any, Dict, Optional
|
|
4
|
+
from typing import Any, Dict, Optional, cast
|
|
5
|
+
from urllib.parse import urlencode
|
|
5
6
|
|
|
6
7
|
import httpx
|
|
7
8
|
|
|
@@ -9,9 +10,14 @@ from pierre_storage.auth import generate_jwt
|
|
|
9
10
|
from pierre_storage.errors import ApiError
|
|
10
11
|
from pierre_storage.repo import DEFAULT_TOKEN_TTL_SECONDS, RepoImpl
|
|
11
12
|
from pierre_storage.types import (
|
|
13
|
+
BaseRepo,
|
|
12
14
|
DeleteRepoResult,
|
|
15
|
+
ForkBaseRepo,
|
|
16
|
+
GitHubBaseRepo,
|
|
13
17
|
GitStorageOptions,
|
|
18
|
+
ListReposResult,
|
|
14
19
|
Repo,
|
|
20
|
+
RepoInfo,
|
|
15
21
|
)
|
|
16
22
|
from pierre_storage.version import get_user_agent
|
|
17
23
|
|
|
@@ -99,17 +105,18 @@ class GitStorage:
|
|
|
99
105
|
self,
|
|
100
106
|
*,
|
|
101
107
|
id: Optional[str] = None,
|
|
102
|
-
default_branch: str =
|
|
103
|
-
base_repo: Optional[
|
|
108
|
+
default_branch: Optional[str] = None,
|
|
109
|
+
base_repo: Optional[BaseRepo] = None,
|
|
104
110
|
ttl: Optional[int] = None,
|
|
105
111
|
) -> Repo:
|
|
106
112
|
"""Create a new repository.
|
|
107
113
|
|
|
108
114
|
Args:
|
|
109
115
|
id: Repository ID (auto-generated if not provided)
|
|
110
|
-
default_branch: Default branch name (default: "main")
|
|
111
|
-
base_repo: Optional base repository for GitHub sync
|
|
112
|
-
|
|
116
|
+
default_branch: Default branch name (default: "main" for non-forks)
|
|
117
|
+
base_repo: Optional base repository for GitHub sync or fork
|
|
118
|
+
GitHub: owner, name, default_branch
|
|
119
|
+
Fork: id, ref, sha
|
|
113
120
|
ttl: Token TTL in seconds
|
|
114
121
|
|
|
115
122
|
Returns:
|
|
@@ -126,19 +133,51 @@ class GitStorage:
|
|
|
126
133
|
)
|
|
127
134
|
|
|
128
135
|
url = f"{self.options['api_base_url']}/api/v{self.options['api_version']}/repos"
|
|
129
|
-
body: Dict[str, Any] = {
|
|
136
|
+
body: Dict[str, Any] = {}
|
|
130
137
|
|
|
131
138
|
# Match backend priority: base_repo.default_branch > default_branch > 'main'
|
|
132
|
-
|
|
139
|
+
explicit_default_branch = default_branch is not None
|
|
140
|
+
resolved_default_branch: Optional[str] = None
|
|
141
|
+
|
|
133
142
|
if base_repo:
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
143
|
+
if "id" in base_repo:
|
|
144
|
+
fork_repo = cast(ForkBaseRepo, base_repo)
|
|
145
|
+
base_repo_token = self._generate_jwt(
|
|
146
|
+
fork_repo["id"],
|
|
147
|
+
{"permissions": ["git:read"], "ttl": ttl},
|
|
148
|
+
)
|
|
149
|
+
base_repo_payload: Dict[str, Any] = {
|
|
150
|
+
"provider": "code",
|
|
151
|
+
"name": fork_repo["id"],
|
|
152
|
+
"operation": "fork",
|
|
153
|
+
"auth": {"token": base_repo_token},
|
|
154
|
+
}
|
|
155
|
+
if fork_repo.get("ref"):
|
|
156
|
+
base_repo_payload["ref"] = fork_repo["ref"]
|
|
157
|
+
if fork_repo.get("sha"):
|
|
158
|
+
base_repo_payload["sha"] = fork_repo["sha"]
|
|
159
|
+
body["base_repo"] = base_repo_payload
|
|
160
|
+
if explicit_default_branch:
|
|
161
|
+
resolved_default_branch = default_branch
|
|
162
|
+
body["default_branch"] = default_branch
|
|
163
|
+
else:
|
|
164
|
+
github_repo = cast(GitHubBaseRepo, base_repo)
|
|
165
|
+
# Ensure provider is set to 'github' if not provided
|
|
166
|
+
base_repo_with_provider = {
|
|
167
|
+
"provider": "github",
|
|
168
|
+
**github_repo,
|
|
169
|
+
}
|
|
170
|
+
body["base_repo"] = base_repo_with_provider
|
|
171
|
+
if github_repo.get("default_branch"):
|
|
172
|
+
resolved_default_branch = github_repo["default_branch"]
|
|
173
|
+
elif explicit_default_branch:
|
|
174
|
+
resolved_default_branch = default_branch
|
|
175
|
+
else:
|
|
176
|
+
resolved_default_branch = "main"
|
|
177
|
+
body["default_branch"] = resolved_default_branch
|
|
178
|
+
else:
|
|
179
|
+
resolved_default_branch = default_branch if explicit_default_branch else "main"
|
|
180
|
+
body["default_branch"] = resolved_default_branch
|
|
142
181
|
|
|
143
182
|
async with httpx.AsyncClient() as client:
|
|
144
183
|
response = await client.post(
|
|
@@ -170,7 +209,7 @@ class GitStorage:
|
|
|
170
209
|
|
|
171
210
|
return RepoImpl(
|
|
172
211
|
repo_id,
|
|
173
|
-
resolved_default_branch,
|
|
212
|
+
resolved_default_branch or "main",
|
|
174
213
|
api_base_url,
|
|
175
214
|
storage_base_url,
|
|
176
215
|
name,
|
|
@@ -178,6 +217,66 @@ class GitStorage:
|
|
|
178
217
|
self._generate_jwt,
|
|
179
218
|
)
|
|
180
219
|
|
|
220
|
+
async def list_repos(
|
|
221
|
+
self,
|
|
222
|
+
*,
|
|
223
|
+
cursor: Optional[str] = None,
|
|
224
|
+
limit: Optional[int] = None,
|
|
225
|
+
ttl: Optional[int] = None,
|
|
226
|
+
) -> ListReposResult:
|
|
227
|
+
"""List repositories for the organization."""
|
|
228
|
+
ttl = ttl or DEFAULT_TOKEN_TTL_SECONDS
|
|
229
|
+
jwt = self._generate_jwt(
|
|
230
|
+
"org",
|
|
231
|
+
{"permissions": ["org:read"], "ttl": ttl},
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
params: Dict[str, str] = {}
|
|
235
|
+
if cursor:
|
|
236
|
+
params["cursor"] = cursor
|
|
237
|
+
if limit is not None:
|
|
238
|
+
params["limit"] = str(limit)
|
|
239
|
+
|
|
240
|
+
url = f"{self.options['api_base_url']}/api/v{self.options['api_version']}/repos"
|
|
241
|
+
if params:
|
|
242
|
+
url += f"?{urlencode(params)}"
|
|
243
|
+
|
|
244
|
+
async with httpx.AsyncClient() as client:
|
|
245
|
+
response = await client.get(
|
|
246
|
+
url,
|
|
247
|
+
headers={
|
|
248
|
+
"Authorization": f"Bearer {jwt}",
|
|
249
|
+
"Code-Storage-Agent": get_user_agent(),
|
|
250
|
+
},
|
|
251
|
+
timeout=30.0,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
if not response.is_success:
|
|
255
|
+
raise ApiError(
|
|
256
|
+
f"Failed to list repositories: {response.status_code} {response.reason_phrase}",
|
|
257
|
+
status_code=response.status_code,
|
|
258
|
+
response=response,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
data = response.json()
|
|
262
|
+
repos: list[RepoInfo] = []
|
|
263
|
+
for repo in data.get("repos", []):
|
|
264
|
+
entry: RepoInfo = {
|
|
265
|
+
"repo_id": repo.get("repo_id", ""),
|
|
266
|
+
"url": repo.get("url", ""),
|
|
267
|
+
"default_branch": repo.get("default_branch", "main"),
|
|
268
|
+
"created_at": repo.get("created_at", ""),
|
|
269
|
+
}
|
|
270
|
+
if repo.get("base_repo"):
|
|
271
|
+
entry["base_repo"] = repo.get("base_repo")
|
|
272
|
+
repos.append(entry)
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
"repos": repos,
|
|
276
|
+
"next_cursor": data.get("next_cursor"),
|
|
277
|
+
"has_more": data.get("has_more", False),
|
|
278
|
+
}
|
|
279
|
+
|
|
181
280
|
async def find_one(self, *, id: str) -> Optional[Repo]:
|
|
182
281
|
"""Find a repository by ID.
|
|
183
282
|
|
pierre_storage/repo.py
CHANGED
|
@@ -33,6 +33,8 @@ from pierre_storage.types import (
|
|
|
33
33
|
ListBranchesResult,
|
|
34
34
|
ListCommitsResult,
|
|
35
35
|
ListFilesResult,
|
|
36
|
+
NoteReadResult,
|
|
37
|
+
NoteWriteResult,
|
|
36
38
|
RefUpdate,
|
|
37
39
|
RestoreCommitResult,
|
|
38
40
|
)
|
|
@@ -498,6 +500,122 @@ class RepoImpl:
|
|
|
498
500
|
"has_more": data["has_more"],
|
|
499
501
|
}
|
|
500
502
|
|
|
503
|
+
async def get_note(
|
|
504
|
+
self,
|
|
505
|
+
*,
|
|
506
|
+
sha: str,
|
|
507
|
+
ttl: Optional[int] = None,
|
|
508
|
+
) -> NoteReadResult:
|
|
509
|
+
"""Read a git note."""
|
|
510
|
+
sha_clean = sha.strip()
|
|
511
|
+
if not sha_clean:
|
|
512
|
+
raise ValueError("get_note sha is required")
|
|
513
|
+
|
|
514
|
+
ttl = ttl or DEFAULT_TOKEN_TTL_SECONDS
|
|
515
|
+
jwt = self.generate_jwt(self._id, {"permissions": ["git:read"], "ttl": ttl})
|
|
516
|
+
|
|
517
|
+
url = f"{self.api_base_url}/api/v{self.api_version}/repos/notes?{urlencode({'sha': sha_clean})}"
|
|
518
|
+
|
|
519
|
+
async with httpx.AsyncClient() as client:
|
|
520
|
+
response = await client.get(
|
|
521
|
+
url,
|
|
522
|
+
headers={
|
|
523
|
+
"Authorization": f"Bearer {jwt}",
|
|
524
|
+
"Code-Storage-Agent": get_user_agent(),
|
|
525
|
+
},
|
|
526
|
+
timeout=30.0,
|
|
527
|
+
)
|
|
528
|
+
response.raise_for_status()
|
|
529
|
+
data = response.json()
|
|
530
|
+
return {
|
|
531
|
+
"sha": data["sha"],
|
|
532
|
+
"note": data["note"],
|
|
533
|
+
"ref_sha": data["ref_sha"],
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
async def create_note(
|
|
537
|
+
self,
|
|
538
|
+
*,
|
|
539
|
+
sha: str,
|
|
540
|
+
note: str,
|
|
541
|
+
expected_ref_sha: Optional[str] = None,
|
|
542
|
+
author: Optional[CommitSignature] = None,
|
|
543
|
+
ttl: Optional[int] = None,
|
|
544
|
+
) -> NoteWriteResult:
|
|
545
|
+
"""Create a git note."""
|
|
546
|
+
return await self._write_note(
|
|
547
|
+
action_label="create_note",
|
|
548
|
+
action="add",
|
|
549
|
+
sha=sha,
|
|
550
|
+
note=note,
|
|
551
|
+
expected_ref_sha=expected_ref_sha,
|
|
552
|
+
author=author,
|
|
553
|
+
ttl=ttl,
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
async def append_note(
|
|
557
|
+
self,
|
|
558
|
+
*,
|
|
559
|
+
sha: str,
|
|
560
|
+
note: str,
|
|
561
|
+
expected_ref_sha: Optional[str] = None,
|
|
562
|
+
author: Optional[CommitSignature] = None,
|
|
563
|
+
ttl: Optional[int] = None,
|
|
564
|
+
) -> NoteWriteResult:
|
|
565
|
+
"""Append to a git note."""
|
|
566
|
+
return await self._write_note(
|
|
567
|
+
action_label="append_note",
|
|
568
|
+
action="append",
|
|
569
|
+
sha=sha,
|
|
570
|
+
note=note,
|
|
571
|
+
expected_ref_sha=expected_ref_sha,
|
|
572
|
+
author=author,
|
|
573
|
+
ttl=ttl,
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
async def delete_note(
|
|
577
|
+
self,
|
|
578
|
+
*,
|
|
579
|
+
sha: str,
|
|
580
|
+
expected_ref_sha: Optional[str] = None,
|
|
581
|
+
author: Optional[CommitSignature] = None,
|
|
582
|
+
ttl: Optional[int] = None,
|
|
583
|
+
) -> NoteWriteResult:
|
|
584
|
+
"""Delete a git note."""
|
|
585
|
+
sha_clean = sha.strip()
|
|
586
|
+
if not sha_clean:
|
|
587
|
+
raise ValueError("delete_note sha is required")
|
|
588
|
+
|
|
589
|
+
ttl = ttl or DEFAULT_TOKEN_TTL_SECONDS
|
|
590
|
+
jwt = self.generate_jwt(self._id, {"permissions": ["git:write"], "ttl": ttl})
|
|
591
|
+
|
|
592
|
+
payload: Dict[str, Any] = {"sha": sha_clean}
|
|
593
|
+
if expected_ref_sha and expected_ref_sha.strip():
|
|
594
|
+
payload["expected_ref_sha"] = expected_ref_sha.strip()
|
|
595
|
+
if author:
|
|
596
|
+
author_name = author.get("name", "").strip()
|
|
597
|
+
author_email = author.get("email", "").strip()
|
|
598
|
+
if not author_name or not author_email:
|
|
599
|
+
raise ValueError("delete_note author name and email are required when provided")
|
|
600
|
+
payload["author"] = {"name": author_name, "email": author_email}
|
|
601
|
+
|
|
602
|
+
url = f"{self.api_base_url}/api/v{self.api_version}/repos/notes"
|
|
603
|
+
|
|
604
|
+
async with httpx.AsyncClient() as client:
|
|
605
|
+
response = await client.request(
|
|
606
|
+
"DELETE",
|
|
607
|
+
url,
|
|
608
|
+
headers={
|
|
609
|
+
"Authorization": f"Bearer {jwt}",
|
|
610
|
+
"Content-Type": "application/json",
|
|
611
|
+
"Code-Storage-Agent": get_user_agent(),
|
|
612
|
+
},
|
|
613
|
+
json=payload,
|
|
614
|
+
timeout=30.0,
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
return self._parse_note_write_response(response, "delete_note")
|
|
618
|
+
|
|
501
619
|
async def get_branch_diff(
|
|
502
620
|
self,
|
|
503
621
|
*,
|
|
@@ -1046,6 +1164,123 @@ class RepoImpl:
|
|
|
1046
1164
|
self.api_version,
|
|
1047
1165
|
)
|
|
1048
1166
|
|
|
1167
|
+
async def _write_note(
|
|
1168
|
+
self,
|
|
1169
|
+
*,
|
|
1170
|
+
action_label: str,
|
|
1171
|
+
action: str,
|
|
1172
|
+
sha: str,
|
|
1173
|
+
note: str,
|
|
1174
|
+
expected_ref_sha: Optional[str],
|
|
1175
|
+
author: Optional[CommitSignature],
|
|
1176
|
+
ttl: Optional[int],
|
|
1177
|
+
) -> NoteWriteResult:
|
|
1178
|
+
sha_clean = sha.strip()
|
|
1179
|
+
if not sha_clean:
|
|
1180
|
+
raise ValueError(f"{action_label} sha is required")
|
|
1181
|
+
|
|
1182
|
+
note_clean = note.strip()
|
|
1183
|
+
if not note_clean:
|
|
1184
|
+
raise ValueError(f"{action_label} note is required")
|
|
1185
|
+
|
|
1186
|
+
ttl = ttl or DEFAULT_TOKEN_TTL_SECONDS
|
|
1187
|
+
jwt = self.generate_jwt(self._id, {"permissions": ["git:write"], "ttl": ttl})
|
|
1188
|
+
|
|
1189
|
+
payload: Dict[str, Any] = {
|
|
1190
|
+
"sha": sha_clean,
|
|
1191
|
+
"action": action,
|
|
1192
|
+
"note": note_clean,
|
|
1193
|
+
}
|
|
1194
|
+
if expected_ref_sha and expected_ref_sha.strip():
|
|
1195
|
+
payload["expected_ref_sha"] = expected_ref_sha.strip()
|
|
1196
|
+
if author:
|
|
1197
|
+
author_name = author.get("name", "").strip()
|
|
1198
|
+
author_email = author.get("email", "").strip()
|
|
1199
|
+
if not author_name or not author_email:
|
|
1200
|
+
raise ValueError(f"{action_label} author name and email are required when provided")
|
|
1201
|
+
payload["author"] = {"name": author_name, "email": author_email}
|
|
1202
|
+
|
|
1203
|
+
url = f"{self.api_base_url}/api/v{self.api_version}/repos/notes"
|
|
1204
|
+
|
|
1205
|
+
async with httpx.AsyncClient() as client:
|
|
1206
|
+
response = await client.post(
|
|
1207
|
+
url,
|
|
1208
|
+
headers={
|
|
1209
|
+
"Authorization": f"Bearer {jwt}",
|
|
1210
|
+
"Content-Type": "application/json",
|
|
1211
|
+
"Code-Storage-Agent": get_user_agent(),
|
|
1212
|
+
},
|
|
1213
|
+
json=payload,
|
|
1214
|
+
timeout=30.0,
|
|
1215
|
+
)
|
|
1216
|
+
|
|
1217
|
+
return self._parse_note_write_response(response, action_label)
|
|
1218
|
+
|
|
1219
|
+
def _parse_note_write_response(
|
|
1220
|
+
self,
|
|
1221
|
+
response: httpx.Response,
|
|
1222
|
+
action_label: str,
|
|
1223
|
+
) -> NoteWriteResult:
|
|
1224
|
+
try:
|
|
1225
|
+
payload = response.json()
|
|
1226
|
+
except Exception as exc:
|
|
1227
|
+
message = f"{action_label} failed with HTTP {response.status_code}"
|
|
1228
|
+
if response.reason_phrase:
|
|
1229
|
+
message += f" {response.reason_phrase}"
|
|
1230
|
+
try:
|
|
1231
|
+
body_text = response.text
|
|
1232
|
+
except Exception:
|
|
1233
|
+
body_text = ""
|
|
1234
|
+
if body_text:
|
|
1235
|
+
message += f": {body_text[:200]}"
|
|
1236
|
+
raise ApiError(message, status_code=response.status_code, response=response) from exc
|
|
1237
|
+
|
|
1238
|
+
if isinstance(payload, dict) and "error" in payload:
|
|
1239
|
+
raise ApiError(
|
|
1240
|
+
str(payload.get("error")),
|
|
1241
|
+
status_code=response.status_code,
|
|
1242
|
+
response=response,
|
|
1243
|
+
)
|
|
1244
|
+
|
|
1245
|
+
if not isinstance(payload, dict) or "result" not in payload:
|
|
1246
|
+
message = f"{action_label} failed with HTTP {response.status_code}"
|
|
1247
|
+
if response.reason_phrase:
|
|
1248
|
+
message += f" {response.reason_phrase}"
|
|
1249
|
+
raise ApiError(message, status_code=response.status_code, response=response)
|
|
1250
|
+
|
|
1251
|
+
result = payload.get("result", {})
|
|
1252
|
+
note_result: NoteWriteResult = {
|
|
1253
|
+
"sha": payload.get("sha", ""),
|
|
1254
|
+
"target_ref": payload.get("target_ref", ""),
|
|
1255
|
+
"new_ref_sha": payload.get("new_ref_sha", ""),
|
|
1256
|
+
"result": {
|
|
1257
|
+
"success": bool(result.get("success")),
|
|
1258
|
+
"status": str(result.get("status", "")),
|
|
1259
|
+
},
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
base_commit = payload.get("base_commit")
|
|
1263
|
+
if isinstance(base_commit, str) and base_commit:
|
|
1264
|
+
note_result["base_commit"] = base_commit
|
|
1265
|
+
if result.get("message"):
|
|
1266
|
+
note_result["result"]["message"] = result.get("message")
|
|
1267
|
+
|
|
1268
|
+
if not result.get("success"):
|
|
1269
|
+
raise RefUpdateError(
|
|
1270
|
+
result.get(
|
|
1271
|
+
"message",
|
|
1272
|
+
f"{action_label} failed with status {result.get('status')}",
|
|
1273
|
+
),
|
|
1274
|
+
status=result.get("status"),
|
|
1275
|
+
ref_update={
|
|
1276
|
+
"branch": payload.get("target_ref", ""),
|
|
1277
|
+
"old_sha": payload.get("base_commit", ""),
|
|
1278
|
+
"new_sha": payload.get("new_ref_sha", ""),
|
|
1279
|
+
},
|
|
1280
|
+
)
|
|
1281
|
+
|
|
1282
|
+
return note_result
|
|
1283
|
+
|
|
1049
1284
|
def _to_ref_update(self, result: Dict[str, Any]) -> RefUpdate:
|
|
1050
1285
|
"""Convert result to ref update."""
|
|
1051
1286
|
return {
|
pierre_storage/types.py
CHANGED
|
@@ -41,7 +41,7 @@ class GitStorageOptions(TypedDict, total=False):
|
|
|
41
41
|
default_ttl: Optional[int]
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
class
|
|
44
|
+
class GitHubBaseRepo(TypedDict, total=False):
|
|
45
45
|
"""Base repository configuration for GitHub sync."""
|
|
46
46
|
|
|
47
47
|
provider: Literal["github"] # required
|
|
@@ -50,6 +50,17 @@ class BaseRepo(TypedDict, total=False):
|
|
|
50
50
|
default_branch: Optional[str]
|
|
51
51
|
|
|
52
52
|
|
|
53
|
+
class ForkBaseRepo(TypedDict, total=False):
|
|
54
|
+
"""Base repository configuration for code storage forks."""
|
|
55
|
+
|
|
56
|
+
id: str # required
|
|
57
|
+
ref: Optional[str]
|
|
58
|
+
sha: Optional[str]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
BaseRepo = Union[GitHubBaseRepo, ForkBaseRepo]
|
|
62
|
+
|
|
63
|
+
|
|
53
64
|
class DeleteRepoResult(TypedDict):
|
|
54
65
|
"""Result from deleting a repository."""
|
|
55
66
|
|
|
@@ -57,6 +68,33 @@ class DeleteRepoResult(TypedDict):
|
|
|
57
68
|
message: str
|
|
58
69
|
|
|
59
70
|
|
|
71
|
+
# Repository list types
|
|
72
|
+
class RepoBaseInfo(TypedDict):
|
|
73
|
+
"""Base repository info for listed repositories."""
|
|
74
|
+
|
|
75
|
+
provider: str
|
|
76
|
+
owner: str
|
|
77
|
+
name: str
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class RepoInfo(TypedDict, total=False):
|
|
81
|
+
"""Repository info in list responses."""
|
|
82
|
+
|
|
83
|
+
repo_id: str
|
|
84
|
+
url: str
|
|
85
|
+
default_branch: str
|
|
86
|
+
created_at: str
|
|
87
|
+
base_repo: NotRequired[RepoBaseInfo]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class ListReposResult(TypedDict):
|
|
91
|
+
"""Result from listing repositories."""
|
|
92
|
+
|
|
93
|
+
repos: List[RepoInfo]
|
|
94
|
+
next_cursor: Optional[str]
|
|
95
|
+
has_more: bool
|
|
96
|
+
|
|
97
|
+
|
|
60
98
|
# Removed: GetRemoteURLOptions - now uses **kwargs
|
|
61
99
|
# Removed: CreateRepoOptions - now uses **kwargs
|
|
62
100
|
# Removed: FindOneOptions - now uses **kwargs
|
|
@@ -127,6 +165,33 @@ class ListCommitsResult(TypedDict):
|
|
|
127
165
|
has_more: bool
|
|
128
166
|
|
|
129
167
|
|
|
168
|
+
# Git notes types
|
|
169
|
+
class NoteReadResult(TypedDict):
|
|
170
|
+
"""Result from reading a git note."""
|
|
171
|
+
|
|
172
|
+
sha: str
|
|
173
|
+
note: str
|
|
174
|
+
ref_sha: str
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class NoteWriteResultPayload(TypedDict):
|
|
178
|
+
"""Result payload for note writes."""
|
|
179
|
+
|
|
180
|
+
success: bool
|
|
181
|
+
status: str
|
|
182
|
+
message: NotRequired[str]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class NoteWriteResult(TypedDict):
|
|
186
|
+
"""Result from writing a git note."""
|
|
187
|
+
|
|
188
|
+
sha: str
|
|
189
|
+
target_ref: str
|
|
190
|
+
base_commit: NotRequired[str]
|
|
191
|
+
new_ref_sha: str
|
|
192
|
+
result: NoteWriteResultPayload
|
|
193
|
+
|
|
194
|
+
|
|
130
195
|
# Diff types
|
|
131
196
|
class DiffStats(TypedDict):
|
|
132
197
|
"""Statistics about a diff."""
|
|
@@ -417,6 +482,50 @@ class Repo(Protocol):
|
|
|
417
482
|
"""List commits in the repository."""
|
|
418
483
|
...
|
|
419
484
|
|
|
485
|
+
async def get_note(
|
|
486
|
+
self,
|
|
487
|
+
*,
|
|
488
|
+
sha: str,
|
|
489
|
+
ttl: Optional[int] = None,
|
|
490
|
+
) -> NoteReadResult:
|
|
491
|
+
"""Read a git note."""
|
|
492
|
+
...
|
|
493
|
+
|
|
494
|
+
async def create_note(
|
|
495
|
+
self,
|
|
496
|
+
*,
|
|
497
|
+
sha: str,
|
|
498
|
+
note: str,
|
|
499
|
+
expected_ref_sha: Optional[str] = None,
|
|
500
|
+
author: Optional["CommitSignature"] = None,
|
|
501
|
+
ttl: Optional[int] = None,
|
|
502
|
+
) -> NoteWriteResult:
|
|
503
|
+
"""Create a git note."""
|
|
504
|
+
...
|
|
505
|
+
|
|
506
|
+
async def append_note(
|
|
507
|
+
self,
|
|
508
|
+
*,
|
|
509
|
+
sha: str,
|
|
510
|
+
note: str,
|
|
511
|
+
expected_ref_sha: Optional[str] = None,
|
|
512
|
+
author: Optional["CommitSignature"] = None,
|
|
513
|
+
ttl: Optional[int] = None,
|
|
514
|
+
) -> NoteWriteResult:
|
|
515
|
+
"""Append to a git note."""
|
|
516
|
+
...
|
|
517
|
+
|
|
518
|
+
async def delete_note(
|
|
519
|
+
self,
|
|
520
|
+
*,
|
|
521
|
+
sha: str,
|
|
522
|
+
expected_ref_sha: Optional[str] = None,
|
|
523
|
+
author: Optional["CommitSignature"] = None,
|
|
524
|
+
ttl: Optional[int] = None,
|
|
525
|
+
) -> NoteWriteResult:
|
|
526
|
+
"""Delete a git note."""
|
|
527
|
+
...
|
|
528
|
+
|
|
420
529
|
async def get_branch_diff(
|
|
421
530
|
self,
|
|
422
531
|
*,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pierre-storage
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.12.0
|
|
4
4
|
Summary: Pierre Git Storage SDK for Python
|
|
5
5
|
Author-email: Pierre <support@pierre.io>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -83,6 +83,15 @@ github_repo = await storage.create_repo(
|
|
|
83
83
|
}
|
|
84
84
|
)
|
|
85
85
|
# This repository will sync with github.com/octocat/Hello-World
|
|
86
|
+
|
|
87
|
+
# Create a repository by forking an existing repo
|
|
88
|
+
forked_repo = await storage.create_repo(
|
|
89
|
+
id="my-fork",
|
|
90
|
+
base_repo={
|
|
91
|
+
"id": "my-template-id",
|
|
92
|
+
"ref": "main", # optional
|
|
93
|
+
},
|
|
94
|
+
)
|
|
86
95
|
```
|
|
87
96
|
|
|
88
97
|
### Finding a Repository
|
|
@@ -149,6 +158,10 @@ repo = await storage.create_repo()
|
|
|
149
158
|
# or
|
|
150
159
|
repo = await storage.find_one(id="existing-repo-id")
|
|
151
160
|
|
|
161
|
+
# List repositories for the org
|
|
162
|
+
repos = await storage.list_repos(limit=20)
|
|
163
|
+
print(repos["repos"])
|
|
164
|
+
|
|
152
165
|
# Get file content (streaming)
|
|
153
166
|
response = await repo.get_file_stream(
|
|
154
167
|
path="README.md",
|
|
@@ -190,6 +203,27 @@ commits = await repo.list_commits(
|
|
|
190
203
|
)
|
|
191
204
|
print(commits["commits"])
|
|
192
205
|
|
|
206
|
+
# Read a git note for a commit
|
|
207
|
+
note = await repo.get_note(sha="abc123...")
|
|
208
|
+
print(note["note"])
|
|
209
|
+
|
|
210
|
+
# Add a git note
|
|
211
|
+
note_result = await repo.create_note(
|
|
212
|
+
sha="abc123...",
|
|
213
|
+
note="Release QA approved",
|
|
214
|
+
author={"name": "Release Bot", "email": "release@example.com"},
|
|
215
|
+
)
|
|
216
|
+
print(note_result["new_ref_sha"])
|
|
217
|
+
|
|
218
|
+
# Append to a git note
|
|
219
|
+
await repo.append_note(
|
|
220
|
+
sha="abc123...",
|
|
221
|
+
note="Follow-up review complete",
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# Delete a git note
|
|
225
|
+
await repo.delete_note(sha="abc123...")
|
|
226
|
+
|
|
193
227
|
# Get branch diff
|
|
194
228
|
branch_diff = await repo.get_branch_diff(
|
|
195
229
|
branch="feature-branch",
|
|
@@ -447,6 +481,23 @@ commits = await repo.list_commits()
|
|
|
447
481
|
3. You can then use all Pierre SDK features (diffs, commits, file access) on the synced content
|
|
448
482
|
4. The provider is automatically set to `"github"` when using `base_repo`
|
|
449
483
|
|
|
484
|
+
### Forking Repositories
|
|
485
|
+
|
|
486
|
+
You can fork an existing repository within the same Pierre org:
|
|
487
|
+
|
|
488
|
+
```python
|
|
489
|
+
forked_repo = await storage.create_repo(
|
|
490
|
+
id="my-fork",
|
|
491
|
+
base_repo={
|
|
492
|
+
"id": "my-template-id",
|
|
493
|
+
"ref": "main", # optional (branch/tag)
|
|
494
|
+
# "sha": "abc123..." # optional commit SHA (overrides ref)
|
|
495
|
+
},
|
|
496
|
+
)
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
When `default_branch` is omitted, the SDK returns `"main"`.
|
|
500
|
+
|
|
450
501
|
### Restoring Commits
|
|
451
502
|
|
|
452
503
|
You can restore a repository to a previous commit:
|
|
@@ -475,7 +526,7 @@ class GitStorage:
|
|
|
475
526
|
self,
|
|
476
527
|
*,
|
|
477
528
|
id: Optional[str] = None,
|
|
478
|
-
default_branch: str = "main"
|
|
529
|
+
default_branch: Optional[str] = None, # defaults to "main"
|
|
479
530
|
base_repo: Optional[BaseRepo] = None,
|
|
480
531
|
ttl: Optional[int] = None,
|
|
481
532
|
) -> Repo: ...
|
|
@@ -614,6 +665,8 @@ Key types are provided via TypedDict for better IDE support:
|
|
|
614
665
|
from pierre_storage.types import (
|
|
615
666
|
GitStorageOptions,
|
|
616
667
|
BaseRepo,
|
|
668
|
+
GitHubBaseRepo,
|
|
669
|
+
ForkBaseRepo,
|
|
617
670
|
CommitSignature,
|
|
618
671
|
CreateCommitOptions,
|
|
619
672
|
ListFilesResult,
|
|
@@ -627,12 +680,19 @@ from pierre_storage.types import (
|
|
|
627
680
|
# ... and more
|
|
628
681
|
)
|
|
629
682
|
|
|
630
|
-
# BaseRepo type for GitHub sync
|
|
631
|
-
class
|
|
632
|
-
provider: Literal["github"]
|
|
683
|
+
# BaseRepo type for GitHub sync or forks
|
|
684
|
+
class GitHubBaseRepo(TypedDict, total=False):
|
|
685
|
+
provider: Literal["github"] # Always "github"
|
|
633
686
|
owner: str # GitHub organization or user
|
|
634
687
|
name: str # Repository name
|
|
635
688
|
default_branch: Optional[str] # Default branch (optional)
|
|
689
|
+
|
|
690
|
+
class ForkBaseRepo(TypedDict, total=False):
|
|
691
|
+
id: str # Source repo ID
|
|
692
|
+
ref: Optional[str] # Optional ref name
|
|
693
|
+
sha: Optional[str] # Optional commit SHA
|
|
694
|
+
|
|
695
|
+
BaseRepo = Union[GitHubBaseRepo, ForkBaseRepo]
|
|
636
696
|
```
|
|
637
697
|
|
|
638
698
|
## Webhook Validation
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
pierre_storage/__init__.py,sha256=8t6r3VUY4MSZhSuBXaoxzuxd4FLRAbVLWFxXiC_fcQY,1905
|
|
2
|
+
pierre_storage/auth.py,sha256=aT0KNaUKa9fXERtQvJb8C6zL5IxrfA1KLxMBcprAq5A,2074
|
|
3
|
+
pierre_storage/client.py,sha256=T6k4lpeOXcK6yMDRTknYMcVq_TKVkG59dXuRaykZPws,14857
|
|
4
|
+
pierre_storage/commit.py,sha256=ks5hKScHHricJ3sx8DyLSAASM72CPmVv-tbtUgHbUF4,16766
|
|
5
|
+
pierre_storage/errors.py,sha256=-vuA2BUGwyDlErFtdh2boLdk0fDFDFYBEIohJk4AsIs,2184
|
|
6
|
+
pierre_storage/py.typed,sha256=8PjyZ1aVoQpRVvt71muvuq5qE-jTFZkK-GLHkhdebmc,26
|
|
7
|
+
pierre_storage/repo.py,sha256=wnTDVtHx9v8tJTHJ9cHJs8cn5dImsCA3M3xxx7RqhS8,44117
|
|
8
|
+
pierre_storage/types.py,sha256=9THfT19hM_qEkozdNH5qhiL34_USubSGIRFHUw8HY9E,14661
|
|
9
|
+
pierre_storage/version.py,sha256=HFSPY5BelU4QBXsW9MXZlFAE6Sa70XR3e_RzOQe1RO4,315
|
|
10
|
+
pierre_storage/webhook.py,sha256=hyjSmTlU_x35m612erXDqNXbLUh5i5As5GRw7kxylFc,7425
|
|
11
|
+
pierre_storage-0.12.0.dist-info/licenses/LICENSE,sha256=CFzxoMyurfMUB0u0RaXBFZ6IDeUd6FQhKrLR_IeXtuU,1063
|
|
12
|
+
pierre_storage-0.12.0.dist-info/METADATA,sha256=LjSrQouhMF3yTdgMog5i949UqRN55UsCRKBTQ18nom4,23306
|
|
13
|
+
pierre_storage-0.12.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
14
|
+
pierre_storage-0.12.0.dist-info/top_level.txt,sha256=RzcYFaSdETlcwX-45G9Q39xUgXWZLJEWcOiK0p6ZepY,15
|
|
15
|
+
pierre_storage-0.12.0.dist-info/RECORD,,
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
pierre_storage/__init__.py,sha256=uHHdwkzdHldq7OW6WpE6C2S-ooqvMw35oSxvYMb5oGc,1745
|
|
2
|
-
pierre_storage/auth.py,sha256=aT0KNaUKa9fXERtQvJb8C6zL5IxrfA1KLxMBcprAq5A,2074
|
|
3
|
-
pierre_storage/client.py,sha256=qJlQrxvt5YhVYp1RJAEt7pmmYLf6LwleTDb5lvs30kE,11080
|
|
4
|
-
pierre_storage/commit.py,sha256=ks5hKScHHricJ3sx8DyLSAASM72CPmVv-tbtUgHbUF4,16766
|
|
5
|
-
pierre_storage/errors.py,sha256=-vuA2BUGwyDlErFtdh2boLdk0fDFDFYBEIohJk4AsIs,2184
|
|
6
|
-
pierre_storage/py.typed,sha256=8PjyZ1aVoQpRVvt71muvuq5qE-jTFZkK-GLHkhdebmc,26
|
|
7
|
-
pierre_storage/repo.py,sha256=EK82B9yBkB-69t9-lQPCE3Mj5BQ729IQYqg3C1UV4Yg,35982
|
|
8
|
-
pierre_storage/types.py,sha256=u7iispDYc4C2bs-NweJc-sYVjwNlHNgqZ9ywltL11U4,12369
|
|
9
|
-
pierre_storage/version.py,sha256=HFSPY5BelU4QBXsW9MXZlFAE6Sa70XR3e_RzOQe1RO4,315
|
|
10
|
-
pierre_storage/webhook.py,sha256=hyjSmTlU_x35m612erXDqNXbLUh5i5As5GRw7kxylFc,7425
|
|
11
|
-
pierre_storage-0.10.0.dist-info/licenses/LICENSE,sha256=CFzxoMyurfMUB0u0RaXBFZ6IDeUd6FQhKrLR_IeXtuU,1063
|
|
12
|
-
pierre_storage-0.10.0.dist-info/METADATA,sha256=Erw1tJIeuOgE1eHNdeu_V_p4xr5L7gW6P5aSQdgpCKc,21790
|
|
13
|
-
pierre_storage-0.10.0.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
14
|
-
pierre_storage-0.10.0.dist-info/top_level.txt,sha256=RzcYFaSdETlcwX-45G9Q39xUgXWZLJEWcOiK0p6ZepY,15
|
|
15
|
-
pierre_storage-0.10.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|