SQLAlchemy-Nested-Mutable
=========================
An advanced SQLAlchemy column type factory that helps map compound Python types (e.g. `list`, `dict`, *Pydantic Model* and their hybrids) to database types (e.g. `ARRAY`, `JSONB`),
And keep track of mutations in deeply nested data structures so that SQLAlchemy can emit proper *UPDATE* statements.
SQLAlchemy-Nested-Mutable is highly inspired by SQLAlchemy-JSON<sup>[[0]](https://github.com/edelooff/sqlalchemy-json)</sup><sup>[[1]](https://variable-scope.com/posts/mutation-tracking-in-nested-json-structures-using-sqlalchemy)</sup>.
However, it does not limit the mapped Python type to be `dict` or `list`.
---
## Why this package?
* By default, SQLAlchemy does not track in-place mutations for non-scalar data types
such as `list` and `dict` (which are usually mapped with `ARRAY` and `JSON/JSONB`).
* Even though SQLAlchemy provides [an extension](https://docs.sqlalchemy.org/en/20/orm/extensions/mutable.html)
to track mutations on compound objects, it's too shallow, i.e. it only tracks mutations on the first level of the compound object.
* There exists the [SQLAlchemy-JSON](https://github.com/edelooff/sqlalchemy-json) package
to help track mutations on nested `dict` or `list` data structures.
However, the db type is limited to `JSON(B)`.
* Also, I would like the mapped Python types can be subclasses of the Pydantic BaseModelModel,
which have strong schemas, with the db type be schema-less JSON.
## Installation
```shell
pip install sqlalchemy-nested-mutable
```
## Usage
> NOTE the example below is first updated in `examples/user-addresses.py` and then updated here.
```python
from typing import Optional, List
import pydantic
import sqlalchemy as sa
from sqlalchemy.orm import Session, DeclarativeBase, Mapped, mapped_column
from sqlalchemy_nested_mutable import MutablePydanticBaseModel
class Base(DeclarativeBase):
pass
class Addresses(MutablePydanticBaseModel):
"""A container for storing various addresses of users.
NOTE: for working with pydantic model, use a subclass of `MutablePydanticBaseModel` for column mapping.
However, the nested models (e.g. `AddressItem` below) should be direct subclasses of `pydantic.BaseModel`.
"""
class AddressItem(pydantic.BaseModel):
street: str
city: str
area: Optional[str]
preferred: AddressItem
work: Optional[AddressItem]
home: Optional[AddressItem]
others: List[AddressItem] = []
class User(Base):
__tablename__ = "user_account"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(sa.String(30))
addresses: Mapped[Addresses] = mapped_column(Addresses.as_mutable(), nullable=True)
engine = sa.create_engine("sqlite://")
Base.metadata.create_all(engine)
with Session(engine) as s:
s.add(u := User(name="foo", addresses={"preferred": {"street": "bar", "city": "baz"}}))
assert isinstance(u.addresses, MutablePydanticBaseModel)
s.commit()
u.addresses.preferred.street = "bar2"
s.commit()
assert u.addresses.preferred.street == "bar2"
u.addresses.others.append(Addresses.AddressItem.parse_obj({"street": "bar3", "city": "baz3"}))
s.commit()
assert isinstance(u.addresses.others[0], Addresses.AddressItem)
print(u.addresses.dict())
```
For more usage, please refer to the following test files:
* tests/test_mutable_list.py
* tests/test_mutable_dict.py
* tests/test_mutable_pydantic_type.py
Raw data
{
"_id": null,
"home_page": "https://github.com/wonderbeyond/sqlalchemy-nested-mutable",
"name": "sqlalchemy-nested-mutable",
"maintainer": "",
"docs_url": null,
"requires_python": ">=3.8,<4.0",
"maintainer_email": "",
"keywords": "SQLAlchemy,Nested,Mutable,Types,JSON",
"author": "Wonder",
"author_email": "wonderbeyond@gmail.com",
"download_url": "https://files.pythonhosted.org/packages/a6/32/2640bff3cdedf612681d805c19dad2d47d0de6558390c63569d77ba204f8/sqlalchemy_nested_mutable-0.1.0.tar.gz",
"platform": null,
"description": "SQLAlchemy-Nested-Mutable\n=========================\n\nAn advanced SQLAlchemy column type factory that helps map compound Python types (e.g. `list`, `dict`, *Pydantic Model* and their hybrids) to database types (e.g. `ARRAY`, `JSONB`),\nAnd keep track of mutations in deeply nested data structures so that SQLAlchemy can emit proper *UPDATE* statements.\n\nSQLAlchemy-Nested-Mutable is highly inspired by SQLAlchemy-JSON<sup>[[0]](https://github.com/edelooff/sqlalchemy-json)</sup><sup>[[1]](https://variable-scope.com/posts/mutation-tracking-in-nested-json-structures-using-sqlalchemy)</sup>.\nHowever, it does not limit the mapped Python type to be `dict` or `list`.\n\n---\n\n## Why this package?\n\n* By default, SQLAlchemy does not track in-place mutations for non-scalar data types\n such as `list` and `dict` (which are usually mapped with `ARRAY` and `JSON/JSONB`).\n\n* Even though SQLAlchemy provides [an extension](https://docs.sqlalchemy.org/en/20/orm/extensions/mutable.html)\n to track mutations on compound objects, it's too shallow, i.e. it only tracks mutations on the first level of the compound object.\n\n* There exists the [SQLAlchemy-JSON](https://github.com/edelooff/sqlalchemy-json) package\n to help track mutations on nested `dict` or `list` data structures.\n However, the db type is limited to `JSON(B)`.\n\n* Also, I would like the mapped Python types can be subclasses of the Pydantic BaseModelModel,\n which have strong schemas, with the db type be schema-less JSON.\n\n\n## Installation\n\n```shell\npip install sqlalchemy-nested-mutable\n```\n\n## Usage\n\n> NOTE the example below is first updated in `examples/user-addresses.py` and then updated here.\n\n```python\nfrom typing import Optional, List\n\nimport pydantic\nimport sqlalchemy as sa\nfrom sqlalchemy.orm import Session, DeclarativeBase, Mapped, mapped_column\nfrom sqlalchemy_nested_mutable import MutablePydanticBaseModel\n\n\nclass Base(DeclarativeBase):\n pass\n\n\nclass Addresses(MutablePydanticBaseModel):\n \"\"\"A container for storing various addresses of users.\n\n NOTE: for working with pydantic model, use a subclass of `MutablePydanticBaseModel` for column mapping.\n However, the nested models (e.g. `AddressItem` below) should be direct subclasses of `pydantic.BaseModel`.\n \"\"\"\n\n class AddressItem(pydantic.BaseModel):\n street: str\n city: str\n area: Optional[str]\n\n preferred: AddressItem\n work: Optional[AddressItem]\n home: Optional[AddressItem]\n others: List[AddressItem] = []\n\n\nclass User(Base):\n __tablename__ = \"user_account\"\n\n id: Mapped[int] = mapped_column(primary_key=True)\n name: Mapped[str] = mapped_column(sa.String(30))\n addresses: Mapped[Addresses] = mapped_column(Addresses.as_mutable(), nullable=True)\n\n\nengine = sa.create_engine(\"sqlite://\")\nBase.metadata.create_all(engine)\n\nwith Session(engine) as s:\n s.add(u := User(name=\"foo\", addresses={\"preferred\": {\"street\": \"bar\", \"city\": \"baz\"}}))\n assert isinstance(u.addresses, MutablePydanticBaseModel)\n s.commit()\n\n u.addresses.preferred.street = \"bar2\"\n s.commit()\n assert u.addresses.preferred.street == \"bar2\"\n\n u.addresses.others.append(Addresses.AddressItem.parse_obj({\"street\": \"bar3\", \"city\": \"baz3\"}))\n s.commit()\n assert isinstance(u.addresses.others[0], Addresses.AddressItem)\n\n print(u.addresses.dict())\n```\n\nFor more usage, please refer to the following test files:\n\n* tests/test_mutable_list.py\n* tests/test_mutable_dict.py\n* tests/test_mutable_pydantic_type.py\n\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "SQLAlchemy Nested Mutable Types.",
"version": "0.1.0",
"project_urls": {
"Documentation": "https://github.com/wonderbeyond/sqlalchemy-nested-mutable",
"Homepage": "https://github.com/wonderbeyond/sqlalchemy-nested-mutable",
"Repository": "https://github.com/wonderbeyond/sqlalchemy-nested-mutable"
},
"split_keywords": [
"sqlalchemy",
"nested",
"mutable",
"types",
"json"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "944a85640f06fc5e00260bd238cbc39b11dc1048e9c8109279826a0ce1c6b655",
"md5": "7afc30acdff8cf95753d2afd952bbdef",
"sha256": "8df5c831d9b64971e73687cd73cbb8c851eb7cf87a28898f2c017fe6ce5c31df"
},
"downloads": -1,
"filename": "sqlalchemy_nested_mutable-0.1.0-py3-none-any.whl",
"has_sig": false,
"md5_digest": "7afc30acdff8cf95753d2afd952bbdef",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.8,<4.0",
"size": 8332,
"upload_time": "2023-06-06T09:13:53",
"upload_time_iso_8601": "2023-06-06T09:13:53.447582Z",
"url": "https://files.pythonhosted.org/packages/94/4a/85640f06fc5e00260bd238cbc39b11dc1048e9c8109279826a0ce1c6b655/sqlalchemy_nested_mutable-0.1.0-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "a6322640bff3cdedf612681d805c19dad2d47d0de6558390c63569d77ba204f8",
"md5": "3e8ac82ba938b36ce0c21a4610bdb332",
"sha256": "3949080b389d5ea7b1fa035f5d1eb93b77149bfa7af33a54611c7db2bcb88892"
},
"downloads": -1,
"filename": "sqlalchemy_nested_mutable-0.1.0.tar.gz",
"has_sig": false,
"md5_digest": "3e8ac82ba938b36ce0c21a4610bdb332",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.8,<4.0",
"size": 7537,
"upload_time": "2023-06-06T09:13:54",
"upload_time_iso_8601": "2023-06-06T09:13:54.419702Z",
"url": "https://files.pythonhosted.org/packages/a6/32/2640bff3cdedf612681d805c19dad2d47d0de6558390c63569d77ba204f8/sqlalchemy_nested_mutable-0.1.0.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2023-06-06 09:13:54",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "wonderbeyond",
"github_project": "sqlalchemy-nested-mutable",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"lcname": "sqlalchemy-nested-mutable"
}