activemodel


Nameactivemodel JSON
Version 0.7.0 PyPI version JSON
download
home_pageNone
SummaryMake SQLModel more like an a real ORM
upload_time2025-02-09 12:20:04
maintainerNone
docs_urlNone
authorNone
requires_python>=3.10
licenseNone
keywords activemodel activerecord orm sqlalchemy sqlmodel
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # ActiveModel: ORM Wrapper for SQLModel

No, this isn't *really* [ActiveModel](https://guides.rubyonrails.org/active_model_basics.html). It's just a wrapper around SQLModel that provides a more ActiveRecord-like interface.

SQLModel is *not* an ORM. It's a SQL query builder and a schema definition tool.

This package provides a thin wrapper around SQLModel that provides a more ActiveRecord-like interface with things like:

* Timestamp column mixins
* Lifecycle hooks

## Getting Started

First, setup your DB:

```python

```

Then, setup some models:

```python
from activemodel import BaseModel
from activemodel.mixins import TimestampsMixin, TypeIDMixin

class User(
    BaseModel,
    # optionally, obviously
    TimestampsMixin,
    # you can use a different pk type, but why would you?
    # put this mixin last otherwise `id` will not be the first column in the DB
    TypeIDMixin("user"),
    # wire this model into the DB, without this alembic will not generate a migration
    table=True
):
    a_field: str
```

## Usage

### Integrating Alembic

`alembic init` will not work out of the box. You need to mutate a handful of files:

