Skip to main content

clirm

Command-Line ORM (clirm) is a library for creating simple ORMs that can be used in command-line programs that allow users to manipulate objects in an interactive context, and that also regularly run scripts that iterate over the entire database.

Features include:

  • Changes are always committed to the database immediately, so that there is no need to worry about a separate later "save" step.
  • There is always only one object per database row, so that users do not need to worry about editing one copy and leaving another ORM object corresponding to the same row unchanged.
  • All columns in a row are always fetched together, to simplify implementation of the above point.
  • Tight integration with the type system. Fields can be declared as Field[T](), where T is a normal Python type.

Clirm requires that every table has an id column containing a unique identifier.

Usage

As an example, we will create a simple database containing animal taxa:

import enum
import sqlite3
from typing import Self

from clirm import Clirm, Field, Model

class Status(enum.Enum):
    living = 1
    recently_extinct = 2
    fossil = 3

CLIRM = Clirm(sqlite3.connect("taxon.db"))

class Taxon(Model):
    clirm = CLIRM
    clirm_table_name = "taxon"

    name = Field[str]()  # string field
    status = Field[Status]()  # enum field
    common_name = Field[str | None]()  # nullable string field
    parent = Field[Self | None]()  # foreign key to self; can also write "Taxon | None"

if __name__ == "__main__":
    txn1 = Taxon.create(
        name="Mammalia", status=Status.living, common_name="Mammals"
    )
    txn2 = Taxon.create(
        name="Rodentia", status=Status.living, common_name="Rodents",
        parent=txn1
    )
    tnx3 = Taxon.create(
        name="Multituberculata", status=Status.fossil, parent=txn1
    )

    living_taxa = Taxon.select().filter(Taxon.status == Status.living)
    assert living_taxa.count() == 2
    assert {txn.common_name for txn in living_taxa} == {"Mammals", "Rodents"}
    for txn in living_taxa:
        txn.common_name = txn.common_name + "!"

    # Change is immediately visible
    assert txn1.common_name == "Mammals!"

Supported types

The following field types are currently supported:

  • Primitive types, e.g., int, str, bool, which are passed directly to the database
  • Enums, which are converted to their value before being passed to the database
  • Foreign keys to other clirm models, which are stored as their IDs
  • Foreign keys to the current class, which can be expressed with typing.Self
  • Nullable versions of any of the above, expressed by adding | None to the type

Additional types can be supported by subclassing Field and overriding the deserialize and serialize methods.

Read-only mode

Set CLIRM_READONLY=1 to make all Clirm databases created by the process read-only. The values 0, false, no, and off (case-insensitive), as well as an unset or empty variable, leave writes enabled; any other nonempty value enables read-only mode.

Read-only mode can also be enabled temporarily in code, either for every database or for a single Clirm instance:

from clirm import ReadOnlyError, readonly

with readonly():
    # Writes through any Clirm instance raise ReadOnlyError.
    inspect_all_databases()

with CLIRM.readonly():
    # Only writes to CLIRM are disabled.
    inspect_taxa()

Both context managers are nestable. Reads continue to work. SQLite's query_only mode is enabled for the connection while a read-only context is active, so writes attempted directly through Clirm.conn are also rejected. The original query_only setting is restored on exit.

Virtual models

Models may also be created as mutable, in-memory objects without inserting a row:

virtual_taxon = Taxon.virtual(name="Lagomorpha", status=Status.living)
virtual_taxon.common_name = "Rabbits and relatives"

persisted_taxon = Taxon.get(name="Rodentia")
proposal = persisted_taxon.virtual_copy(common_name="Rodents and allies")
assert proposal.virtual_origin is persisted_taxon
assert proposal.virtual_origin_id == persisted_taxon.id
assert proposal.virtual_changes() == {"common_name": "Rodents and allies"}

Every virtual model has a unique, process-local negative VirtualId, including a virtual copy of an existing row. Virtual models do not enter the database-backed instance cache. Nullable fields and fields with defaults are populated automatically; reading any other omitted field raises UnsetVirtualFieldError.

Assignments to virtual models change only their in-memory values, including inside a read-only context. Calling load(), save(), or delete_instance() on a virtual model raises VirtualModelError. Persisted models and Model.create() reject direct foreign keys to virtual models with VirtualReferenceError. There is intentionally no method to commit a virtual model to the database.

Code that validates a proposed object can temporarily substitute virtual copies for their persisted origins when following ORM references:

with substitute_virtual_models([proposal]):
    assert proposal.taxon.base_name is proposal
    assert list(NameUsage.filter(NameUsage.name == proposal))

Within this context, foreign keys to an origin resolve to its virtual copy, and query predicates involving that copy use the origin's database ID. This is a reference overlay, not a virtual database: newly created virtual objects are not added to queries, and predicates on changed scalar fields still query persisted data.

Backends

For now only SQLite is supported as a backend.

Changelog

Version 0.3.1 (August 3, 2026)

  • Fix version

Version 0.3.0 (August 3, 2026)

  • Add support for substituting virtual models

Version 0.2.0 (August 3, 2026)

  • Add support for virtual mode
  • Add support for read-only mode

Version 0.1 (April 8, 2024)

  • Initial release

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

clirm-0.3.1.tar.gz (13.5 kB view details)

Uploaded Source

Built Distribution

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

clirm-0.3.1-py3-none-any.whl (11.3 kB view details)

Uploaded Python 3

File details

Details for the file clirm-0.3.1.tar.gz.

File metadata

  • Download URL: clirm-0.3.1.tar.gz
  • Upload date:
  • Size: 13.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for clirm-0.3.1.tar.gz
Algorithm Hash digest
SHA256 5c00ea237a9d4c8bcb098e637332aeeb505332165b1a78e5446ea6c48ed2b9c2
MD5 7b2612f20e6db22c31d2b1a09b62f2b2
BLAKE2b-256 b761440303cf73bc8ae7c8917cea400f8e6f79c81ad15425111ae7b6c2c2e4d8

See more details on using hashes here.

Provenance

The following attestation bundles were made for clirm-0.3.1.tar.gz:

Publisher: release.yml on JelleZijlstra/clirm

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

File details

Details for the file clirm-0.3.1-py3-none-any.whl.

File metadata

  • Download URL: clirm-0.3.1-py3-none-any.whl
  • Upload date:
  • Size: 11.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for clirm-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 fc7544dcb2285a24c90fb6740d8b1aab80d8ddd1c9d2d7aa7dac4d52ca8000dc
MD5 eebf0aa0f7823b658fc8699600989561
BLAKE2b-256 aea469eace1876070d49be64be982b1ae384d6cbcf2cc61d37ba5b0d861a1d62

See more details on using hashes here.

Provenance

The following attestation bundles were made for clirm-0.3.1-py3-none-any.whl:

Publisher: release.yml on JelleZijlstra/clirm

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