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.
- pierre_storage-0.1.3/LICENSE +21 -0
- pierre_storage-0.1.3/MANIFEST.in +5 -0
- pierre_storage-0.1.3/PKG-INFO +401 -0
- pierre_storage-0.1.3/README.md +363 -0
- pierre_storage-0.1.3/pierre_storage/__init__.py +84 -0
- pierre_storage-0.1.3/pierre_storage/auth.py +77 -0
- pierre_storage-0.1.3/pierre_storage/client.py +250 -0
- pierre_storage-0.1.3/pierre_storage/commit.py +427 -0
- pierre_storage-0.1.3/pierre_storage/errors.py +78 -0
- pierre_storage-0.1.3/pierre_storage/py.typed +1 -0
- pierre_storage-0.1.3/pierre_storage/repo.py +587 -0
- pierre_storage-0.1.3/pierre_storage/types.py +427 -0
- pierre_storage-0.1.3/pierre_storage/webhook.py +175 -0
- pierre_storage-0.1.3/pierre_storage.egg-info/PKG-INFO +401 -0
- pierre_storage-0.1.3/pierre_storage.egg-info/SOURCES.txt +20 -0
- pierre_storage-0.1.3/pierre_storage.egg-info/dependency_links.txt +1 -0
- pierre_storage-0.1.3/pierre_storage.egg-info/requires.txt +16 -0
- pierre_storage-0.1.3/pierre_storage.egg-info/top_level.txt +1 -0
- pierre_storage-0.1.3/pyproject.toml +73 -0
- pierre_storage-0.1.3/setup.cfg +4 -0
- pierre_storage-0.1.3/tests/test_client.py +236 -0
- pierre_storage-0.1.3/tests/test_webhook.py +158 -0
|
@@ -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,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
|