A python adapter for interacting with Ceph RADOS Gateway (S3) object storage
Project description
ceph-adapter
A small, friendly Python wrapper around boto3 for administering and using a Ceph RADOS Gateway (RGW) over its S3-compatible API.
Ceph RGW implements S3, but it has quirks that plain boto3 makes awkward — most
notably tenants (buckets addressed as tenant:bucket, whose : boto3
rejects by default) and the fact that policies attach to buckets, not users.
ceph-adapter smooths those over with two classes:
CephAdapter— bucket lifecycle, object upload/download (file, path, or stream), presigned URLs, an fsspec handle, and policy management.CephPolicyBuilder— a fluent builder that produces valid S3 bucket policy documents (per-user grants, public read-only, custom statements).
The docstrings on
CephAdapterandCephPolicyBuilderare the authoritative per-method reference (parameters, tenant behaviour, and which exceptions each method raises). This README is the tour;help(CephAdapter)is the manual.
Installation
pip install ceph-adapter
Or with Poetry:
poetry add ceph-adapter
Requires Python 3.10+.
Quick start
from ceph_adapter import CephAdapter
ceph = CephAdapter(
url="https://rgw.example.com",
access_key="ACCESS_KEY",
secret_key="SECRET_KEY", # str or pydantic SecretStr; stored as SecretStr
)
# List buckets visible to these credentials
print(ceph.list_buckets())
# Upload / download a single object
ceph.upload_file(bucket_name="reports", file_path="./q3.pdf", path_in_bucket="2025/")
ceph.download_file(bucket_name="reports", file_name="2025/q3.pdf", path="./q3.pdf")
# Presigned URL for temporary sharing (default: GET, 1 hour)
url = ceph.get_signed_url("reports", "2025/q3.pdf", expiration_time=600)
Credentials are always wrapped in a pydantic.SecretStr, so they won't leak
into logs or reprs. Use ceph.get_credentials() when you need the raw values.
Tenants
Ceph namespaces buckets per tenant and addresses a tenanted bucket as
"tenant:bucket". Every data and policy method therefore accepts an optional
tenant_name:
# Address a bucket that lives under the "acme" tenant
ceph.list_bucket_files(bucket_name="reports", tenant_name="acme")
ceph.upload_file("reports", "./q3.pdf", tenant_name="acme")
- When
tenant_nameis given, the adapter joins it to the bucket name (acme:reports) before the request. - When it's omitted, the bare bucket name is used and the request resolves against the tenant of the authenticated credentials.
Creating a bucket is the one exception: the S3 API cannot create a bucket
inside an arbitrary tenant, so create_user_bucket always creates in the
authenticated tenant, and there tenant_name only feeds the generated grant
policy's principal ARN.
get_tenant() is a best-effort helper that infers the current tenant name from
existing buckets; treat it as experimental.
Buckets and users
# Create a private bucket and grant a user full access in one call
ceph.create_user_bucket(bucket_name="reports", user_name="alice", tenant_name="acme")
ceph.bucket_exists("reports", tenant_name="acme") # -> bool (HEAD-based)
ceph.delete_user_bucket("reports", tenant_name="acme") # bucket must be empty
create_user_bucket creates the bucket, sets its ACL to private, and attaches
a full-privileges (ListBucket/GetObject/PutObject/DeleteObject) policy
whose principal is arn:aws:iam::acme:user/alice.
Objects
| Method | Purpose |
|---|---|
upload_file(bucket, file_path, path_in_bucket, tenant_name) |
Upload one local file |
upload_path(bucket, path, path_in_bucket, recursive) |
Upload every file in a directory |
upload_stream(bucket, stream, file_name, tenant_name) |
Upload from a file-like/bytes stream |
download_file(bucket, file_name, path, tenant_name) |
Download to a local path |
download_stream(bucket, file_name, tenant_name) |
Return a streaming body |
delete_file(bucket, file_name, tenant_name) |
Delete an object |
list_bucket_files(bucket, tenant_name, verbose) |
List keys (or full metadata when verbose=True) |
get_signed_url(bucket, object_key, method, expiration_time, tenant_name) |
Presigned URL |
get_fsspec(bucket) |
An fsspec S3 filesystem bound to these credentials |
Bucket policies
Ceph attaches policies to buckets, not users. CephPolicyBuilder builds the
policy document; CephAdapter.grant_policy_to_bucket attaches it.
import json
from ceph_adapter import CephPolicyBuilder
policy = (
CephPolicyBuilder("reports-access")
.add_users_read_privileges("reports", tenant_users=[("acme", "reader")])
.add_users_write_privileges("reports", tenant_users=[("acme", "writer")])
.build()
)
ceph.grant_policy_to_bucket(
bucket_name="reports",
policy_string=json.dumps(policy),
tenant_name="acme",
)
Builder highlights:
add_users_read_privileges/_write_/_delete_/_full_privileges— convenience grants for one or more(tenant, user)principals. Passtenant=Nonein the tuple for a user in the default (untenanted) namespace.add_entry(bucket, object_names, privileges, conditions, tenant_users, allow)— full control: choose actions, scope to specific object keys, add IAM conditions, or make aDenystatement.tenant_users="*"makes the statement public (principal*).- Calls chain and accumulate statements;
build()returns the policydict, andwrite_policy_file(name, path)serializes it to disk.
Convenience shortcut for public content:
# Keeps the bucket ACL private, publishes objects via a public GetObject policy
ceph.set_bucket_public_readonly_access("assets", object_names=["*"], tenant_name="acme")
# Inspect what is currently attached
print(ceph.describe_bucket_policy("assets", tenant_name="acme"))
Error handling
BucketError— raised by the bucket-lifecycle helpers (create_user_bucket,delete_user_bucket,bucket_exists) so you can catch bucket problems (already exists, missing, not empty, forbidden) without importing botocore. The underlying error is preserved as__cause__.botocore.exceptions.ClientError— surfaced directly by the object and policy methods (e.g.NoSuchBucketPolicyfromdescribe_bucket_policywhen a bucket has no policy). Each method's docstring notes its specific failure modes.
from ceph_adapter import BucketError
try:
ceph.create_user_bucket("reports", "alice", "acme")
except BucketError as err:
print("bucket op failed:", err)
Development
poetry install # install the package + dev tooling
poetry run pytest # run the test suite
poetry run ruff check . # lint
poetry run black . # format
Testing scope
The unit tests cover the parts that are genuinely our logic: the
CephPolicyBuilder policy construction and the CephAdapter boto3
customisations (tenant-name handler removal, path-style addressing, credential
wrapping) — none of which touch the network. The Ceph-specific S3 semantics
(tenant addressing, policy enforcement) can't be faithfully reproduced by a
generic S3 mock like moto, so they are intentionally left to integration testing
against a real RGW rather than tests that would only re-assert boto3's behaviour.
CI / release
- CI (
.github/workflows/ci.yml) runs ruff, black--check, and pytest on every push and pull request across Python 3.10–3.12. - Release (
.github/workflows/release.yml) publishes to PyPI via trusted publishing when aX.Y.Ztag is pushed, after re-running lint/tests and verifying the tag matches the version inpyproject.toml.
To cut a release: bump version in pyproject.toml, commit, then
git tag 0.2.0 && git push --tags.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ceph_adapter-0.2.0.tar.gz.
File metadata
- Download URL: ceph_adapter-0.2.0.tar.gz
- Upload date:
- Size: 11.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9677098e3125b8fb52af6b71e85224a5db7abc62825a6e5ada275ebba44d55cb
|
|
| MD5 |
0f66d62cfdb4bff0e3caa68ac76d29eb
|
|
| BLAKE2b-256 |
8687d86b5297a8b92ff7a4ef5de177cabf4adce7e957e9794c8bd4314358f5f0
|
Provenance
The following attestation bundles were made for ceph_adapter-0.2.0.tar.gz:
Publisher:
python-publish.yml on eodcgmbh/ceph
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ceph_adapter-0.2.0.tar.gz -
Subject digest:
9677098e3125b8fb52af6b71e85224a5db7abc62825a6e5ada275ebba44d55cb - Sigstore transparency entry: 2162799633
- Sigstore integration time:
-
Permalink:
eodcgmbh/ceph@ef64658566d9c2994529446ddcc7d589a93f09d5 -
Branch / Tag:
refs/tags/0.2.0 - Owner: https://github.com/eodcgmbh
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@ef64658566d9c2994529446ddcc7d589a93f09d5 -
Trigger Event:
push
-
Statement type:
File details
Details for the file ceph_adapter-0.2.0-py3-none-any.whl.
File metadata
- Download URL: ceph_adapter-0.2.0-py3-none-any.whl
- Upload date:
- Size: 10.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
057f7bf9eb2e407c91053d1d448a58d1f5d4768155fe0d6d152d9a2ca7f75a08
|
|
| MD5 |
c4190faabf46c28919529b7aada22eb4
|
|
| BLAKE2b-256 |
7ab1c3bbfcbe8815578769bbf12084cbd87f7cc575c74cf84c26b51f9b2f52e6
|
Provenance
The following attestation bundles were made for ceph_adapter-0.2.0-py3-none-any.whl:
Publisher:
python-publish.yml on eodcgmbh/ceph
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ceph_adapter-0.2.0-py3-none-any.whl -
Subject digest:
057f7bf9eb2e407c91053d1d448a58d1f5d4768155fe0d6d152d9a2ca7f75a08 - Sigstore transparency entry: 2162799682
- Sigstore integration time:
-
Permalink:
eodcgmbh/ceph@ef64658566d9c2994529446ddcc7d589a93f09d5 -
Branch / Tag:
refs/tags/0.2.0 - Owner: https://github.com/eodcgmbh
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@ef64658566d9c2994529446ddcc7d589a93f09d5 -
Trigger Event:
push
-
Statement type: