Skip to content

feat(python): add a customer directory example - #1621

Open
yujiyokoo wants to merge 2 commits into
feat/python-sdk-lockfrom
feat/python-sdk-example
Open

yujiyokoo wants to merge 2 commits into
feat/python-sdk-lockfrom
feat/python-sdk-example

Conversation

@yujiyokoo

Copy link
Copy Markdown
Contributor

Summary

This PR adds a small FastAPI service that uses the Python SDK (stackencrypt) the way a real application does. It stores customers in PostgreSQL. The email is encrypted, and you can still search it by exact match. The same app is written twice: once with psycopg, and once with SQLAlchemy.

Changes

  • app.py: the psycopg version. Request threads share one Cipher, and one Pydantic class is both the request body and the model.
  • app_sqlalchemy.py: the SQLAlchemy version. It shows the code a SQLAlchemy user has to write by hand today: an EQL column type, encryption before an INSERT, a cast on the query term, and decryption after a SELECT.
  • schema.sql: one table for both apps. A unique index on the email's equality term refuses duplicate emails.
  • errors.py: error handlers that both apps use.
  • tests/test_app.py: one set of tests, run against both apps.
  • compose.yaml, mise.toml, pyproject.toml, uv.lock: a local PostgreSQL, the tasks, and the dependencies. The example installs stackencrypt from this checkout.
  • AGENTS.md and SECURITY.md now list the example.

Verification

The tests need real CipherStash credentials, so CI does not run them. They were run by hand against a local PostgreSQL with EQL v4.

Related

Refs #1341

Review notes

The example is internal and is not published, so there is no changeset. The SQLAlchemy app shows the manual steps on purpose. A later PR (feat/python-sqlalchemy) replaces them with stackencrypt.sqlalchemy.

🤖 Generated with Claude Code

yujiyokoo and others added 2 commits November 9, 2026 18:16
The Python SDK has tests for each call, but nothing used it the way an
application does. This FastAPI service shares one Cipher between request
threads, uses a Pydantic class as both the request body and the model, and
stores customers in PostgreSQL with EQL v4.

The email is encrypted and searchable by exact match, and a unique index
on its equality term refuses duplicates. The live tests need real
credentials, so they are run by hand and CI does not run them.

AGENTS.md and SECURITY.md list the new example.

Refs #1341

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The SDK README shows how to declare a SQLAlchemy model, but not how to
insert or query one. This second app shows the hand-written code that a
SQLAlchemy user needs today: an EQL column type, encrypt before an
INSERT, a cast on the query term, and decrypt after a SELECT. Both apps
share one table, and the tests run against both. The error handlers
move to errors.py, so that both apps use the same ones.

Refs #1341

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@yujiyokoo
yujiyokoo requested a review from a team as a code owner November 12, 2026 01:48
@changeset-bot

changeset-bot Bot commented Nov 12, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 10cafea

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 12, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI; Enforced overrides: Organization
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 821843b4-de76-497a-b5ea-cd1b6351acd0

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review

@cipherstash-agent cipherstash-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review attestation

Reviewed 10cafea · posted from Claude Code · claude-opus-5-5 · effort medium
Sources: code-review · claude-opus-5-5 (findings verified at head by review-cross-check delegates · claude-opus-5-5)
Skills: code-review, rust-security, review-cross-check, review-post

Findings (5): 5 minor — 5 inline, 0 below.

rust-security was loaded but the PR contains no Rust; only its logging-discipline rule was applied to the Python logging path. Two further code-review findings were refuted at head (no endpoint input makes PostgreSQL quote a value in diag.message_primary; dropping hide_input_in_errors from either Search leaks nothing because errors.py replaces the 422 body), and four design questions (connection pooling in app.py, the errors.py:17 docstring's general claim about PostgreSQL primary messages, Dependabot not covering this example's uv.lock, keyword-DSN DATABASE_URL) are left to the author and not posted as findings.


Generated by Claude Code

Comment on lines +108 to +118
def test_error_logs_contain_no_plaintext(client, caplog):
client.post("/customers", json=customer())
naive = {**customer(), "created_at": "2026-01-01T00:00:00"}

with caplog.at_level(logging.INFO):
client.post("/customers", json=customer())
client.post("/customers", json=naive)

assert "customer_error" in caplog.text
assert EMAIL not in caplog.text
assert NAME not in caplog.text

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor The setup POSTs are unchecked, and "customer_error" in caplog.text is always satisfied by the naive-datetime request (the engine refuses it before any SQL), so this test never shows that the duplicate (409) path logged anything. Assert each status (201, 409, 422) and check the 409 path's line, e.g. sqlstate=23505; the same unchecked setup lets test_a_different_email_is_not_found (line 62) pass on a failed insert, and lines 70, 80, 89, 161, 172-173 fail with TypeError/KeyError instead of the status.

Source: code-review · claude-opus-5-5


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changed yet. The example's tests need its own customer_directory database, so I could not run a change to them. It stays open.

Generated by Claude Code (Claude Opus 5.5)

@yujiyokoo
yujiyokoo added this pull request to stack #1520 November 12, 2026 01:59
@yujiyokoo
yujiyokoo removed this pull request from stack #1520 November 12, 2026 09:12
@yujiyokoo
yujiyokoo added this pull request to stack #1634 November 12, 2026 09:13

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.

2 participants