Skip to content

feat(python): encrypt models into EQL v4 rows - #1499

Draft
yujiyokoo wants to merge 3 commits into
feat/python-sdk-cipherfrom
feat/python-sdk-encrypt
Draft

yujiyokoo wants to merge 3 commits into
feat/python-sdk-cipherfrom
feat/python-sdk-encrypt

Conversation

@yujiyokoo

Copy link
Copy Markdown
Contributor

Summary

Cipher.encrypt now turns a list of declared models into rows for PostgreSQL with EQL v4. EQL is CipherStash's set of PostgreSQL types for encrypted values. A sealed field becomes a str of EQL JSON. psycopg inserts it with a plain %s and no cast. A passthrough field is the value itself.

Changes

  • Python API: Cipher.encrypt(models) returns one dict per model. The new InvalidValueError is a StashError and a ValueError. A README stub shows the CREATE TABLE, the encrypt call, and the insert.
  • Value checks: the SDK checks the exact type and range of every value before any request. A bad value raises InvalidValueError with the field's name. The error never contains the value. The codes are value_none, value_wrong_type, value_out_of_range, value_not_representable, and value_naive_datetime.
  • Engine client: one client per process, made on the first encrypt of any Cipher. The Python global interpreter lock (GIL) is released during each engine call. The client reads CS_ZEROKMS_HOST. The client's ZeroKMS connection counts requests for the tests.
  • Rust dependencies: the http feature is on. stack-kms, tokio, and zerokms-protocol are new. Every new crate in the Python Cargo.lock has the root lock's version.
  • CI: a new live job in tests-python.yml runs mise run test:live. The job encrypts against ZeroKMS and inserts rows into a postgres:17 service with the EQL v4 bundle from this tree. The job skips on a pull request from a fork.

Verification

  • pytest on Python 3.14 and 3.9: 247 passed, 7 skipped. The skips are the 5 live tests and 2 gate-probe tests.
  • cargo test --locked, clippy, and cargo fmt --check: no failures or warnings.
  • pnpm run test:scripts: 83 of 83 files pass.
  • The live tests have not run yet. This machine has no CipherStash credentials. The first run is the live job on this PR.

Related

Closes #1389. Refs #1341.

Review notes

  • A passthrough field has no "type" key in the output. python: query an equality term #1391 can add it.
  • KmsError is not built yet. A ZeroKMS failure is a StashError.
  • eql-suite-ci.test.mjs now accepts a Rust cache step for a second workspace in the same job. The live job needs one for languages/python and one for packages/eql.

🤖 Generated with Claude Code

yujiyokoo and others added 2 commits October 30, 2026 19:56
Cipher.encrypt returns one dict per model. A sealed field is a str of
EQL JSON, which psycopg inserts with a plain %s and no cast. A
passthrough field is the value itself, echoed by the engine.

Every value is checked by exact type and range while the models are
written into the engine's input frame. A bad value raises the new
InvalidValueError with the field's name. No request is made, and the
engine client is not built. A property that raises propagates
unchanged.

One engine client is shared per process, with one multi-thread tokio
runtime. It is built on the first encrypt of any Cipher, and the GIL
is released around each engine call. Its ZeroKMS connection counts
requests, so the tests check the 500-keys-per-request rule. The http
feature is on, and every new crate matches the root lock's version.

A live CI job in tests-python.yml encrypts against ZeroKMS and inserts
a row into Postgres with the EQL v4 bundle from the tree. It skips on
a fork's pull request. The EQL cache test now accepts a second
workspace's cache step in the same job.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Decimal passthrough now comes back as a Decimal. Its digits go to
Decimal as a tuple, not as bytes.

A zero Decimal with a huge exponent no longer hangs the process with
the GIL held. A zero with a positive exponent returns at once. An
exponent above 38 is refused as value_not_representable.

The shared client now reads CS_ZEROKMS_HOST, then CS_VITUR_HOST, as
the other SDKs do. A bad value is refused as stack_kms_invalid_endpoint,
before the credentials are read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: f4a374a

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai

coderabbitai Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

@blacksmith-sh

This comment has been minimized.

encrypt failed on every model with a Passthrough field. The extension
wrote the value as a passthrough node. The plan's "passthrough" output
already carries the field's frame as it is, so the engine returned the
value nested in a second passthrough, and reading it failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

1 participant