Skip to main content

A modern, fast ORM for Python.

Project description

iceaxe

Iceaxe Logo

Python Version Test status

A modern, fast ORM for Python. We have the following goals:

  • 🏎️ Performance: We want to exceed or match the fastest ORMs in Python. We want our ORM to be as close as possible to raw-asyncpg speeds. See the "Benchmarks" section for more.
  • 📝 Typehinting: Everything should be typehinted with expected types. Declare your data as you expect in Python and it should bidirectionally sync to the database.
  • 🐘 Postgres only: Leverage native Postgres features and simplify the implementation.
  • Common things are easy, rare things are possible: 99% of the SQL queries we write are vanilla SELECT/INSERT/UPDATEs. These should be natively supported by your ORM. If you're writing really complex queries, these are better done by hand so you can see exactly what SQL will be run.

Iceaxe is used in production at several companies. It's also an independent project. It's compatible with the Mountaineer ecosystem, but you can use it in whatever project and web framework you're using.

For comprehensive documentation, visit https://iceaxe.sh.

To auto-optimize your self hosted Postgres install, check out our new autopg project.

Installation

If you're using poetry to manage your dependencies:

uv add iceaxe

Otherwise install with pip:

pip install iceaxe

Usage

Define your models as a TableBase subclass:

from iceaxe import TableBase

class Person(TableBase):
    id: int
    name: str
    age: int

TableBase is a subclass of Pydantic's BaseModel, so you get all of the validation and Field customization out of the box. We provide our own Field constructor that adds database-specific configuration. For instance, to make the id field a primary key / auto-incrementing you can do:

from iceaxe import Field

class Person(TableBase):
    id: int = Field(primary_key=True)
    name: str
    age: int

Okay now you have a model. How do you interact with it?

Databases are based on a few core primitives to insert data, update it, and fetch it out again. To do so you'll need a database connection, which is a connection over the network from your code to your Postgres database. The DBConnection is the core class for all ORM actions against the database.

from iceaxe import DBConnection
import asyncpg

conn = DBConnection(
    await asyncpg.connect(
        host="localhost",
        port=5432,
        user="db_user",
        password="yoursecretpassword",
        database="your_db",
    )
)

The Person class currently just lives in memory. To back it with a full database table, we can run raw SQL or run a migration to add it:

await conn.conn.execute(
    """
    CREATE TABLE IF NOT EXISTS person (
        id SERIAL PRIMARY KEY,
        name TEXT NOT NULL,
        age INT NOT NULL
    )
    """
)

Magic Migrations

For local development or side projects, you can use magic_migrate to automatically sync your database schema with your models:

await conn.magic_migrate("my_project")

This will:

  1. Compare your current database schema against your model definitions
  2. Generate a migration file if changes are detected
  3. Apply all pending migrations

The migration files are written to your package's migrations/ folder, giving you a history of schema changes.

Recommended workflow for production:

While magic_migrate is convenient for rapid local iteration, we recommend a more controlled approach before merging to production:

  1. Iterate freely during development using magic_migrate
  2. Before merging, reset your database to the production schema state
  3. Run uv run migrate generate once to generate a single, clean migration file
  4. Commit this migration file with your PR

This ensures your production migrations are clean and reviewable, while still giving you the speed of automatic migrations during development.

Inserting Data

Instantiate object classes as you normally do:

people = [
    Person(name="Alice", age=30),
    Person(name="Bob", age=40),
    Person(name="Charlie", age=50),
]
await conn.insert(people)

print(people[0].id) # 1
print(people[1].id) # 2

Because we're using an auto-incrementing primary key, the id field will be populated after the insert. Iceaxe will automatically update the object in place with the newly assigned value.

Updating data

Now that we have these lovely people, let's modify them.

person = people[0]
person.name = "Blice"

Right now, we have a Python object that's out of state with the database. But that's often okay. We can inspect it and further write logic - it's fully decoupled from the database.

def ensure_b_letter(person: Person):
    if person.name[0].lower() != "b":
        raise ValueError("Name must start with 'B'")

ensure_b_letter(person)

To sync the values back to the database, we can call update:

await conn.update([person])

If we were to query the database directly, we see that the name has been updated:

id | name  | age
----+-------+-----
  1 | Blice |  31
  2 | Bob   |  40
  3 | Charlie | 50

But no other fields have been touched. This lets a potentially concurrent process modify Alice's record - say, updating the age to 31. By the time we update the data, we'll change the name but nothing else. Under the hood we do this by tracking the fields that have been modified in-memory and creating a targeted UPDATE to modify only those values.

Selecting data

To select data, we can use a QueryBuilder. For a shortcut to select query functions, you can also just import select directly. This method takes the desired value parameters and returns a list of the desired objects.

from iceaxe import select

query = select(Person).where(Person.name == "Blice", Person.age > 25)
results = await conn.exec(query)

If we inspect the typing of results, we see that it's a list[Person] objects. This matches the typehint of the select function. You can also target columns directly:

