pierre-storage 0.1.3__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Pierre
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,5 @@
1
+ include README.md
2
+ include LICENSE
3
+ include pyproject.toml
4
+ recursive-include pierre_storage *.py
5
+ recursive-include pierre_storage py.typed
@@ -0,0 +1,401 @@
1
+ Metadata-Version: 2.4
2
+ Name: pierre-storage
3
+ Version: 0.1.3
4
+ Summary: Pierre Git Storage SDK for Python
5
+ Author-email: Pierre <support@pierre.io>
6
+ License: MIT
7
+ Project-URL: Homepage, https://pierre.io
8
+ Project-URL: Documentation, https://docs.pierre.io
9
+ Project-URL: Repository, https://github.com/pierre/monorepo
10
+ Project-URL: Issues, https://github.com/pierre/monorepo/issues
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Requires-Python: >=3.8
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: httpx>=0.27.0
25
+ Requires-Dist: pyjwt>=2.8.0
26
+ Requires-Dist: cryptography>=41.0.0
27
+ Requires-Dist: pydantic>=2.0.0
28
+ Requires-Dist: typing-extensions>=4.5.0; python_version < "3.10"
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=7.4.0; extra == "dev"
31
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
32
+ Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
33
+ Requires-Dist: mypy>=1.5.0; extra == "dev"
34
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
35
+ Requires-Dist: build>=1.0.0; extra == "dev"
36
+ Requires-Dist: twine>=4.0.0; extra == "dev"
37
+ Dynamic: license-file
38
+
39
+ # pierre-storage
40
+
41
+ Pierre Git Storage SDK for Python applications.
42
+
43
+ ## Installation
44
+
45
+ ```bash
46
+ pip install pierre-storage
47
+ ```
48
+
49
+ ## Usage
50
+
51
+ ### Basic Setup
52
+
53
+ ```python
54
+ from pierre_storage import GitStorage
55
+
56
+ # Initialize the client with your name and key
57
+ storage = GitStorage({
58
+ "name": "your-name", # e.g., 'v0'
59
+ "key": "your-key", # Your API key in PEM format
60
+ })
61
+ ```
62
+
63
+ ### Creating a Repository
64
+
65
+ ```python
66
+ # Create a new repository with auto-generated ID
67
+ repo = await storage.create_repo()
68
+ print(repo.id) # e.g., '123e4567-e89b-12d3-a456-426614174000'
69
+
70
+ # Create a repository with custom ID
71
+ custom_repo = await storage.create_repo({"id": "my-custom-repo"})
72
+ print(custom_repo.id) # 'my-custom-repo'
73
+ ```
74
+
75
+ ### Finding a Repository
76
+
77
+ ```python
78
+ found_repo = await storage.find_one({"id": "repo-id"})
79
+ if found_repo:
80
+ url = await found_repo.get_remote_url()
81
+ print(f"Repository URL: {url}")
82
+ ```
83
+
84
+ ### Getting Remote URLs
85
+
86
+ The SDK generates secure URLs with JWT authentication for Git operations:
87
+
88
+ ```python
89
+ # Get URL with default permissions (git:write, git:read) and 1-year TTL
90
+ url = await repo.get_remote_url()
91
+ # Returns: https://t:JWT@your-name.code.storage/repo-id.git
92
+
93
+ # Configure the Git remote
94
+ print(f"Run: git remote add origin {url}")
95
+
96
+ # Get URL with custom permissions and TTL
97
+ read_only_url = await repo.get_remote_url({
98
+ "permissions": ["git:read"], # Read-only access
99
+ "ttl": 3600, # 1 hour in seconds
100
+ })
101
+
102
+ # Available permissions:
103
+ # - 'git:read' - Read access to Git repository
104
+ # - 'git:write' - Write access to Git repository
105
+ # - 'repo:write' - Create a repository
106
+ ```
107
+
108
+ ### Working with Repository Content
109
+
110
+ Once you have a repository instance, you can perform various Git operations:
111
+
112
+ ```python
113
+ repo = await storage.create_repo()
114
+ # or
115
+ repo = await storage.find_one({"id": "existing-repo-id"})
116
+
117
+ # Get file content (streaming)
118
+ response = await repo.get_file_stream({
119
+ "path": "README.md",
120
+ "ref": "main", # optional, defaults to default branch
121
+ })
122
+ text = await response.aread()
123
+ print(text.decode())
124
+
125
+ # List all files in the repository
126
+ files = await repo.list_files({
127
+ "ref": "main", # optional, defaults to default branch
128
+ })
129
+ print(files["paths"]) # List of file paths
130
+
131
+ # List branches
132
+ branches = await repo.list_branches({
133
+ "limit": 10,
134
+ "cursor": None, # for pagination
135
+ })
136
+ print(branches["branches"])
137
+
138
+ # List commits
139
+ commits = await repo.list_commits({
140
+ "branch": "main", # optional
141
+ "limit": 20,
142
+ "cursor": None, # for pagination
143
+ })
144
+ print(commits["commits"])
145
+
146
+ # Get branch diff
147
+ branch_diff = await repo.get_branch_diff({
148
+ "branch": "feature-branch",
149
+ "base": "main", # optional, defaults to main
150
+ })
151
+ print(branch_diff["stats"])
152
+ print(branch_diff["files"])
153
+
154
+ # Get commit diff
155
+ commit_diff = await repo.get_commit_diff({
156
+ "sha": "abc123...",
157
+ })
158
+ print(commit_diff["stats"])
159
+ print(commit_diff["files"])
160
+ ```
161
+
162
+ ### Creating Commits
163
+
164
+ The SDK provides a fluent builder API for creating commits with streaming support:
165
+
166
+ ```python
167
+ # Create a commit
168
+ result = await (
169
+ repo.create_commit({
170
+ "target_branch": "main",
171
+ "commit_message": "Update docs",
172
+ "author": {"name": "Docs Bot", "email": "docs@example.com"},
173
+ })
174
+ .add_file_from_string("docs/changelog.md", "# v2.0.1\n- add streaming SDK\n")
175
+ .add_file("docs/readme.md", b"Binary content here")
176
+ .delete_path("docs/legacy.txt")
177
+ .send()
178
+ )
179
+
180
+ print(result["commit_sha"])
181
+ print(result["ref_update"]["new_sha"])
182
+ print(result["ref_update"]["old_sha"]) # All zeroes when ref is created
183
+ ```
184
+
185
+ The builder exposes:
186
+
187
+ - `add_file(path, source, options)` - Attach bytes from various sources
188
+ - `add_file_from_string(path, contents, encoding, options)` - Add text files (defaults to UTF-8)
189
+ - `delete_path(path)` - Remove files or folders
190
+ - `send()` - Finalize the commit and receive metadata
191
+
192
+ `send()` returns a result with:
193
+
194
+ ```python
195
+ {
196
+ "commit_sha": str,
197
+ "tree_sha": str,
198
+ "target_branch": str,
199
+ "pack_bytes": int,
200
+ "blob_count": int,
201
+ "ref_update": {
202
+ "branch": str,
203
+ "old_sha": str, # All zeroes when the ref is created
204
+ "new_sha": str,
205
+ }
206
+ }
207
+ ```
208
+
209
+ If the backend reports a failure, the builder raises a `RefUpdateError` containing the status, reason, and ref details.
210
+
211
+ **Options:**
212
+
213
+ - `target_branch` (required): Branch name (without `refs/heads/` prefix)
214
+ - `expected_head_sha` (optional): Branch or commit that must match the remote tip
215
+ - `commit_message` (required): The commit message
216
+ - `author` (required): Dictionary with `name` and `email`
217
+ - `committer` (optional): Dictionary with `name` and `email` (defaults to author)
218
+
219
+ > Files are chunked into 4 MiB segments, allowing streaming of large assets without buffering in memory.
220
+
221
+ > The `target_branch` must already exist on the remote repository. To seed an empty repository, omit `expected_head_sha`; the service will create the first commit only when no refs are present.
222
+
223
+ ### Streaming Large Files
224
+
225
+ The commit builder accepts async iterables, allowing streaming of large files:
226
+
227
+ ```python
228
+ async def file_chunks():
229
+ """Generate file chunks asynchronously."""
230
+ with open("/tmp/large-file.zip", "rb") as f:
231
+ while chunk := f.read(1024 * 1024): # Read 1MB at a time
232
+ yield chunk
233
+
234
+ result = await (
235
+ repo.create_commit({
236
+ "target_branch": "assets",
237
+ "expected_head_sha": "abc123...",
238
+ "commit_message": "Upload latest design bundle",
239
+ "author": {"name": "Assets Uploader", "email": "assets@example.com"},
240
+ })
241
+ .add_file("assets/design-kit.zip", file_chunks())
242
+ .send()
243
+ )
244
+ ```
245
+
246
+ ### Restoring Commits
247
+
248
+ You can restore a repository to a previous commit:
249
+
250
+ ```python
251
+ result = await repo.restore_commit({
252
+ "target_branch": "main",
253
+ "target_commit_sha": "abc123...", # Commit to restore to
254
+ "expected_head_sha": "def456...", # Optional: current HEAD for safety
255
+ "commit_message": "Restore to stable version",
256
+ "author": {"name": "DevOps", "email": "devops@example.com"},
257
+ })
258
+
259
+ print(result["commit_sha"])
260
+ print(result["ref_update"])
261
+ ```
262
+
263
+ ## API Reference
264
+
265
+ ### GitStorage
266
+
267
+ ```python
268
+ class GitStorage:
269
+ def __init__(self, options: GitStorageOptions) -> None: ...
270
+ async def create_repo(self, options: CreateRepoOptions = None) -> Repo: ...
271
+ async def find_one(self, options: FindOneOptions) -> Optional[Repo]: ...
272
+ def get_config(self) -> GitStorageOptions: ...
273
+ ```
274
+
275
+ ### Repo
276
+
277
+ ```python
278
+ class Repo:
279
+ @property
280
+ def id(self) -> str: ...
281
+
282
+ async def get_remote_url(self, options: GetRemoteURLOptions = None) -> str: ...
283
+ async def get_file_stream(self, options: GetFileOptions) -> Response: ...
284
+ async def list_files(self, options: ListFilesOptions = None) -> ListFilesResult: ...
285
+ async def list_branches(self, options: ListBranchesOptions = None) -> ListBranchesResult: ...
286
+ async def list_commits(self, options: ListCommitsOptions = None) -> ListCommitsResult: ...
287
+ async def get_branch_diff(self, options: GetBranchDiffOptions) -> GetBranchDiffResult: ...
288
+ async def get_commit_diff(self, options: GetCommitDiffOptions) -> GetCommitDiffResult: ...
289
+ async def pull_upstream(self, options: PullUpstreamOptions = None) -> None: ...
290
+ async def restore_commit(self, options: RestoreCommitOptions) -> RestoreCommitResult: ...
291
+ def create_commit(self, options: CreateCommitOptions) -> CommitBuilder: ...
292
+ ```
293
+
294
+ ### Type Definitions
295
+
296
+ All types are provided via TypedDict for better IDE support:
297
+
298
+ ```python
299
+ from pierre_storage.types import (
300
+ GitStorageOptions,
301
+ CreateRepoOptions,
302
+ GetRemoteURLOptions,
303
+ CommitSignature,
304
+ # ... and more
305
+ )
306
+ ```
307
+
308
+ ## Webhook Validation
309
+
310
+ The SDK includes utilities for validating webhook signatures:
311
+
312
+ ```python
313
+ from pierre_storage import validate_webhook, parse_push_event
314
+
315
+ # Validate webhook signature
316
+ result = validate_webhook(
317
+ payload=request.body.decode(), # Raw payload string
318
+ signature=request.headers["X-Pierre-Signature"],
319
+ secret="your-webhook-secret",
320
+ options={"max_age_seconds": 300}, # 5 minutes
321
+ )
322
+
323
+ if result["valid"]:
324
+ # Parse the event
325
+ data = json.loads(request.body)
326
+
327
+ if result["event_type"] == "push":
328
+ event = parse_push_event(data)
329
+ print(f"Push to {event['ref']}")
330
+ print(f"Commit: {event['before']} -> {event['after']}")
331
+ else:
332
+ print(f"Invalid webhook: {result['error']}")
333
+ ```
334
+
335
+ ## Authentication
336
+
337
+ The SDK uses JWT (JSON Web Tokens) for authentication. When you call `get_remote_url()`, it:
338
+
339
+ 1. Creates a JWT with your name, repository ID, and requested permissions
340
+ 2. Signs it with your private key (ES256, RS256, or EdDSA)
341
+ 3. Embeds it in the Git remote URL as the password
342
+
343
+ The generated URLs are compatible with standard Git clients and include all necessary authentication.
344
+
345
+ ## Error Handling
346
+
347
+ The SDK provides specific error classes:
348
+
349
+ ```python
350
+ from pierre_storage import ApiError, RefUpdateError
351
+
352
+ try:
353
+ repo = await storage.create_repo({"id": "existing"})
354
+ except ApiError as e:
355
+ print(f"API error: {e.message}")
356
+ print(f"Status code: {e.status_code}")
357
+
358
+ try:
359
+ result = await builder.send()
360
+ except RefUpdateError as e:
361
+ print(f"Ref update failed: {e.message}")
362
+ print(f"Status: {e.status}")
363
+ print(f"Reason: {e.reason}")
364
+ print(f"Ref update: {e.ref_update}")
365
+ ```
366
+
367
+ ## Development
368
+
369
+ ### Setup
370
+
371
+ ```bash
372
+ # Create virtual environment and install dependencies
373
+ python3 -m venv venv
374
+ source venv/bin/activate
375
+ pip install -e ".[dev]"
376
+
377
+ # Or use Moon
378
+ moon run git-storage-sdk-python:setup
379
+
380
+ # Run tests
381
+ pytest
382
+
383
+ # Run tests with coverage
384
+ pytest --cov=pierre_storage --cov-report=html
385
+
386
+ # Type checking
387
+ mypy pierre_storage
388
+
389
+ # Linting
390
+ ruff check pierre_storage
391
+ ```
392
+
393
+ ### Building
394
+
395
+ ```bash
396
+ python -m build
397
+ ```
398
+
399
+ ## License
400
+
401
+ MIT