Metadata-Version: 2.5
Name: spacefs-s3sdk
Version: 0.2.0
Summary: s3sdk SDK: S3-compatible access to Space drives plus partial writes, copy-on-write forks, and version history.
License-Expression: MIT
License-File: LICENSE
Requires-Python: >=3.10
Requires-Dist: boto3>=1.36
Provides-Extra: test
Requires-Dist: pytest; extra == 'test'
Description-Content-Type: text/markdown

# s3sdk — Python SDK

s3sdk is the S3 API over Space drives, plus extensions S3 can't express:
writes at an offset, batched patches, splices (insert / remove bytes), renames, copy-on-write
drive forks, and per-object version history with restore. Wire protocol: [`../spec/PROTOCOL.md`](../spec/PROTOCOL.md).

## Install

```sh
pip install spacefs-s3sdk        # Python >= 3.10; pulls in boto3; import s3sdk
```

## Usage

```python
from s3sdk import S3SDKClient, S3SDKError, Edit

c = S3SDKClient("S3SDK...", "secret...")          # endpoint defaults to https://s3sdk.spacefs.com

c.create_drive("footage", display_name="Footage")
v1 = c.put_object("footage", "cuts/a.txt", "hello world")
c.write_at("footage", "cuts/a.txt", 6, b"WORLD")               # pwrite, one new version
c.patch("footage", "cuts/a.txt", [Edit(0, b"H"), Edit(11, b"!")])
c.truncate("footage", "cuts/a.txt", 5)
print(c.read_range("footage", "cuts/a.txt", 1, 3))            # b"ell"

# Splices shift the bytes after the offset (write_at overwrites them); size changes
c.insert("footage", "cuts/a.txt", 5, ", world")               # "Hello, world"
c.remove_range("footage", "cuts/a.txt", 0, 7)                  # "world"
c.splice("footage", "cuts/a.txt", 0, 5, b"WORLD!")            # replace 5 bytes with 6

# Rename / move: metadata-only, ETag and history unchanged; a folder moves its subtree
c.rename("footage", "cuts/a.txt", "final/a.txt")
c.rename("footage", "final/", "archive/final/")

# Optimistic concurrency
try:
    c.write_at("footage", "cuts/a.txt", 0, b"x", if_version=v1.version_id)
except S3SDKError as e:
    if e.status == 412:
        print("head is now", e.current_version_id)

# History
versions = c.list_versions("footage", "cuts/a.txt")           # oldest first
old = c.get_object("footage", "cuts/a.txt", version_id=versions[0].version_id)
then = c.get_object("footage", "cuts/a.txt", as_of=versions[0].last_modified_raw)
c.restore_version("footage", "cuts/a.txt", versions[0].version_id)

# Copy-on-write forks
fork = c.fork_drive("footage", "footage-experiment")
print(c.describe_drive("footage-experiment").fork_of)
c.delete_drive("footage-experiment", hard=True)

# Any standard S3 call: c.s3 is a boto3 client (path-style)
c.s3.head_object(Bucket="footage", Key="cuts/a.txt")
```

Every error — gateway extensions and boto3 calls alike — is raised as
`S3SDKError(status, code, message, current_version_id, request_id)`.

## Development

```sh
python3 -m venv .venv && .venv/bin/pip install -e '.[test]'
.venv/bin/python -m pytest tests

# Conformance suite against a gateway (see ../spec/conformance/README.md)
S3SDK_ENDPOINT=http://127.0.0.1:19000 \
S3SDK_ACCESS_KEY_ID=S3SDKCONFORMANCE0001 \
S3SDK_SECRET_ACCESS_KEY=conformance-secret-0000000000000000000000 \
  .venv/bin/python -m s3sdk.conformance ../spec/conformance/cases.json
```
