Skip to main content

A modern, fast ORM for Python.

Project description

iceaxe

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 in early alpha. 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.

Installation

If you're using poetry to manage your dependencies:

poetry 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
    )
    """
)

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

[!IMPORTANT] Iceaxe is in early alpha. We're using it internally and showly rolling out to our production applications, but we're not yet ready to recommend it for general use. The API and larger stability is subject to change.

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:

poetry 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 poetry install. Poetry is set up to create a dynamic setup.py based on our build.py definition.

poetry install

TODOs

  • Additional documentation with usage examples.

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.2.3.dev1.tar.gz (70.9 kB view details)

Uploaded Source

Built Distributions

iceaxe-0.2.3.dev1-cp312-cp312-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl (322.7 kB view details)

Uploaded CPython 3.12 manylinux: glibc 2.17+ x86-64 manylinux: glibc 2.5+ x86-64

iceaxe-0.2.3.dev1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (318.7 kB view details)

Uploaded CPython 3.12 manylinux: glibc 2.17+ ARM64

iceaxe-0.2.3.dev1-cp312-cp312-macosx_14_0_arm64.whl (173.5 kB view details)

Uploaded CPython 3.12 macOS 14.0+ ARM64

iceaxe-0.2.3.dev1-cp311-cp311-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl (313.8 kB view details)

Uploaded CPython 3.11 manylinux: glibc 2.17+ x86-64 manylinux: glibc 2.5+ x86-64

iceaxe-0.2.3.dev1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (312.0 kB view details)

Uploaded CPython 3.11 manylinux: glibc 2.17+ ARM64

iceaxe-0.2.3.dev1-cp311-cp311-macosx_14_0_arm64.whl (173.2 kB view details)

Uploaded CPython 3.11 macOS 14.0+ ARM64

File details

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

File metadata

  • Download URL: iceaxe-0.2.3.dev1.tar.gz
  • Upload date:
  • Size: 70.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/5.1.1 CPython/3.12.7

File hashes

Hashes for iceaxe-0.2.3.dev1.tar.gz
Algorithm Hash digest
SHA256 186269f8dae7d54056936a1cb6883ad7957aef568e5b91137e54a81319998192
MD5 5b31a0d1d428cba2dd038a70a1f96c7e
BLAKE2b-256 6c72e03a26967d9bb8f64cc26e8edcaff0a4de289e855ec3a44a28e552f7d789

See more details on using hashes here.

Provenance

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

Publisher: test.yml on piercefreeman/iceaxe

Attestations:

File details

Details for the file iceaxe-0.2.3.dev1-cp312-cp312-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for iceaxe-0.2.3.dev1-cp312-cp312-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 013f553c37f0f82537c3e967973bc70c3c95264048a1a3d92c3e7a34865536f6
MD5 736212682041cb2b8641525aa41cd80e
BLAKE2b-256 6c9cf01a7f5fda4e3ee29a98b8671685dd19296d935c32ba05c76676f74b663a

See more details on using hashes here.

Provenance

The following attestation bundles were made for iceaxe-0.2.3.dev1-cp312-cp312-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl:

Publisher: test.yml on piercefreeman/iceaxe

Attestations:

File details

Details for the file iceaxe-0.2.3.dev1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for iceaxe-0.2.3.dev1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 61fd20943292b71ff164f863a1b3a0725baeca946a86ec3bc75447d7fe746f7e
MD5 d944df2f8f1178bf96155ed66aa3c8dc
BLAKE2b-256 32a39515cf70e36c908728de74474f810714ca39977b75a2cdadedce86ed888c

See more details on using hashes here.

Provenance

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

Publisher: test.yml on piercefreeman/iceaxe

Attestations:

File details

Details for the file iceaxe-0.2.3.dev1-cp312-cp312-macosx_14_0_arm64.whl.

File metadata

File hashes

Hashes for iceaxe-0.2.3.dev1-cp312-cp312-macosx_14_0_arm64.whl
Algorithm Hash digest
SHA256 2785433b2310ece7961de1f9c96f8b2c204d81b29594aff1517083a0d491b420
MD5 9884e0303483561e218b533673c53799
BLAKE2b-256 eda5e487da489bc4cd936b8d7a3277142c64a2aeda8fb25cdf6545a58bd4160c

See more details on using hashes here.

Provenance

The following attestation bundles were made for iceaxe-0.2.3.dev1-cp312-cp312-macosx_14_0_arm64.whl:

Publisher: test.yml on piercefreeman/iceaxe

Attestations:

File details

Details for the file iceaxe-0.2.3.dev1-cp311-cp311-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for iceaxe-0.2.3.dev1-cp311-cp311-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 46eb88db07abafa17653b3fcfed6514020a72e7fade1483e001e7b09e8c7b96c
MD5 f42c66c5197f88bedaa56097bd0e299f
BLAKE2b-256 c23e4d9cbbc233982dfbacd51fd476a2532574301acdd843e9d742fa65f354cd

See more details on using hashes here.

Provenance

The following attestation bundles were made for iceaxe-0.2.3.dev1-cp311-cp311-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl:

Publisher: test.yml on piercefreeman/iceaxe

Attestations:

File details

Details for the file iceaxe-0.2.3.dev1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for iceaxe-0.2.3.dev1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 769b77427a0797fc863a6cdf6e18a488e6555ee141b87744b018148834ab256b
MD5 15164ca97d6f5d77493ef70111e81de6
BLAKE2b-256 2a7c40c94d960710e1ff66d146805c3f2d41422e8e32019ec71cdee0115f1452

See more details on using hashes here.

Provenance

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

Publisher: test.yml on piercefreeman/iceaxe

Attestations:

File details

Details for the file iceaxe-0.2.3.dev1-cp311-cp311-macosx_14_0_arm64.whl.

File metadata

File hashes

Hashes for iceaxe-0.2.3.dev1-cp311-cp311-macosx_14_0_arm64.whl
Algorithm Hash digest
SHA256 d450827f77625cbc913c85985735d3b82dc29534bb6cca118abdde2677e0a924
MD5 1c8e6f65932dac09ac5f23d576a93a1b
BLAKE2b-256 147d4698983e88c27476c67b52104b74730cf24b80719fa0154ffc6eb60b4916

See more details on using hashes here.

Provenance

The following attestation bundles were made for iceaxe-0.2.3.dev1-cp311-cp311-macosx_14_0_arm64.whl:

Publisher: test.yml on piercefreeman/iceaxe

Attestations:

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page