Skip to main content

A Django-style ORM for PostgreSQL and SQLite with migrations, relationships, and a friendly API.

Project description

PookieDB ORM

A Django-style Python ORM for PostgreSQL and SQLite - with auto migrations, relationships, a chainable QuerySet API, and a friendly CLI.

Python Version License: MIT Documentation Status

Installation

pip install pookiedb
# PostgreSQL support is included via psycopg2-binary

Quick Start

1. Connect to your database

import pookiedb

# SQLite
pookiedb.connect("sqlite:///mydb.sqlite3")

# PostgreSQL
pookiedb.connect("postgresql://postgres:password@localhost:5432/mydb")

# Or using kwargs
pookiedb.connect(engine="postgresql", name="mydb", host="localhost", user="postgres", password="secret")

2. Define models

import pookiedb

class Author(pookiedb.Model):
    name = pookiedb.CharField(max_length=100)
    email = pookiedb.EmailField(unique=True)
    bio = pookiedb.TextField(null=True, blank=True)
    joined = pookiedb.DateTimeField(auto_now_add=True)

    class Meta:
        db_table = "authors"
        ordering = ["-joined"]


class Post(pookiedb.Model):
    title = pookiedb.CharField(max_length=200)
    slug = pookiedb.SlugField(unique=True)
    body = pookiedb.TextField()
    author = pookiedb.ForeignKey(Author, on_delete=pookiedb.CASCADE)
    published = pookiedb.BooleanField(default=False)
    metadata = pookiedb.JSONField(null=True)
    tags = pookiedb.ManyToManyField("Tag", related_name="posts")
    created_at = pookiedb.DateTimeField(auto_now_add=True)

    class Meta:
        db_table = "posts"


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

    class Meta:
        db_table = "tags"

3. Create tables

Author.create_table()
Tag.create_table()
Post.create_table()

QuerySet API

Pookie's QuerySet is lazy and chainable - queries only hit the database when you iterate or evaluate.

# Create
alice = Author.objects.create(name="Alice", email="alice@example.com")

# Get
author = Author.objects.get(email="alice@example.com")

# Filter (chained)
posts = (
    Post.objects
    .filter(published=True)
    .filter(author=alice)
    .order_by("-created_at")
    .limit(10)
)

# Exclude
drafts = Post.objects.exclude(published=True)

# Q objects for OR / AND / NOT logic
from pookiedb.queryset.queryset import Q

results = Post.objects.filter(
    Q(title__icontains="python") | Q(tags__name="python")
)

# get_or_create / update_or_create
tag, created = Tag.objects.get_or_create(name="django")

# Slicing
first_five = Post.objects.all()[:5]

# values / values_list
names = Author.objects.values("name", "email")
ids = Post.objects.values_list("id", flat=True)

# Aggregates
from pookiedb.queryset.queryset import Sum, Avg, Count
stats = Post.objects.aggregate(total=Count("id"), avg_len=Avg("id"))

# Bulk create
posts = Post.objects.bulk_create([
    Post(title="Post 1", slug="post-1", body="...", author=alice),
    Post(title="Post 2", slug="post-2", body="...", author=alice),
])

# Raw SQL
authors = Author.objects.raw("SELECT * FROM authors WHERE name ILIKE %s", ["%ali%"])

# Delete
Post.objects.filter(published=False).delete()

# Bulk update
Post.objects.filter(author=alice).bulk_update(published=True)

Relationships

ForeignKey

post = Post.objects.get(id=1)
print(post.author)       # lazy-loads the Author
print(post.author.name)

ManyToMany

post = Post.objects.get(id=1)
tag = Tag.objects.get(name="python")

post.tags.add(tag)
post.tags.all()     # QuerySet of related Tags
post.tags.remove(tag)
post.tags.set([tag1, tag2])
post.tags.clear()
post.tags.count()

OneToOne

class UserProfile(pookiedb.Model):
    user = pookiedb.OneToOneField("User", on_delete=pookiedb.CASCADE)
    avatar_url = pookiedb.URLField(null=True)

Field Reference

Field Description
CharField(max_length=N) VARCHAR with length limit
TextField() Unlimited text
IntegerField() INTEGER
BigIntegerField() BIGINT
FloatField() REAL / DOUBLE PRECISION
DecimalField(max_digits, decimal_places) NUMERIC
BooleanField() BOOLEAN / INTEGER
DateField(auto_now, auto_now_add) DATE
DateTimeField(auto_now, auto_now_add) TIMESTAMP
TimeField() TIME
EmailField() VARCHAR with email validation
URLField() VARCHAR with URL validation
SlugField() VARCHAR with slug validation
UUIDField(auto=True) UUID / TEXT
AutoField() Auto-increment primary key
BigAutoField() Big auto-increment primary key
JSONField() JSONB (Postgres) / TEXT (SQLite)
ArrayField(base_field) ARRAY (Postgres) / JSON TEXT (SQLite)
ForeignKey(to, on_delete) Many-to-one FK
OneToOneField(to, on_delete) Unique FK
ManyToManyField(to) Join table relationship

