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.
Files changed (43) hide show
  1. riffsdk-0.1.0/.github/workflows/ci.yml +27 -0
  2. riffsdk-0.1.0/.github/workflows/pr-comment.yml +31 -0
  3. riffsdk-0.1.0/.github/workflows/release.yml +95 -0
  4. riffsdk-0.1.0/.gitignore +6 -0
  5. riffsdk-0.1.0/.python-version +1 -0
  6. riffsdk-0.1.0/AGENTS.md +80 -0
  7. riffsdk-0.1.0/CHANGELOG.md +7 -0
  8. riffsdk-0.1.0/CLAUDE.md +1 -0
  9. riffsdk-0.1.0/LICENCE +7 -0
  10. riffsdk-0.1.0/PKG-INFO +9 -0
  11. riffsdk-0.1.0/README.md +113 -0
  12. riffsdk-0.1.0/examples/async_client.py +32 -0
  13. riffsdk-0.1.0/examples/basic_crud.py +34 -0
  14. riffsdk-0.1.0/examples/file_upload_download.py +48 -0
  15. riffsdk-0.1.0/examples/optimistic_concurrency.py +55 -0
  16. riffsdk-0.1.0/mise.toml +24 -0
  17. riffsdk-0.1.0/pyproject.toml +53 -0
  18. riffsdk-0.1.0/src/riffsdk/__init__.py +0 -0
  19. riffsdk-0.1.0/src/riffsdk/_internal/__init__.py +0 -0
  20. riffsdk-0.1.0/src/riffsdk/_internal/auth.py +97 -0
  21. riffsdk-0.1.0/src/riffsdk/_internal/config.py +10 -0
  22. riffsdk-0.1.0/src/riffsdk/storage/__init__.py +59 -0
  23. riffsdk-0.1.0/src/riffsdk/storage/_async_client.py +555 -0
  24. riffsdk-0.1.0/src/riffsdk/storage/_client.py +586 -0
  25. riffsdk-0.1.0/src/riffsdk/storage/_http.py +351 -0
  26. riffsdk-0.1.0/src/riffsdk/storage/_models.py +68 -0
  27. riffsdk-0.1.0/src/riffsdk/storage/_session.py +33 -0
  28. riffsdk-0.1.0/src/riffsdk/storage/_streaming.py +177 -0
  29. riffsdk-0.1.0/src/riffsdk/storage/_upload.py +229 -0
  30. riffsdk-0.1.0/src/riffsdk/storage/exceptions.py +87 -0
  31. riffsdk-0.1.0/src/riffsdk/storage/py.typed +0 -0
  32. riffsdk-0.1.0/tests/__init__.py +0 -0
  33. riffsdk-0.1.0/tests/test_async_client.py +644 -0
  34. riffsdk-0.1.0/tests/test_client.py +901 -0
  35. riffsdk-0.1.0/tests/test_exceptions.py +89 -0
  36. riffsdk-0.1.0/tests/test_http.py +302 -0
  37. riffsdk-0.1.0/tests/test_imports.py +59 -0
  38. riffsdk-0.1.0/tests/test_integration.py +861 -0
  39. riffsdk-0.1.0/tests/test_models.py +114 -0
  40. riffsdk-0.1.0/tests/test_session.py +69 -0
  41. riffsdk-0.1.0/tests/test_streaming.py +169 -0
  42. riffsdk-0.1.0/tests/test_upload.py +344 -0
  43. 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
@@ -0,0 +1,6 @@
1
+ dist/
2
+ *.egg-info/
3
+ __pycache__/
4
+ *.pyc
5
+ .pytest_cache/
6
+ .ruff_cache/
@@ -0,0 +1 @@
1
+ 3.11
@@ -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.
@@ -0,0 +1,7 @@
1
+ # CHANGELOG
2
+
3
+ <!-- version list -->
4
+
5
+ ## v0.1.0 (2026-06-01)
6
+
7
+ - Initial Release
@@ -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
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: riffsdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK for the Riff Storage API
5
+ License-File: LICENCE
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: httpx>=0.27
8
+ Requires-Dist: pydantic>=2.6
9
+ Requires-Dist: tenacity>=8.3
@@ -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")
@@ -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