* To import all of your models you want in your DB. [Here's my recommended way to do this.](https://github.com/iloveitaly/python-starter-template/blob/master/app/models/__init__.py)
* Use your DB URL from the ENV
* Target sqlalchemy metadata to the sqlmodel-generated metadata

[Take a look at these scripts for an example of how to fully integrate Alembic into your development workflow.](https://github.com/iloveitaly/python-starter-template/blob/0af2c7e95217e34bde7357cc95be048900000e48/Justfile#L618-L712)

Here's a diff from the bare `alembic init` from version `1.14.1`.

```diff
diff --git i/test/migrations/alembic.ini w/test/migrations/alembic.ini
index 0d07420..a63631c 100644
--- i/test/migrations/alembic.ini
+++ w/test/migrations/alembic.ini
@@ -3,13 +3,14 @@
 [alembic]
 # path to migration scripts
 # Use forward slashes (/) also on windows to provide an os agnostic path
-script_location = .
+script_location = migrations
 
 # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
 # Uncomment the line below if you want the files to be prepended with date and time
 # see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
 # for all available tokens
 # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
+file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(rev)s_%%(slug)s
 
 # sys.path path, will be prepended to sys.path if present.
 # defaults to the current working directory.
diff --git i/test/migrations/env.py w/test/migrations/env.py
index 36112a3..a1e15c2 100644
--- i/test/migrations/env.py
+++ w/test/migrations/env.py
@@ -1,3 +1,6 @@
+# fmt: off
+# isort: off
+
 from logging.config import fileConfig
 
 from sqlalchemy import engine_from_config
@@ -14,11 +17,17 @@ config = context.config
 if config.config_file_name is not None:
     fileConfig(config.config_file_name)
 
+from sqlmodel import SQLModel
+from test.models import *
+from test.utils import database_url
+
+config.set_main_option("sqlalchemy.url", database_url())
+
 # add your model's MetaData object here
 # for 'autogenerate' support
 # from myapp import mymodel
 # target_metadata = mymodel.Base.metadata
-target_metadata = None
+target_metadata = SQLModel.metadata
 
 # other values from the config, defined by the needs of env.py,
 # can be acquired:
diff --git i/test/migrations/script.py.mako w/test/migrations/script.py.mako
index fbc4b07..9dc78bb 100644
--- i/test/migrations/script.py.mako
+++ w/test/migrations/script.py.mako
@@ -9,6 +9,8 @@ from typing import Sequence, Union
 
 from alembic import op
 import sqlalchemy as sa
+import sqlmodel
+import activemodel
 ${imports if imports else ""}
 
 # revision identifiers, used by Alembic.
```

Here are some useful resources around Alembic + SQLModel:

* https://github.com/fastapi/sqlmodel/issues/85
* https://testdriven.io/blog/fastapi-sqlmodel/

### Query Wrapper

This tool is added to all `BaseModel`s and makes it easy to write SQL queries. Some examples:



### Easy Database Sessions

I hate the idea f 

* Behavior should be intuitive and easy to understand. If you run `save()`, it should save, not stick the save in a transaction.
* Don't worry about dead sessions. This makes it easy to lazy-load computed properties and largely eliminates the need to think about database sessions.

There are a couple of thorny problems we need to solve for here:

* In-memory fastapi servers are not the same as a uvicorn server, which is threaded *and* uses some sort of threadpool model for handling async requests. I don't claim to understand the entire implementation. For global DB session state (a) we can't use global variables (b) we can't use thread-local variables.
* 

https://github.com/tomwojcik/starlette-context

### Example Queries

* Conditional: `Scrape.select().where(Scrape.id < last_scraped.id).all()`
* Equality: `MenuItem.select().where(MenuItem.menu_id == menu.id).all()`
* `IN` example: `CanonicalMenuItem.select().where(col(CanonicalMenuItem.id).in_(canonized_ids)).all()`

### TypeID

I'm a massive fan of Stripe-style prefixed UUIDs. [There's an excellent project](https://github.com/jetify-com/typeid)
that defined a clear spec for these IDs. I've used the python implementation of this spec and developed a clean integration
with SQLModel that plays well with fastapi as well.

Here's an example of defining a relationship:

```python
import uuid

from activemodel import BaseModel
from activemodel.mixins import TimestampsMixin, TypeIDMixin
from activemodel.types import TypeIDType
from sqlmodel import Field, Relationship

from .patient import Patient

class Appointment(
    BaseModel,
    # this adds an `id` field to the model with the correct type
    TypeIDMixin("appointment"),
    table=True
):
    # `foreign_key` is a activemodel-specific method to generate the right `Field` for the relationship
    # TypeIDType is really important here for fastapi serialization
    doctor_id: TypeIDType = Doctor.foreign_key()
    doctor: Doctor = Relationship()
```

## Limitations

### Validation

SQLModel does not currently support pydantic validations (when `table=True`). This is very surprising, but is actually the intended functionality:

* https://github.com/fastapi/sqlmodel/discussions/897
* https://github.com/fastapi/sqlmodel/pull/1041
* https://github.com/fastapi/sqlmodel/issues/453
* https://github.com/fastapi/sqlmodel/issues/52#issuecomment-1311987732

For validation:

* When consuming API data, use a separate shadow model to validate the data with `table=False` and then inherit from that model in a model with `table=True`.
* When validating ORM data, use SQL Alchemy hooks.

<!--

This looks neat
https://github.com/DarylStark/my_data/blob/a17b8b3a8463b9953821b89fee895e272f94d2a4/src/my_model/model.py#L155
        schema_extra={
            'pattern': r'^[a-z0-9_\-\.]+\@[a-z0-9_\-\.]+\.[a-z\.]+$'
        },

extra constraints

https://github.com/DarylStark/my_data/blob/a17b8b3a8463b9953821b89fee895e272f94d2a4/src/my_model/model.py#L424C1-L426C6
-->
## Related Projects

* https://github.com/woofz/sqlmodel-basecrud
* https://github.com/0xthiagomartins/sqlmodel-controller

## Inspiration

* https://github.com/peterdresslar/fastapi-sqlmodel-alembic-pg
* [Albemic instructions](https://github.com/fastapi/sqlmodel/pull/899/files)
* https://github.com/fastapiutils/fastapi-utils/
* https://github.com/fastapi/full-stack-fastapi-template
* https://github.com/DarylStark/my_data/
* https://github.com/petrgazarov/FastAPI-app/tree/main/fastapi_app

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "activemodel",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.10",
    "maintainer_email": null,
    "keywords": "activemodel, activerecord, orm, sqlalchemy, sqlmodel",
    "author": null,
    "author_email": "Michael Bianco <iloveitaly@gmail.com>",
    "download_url": "https://files.pythonhosted.org/packages/04/19/eea2abc519377a0f15df6e8df002580ee6d21905a7c6d5422ed2aa5754b2/activemodel-0.7.0.tar.gz",
    "platform": null,
    "description": "# ActiveModel: ORM Wrapper for SQLModel\n\nNo, this isn't *really* [ActiveModel](https://guides.rubyonrails.org/active_model_basics.html). It's just a wrapper around SQLModel that provides a more ActiveRecord-like interface.\n\nSQLModel is *not* an ORM. It's a SQL query builder and a schema definition tool.\n\nThis package provides a thin wrapper around SQLModel that provides a more ActiveRecord-like interface with things like:\n\n* Timestamp column mixins\n* Lifecycle hooks\n\n## Getting Started\n\nFirst, setup your DB:\n\n```python\n\n```\n\nThen, setup some models:\n\n```python\nfrom activemodel import BaseModel\nfrom activemodel.mixins import TimestampsMixin, TypeIDMixin\n\nclass User(\n    BaseModel,\n    # optionally, obviously\n    TimestampsMixin,\n    # you can use a different pk type, but why would you?\n    # put this mixin last otherwise `id` will not be the first column in the DB\n    TypeIDMixin(\"user\"),\n    # wire this model into the DB, without this alembic will not generate a migration\n    table=True\n):\n    a_field: str\n```\n\n## Usage\n\n### Integrating Alembic\n\n`alembic init` will not work out of the box. You need to mutate a handful of files:\n\n* To import all of your models you want in your DB. [Here's my recommended way to do this.](https://github.com/iloveitaly/python-starter-template/blob/master/app/models/__init__.py)\n* Use your DB URL from the ENV\n* Target sqlalchemy metadata to the sqlmodel-generated metadata\n\n[Take a look at these scripts for an example of how to fully integrate Alembic into your development workflow.](https://github.com/iloveitaly/python-starter-template/blob/0af2c7e95217e34bde7357cc95be048900000e48/Justfile#L618-L712)\n\nHere's a diff from the bare `alembic init` from version `1.14.1`.\n\n```diff\ndiff --git i/test/migrations/alembic.ini w/test/migrations/alembic.ini\nindex 0d07420..a63631c 100644\n--- i/test/migrations/alembic.ini\n+++ w/test/migrations/alembic.ini\n@@ -3,13 +3,14 @@\n [alembic]\n # path to migration scripts\n # Use forward slashes (/) also on windows to provide an os agnostic path\n-script_location = .\n+script_location = migrations\n \n # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s\n # Uncomment the line below if you want the files to be prepended with date and time\n # see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file\n # for all available tokens\n # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s\n+file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(rev)s_%%(slug)s\n \n # sys.path path, will be prepended to sys.path if present.\n # defaults to the current working directory.\ndiff --git i/test/migrations/env.py w/test/migrations/env.py\nindex 36112a3..a1e15c2 100644\n--- i/test/migrations/env.py\n+++ w/test/migrations/env.py\n@@ -1,3 +1,6 @@\n+# fmt: off\n+# isort: off\n+\n from logging.config import fileConfig\n \n from sqlalchemy import engine_from_config\n@@ -14,11 +17,17 @@ config = context.config\n if config.config_file_name is not None:\n     fileConfig(config.config_file_name)\n \n+from sqlmodel import SQLModel\n+from test.models import *\n+from test.utils import database_url\n+\n+config.set_main_option(\"sqlalchemy.url\", database_url())\n+\n # add your model's MetaData object here\n # for 'autogenerate' support\n # from myapp import mymodel\n # target_metadata = mymodel.Base.metadata\n-target_metadata = None\n+target_metadata = SQLModel.metadata\n \n # other values from the config, defined by the needs of env.py,\n # can be acquired:\ndiff --git i/test/migrations/script.py.mako w/test/migrations/script.py.mako\nindex fbc4b07..9dc78bb 100644\n--- i/test/migrations/script.py.mako\n+++ w/test/migrations/script.py.mako\n@@ -9,6 +9,8 @@ from typing import Sequence, Union\n \n from alembic import op\n import sqlalchemy as sa\n+import sqlmodel\n+import activemodel\n ${imports if imports else \"\"}\n \n # revision identifiers, used by Alembic.\n```\n\nHere are some useful resources around Alembic + SQLModel:\n\n* https://github.com/fastapi/sqlmodel/issues/85\n* https://testdriven.io/blog/fastapi-sqlmodel/\n\n### Query Wrapper\n\nThis tool is added to all `BaseModel`s and makes it easy to write SQL queries. Some examples:\n\n\n\n### Easy Database Sessions\n\nI hate the idea f \n\n* Behavior should be intuitive and easy to understand. If you run `save()`, it should save, not stick the save in a transaction.\n* Don't worry about dead sessions. This makes it easy to lazy-load computed properties and largely eliminates the need to think about database sessions.\n\nThere are a couple of thorny problems we need to solve for here:\n\n* In-memory fastapi servers are not the same as a uvicorn server, which is threaded *and* uses some sort of threadpool model for handling async requests. I don't claim to understand the entire implementation. For global DB session state (a) we can't use global variables (b) we can't use thread-local variables.\n* \n\nhttps://github.com/tomwojcik/starlette-context\n\n### Example Queries\n\n* Conditional: `Scrape.select().where(Scrape.id < last_scraped.id).all()`\n* Equality: `MenuItem.select().where(MenuItem.menu_id == menu.id).all()`\n* `IN` example: `CanonicalMenuItem.select().where(col(CanonicalMenuItem.id).in_(canonized_ids)).all()`\n\n### TypeID\n\nI'm a massive fan of Stripe-style prefixed UUIDs. [There's an excellent project](https://github.com/jetify-com/typeid)\nthat defined a clear spec for these IDs. I've used the python implementation of this spec and developed a clean integration\nwith SQLModel that plays well with fastapi as well.\n\nHere's an example of defining a relationship:\n\n```python\nimport uuid\n\nfrom activemodel import BaseModel\nfrom activemodel.mixins import TimestampsMixin, TypeIDMixin\nfrom activemodel.types import TypeIDType\nfrom sqlmodel import Field, Relationship\n\nfrom .patient import Patient\n\nclass Appointment(\n    BaseModel,\n    # this adds an `id` field to the model with the correct type\n    TypeIDMixin(\"appointment\"),\n    table=True\n):\n    # `foreign_key` is a activemodel-specific method to generate the right `Field` for the relationship\n    # TypeIDType is really important here for fastapi serialization\n    doctor_id: TypeIDType = Doctor.foreign_key()\n    doctor: Doctor = Relationship()\n```\n\n## Limitations\n\n### Validation\n\nSQLModel does not currently support pydantic validations (when `table=True`). This is very surprising, but is actually the intended functionality:\n\n* https://github.com/fastapi/sqlmodel/discussions/897\n* https://github.com/fastapi/sqlmodel/pull/1041\n* https://github.com/fastapi/sqlmodel/issues/453\n* https://github.com/fastapi/sqlmodel/issues/52#issuecomment-1311987732\n\nFor validation:\n\n* When consuming API data, use a separate shadow model to validate the data with `table=False` and then inherit from that model in a model with `table=True`.\n* When validating ORM data, use SQL Alchemy hooks.\n\n<!--\n\nThis looks neat\nhttps://github.com/DarylStark/my_data/blob/a17b8b3a8463b9953821b89fee895e272f94d2a4/src/my_model/model.py#L155\n        schema_extra={\n            'pattern': r'^[a-z0-9_\\-\\.]+\\@[a-z0-9_\\-\\.]+\\.[a-z\\.]+$'\n        },\n\nextra constraints\n\nhttps://github.com/DarylStark/my_data/blob/a17b8b3a8463b9953821b89fee895e272f94d2a4/src/my_model/model.py#L424C1-L426C6\n-->\n## Related Projects\n\n* https://github.com/woofz/sqlmodel-basecrud\n* https://github.com/0xthiagomartins/sqlmodel-controller\n\n## Inspiration\n\n* https://github.com/peterdresslar/fastapi-sqlmodel-alembic-pg\n* [Albemic instructions](https://github.com/fastapi/sqlmodel/pull/899/files)\n* https://github.com/fastapiutils/fastapi-utils/\n* https://github.com/fastapi/full-stack-fastapi-template\n* https://github.com/DarylStark/my_data/\n* https://github.com/petrgazarov/FastAPI-app/tree/main/fastapi_app\n",
    "bugtrack_url": null,
    "license": null,
    "summary": "Make SQLModel more like an a real ORM",
    "version": "0.7.0",
    "project_urls": {
        "Repository": "https://github.com/iloveitaly/activemodel"
    },
    "split_keywords": [
        "activemodel",
        " activerecord",
        " orm",
        " sqlalchemy",
        " sqlmodel"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "a10e4225e1f1124d805c369d6be2d5afb664af848b2e07a3666a4a9243f519cf",
                "md5": "8d3ce7711f6fb7cc8c6d319f47005c5e",
                "sha256": "804081eb3d25581eb3d678af2fd96af389062e3ce59194e21fad18db7a2af7c8"
            },
            "downloads": -1,
            "filename": "activemodel-0.7.0-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "8d3ce7711f6fb7cc8c6d319f47005c5e",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.10",
            "size": 24358,
            "upload_time": "2025-02-09T12:20:02",
            "upload_time_iso_8601": "2025-02-09T12:20:02.529673Z",
            "url": "https://files.pythonhosted.org/packages/a1/0e/4225e1f1124d805c369d6be2d5afb664af848b2e07a3666a4a9243f519cf/activemodel-0.7.0-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "0419eea2abc519377a0f15df6e8df002580ee6d21905a7c6d5422ed2aa5754b2",
                "md5": "1cba5b0c549ede49a4c5c79c17bc3f70",
                "sha256": "3936018a3b57fdbd5942f087983ae5f1b45dfb7e290f93bd01a3b8cd5ebd547c"
            },
            "downloads": -1,
            "filename": "activemodel-0.7.0.tar.gz",
            "has_sig": false,
            "md5_digest": "1cba5b0c549ede49a4c5c79c17bc3f70",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.10",
            "size": 92097,
            "upload_time": "2025-02-09T12:20:04",
            "upload_time_iso_8601": "2025-02-09T12:20:04.292064Z",
            "url": "https://files.pythonhosted.org/packages/04/19/eea2abc519377a0f15df6e8df002580ee6d21905a7c6d5422ed2aa5754b2/activemodel-0.7.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-02-09 12:20:04",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "iloveitaly",
    "github_project": "activemodel",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "activemodel"
}
        
Elapsed time: 1.61166s