Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 11 additions & 13 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
strategy:
matrix:
python-version:
- "3.7"
- "3.9"
- "3.12"
DB:
- "sqlite"
Expand Down Expand Up @@ -72,17 +72,17 @@ jobs:
--health-interval 30s
--health-timeout 15s
--health-retries 5
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v5
- name: Install Python
uses: actions/setup-python@v4
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Install Project
run: |
python -m pip install --upgrade pip
pip install --upgrade poetry
pip install --upgrade poetry
poetry install
- name: install oracle dependencies
if: ${{ matrix.DB == 'oracle' }}
Expand Down Expand Up @@ -111,17 +111,15 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Black Validation
uses: psf/black@stable
- uses: actions/checkout@v5
- uses: astral-sh/ruff-action@v3
with:
version: "23.3.0" # Last version which can be used in py3.7
options: "--check --verbose"
- uses: actions/checkout@v3
- name: ruff-action
uses: chartboost/ruff-action@v1
args: "--version"
- run: ruff check --fix
- run: ruff format

generate-coverage-report:
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
needs: [test-sqlalchemy-history]
steps:
- name: Coveralls
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
- name: Setup Python
uses: actions/setup-python@v2
uses: actions/setup-python@v6
with:
python-version: 3.9
- name: Install Dependencies
Expand Down
5 changes: 3 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ git checkout -b add-issue-num
# Run tests locally
DB=sqlite poetry run pytest

