First Project¶
Let's build a simple blog application to learn Oxyde's core features.
Project Structure¶
blog/
├── models.py # Database models
├── oxyde_config.py # Oxyde configuration
├── migrations/ # Migration files
├── main.py # Application logic
└── blog.db # SQLite database (auto-created)
Step 1: Initialize Project¶
Create a new directory and initialize Oxyde:
When prompted:
- Models module:
models - Dialect:
sqlite - Database URL:
sqlite:///blog.db - Migrations directory:
migrations
Step 2: Define Models¶
Create models.py:
from datetime import datetime
from oxyde import Model, Field
class Author(Model):
id: int | None = Field(default=None, db_pk=True)
name: str
email: str = Field(db_unique=True)
bio: str | None = Field(default=None)
created_at: datetime = Field(db_default="CURRENT_TIMESTAMP")
class Meta:
is_table = True
table_name = "authors"
class Post(Model):
id: int | None = Field(default=None, db_pk=True)
title: str
content: str
published: bool = Field(default=False)
views: int = Field(default=0)
author: Author | None = Field(default=None, db_on_delete="CASCADE")
created_at: datetime = Field(db_default="CURRENT_TIMESTAMP")
class Meta:
is_table = True
table_name = "posts"
class Tag(Model):
id: int | None = Field(default=None, db_pk=True)
name: str = Field(db_unique=True)
class Meta:
is_table = True
table_name = "tags"
Key concepts:
table_nameoverrides the default table namedb_default="CURRENT_TIMESTAMP"sets a SQL defaultauthor: Author | Nonecreates an optional foreign key toAuthordb_on_delete="CASCADE"deletes posts when author is deleted
Type annotations and model definition order
Oxyde uses Python type annotations to define foreign keys. The FK target class
must be already defined above the model that references it. In the example above,
Author is defined before Post, so author: Author | None works directly.
If you need to reference a model that is defined later in the file (a forward reference),
add from __future__ import annotations at the top of your module:
from __future__ import annotations # all annotations become lazy strings
class Post(Model):
author: Author | None = Field(default=None, db_on_delete="CASCADE")
# Works even though Author is defined below
class Author(Model):
...
Without this import, a forward reference like author: Author | None will raise
a NameError (Python 3.10–3.13). Starting with Python 3.14, annotations are lazy
by default and this import is no longer needed.
Alternatively, you can use a string literal inside generic types for reverse relations:
comments: list["Comment"] — this is handled by Pydantic and works on all Python versions.
Step 3: Create and Apply Migrations¶
Generate migrations from models:
Apply to create tables:
Step 4: Create the Application¶
Create main.py:
import asyncio
from oxyde import db, F, Q
from models import Author, Post, Tag
async def main():
# Connect to database
await db.init(default="sqlite:///blog.db")
try:
# Create sample data
await create_sample_data()
# Run queries
await demo_queries()
# Show statistics
await show_stats()
finally:
await db.close()
async def create_sample_data():
"""Create authors, posts, and tags."""
print("Creating sample data...")
# Create authors
alice = await Author.objects.create(
name="Alice Johnson",
email="alice@example.com",
bio="Python developer and tech writer"
)
bob = await Author.objects.create(
name="Bob Smith",
email="bob@example.com",
bio="Backend engineer"
)
# Create posts
await Post.objects.create(
title="Getting Started with Oxyde",
content="Oxyde is a high-performance async ORM...",
published=True,
author_id=alice.id
)
await Post.objects.create(
title="Advanced Query Patterns",
content="In this post, we explore advanced queries...",
published=True,
views=150,
author_id=alice.id
)
await Post.objects.create(
title="Draft: Performance Tips",
content="Work in progress...",
published=False,
author_id=bob.id
)
# Create tags
for name in ["python", "orm", "async", "tutorial"]:
await Tag.objects.create(name=name)
print("Sample data created!\n")
async def demo_queries():
"""Demonstrate various query patterns."""
print("=== Query Examples ===\n")
# Basic filtering
published = await Post.objects.filter(published=True).all()
print(f"Published posts: {len(published)}")
# Multiple conditions with Q
popular = await Post.objects.filter(
Q(published=True) & Q(views__gte=100)
).all()
print(f"Popular posts (100+ views): {len(popular)}")
# Ordering and limiting
recent = await Post.objects.filter(
published=True
).order_by("-created_at").limit(5).all()
print(f"Recent posts: {[p.title for p in recent]}")
# Get single record
author = await Author.objects.get(email="alice@example.com")
print(f"Found author: {author.name}")
# Update with F expression (atomic increment)
await Post.objects.filter(title__contains="Oxyde").update(
views=F("views") + 1
)
print("Incremented views for Oxyde posts")
# Values (return dicts instead of models)
emails = await Author.objects.values("name", "email").all()
print(f"Author emails: {emails}")
# Exists check
has_drafts = await Post.objects.filter(published=False).exists()
print(f"Has draft posts: {has_drafts}")
print()
async def show_stats():
"""Show aggregate statistics."""
print("=== Statistics ===\n")
# Count
total_posts = await Post.objects.count()
print(f"Total posts: {total_posts}")
# Sum
total_views = await Post.objects.sum("views")
print(f"Total views: {total_views}")
# Average
avg_views = await Post.objects.avg("views")
print(f"Average views: {avg_views:.1f}")
# Count with filter
published_count = await Post.objects.filter(published=True).count()
print(f"Published posts: {published_count}")
if __name__ == "__main__":
asyncio.run(main())
Step 5: Run the Application¶
Expected output:
Creating sample data...
Sample data created!
=== Query Examples ===
Published posts: 2
Popular posts (100+ views): 1
Recent posts: ['Advanced Query Patterns', 'Getting Started with Oxyde']
Found author: Alice Johnson
Incremented views for Oxyde posts
Author emails: [{'name': 'Alice Johnson', 'email': 'alice@example.com'}, ...]
Has draft posts: True
=== Statistics ===
Total posts: 3
Total views: 151
Average views: 50.3
Published posts: 2
Step 6: Add Transactions¶
For operations that must succeed or fail together, use transactions:
from oxyde.db import transaction
async def transfer_post(post_id: int, new_author_id: int):
"""Transfer a post to a different author (atomic operation)."""
async with transaction.atomic():
# Get the post
post = await Post.objects.get(id=post_id)
# Verify new author exists
new_author = await Author.objects.get(id=new_author_id)
# Update the post
post.author_id = new_author_id
await post.save()
print(f"Transferred '{post.title}' to {new_author.name}")
If any operation fails, the entire transaction rolls back.
Step 7: Add Joins¶
Load related data efficiently:
async def show_posts_with_authors():
"""Load posts with their authors in a single query."""
posts = await Post.objects.join("author").filter(published=True).all()
for post in posts:
print(f"'{post.title}' by {post.author.name}")
Next Steps¶
Now that you have a working project:
- Models — Learn all field types and options
- Filtering — Master the filter syntax
- Transactions — Understand transaction handling
- Relations — Work with foreign keys and joins