Skip to content

feat(python): expand bindings to cover all major read domains, document parity scope (closes #66) - #113

Merged
joshrotenberg merged 2 commits into
mainfrom
feat/66-python-parity
Jun 2, 2026
Merged

feat(python): expand bindings to cover all major read domains, document parity scope (closes #66)#113
joshrotenberg merged 2 commits into
mainfrom
feat/66-python-parity

Conversation

@joshrotenberg

Copy link
Copy Markdown
Contributor

Task: Python bindings parity — define scope and implement high-value domains (issue #66)

Setup (sequential — verify each step before proceeding)

  1. Confirm you are in /Users/josh.rotenberg/Code/active/redis-cloud-rs
  2. Confirm you are on branch feat/66-python-parity: git branch --show-current
  3. Confirm the tree is clean: git status
  4. Confirm baseline compiles (Rust library): cargo check --workspace 2>&1 | tail -5
  5. Confirm the Python extension builds: cd python && maturin develop --uv 2>&1 | tail -10
  6. Confirm baseline Python tests pass: cd python && python -m pytest tests/ -v 2>&1 | tail -20
  7. Return to repo root after steps 5-6: cd /Users/josh.rotenberg/Code/active/redis-cloud-rs

Context

This repo is a Rust client library (redis-cloud) for the Redis Cloud REST API, with PyO3-based
Python bindings in the python/ subdirectory. The Python extension is built with maturin.

The Rust library was harmonized in issue #65 (PR #112, merged): every domain handler now has
both verbose legacy names (get_all_subscriptions, get_subscription_by_id, etc.) AND simplified
alias methods (list, get, create, update, delete). The Python bindings should align with
the simplified alias names where possible.

Current Python surface (python/src/client.rs)

Read /Users/josh.rotenberg/Code/active/redis-cloud-rs/python/src/client.rs completely.

Currently exposed:

  • account / account_sync — via AccountHandler::get_current_account()
  • subscriptions / subscriptions_sync — via SubscriptionHandler::get_all_subscriptions()
  • subscription / subscription_sync — via SubscriptionHandler::get_subscription_by_id()
  • databases / databases_sync — via DatabaseHandler::get_subscription_databases()
  • database / database_sync — via DatabaseHandler::get_subscription_database_by_id()
  • all_databases / all_databases_sync — via DatabaseHandler::get_all_databases()
  • get, get_sync, post, post_sync, delete, delete_sync — raw HTTP
  • timeout property

Rust handler inventory (after #65)

Read these files to understand available handlers and method signatures:

  • /Users/josh.rotenberg/Code/active/redis-cloud-rs/src/lib.rs — re-exports and handler overview
  • /Users/josh.rotenberg/Code/active/redis-cloud-rs/src/tasks.rsTasksHandler: list(), get(task_id: String)
  • /Users/josh.rotenberg/Code/active/redis-cloud-rs/src/users.rsUsersHandler: list(), get(user_id: i32), delete(user_id: i32)
  • /Users/josh.rotenberg/Code/active/redis-cloud-rs/src/acl.rsAclHandler: list_redis_rules(), list_roles(), list_acl_users(), get_acl_user(id)
  • /Users/josh.rotenberg/Code/active/redis-cloud-rs/src/cloud_accounts.rsCloudAccountsHandler: list(), get(id), delete(id)
  • /Users/josh.rotenberg/Code/active/redis-cloud-rs/src/fixed/subscriptions.rsFixedSubscriptionHandler: list(), get_by_id(id), delete_by_id(id)
  • /Users/josh.rotenberg/Code/active/redis-cloud-rs/src/fixed/databases.rsFixedDatabaseHandler: list(sub_id), get_by_id(sub_id, db_id), backup(sub_id, db_id)
  • /Users/josh.rotenberg/Code/active/redis-cloud-rs/src/account.rsAccountHandler: get(), system_logs(...), payment_methods()

Decision: Python parity scope

This task implements option 1 from the issue: Python is a deliberately smaller convenience
layer.
The rationale:

  • The Python bindings are a read-oriented convenience layer for scripting, dashboards, and
    field engineering tooling.
  • Write operations (create, update complex configs) are better served by the Rust client or
    the raw HTTP helpers (post, put, patch) that are already exposed.
  • The Python surface should be predictable and complete for reads, covering all major
    domains with at minimum a list and/or get method.

The scope decision must be documented in two places:

  1. python/README.md (create this file — it does not currently exist)
  2. README.md (update the "Python Bindings" section to reference the scope decision)

Task: what exactly to implement

Phase 1: Update python/src/client.rs — add new domain methods

Add the following methods to PyCloudClient. Each method needs an async variant and a
_sync variant, following the exact same pattern as the existing methods.

The pattern (study before coding):

Async variant:

/// <Docstring> (async)
fn method_name<'py>(&self, py: Python<'py>, ...args) -> PyResult<Bound<'py, PyAny>> {
    let client = self.client.clone();
    future_into_py(py, async move {
        let handler = XxxHandler::new((*client).clone());
        let result = handler.simplified_method(...args).await.into_py_result()?;
        let json = serde_json::to_value(&result)
            .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
        Python::with_gil(|py| Ok(json_to_py(py, json)))
    })
}

Sync variant:

/// <Docstring> (sync)
fn method_name_sync(&self, py: Python<'_>, ...args) -> PyResult<Py<PyAny>> {
    let client = self.client.clone();
    let result = block_on(py, async move {
        let handler = XxxHandler::new((*client).clone());
        handler.simplified_method(...args).await.into_py_result()
    })?;
    let json = serde_json::to_value(&result)
        .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
    Ok(json_to_py(py, json))
}

Methods to add, grouped by domain:

Tasks domain

  • tasks() / tasks_sync() — calls TasksHandler::list(), returns all tasks
  • task(task_id: String) / task_sync(task_id: String) — calls TasksHandler::get(task_id)

Users domain

  • users() / users_sync() — calls UsersHandler::list()
  • user(user_id: i64) / user_sync(user_id: i64) — calls UsersHandler::get(user_id as i32)

ACL domain

  • acl_redis_rules() / acl_redis_rules_sync() — calls AclHandler::list_redis_rules()
  • acl_roles() / acl_roles_sync() — calls AclHandler::list_roles()
  • acl_users() / acl_users_sync() — calls AclHandler::list_acl_users()

Cloud accounts domain

  • cloud_accounts() / cloud_accounts_sync() — calls CloudAccountsHandler::list()
  • cloud_account(cloud_account_id: i64) / cloud_account_sync(cloud_account_id: i64) — calls CloudAccountsHandler::get(id as i32)

Fixed (Essentials) subscriptions domain

  • fixed_subscriptions() / fixed_subscriptions_sync() — calls FixedSubscriptionHandler::list()
  • fixed_subscription(subscription_id: i64) / fixed_subscription_sync(subscription_id: i64) — calls FixedSubscriptionHandler::get_by_id(id as i32)

Fixed (Essentials) databases domain

  • fixed_databases(subscription_id: i64) / fixed_databases_sync(subscription_id: i64) — calls FixedDatabaseHandler::list(sub_id as i32)
  • fixed_database(subscription_id: i64, database_id: i64) / fixed_database_sync(...) — calls FixedDatabaseHandler::get_by_id(sub_id as i32, db_id as i32)

Import additions needed in python/src/client.rs:

use redis_cloud::{
    AccountHandler, AclHandler, CloudAccountHandler, CloudClient,
    DatabaseHandler, FixedDatabaseHandler, FixedSubscriptionHandler,
    SubscriptionHandler, TaskHandler, UserHandler,
};

Check the actual re-export names in src/lib.rs carefully before writing the imports:

  • TasksHandler is re-exported as TaskHandler
  • UsersHandler is re-exported as UserHandler
  • CloudAccountsHandler is re-exported as CloudAccountHandler

Phase 2: Update python/src/lib.rs — no changes needed unless new types are exported

Review python/src/lib.rs. No changes are expected unless new types need to be registered
as Python classes (they don't for this task — we continue using the JSON-to-Python pattern).

Phase 3: Update Python tests (python/tests/test_client.py)

Add a new test class TestNewDomainMethods with hasattr existence checks for each new
method pair. Follow the existing TestClientMethods pattern exactly:

class TestNewDomainMethods:
    """Tests that new domain methods are present on CloudClient."""

    @pytest.fixture
    def client(self):
        return CloudClient(api_key="test-key", api_secret="test-secret")

    def test_client_has_tasks_methods(self, client):
        assert hasattr(client, "tasks")
        assert hasattr(client, "tasks_sync")

    def test_client_has_task_method(self, client):
        assert hasattr(client, "task")
        assert hasattr(client, "task_sync")

    # ... one test per method pair

Add tests for: tasks, task, users, user, acl_redis_rules, acl_roles, acl_users,
cloud_accounts, cloud_account, fixed_subscriptions, fixed_subscription,
fixed_databases, fixed_database.

Phase 4: Create python/README.md

Create /Users/josh.rotenberg/Code/active/redis-cloud-rs/python/README.md with:

  1. Parity scope statement (first section, not buried): "Python is a deliberately
    smaller convenience layer. It covers read-oriented operations across all major API
    domains. Write operations (create, update, delete) are available via the raw HTTP
    helpers (post, put, patch, delete) or the Rust client."
  2. Supported API coverage table — one row per domain with the methods exposed:
Domain Methods
Account account, account_sync
Pro subscriptions subscriptions, subscriptions_sync, subscription, subscription_sync
Pro databases databases, databases_sync, database, database_sync, all_databases, all_databases_sync
Tasks tasks, tasks_sync, task, task_sync
Users users, users_sync, user, user_sync
ACL acl_redis_rules, acl_redis_rules_sync, acl_roles, acl_roles_sync, acl_users, acl_users_sync
Cloud accounts cloud_accounts, cloud_accounts_sync, cloud_account, cloud_account_sync
Essentials subscriptions fixed_subscriptions, fixed_subscriptions_sync, fixed_subscription, fixed_subscription_sync
Essentials databases fixed_databases, fixed_databases_sync, fixed_database, fixed_database_sync
Raw HTTP get, get_sync, post, post_sync, delete, delete_sync
  1. Out-of-scope section: List what's intentionally deferred and why (connectivity
    handlers, cost_report, write operations, pagination beyond all_databases).

  2. Quick start example in Python showing how to use the client.

  3. Installation instructions (maturin / PyPI).

Phase 5: Update README.md Python Bindings section

Find the "Python Bindings" section in the root README.md (around line 132-142). Update it to:

Verification gates

After each phase:

After Phase 1 (Rust changes):

  1. cargo check --workspace 2>&1 | tail -10 — must be clean
  2. cargo clippy --workspace --all-targets -- -D warnings 2>&1 | tail -20 — must be clean
  3. cargo fmt --all

After Phase 2 (build Python extension):
4. cd /Users/josh.rotenberg/Code/active/redis-cloud-rs/python && maturin develop --uv 2>&1 | tail -15
Must complete without error. If it fails, read the full error and fix.

After Phase 3 (Python tests):
5. cd /Users/josh.rotenberg/Code/active/redis-cloud-rs/python && python -m pytest tests/ -v 2>&1
All tests must pass. The new TestNewDomainMethods tests must all be present and green.

Final:
6. cargo test --workspace 2>&1 | tail -20 — Rust tests must pass
7. cd /Users/josh.rotenberg/Code/active/redis-cloud-rs && cargo test --test openapi_route_coverage 2>&1 — must stay green

Tool-call discipline

  • Read each file completely before editing it.
  • For client.rs, read the whole file before adding any new methods.
  • After a compilation failure, read the full error output before fixing. Do not guess.
  • Check src/lib.rs re-exports to confirm the exact Rust type names to import.
  • Do not modify any existing method signatures.
  • Do not add new Rust dependencies (no new Cargo.toml entries).
  • The json_to_py and py_to_json helpers are already in python/src/client.rs — use them.

Constraints

  • Do NOT push to remote (the runner handles push after roba completes).
  • Do NOT run gh pr create (the runner created the draft PR).
  • Do NOT run git push.
  • Do NOT remove or modify existing methods in python/src/client.rs.
  • Do NOT change python/Cargo.toml dependencies.
  • Do NOT modify files under src/ (Rust library) except as needed to satisfy compilation.
    Actually: do NOT modify the Rust library files at all — they were settled in api(consistency): finish harmonized handler surface across remaining domains #65.
  • Do NOT add connectivity (vpc_peering, transit_gateway, psc, private_link) domain methods
    to the Python bindings — these are explicitly out of scope for this parity round.
  • Do NOT add cost_report domain methods — out of scope for this round.

Commit (at the end, after all checks pass)

git add python/src/client.rs python/tests/test_client.py python/README.md README.md
git commit -m "feat(python): expand bindings to cover all major read domains, document parity scope (closes #66)"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

python(parity): define and close the gap between Rust client and Python bindings

1 participant