Skip to content

Commit 39fec24

Browse files
authored
Merge pull request #3 from ardriveapp/PE-8859-multipart-uploads
feat: add multipart upload support with progress callbacks
2 parents 47f881c + 85e966b commit 39fec24

18 files changed

Lines changed: 2844 additions & 60 deletions

.github/workflows/test-and-build.yml

Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ jobs:
6767
run: |
6868
python -m pip install --upgrade pip
6969
pip install -e ".[dev]"
70+
pip install "black==25.11.0"
7071
7172
- name: Run Black formatter check
7273
run: |
@@ -112,38 +113,3 @@ jobs:
112113
name: python-package
113114
path: dist/
114115
retention-days: 7
115-
116-
security:
117-
name: Security Scan
118-
runs-on: ubuntu-latest
119-
120-
steps:
121-
- uses: actions/checkout@v4
122-
123-
- name: Set up Python 3.11
124-
uses: actions/setup-python@v5
125-
with:
126-
python-version: "3.11"
127-
128-
- name: Install dependencies
129-
run: |
130-
python -m pip install --upgrade pip
131-
pip install -e ".[dev]" safety bandit
132-
133-
- name: Run safety check for known vulnerabilities
134-
run: |
135-
safety check
136-
continue-on-error: true
137-
138-
- name: Run bandit security linter
139-
run: |
140-
bandit -r turbo_sdk/ -f json -o bandit-report.json
141-
continue-on-error: true
142-
143-
- name: Upload security report
144-
if: always()
145-
uses: actions/upload-artifact@v4
146-
with:
147-
name: security-reports
148-
path: bandit-report.json
149-
retention-days: 7

README.md

Lines changed: 155 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,21 +61,45 @@ print(f"✅ Uploaded! URI: ar://{result.id}")
6161

6262
### Core Classes
6363

64-
#### `Turbo(signer, network="mainnet")`
64+
#### `Turbo(signer, network="mainnet", upload_url=None, payment_url=None)`
6565

6666
Main client for interacting with Turbo services.
6767

6868
**Parameters:**
69+
6970
- `signer`: Either `EthereumSigner` or `ArweaveSigner` instance
7071
- `network`: `"mainnet"` or `"testnet"` (default: `"mainnet"`)
72+
- `upload_url`: Optional custom upload service URL (overrides network default)
73+
- `payment_url`: Optional custom payment service URL (overrides network default)
74+
75+
```python
76+
# Using default URLs (mainnet)
77+
turbo = Turbo(signer)
78+
79+
# Using testnet
80+
turbo = Turbo(signer, network="testnet")
81+
82+
# Using custom URLs
83+
turbo = Turbo(signer, upload_url="https://my-upload-service.example.com")
84+
```
7185

7286
**Methods:**
7387

74-
##### `upload(data, tags=None) -> TurboUploadResponse`
88+
##### `upload(data=None, tags=None, on_progress=None, chunking=None, data_size=None, stream_factory=None) -> TurboUploadResponse`
89+
90+
Upload data to the Turbo datachain. Supports both small files (single request) and large files (chunked multipart upload).
91+
92+
**Parameters:**
7593

76-
Upload data to the Turbo datachain.
94+
- `data`: Data to upload (`bytes` or file-like `BinaryIO` object)
95+
- `tags`: Optional list of metadata tags
96+
- `on_progress`: Optional callback `(processed_bytes, total_bytes) -> None`
97+
- `chunking`: Optional `ChunkingParams` for upload configuration
98+
- `data_size`: Required when `data` is a file-like object or when using `stream_factory`
99+
- `stream_factory`: Optional callable that returns a fresh `BinaryIO` stream each time it's called. Use this for non-seekable streams or when you want to avoid loading the entire file into memory.
77100

78101
```python
102+
# Simple upload
79103
result = turbo.upload(
80104
data=b"Your data here",
81105
tags=[
@@ -86,6 +110,7 @@ result = turbo.upload(
86110
```
87111

