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.
@@ -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 = "main",
103
- base_repo: Optional[Dict[str, Any]] = None,
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
- (provider, owner, name, default_branch)
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] = {"default_branch": default_branch}
136
+ body: Dict[str, Any] = {}
130
137
 
131
138
  # Match backend priority: base_repo.default_branch > default_branch > 'main'
132
- resolved_default_branch = default_branch
139
+ explicit_default_branch = default_branch is not None
140
+ resolved_default_branch: Optional[str] = None
141
+
133
142
  if base_repo:
134
- # Ensure provider is set to 'github' if not provided
135
- base_repo_with_provider = {
136
- "provider": "github",
137
- **base_repo,
138
- }
139
- body["base_repo"] = base_repo_with_provider
140
- if base_repo.get("default_branch"):
141
- resolved_default_branch = base_repo["default_branch"]
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 BaseRepo(TypedDict, total=False):
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.10.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 BaseRepo(TypedDict, total=False):
632
- provider: Literal["github"] # Always "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,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.10.1)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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,,