Skip to content

Commit c386815

Browse files
committed
Replace global config with registry and add Flask example
1 parent e2ad2df commit c386815

17 files changed

Lines changed: 239 additions & 100 deletions

File tree

README.md

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,9 @@ pip install sqlmodel-encrypted-fields
1414
from sqlalchemy import Column
1515
from sqlmodel import Field, SQLModel
1616

17-
from sqlmodel_encrypted_fields import (
18-
configure_keysets,
19-
EncryptedString,
20-
DeterministicEncryptedString,
21-
)
17+
from sqlmodel_encrypted_fields import KeysetRegistry
2218

23-
configure_keysets(
19+
registry = KeysetRegistry(
2420
{
2521
"default": {"path": "/path/to/aead_keyset.json", "cleartext": True},
2622
"searchable": {"path": "/path/to/daead_keyset.json", "cleartext": True},
@@ -30,8 +26,28 @@ configure_keysets(
3026

3127
class Customer(SQLModel, table=True):
3228
id: int | None = Field(default=None, primary_key=True)
33-
email: str = Field(sa_column=Column(EncryptedString()))
34-
email_lookup: str = Field(sa_column=Column(DeterministicEncryptedString(keyset="searchable")))
29+
email: str = Field(sa_column=Column(registry.encrypted_string()))
30+
email_lookup: str = Field(sa_column=Column(registry.deterministic_encrypted_string(keyset="searchable")))
31+
```
32+
33+
## Example Apps
34+
35+
- FastAPI example: `example_app_fastapi/`
36+
- Flask example: `example_app_flask/`
37+
38+
### Flask Example Snippet
39+
40+
```python
41+
from flask import Flask
42+
43+
from example_app_flask.database import init_db
44+
from example_app_flask.models import Customer
45+
46+
app = Flask(__name__)
47+
48+
@app.before_first_request
49+
def _init_db() -> None:
50+
init_db()
3551
```
3652

3753
## Notes
@@ -54,7 +70,13 @@ Regular AEAD fields change ciphertext on every write. Use deterministic fields o
5470
```python
5571
from datetime import date
5672

57-
from sqlmodel_encrypted_fields import EncryptedType
73+
from sqlmodel_encrypted_fields import KeysetRegistry
74+
75+
registry = KeysetRegistry(
76+
{
77+
"default": {"path": "/path/to/aead_keyset.json", "cleartext": True},
78+
}
79+
)
5880

5981

6082
def serialize_date(value: date) -> str:
@@ -67,5 +89,7 @@ def deserialize_date(value: str) -> date:
6789

6890
class User(SQLModel, table=True):
6991
id: int | None = Field(default=None, primary_key=True)
70-
birthday: date = Field(sa_column=EncryptedType(serializer=serialize_date, deserializer=deserialize_date))
92+
birthday: date = Field(
93+
sa_column=registry.encrypted_type(serializer=serialize_date, deserializer=deserialize_date)
94+
)
7195
```

example_app/models.py

Lines changed: 0 additions & 14 deletions
This file was deleted.

example_app_fastapi/crypto.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from __future__ import annotations
2+
3+
from pathlib import Path
4+
5+
from sqlmodel_encrypted_fields import KeysetRegistry
6+
7+
8+
def _default_config() -> dict[str, dict[str, object]]:
9+
root = Path(__file__).resolve().parents[1]
10+
return {
11+
"default": {"path": str(root / "tests" / "fixtures" / "aead_keyset.json"), "cleartext": True},
12+
"deterministic": {
13+
"path": str(root / "tests" / "fixtures" / "daead_keyset.json"),
14+
"cleartext": True,
15+
},
16+
}
17+
18+
19+
registry = KeysetRegistry(_default_config())

example_app_fastapi/database.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from __future__ import annotations
2+
3+
from sqlmodel import Session, SQLModel, create_engine
4+
5+
engine = create_engine("sqlite:///./example_app_fastapi.db", echo=False)
6+
7+
8+
def init_db() -> None:
9+
SQLModel.metadata.create_all(engine)
10+
11+
12+
def get_session() -> Session:
13+
return Session(engine)
Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,11 @@
55
from fastapi import Depends, FastAPI, HTTPException
66
from sqlmodel import Session, select
77

8-
from example_app.database import get_session, init_db
9-
from example_app.models import Customer
10-
from sqlmodel_encrypted_fields import configure_keysets
8+
from example_app_fastapi.database import get_session, init_db
9+
from example_app_fastapi.models import Customer
1110

1211
@asynccontextmanager
1312
async def lifespan(_: FastAPI):
14-
configure_keysets(
15-
{
16-
"default": {"path": "./tests/fixtures/aead_keyset.json", "cleartext": True},
17-
"deterministic": {"path": "./tests/fixtures/daead_keyset.json", "cleartext": True},
18-
}
19-
)
2013
init_db()
2114
yield
2215

example_app_fastapi/models.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from __future__ import annotations
2+
3+
from typing import Optional
4+
5+
from sqlalchemy import Column
6+
from sqlmodel import Field, SQLModel
7+
8+
from example_app_fastapi.crypto import registry
9+
10+
11+
class Customer(SQLModel, table=True):
12+
id: Optional[int] = Field(default=None, primary_key=True)
13+
email: str = Field(sa_column=Column(registry.encrypted_string()))
14+
email_lookup: str = Field(sa_column=Column(registry.deterministic_encrypted_string(keyset="deterministic")))

example_app/tests/test_integration.py renamed to example_app_fastapi/tests/test_integration.py

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,33 +5,16 @@
55
from fastapi.testclient import TestClient
66
from sqlmodel import Session, SQLModel, create_engine, select
77

8-
from example_app.database import get_session
9-
from example_app.main import app
10-
from example_app.models import Customer
11-
from sqlmodel_encrypted_fields import configure_keysets
12-
13-
14-
def _project_root() -> Path:
15-
return Path(__file__).resolve().parents[2]
16-
17-
18-
def _configure_keysets() -> None:
19-
root = _project_root()
20-
configure_keysets(
21-
{
22-
"default": {"path": str(root / "tests" / "fixtures" / "aead_keyset.json"), "cleartext": True},
23-
"deterministic": {"path": str(root / "tests" / "fixtures" / "daead_keyset.json"), "cleartext": True},
24-
}
25-
)
8+
from example_app_fastapi.database import get_session
9+
from example_app_fastapi.main import app
10+
from example_app_fastapi.models import Customer
2611

2712

2813
def _test_engine(tmp_path: Path):
2914
return create_engine(f"sqlite:///{tmp_path / 'test.db'}", echo=False)
3015

3116

3217
def test_customer_create_and_lookup(tmp_path: Path) -> None:
33-
_configure_keysets()
34-
3518
engine = _test_engine(tmp_path)
3619
SQLModel.metadata.create_all(engine)
3720

example_app_flask/__init__.py

Whitespace-only changes.

example_app_flask/app.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from __future__ import annotations
2+
3+
from flask import Flask, jsonify, request
4+
from sqlmodel import Session, select
5+
6+
from example_app_flask.database import get_session, init_db
7+
from example_app_flask.models import Customer
8+
9+
10+
def create_app() -> Flask:
11+
app = Flask(__name__)
12+
13+
@app.before_first_request
14+
def _init_db() -> None:
15+
init_db()
16+
17+
@app.post("/customers")
18+
def create_customer():
19+
payload = request.get_json(force=True)
20+
customer = Customer(**payload)
21+
with get_session() as session:
22+
session.add(customer)
23+
session.commit()
24+
session.refresh(customer)
25+
return jsonify(customer.model_dump())
26+
27+
@app.get("/customers/<int:customer_id>")
28+
def get_customer(customer_id: int):
29+
with get_session() as session:
30+
customer = session.get(Customer, customer_id)
31+
if customer is None:
32+
return jsonify({"detail": "Customer not found"}), 404
33+
return jsonify(customer.model_dump())
34+
35+
@app.get("/customers/by-email/<string:email>")
36+
def get_customer_by_email(email: str):
37+
with get_session() as session:
38+
statement = select(Customer).where(Customer.email_lookup == email)
39+
customer = session.exec(statement).first()
40+
if customer is None:
41+
return jsonify({"detail": "Customer not found"}), 404
42+
return jsonify(customer.model_dump())
43+
44+
return app
45+
46+
47+
if __name__ == "__main__":
48+
create_app().run(debug=True)

0 commit comments

Comments
 (0)