88112
**Returns:** `TurboUploadResponse`
113+
89114
```python
90115
@dataclass
91116
class TurboUploadResponse:
@@ -96,6 +121,54 @@ class TurboUploadResponse:
96121
winc: str # Winston credits cost
97122
```
98123

124+
##### Large File Uploads with Progress
125+
126+
For files >= 5 MiB, the SDK automatically uses chunked multipart uploads. Use `stream_factory` to avoid loading the entire file into memory. A factory is needed because the stream is consumed twice — once for signing and once for uploading — so the SDK calls it each time to get a fresh stream.
127+
128+
```python
129+
import os
130+
131+
def on_progress(processed: int, total: int):
132+
pct = (processed / total) * 100
133+
print(f"Upload progress: {pct:.1f}%")
134+
135+
file_path = "large-video.mp4"
136+
137+
result = turbo.upload(
138+
stream_factory=lambda: open(file_path, "rb"),
139+
data_size=os.path.getsize(file_path),
140+
tags=[{"name": "Content-Type", "value": "video/mp4"}],
141+
on_progress=on_progress,
142+
)
143+
```
144+
145+
##### Chunking Configuration
146+
147+
Use `ChunkingParams` to customize chunked upload behavior:
148+
149+
```python
150+
from turbo_sdk import ChunkingParams
151+
152+
result = turbo.upload(
153+
data=large_data,
154+
chunking=ChunkingParams(
155+
chunk_size=10 * 1024 * 1024, # 10 MiB chunks (default: 5 MiB)
156+
max_chunk_concurrency=3, # Parallel chunk uploads (default: 1)
157+
chunking_mode="auto", # "auto", "force", or "disabled"
158+
),
159+
on_progress=lambda p, t: print(f"{p}/{t} bytes"),
160+
)
161+
```
162+
163+
**ChunkingParams options:**
164+
165+
- `chunk_size`: Chunk size in bytes (5-500 MiB, default: 5 MiB)
166+
- `max_chunk_concurrency`: Number of parallel chunk uploads (default: 1)
167+
- `chunking_mode`:
168+
- `"auto"` (default): Use chunked upload for files >= 5 MiB
169+
- `"force"`: Always use chunked upload
170+
- `"disabled"`: Always use single request upload
171+
99172
##### `get_balance(address=None) -> TurboBalanceResponse`
100173

101174
Get winston credit balance. Uses signed request for authenticated balance check when no address specified.
@@ -111,6 +184,7 @@ print(f"Other balance: {other_balance.winc} winc")
111184
```
112185

113186
**Returns:** `TurboBalanceResponse`
187+
114188
```python
115189
@dataclass
116190
class TurboBalanceResponse:
@@ -135,6 +209,7 @@ print(f"Upload cost: {cost} winc")
135209
Ethereum signer using ECDSA signatures.
136210

137211
**Parameters:**
212+
138213
- `private_key` (str): Hex private key with or without `0x` prefix
139214

