Metadata-Version: 2.5
Name: spacefs-s3sdk
Version: 0.3.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")
c.create_drive("rushes", placement="weur")                     # a drive for a team in Europe, made from anywhere
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)`.

## Placement

A drive's authority lives in one region, by default near whoever creates it. Every write, and the lookup at the start of every read, goes to it, so both are fastest next to it; the bytes of a read come from the location nearest the reader when they are already there. Say where when the writers are somewhere else:

```python
from s3sdk import PLACEMENTS

c.create_drive("rushes", placement="weur")
```

`PLACEMENTS` lists the hints: `wnam`, `enam`, `sam`, `weur`, `eeur`, `apac`, `oc`, `afr`, `me`. The hint is fixed at creation and does not apply to forks. One the service does not know is a `400 InvalidArgument`.

## 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
```
