# 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"
}