The PostgreSQL-native ORM that stays out of your way
halfORM lets you keep your database schema in SQL where it belongs, while giving you the comfort of Python for data manipulation. No migrations, no schema conflicts, no ORM fighting β just PostgreSQL and Python working together.
from half_orm.model import Model
# Connect to your existing database
blog = Model('blog_db')
# Work with your existing tables instantly
Post = blog.get_relation_class('blog.post')
Author = blog.get_relation_class('blog.author')
# Clean, intuitive operations
post = Post(title='Hello halfORM!', content='Simple and powerful.')
result = post.ho_insert()
print(f"Created post #{result['id']}")
Database-First Approach: Your PostgreSQL schema is the source of truth. halfORM adapts to your database, not the other way around.
SQL Transparency: See exactly what queries are generated with ho_mogrify()
. No mysterious SQL, no query surprises.
PostgreSQL Native: Use views, triggers, stored procedures, and advanced PostgreSQL features without compromise.
pip install half_orm
# Create config directory
mkdir ~/.half_orm
export HALFORM_CONF_DIR=~/.half_orm
# Create connection file: ~/.half_orm/my_database
echo "[database]
name = my_database
user = username
password = password
host = localhost
port = 5432" > ~/.half_orm/my_database
from half_orm.model import Model
# Connect to your database
db = Model('my_database')
# See all your tables and views
print(db)
# Create a class for any table
Person = db.get_relation_class('public.person')
# See the table structure
print(Person())
# Create
person = Person(first_name='Alice', last_name='Smith', email='[email protected]')
result = person.ho_insert()
# Read
for person in Person(last_name='Smith').ho_select():
print(f"{person['first_name']} {person['last_name']}")
# Update
Person(email='[email protected]').ho_update(last_name='Johnson')
# Delete
Person(email='[email protected]').ho_delete()
# No .filter() method needed - the object IS the filter
young_people = Person(birth_date=('>', '1990-01-01'))
gmail_users = Person(email=('ilike', '%@gmail.com'))
# Navigate and constrain in one step
alice_posts = Post().author_fk(name=('ilike', 'alice%'))
# Chainable operations
recent_posts = (Post(is_published=True)
.ho_order_by('created_at desc')
.ho_limit(10)
.ho_offset(20))
# Set operations
active_or_recent = active_users | recent_users
power_users = premium_users & active_users
Override generic relation classes with custom implementations containing business logic and personalized foreign key mappings:
from half_orm.model import Model, register
from half_orm.relation import singleton
blog = Model('blog_db')
@register
class Author(blog.get_relation_class('blog.author')):
Fkeys = {
'posts_rfk': '_reverse_fkey_blog_post_author_id',
'comments_rfk': '_reverse_fkey_blog_comment_author_id',
}
@singleton
def create_post(self, title, content):
"""Create a new blog post for this author."""
return self.posts_rfk(title=title, content=content).ho_insert()
@singleton
def get_author_s_recent_posts(self, limit=10):
"""Get author's most recent posts."""
return self.posts_rfk().ho_order_by('published_at desc').ho_limit(limit).ho_select()
def get_recent_posts(self, limit=10):
"""Get most recent posts."""
return self.posts_rfk().ho_order_by('published_at desc').ho_limit(limit).ho_select()
@register
class Post(blog.get_relation_class('blog.post')):
Fkeys = {
'author_fk': 'author_id',
'comments_rfk': '_reverse_fkey_blog_comment_post_id',
}
def publish(self):
"""Publish this post."""
from datetime import datetime
self.published_at.value = datetime.now()
return self.ho_update()
# This returns your custom Author class with all methods!
post = Post(title='Welcome').ho_get()
author = post.author_fk().ho_get() # Instance of your custom Author class
# Use your custom methods
author.create_post("New Post", "Content here")
recent_posts = author.get_recent_posts(5)
# Chain relationships seamlessly
author.posts_rfk().comments_rfk().author_fk() # The authors that commented any post of the author
from half_orm.relation import transaction
class Author(db.get_relation_class('blog.author')):
@transaction
def create_with_posts(self, posts_data):
# Everything in one transaction
author_result = self.ho_insert()
for post_data in posts_data:
Post(author_id=author_result['id'], **post_data).ho_insert()
return author_result
# Execute functions
results = db.execute_function('my_schema.calculate_stats', user_id=123)
# Call procedures
db.call_procedure('my_schema.cleanup_old_data', days=30)
# See the exact SQL being generated
person = Person(last_name=('ilike', 'sm%'))
person.ho_mogrify()
list(person.ho_select()) # or simply list(person)
# Prints: SELECT * FROM person WHERE last_name ILIKE 'sm%'
# Works with all operations
person = Person(email='[email protected]')
person.ho_mogrify()
person.ho_update(email='[email protected]')
# Prints the UPDATE query
# Performance analysis
count = Person().ho_count()
is_empty = Person(email='[email protected]').ho_is_empty()
from half_orm.model import Model, register
from half_orm.relation import singleton
# Blog application
blog = Model('blog')
@register
class Author(blog.get_relation_class('blog.author')):
Fkeys = {
'posts_rfk': '_reverse_fkey_blog_post_author_id'
}
@singleton
def create_post(self, title, content):
return self.posts_rfk(title=title, content=content).ho_insert()
@register
class Post(blog.get_relation_class('blog.post')):
Fkeys = {
'author_fk': 'author_id',
'comments_rfk': '_reverse_fkey_blog_comment_post_id'
}
# Usage
author = Author(name='Jane Doe', email='[email protected]')
if author.ho_is_empty():
author.ho_insert()
# Create post through relationship
post_data = author.create_post(
title='halfORM is Awesome!',
content='Here is why you should try it...'
)
post = Post(**post_data)
print(f"Published: {post.title.value}")
print(f"Comments: {post.comments_rfk().ho_count()}")
Feature | SQLAlchemy | Django ORM | Peewee | halfORM |
---|---|---|---|---|
Learning Curve | Steep | Moderate | Gentle | Minimal |
SQL Control | Limited | Limited | Good | Complete |
Custom Business Logic | Classes/Mixins | Model Methods | Model Methods | @register decorator |
Database Support | Multi | Multi | Multi | PostgreSQL only |
PostgreSQL-Native | Partial | Partial | No | β Full |
Database-First | No | No | Partial | β Native |
Setup Complexity | High | Framework | Low | Ultra-Low |
Best For | Complex Apps | Django Web | Multi-DB Apps | PostgreSQL + Python |
- PostgreSQL-centric applications - You want to leverage PostgreSQL's full power
- Existing database projects - You have a schema and want Python access
- SQL-comfortable teams - You prefer SQL for complex queries and logic
- Rapid prototyping - Get started in seconds, not hours
- Microservices - Lightweight, focused ORM without framework baggage
- Multi-database support needed - halfORM is PostgreSQL-only
- Django ecosystem - Django ORM integrates better with Django
- Team prefers code-first - You want to define models in Python
- Heavy ORM features needed - You need advanced ORM patterns like lazy loading, identity maps, etc.
π Complete Documentation - Full documentation site π§
- π Quick Start - Get running in 5 minutes
- π Tutorial - Step-by-step learning path (WIP)
- π API Reference - Complete method documentation (WIP)
We welcome contributions! halfORM is designed to stay simple and focused.
- Issues - Bug reports and feature requests
- Discussions - Questions and community
- Contributing Guide - How to contribute code
halfORM is actively maintained and used in production. Current focus:
- β Stable API - Core features are stable since v0.8
- π Performance optimizations - Query generation improvements
- π Documentation expansion - More examples and guides
- π§ͺ Advanced PostgreSQL features - Better support for newer PostgreSQL versions
halfORM is licensed under the LGPL-3.0 license.
"Database-first development shouldn't be this hard. halfORM makes it simple."
Made with β€οΈ for PostgreSQL and Python developers