pierre-storage 0.11.0__tar.gz → 0.12.1__tar.gz

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.
Files changed (26) hide show
  1. {pierre_storage-0.11.0/pierre_storage.egg-info → pierre_storage-0.12.1}/PKG-INFO +40 -5
  2. {pierre_storage-0.11.0 → pierre_storage-0.12.1}/README.md +39 -4
  3. {pierre_storage-0.11.0 → pierre_storage-0.12.1}/pierre_storage/client.py +54 -17
  4. {pierre_storage-0.11.0 → pierre_storage-0.12.1}/pierre_storage/types.py +12 -1
  5. {pierre_storage-0.11.0 → pierre_storage-0.12.1}/pierre_storage/version.py +1 -1
  6. {pierre_storage-0.11.0 → pierre_storage-0.12.1/pierre_storage.egg-info}/PKG-INFO +40 -5
  7. {pierre_storage-0.11.0 → pierre_storage-0.12.1}/pyproject.toml +1 -1
  8. {pierre_storage-0.11.0 → pierre_storage-0.12.1}/tests/test_client.py +37 -0
  9. {pierre_storage-0.11.0 → pierre_storage-0.12.1}/LICENSE +0 -0
  10. {pierre_storage-0.11.0 → pierre_storage-0.12.1}/MANIFEST.in +0 -0
  11. {pierre_storage-0.11.0 → pierre_storage-0.12.1}/pierre_storage/__init__.py +0 -0
  12. {pierre_storage-0.11.0 → pierre_storage-0.12.1}/pierre_storage/auth.py +0 -0
  13. {pierre_storage-0.11.0 → pierre_storage-0.12.1}/pierre_storage/commit.py +0 -0
  14. {pierre_storage-0.11.0 → pierre_storage-0.12.1}/pierre_storage/errors.py +0 -0
  15. {pierre_storage-0.11.0 → pierre_storage-0.12.1}/pierre_storage/py.typed +0 -0
  16. {pierre_storage-0.11.0 → pierre_storage-0.12.1}/pierre_storage/repo.py +0 -0
  17. {pierre_storage-0.11.0 → pierre_storage-0.12.1}/pierre_storage/webhook.py +0 -0
  18. {pierre_storage-0.11.0 → pierre_storage-0.12.1}/pierre_storage.egg-info/SOURCES.txt +0 -0
  19. {pierre_storage-0.11.0 → pierre_storage-0.12.1}/pierre_storage.egg-info/dependency_links.txt +0 -0
  20. {pierre_storage-0.11.0 → pierre_storage-0.12.1}/pierre_storage.egg-info/requires.txt +0 -0
  21. {pierre_storage-0.11.0 → pierre_storage-0.12.1}/pierre_storage.egg-info/top_level.txt +0 -0
  22. {pierre_storage-0.11.0 → pierre_storage-0.12.1}/setup.cfg +0 -0
  23. {pierre_storage-0.11.0 → pierre_storage-0.12.1}/tests/test_commit.py +0 -0
  24. {pierre_storage-0.11.0 → pierre_storage-0.12.1}/tests/test_repo.py +0 -0
  25. {pierre_storage-0.11.0 → pierre_storage-0.12.1}/tests/test_version.py +0 -0
  26. {pierre_storage-0.11.0 → pierre_storage-0.12.1}/tests/test_webhook.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pierre-storage
3
- Version: 0.11.0
3
+ Version: 0.12.1
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
@@ -472,6 +481,23 @@ commits = await repo.list_commits()
472
481
  3. You can then use all Pierre SDK features (diffs, commits, file access) on the synced content
473
482
  4. The provider is automatically set to `"github"` when using `base_repo`
474
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
+
475
501
  ### Restoring Commits
476
502
 
477
503
  You can restore a repository to a previous commit:
@@ -500,7 +526,7 @@ class GitStorage:
500
526
  self,
501
527
  *,
502
528
  id: Optional[str] = None,
503
- default_branch: str = "main",
529
+ default_branch: Optional[str] = None, # defaults to "main"
504
530
  base_repo: Optional[BaseRepo] = None,
505
531
  ttl: Optional[int] = None,
