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.10.0.tar.gz (219.7 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.10.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (291.7 kB view details)

Uploaded CPython 3.13manylinux: glibc 2.17+ x86-64

iceaxe-0.10.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl (288.7 kB view details)

Uploaded CPython 3.13manylinux: glibc 2.17+ ARM64

iceaxe-0.10.0-cp313-cp313-macosx_11_0_arm64.whl (285.1 kB view details)

Uploaded CPython 3.13macOS 11.0+ ARM64

iceaxe-0.10.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (291.7 kB view details)

Uploaded CPython 3.12manylinux: glibc 2.17+ x86-64

iceaxe-0.10.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl (288.7 kB view details)

Uploaded CPython 3.12manylinux: glibc 2.17+ ARM64

iceaxe-0.10.0-cp312-cp312-macosx_11_0_arm64.whl (285.1 kB view details)

Uploaded CPython 3.12macOS 11.0+ ARM64

iceaxe-0.10.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (287.0 kB view details)

Uploaded CPython 3.11manylinux: glibc 2.17+ x86-64

iceaxe-0.10.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl (284.3 kB view details)

Uploaded CPython 3.11manylinux: glibc 2.17+ ARM64

iceaxe-0.10.0-cp311-cp311-macosx_11_0_arm64.whl (280.8 kB view details)

Uploaded CPython 3.11macOS 11.0+ ARM64

File details

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

File metadata

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

File hashes

Hashes for iceaxe-0.10.0.tar.gz
Algorithm Hash digest
SHA256 29336f570c9a5b5e82514be0e1ce47099492489fb9f9c798a11c818aaad336ef
MD5 9e0876b2aaf5d64d8eb0843c0df06322
BLAKE2b-256 7244915c899f9171357b6d95350c40e32d295f16a13966437e83f47a23f173b8

See more details on using hashes here.

Provenance

The following attestation bundles were made for iceaxe-0.10.0.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.10.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.

File metadata

File hashes

Hashes for iceaxe-0.10.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
Algorithm Hash digest
SHA256 d5d3e58fbafc8a4d6ae5bc30e6eba47705b5f2c600db3cd73a69dd36cb5a2ae2
MD5 b9d70ecb17aab85de0a07445d4a2236b
BLAKE2b-256 c416fe82eddf0978978bc17a02430c84ec1f92f03187ccd9468fd61a6fdf2e85

See more details on using hashes here.

Provenance

The following attestation bundles were made for iceaxe-0.10.0-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.10.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl.

File metadata

File hashes

Hashes for iceaxe-0.10.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl
Algorithm Hash digest
SHA256 74b49a7ffcfb08edf33ffbab404c0b607794ee29b99fb1d3c9883f33f00760e4
MD5 9b7c2a598938944207cdfcfff79bb0f8
BLAKE2b-256 aac722ee35a44697c4510ecb9db1cf38db52cf3ba7e8cea299f040024f566632

See more details on using hashes here.

Provenance

The following attestation bundles were made for iceaxe-0.10.0-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.10.0-cp313-cp313-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for iceaxe-0.10.0-cp313-cp313-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 98bf4d96145aa5474daec22db3847553e99a92117ab4f23d1ff76bdfcecd3415
MD5 42b2d5b83c133bf07ecb6ce00f0accd8
BLAKE2b-256 f0c331d4a750148b85697c19cc0fa693a15e037c15d3587e36cbd252b3b19b31

See more details on using hashes here.

Provenance

The following attestation bundles were made for iceaxe-0.10.0-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.10.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.

File metadata

File hashes

Hashes for iceaxe-0.10.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
Algorithm Hash digest
SHA256 dc09dfbb6e38a1efe50a8a5cfa5a0f30bb843f9fe44a567a679350e3fa63c141
MD5 9f9c5492a5058c2b584e1eff054708a5
BLAKE2b-256 bb21c023a09622c0da743d74802235872e9e8e3d0195da48048352653753687b

See more details on using hashes here.

Provenance

The following attestation bundles were made for iceaxe-0.10.0-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.10.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl.

File metadata

File hashes

Hashes for iceaxe-0.10.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl
Algorithm Hash digest
SHA256 9bd95a2289956433ae6e0a24ad05893dfdf1544af21c90036c6c94911a1f7577
MD5 86a7c0a63f8ea8c006a7d11e32e766c8
BLAKE2b-256 fc03b0366c1de948534dc06c71ab27c07e79ed65a110f66d1ee0199274432267

See more details on using hashes here.

Provenance

The following attestation bundles were made for iceaxe-0.10.0-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.10.0-cp312-cp312-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for iceaxe-0.10.0-cp312-cp312-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 187a4059c7111548c38485e4cd20ad777bdbab2c71b7a459e23b9b3c1d596057
MD5 78d444d2fde233927e8b07045ab3b364
BLAKE2b-256 3733f8cb76a5dae2bd981c4d9ba3ba43622e364c9bcb985bda8f96c841116625

See more details on using hashes here.

Provenance

The following attestation bundles were made for iceaxe-0.10.0-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.10.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.

File metadata

File hashes

Hashes for iceaxe-0.10.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
Algorithm Hash digest
SHA256 0c450c49ae5661b9bb76e10047d174e1290ec45e34eacc57e0435340655003cd
MD5 62c239a880a0e5508e1263794833c72d
BLAKE2b-256 b84dfe156e58e964b98d3febffde91609ccf88c0b68ba7a5c8232695c9cb1a89

See more details on using hashes here.

Provenance

The following attestation bundles were made for iceaxe-0.10.0-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.10.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl.

File metadata

File hashes

Hashes for iceaxe-0.10.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl
Algorithm Hash digest
SHA256 d217fc61a31045992023231b09dbad74517f191c366a7cf9e31eb649e640881c
MD5 bd8058ca722db43320c79816d6462c78
BLAKE2b-256 f57eafd804f8bf4d2f53e573ea755a57f507372841c66152d9e190db06fabaac

See more details on using hashes here.

Provenance

The following attestation bundles were made for iceaxe-0.10.0-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.10.0-cp311-cp311-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for iceaxe-0.10.0-cp311-cp311-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 4f1c3de6e5c0e79d43e347ca8eeebab54dd6ff2c29221f1775e5bb2cf4978ce0
MD5 1723187dd7deacfe1363df54be932ac1
BLAKE2b-256 33cc6e70e5b1a672ec5a147f36858a8280c57eeb0b7cfe36aff997f1e473e13a

See more details on using hashes here.

Provenance

The following attestation bundles were made for iceaxe-0.10.0-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