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.12.0.dev1.tar.gz (227.4 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.12.0.dev1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (300.0 kB view details)

Uploaded CPython 3.13manylinux: glibc 2.17+ x86-64

iceaxe-0.12.0.dev1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl (296.9 kB view details)

Uploaded CPython 3.13manylinux: glibc 2.17+ ARM64

iceaxe-0.12.0.dev1-cp313-cp313-macosx_11_0_arm64.whl (293.3 kB view details)

Uploaded CPython 3.13macOS 11.0+ ARM64

iceaxe-0.12.0.dev1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (299.9 kB view details)

Uploaded CPython 3.12manylinux: glibc 2.17+ x86-64

iceaxe-0.12.0.dev1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl (296.9 kB view details)

Uploaded CPython 3.12manylinux: glibc 2.17+ ARM64

iceaxe-0.12.0.dev1-cp312-cp312-macosx_11_0_arm64.whl (293.3 kB view details)

Uploaded CPython 3.12macOS 11.0+ ARM64

iceaxe-0.12.0.dev1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (295.3 kB view details)

Uploaded CPython 3.11manylinux: glibc 2.17+ x86-64

iceaxe-0.12.0.dev1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl (292.5 kB view details)

Uploaded CPython 3.11manylinux: glibc 2.17+ ARM64

iceaxe-0.12.0.dev1-cp311-cp311-macosx_11_0_arm64.whl (289.0 kB view details)

Uploaded CPython 3.11macOS 11.0+ ARM64

File details

Details for the file iceaxe-0.12.0.dev1.tar.gz.

File metadata

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

File hashes

Hashes for iceaxe-0.12.0.dev1.tar.gz
Algorithm Hash digest
SHA256 6733e3ee01288c6a923966585a9ce95a3d723b53a79ed2a554d1d1bbc36a1318
MD5 d26414e44a2244fae08c90558a544b12
BLAKE2b-256 564c627d4d77aa83a9cb5a786691ed74b2756dac85025855c1b026a582a1003a

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for iceaxe-0.12.0.dev1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
Algorithm Hash digest
SHA256 6a989689bf765d79d7537dc92c0343202867d4ae12bd4ef8934c595d433bb601
MD5 83d2d46ee947d9dfea00d04e0a526d83
BLAKE2b-256 2eba0ff41a951775e3af447caf66d7819fa4f359d14e699fec86a2804511c626

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for iceaxe-0.12.0.dev1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl
Algorithm Hash digest
SHA256 57cdcfe435c9a40b22465abff26130a456fc62de081649b6d055ea10859bc340
MD5 46fff57176d494accf24e3f607a5b408
BLAKE2b-256 ec9457c16bd47cbb6990fe2bdd605ef60c0f62a8a7b8020d6a39c260a073070f

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for iceaxe-0.12.0.dev1-cp313-cp313-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 ad91d7d40bac7587f4ebe722b0ae9ddc124c2b25b20cc07a1c8d7c0fa12525a7
MD5 6645b07a19af0a1d17dd910b6d837948
BLAKE2b-256 65633be62bddc31862b1ff31d05e57398a3d8734bf9da547e67a8d41505322aa

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for iceaxe-0.12.0.dev1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
Algorithm Hash digest
SHA256 c520245a3e381bb2157fa702f1e2c23470784574b9651f9095725ad56d5c3e1b
MD5 3991b4091b9e9c7414abf6351373aa12
BLAKE2b-256 79222babc4e19f265ad9b42c72520a5bd0dca930e970bec0878d841170d08ca4

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for iceaxe-0.12.0.dev1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl
Algorithm Hash digest
SHA256 c7dfbca81bad8ef45cd4b40fcf14ce93b6d6cf7428556a4734431ebb8a5e0deb
MD5 37626b691a904653089eb44d0ef558f8
BLAKE2b-256 10482ea1174e395a60c3708fccd4afbc5c16de3949b39dca16011cf28be05d4b

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for iceaxe-0.12.0.dev1-cp312-cp312-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 1851711ddedca2e0bd9610d340303bfeec17fc24f1df4e098b12130acfacf133
MD5 186aa0381c32f578ee080aba3afbe173
BLAKE2b-256 bb5f6321e960db0fe536150edc523a4daaa42a78b69d8d3ce0bca35ca3621d72

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for iceaxe-0.12.0.dev1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
Algorithm Hash digest
SHA256 c5ceb154b258aa8a7508fdba2e6ac3b56e9355bbb4202721819ee1a5e1935603
MD5 50345a4563be79486069b47a689e40ed
BLAKE2b-256 d19ce32c5cd840d6e9748633113371da2e0c41fa593b65be878fa881cd16f6ce

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for iceaxe-0.12.0.dev1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl
Algorithm Hash digest
SHA256 55b1c62d60bb000ec394edd7178467cebf1074b66e4ec50e14c4d3f5f0dd54cd
MD5 590094f94124a5dff4fc9594b08552bf
BLAKE2b-256 283dfa1e4e3e5f15e4f95f6689612376ac2f9220c85601350dde7004b6f6d83b

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for iceaxe-0.12.0.dev1-cp311-cp311-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 33cde1230328a7e41a47c8993234b338760d260e755e8d22a02dab9e0f0f2edc
MD5 2b4ee2557fe9bf711e8cb7c665562d6e
BLAKE2b-256 e2c411c8f3431f49be36b00c3adc34843050a5423b69ffb1504606bd0130db4c

See more details on using hashes here.

Provenance

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