返回资讯中心
外部精选
软件工程
#开发者实时流

DoveORM – Small, security-hardened Python ORM with built-in migrations

A lightweight, expressive, security-hardened Python ORM.

Hacker News 最新tituskilunda2026/10/27 · 今天30 分钟阅读

以下正文同步自 Hacker News 最新,版权归原站所有,已转换为易读排版。

A lightweight, expressive, security-hardened Python ORM.

Python 3.9+ · Zero dependencies for SQLite · mysqlclient for MySQL · psycopg for PostgreSQL · Works with Flask, FastAPI, or standalone

Quick Start • Configuration • Models • Fields • Querying • Instances • Relationships • Transactions • Migrations • CLI • Drivers • Security

DoveORM is a lightweight yet fully-featured Python ORM with validated models, chainable queries, relationships and migrations behind one small, consistent API.

Feature

@table() decorator and class Meta model declarations ✅

Dynamic finders — findByEmail, findAllByActive, countBy…, existsBy… ✅

Chainable, lazy QuerySet with Q objects, result caching and slicing ✅

30+ field types with validation ✅

SQLite, MySQL/MariaDB and PostgreSQL behind a pluggable driver interface ✅

Thread-safe & fork-safe connections, nested transactions (SAVEPOINTs) ✅

Deadlock-retrying transactions — db.run_in_transaction(fn) ✅

Migrations — autodetected, optimized, reversible, squashable ✅

doveorm command — generate, apply, revert, status, doctor, console… ✅

pip-installable, standalone — no web framework required ✅

Optimistic locking, mass-assignment protection, sensitive fields ✅

Injection-hardened — bound parameters, validated identifiers, strict SQL modes ✅

Explicit configuration — the ORM never guesses where your data lives ✅

🚀 Quick Start

Installation

DoveORM is a normal pip package (Python 3.9+):

pip install doveorm

with MySQL / MariaDB support (adds mysqlclient)

pip install "doveorm[mysql]"

with PostgreSQL support (adds psycopg 3)

pip install "doveorm[postgres]"

In a requirements.txt:

doveorm[postgres]==0.8.0

Installing gives you the doveorm package and the doveorm command. SQLite needs nothing else. PostgreSQL uses psycopg (version 3) and falls back to psycopg2 if that is what you already have installed. On Python 3.9/3.10 pip also installs tomli, which the command uses to read its config file.

from doveorm import Model, configure, CharField, IntegerField

configure(DATABASE_PATH="app.db") # or ":memory:" for a throw-away database

class User(Model): name = CharField(max_length=100, nullable=False) age = IntegerField(default=0)

User.create_table()

user = User.create(name="Titus", age=25) print(user.id) # 1 (auto-generated primary key)

user.age = 26 user.save() # UPDATE

adults = User.where(age__gte=18).order_by("name").get() user.delete()

Hello World — MySQL

import os from doveorm import Model, configure, CharField, FloatField

