Skip to main content

A simple PostgreSQL to Python mapper.

Project description

halfORM

PyPI version Python versions PostgreSQL versions License Tests Coverage Downloads

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']}")

🎯 Why halfORM?

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.

⚡ Quick Start

Installation

pip install half_orm

Configuration (one-time setup)

# 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

First Steps

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())

🚀 Core Operations

CRUD Made Simple

# Create
person = Person(first_name='Alice', last_name='Smith', email='alice@example.com')
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='alice@example.com').ho_update(last_name='Johnson')

# Delete  
Person(email='alice@example.com').ho_delete()

Smart Querying

# 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

🎨 Custom Relation Classes with Foreign Key Navigation

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

🔧 Advanced Features

Transactions

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

PostgreSQL Functions & Procedures

# 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)

Query Debugging

# 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='old@example.com')
person.ho_mogrify()
person.ho_update(email='new@example.com')
# Prints the UPDATE query

# Performance analysis
count = Person().ho_count()
is_empty = Person(email='nonexistent@example.com').ho_is_empty()

SQL Trace Mode

# Enable SQL trace to see queries with caller context
db.sql_trace = True

# Now every query shows where it was called from
person = Person(last_name='Smith').ho_get()
# Output shows:
# SQL TRACE:
#   Called from: script.py:42
#   SELECT * FROM person WHERE last_name = 'Smith'

# Useful for debugging complex applications
db.sql_trace = False  # Disable when done

🏗️ Real-World Example

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='jane@collorg.org')
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()}")

📊 halfORM vs. Others

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

🎓 When to Choose halfORM

✅ Perfect For

  • 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

⚠️ Consider Alternatives If

  • 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.

📚 Documentation (WIP)

📖 Complete Documentation - Full documentation site 🚧

Quick Links

🤝 Contributing

We welcome contributions! halfORM is designed to stay simple and focused.

📈 Status & Roadmap

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

📜 License

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

Project details


Release history Release notifications | RSS feed

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

half_orm-0.17.5.tar.gz (50.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

half_orm-0.17.5-py3-none-any.whl (45.8 kB view details)

Uploaded Python 3

File details

Details for the file half_orm-0.17.5.tar.gz.

File metadata

  • Download URL: half_orm-0.17.5.tar.gz
  • Upload date:
  • Size: 50.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.8.19

File hashes

Hashes for half_orm-0.17.5.tar.gz
Algorithm Hash digest
SHA256 59e0597c5729c2c6a9099824a2e743ee528a3b5e70a3537a9e87cd7b7eb83d2c
MD5 39de5f2a8fafd128d170e591240fd742
BLAKE2b-256 36583716414bf80e50594e2e086e556e837be514c9136150322b955d8630c202

See more details on using hashes here.

File details

Details for the file half_orm-0.17.5-py3-none-any.whl.

File metadata

  • Download URL: half_orm-0.17.5-py3-none-any.whl
  • Upload date:
  • Size: 45.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.8.19

File hashes

Hashes for half_orm-0.17.5-py3-none-any.whl
Algorithm Hash digest
SHA256 c26ce39e491834164ed9f11223dad00cb62644e3c3ee93245f23f024542cf964
MD5 73c40eed96035b188d981be2e6bbd425
BLAKE2b-256 14a980a2e4cb7c4d2887db2860c80b9d2854323e662a4bec67bbec00c6a235d5

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page