riffsdk 0.1.0__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.
- riffsdk-0.1.0/.github/workflows/ci.yml +27 -0
- riffsdk-0.1.0/.github/workflows/pr-comment.yml +31 -0
- riffsdk-0.1.0/.github/workflows/release.yml +95 -0
- riffsdk-0.1.0/.gitignore +6 -0
- riffsdk-0.1.0/.python-version +1 -0
- riffsdk-0.1.0/AGENTS.md +80 -0
- riffsdk-0.1.0/CHANGELOG.md +7 -0
- riffsdk-0.1.0/CLAUDE.md +1 -0
- riffsdk-0.1.0/LICENCE +7 -0
- riffsdk-0.1.0/PKG-INFO +9 -0
- riffsdk-0.1.0/README.md +113 -0
- riffsdk-0.1.0/examples/async_client.py +32 -0
- riffsdk-0.1.0/examples/basic_crud.py +34 -0
- riffsdk-0.1.0/examples/file_upload_download.py +48 -0
- riffsdk-0.1.0/examples/optimistic_concurrency.py +55 -0
- riffsdk-0.1.0/mise.toml +24 -0
- riffsdk-0.1.0/pyproject.toml +53 -0
- riffsdk-0.1.0/src/riffsdk/__init__.py +0 -0
- riffsdk-0.1.0/src/riffsdk/_internal/__init__.py +0 -0
- riffsdk-0.1.0/src/riffsdk/_internal/auth.py +97 -0
- riffsdk-0.1.0/src/riffsdk/_internal/config.py +10 -0
- riffsdk-0.1.0/src/riffsdk/storage/__init__.py +59 -0
- riffsdk-0.1.0/src/riffsdk/storage/_async_client.py +555 -0
- riffsdk-0.1.0/src/riffsdk/storage/_client.py +586 -0
- riffsdk-0.1.0/src/riffsdk/storage/_http.py +351 -0
- riffsdk-0.1.0/src/riffsdk/storage/_models.py +68 -0
- riffsdk-0.1.0/src/riffsdk/storage/_session.py +33 -0
- riffsdk-0.1.0/src/riffsdk/storage/_streaming.py +177 -0
- riffsdk-0.1.0/src/riffsdk/storage/_upload.py +229 -0
- riffsdk-0.1.0/src/riffsdk/storage/exceptions.py +87 -0
- riffsdk-0.1.0/src/riffsdk/storage/py.typed +0 -0
- riffsdk-0.1.0/tests/__init__.py +0 -0
- riffsdk-0.1.0/tests/test_async_client.py +644 -0
- riffsdk-0.1.0/tests/test_client.py +901 -0
- riffsdk-0.1.0/tests/test_exceptions.py +89 -0
- riffsdk-0.1.0/tests/test_http.py +302 -0
- riffsdk-0.1.0/tests/test_imports.py +59 -0
- riffsdk-0.1.0/tests/test_integration.py +861 -0
- riffsdk-0.1.0/tests/test_models.py +114 -0
- riffsdk-0.1.0/tests/test_session.py +69 -0
- riffsdk-0.1.0/tests/test_streaming.py +169 -0
- riffsdk-0.1.0/tests/test_upload.py +344 -0
- riffsdk-0.1.0/uv.lock +821 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: CI
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
concurrency:
|
|
8
|
+
group: ci-${{ github.ref }}
|
|
9
|
+
cancel-in-progress: true
|
|
10
|
+
jobs:
|
|
11
|
+
test:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
strategy:
|
|
14
|
+
matrix:
|
|
15
|
+
python-version: ['3.11', '3.12', '3.13']
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v6
|
|
18
|
+
- name: Install uv
|
|
19
|
+
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
|
20
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
21
|
+
run: uv python install ${{ matrix.python-version }}
|
|
22
|
+
- name: Install dependencies
|
|
23
|
+
run: uv sync --dev
|
|
24
|
+
- name: Lint
|
|
25
|
+
run: uv run ruff check src tests
|
|
26
|
+
- name: Run tests
|
|
27
|
+
run: uv run pytest
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: PR Install Instructions
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
types: [opened, synchronize]
|
|
6
|
+
permissions:
|
|
7
|
+
pull-requests: write
|
|
8
|
+
jobs:
|
|
9
|
+
comment:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- name: Post or update install instructions
|
|
13
|
+
uses: marocchino/sticky-pull-request-comment@v3
|
|
14
|
+
with:
|
|
15
|
+
header: install-instructions
|
|
16
|
+
message: |-
|
|
17
|
+
### Install pre-release version
|
|
18
|
+
```bash
|
|
19
|
+
# With pip
|
|
20
|
+
pip install git+https://github.com/databutton/riff-sdk-python.git@${{ github.head_ref }}
|
|
21
|
+
|
|
22
|
+
# With uv
|
|
23
|
+
uv pip install git+https://github.com/databutton/riff-sdk-python.git@${{ github.head_ref }}
|
|
24
|
+
```
|
|
25
|
+
Or as a dependency in `pyproject.toml`:
|
|
26
|
+
```toml
|
|
27
|
+
dependencies = [
|
|
28
|
+
"riffsdk @ git+https://github.com/databutton/riff-sdk-python.git@${{ github.head_ref }}",
|
|
29
|
+
]
|
|
30
|
+
```
|
|
31
|
+
<sub>Commit: ${{ github.event.pull_request.head.sha }}</sub>
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Release
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
concurrency:
|
|
7
|
+
group: release
|
|
8
|
+
cancel-in-progress: false
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
jobs:
|
|
12
|
+
release:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
permissions:
|
|
15
|
+
contents: write
|
|
16
|
+
outputs:
|
|
17
|
+
released: ${{ steps.release.outputs.released }}
|
|
18
|
+
tag: ${{ steps.release.outputs.tag }}
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v6
|
|
21
|
+
with:
|
|
22
|
+
fetch-depth: 0
|
|
23
|
+
ref: ${{ github.ref_name }}
|
|
24
|
+
- run: git reset --hard ${{ github.sha }}
|
|
25
|
+
- name: Install uv
|
|
26
|
+
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
|
27
|
+
- run: uv python install 3.11
|
|
28
|
+
- run: uv sync --dev
|
|
29
|
+
- name: Lint
|
|
30
|
+
run: uv run ruff check src tests
|
|
31
|
+
- name: Run tests
|
|
32
|
+
run: uv run pytest
|
|
33
|
+
- name: Check for new version
|
|
34
|
+
id: check
|
|
35
|
+
run: |
|
|
36
|
+
NEXT=$(uv run semantic-release version --print 2>/dev/null || true)
|
|
37
|
+
LAST=$(uv run semantic-release version --print-last-released 2>/dev/null || true)
|
|
38
|
+
echo "next=$NEXT" >> "$GITHUB_OUTPUT"
|
|
39
|
+
echo "last=$LAST" >> "$GITHUB_OUTPUT"
|
|
40
|
+
if [ -n "$NEXT" ] && [ "$NEXT" != "$LAST" ]; then
|
|
41
|
+
echo "should_release=true" >> "$GITHUB_OUTPUT"
|
|
42
|
+
else
|
|
43
|
+
echo "should_release=false" >> "$GITHUB_OUTPUT"
|
|
44
|
+
fi
|
|
45
|
+
- name: Create version and tag
|
|
46
|
+
if: steps.check.outputs.should_release == 'true'
|
|
47
|
+
env:
|
|
48
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
49
|
+
run: |
|
|
50
|
+
# Stamp version, build, commit locally, tag — but don't push the commit
|
|
51
|
+
# (branch protection blocks direct pushes to main)
|
|
52
|
+
uv run semantic-release -v version --no-push --no-vcs-release
|
|
53
|
+
- name: Push tag
|
|
54
|
+
if: steps.check.outputs.should_release == 'true'
|
|
55
|
+
run: |
|
|
56
|
+
TAG="v${{ steps.check.outputs.next }}"
|
|
57
|
+
# Push only the tag — tags are not subject to branch protection
|
|
58
|
+
git push origin "$TAG"
|
|
59
|
+
- name: Create GitHub release
|
|
60
|
+
id: release
|
|
61
|
+
if: steps.check.outputs.should_release == 'true'
|
|
62
|
+
env:
|
|
63
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
64
|
+
run: |
|
|
65
|
+
TAG="v${{ steps.check.outputs.next }}"
|
|
66
|
+
uv run semantic-release -v publish --tag "$TAG"
|
|
67
|
+
echo "released=true" >> "$GITHUB_OUTPUT"
|
|
68
|
+
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
|
|
69
|
+
- name: No release needed
|
|
70
|
+
if: steps.check.outputs.should_release != 'true'
|
|
71
|
+
run: |
|
|
72
|
+
echo "No version bump detected, skipping release"
|
|
73
|
+
- name: Upload dist artifacts
|
|
74
|
+
if: steps.check.outputs.should_release == 'true'
|
|
75
|
+
uses: actions/upload-artifact@v7
|
|
76
|
+
with:
|
|
77
|
+
name: dist
|
|
78
|
+
path: dist/
|
|
79
|
+
if-no-files-found: error
|
|
80
|
+
publish:
|
|
81
|
+
needs: release
|
|
82
|
+
if: needs.release.outputs.released == 'true'
|
|
83
|
+
runs-on: ubuntu-latest
|
|
84
|
+
environment: pypi
|
|
85
|
+
permissions:
|
|
86
|
+
contents: read
|
|
87
|
+
id-token: write
|
|
88
|
+
steps:
|
|
89
|
+
- name: Download dist artifacts
|
|
90
|
+
uses: actions/download-artifact@v8
|
|
91
|
+
with:
|
|
92
|
+
name: dist
|
|
93
|
+
path: dist/
|
|
94
|
+
- name: Publish to PyPI
|
|
95
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
riffsdk-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.11
|
riffsdk-0.1.0/AGENTS.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Agent guide
|
|
2
|
+
|
|
3
|
+
Instructions for AI agents and human developers working on this repo.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
This project uses [uv](https://docs.astral.sh/uv/) for dependency management and [mise](https://mise.jdx.dev/) as a task runner. Python commands must be run through `uv run` (or the mise tasks that wrap it) so they execute in the correct virtualenv.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
uv sync --dev # Install all dependencies including dev tools
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Commands
|
|
14
|
+
|
|
15
|
+
Use `mise run <task>` for all common operations:
|
|
16
|
+
|
|
17
|
+
| Command | What it does |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `mise run test` | Run unit tests (`uv run pytest`) |
|
|
20
|
+
| `mise run test:integration` | Run integration tests against local riff-api |
|
|
21
|
+
| `mise run lint` | Lint with ruff (`uv run ruff check src tests`) |
|
|
22
|
+
| `mise run lint:fix` | Lint and auto-fix |
|
|
23
|
+
| `mise run format` | Format code (`uv run ruff format src tests`) |
|
|
24
|
+
|
|
25
|
+
If you need to run something not covered by a mise task, prefix it with `uv run`:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
uv run pytest tests/test_client.py -k "test_put" -v
|
|
29
|
+
uv run ruff check --fix src
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Do **not** run bare `pytest`, `ruff`, or `python` -- they won't use the project's virtualenv or dependencies.
|
|
33
|
+
|
|
34
|
+
## Integration tests
|
|
35
|
+
|
|
36
|
+
Integration tests require a running riff-api server and are skipped by default. To run them:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Start the server first (in the riff-api repo)
|
|
40
|
+
cd ../riff-api && mise run dev:local
|
|
41
|
+
|
|
42
|
+
# Then run integration tests
|
|
43
|
+
mise run test:integration
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The integration test task sets `RIFF_BASE_URL=http://localhost:8080/riff-api` and `RIFF_INTEGRATION_TEST=true` automatically.
|
|
47
|
+
|
|
48
|
+
## Repository structure
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
src/riffsdk/
|
|
52
|
+
__init__.py # Public API re-exports (everything users import)
|
|
53
|
+
_internal/
|
|
54
|
+
auth.py # RiffAuth - authentication
|
|
55
|
+
config.py # Configuration handling
|
|
56
|
+
storage/
|
|
57
|
+
__init__.py # Re-exports from submodules
|
|
58
|
+
_client.py # StorageClient (sync)
|
|
59
|
+
_async_client.py # AsyncStorageClient (async)
|
|
60
|
+
_http.py # HTTP transport layer
|
|
61
|
+
_models.py # Pydantic models (ObjectMeta, Scope, etc.)
|
|
62
|
+
_session.py # Session management
|
|
63
|
+
_streaming.py # StorageReader, StorageWriter
|
|
64
|
+
_upload.py # ResumableUpload, AsyncResumableUpload
|
|
65
|
+
exceptions.py # Exception hierarchy (StorageError and subclasses)
|
|
66
|
+
|
|
67
|
+
tests/ # pytest tests (mirrors src structure)
|
|
68
|
+
examples/ # Runnable example scripts
|
|
69
|
+
|
|
70
|
+
.github/workflows/
|
|
71
|
+
ci.yml # Lint + test on PRs and main (Python 3.11/3.12/3.13)
|
|
72
|
+
release.yml # Semantic versioning + PyPI publish on merge to main
|
|
73
|
+
pr-comment.yml # Posts git-install instructions on PRs
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Conventions
|
|
77
|
+
|
|
78
|
+
- **Commits**: use [conventional commits](https://www.conventionalcommits.org/) (`fix:`, `feat:`, `chore:`, etc.). Versioning is automatic -- `fix:` bumps patch, `feat:` bumps minor.
|
|
79
|
+
- **Underscore-prefixed modules** (`_client.py`, `_http.py`) are internal. Public API is re-exported through `__init__.py`.
|
|
80
|
+
- **Tests**: unit tests run without external services. Integration tests are gated by the `RIFF_INTEGRATION_TEST` env var and skip automatically in CI.
|
riffsdk-0.1.0/CLAUDE.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@./AGENTS.md
|
riffsdk-0.1.0/LICENCE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2026 Databutton AS
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
riffsdk-0.1.0/PKG-INFO
ADDED
riffsdk-0.1.0/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# riffsdk
|
|
2
|
+
|
|
3
|
+
Python SDK for the Riff Storage API. Provides sync and async clients for storing, retrieving, and managing objects.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
uv add riffsdk
|
|
9
|
+
# or
|
|
10
|
+
pip install riffsdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or install from a branch (for pre-release testing):
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uv add git+https://github.com/databutton/riff-sdk-python.git@main
|
|
17
|
+
# or
|
|
18
|
+
pip install git+https://github.com/databutton/riff-sdk-python.git@main
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from riffsdk.storage import StorageClient
|
|
25
|
+
|
|
26
|
+
client = StorageClient()
|
|
27
|
+
|
|
28
|
+
# Upload
|
|
29
|
+
meta = client.put("hello.txt", "Hello, world!")
|
|
30
|
+
|
|
31
|
+
# Download
|
|
32
|
+
data = client.get("hello.txt")
|
|
33
|
+
|
|
34
|
+
# List
|
|
35
|
+
for obj in client.list("hello"):
|
|
36
|
+
print(f"{obj.key} ({obj.size} bytes)")
|
|
37
|
+
|
|
38
|
+
# Delete
|
|
39
|
+
client.delete("hello.txt")
|
|
40
|
+
|
|
41
|
+
client.close()
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Async
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from riffsdk.storage import AsyncStorageClient
|
|
48
|
+
|
|
49
|
+
async with AsyncStorageClient() as client:
|
|
50
|
+
await client.put("key", b"data", content_type="application/octet-stream")
|
|
51
|
+
data = await client.get("key")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Authentication
|
|
55
|
+
|
|
56
|
+
Set the `RIFF_TOKEN` environment variable. The SDK picks it up automatically.
|
|
57
|
+
|
|
58
|
+
## API
|
|
59
|
+
|
|
60
|
+
### Clients
|
|
61
|
+
|
|
62
|
+
- `StorageClient` -- sync client
|
|
63
|
+
- `AsyncStorageClient` -- async client
|
|
64
|
+
|
|
65
|
+
Both support: `put`, `get`, `stat`, `exists`, `list`, `delete`, `close`, and context manager usage.
|
|
66
|
+
|
|
67
|
+
### Models
|
|
68
|
+
|
|
69
|
+
- `ObjectMeta` -- metadata for a stored object (key, version, size, content_type, timestamps)
|
|
70
|
+
- `UploadResult`, `DownloadResult` -- operation results
|
|
71
|
+
- `ListPage` -- paginated listing
|
|
72
|
+
- `Scope` -- access scope (use `account_scope()`, `project_scope()`, `session_scope()`)
|
|
73
|
+
|
|
74
|
+
### Uploads
|
|
75
|
+
|
|
76
|
+
- `ResumableUpload` / `AsyncResumableUpload` -- multipart resumable uploads for large files
|
|
77
|
+
- `StorageReader` / `StorageWriter` -- streaming read/write
|
|
78
|
+
|
|
79
|
+
### Exceptions
|
|
80
|
+
|
|
81
|
+
All exceptions inherit from `StorageError`:
|
|
82
|
+
|
|
83
|
+
- `AuthorisationError`
|
|
84
|
+
- `ObjectNotFoundError`
|
|
85
|
+
- `VersionConflictError`
|
|
86
|
+
- `AlreadyExistsError`
|
|
87
|
+
- `LeaseConflictError`
|
|
88
|
+
- `UploadNotFoundError`
|
|
89
|
+
- `QuotaExceededError`
|
|
90
|
+
- `PartMismatchError`
|
|
91
|
+
- `StorageTransportError`
|
|
92
|
+
|
|
93
|
+
## Examples
|
|
94
|
+
|
|
95
|
+
See the `examples/` directory for complete working examples:
|
|
96
|
+
|
|
97
|
+
- `basic_crud.py` -- put, get, list, delete
|
|
98
|
+
- `async_client.py` -- async usage with asyncio
|
|
99
|
+
- `file_upload_download.py` -- file uploads with progress
|
|
100
|
+
- `optimistic_concurrency.py` -- version-based conflict handling
|
|
101
|
+
|
|
102
|
+
## Development
|
|
103
|
+
|
|
104
|
+
Requires Python 3.11+ and [uv](https://docs.astral.sh/uv/).
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
uv sync --dev # Install dependencies
|
|
108
|
+
mise run test # Run tests
|
|
109
|
+
mise run lint # Lint
|
|
110
|
+
mise run format # Format code
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
See `AGENTS.md` for full development workflow details.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Async client usage with asyncio.
|
|
2
|
+
|
|
3
|
+
Requires RIFF_TOKEN environment variable to be set.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
|
|
8
|
+
from riffsdk.storage import AsyncStorageClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def main() -> None:
|
|
12
|
+
async with AsyncStorageClient() as client:
|
|
13
|
+
# Upload
|
|
14
|
+
meta = await client.put(
|
|
15
|
+
"async-demo.txt", b"async hello!", content_type="text/plain"
|
|
16
|
+
)
|
|
17
|
+
print(f"Uploaded {meta.key} (version={meta.version})")
|
|
18
|
+
|
|
19
|
+
# Read
|
|
20
|
+
data = await client.get("async-demo.txt")
|
|
21
|
+
print(f"Contents: {data.decode()}")
|
|
22
|
+
|
|
23
|
+
# List
|
|
24
|
+
async for obj in client.list("async-"):
|
|
25
|
+
print(f" {obj.key} ({obj.size} bytes)")
|
|
26
|
+
|
|
27
|
+
# Cleanup
|
|
28
|
+
await client.delete("async-demo.txt")
|
|
29
|
+
print("Deleted async-demo.txt")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Basic CRUD operations: put, get, list, delete.
|
|
2
|
+
|
|
3
|
+
Requires RIFF_TOKEN environment variable to be set.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from riffsdk.storage import StorageClient
|
|
7
|
+
|
|
8
|
+
client = StorageClient()
|
|
9
|
+
|
|
10
|
+
# Upload bytes
|
|
11
|
+
meta = client.put("hello.txt", b"Hello, world!", content_type="text/plain")
|
|
12
|
+
print(f"Uploaded {meta.key} (version={meta.version}, size={meta.size})")
|
|
13
|
+
|
|
14
|
+
# Read it back
|
|
15
|
+
data = client.get("hello.txt")
|
|
16
|
+
print(f"Contents: {data.decode()}")
|
|
17
|
+
|
|
18
|
+
# Check metadata
|
|
19
|
+
meta = client.stat("hello.txt")
|
|
20
|
+
print(f"Content-Type: {meta.content_type}, Updated: {meta.updated_at}")
|
|
21
|
+
|
|
22
|
+
# Check existence
|
|
23
|
+
print(f"Exists: {client.exists('hello.txt')}")
|
|
24
|
+
print(f"Missing: {client.exists('no-such-key.txt')}")
|
|
25
|
+
|
|
26
|
+
# List objects with a prefix
|
|
27
|
+
for obj in client.list("hello"):
|
|
28
|
+
print(f" {obj.key} ({obj.size} bytes)")
|
|
29
|
+
|
|
30
|
+
# Delete
|
|
31
|
+
client.delete("hello.txt")
|
|
32
|
+
print("Deleted hello.txt")
|
|
33
|
+
|
|
34
|
+
client.close()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Upload and download files with progress reporting.
|
|
2
|
+
|
|
3
|
+
Requires RIFF_TOKEN environment variable to be set.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from riffsdk.storage import StorageClient
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def show_progress(bytes_done: int, total: int) -> None:
|
|
13
|
+
pct = bytes_done * 100 // total
|
|
14
|
+
print(f"\r {pct}% ({bytes_done}/{total} bytes)", end="", flush=True)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
with StorageClient() as client:
|
|
18
|
+
# Create a sample file
|
|
19
|
+
src = Path(tempfile.mkstemp(suffix=".bin")[1])
|
|
20
|
+
src.write_bytes(b"x" * 1_000_000)
|
|
21
|
+
|
|
22
|
+
# Upload with progress
|
|
23
|
+
print("Uploading...")
|
|
24
|
+
result = client.upload_file(
|
|
25
|
+
"example/large.bin",
|
|
26
|
+
src,
|
|
27
|
+
on_progress=show_progress,
|
|
28
|
+
)
|
|
29
|
+
print(f"\nUploaded {result.key} (version={result.version})")
|
|
30
|
+
|
|
31
|
+
# Download with progress
|
|
32
|
+
dst = Path(tempfile.mkstemp(suffix=".bin")[1])
|
|
33
|
+
print("Downloading...")
|
|
34
|
+
dl = client.download_file(
|
|
35
|
+
"example/large.bin",
|
|
36
|
+
dst,
|
|
37
|
+
on_progress=show_progress,
|
|
38
|
+
)
|
|
39
|
+
print(f"\nDownloaded {dl.key} ({dl.size} bytes)")
|
|
40
|
+
|
|
41
|
+
# Verify
|
|
42
|
+
assert src.read_bytes() == dst.read_bytes()
|
|
43
|
+
print("Content verified!")
|
|
44
|
+
|
|
45
|
+
# Cleanup
|
|
46
|
+
client.delete("example/large.bin")
|
|
47
|
+
src.unlink()
|
|
48
|
+
dst.unlink()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Optimistic concurrency: conditional writes, atomic update, and leases.
|
|
2
|
+
|
|
3
|
+
Requires RIFF_TOKEN environment variable to be set.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
from riffsdk.storage import StorageClient
|
|
9
|
+
from riffsdk.storage.exceptions import AlreadyExistsError, VersionConflictError
|
|
10
|
+
|
|
11
|
+
with StorageClient() as client:
|
|
12
|
+
# -- Create-if-not-exists --
|
|
13
|
+
meta = client.put("config.json", b'{"count": 0}', if_not_exists=True)
|
|
14
|
+
print(f"Created config.json (version={meta.version})")
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
client.put("config.json", b'{"count": 0}', if_not_exists=True)
|
|
18
|
+
except AlreadyExistsError:
|
|
19
|
+
print("Already exists, as expected")
|
|
20
|
+
|
|
21
|
+
# -- Conditional update (compare-and-swap) --
|
|
22
|
+
data, version = client.get_with_version("config.json")
|
|
23
|
+
config = json.loads(data)
|
|
24
|
+
config["count"] += 1
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
meta = client.put(
|
|
28
|
+
"config.json",
|
|
29
|
+
json.dumps(config).encode(),
|
|
30
|
+
if_version_matches=version,
|
|
31
|
+
)
|
|
32
|
+
print(f"Updated to version {meta.version}")
|
|
33
|
+
except VersionConflictError:
|
|
34
|
+
print("Someone else modified it first!")
|
|
35
|
+
|
|
36
|
+
# -- Atomic read-modify-write with automatic retries --
|
|
37
|
+
def increment(data: bytes) -> bytes:
|
|
38
|
+
config = json.loads(data)
|
|
39
|
+
config["count"] += 1
|
|
40
|
+
return json.dumps(config).encode()
|
|
41
|
+
|
|
42
|
+
meta = client.update("config.json", increment)
|
|
43
|
+
print(f"After atomic update: version={meta.version}")
|
|
44
|
+
|
|
45
|
+
# -- Lease for exclusive access --
|
|
46
|
+
with client.lease("config.json", ttl=30) as lease:
|
|
47
|
+
print(f"Acquired lease (expires {lease.expires_at})")
|
|
48
|
+
data = client.get("config.json")
|
|
49
|
+
config = json.loads(data)
|
|
50
|
+
config["count"] += 10
|
|
51
|
+
client.put("config.json", json.dumps(config).encode())
|
|
52
|
+
print("Lease released")
|
|
53
|
+
|
|
54
|
+
# Cleanup
|
|
55
|
+
client.delete("config.json")
|
riffsdk-0.1.0/mise.toml
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[tools]
|
|
2
|
+
python = "3.13"
|
|
3
|
+
uv = "latest"
|
|
4
|
+
|
|
5
|
+
[tasks.test]
|
|
6
|
+
description = "Run tests"
|
|
7
|
+
run = "uv run pytest"
|
|
8
|
+
|
|
9
|
+
[tasks.format]
|
|
10
|
+
description = "Format code"
|
|
11
|
+
run = "uv run ruff format src tests"
|
|
12
|
+
|
|
13
|
+
[tasks.lint]
|
|
14
|
+
description = "Lint code"
|
|
15
|
+
run = "uv run ruff check src tests"
|
|
16
|
+
|
|
17
|
+
[tasks."lint:fix"]
|
|
18
|
+
description = "Lint and auto-fix"
|
|
19
|
+
run = "uv run ruff check --fix src tests"
|
|
20
|
+
|
|
21
|
+
[tasks."test:integration"]
|
|
22
|
+
description = "Run integration tests against local riff-api"
|
|
23
|
+
env = { RIFF_BASE_URL = "http://localhost:8080/riff-api", RIFF_INTEGRATION_TEST = "true" }
|
|
24
|
+
run = "uv run pytest tests/test_integration.py -v"
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "riffsdk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python SDK for the Riff Storage API"
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
dependencies = ["httpx>=0.27", "pydantic>=2.6", "tenacity>=8.3"]
|
|
11
|
+
|
|
12
|
+
[dependency-groups]
|
|
13
|
+
dev = [
|
|
14
|
+
"pytest>=9.0",
|
|
15
|
+
"pytest-asyncio>=0.25",
|
|
16
|
+
"python-semantic-release>=10.5.3",
|
|
17
|
+
"ruff>=0.15",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[tool.pytest.ini_options]
|
|
21
|
+
testpaths = ["tests"]
|
|
22
|
+
pythonpath = ["src"]
|
|
23
|
+
|
|
24
|
+
[tool.ruff]
|
|
25
|
+
src = ["src"]
|
|
26
|
+
target-version = "py311"
|
|
27
|
+
|
|
28
|
+
[tool.ruff.lint]
|
|
29
|
+
select = ["E", "F", "I", "UP"]
|
|
30
|
+
|
|
31
|
+
[tool.semantic_release]
|
|
32
|
+
commit_parser = "conventional"
|
|
33
|
+
version_toml = ["pyproject.toml:project.version"]
|
|
34
|
+
allow_zero_version = true
|
|
35
|
+
major_on_zero = false
|
|
36
|
+
build_command = """
|
|
37
|
+
uv lock --upgrade-package riffsdk
|
|
38
|
+
git add uv.lock
|
|
39
|
+
uv build
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
[tool.semantic_release.changelog.default_templates]
|
|
43
|
+
changelog_file = "CHANGELOG.md"
|
|
44
|
+
|
|
45
|
+
[tool.semantic_release.remote]
|
|
46
|
+
type = "github"
|
|
47
|
+
|
|
48
|
+
[tool.semantic_release.remote.token]
|
|
49
|
+
env = "GH_TOKEN"
|
|
50
|
+
|
|
51
|
+
[tool.semantic_release.publish]
|
|
52
|
+
dist_glob_patterns = ["dist/*"]
|
|
53
|
+
upload_to_vcs_release = true
|
|
File without changes
|
|
File without changes
|