506
532
  ) -> Repo: ...
@@ -639,6 +665,8 @@ Key types are provided via TypedDict for better IDE support:
639
665
  from pierre_storage.types import (
640
666
  GitStorageOptions,
641
667
  BaseRepo,
668
+ GitHubBaseRepo,
669
+ ForkBaseRepo,
642
670
  CommitSignature,
643
671
  CreateCommitOptions,
644
672
  ListFilesResult,
@@ -652,12 +680,19 @@ from pierre_storage.types import (
652
680
  # ... and more
653
681
  )
654
682
 
655
- # BaseRepo type for GitHub sync
656
- class BaseRepo(TypedDict, total=False):
657
- 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"
658
686
  owner: str # GitHub organization or user
659
687
  name: str # Repository name
660
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]
661
696
  ```
662
697
 
663
698
  ## Webhook Validation
@@ -47,6 +47,15 @@ github_repo = await storage.create_repo(
47
47
  }
48
48
  )
49
49
  # This repository will sync with github.com/octocat/Hello-World
50
+
51
+ # Create a repository by forking an existing repo
52
+ forked_repo = await storage.create_repo(
53
+ id="my-fork",
54
+ base_repo={
55
+ "id": "my-template-id",
56
+ "ref": "main", # optional
57
+ },
58
+ )
50
59
  ```
51
60
 
52
61
  ### Finding a Repository
@@ -436,6 +445,23 @@ commits = await repo.list_commits()
436
445
  3. You can then use all Pierre SDK features (diffs, commits, file access) on the synced content
437
446
  4. The provider is automatically set to `"github"` when using `base_repo`
438
447
 
448
+ ### Forking Repositories
449
+
450
+ You can fork an existing repository within the same Pierre org:
451
+
452
+ ```python
453
+ forked_repo = await storage.create_repo(
454
+ id="my-fork",
455
+ base_repo={
456
+ "id": "my-template-id",
457
+ "ref": "main", # optional (branch/tag)
458
+ # "sha": "abc123..." # optional commit SHA (overrides ref)
459
+ },
460
+ )
461
+ ```
462
+
463
+ When `default_branch` is omitted, the SDK returns `"main"`.
464
+
439
465
  ### Restoring Commits
440
466
 
441
467
  You can restore a repository to a previous commit:
@@ -464,7 +490,7 @@ class GitStorage:
464
490
  self,
465
491
  *,
466
492
  id: Optional[str] = None,
467
- default_branch: str = "main",
493
+ default_branch: Optional[str] = None, # defaults to "main"
468
494
  base_repo: Optional[BaseRepo] = None,
469
495
  ttl: Optional[int] = None,
470
496
  ) -> Repo: ...