Common field kwargs

CharField(
    max_length=100,
    null=False,         # allow NULL in DB
    blank=False,        # allow empty value in forms/validation
    default=None,       # default value (or callable)
    unique=False,       # UNIQUE constraint
    db_index=False,     # CREATE INDEX
    db_column=None,     # override column name
    choices=[("draft", "Draft"), ("pub", "Published")],
    verbose_name="My Field",
)

Lookup Types

# Exact (default)
filter(name="Alice")
filter(name__exact="Alice")

# Case-insensitive
filter(name__iexact="alice")
filter(name__icontains="ali")

# Wildcards
filter(title__startswith="Hello")
filter(title__endswith="world")
filter(body__contains="pookie")

# Comparison
filter(age__gt=18)
filter(age__gte=18)
filter(age__lt=65)
filter(age__lte=65)

# IN / NULL / RANGE
filter(id__in=[1, 2, 3])
filter(email__isnull=True)
filter(age__range=(18, 65))
filter(name__ne="Bob")

Migrations

Generate a migration

pookiedb makemigrations --settings settings.py
pookiedb makemigrations --name add_slug_field --settings settings.py

Apply migrations

pookiedb migrate --settings settings.py

View migration status

pookiedb showmigrations --settings settings.py

Roll back

pookiedb rollback --steps 1 --settings settings.py

Your settings.py just needs to call pookiedb.connect() and import your models:

# settings.py
import pookiedb
from myapp.models import Author, Post, Tag

pookiedb.connect("sqlite:///mydb.sqlite3")

CLI Commands

pookiedb --help

Commands:
  makemigrations   Detect model changes and generate a migration file
  migrate          Apply pending migrations to the database
  rollback         Roll back the last N migrations
  showmigrations   List all migrations and their status
  shell            Interactive Python REPL with pookiedb pre-imported
  dbshell          Open raw psql / sqlite3 shell
  inspectdb        Introspect an existing DB and generate model code

Shell

pookiedb shell --settings settings.py
# All your models are available by name
>>> Author.objects.all()
>>> Post.objects.filter(published=True).count()

DBShell

pookiedb dbshell --settings settings.py   # opens psql or sqlite3

InspectDB

# Introspect all tables
pookiedb inspectdb --settings settings.py

# Single table
pookiedb inspectdb --table users --settings settings.py

# Write to file
pookiedb inspectdb --output models.py --settings settings.py

Transactions

from pookiedb.db.connection import transaction

with transaction() as conn:
    author = Author(name="Bob", email="bob@example.com")
    author.save()
    post = Post(title="Hello", slug="hello", body="...", author=author)
    post.save()
    # Auto commits on exit, rolls back on exception

Pagination

from pookiedb.utils import Paginator

paginator = Paginator(Post.objects.filter(published=True), per_page=20)
page = paginator.page(1)

print(page.object_list)     # list of Post instances
print(page.has_next())      # True / False
print(page.num_pages)       # total pages

Multiple Databases

pookiedb.connect("postgresql://...", alias="primary")
pookiedb.connect("sqlite:///analytics.sqlite3", alias="analytics")

class EventLog(pookiedb.Model):
    class Meta:
        db_alias = "analytics"

Model Meta options

class Meta:
    db_table = "custom_table_name"      # override table name
    ordering = ["-created_at", "name"]  # default ordering
    unique_together = [["first_name", "last_name"]]
    db_alias = "default"                # which DB connection to use
    abstract = True                     # don't create a table
    verbose_name = "blog post"
    verbose_name_plural = "blog posts"

License

MIT © Grace Peter Mutiibwa

Project details


Download files

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

Source Distribution

pookiedb-0.1.4.tar.gz (47.5 kB view details)

Uploaded Source

Built Distribution

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

pookiedb-0.1.4-py3-none-any.whl (43.6 kB view details)

Uploaded Python 3

File details

Details for the file pookiedb-0.1.4.tar.gz.

File metadata

  • Download URL: pookiedb-0.1.4.tar.gz
  • Upload date:
  • Size: 47.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for pookiedb-0.1.4.tar.gz
Algorithm Hash digest
SHA256 173e17664f5ba5d0bcd34547c0c8f1ed5a8ba42bb994050e468129dcfc134eb9
MD5 a5976e314c2187fb845f7d0aa4d833c1
BLAKE2b-256 768d37c3219af3548867607f6dc9c6151b722e56e23510bad3ec7c40d8741aac

See more details on using hashes here.

File details

Details for the file pookiedb-0.1.4-py3-none-any.whl.

File metadata

  • Download URL: pookiedb-0.1.4-py3-none-any.whl
  • Upload date:
  • Size: 43.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for pookiedb-0.1.4-py3-none-any.whl
Algorithm Hash digest
SHA256 051ee8f800b3c52be2ca99adfe2ee8b300529c6f076f52857ba9a16d85e18817
MD5 520a99e72268c6010cc1d502287f4a76
BLAKE2b-256 9ee677ba26e4e6aaeb3c13d21e07cc9a46719f335f4f1da8a189123710a91344

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