From b5092e29f8dd373612437b99ad9810eae8bafe25 Mon Sep 17 00:00:00 2001 From: Ravishankar Sivasubramaniam Date: Thu, 25 Sep 2025 12:41:51 -0500 Subject: [PATCH 1/2] feat: Add tutorial and advanced page for EncryptedType --- docs/advanced/encrypted-type.md | 19 ++++ docs/tutorial/encrypted-type.md | 68 +++++++++++++ .../advanced/encrypted_type/tutorial001.py | 39 ++++++++ .../tutorial/encrypted_type/requirements.txt | 3 + .../tutorial/encrypted_type/tutorial001.py | 96 +++++++++++++++++++ mkdocs.yml | 2 + 6 files changed, 227 insertions(+) create mode 100644 docs/advanced/encrypted-type.md create mode 100644 docs/tutorial/encrypted-type.md create mode 100644 docs_src/advanced/encrypted_type/tutorial001.py create mode 100644 docs_src/tutorial/encrypted_type/requirements.txt create mode 100644 docs_src/tutorial/encrypted_type/tutorial001.py diff --git a/docs/advanced/encrypted-type.md b/docs/advanced/encrypted-type.md new file mode 100644 index 0000000000..98f39c7575 --- /dev/null +++ b/docs/advanced/encrypted-type.md @@ -0,0 +1,19 @@ +# Encrypted Type + +Sometimes you need to store sensitive data like secrets, tokens, or personal information in your database in an encrypted format. + +You can use the `EncryptedType` from the `sqlalchemy-utils` package to achieve this with SQLModel. + +## A Model with an Encrypted Field + +You can define a field with `EncryptedType` in your SQLModel model using `sa_column`: + +{* ./docs_src/advanced/encrypted_type/tutorial001.py *} + +In this example, the `secret_name` field will be automatically encrypted when you save a `Hero` object to the database and decrypted when you access it. + +/// tip +For this to work, you need to have `sqlalchemy-utils` and `cryptography` installed. +/// + +For a more detailed walkthrough, including how to create and query data with encrypted fields, check out the [Encrypting Data Tutorial](../tutorial/encrypted-type.md). \ No newline at end of file diff --git a/docs/tutorial/encrypted-type.md b/docs/tutorial/encrypted-type.md new file mode 100644 index 0000000000..d279db2e3e --- /dev/null +++ b/docs/tutorial/encrypted-type.md @@ -0,0 +1,68 @@ +# Encrypting Data + +In this tutorial, you'll learn how to encrypt data before storing it in your database using SQLModel and `sqlalchemy-utils`. + +## The Scenario + +Let's imagine we're building an application to store information about characters from our favorite TV show, Ted Lasso. We want to store their names, secret names (which should be encrypted), and their ages. + +## The Code + +Here's the complete code to achieve this: + +{* ./docs_src/tutorial/encrypted_type/tutorial001.py *} + +### Understanding the Code + +Let's break down the key parts of the code: + +1. **`EncryptedType`**: We use `EncryptedType` from `sqlalchemy-utils` as a `sa_column` for the `secret_name` field. This tells SQLModel to use this special type for the column in the database. + +2. **Encryption Key**: We provide an encryption key to `EncryptedType`. In a real-world application, you should **never** hardcode the key like this. Instead, you should load it from a secure source like a secret manager or an environment variable. + +3. **`demonstrate_encryption` function**: This function shows the power of `EncryptedType`. + * First, it queries the database directly using raw SQL. When we print the `secret_name` from this query, you'll see the encrypted string, not the original secret name. + * Then, it queries the database using SQLModel. When we access the `secret_name` attribute of the `Character` objects, `EncryptedType` automatically decrypts the data for us, so we get the original, readable secret names. + +## How to Test + +To run this example, first create a virtual environment: + +```bash +python -m venv venv +source venv/bin/activate +``` + +Then, install the required packages from the `requirements.txt` file: + +```bash +pip install -r docs_src/tutorial/encrypted_type/requirements.txt +``` + +Then, you can run the python script: + +```bash +python docs_src/tutorial/encrypted_type/tutorial001.py +``` + +## Running the Code + +When you run the code, you'll see the following output: + +```console +Creating database and tables... +Creating characters... + +Demonstrating encryption... +Data as stored in the database: +Name: Roy Kent, Encrypted Secret Name: b'5dBrkurIL+fEin+1eUBc0A==' +Name: Jamie Tartt, Encrypted Secret Name: b'CDLkQWx5ezXn+U4kRlVFyQ==' +Name: Dani Rojas, Encrypted Secret Name: b'SqSjH+biJttbs9zH+DBw8A==' + +Data as accessed through SQLModel: +Name: Roy Kent, Decrypted Secret Name: The Special One +Name: Jamie Tartt, Decrypted Secret Name: Baby Shark +Name: Dani Rojas, Decrypted Secret Name: Fútbol is Life +``` + +As you can see, `EncryptedType` handles the encryption and decryption for you automatically, making it easy to store sensitive data securely. \ No newline at end of file diff --git a/docs_src/advanced/encrypted_type/tutorial001.py b/docs_src/advanced/encrypted_type/tutorial001.py new file mode 100644 index 0000000000..9017d657ab --- /dev/null +++ b/docs_src/advanced/encrypted_type/tutorial001.py @@ -0,0 +1,39 @@ + +import os +from typing import Optional + +import sqlalchemy +from sqlalchemy import Column +from sqlalchemy_utils import EncryptedType +from sqlalchemy_utils.types.encrypted.encrypted_type import AesEngine +from sqlmodel import create_engine, Field, Session, SQLModel + + +# For a real application, use a securely managed key +ENCRYPTION_KEY = "a-super-secret-key" + +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str + # Because the secret name should stay a secret + secret_name: str = Field( + sa_column=Column( + EncryptedType( + sqlalchemy.Unicode, + ENCRYPTION_KEY, + AesEngine, + "pkcs5", + ) + ) + ) + age: Optional[int] = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" + +engine = create_engine(sqlite_url) + + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) diff --git a/docs_src/tutorial/encrypted_type/requirements.txt b/docs_src/tutorial/encrypted_type/requirements.txt new file mode 100644 index 0000000000..2b44aabf85 --- /dev/null +++ b/docs_src/tutorial/encrypted_type/requirements.txt @@ -0,0 +1,3 @@ +sqlmodel +sqlalchemy-utils +cryptography \ No newline at end of file diff --git a/docs_src/tutorial/encrypted_type/tutorial001.py b/docs_src/tutorial/encrypted_type/tutorial001.py new file mode 100644 index 0000000000..0022b61dfa --- /dev/null +++ b/docs_src/tutorial/encrypted_type/tutorial001.py @@ -0,0 +1,96 @@ +# Import necessary modules +import os +from typing import Optional + +import sqlalchemy +from sqlalchemy import Column, text +from sqlalchemy_utils import EncryptedType +from sqlalchemy_utils.types.encrypted.encrypted_type import AesEngine +from sqlmodel import create_engine, Field, Session, SQLModel, select + +# Define a secret key for encryption. +# In a real application, this key should be stored securely and not hardcoded. +# For example, you could load it from an environment variable or a secret management service. +ENCRYPTION_KEY = "a-super-secret-key" + +# Define the Character model +# This model represents a table named 'character' in the database. +class Character(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str + # The secret_name field is encrypted in the database. + # We use EncryptedType from sqlalchemy-utils for this. + secret_name: str = Field( + sa_column=Column( + EncryptedType( + sqlalchemy.Unicode, + ENCRYPTION_KEY, + AesEngine, + "pkcs5", + ) + ) + ) + age: Optional[int] = None + +# Define the database URL and create the engine +# We are using a SQLite database for this example. +sqlite_file_name = "database.db" +sqlite_url = f"sqlite:///{sqlite_file_name}" +engine = create_engine(sqlite_url) + +# This function creates the database and the Character table. +# It first drops the existing table to ensure a clean state for the example. +def create_db_and_tables(): + SQLModel.metadata.drop_all(engine) + SQLModel.metadata.create_all(engine) + +# This function creates some sample characters and adds them to the database. +def create_characters(): + # Create instances of the Character model + roy_kent = Character(name="Roy Kent", secret_name="The Special One", age=40) + jamie_tartt = Character(name="Jamie Tartt", secret_name="Baby Shark", age=25) + dani_rojas = Character(name="Dani Rojas", secret_name="Fútbol is Life", age=23) + + # Use a session to interact with the database + with Session(engine) as session: + # Add the characters to the session + session.add(roy_kent) + session.add(jamie_tartt) + session.add(dani_rojas) + + # Commit the changes to the database + session.commit() + +# This function demonstrates how the encryption works. +def demonstrate_encryption(): + with Session(engine) as session: + # Query the database directly to see the encrypted data + # We use a raw SQL query for this. + statement = text("SELECT name, secret_name FROM character") + results = session.exec(statement).all() + print("Data as stored in the database:") + for row in results: + # The secret_name will be an encrypted string. + print(f"Name: {row.name}, Encrypted Secret Name: {row.secret_name}") + + # Query through SQLModel to see the decrypted data + # SQLModel will automatically decrypt the secret_name. + statement = select(Character) + characters = session.exec(statement).all() + print("\nData as accessed through SQLModel:") + for character in characters: + # The secret_name will be the original, decrypted string. + print(f"Name: {character.name}, Decrypted Secret Name: {character.secret_name}") + +# The main function that runs the example. +def main(): + print("Creating database and tables...") + create_db_and_tables() + print("Creating characters...") + create_characters() + print("\nDemonstrating encryption...") + demonstrate_encryption() + +# Run the main function when the script is executed. +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index c59ccd245a..60c757ce08 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -87,6 +87,7 @@ nav: - tutorial/limit-and-offset.md - tutorial/update.md - tutorial/delete.md + - tutorial/encrypted-type.md - Connect Tables - JOIN: - tutorial/connect/index.md - tutorial/connect/create-connected-tables.md @@ -128,6 +129,7 @@ nav: - advanced/index.md - advanced/decimal.md - advanced/uuid.md + - advanced/encrypted-type.md - Resources: - resources/index.md - help.md From beb184fa001c5d1ba8598a06d2226b24eafe2b36 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 25 Sep 2025 17:45:00 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20for?= =?UTF-8?q?mat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/advanced/encrypted-type.md | 2 +- docs/tutorial/encrypted-type.md | 2 +- docs_src/advanced/encrypted_type/tutorial001.py | 6 ++---- .../tutorial/encrypted_type/requirements.txt | 2 +- docs_src/tutorial/encrypted_type/tutorial001.py | 16 ++++++++++++---- 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/docs/advanced/encrypted-type.md b/docs/advanced/encrypted-type.md index 98f39c7575..d8dc6a86f8 100644 --- a/docs/advanced/encrypted-type.md +++ b/docs/advanced/encrypted-type.md @@ -16,4 +16,4 @@ In this example, the `secret_name` field will be automatically encrypted when yo For this to work, you need to have `sqlalchemy-utils` and `cryptography` installed. /// -For a more detailed walkthrough, including how to create and query data with encrypted fields, check out the [Encrypting Data Tutorial](../tutorial/encrypted-type.md). \ No newline at end of file +For a more detailed walkthrough, including how to create and query data with encrypted fields, check out the [Encrypting Data Tutorial](../tutorial/encrypted-type.md). diff --git a/docs/tutorial/encrypted-type.md b/docs/tutorial/encrypted-type.md index d279db2e3e..c4bc008cd9 100644 --- a/docs/tutorial/encrypted-type.md +++ b/docs/tutorial/encrypted-type.md @@ -65,4 +65,4 @@ Name: Jamie Tartt, Decrypted Secret Name: Baby Shark Name: Dani Rojas, Decrypted Secret Name: Fútbol is Life ``` -As you can see, `EncryptedType` handles the encryption and decryption for you automatically, making it easy to store sensitive data securely. \ No newline at end of file +As you can see, `EncryptedType` handles the encryption and decryption for you automatically, making it easy to store sensitive data securely. diff --git a/docs_src/advanced/encrypted_type/tutorial001.py b/docs_src/advanced/encrypted_type/tutorial001.py index 9017d657ab..d95b7844db 100644 --- a/docs_src/advanced/encrypted_type/tutorial001.py +++ b/docs_src/advanced/encrypted_type/tutorial001.py @@ -1,17 +1,15 @@ - -import os from typing import Optional import sqlalchemy from sqlalchemy import Column from sqlalchemy_utils import EncryptedType from sqlalchemy_utils.types.encrypted.encrypted_type import AesEngine -from sqlmodel import create_engine, Field, Session, SQLModel - +from sqlmodel import Field, SQLModel, create_engine # For a real application, use a securely managed key ENCRYPTION_KEY = "a-super-secret-key" + class Hero(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) name: str diff --git a/docs_src/tutorial/encrypted_type/requirements.txt b/docs_src/tutorial/encrypted_type/requirements.txt index 2b44aabf85..67f7b5b649 100644 --- a/docs_src/tutorial/encrypted_type/requirements.txt +++ b/docs_src/tutorial/encrypted_type/requirements.txt @@ -1,3 +1,3 @@ sqlmodel sqlalchemy-utils -cryptography \ No newline at end of file +cryptography diff --git a/docs_src/tutorial/encrypted_type/tutorial001.py b/docs_src/tutorial/encrypted_type/tutorial001.py index 0022b61dfa..72b2c63123 100644 --- a/docs_src/tutorial/encrypted_type/tutorial001.py +++ b/docs_src/tutorial/encrypted_type/tutorial001.py @@ -1,18 +1,18 @@ # Import necessary modules -import os from typing import Optional import sqlalchemy from sqlalchemy import Column, text from sqlalchemy_utils import EncryptedType from sqlalchemy_utils.types.encrypted.encrypted_type import AesEngine -from sqlmodel import create_engine, Field, Session, SQLModel, select +from sqlmodel import Field, Session, SQLModel, create_engine, select # Define a secret key for encryption. # In a real application, this key should be stored securely and not hardcoded. # For example, you could load it from an environment variable or a secret management service. ENCRYPTION_KEY = "a-super-secret-key" + # Define the Character model # This model represents a table named 'character' in the database. class Character(SQLModel, table=True): @@ -32,18 +32,21 @@ class Character(SQLModel, table=True): ) age: Optional[int] = None + # Define the database URL and create the engine # We are using a SQLite database for this example. sqlite_file_name = "database.db" sqlite_url = f"sqlite:///{sqlite_file_name}" engine = create_engine(sqlite_url) + # This function creates the database and the Character table. # It first drops the existing table to ensure a clean state for the example. def create_db_and_tables(): SQLModel.metadata.drop_all(engine) SQLModel.metadata.create_all(engine) + # This function creates some sample characters and adds them to the database. def create_characters(): # Create instances of the Character model @@ -61,6 +64,7 @@ def create_characters(): # Commit the changes to the database session.commit() + # This function demonstrates how the encryption works. def demonstrate_encryption(): with Session(engine) as session: @@ -80,7 +84,10 @@ def demonstrate_encryption(): print("\nData as accessed through SQLModel:") for character in characters: # The secret_name will be the original, decrypted string. - print(f"Name: {character.name}, Decrypted Secret Name: {character.secret_name}") + print( + f"Name: {character.name}, Decrypted Secret Name: {character.secret_name}" + ) + # The main function that runs the example. def main(): @@ -91,6 +98,7 @@ def main(): print("\nDemonstrating encryption...") demonstrate_encryption() + # Run the main function when the script is executed. if __name__ == "__main__": - main() \ No newline at end of file + main()