query = select((Person.id, Person.name)).where(Person.age > 25)
results = await conn.exec(query)

This will return a list of tuples, where each tuple is the id and name of the person: list[tuple[int, str]].

We support most of the common SQL operations. Just like the results, these are typehinted to their proper types as well. Static typecheckers and your IDE will throw an error if you try to compare a string column to an integer, for instance. A more complex example of a query:

query = select((
    Person.id,
    FavoriteColor,
)).join(
    FavoriteColor,
    Person.id == FavoriteColor.person_id,
).where(
    Person.age > 25,
    Person.name == "Blice",
).order_by(
    Person.age.desc(),
).limit(10)
results = await conn.exec(query)

As expected this will deliver results - and typehint - as a list[tuple[int, FavoriteColor]]

Production

Note that underlying Postgres connection wrapped by conn will be alive for as long as your object is in memory. This uses up one of the allowable connections to your database. Your overall limit depends on your Postgres configuration or hosting provider, but most managed solutions top out around 150-300. If you need more concurrent clients connected (and even if you don't - connection creation at the Postgres level is expensive), you can adopt a load balancer like pgbouncer to better scale to traffic. More deployment notes to come.

It's also worth noting the absence of request pooling in this initialization. This is a feature of many ORMs that lets you limit the overall connections you make to Postgres, and re-use these over time. We specifically don't offer request pooling as part of Iceaxe, despite being supported by our underlying engine asyncpg. This is a bit more aligned to how things should be structured in production. Python apps are always bound to one process thanks to the GIL. So no matter what your connection pool will always be tied to the current Python process / runtime. When you're deploying onto a server with multiple cores, the pool will be duplicated across CPUs and largely defeats the purpose of capping network connections in the first place.

Benchmarking

We have basic benchmarking tests in the __tests__/benchmarks directory. To run them, you'll need to execute the pytest suite:

uv run pytest -m integration_tests

Current benchmarking as of October 11 2024 is:

raw asyncpg iceaxe external overhead
TableBase columns 0.098s 0.093s
TableBase full 0.164s 1.345s 10%: dict construction 90%: pydantic overhead

Development

If you update your Cython implementation during development, you'll need to re-compile the Cython code. This can be done with a simple uv sync.

uv sync

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

iceaxe-0.9.1.tar.gz (218.8 kB view details)

Uploaded Source

Built Distributions

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

iceaxe-0.9.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (290.8 kB view details)

Uploaded CPython 3.13manylinux: glibc 2.17+ x86-64

iceaxe-0.9.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl (287.8 kB view details)

Uploaded CPython 3.13manylinux: glibc 2.17+ ARM64

iceaxe-0.9.1-cp313-cp313-macosx_11_0_arm64.whl (284.1 kB view details)

Uploaded CPython 3.13macOS 11.0+ ARM64

iceaxe-0.9.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (290.8 kB view details)

Uploaded CPython 3.12manylinux: glibc 2.17+ x86-64

iceaxe-0.9.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl (287.8 kB view details)

Uploaded CPython 3.12manylinux: glibc 2.17+ ARM64

iceaxe-0.9.1-cp312-cp312-macosx_11_0_arm64.whl (284.1 kB view details)

Uploaded CPython 3.12macOS 11.0+ ARM64

iceaxe-0.9.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (286.1 kB view details)

Uploaded CPython 3.11manylinux: glibc 2.17+ x86-64

iceaxe-0.9.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl (283.3 kB view details)

Uploaded CPython 3.11manylinux: glibc 2.17+ ARM64

iceaxe-0.9.1-cp311-cp311-macosx_11_0_arm64.whl (279.8 kB view details)

Uploaded CPython 3.11macOS 11.0+ ARM64

File details

Details for the file iceaxe-0.9.1.tar.gz.

File metadata

  • Download URL: iceaxe-0.9.1.tar.gz
  • Upload date:
  • Size: 218.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for iceaxe-0.9.1.tar.gz
Algorithm Hash digest
SHA256 ae72b788974f2023ab4dbc75db133eea15c1d0d8fb04250925ebbd98ef48747a
MD5 a5deaf86987f22a745deceddab15645c
BLAKE2b-256 030832f2641945ee9ba5da3a9aac358964f6c51b8482a7b5842a251147d4aa71

See more details on using hashes here.

Provenance

The following attestation bundles were made for iceaxe-0.9.1.tar.gz:

Publisher: test.yml on piercefreeman/iceaxe

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file iceaxe-0.9.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.

File metadata

File hashes

Hashes for iceaxe-0.9.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
Algorithm Hash digest
SHA256 57f0f6484e3fadc17a1c608a581aedc450b01486e2359d3e7d31a061b0c9319a
MD5 f3660bcc222331b320a5b88ce2937741
BLAKE2b-256 70eccfaa80d025c6a1f849a6d1c7284359c4a17007b4178dff9732f198a5ee66

See more details on using hashes here.

Provenance

The following attestation bundles were made for iceaxe-0.9.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl:

Publisher: test.yml on piercefreeman/iceaxe

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file iceaxe-0.9.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl.

File metadata

File hashes

Hashes for iceaxe-0.9.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl
Algorithm Hash digest
SHA256 573c4696c2870cc315ef7dc01f98a14cbf41d14b6bee69be54a4a54e3a95b5c1
MD5 1ca0450c8fd8e3f145d94734ff5da110
BLAKE2b-256 9f3391d6fa9cb8d035a5f552e38afe15461a728d94fca35fc5afe73c0e752371

See more details on using hashes here.

Provenance

The following attestation bundles were made for iceaxe-0.9.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl:

Publisher: test.yml on piercefreeman/iceaxe

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file iceaxe-0.9.1-cp313-cp313-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for iceaxe-0.9.1-cp313-cp313-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 d3ffabfb6cb61324fd1c1a7b751876809413b2f782a3782f18cd34792a6a1fdc
MD5 f098d089f1056890fec08c2b1932b2ae
BLAKE2b-256 3e71c8a022fb6ba44991be1f78ccc3f442cb5d6817289afbdc6f78efa2c469df

See more details on using hashes here.

Provenance

The following attestation bundles were made for iceaxe-0.9.1-cp313-cp313-macosx_11_0_arm64.whl:

Publisher: test.yml on piercefreeman/iceaxe

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file iceaxe-0.9.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.

File metadata

File hashes

Hashes for iceaxe-0.9.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
Algorithm Hash digest
SHA256 3d57c061ae748b395b9a559925b2055471bf7d89bb66d4d1cddf783ba2e4fa67
MD5 8a71e65f124492986131a71b336983ad
BLAKE2b-256 4b08b0e24b10f6acaa906507cced3f179f4dbe8b2c213ae9a07d692e8d6fd745

See more details on using hashes here.

Provenance

The following attestation bundles were made for iceaxe-0.9.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl:

Publisher: test.yml on piercefreeman/iceaxe

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file iceaxe-0.9.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl.

File metadata

File hashes

Hashes for iceaxe-0.9.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl
Algorithm Hash digest
SHA256 0b543d211d94c458d66e2dfc8058a909a78b921a27fa174770b9aea1fca605ef
MD5 cd909f8f70554d5c929c5f0338475796
BLAKE2b-256 e738f15d0eb432717e6c5ff3b16be15d3bb6ad483f5c85264473738fbfa36ffa

See more details on using hashes here.

Provenance

The following attestation bundles were made for iceaxe-0.9.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl:

Publisher: test.yml on piercefreeman/iceaxe

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file iceaxe-0.9.1-cp312-cp312-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for iceaxe-0.9.1-cp312-cp312-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 2559ce3c5f6424be725e97568e859ce62a97de9b82fca1e1bd88fc03e60500be
MD5 4ec6a171b4d2d4a4dcf28af6181c711a
BLAKE2b-256 de332e65234fa70f2c7f06f06fc15296efa4c592c523bb6edb5d912d12a2c9f1

See more details on using hashes here.

Provenance

The following attestation bundles were made for iceaxe-0.9.1-cp312-cp312-macosx_11_0_arm64.whl:

Publisher: test.yml on piercefreeman/iceaxe

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file iceaxe-0.9.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.

File metadata

File hashes

Hashes for iceaxe-0.9.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
Algorithm Hash digest
SHA256 29ff04352a3bbfc6cc4706e0c8a94ef5bbb6dc6d2aab53326d3031646fe3bce8
MD5 88346d1b7d792ba45f8cea2365e9e65d
BLAKE2b-256 08dabd9ea2c65c9c693f5232b975abd70290249bc9e88a9d01749156e92092bd

See more details on using hashes here.

Provenance

The following attestation bundles were made for iceaxe-0.9.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl:

Publisher: test.yml on piercefreeman/iceaxe

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file iceaxe-0.9.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl.

File metadata

File hashes

Hashes for iceaxe-0.9.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl
Algorithm Hash digest
SHA256 c0987d80603abd148c6a8662a86953a354fbeb1a850bd6317cbddf10d48f8835
MD5 79cce298ea4997f4f41862c2c639c072
BLAKE2b-256 aa755e75c39a9ddbe030c56aec698c1a8240ce79bbf7308c3d403299c7834b60

See more details on using hashes here.

Provenance

The following attestation bundles were made for iceaxe-0.9.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl:

Publisher: test.yml on piercefreeman/iceaxe

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file iceaxe-0.9.1-cp311-cp311-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for iceaxe-0.9.1-cp311-cp311-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 b7ed263a3aab86462f67bcb551af7e5fc284ad2f9436c760ed2e93307cdd909f
MD5 5e44e6ceda76d2bdea0836ec9e0abe13
BLAKE2b-256 4c4179b31b5e49c1d15381672b6a7c816253f4d0555dc8309c61f4daba5dd704

See more details on using hashes here.

Provenance

The following attestation bundles were made for iceaxe-0.9.1-cp311-cp311-macosx_11_0_arm64.whl:

Publisher: test.yml on piercefreeman/iceaxe

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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