Quick Start#

Installation#

You can install deev from PyPI through the usual means, such as pip:

pip install deev

Usage#

Two popular use cases are shown: using Python objects for CRUD operations, and using the db-migrate CLI tool to manage DB schema.

Entity CRUD#

First, let’s define a “SimpleEntity” class we will use as a database entity:

from datetime import datetime, timezone
from deev import entity, field
from typing import Optional

# ./SimpleEntity.py
@entity
class SimpleEntity:
    column1: int
    column2: Optional[list[str]] = field(default=None)
    column3: Optional[datetime] = field(default=lambda: datetime.now(timezone.utc))
    id: int = field(autoincrement=True, default=None, primary_key=True)

Next, the CRUD-based code:

# imports
from deev import entity, field

# define a simple entity with an auto-increment PK, an int value column,
# and a list[str] column
@entity
class SimpleEntity:
    id: int = field(autoincrement=True, primary_key=True)
    column1: int
    column2: list[str]

# create a database using familiar connection-string syntax
from deev.utils import create_database
connection_str = 'Server=./test_data/;Database=sqlite3/test.db;Provider=sqlite3'
create_database(connection_str)

# connect to your database, create a table for storage, and perform some CRUD operations
from deev import connect
from deev.sqlite import SqliteTableAdapter

with connect(connection_str) as db:
    table = SqliteTableAdapter[SimpleEntity](db)
    table.create_table()

    # CREATE
    entity_key = table.create(
        SimpleEntity(
            column1=1,
            column2=['3', '2', '1']
        )
    )

    # READ
    e = table.read(**entity_key)
    assert e.id is not None
    assert e.column1 == 1
    assert e.column2[0] == '3'
    assert e.column2[1] == '2'
    assert e.column2[2] == '1'

    # UPDATE
    e.column2[1] = 4
    table.update(e)

    # DELETE
    table.delete(**entity_key)

    # alternatives: upsert + query
    entity_key = table.upsert(
        SimpleEntity(
            column1=2,
            column2=['5']
        )
    )
    entity_key = table.upsert(
        SimpleEntity(
            column1=2,
            column2=['6']
        )
    )
    results = table.query(
        where='column1 = %?',
        orderby='column1 DESC',
        limit=2,
        params=(2,)
    )
    count = 0
    for result in results:
        assert result.column2[0] in ('5', '6')
        count += 1
    assert count == 2

    # query kwargs are optional; this creates a generator for all table records:
    results = table.query()

CLI db-migrate Tool#

The db-migrate tool can be used to apply a migration script or undo a previously applied migration script. Here is the main syntax from CLI help:

$ db-migrate -h
usage: db-migrate [-h] [--verbose] <COMMAND> ...

Utility for applying, undoing, or generating migrations.

positional arguments:
<COMMAND>   Action to perform.
    apply     Apply migrations.
    undo      Undo migrations.

options:
-h, --help  show this help message and exit
--verbose   Enable verbose logging.

$ db-migrate apply -h
usage: db-migrate apply [-h] [--stop-at name] connectionstring [path]

positional arguments:
connectionstring  Database connection string.
path              Directory containing migration scripts (optional). If omitted, a path is calculated from the connectionstring argument, ie.
                    `./migrations/databnase_name/`.

options:
-h, --help        show this help message and exit
--stop-at name    Stop processing at the named migration.

A migration script is a Python file which defines two functions apply(...) and undo(...), each receiving a DbTransactionContext you can use to modify the database transactionally.

As an example, we will create two migration scripts “000_initial_schema.py” and “001_initial_seed.py”, we name them so their sort order ensures the schema script runs before the seed script. (A practice used on internal projects is to use a datecode, issue number, or similar linearly progressing value.)

# ./migrations/test_db/000_initial_schema.py
from deev.common import DbTransactionContext
from deev.utils import create_table_adapter
from .SimpleEntity import SimpleEntity

def apply(transaction: DbTransactionContext) -> None:
    table_adapter = create_table_adapter(SimpleEntity, transaction)
    table_adapter.create_table()
    transaction.commit()

def undo(transaction: DbTransactionContext) -> None:
    transaction.execute_nonquery('DROP TABLE `SimpleEntities`;')
    transaction.commit()
# ./migrations/test_db/001_initial_seed.py
from deev.common import DbTransactionContext
from deev.utils import create_table_adapter
from .SimpleEntity import SimpleEntity

def apply(transaction: DbTransactionContext) -> None:
    table_adapter = create_table_adapter(SimpleEntity, transaction)
    table_adapter.create(SimpleEntity(
        column1 = 345
    ))
    table_adapter.create(SimpleEntity(
        column1 = 456
    ))
    transaction.commit()

def undo(transaction: DbTransactionContext) -> None:
    transaction.execute_nonquery('DELETE FROM `SimpleEntities` WHERE `column1` IN (345, 456)')
    transaction.commit()

Apply the change to the existing database:

# apply schema change
db-migrate apply 'Server=./test_data/;Database=sqlite3/test.db;Provider=sqlite3' ./migrations/test_db/

The tool reports:

..apply migration "000_initial_schema"
..apply migration "001_initial_seed"
Migrations applied 2, skipped 0, available 2.

Undo the change after it has been applied:

# undo schema change
db-migrate undo 'Server=./test_data/;Database=sqlite3/test.db;Provider=sqlite3' ./migration/test_db/

The tool reports:

..undo migration "001_initial_seed"
..undo migration "000_initial_schema"
Migrations undone 2, skipped 0, available 2.