The repository or configuration that created this release differ from a previous release. View details.
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_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_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. 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("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.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_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 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.
Metadata
Release files for ceph-adapter 1.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Change of provenance
The repository or configuration that created these release files differ from that of a previous release.
We recommend inspecting the repository and configuration before proceeding. View details.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| ceph_adapter-1.1.0.tar.gz | 15.6 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ceph_adapter-1.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 28.9 kB
Release files / ceph_adapter-1.1.0.tar.gz
Change of provenance
This file belongs to a release with changed provenance: the repository or configuration that created the release files differ from that of a previous release.
We recommend inspecting the repository and configuration before proceeding. View details.
| Download URL | ceph_adapter-1.1.0.tar.gz |
|---|---|
| Size | 15.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
34d45797f3aa3b171300120ff68f409562cd09f6c95fd4a3ea1748b2fc191fdd
|
|
BLAKE2b-256 checksum How to use checksums |
3dd3f8529ad0524fb23935722deed430f2e5643b1d59b3e3f31122fdf96823e4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 15, 2026.
Transparency logRelease files / ceph_adapter-1.1.0-py3-none-any.whl
Change of provenance
This file belongs to a release with changed provenance: the repository or configuration that created the release files differ from that of a previous release.
We recommend inspecting the repository and configuration before proceeding. View details.
| Download URL | ceph_adapter-1.1.0-py3-none-any.whl |
|---|---|
| Size | 13.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
20f8bfa58e8e36db59027e6262d39dfc490463da1b32d34ff4a2c8a043e9b8e4
|
|
BLAKE2b-256 checksum How to use checksums |
cf0fd0d6e2b022cbf0405ac9f0f594d53500e2913e78102016a4ed35cb47bf51
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 15, 2026.
Transparency logSecurity for ceph-adapter 1.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Change of provenance
The configuration that created this release differs from a previous release.
While this could reflect a routine change by the project maintainers, it may also indicate that the release files have been compromised.
Configuration
- Removed 0.2.0 python-publish.yml
- Added 1.1.0 release.yml
Attestation for ceph_adapter-1.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 15, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.