Metadata-Version: 2.4
Name: SQLAlchemy-History
Version: 2.1.6
Summary: History tracking extension for SQLAlchemy.
Author-email: Corridor Platforms <postmaster@corridorplatforms.com>
License-Expression: Apache-2.0 AND BSD-3-Clause
Project-URL: Homepage, https://github.com/corridor/sqlalchemy-history
Project-URL: Repository, https://github.com/corridor/sqlalchemy-history
Project-URL: Issues, https://github.com/corridor/sqlalchemy-history/issues
Project-URL: Changelog, https://github.com/corridor/sqlalchemy-history/releases
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Typing :: Typed
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: SQLAlchemy>=2
Requires-Dist: SQLAlchemy-Utils>=0.30.12
Requires-Dist: anyio>3
Provides-Extra: asyncio
Requires-Dist: SQLAlchemy[asyncio]>=2; extra == "asyncio"
Provides-Extra: alembic
Requires-Dist: alembic; extra == "alembic"
Dynamic: license-file

# SQLAlchemy-History

SQLAlchemy-History is a fork of SQLAlchemy-Continuum. It is an auditing
extension that tracks the history of SQLAlchemy models.

## Features

- Supports SQLAlchemy 2+ and Python 3.9+
- Tracks history for inserts, deletes, and updates
- Does not store updates which don't change anything
- Supports Alembic migrations
- Can revert objects data as well as all object relations at given transaction even if the object was deleted
- Transactions can be queried afterwards using SQLAlchemy select syntax
- Query for changed records at given transaction
- Temporal relationship reflection. Get the relationships of an object in that point in time.
- Supports async SQLAlchemy

## Quickstart

```sh
uv pip install sqlalchemy-history
```

In order to make your models versioned you need two things:

1. Call `make_versioned()` before your models are defined.
2. Add `__versioned__` to all models you wish to add versioning to

```python
>>> from sqlalchemy_history import make_versioned
>>> make_versioned(user_cls=None)
>>> class Article(Base):
...    __versioned__ = {}
...    __tablename__ = 'article'
...    id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
...    name = sa.Column(sa.Unicode(255))
...    content = sa.Column(sa.UnicodeText)
>>> article = Article(name='Some article', content='Some content')
>>> session.add(article)
>>> session.commit()
'article has now one version stored in database'
>>> article.versions[0].name
'Some article'
>>> article.name = 'Updated name'
>>> session.commit()
>>> article.versions[1].name
'Updated name'
>>> article.versions[0].revert()
'lets revert back to first version'
>>> article.name
'Some article'
```

For completeness, below is a working example.

```python
from sqlalchemy_history import make_versioned
from sqlalchemy import Column, Integer, Unicode, UnicodeText, create_engine
from sqlalchemy.orm import DeclarativeBase, create_session, configure_mappers

make_versioned(user_cls=None)


class Base(DeclarativeBase):
    pass


class Article(Base):
    __versioned__ = {}
    __tablename__ = "article"
    id = Column(Integer, primary_key=True, autoincrement=True)
    name = Column(Unicode(255))
    content = Column(UnicodeText)


configure_mappers()
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
session = create_session(bind=engine, autocommit=False)
article = Article(name="Some article", content="Some content")
session.add(article)
session.commit()
print(article.versions[0].name)  # 'Some article'
article.name = "Updated name"
session.commit()
print(article.versions[1].name)  # 'Updated name'
article.versions[0].revert()
print(article.name)  # 'Some article'
```

<details>
<summary>Async working example</summary>

```python
import asyncio

import sqlalchemy as sa
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase, configure_mappers

from sqlalchemy_history import make_versioned

make_versioned(user_cls=None, options={"support_async": True})


class Base(DeclarativeBase):
    pass


class Article(Base):
    __versioned__ = {}
    __tablename__ = "article"
    id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
    name = sa.Column(sa.Unicode(255))
    content = sa.Column(sa.UnicodeText)


async def main():
    configure_mappers()

    engine = create_async_engine("sqlite+aiosqlite://")

    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)

    Session = async_sessionmaker(engine, expire_on_commit=False)

    async with Session() as session:
        article = Article(name="Some article", content="Some content")
        session.add(article)
        await session.commit()

        versions = (await session.scalars(article.versions.select())).all()
        print(versions[0].name)  # 'Some article'

        article.name = "Updated name"
        await session.commit()

        versions = (await session.scalars(article.versions.select())).all()
        print(versions[1].name)  # 'Updated name'

        versions[0].revert()
        await session.commit()
        print(article.name)  # 'Some article'

    await engine.dispose()


asyncio.run(main())
```

</details>

For more async querying and revert examples, see [Async support](docs/async.md).

## Resources

- [Documentation](https://corridor.github.io/sqlalchemy-history/)
- [Issue Tracker](https://github.com/corridor/sqlalchemy-history/issues)
- [Code](https://github.com/corridor/sqlalchemy-history/)

## More information

- [http://en.wikipedia.org/wiki/Slowly_changing_dimension](http://en.wikipedia.org/wiki/Slowly_changing_dimension)
- [http://en.wikipedia.org/wiki/Change_data_capture](http://en.wikipedia.org/wiki/Change_data_capture)
- [http://en.wikipedia.org/wiki/Anchor_Modeling](http://en.wikipedia.org/wiki/Anchor_Modeling)
- [http://en.wikipedia.org/wiki/Shadow_table](http://en.wikipedia.org/wiki/Shadow_table)
- [https://wiki.postgresql.org/wiki/Audit_trigger](https://wiki.postgresql.org/wiki/Audit_trigger)
- [https://wiki.postgresql.org/wiki/Audit_trigger_91plus](https://wiki.postgresql.org/wiki/Audit_trigger_91plus)
- [http://kosalads.blogspot.fi/2014/06/implement-audit-functionality-in.html](http://kosalads.blogspot.fi/2014/06/implement-audit-functionality-in.html)
- [https://github.com/2ndQuadrant/pgaudit](https://github.com/2ndQuadrant/pgaudit)

## Comparison

Primary reasons to create another library:

- Be future looking and support sqlalchemy 2.x
- Support multiple databases (sqlite, mysql, postgres, mssql, oracle)
- Focus on the history tracking and be as efficient as possible when doing it

We found multiple libraries which has an implementation of history tracking:

1. [sqlalchemy-continuum](https://github.com/kvesteri/sqlalchemy-continuum)
    - Does not support oracle, mssql
    - Feature filled making it difficult to maintain all plugins/extensions
2. [flask-continuum](https://github.com/bprinty/flask-continuum)
    - Thin wrapper on sqlalchemy-continuum specifically for flask
3. [postgresql-audit](https://github.com/kvesteri/postgresql-audit)
    - Supports only postgres
4. [versionalchemy](https://github.com/NerdWalletOSS/versionalchemy)
    - Not updated in a while
    - No reverting capability, Relationship queries on history not available
5. [django-simple-history](https://github.com/jazzband/django-simple-history)
    - Uses django ORM, does not support sqlalchemy
6. [sqlalchemy example versioning-objects](http://docs.sqlalchemy.org/en/latest/orm/examples.html#versioning-objects)
    - Simple example to demonstrate implementation - but very minimal
