deev Python entity framework — define entities with @entity and field(), perform CRUD via TableAdapter, translate between entity/dict/SQL via splat()/hydrate(), validate entities, manage migrations. Use as a reference document for deev API and concepts.
Resources
12Install
npx skillscat add wilson0x4d/deev Install via the SkillsCat registry.
deev — AI-First Library Reference
deev is an entity framework for Python 3.11+. It lets you perform CRUD using Python objects instead of hand-crafting SQL, provides a migration CLI (db-migrate), and abstracts across SQLite, MySQL, and MongoDB behind PEP 249-compatible protocols.
Installation
pip install deev # core
pip install deev[mongodb] # add MongoDB support
pip install deev[mysql] # add MySQL support1. Defining Entities
Decorate a class with @entity and annotate its fields. Use field(...) from deev to configure PK, autoincrement, nullability, validation bounds, etc.
from datetime import datetime, timezone
from typing import Optional
from deev import entity, field
@entity
class User:
id: int = field(autoincrement=True, primary_key=True, default=None)
name: str = field(max=256)
email: Optional[str] = field(default=None)
score: Optional[int] = field(default=None, min=0, max=100)
created: datetime = field(default=lambda: datetime.now(timezone.utc))field(...) parameters
| Parameter | Type | Meaning |
|---|---|---|
autoincrement |
bool |
Column is auto-increment (PK only). Default: False. |
default |
Any | Callable |
Static value or callable producing a value at instantiation. |
index |
str | IndexOptions |
Name of an index, or IndexOptions(...) descriptor. |
mapped |
bool |
If False, field is excluded from SQL translation. |
min |
int | float | Decimal |
Minimum value (strings: min length, numeric: min value). |
max |
int | float | Decimal |
Maximum value (strings: max length, numeric: max value). |
nullable |
bool |
True allows NULL. Inferred from Optional[...] automatically. |
primary_key |
bool |
Mark the field as part of the PK. |
dbtype |
str |
Override the auto-detected DB type. |
unique |
bool |
Add a unique constraint. |
validator |
Callable[[Any], ValidationError | None] |
Custom validation function. Return None to pass. |
@entity parameters
| Parameter | Type | Meaning |
|---|---|---|
table_name |
str |
Override auto-generated table name (pluralized class name). |
no_pluralization |
bool |
If True, use class name as-is (don't pluralize). |
Defining indexes
from deev import entity, field
from deev.entities import IndexOptions, IndexOrder
@entity
class Post:
id: int = field(autoincrement=True, primary_key=True)
title: str = field(index='idx_post_title') # shorthand: by index name
author: str = field(index=IndexOptions(
name='idx_post_author', direction=IndexOrder.DESCENDING, rank=1
))Custom table names and non-pluralized names
@entity(table_name='users')
class Users:
...
@entity(no_pluralization=True)
class Category:
... # table_name stays "Category", not "Categorys"2. Connecting to a Database
Use deev.connect(...) with a SQL-connection-string style string. It returns a context-managed DbConnection.
from deev import connect
conn_str = 'Server=./data/;Database=mydb.db;Provider=sqlite3'
# MySQL: 'Server=localhost;Database=mydb;UID=root;PWD=secret;Provider=mysql'
# MongoDB: 'Server=localhost;Database=mydb;Provider=mongodb'
with connect(conn_str) as db:
...Timeouts can be specified in the connection string or as kwargs:
with connect(conn_str, connect_timeout=5, command_timeout=15) as db:
...Supported providers
| Provider string | Package required |
|---|---|
sqlite3 / sqlite |
Built-in sqlite3 |
mysql / mysql.connector |
mysql-connector-python |
mongodb / pymongo |
pymongo |
Creating a database
from deev.utils import create_database
create_database(conn_str) # creates DB if it doesn't exist3. Table Adapters (CRUD)
Create a typed table adapter using Python 3.12+ generic syntax:
from deev.sqlite import SqliteTableAdapter
with connect(conn_str) as db:
table = SqliteTableAdapter[User](db)
table.create_table() # DDLThe same interface exists for MySQL (MysqlTableAdapter) and MongoDB (MongoTableAdapter).
Helper: auto-create table adapter from connection string
from deev.utils import create_table_adapter
table = create_table_adapter(User, conn_str) # auto-detects providerSqliteTableAdapter[User] methods
| Method | Signature | Returns |
|---|---|---|
create |
create(entity: User, | \*\*kwargs) -> dict[str, Any] |
{id: <pk_value>} |
read |
read(\*\*pk_kwargs) -> User | None |
Single entity, or None |
update |
update(entity: User) -> None |
— |
delete |
delete(\*\*pk_kwargs) -> None |
— |
exists |
exists(\*\*pk_kwargs) -> bool |
True if found |
upsert |
upsert(entity: User) -> dict[str, Any] |
{id: <pk_value>} |
query |
query(where?, params?, orderby?, limit?) -> Generator[...] |
Yields User objects |
create_table |
create_table() -> None |
— |
Examples
# CREATE
pk = table.create(User(name="Alice", email="a@b.com"))
# pk == {"id": 1}
# READ
user = table.read(id=1)
# UPDATE
user.email = "alice@new.com"
table.update(user)
# DELETE
table.delete(id=1)
# EXISTS
table.exists(id=1) # False
# UPSERT (insert or update on PK match)
pk = table.upsert(User(id=2, name="Bob", email="b@c.com"))
# QUERY
results = table.query(
where='score > %? AND name LIKE %?',
params=(50, 'A%'),
orderby='score DESC',
limit=10
)
for user in results:
print(user.name, user.score)Parameter syntax
All providers use normalized %? placeholders, translated automatically at runtime:
- SQLite →
? - MySQL →
%s(native prepared statements) - MongoDB → passed through
4. Translation: splat() / hydrate()
Convert between entity objects and dictionaries.
from deev.translation import splat, hydrate
# Entity → dict (useful for partial updates, logging, etc.)
user = User(name="Alice", score=42)
d = splat(user)
# {"id": 1, "name": "Alice", "email": None, "score": 42, "created": datetime(...)}
d_sql = splat(user, to_sql=True) # serialize complex types for DB storage
# Dict → entity (from DB query results)
row = {"id": 3, "name": "Charlie", "score": 77, "created": "2025-01-01T00:00:00+00:00"}
user = hydrate(User, row, from_sql=True)
# User(id=3, name="Charlie", score=77, created=datetime(...))
# Hydrate into existing entity
hydrate(user, row) # modifies in-placeSupported complex types (auto-serialized to JSON)
list, dict, set, tuple, Decimal, UUID, datetime, date, time, timedelta, bytes
Custom serialization
from deev.translation import configure_serialization
import json
configure_serialization(
encoder=MyCustomEncoder,
decoder=MyCustomDecoder
)
# or replace entirely:
configure_serialization(
serializer=my_serialize_fn,
deserializer=my_deserialize_fn
)5. Validation
Entities validate before persistence automatically. You can also validate on demand.
from deev.validation import validate, ValidationError
user = User(name="", score=200) # violates max=100
errors = validate(user)
# [ValidationError("score"): "VAL > 100"]Custom validators per field:
@entity
class Account:
pin: int = field(validator=lambda v: None if str(v).isdigit() and len(str(v)) == 4 else ValidationError("pin", "must be 4 digits"))6. Transaction Contexts
Enter a transactional scope using begin_transaction() or provider-specific contexts.
from deev.utils import begin_transaction
with begin_transaction(conn_str) as tx:
table = create_table_adapter(User, tx)
table.create(User(name="Alice"))
table.create(User(name="Bob"))
tx.commit() # required — migration writer must call this explicitlyDbTransactionContext methods
| Method | Signature | Returns |
|---|---|---|
cursor |
cursor() -> DbCursor |
— |
commit |
commit() -> None |
— |
rollback |
rollback() -> None |
— |
execute |
execute(sql, params?) -> DbCursor |
Cursor |
execute_script |
execute_script(sql) -> None |
— |
execute_nonquery |
execute_nonquery(sql, params?) -> None |
— |
execute_reader |
execute_reader(sql, params?) -> Generator[...] |
Yields tuples |
execute_scalar |
execute_scalar(sql, params?) -> Any |
Single value |
Raw SQL example
with begin_transaction(conn_str) as tx:
count = tx.execute_scalar('SELECT COUNT(*) FROM Users WHERE score > %?', (50,))
tx.rollback() # or tx.commit()7. Database Migrations with db-migrate CLI
Basic usage
# Apply migrations
db-migrate apply 'Server=./data/;Database=mydb;Provider=sqlite3' ./migrations/mydb/
# Undo last migration
db-migrate undo --stop-at "002_somemigration" 'Server=...' ./migrations/mydb/
# Undo all
db-migrate undo 'Server=...' ./migrations/mydb/
# Use a named connection from appsettings2 config
db-migrate apply my_productionCreating a migration script
# migrations/mydb/001_create_users.py
from deev.common import DbTransactionContext
from deev.utils import create_table_adapter
from myapp.entities import User
def apply(tx: DbTransactionContext) -> None:
table = create_table_adapter(User, tx)
table.create_table()
tx.commit() # REQUIRED: must call explicitly
def undo(tx: DbTransactionContext) -> None:
tx.execute_nonquery('DROP TABLE IF EXISTS Users')
tx.commit()Migration conventions
- Ordering: alphabetical by filename. Prefix with numbers:
001_,002_, ... - Tracking: applied migrations stored in
_migrationdatatable/collection. - Idempotency: the CLI skips already-applied migrations.
- Stop-at point:
--stop-at "migration_name"halts processing at that file.
8. ConnectionString
Parse and build connection strings programmatically.
from deev import ConnectionString
cs = ConnectionString('Server=localhost;Database=prod;UID=admin;PWD=s3cret;Provider=mysql')
print(cs.server) # "localhost"
print(cs.database) # "prod"
print(cs.user) # "admin"
print(cs.provider) # "mysql"
str(cs) # "Server=localhost;Database=prod;UID=admin;PWD=s3cret;Provider=mysql"Supported keys: Server, Database, UID/User/Username, PWD/Password/Pass, Provider, Connection Timeout, Command Timeout.
9. Error Handling
from deev import DbError
try:
table.create(...)
except DbError as e:
print(e.reason) # descriptive error message10. Provider Type Mappings
| Python type | SQLite | MySQL |
|---|---|---|
int |
INTEGER |
INT |
float |
REAL |
DOUBLE |
str |
TEXT |
TEXT |
bool |
INTEGER |
TINYINT |
datetime |
TEXT (ISO) |
DATETIME |
date |
TEXT (ISO) |
DATE |
bytes |
BLOB |
BLOB |
list / dict |
TEXT (JSON) |
TEXT (JSON) |
Override with dbtype="..." on any field.
11. MongoDB-specific notes
- Collections are created implicitly on first insert — no DDL needed.
- The
_migrationdatauses UUID PKs (MongoDB doesn't support auto-increment). - Native
apply(...)andundo(...)signatures are the same, but DDL auto-commit doesn't apply. - Access
mongo_client,mongo_database,mongo_sessionproperties on the transaction object for advanced operations.
def apply(tx: DbTransactionContext) -> None:
# tx.mongo_session is a pymongo.ClientSession
# tx.mongo_database is a pymongo.Database
...Quick Reference: Entry Points
Import from deev |
What it provides |
|---|---|
entity |
Decorator to define entity classes |
field |
Configure entity fields |
connect |
Create a context-managed DB connection |
ConnectionString |
Parse/build connection strings |
DbError |
DB operation error exception |
common.DbTransactionContext |
Protocol for transactional scope |
validation.validate |
On-demand entity validation |
translation.splat / hydrate |
Entity ↔ dict conversion |
utils.create_database |
Create DB if missing |
utils.create_table_adapter |
Auto-detect provider, return adapter |
utils.begin_transaction |
Start a transactional scope |
Provider adapters:
deev.sqlite.SqliteTableAdapter[Entity](db)deev.mysql.MysqlTableAdapter[Entity](db)deev.mongodb.MongoTableAdapter[Entity](db)