Skip to main content

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 CephAdapter and CephPolicyBuilder are 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_files(bucket_name="reports", tenant_name="acme")
ceph.upload_file("reports", "./q3.pdf", tenant_name="acme")
  • When tenant_name is 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_bucket always creates in the authenticated tenant, and there tenant_name only feeds the generated grant policy's principal ARN.

get_tenant() returns the authenticated user's own tenant (or "" if untenanted), read from the Owner of the ListBuckets response — so it needs neither an existing bucket nor any objects.

Buckets and users

# Create a private bucket and grant a user full access in one call
ceph.create_bucket(bucket_name="reports", grant_user="alice", tenant_name="acme")

ceph.bucket_exists("reports", tenant_name="acme")   # -> bool (HEAD-based)
ceph.delete_bucket("reports", tenant_name="acme")  # bucket must be empty

create_bucket creates the bucket, sets its ACL to private, and — when grant_user is given — attaches a full-privileges (ListBucket/GetObject/PutObject/DeleteObject) policy whose principal is arn:aws:iam::acme:user/alice. Omit grant_user to just create a private bucket.

describe_bucket("reports", tenant_name="acme") returns a small summary — {"url", "name", "resolved", "owner", "object_count", "bytes_used", "empty"}. It's handy for a quick sanity check (seeing url next to empty makes a wrong-endpoint mix-up obvious), and object_count / bytes_used come straight from Ceph's x-rgw-* HEAD headers — an O(1) way to see how much data a bucket holds, no object listing required:

info = ceph.describe_bucket("coverage-maps")
print(info["object_count"], info["bytes_used"])   # 260  1342013096

# Pass unit= to get the size in KB/MB/GB/TB (1000-based) or KiB/MiB/GiB/...
# (1024-based); bytes_used is always kept raw alongside.
ceph.describe_bucket("coverage-maps", unit="GB")["size"]    # 1.342013096
ceph.describe_bucket("coverage-maps", unit="GiB")["size"]   # 1.2498470917...

(object_count / bytes_used are None on gateways that don't send those headers, e.g. AWS or older RGW.) The same unit= argument works on stat_file, and CephAdapter.convert_bytes(n, "GB") is exposed as a standalone helper.

Quotas (max size)

get_quota(bucket, tenant_name=None, unit="B") reports the limits Ceph applies, read from the same HEAD headers (-1/unset → None, meaning "no limit"):

ceph.get_quota("coverage-maps", unit="GiB")
# {'bucket_max_bytes': None,          # no per-bucket size cap
#  'bucket_max_objects': None,
#  'user_max_bytes': 1073741824000,   # shared across the user's buckets
#  'user_max_objects': None,
#  'max_buckets': 1000,
#  'bucket_max': None,                # bucket_max_bytes in `unit`
#  'user_max': 1000.0,                # user_max_bytes in `unit` (GiB)
#  'unit': 'GiB'}

There are two independent quotas: a per-bucket cap and a per-user cap that the user's buckets share. The effective ceiling for one bucket is the per-bucket quota when set, otherwise it's bounded by the shared user quota. The raw *_max_bytes values are always kept; bucket_max / user_max are those converted to unit.

Quotas (max size)

get_quota(bucket, tenant_name=None) reports the limits Ceph applies, read from the same HEAD headers (-1/unset → None, meaning "no limit"):

ceph.get_quota("coverage-maps")
# {'bucket_max_bytes': None,          # no per-bucket size cap
#  'bucket_max_objects': None,
#  'user_max_bytes': 1073741824000,   # 1000 GiB, shared across the user's buckets
#  'user_max_objects': None,
#  'max_buckets': 1000}

There are two independent quotas: a per-bucket cap and a per-user cap that the user's buckets share. The effective ceiling for one bucket is the per-bucket quota when set, otherwise it's bounded by the shared user quota. Byte values are raw — format with convert_bytes (e.g. ceph.convert_bytes(q["user_max_bytes"], "GiB")1000.0).

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_files(bucket, tenant_name, verbose) List keys, paginated; verbose=True returns full metadata dicts (Size, LastModified, ETag, …)
stat_file(bucket, file_name, tenant_name, unit) Metadata for one object without downloading it: {bytes, size, unit, last_modified, content_type, etag, metadata}
get_signed_url(bucket, file_name, method, expiration_time, tenant_name) Presigned URL
get_filesystem(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. Pass tenant=None in 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 a Deny statement. tenant_users="*" makes the statement public (principal *).
  • Calls chain and accumulate statements; build() returns the policy dict, and write_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("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_bucket, delete_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. NoSuchBucketPolicy from describe_bucket_policy when a bucket has no policy). Each method's docstring notes its specific failure modes.
from ceph_adapter import BucketError

try:
    ceph.create_bucket("reports", grant_user="alice", tenant_name="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 a X.Y.Z tag is pushed, after re-running lint/tests and verifying the tag matches the version in pyproject.toml.

To cut a release: bump version in pyproject.toml, commit, then git tag 0.2.0 && git push --tags.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ceph_adapter-1.1.0.tar.gz (15.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ceph_adapter-1.1.0-py3-none-any.whl (13.3 kB view details)

Uploaded Python 3

File details

Details for the file ceph_adapter-1.1.0.tar.gz.

File metadata

  • Download URL: ceph_adapter-1.1.0.tar.gz
  • Upload date:
  • Size: 15.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for ceph_adapter-1.1.0.tar.gz
Algorithm Hash digest
SHA256 34d45797f3aa3b171300120ff68f409562cd09f6c95fd4a3ea1748b2fc191fdd
MD5 2c3ed2c1a5e1f14f276d506661fe4809
BLAKE2b-256 3dd3f8529ad0524fb23935722deed430f2e5643b1d59b3e3f31122fdf96823e4

See more details on using hashes here.

Provenance

The following attestation bundles were made for ceph_adapter-1.1.0.tar.gz:

Publisher: release.yml on eodcgmbh/ceph

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file ceph_adapter-1.1.0-py3-none-any.whl.

File metadata

  • Download URL: ceph_adapter-1.1.0-py3-none-any.whl
  • Upload date:
  • Size: 13.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for ceph_adapter-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 20f8bfa58e8e36db59027e6262d39dfc490463da1b32d34ff4a2c8a043e9b8e4
MD5 26f1531dfc3dae57059658d30ac467d1
BLAKE2b-256 cf0fd0d6e2b022cbf0405ac9f0f594d53500e2913e78102016a4ed35cb47bf51

See more details on using hashes here.

Provenance

The following attestation bundles were made for ceph_adapter-1.1.0-py3-none-any.whl:

Publisher: release.yml on eodcgmbh/ceph

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

1.1.0 This release

2 files

0.2.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page