@@ -603,6 +629,8 @@ Key types are provided via TypedDict for better IDE support:
603
629
  from pierre_storage.types import (
604
630
  GitStorageOptions,
605
631
  BaseRepo,
632
+ GitHubBaseRepo,
633
+ ForkBaseRepo,
606
634
  CommitSignature,
607
635
  CreateCommitOptions,
608
636
  ListFilesResult,
@@ -616,12 +644,19 @@ from pierre_storage.types import (
616
644
  # ... and more
617
645
  )
618
646
 
619
- # BaseRepo type for GitHub sync
620
- class BaseRepo(TypedDict, total=False):
621
- provider: Literal["github"] # Always "github"
647
+ # BaseRepo type for GitHub sync or forks
648
+ class GitHubBaseRepo(TypedDict, total=False):
649
+ provider: Literal["github"] # Always "github"
622
650
  owner: str # GitHub organization or user
623
651
  name: str # Repository name
624
652
  default_branch: Optional[str] # Default branch (optional)
653
+
654
+ class ForkBaseRepo(TypedDict, total=False):
655
+ id: str # Source repo ID
656
+ ref: Optional[str] # Optional ref name
657
+ sha: Optional[str] # Optional commit SHA
658
+
659
+ BaseRepo = Union[GitHubBaseRepo, ForkBaseRepo]
625
660
  ```
626
661
 
627
662
  ## Webhook Validation
@@ -1,7 +1,7 @@
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
5
  from urllib.parse import urlencode
6
6
 
7
7
  import httpx
@@ -10,7 +10,10 @@ from pierre_storage.auth import generate_jwt
10
10
  from pierre_storage.errors import ApiError
11
11
  from pierre_storage.repo import DEFAULT_TOKEN_TTL_SECONDS, RepoImpl
12
12
  from pierre_storage.types import (
13
+ BaseRepo,
13
14
  DeleteRepoResult,
15
+ ForkBaseRepo,
16
+ GitHubBaseRepo,
14
17
  GitStorageOptions,
15
18
  ListReposResult,
16
19
  Repo,
@@ -102,17 +105,18 @@ class GitStorage:
102
105
  self,
103
106
  *,
104
107
  id: Optional[str] = None,
105
- default_branch: str = "main",
106
- base_repo: Optional[Dict[str, Any]] = None,
108
+ default_branch: Optional[str] = None,
109
+ base_repo: Optional[BaseRepo] = None,
107
110
  ttl: Optional[int] = None,
108
111
  ) -> Repo:
109
112
  """Create a new repository.
110
113
 
111
114
  Args:
112
115
  id: Repository ID (auto-generated if not provided)
113
- default_branch: Default branch name (default: "main")
114
- base_repo: Optional base repository for GitHub sync
115
- (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
116
120
  ttl: Token TTL in seconds
117
121
 
118
122
  Returns:
@@ -129,19 +133,52 @@ class GitStorage:
129
133
  )
130
134
 
131
135
  url = f"{self.options['api_base_url']}/api/v{self.options['api_version']}/repos"
132
- body: Dict[str, Any] = {"default_branch": default_branch}
136
+ body: Dict[str, Any] = {}
133
137
 
134
138
  # Match backend priority: base_repo.default_branch > default_branch > 'main'
135
- resolved_default_branch = default_branch
139
+ explicit_default_branch = default_branch is not None
140
+ resolved_default_branch: Optional[str] = None
141
+
136
142
  if base_repo:
137
- # Ensure provider is set to 'github' if not provided
138
- base_repo_with_provider = {
139
- "provider": "github",
140
- **base_repo,
141
- }
142
- body["base_repo"] = base_repo_with_provider
143
- if base_repo.get("default_branch"):
144
- 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
+ "owner": self.options["name"],
152
+ "name": fork_repo["id"],
153
+ "operation": "fork",
154
+ "auth": {"token": base_repo_token},
155
+ }
156
+ if fork_repo.get("ref"):
157
+ base_repo_payload["ref"] = fork_repo["ref"]
158
+ if fork_repo.get("sha"):
159
+ base_repo_payload["sha"] = fork_repo["sha"]
160
+ body["base_repo"] = base_repo_payload
161
+ if explicit_default_branch:
162
+ resolved_default_branch = default_branch
163
+ body["default_branch"] = default_branch
164
+ else:
165
+ github_repo = cast(GitHubBaseRepo, base_repo)
166
+ # Ensure provider is set to 'github' if not provided
167
+ base_repo_with_provider = {
168
+ "provider": "github",
169
+ **github_repo,
170
+ }
171
+ body["base_repo"] = base_repo_with_provider
172
+ if github_repo.get("default_branch"):
173
+ resolved_default_branch = github_repo["default_branch"]
174
+ elif explicit_default_branch:
175
+ resolved_default_branch = default_branch
176
+ else:
177
+ resolved_default_branch = "main"
178
+ body["default_branch"] = resolved_default_branch
179
+ else:
180
+ resolved_default_branch = default_branch if explicit_default_branch else "main"
181
+ body["default_branch"] = resolved_default_branch
145
182
 
146
183
  async with httpx.AsyncClient() as client:
147
184
  response = await client.post(
@@ -173,7 +210,7 @@ class GitStorage:
173
210
 
174
211
  return RepoImpl(
175
212
  repo_id,
176
- resolved_default_branch,
213
+ resolved_default_branch or "main",
177
214
  api_base_url,
178
215
  storage_base_url,
179
216
  name,
@@ -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
 
@@ -1,7 +1,7 @@
1
1
  """Version information for Pierre Storage SDK."""
2
2
 
3
3
  PACKAGE_NAME = "code-storage-py-sdk"
4
- PACKAGE_VERSION = "0.9.0"
4
+ PACKAGE_VERSION = "0.12.1"
5
5
 
6
6
 
7
7
  def get_user_agent() -> str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pierre-storage
3
- Version: 0.11.0
3
+ Version: 0.12.1
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
@@ -472,6 +481,23 @@ commits = await repo.list_commits()
472
481
  3. You can then use all Pierre SDK features (diffs, commits, file access) on the synced content
473
482
  4. The provider is automatically set to `"github"` when using `base_repo`
474
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
+
475
501
  ### Restoring Commits
476
502
 
477
503
  You can restore a repository to a previous commit:
@@ -500,7 +526,7 @@ class GitStorage:
500
526
  self,
501
527
  *,
502
528
  id: Optional[str] = None,
503
- default_branch: str = "main",
529
+ default_branch: Optional[str] = None, # defaults to "main"
504
530
  base_repo: Optional[BaseRepo] = None,
505
531
  ttl: Optional[int] = None,
506
532
  ) -> Repo: ...
@@ -639,6 +665,8 @@ Key types are provided via TypedDict for better IDE support:
639
665
  from pierre_storage.types import (
640
666
  GitStorageOptions,
641
667
  BaseRepo,
668
+ GitHubBaseRepo,
669
+ ForkBaseRepo,
642
670
  CommitSignature,
643
671
  CreateCommitOptions,
644
672
  ListFilesResult,
@@ -652,12 +680,19 @@ from pierre_storage.types import (
652
680
  # ... and more
653
681
  )
654
682
 
655
- # BaseRepo type for GitHub sync
656
- class BaseRepo(TypedDict, total=False):
657
- 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"
658
686
  owner: str # GitHub organization or user
659
687
  name: str # Repository name
660
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]
661
696
  ```
662
697
 
663
698
  ## Webhook Validation
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pierre-storage"
7
- version = "0.11.0"
7
+ version = "0.12.1"
8
8
  description = "Pierre Git Storage SDK for Python"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -147,6 +147,43 @@ class TestGitStorage:
147
147
  body = call_kwargs["json"]
148
148
  assert body["base_repo"]["provider"] == "github"
149
149
 
150
+ @pytest.mark.asyncio
151
+ async def test_create_repo_with_fork_base_repo(self, git_storage_options: dict) -> None:
152
+ """Test creating a forked repository."""
153
+ storage = GitStorage(git_storage_options)
154
+
155
+ mock_post_response = MagicMock()
156
+ mock_post_response.status_code = 200
157
+ mock_post_response.is_success = True
158
+ mock_post_response.json.return_value = {"repo_id": "test-repo"}
159
+
160
+ with patch("httpx.AsyncClient") as mock_client:
161
+ client_instance = mock_client.return_value.__aenter__.return_value
162
+ client_instance.post = AsyncMock(return_value=mock_post_response)
163
+
164
+ repo = await storage.create_repo(
165
+ id="test-repo",
166
+ base_repo={
167
+ "id": "template-repo",
168
+ "ref": "develop",
169
+ },
170
+ )
171
+ assert repo.default_branch == "main"
172
+
173
+ call_kwargs = client_instance.post.call_args[1]
174
+ body = call_kwargs["json"]
175
+ assert "default_branch" not in body
176
+ assert body["base_repo"]["provider"] == "code"
177
+ assert body["base_repo"]["owner"] == "test-customer"
178
+ assert body["base_repo"]["name"] == "template-repo"
179
+ assert body["base_repo"]["operation"] == "fork"
180
+ assert body["base_repo"]["ref"] == "develop"
181
+
182
+ token = body["base_repo"]["auth"]["token"]
183
+ payload = jwt.decode(token, options={"verify_signature": False})
184
+ assert payload["repo"] == "template-repo"
185
+ assert payload["scopes"] == ["git:read"]
186
+
150
187
  @pytest.mark.asyncio
151
188
  async def test_create_repo_conflict(self, git_storage_options: dict) -> None:
152
189
  """Test creating a repository that already exists."""
File without changes