pierre-storage 0.10.0__py3-none-any.whl → 0.11.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.
@@ -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
@@ -2,6 +2,7 @@
2
2
 
3
3
  import uuid
4
4
  from typing import Any, Dict, Optional
5
+ from urllib.parse import urlencode
5
6
 
6
7
  import httpx
7
8
 
@@ -11,7 +12,9 @@ from pierre_storage.repo import DEFAULT_TOKEN_TTL_SECONDS, RepoImpl
11
12
  from pierre_storage.types import (
12
13
  DeleteRepoResult,
13
14
  GitStorageOptions,
15
+ ListReposResult,
14
16
  Repo,
17
+ RepoInfo,
15
18
  )
16
19
  from pierre_storage.version import get_user_agent
17
20
 
@@ -178,6 +181,66 @@ class GitStorage:
178
181
  self._generate_jwt,
179
182
  )
180
183
 
184
+ async def list_repos(
185
+ self,
186
+ *,
187
+ cursor: Optional[str] = None,
188
+ limit: Optional[int] = None,
189
+ ttl: Optional[int] = None,
190
+ ) -> ListReposResult:
191
+ """List repositories for the organization."""
192
+ ttl = ttl or DEFAULT_TOKEN_TTL_SECONDS
193
+ jwt = self._generate_jwt(
194
+ "org",
195
+ {"permissions": ["org:read"], "ttl": ttl},
196
+ )
197
+
198
+ params: Dict[str, str] = {}
199
+ if cursor:
200
+ params["cursor"] = cursor
201
+ if limit is not None:
202
+ params["limit"] = str(limit)
203
+
204
+ url = f"{self.options['api_base_url']}/api/v{self.options['api_version']}/repos"
205
+ if params:
206
+ url += f"?{urlencode(params)}"
207
+
208
+ async with httpx.AsyncClient() as client:
209
+ response = await client.get(
210
+ url,
211
+ headers={
212
+ "Authorization": f"Bearer {jwt}",
213
+ "Code-Storage-Agent": get_user_agent(),
214
+ },
215
+ timeout=30.0,
216
+ )
217
+
218
+ if not response.is_success:
219
+ raise ApiError(
220
+ f"Failed to list repositories: {response.status_code} {response.reason_phrase}",
221
+ status_code=response.status_code,
222
+ response=response,
223
+ )
224
+
225
+ data = response.json()
226
+ repos: list[RepoInfo] = []
227
+ for repo in data.get("repos", []):
228
+ entry: RepoInfo = {
229
+ "repo_id": repo.get("repo_id", ""),
230
+ "url": repo.get("url", ""),
231
+ "default_branch": repo.get("default_branch", "main"),
232
+ "created_at": repo.get("created_at", ""),
233
+ }
234
+ if repo.get("base_repo"):
235
+ entry["base_repo"] = repo.get("base_repo")
236
+ repos.append(entry)
237
+
238
+ return {
239
+ "repos": repos,
240
+ "next_cursor": data.get("next_cursor"),
241
+ "has_more": data.get("has_more", False),
242
+ }
243
+
181
244
  async def find_one(self, *, id: str) -> Optional[Repo]:
182
245
  """Find a repository by ID.
183
246
 
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
@@ -57,6 +57,33 @@ class DeleteRepoResult(TypedDict):
57
57
  message: str
58
58
 
59
59
 
60
+ # Repository list types
61
+ class RepoBaseInfo(TypedDict):
62
+ """Base repository info for listed repositories."""
63
+
64
+ provider: str
65
+ owner: str
66
+ name: str
67
+
68
+
69
+ class RepoInfo(TypedDict, total=False):
70
+ """Repository info in list responses."""
71
+
72
+ repo_id: str
73
+ url: str
74
+ default_branch: str
75
+ created_at: str
76
+ base_repo: NotRequired[RepoBaseInfo]
77
+
78
+
79
+ class ListReposResult(TypedDict):
80
+ """Result from listing repositories."""
81
+
82
+ repos: List[RepoInfo]
83
+ next_cursor: Optional[str]
84
+ has_more: bool
85
+
86
+
60
87
  # Removed: GetRemoteURLOptions - now uses **kwargs
61
88
  # Removed: CreateRepoOptions - now uses **kwargs
62
89
  # Removed: FindOneOptions - now uses **kwargs
@@ -127,6 +154,33 @@ class ListCommitsResult(TypedDict):
127
154
  has_more: bool
128
155
 
129
156
 
157
+ # Git notes types
158
+ class NoteReadResult(TypedDict):
159
+ """Result from reading a git note."""
160
+
161
+ sha: str
162
+ note: str
163
+ ref_sha: str
164
+
165
+
166
+ class NoteWriteResultPayload(TypedDict):
167
+ """Result payload for note writes."""
168
+
169
+ success: bool
170
+ status: str
171
+ message: NotRequired[str]
172
+
173
+
174
+ class NoteWriteResult(TypedDict):
175
+ """Result from writing a git note."""
176
+
177
+ sha: str
178
+ target_ref: str
179
+ base_commit: NotRequired[str]
180
+ new_ref_sha: str
181
+ result: NoteWriteResultPayload
182
+
183
+
130
184
  # Diff types
131
185
  class DiffStats(TypedDict):
132
186
  """Statistics about a diff."""
@@ -417,6 +471,50 @@ class Repo(Protocol):
417
471
  """List commits in the repository."""
418
472
  ...
419
473
 
474
+ async def get_note(
475
+ self,
476
+ *,
477
+ sha: str,
478
+ ttl: Optional[int] = None,
479
+ ) -> NoteReadResult:
480
+ """Read a git note."""
481
+ ...
482
+
483
+ async def create_note(
484
+ self,
485
+ *,
486
+ sha: str,
487
+ note: str,
488
+ expected_ref_sha: Optional[str] = None,
489
+ author: Optional["CommitSignature"] = None,
490
+ ttl: Optional[int] = None,
491
+ ) -> NoteWriteResult:
492
+ """Create a git note."""
493
+ ...
494
+
495
+ async def append_note(
496
+ self,
497
+ *,
498
+ sha: str,
499
+ note: str,
500
+ expected_ref_sha: Optional[str] = None,
501
+ author: Optional["CommitSignature"] = None,
502
+ ttl: Optional[int] = None,
503
+ ) -> NoteWriteResult:
504
+ """Append to a git note."""
505
+ ...
506
+
507
+ async def delete_note(
508
+ self,
509
+ *,
510
+ sha: str,
511
+ expected_ref_sha: Optional[str] = None,
512
+ author: Optional["CommitSignature"] = None,
513
+ ttl: Optional[int] = None,
514
+ ) -> NoteWriteResult:
515
+ """Delete a git note."""
516
+ ...
517
+
420
518
  async def get_branch_diff(
421
519
  self,
422
520
  *,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pierre-storage
3
- Version: 0.10.0
3
+ Version: 0.11.0
4
4
  Summary: Pierre Git Storage SDK for Python
5
5
  Author-email: Pierre <support@pierre.io>
6
6
  License-Expression: MIT
@@ -149,6 +149,10 @@ repo = await storage.create_repo()
149
149
  # or
150
150
  repo = await storage.find_one(id="existing-repo-id")
151
151
 
152
+ # List repositories for the org
153
+ repos = await storage.list_repos(limit=20)
154
+ print(repos["repos"])
155
+
152
156
  # Get file content (streaming)
153
157
  response = await repo.get_file_stream(
154
158
  path="README.md",
@@ -190,6 +194,27 @@ commits = await repo.list_commits(
190
194
  )
191
195
  print(commits["commits"])
192
196
 
197
+ # Read a git note for a commit
198
+ note = await repo.get_note(sha="abc123...")
199
+ print(note["note"])
200
+
201
+ # Add a git note
202
+ note_result = await repo.create_note(
203
+ sha="abc123...",
204
+ note="Release QA approved",
205
+ author={"name": "Release Bot", "email": "release@example.com"},
206
+ )
207
+ print(note_result["new_ref_sha"])
208
+
209
+ # Append to a git note
210
+ await repo.append_note(
211
+ sha="abc123...",
212
+ note="Follow-up review complete",
213
+ )
214
+
215
+ # Delete a git note
216
+ await repo.delete_note(sha="abc123...")
217
+
193
218
  # Get branch diff
194
219
  branch_diff = await repo.get_branch_diff(
195
220
  branch="feature-branch",
@@ -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=LPU0iZoduQvE7CDAMEKcYnMDn6WDH8vZtJw4BKO8lAg,13213
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=MNTp4LYIActTUaxII-Qa0xjsKszVSPJKlLOvaw6nIOY,14425
9
+ pierre_storage/version.py,sha256=HFSPY5BelU4QBXsW9MXZlFAE6Sa70XR3e_RzOQe1RO4,315
10
+ pierre_storage/webhook.py,sha256=hyjSmTlU_x35m612erXDqNXbLUh5i5As5GRw7kxylFc,7425
11
+ pierre_storage-0.11.0.dist-info/licenses/LICENSE,sha256=CFzxoMyurfMUB0u0RaXBFZ6IDeUd6FQhKrLR_IeXtuU,1063
12
+ pierre_storage-0.11.0.dist-info/METADATA,sha256=0TaxHV-8GRD2iX1u6JpUrZHGe7P0wKdmp1sg0F8DyVg,22367
13
+ pierre_storage-0.11.0.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
14
+ pierre_storage-0.11.0.dist-info/top_level.txt,sha256=RzcYFaSdETlcwX-45G9Q39xUgXWZLJEWcOiK0p6ZepY,15
15
+ pierre_storage-0.11.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,,