configure( DATABASE_ENGINE="mysql", DATABASE_NAME="myapp", DATABASE_HOST="127.0.0.1", DATABASE_USER="app", DATABASE_PASSWORD=os.environ["DB_PASSWORD"], # never hard-code secrets )

class Product(Model): name = CharField(max_length=200) price = FloatField(default=0.0)

Product.create_table() Product.create(name="Widget", price=9.99)

Hello World — PostgreSQL

import os from doveorm import Model, configure, CharField, DecimalField

configure( DATABASE_ENGINE="postgresql", DATABASE_NAME="myapp", DATABASE_HOST="127.0.0.1", # or a socket dir: "/var/run/postgresql" DATABASE_USER="app", DATABASE_PASSWORD=os.environ["DB_PASSWORD"], )

class Product(Model): name = CharField(max_length=200) price = DecimalField(max_digits=10, decimal_places=2, default=0)

Product.create_table() Product.create(name="Widget", price="9.99")

⚙️ Configuration

Your application owns the configuration; the ORM only consumes it. The ORM ships defaults for its own behaviour (logging, timeouts, strictness) but never for where your data lives:

import os from doveorm import configure

SQLite

db = configure( DATABASE_ENGINE="sqlite", # "sqlite" (default), "mysql" or "postgresql" DATABASE_PATH="app.db", # file path or ":memory:" LOG_QUERIES=True, # log every SQL statement (values redacted) )

MySQL

db = configure( DATABASE_ENGINE="mysql", DATABASE_NAME="myapp", DATABASE_HOST="127.0.0.1", DATABASE_PORT=3306, DATABASE_USER="app", DATABASE_PASSWORD=os.environ["DB_PASSWORD"], DATABASE_OPTIONS={"ssl_mode": "REQUIRED", "isolation_level": "READ COMMITTED"}, )

PostgreSQL

db = configure( DATABASE_ENGINE="postgresql", # "postgres" and "django.db.backends.postgresql" work too DATABASE_NAME="myapp", DATABASE_HOST="127.0.0.1", DATABASE_PORT=5432, # optional: 5432 is the default DATABASE_USER="app", DATABASE_PASSWORD=os.environ["DB_PASSWORD"], DATABASE_OPTIONS={"sslmode": "require", "schema": "app", "statement_timeout": 30}, )

  1. DATABASES dict

import os from doveorm import configure

configure(DATABASES={ "default": { "ENGINE": "mysql", "NAME": "myapp", "HOST": "127.0.0.1", "PORT": 3306, "USER": "app", "PASSWORD": os.environ["DB_PASSWORD"], } })

PostgreSQL works the same way:

"ENGINE": "postgresql" (or "django.db.backends.postgresql"), "PORT": 5432

  1. Environment variables

export DOVEORM_DATABASE_ENGINE=mysql export DOVEORM_DATABASE_NAME=myapp export DOVEORM_DATABASE_HOST=127.0.0.1 export DOVEORM_DATABASE_USER=app export DOVEORM_DATABASE_PASSWORD='...' # from your secret store export DOVEORM_DATABASE_OPTIONS='{"ssl_mode": "REQUIRED"}' # dict settings are JSON

PostgreSQL: DOVEORM_DATABASE_ENGINE=postgresql, DOVEORM_DATABASE_OPTIONS='{"sslmode": "require"}'

from doveorm import configure_from_env db = configure_from_env() # reads DOVEORM_* variables

  1. From your own settings module

settings.py (part of your application)

import os ORM_SETTINGS = { "DATABASE_ENGINE": "sqlite", "DATABASE_PATH": os.environ.get("APP_DB_PATH", "production.db"), "LOG_QUERIES": False, }

from doveorm import configure from settings import ORM_SETTINGS db = configure(**ORM_SETTINGS)

configure_from_file() still works but is deprecated: it executes the file it is given.

For tests or several databases in one process, skip configure() and bind models directly:

from doveorm import Database

db = Database(":memory:") # or Database("app.db"), or Database(driver=...) User.use(db) User.create_table()

All configuration options

Setting Default Description

DATABASE_ENGINE "sqlite" "sqlite", "mysql" or "postgresql"

DATABASE_PATH required (SQLite) SQLite file path, or ":memory:"

DATABASE_NAME required (MySQL / PostgreSQL) Database name / alias for the path (SQLite)

DATABASE_HOST required (MySQL / PostgreSQL) Server host. MySQL: or DATABASE_OPTIONS["unix_socket"]; PostgreSQL: or a socket directory

DATABASE_PORT engine default 3306 for MySQL, 5432 for PostgreSQL

DATABASE_USER required (MySQL / PostgreSQL) Database user

DATABASE_PASSWORD required (MySQL / PostgreSQL) Password ("" must be explicit)

DATABASE_CHARSET "utf8mb4" MySQL charset (PostgreSQL always uses UTF8)

DATABASE_OPTIONS {} Extra driver options — see Drivers

LOG_QUERIES False Log every SQL statement to the doveorm logger

LOG_QUERY_PARAMS False Also log bound values (may expose secrets / PII)

AUTO_WIRE_MODELS True Bind every model to the configured database automatically

USE_TZ False DateTimeField returns aware UTC datetimes

📐 Defining Models

Style 1: @table() decorator

from doveorm import Model, table, CharField, BooleanField, HasMany

@table( name="users", # explicit table name indexes=[("username",), ("email",)], # (composite) indexes unique_together=[("email",)], # unique constraints ordering=["-id"], # default ORDER BY for every query engine="InnoDB", # MySQL engine charset="utf8mb4", # MySQL charset ) class User(Model): username = CharField(max_length=80, nullable=False, unique=True) email = CharField(max_length=120, nullable=False, unique=True) active = BooleanField(default=True)

posts = HasMany("Post", foreign_key="author_id")

Style 2: class Meta

from doveorm import Model, CharField, TextField, BooleanField, ForeignKey, DateTimeField

class Post(Model): title = CharField(max_length=200, nullable=False) body = TextField(nullable=False) published = BooleanField(default=False) author_id = ForeignKey("User", on_delete="CASCADE", nullable=False) created_at = DateTimeField(auto_now_add=True, nullable=True)

class Meta: table_name = "posts" indexes = [("author_id",), ("published",)] ordering = ["-created_at"]

If both are present, @table() values win. Supported options: table_name / name, indexes, unique_together, ordering, engine, charset, version_field, migrate, and abstract (Meta only).

class Tag(Model): name = CharField(max_length=50, unique=True)

Table name: "tags" (pluralized snake_case)

Primary key: id = AutoField() is injected automatically

Abstract base models

class TimeStamped(Model): created_at = DateTimeField(auto_now_add=True, nullable=True) updated_at = DateTimeField(auto_now=True, nullable=True)

class Meta: abstract = True # no table, cannot be instantiated or queried

class Article(TimeStamped): title = CharField(max_length=200)

Models that migrations should not touch (migrate = False)

Use migrate = False for a table that DoveORM must not create, alter or drop: a database view, or a table owned by another service or a legacy system. The model works normally for queries, saves and relationships, but doveorm generate, verify and the schema checks ignore it, and doveorm models lists it as external.

@table(name="legacy_customers", migrate=False) # or: class Meta: migrate = False class LegacyCustomer(Model): name = CharField(max_length=100)

You create the table yourself, so it must already exist before you query it. A model that is migrated cannot have a ForeignKey to a migrate = False model: use a plain integer or UUID field for the key instead. Unlike abstract, the option is not inherited by subclasses.

import uuid from doveorm import Model, UUIDField, CharField

class Ticket(Model): id = UUIDField(primary_key=True, default=uuid.uuid4) # pass the callable title = CharField(max_length=200)

Ticket.create_table() ticket = Ticket.create(title="Bug report") print(ticket.id) # UUID('a1b2c3d4-...')

Table name rules

Class name Auto table name

User users

Category categories

Address addresses

BlogPost blog_posts

Naming rules (enforced)

Table, column, index and join-table names must be plain identifiers ([A-Za-z_][A-Za-z0-9_]*, ≤ 64 chars).

Reserved words (order, user, group, limit, …) and mixed-case names (firstName) work as table and column names on every backend: DoveORM quotes a name only when it has to ("order", or order on MySQL), so ordinary names produce plain, unquoted SQL. On PostgreSQL a mixed-case name is case-sensitive. Keep names to 63 characters there; PostgreSQL truncates longer ones.

Field names may not start with _ and may not shadow the Model API (save, delete, where, to_dict, …).

A model may declare at most one primary key.

📋 Fields Reference

Auto-increment primary keys

Field SQLite MySQL PostgreSQL

AutoField() INTEGER PRIMARY KEY AUTOINCREMENT INT PRIMARY KEY AUTO_INCREMENT INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY

BigAutoField() INTEGER PRIMARY KEY AUTOINCREMENT BIGINT PRIMARY KEY AUTO_INCREMENT BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY

SmallAutoField() INTEGER PRIMARY KEY AUTOINCREMENT SMALLINT PRIMARY KEY AUTO_INCREMENT SMALLINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY

An explicit value (User(id=99)) is inserted as given, and the next generated key continues after it on every backend: PostgreSQL's identity sequence is moved forward automatically (never backwards). Rows inserted with raw SQL bypass this; after a raw bulk import on PostgreSQL, run SELECT setval(pg_get_serial_sequence('users', 'id'), (SELECT MAX(id) FROM users));

Field SQLite MySQL PostgreSQL Validation

IntegerField() INTEGER INT INTEGER 32-bit range; bool rejected

BigIntegerField() INTEGER BIGINT BIGINT 64-bit range

SmallIntegerField() INTEGER SMALLINT SMALLINT 16-bit range

PositiveIntegerField() / PositiveSmallIntegerField() / PositiveBigIntegerField() INTEGER INT / SMALLINT / BIGINT INTEGER / SMALLINT / BIGINT ≥ 0

FloatField() REAL DOUBLE DOUBLE PRECISION finite numbers only

DecimalField(max_digits=10, decimal_places=2) TEXT DECIMAL(m,d) NUMERIC(m,d) dig

正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。

打开原文
去社区讨论 →