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

Uploaded CPython 3.13manylinux: glibc 2.17+ x86-64

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

Uploaded CPython 3.13manylinux: glibc 2.17+ ARM64

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

Uploaded CPython 3.13macOS 11.0+ ARM64

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

Uploaded CPython 3.12manylinux: glibc 2.17+ x86-64

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

Uploaded CPython 3.12manylinux: glibc 2.17+ ARM64

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

Uploaded CPython 3.12macOS 11.0+ ARM64

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

Uploaded CPython 3.11manylinux: glibc 2.17+ x86-64

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

Uploaded CPython 3.11manylinux: glibc 2.17+ ARM64

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

Uploaded CPython 3.11macOS 11.0+ ARM64

File details

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

File metadata

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

File hashes

Hashes for iceaxe-0.12.0.tar.gz
Algorithm Hash digest
SHA256 93cad026d59c032b3f35a6319418aeb986c566e3d85ba9201ec4a51043bc48d4
MD5 6f4a5118ae85a14ea3da0775a821a600
BLAKE2b-256 98ee0b891f7640f4a8ae7dcdb30b85a230eb9a339ea4435dd36ebbcee2bd2ef3

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for iceaxe-0.12.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
Algorithm Hash digest
SHA256 847d65e4a09fa2a4e86b299b7bddfae4c61db1659e9dc929927e4c26dbc184c5
MD5 e78065dc5a4dc98f7747c31ca11f30cb
BLAKE2b-256 3ea9b7e95d33bf87590f723ce6b04eb6b9389b186ccaae4a1fc7bcbfb7d0e2b5

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for iceaxe-0.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl
Algorithm Hash digest
SHA256 be1783a8f5ffc096731779efdd8acea61c065e2aab496b612b13bc3794fe0dce
MD5 8a401a9a1e2d23f7f87885e6c0951644
BLAKE2b-256 0be6ec77c345d31dc47758b0d629aeafa854025b65c9969511d25ad9dfe600be

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for iceaxe-0.12.0-cp313-cp313-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 ab72f11c958379cf1285646988ff27a4aae008d9bd2ed0bd8bd590f75da4df2a
MD5 d4114525da1645d16457ddbff515da10
BLAKE2b-256 09c5433e81e26e43a80d1bd6e09a9c685630c874ae1d4cb11f9b205a967543b6

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for iceaxe-0.12.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
Algorithm Hash digest
SHA256 e668d7d786ea6bcd36166344686e8ce63fff07b9df6e6569366b5cdf28c9a2a4
MD5 668f26ad2df78b390b1ba92786b54ef4
BLAKE2b-256 4d9289666edf09549c448352fb98bf91ae013adb818ede7bf1fb45df62ff74d7

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for iceaxe-0.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl
Algorithm Hash digest
SHA256 175200f9bc68c40dc1533ae6c7fdfcc4e0dcc955de148df6df85111107e0c7f8
MD5 a2eedc77fd02bcd76acd3efea499ec8d
BLAKE2b-256 14bee20fec85eb2908abd9fcd48281e50f73569a4bf12fa1b206a787cae37aa4

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for iceaxe-0.12.0-cp312-cp312-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 1c379ca2e1c306e71f75cb4f7648512ff8d67d4bc49b43bc09a0d8bd8bdbe557
MD5 c3b8b2d7f629ee0195afe65312baadc1
BLAKE2b-256 b886759d8b96474943d900d48ffa3a221f658e8c54107ed4145eb3fd5eaf86ae

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for iceaxe-0.12.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
Algorithm Hash digest
SHA256 5e29e39522b67dd2d9ff535593b4455c5aa3fcb132481f3cd8de398d330a5496
MD5 48c3648fbff180d998c0f1a5714aa754
BLAKE2b-256 28cc64b92a7020a0ffe60f93073345c51be041054c155c13db4702e1d5919348

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for iceaxe-0.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl
Algorithm Hash digest
SHA256 7937b69f51337b59129cf0679f616086af76daf233deb3f50a35142f1bc2f16f
MD5 0f1641c514c1af0cef1cea619868f800
BLAKE2b-256 559261af6058ec8bd514e8a0a55b081398b4b85d4f406c90f805e571190c22a0

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for iceaxe-0.12.0-cp311-cp311-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 9a446dde3ea81ec6c41c8ca877ba8930c8d5e51a794713dbe860202b9094a074
MD5 6153f1c41fbc2fcfb8edd7052b56338b
BLAKE2b-256 668f50fbdcff362cc2f9e4cd82a1d80445cbfb77c76a1a3e592bd15d98574e2a

See more details on using hashes here.

Provenance

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