140215
```python
@@ -146,6 +221,7 @@ signer = EthereumSigner("0x1234567890abcdef...")
146221
Arweave signer using RSA-PSS signatures.
147222

148223
**Parameters:**
224+
149225
- `jwk` (dict): Arweave wallet in JWK format
150226

151227
```python
@@ -179,31 +255,102 @@ Create signed headers for authenticated API requests.
179255
headers = signer.create_signed_headers()
180256
```
181257

258+
### Exceptions
259+
260+
The SDK provides specific exceptions for error handling:
261+
262+
```python
263+
from turbo_sdk import UnderfundedError, ChunkedUploadError
264+
265+
try:
266+
result = turbo.upload(large_data)
267+
except UnderfundedError:
268+
print("Insufficient balance - please top up your account")
269+
except ChunkedUploadError as e:
270+
print(f"Upload failed: {e}")
271+
```
272+
273+
**Exception types:**
274+
275+
- `ChunkedUploadError`: Base exception for chunked upload failures
276+
- `UnderfundedError`: Account has insufficient balance (HTTP 402)
277+
- `UploadValidationError`: Upload validation failed (INVALID status)
278+
- `UploadFinalizationError`: Finalization timed out or failed
182279

183280
## Developers
184281

185282
### Setup
186283

187-
1. **Crete a virtual environment:**
284+
1. **Create a virtual environment:**
188285

189286
```bash
190287
python -m venv venv
191-
source venv/bin/activate
288+
source venv/bin/activate
192289
```
193290

194-
1. **Install dependencies:**
291+
2. **Install dependencies:**
195292

196293
```bash
197294
pip install -e ".[dev]"
198295
```
199296

200-
2. **Run tests:**
297+
3. **Run tests:**
201298

202299
```bash
203300
pytest
204301
```
205302

206-
That's it! The test suite includes comprehensive tests for all components without requiring network access.
303+
With coverage
304+
305+
```bash
306+
pytest --cov=turbo_sdk
307+
```
308+
309+
4. **Lint and format:**
310+
311+
```bash
312+
black turbo_sdk tests
313+
flake8 turbo_sdk tests
314+
```
315+
316+
5. **Run performance benchmarks** (requires funded wallet):
317+
318+
```bash
319+
export TURBO_TEST_WALLET=/path/to/wallet.json
320+
export TURBO_UPLOAD_URL=https://upload.ardrive.dev # optional, defaults to testnet
321+
pytest -m performance -v -s
322+
```
323+
324+
The test suite includes comprehensive unit tests for all components. Performance tests measure real upload throughput against the Turbo service.
325+
326+
## Publishing
327+
328+
Releases are published to PyPI via the GitHub Actions workflow at `.github/workflows/release.yml`. It runs on `release` events or can be triggered manually via `workflow_dispatch`.
329+
330+
There is no automated versioning. Before publishing, update the `version` field in `pyproject.toml` to reflect the new release:
331+
332+
```toml
333+
[project]
334+
version = "0.0.5"
335+
```
336+
337+
Steps to release:
338+
339+
1. Merge feature branches into `alpha`.
340+
2. Review the commits and update the `version` field in `pyproject.toml` accordingly.
341+
3. Push to the `alpha` branch.
342+
4. Manually run the release workflow at `.github/workflows/release.yml` via `workflow_dispatch`.
343+
344+
The workflow runs tests across Python 3.8-3.12, builds the package, and publishes to PyPI using trusted OIDC publishing.
345+
346+
To publish locally instead:
347+
348+
```bash
349+
pip install build twine
350+
python -m build
351+
twine check dist/*
352+
twine upload dist/*
353+
```
207354

208355
## Acknowledgments
209356

examples/arweave_upload.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"""
33
Example: Upload data using Arweave JWK wallet
44
"""
5+
56
from turbo_sdk import Turbo, ArweaveSigner
67
import json
78
import sys

examples/ethereum_upload.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"""
33
Example: Upload data using Ethereum private key
44
"""
5+
56
from turbo_sdk import Turbo, EthereumSigner
67

78

examples/test_wallet_integration.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"""
33
Test integration with real Arweave wallet (without network calls)
44
"""
5+
56
import json
67
from pathlib import Path
78
from turbo_sdk import Turbo, ArweaveSigner

pyproject.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ line-length = 100
5858
target-version = ["py38", "py39", "py310", "py311", "py312"]
5959

6060
[tool.mypy]
61-
python_version = "3.8"
61+
python_version = "3.9"
6262
warn_return_any = true
6363
warn_unused_configs = true
6464
disallow_untyped_defs = false
@@ -79,7 +79,10 @@ testpaths = ["tests"]
7979
python_files = ["test_*.py"]
8080
python_classes = ["Test*"]
8181
python_functions = ["test_*"]
82-
addopts = "-v --tb=short"
82+
addopts = "-v --tb=short -m 'not performance'"
83+
markers = [
84+
"performance: marks tests as performance benchmarks (may be slow and consume credits)"
85+
]
8386
filterwarnings = [
8487
"ignore::DeprecationWarning",
8588
"ignore::PendingDeprecationWarning"

0 commit comments

Comments
 (0)