# Lint
poetry run black .
# Lint & Format
poetry run ruff format .
poetry run ruff check --fix .
```
- Add commit for your changes with message title and message description brifly explaining the approach
- Keep commit message title 72 characters
Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ An auditing extension for sqlalchemy which keeps a track of the history of your

## Features

- Supports sqlalchemy 2+ and python 3.7+
- 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
Expand Down
11 changes: 4 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,13 @@ classifiers=[
]

[tool.poetry.dependencies]
python = "^3.7"
python = "^3.9"

SQLAlchemy = ">=2"
SQLAlchemy-Utils = ">=0.30.12"
cached-property = "*"

[tool.poetry.dev-dependencies]
black = "*"
[tool.poetry.group.dev.dependencies]
ruff = "*"
pre-commit = "2.21.0"

Expand All @@ -41,11 +40,9 @@ pymssql = ">=2.2.0"
cx-Oracle = "8.3.0"
pytest-cov = "*"


[tool.black]
# Set line-length lower than 120 as black will go 10% above if needed
[tool.ruff]
line-length = 110
target-version = ['py37']
target-version = 'py39'

[tool.coverage.run]
dynamic_context = "test_function"
Expand Down
2 changes: 1 addition & 1 deletion sqlalchemy_history/builder.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Builder Module Detects and builds version class for models and version tables collected during instrument
phase by the manager
phase by the manager
"""

from copy import copy
Expand Down
4 changes: 2 additions & 2 deletions sqlalchemy_history/expression_reflector.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""This is ExpressionReflector used for generating expression queries.
"""
"""This is ExpressionReflector used for generating expression queries."""

import sqlalchemy as sa
from sqlalchemy.sql.expression import bindparam

Expand Down
4 changes: 2 additions & 2 deletions sqlalchemy_history/fetcher.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Fetcher Module helps traverse across versions for a given versioned object.
"""
"""Fetcher Module helps traverse across versions for a given versioned object."""

import operator
import sqlalchemy as sa
from sqlalchemy_utils import get_primary_keys, identity
Expand Down
1 change: 1 addition & 0 deletions sqlalchemy_history/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
and the actual versioning to UnitOfWork class.

"""

from functools import wraps

import sqlalchemy as sa
Expand Down
4 changes: 2 additions & 2 deletions sqlalchemy_history/model_builder.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Model Builder module build Versioned Models
"""
"""Model Builder module build Versioned Models"""

from copy import copy
import sqlalchemy as sa
from sqlalchemy.ext.declarative import declared_attr
Expand Down
10 changes: 7 additions & 3 deletions sqlalchemy_history/operation.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"""Operations module contains Operation Class.
"""
"""Operations module contains Operation Class."""

from copy import copy

Expand Down Expand Up @@ -90,7 +89,12 @@ def add_update(self, target):
del state_copy[rel_key]

if state_copy:
self.add(Operation(target, Operation.UPDATE))
if target in self:
# If already in current transaction and some event hook did a update
# prior to commit hook, continue with operation type as it is
self.add(Operation(target, self[self.format_key(target)].type))
else:
self.add(Operation(target, Operation.UPDATE))

def add_delete(self, target):
self.add(Operation(target, Operation.DELETE))
1 change: 1 addition & 0 deletions sqlalchemy_history/plugins/transaction_changes.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
233678 Article
================ =================
"""

import sqlalchemy as sa

from sqlalchemy_history.plugins.base import Plugin
Expand Down
3 changes: 2 additions & 1 deletion sqlalchemy_history/relationship_builder.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Relationship Builder builds and manages relations between versioned model built by builder
module for versioned package
module for versioned package
"""

import sqlalchemy as sa

from sqlalchemy_history.exc import ClassNotVersioned
Expand Down
4 changes: 2 additions & 2 deletions sqlalchemy_history/reverter.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Reverter Reverts.
"""
"""Reverter Reverts."""

import sqlalchemy as sa
from sqlalchemy_history.operation import Operation
from sqlalchemy_history.utils import versioned_column_properties, parent_class
Expand Down
6 changes: 3 additions & 3 deletions sqlalchemy_history/table_builder.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Table Builder Builds versioned table.
"""
"""Table Builder Builds versioned table."""

import sqlalchemy as sa

from sqlalchemy.sql.sqltypes import Enum
Expand Down Expand Up @@ -34,7 +34,7 @@ def reflect_column(self, column):
if column_copy.name == self.option("transaction_column_name"):
column_copy.nullable = False
if isinstance(column_copy.type, Enum):
column_copy.type.name = 'history_' + column_copy.type.name
column_copy.type.name = "history_" + column_copy.type.name

if not column_copy.primary_key:
column_copy.nullable = True
Expand Down
6 changes: 3 additions & 3 deletions sqlalchemy_history/transaction.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"""Transaction model makes transactions for history tables
"""
"""Transaction model makes transactions for history tables"""

from collections import OrderedDict
import datetime
Expand Down Expand Up @@ -113,7 +112,8 @@ def __repr__(self):
)
return "<Transaction %s>" % ", ".join(
(
"%s=%r" % (field, value) if not isinstance(value, int)
"%s=%r" % (field, value)
if not isinstance(value, int)
# We want the following line to ensure that longs get
# shown without the ugly L suffix on python 2.x
# versions
Expand Down
8 changes: 4 additions & 4 deletions sqlalchemy_history/unit_of_work.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
"""UnitOfWork module tracks all unit of transaction needed to be done to track history models trnasactions
"""
"""UnitOfWork module tracks all unit of transaction needed to be done to track history models transactions"""

from copy import copy

import sqlalchemy as sa
from sqlalchemy_utils import get_primary_keys, identity

from sqlalchemy_history.operation import Operations
from sqlalchemy_history.schema import update_end_tx_column
from sqlalchemy_history.utils import (
end_tx_column_name,
version_class,
is_session_modified,
tx_column_name,
version_class,
versioned_column_properties,
)
from sqlalchemy_history.schema import update_end_tx_column


class UnitOfWork(object):
Expand Down
10 changes: 3 additions & 7 deletions tests/builders/test_table_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,7 @@ class Article(self.Model):
last_update = sa.Column(
sa.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False
)
enum_col = sa.Column(
sa.Enum('TYPE_A', 'TYPE_B', name='test_enum')
)
enum_col = sa.Column(sa.Enum("TYPE_A", "TYPE_B", name="test_enum"))

self.Article = Article

Expand Down Expand Up @@ -139,12 +137,10 @@ class Article(self.Model):
sa.Integer, sa.Sequence(f"{__tablename__}_seq", start=1), autoincrement=True, primary_key=True
)

enum_col = sa.Column(
sa.Enum('TYPE_A', 'TYPE_B', name='test_enum')
)
enum_col = sa.Column(sa.Enum("TYPE_A", "TYPE_B", name="test_enum"))

self.Article = Article

def test_name_enums(self):
version_model = version_class(self.Article)
assert version_model.enum_col.type.name == 'history_test_enum'
assert version_model.enum_col.type.name == "history_test_enum"
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import sqlalchemy as sa
from copy import copy

from tests import TestCase
from sqlalchemy_history import version_class


class TestBug141(TestCase):
# ref: https://github.com/corridor/sqlalchemy-history/issues/141
def create_models(self):
class Author(self.Model):
__tablename__ = "author"
__versioned__ = copy(self.options)

id = sa.Column(
sa.Integer, sa.Sequence(f"{__tablename__}_seq", start=1), autoincrement=True, primary_key=True
)
name = sa.Column(sa.Unicode(255))

self.Author = Author

def test_add_record(self):
author = self.Author(name="Author 1")

@sa.event.listens_for(self.session, "after_flush_postexec")
def after_flush_postexec(session, flush_context):
if author.name != "yoyoyoyoyo":
author.name = "yoyoyoyoyo"

self.session.add(author)
self.session.commit()

versioned_objs = self.session.query(version_class(self.Author)).all()
assert len(versioned_objs) == 1
assert versioned_objs[0].operation_type == 0
assert versioned_objs[0].name == "yoyoyoyoyo"
author.name = "sdfeoinfe"
self.session.add(author)
self.session.commit()
versioned_objs = self.session.query(version_class(self.Author)).all()
assert len(versioned_objs) == 2
assert versioned_objs[0].operation_type == 0
assert versioned_objs[1].operation_type == 1
assert versioned_objs[0].name == versioned_objs[1].name == "yoyoyoyoyo"
sa.event.remove(self.session, "after_flush_postexec", after_flush_postexec)
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def create_models(self):
sa.DateTime,
nullable=False,
server_default=sa.func.current_timestamp(),
default=lambda: datetime.datetime.now(datetime.timezone.utc)
default=lambda: datetime.datetime.now(datetime.timezone.utc),
),
)

Expand Down
2 changes: 1 addition & 1 deletion tests/schema/test_update_end_transaction_id.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def create_models(self):
sa.DateTime,
nullable=False,
server_default=sa.func.current_timestamp(),
default=lambda: datetime.datetime.now(datetime.timezone.utc)
default=lambda: datetime.datetime.now(datetime.timezone.utc),
),
)

Expand Down
9 changes: 1 addition & 8 deletions tests/test_exotic_operation_combos.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import os
from pytest import mark

from sqlalchemy_history.operation import Operation
from tests import TestCase, create_test_cases

Expand Down Expand Up @@ -40,10 +37,6 @@ def test_insert_deleted_and_flushed_object(self):
assert article2.versions[0].operation_type == Operation.INSERT
assert article2.versions[1].operation_type == Operation.UPDATE

# Ref for mssql: https://github.com/sqlalchemy/sqlalchemy/discussions/8829
@mark.skipif(
os.environ.get("DB") == "mssql", reason="mssql does not support changing the IDENTITY column"
)
def test_replace_deleted_object_with_update(self):
article = self.Article()
article.name = "Some article"
Expand All @@ -58,7 +51,7 @@ def test_replace_deleted_object_with_update(self):
self.session.delete(article)
self.session.flush()

article2.id = article.id
article2.name = article.name
Copy link
Contributor

@AbdealiLoKo AbdealiLoKo Aug 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually updating ID seems to be the reason this testcase was written

If you see the skipif - it is trying to update the "identity" and seeing how that works

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so that skipif was added by us only, when we were extending package support to mssql db, i changed it because changing ID won't be right in real sense and i tried it out as well
.
say i change the ID of article2 to article now if i try to collect version objects of article2 i'll get version object of article1
.
i missed removing the skipif, i'll remove the skipif since we no longer update the identity so this should not fail with mssql.

self.session.commit()
assert article2.versions.count() == 2
assert article2.versions[0].operation_type == Operation.INSERT
Expand Down
6 changes: 4 additions & 2 deletions tests/test_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,13 @@ def test_changed_entities(self):
}

def test_transaction_issued_at(self):
time.sleep(1)
time.sleep(1)
self.article.name = "Some article 2"
self.session.add(self.article)
self.session.commit()
assert self.article.versions[0].transaction.issued_at != self.article.versions[1].transaction.issued_at
assert (
self.article.versions[0].transaction.issued_at != self.article.versions[1].transaction.issued_at
)


# Check that the tests pass without TransactionChangesPlugin
Expand Down
Loading