relix
relix is a small SQLite ORM built around Python dataclasses. Models behave like ordinary dataclass objects while providing simple persistence, querying, foreign keys, transactions, datetime conversion, and JSON-backed container fields.
Features
Defining a model
Subclass Model and decorate the class with @dataclass.
from dataclasses import dataclass
from relix import Model
@dataclass
class User(Model):
name: str
email: str
is_admin: bool = False
Every model automatically has an optional integer id.
New objects start without an ID:
user = User(
name="Alice",
email="alice@example.com",
)
print(user.id) # None
The ID is assigned when the object is inserted into SQLite.
Initializing a database
Call Database.init() after defining your models.
from relix import Database
database = Database.init("app.db")
This registers the database with Model and creates tables for the currently registered models.
For an in-memory database:
database = Database.init(":memory:")
Models should be defined before calling Database.init():
from dataclasses import dataclass
from relix import Database, Model
@dataclass
class User(Model):
name: str
database = Database.init("app.db")
Saving a model
Call save() to insert a new object.
user = User(
name="Alice",
email="alice@example.com",
)
user.save()
print(user.id)
After insertion, user.id contains the SQLite row ID.
Calling save() again updates the existing row:
user.name = "Alice Smith"
user.save()
Getting a model by ID
Use get() to retrieve a row by primary key.
user = User.get(42)
if user is not None:
print(user.name)
get() returns None when the row does not exist.
Getting all models
Use all() to retrieve every row.
users = User.all()
for user in users:
print(user.name)
Filtering models
Use where() with model fields to build a query.
admins = User.where(
User.is_admin == True
).all()
Comparison operators can be used directly:
User.where(User.id == 10)
User.where(User.id != 10)
User.where(User.id < 10)
User.where(User.id <= 10)
User.where(User.id > 10)
User.where(User.id >= 10)
where() returns a query object, so the database is not read until a result method such as all(), first(), or count() is called.
Combining conditions
Use & for AND:
users = User.where(
(User.is_admin == True) &
(User.name == "Alice")
).all()
Use | for OR:
users = User.where(
(User.name == "Alice") |
(User.name == "Bob")
).all()
Multiple arguments to where() are also combined with AND:
users = User.where(
User.is_admin == True,
User.name == "Alice",
).all()
Getting the first result
Use first() when only one matching row is needed.
user = User.where(
User.email == "alice@example.com"
).first()
if user is not None:
print(user.name)
first() returns None when no row matches.
Counting rows
Use count() to count matching records without loading them.
admin_count = User.where(
User.is_admin == True
).count()
To count every row:
user_count = User.where().count()
Limiting queries
Use limit() to restrict the number of results.
users = User.where(
User.is_admin == True
).limit(10).all()
Query methods can be chained.
Ordering queries
Pass a model field to order_by() for ascending order.
users = User.where().order_by(
User.name
).all()
Use asc() or desc() explicitly when needed:
users = User.where().order_by(
User.name.asc()
).all()
users = User.where().order_by(
User.name.desc()
).all()
Ordering and limiting can be combined:
users = (
User.where(User.is_admin == True)
.order_by(User.name)
.limit(10)
.all()
)
Deleting models
Call delete() on a saved model.
user = User.get(42)
if user is not None:
user.delete()
After deletion, the object's id is reset to None.
Nullable fields
Use Optional for nullable columns.
from dataclasses import dataclass
from typing import Optional
from relix import Model
@dataclass
class User(Model):
name: str
nickname: Optional[str] = None
Nullable values can be queried:
users = User.where(
User.nickname == None
).all()
and:
users = User.where(
User.nickname != None
).all()
These are translated into the appropriate SQLite NULL comparisons.
Basic field types
relix maps common Python types to SQLite automatically.
from dataclasses import dataclass
from relix import Model
@dataclass
class Example(Model):
count: int
enabled: bool
score: float
name: str
contents: bytes
The corresponding SQLite storage types are selected automatically.
Datetime fields
datetime values are stored as Unix timestamps.
from dataclasses import dataclass
from datetime import datetime
from relix import Model
@dataclass
class Event(Model):
name: str
created_at: datetime
event = Event(
name="Example",
created_at=datetime.now(),
)
event.save()
When the row is loaded, the timestamp is converted back into a datetime.
Datetime values can also be used in queries:
events = Event.where(
Event.created_at >= datetime(2025, 1, 1)
).all()
SQLite stores datetime values as REAL, preserving fractional seconds.
Dictionary fields
Fields typed as dict are automatically serialized as JSON.
from dataclasses import dataclass
from relix import Model
@dataclass
class Document(Model):
metadata: dict
Use them as ordinary dictionaries:
document = Document(
metadata={
"author": "Alice",
"published": True,
},
)
document.save()
When the model is loaded, the JSON is converted back into a Python dictionary.
List fields
Fields typed as list are automatically serialized as JSON too.
from dataclasses import dataclass
from relix import Model
@dataclass
class Document(Model):
tags: list
For example:
document = Document(
tags=[
"python",
"sqlite",
"orm",
],
)
document.save()
Dictionary and list subclasses
Subclasses of dict and list can be used as model fields.
from dataclasses import dataclass
from relix import Model
class Settings(dict):
pass
class Tags(list):
pass
@dataclass
class Document(Model):
settings: Settings
tags: Tags
When a row is loaded, the values are reconstructed using the declared subclass:
document = Document(
settings=Settings({
"theme": "dark",
}),
tags=Tags([
"python",
"sqlite",
]),
)
document.save()
loaded = Document.get(document.id)
print(type(loaded.settings))
print(type(loaded.tags))
Foreign keys
Use ForeignKey to reference another model.
from dataclasses import dataclass
from relix import ForeignKey, Model
@dataclass
class User(Model):
name: str
@dataclass
class Post(Model):
title: str
author: ForeignKey[User]
Save the referenced object first:
user = User(
name="Alice",
)
user.save()
post = Post(
title="Hello",
author=user,
)
post.save()
SQLite stores the relationship using an integer foreign-key column.
For this example, the column is named:
author_id
Loading foreign keys
Foreign-key fields return the related model object.
post = Post.get(1)
if post is not None:
print(post.author.name)
The related object is loaded lazily when the relationship is first accessed.
The raw ID is also available:
print(post.author_id)
This does not require loading the related User.
Querying foreign keys
A foreign key can be queried using the related object.
posts = Post.where(
Post.author == user
).all()
The underlying ID column can also be queried directly:
posts = Post.where(
Post.author_id == user.id
).all()
Nullable foreign keys
Use Optional for nullable relationships.
from dataclasses import dataclass
from typing import Optional
from relix import ForeignKey, Model
@dataclass
class Employee(Model):
name: str
manager: Optional[ForeignKey["Employee"]] = None
A foreign-key value of None is stored as SQL NULL.
Circular foreign keys
String foreign-key targets allow models to refer to classes that have not yet been defined.
A model can refer to itself:
from dataclasses import dataclass
from typing import Optional
from relix import ForeignKey, Model
@dataclass
class Employee(Model):
name: str
manager: Optional[ForeignKey["Employee"]] = None
Two different models can also reference each other:
@dataclass
class User(Model):
name: str
team: Optional[ForeignKey["Team"]] = None
@dataclass
class Team(Model):
name: str
owner: ForeignKey[User]
The target name is resolved against the models registered with Model.
Transactions
Use transaction() when several operations must succeed or fail together.
from relix import Database
database = Database.init("app.db")
with database.transaction():
alice = User(
name="Alice",
email="alice@example.com",
)
alice.save()
bob = User(
name="Bob",
email="bob@example.com",
)
bob.save()
If the block completes normally, the transaction is committed.
If an exception leaves the block, the transaction is rolled back.
Accessing the database directly
Database.init() returns the configured database instance.
from relix import Database
database = Database.init("app.db")
This gives application code access to lower-level database functionality such as transactions and explicit connection management when necessary.
Model registration
Every Model subclass is registered automatically.
from dataclasses import dataclass
from relix import Model
@dataclass
class User(Model):
name: str
@dataclass
class Post(Model):
title: str
The registered model classes are available through:
Model.subclasses
relix uses this registry when creating tables and resolving string foreign-key references.
DotDict
relix also provides DotDict, a dictionary with attribute access and mutation tracking.
from relix import DotDict
settings = DotDict({
"theme": "dark",
})
print(settings.theme)
settings.theme = "light"
print(settings.changed)
Dictionary-style access still works:
settings["theme"] = "dark"
Call mark_clean() after persisting the value:
settings.mark_clean()
print(settings.changed)
Nested dictionaries are wrapped so nested changes can also be tracked:
settings.profile = {
"name": "Alice",
}
settings.mark_clean()
settings.profile.name = "Alice Smith"
print(settings.changed)
DotDict is useful when an application needs to know whether mutable dictionary state actually needs to be persisted.
A small application
A complete relix application can stay very small.
from dataclasses import dataclass
from datetime import datetime
from typing import Optional
from relilx import Database, ForeignKey, Model
@dataclass
class User(Model):
username: str
display_name: str
@dataclass
class Post(Model):
author: ForeignKey[User]
title: str
body: str
created_at: datetime
database = Database.init("app.db")
alice = User(
username="alice",
display_name="Alice",
)
alice.save()
post = Post(
author=alice,
title="Hello",
body="My first post.",
created_at=datetime.now(),
)
post.save()
posts = (
Post.where(Post.author == alice)
.order_by(Post.created_at.desc())
.all()
)
for post in posts:
print(post.title)
relix deliberately keeps its scope small. It provides straightforward dataclass persistence and querying without attempting to reproduce the feature set or abstraction level of a larger ORM.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file relix-0.1.0.tar.gz.
File metadata
- Download URL: relix-0.1.0.tar.gz
- Upload date:
- Size: 15.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1898db59f67f7d037f304dadbf69304b0ac52001bfc90cc44e12a8bb96f0d809
|
|
| MD5 |
69c05a6b82b3681eccb62e06de31d541
|
|
| BLAKE2b-256 |
3bc235252ddcc0515dde15b3d44719dbed88ee73e3d2a6f7db636356fab3139f
|
Provenance
The following attestation bundles were made for relix-0.1.0.tar.gz:
Publisher:
publish.yml on mizuki-hikaru/relix
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
relix-0.1.0.tar.gz -
Subject digest:
1898db59f67f7d037f304dadbf69304b0ac52001bfc90cc44e12a8bb96f0d809 - Sigstore transparency entry: 2733903607
- Sigstore integration time:
-
Permalink:
mizuki-hikaru/relix@0227056ff7fc41cc8e0fa3c7d29416841edddae9 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/mizuki-hikaru
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@0227056ff7fc41cc8e0fa3c7d29416841edddae9 -
Trigger Event:
push
-
Statement type:
File details
Details for the file relix-0.1.0-py3-none-any.whl.
File metadata
- Download URL: relix-0.1.0-py3-none-any.whl
- Upload date:
- Size: 13.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6679c6ca26351a17268dfb3680229f5e3f44e5ad0b2ef69b5a7e6fbaf928aa51
|
|
| MD5 |
bdc55ab8e20287d36c0e706b660ec8d2
|
|
| BLAKE2b-256 |
5480578f1d1c16010e650199e9c31d1b3c177c4f5810a5da8421d5d00ee30541
|
Provenance
The following attestation bundles were made for relix-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on mizuki-hikaru/relix
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
relix-0.1.0-py3-none-any.whl -
Subject digest:
6679c6ca26351a17268dfb3680229f5e3f44e5ad0b2ef69b5a7e6fbaf928aa51 - Sigstore transparency entry: 2733903615
- Sigstore integration time:
-
Permalink:
mizuki-hikaru/relix@0227056ff7fc41cc8e0fa3c7d29416841edddae9 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/mizuki-hikaru
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@0227056ff7fc41cc8e0fa3c7d29416841edddae9 -
Trigger Event:
push
-
Statement type: