sqlalchemy-events-lib 0.3.1__tar.gz → 0.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. {sqlalchemy_events_lib-0.3.1 → sqlalchemy_events_lib-0.3.2}/PKG-INFO +1 -1
  2. {sqlalchemy_events_lib-0.3.1 → sqlalchemy_events_lib-0.3.2}/pyproject.toml +1 -1
  3. {sqlalchemy_events_lib-0.3.1 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/__init__.py +1 -1
  4. {sqlalchemy_events_lib-0.3.1 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/callbacks_strategies/postgres_callback.py +1 -1
  5. {sqlalchemy_events_lib-0.3.1 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/handlers.py +10 -13
  6. {sqlalchemy_events_lib-0.3.1 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/registry.py +1 -1
  7. sqlalchemy_events_lib-0.3.2/src/sqlalchemy_events/types.py +40 -0
  8. {sqlalchemy_events_lib-0.3.1 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events_lib.egg-info/PKG-INFO +1 -1
  9. {sqlalchemy_events_lib-0.3.1 → sqlalchemy_events_lib-0.3.2}/src/tests/conftest.py +9 -2
  10. {sqlalchemy_events_lib-0.3.1 → sqlalchemy_events_lib-0.3.2}/src/tests/test_autodiscover.py +4 -5
  11. sqlalchemy_events_lib-0.3.2/src/tests/test_handlers.py +79 -0
  12. sqlalchemy_events_lib-0.3.1/src/sqlalchemy_events/types.py +0 -26
  13. sqlalchemy_events_lib-0.3.1/src/tests/test_handlers.py +0 -17
  14. {sqlalchemy_events_lib-0.3.1 → sqlalchemy_events_lib-0.3.2}/LICENSE +0 -0
  15. {sqlalchemy_events_lib-0.3.1 → sqlalchemy_events_lib-0.3.2}/README.md +0 -0
  16. {sqlalchemy_events_lib-0.3.1 → sqlalchemy_events_lib-0.3.2}/setup.cfg +0 -0
  17. {sqlalchemy_events_lib-0.3.1 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/callbacks_strategies/__init__.py +0 -0
  18. {sqlalchemy_events_lib-0.3.1 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/callbacks_strategies/base.py +0 -0
  19. {sqlalchemy_events_lib-0.3.1 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/core.py +0 -0
  20. {sqlalchemy_events_lib-0.3.1 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/decorators.py +0 -0
  21. {sqlalchemy_events_lib-0.3.1 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/default_logger.py +0 -0
  22. {sqlalchemy_events_lib-0.3.1 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/discovery.py +0 -0
  23. {sqlalchemy_events_lib-0.3.1 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/events.py +0 -0
  24. {sqlalchemy_events_lib-0.3.1 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/exceptions.py +0 -0
  25. {sqlalchemy_events_lib-0.3.1 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/init_triggers_strategies/__init__.py +0 -0
  26. {sqlalchemy_events_lib-0.3.1 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/init_triggers_strategies/base.py +0 -0
  27. {sqlalchemy_events_lib-0.3.1 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/init_triggers_strategies/postgres_init_triggers.py +0 -0
  28. {sqlalchemy_events_lib-0.3.1 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/utils.py +0 -0
  29. {sqlalchemy_events_lib-0.3.1 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events_lib.egg-info/SOURCES.txt +0 -0
  30. {sqlalchemy_events_lib-0.3.1 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events_lib.egg-info/dependency_links.txt +0 -0
  31. {sqlalchemy_events_lib-0.3.1 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events_lib.egg-info/requires.txt +0 -0
  32. {sqlalchemy_events_lib-0.3.1 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events_lib.egg-info/top_level.txt +0 -0
  33. {sqlalchemy_events_lib-0.3.1 → sqlalchemy_events_lib-0.3.2}/src/tests/test_is_created_triggers.py +0 -0
  34. {sqlalchemy_events_lib-0.3.1 → sqlalchemy_events_lib-0.3.2}/src/tests/test_models.py +0 -0
  35. {sqlalchemy_events_lib-0.3.1 → sqlalchemy_events_lib-0.3.2}/src/tests/test_sa_events.py +0 -0
  36. {sqlalchemy_events_lib-0.3.1 → sqlalchemy_events_lib-0.3.2}/src/tests/test_with_events_deco.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlalchemy-events-lib
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Event-driven extension for SQLAlchemy that enables listening to database CUD events. This library allows you to react to database changes in real time using a clean, declarative API.
5
5
  Author-email: Alexey Kostarev <normjkeeewm@gmail.com>
6
6
  License: MIT
@@ -10,7 +10,7 @@ where = ["src"]
10
10
 
11
11
  [project]
12
12
  name = "sqlalchemy-events-lib"
13
- version = "0.3.1"
13
+ version = "0.3.2"
14
14
  description = "Event-driven extension for SQLAlchemy that enables listening to database CUD events. This library allows you to react to database changes in real time using a clean, declarative API."
15
15
  requires-python = ">=3.12"
16
16
  authors = [
@@ -11,4 +11,4 @@ __all__ = [
11
11
  'with_events',
12
12
  'SaEvent'
13
13
  ]
14
- __version__ = '0.3.1'
14
+ __version__ = '0.3.2'
@@ -66,4 +66,4 @@ class PostgresCallback(SaEventsCallbacksStrategy):
66
66
  )
67
67
  )
68
68
 
69
- await asyncio.gather(*tasks)
69
+ return await asyncio.gather(*tasks)
@@ -1,5 +1,3 @@
1
- import inspect
2
- from pathlib import Path
3
1
  from typing import Callable, Type
4
2
 
5
3
  from sqlalchemy.orm import DeclarativeBase
@@ -13,21 +11,20 @@ def __inner(func: Callable, sa_event: SaEvent, model: Type[DeclarativeBase]):
13
11
  if not issubclass(model, DeclarativeBase):
14
12
  raise RuntimeError('Model must inherit from DeclarativeBase')
15
13
 
14
+ if '.' in func.__qualname__ and not isinstance(func, staticmethod):
15
+ raise RuntimeError(f'Handler must be a regular function or staticmethod, not method or classmethod of '
16
+ f'{func.__qualname__.split('.')[0]}')
17
+
18
+ if isinstance(func, staticmethod):
19
+ func = func.__func__
20
+
16
21
  event_handlers = get_event_handlers()
17
22
  trig_name = f'sa_{model.__tablename__}_{sa_event.lower()}_notify'
18
- func_path = inspect.getsourcefile(func) or inspect.getfile(func)
19
- func_path = Path(func_path)
20
- func_path_name = f'{func_path.parent.name}/{func_path.name}/{func.__name__}'
21
23
  handlers: list[Handler] | None = event_handlers.get(trig_name)
24
+
22
25
  obj_handler = Handler(func=func, args={'model': model})
23
- if handlers:
24
- for handler in handlers:
25
- handler_path = inspect.getsourcefile(handler.func) or inspect.getfile(handler.func)
26
- handler_path = Path(handler_path)
27
- handler_path_name = f'{handler_path.parent.name}/{handler_path.name}/{handler.func.__name__}'
28
- if func_path_name == handler_path_name:
29
- continue
30
- handlers.append(obj_handler)
26
+ if handlers and obj_handler not in handlers:
27
+ handlers.append(obj_handler)
31
28
  else:
32
29
  event_handlers[trig_name] = [obj_handler]
33
30
 
@@ -11,5 +11,5 @@ if _GLOBAL_KEY not in sys.modules:
11
11
  _registry = sys.modules[_GLOBAL_KEY]
12
12
 
13
13
 
14
- def get_event_handlers():
14
+ def get_event_handlers() -> dict:
15
15
  return _registry.handlers
@@ -0,0 +1,40 @@
1
+ import inspect
2
+ from dataclasses import dataclass
3
+ from enum import StrEnum
4
+ from pathlib import Path
5
+ from typing import Any, Awaitable, Callable, TypeAlias, Union
6
+
7
+
8
+ class SaEvent(StrEnum):
9
+ INSERT = 'INSERT'
10
+ UPDATE = 'UPDATE'
11
+ DELETE = 'DELETE'
12
+
13
+
14
+ class Dialect(StrEnum):
15
+ POSTGRESQL = 'postgresql'
16
+ SQLITE = 'sqlite'
17
+ MYSQL = 'mysql'
18
+ MSSQL = 'mssql'
19
+ ORACLE = 'oracle'
20
+
21
+
22
+ @dataclass
23
+ class Handler:
24
+ func: Union[Callable, Awaitable]
25
+ args: dict[str, Any]
26
+ full_path: str | None = None
27
+
28
+ def __post_init__(self):
29
+ func_path = inspect.getsourcefile(self.func) or inspect.getfile(self.func)
30
+ func_path = Path(func_path)
31
+ self.full_path = f'{func_path.parent.name}/{func_path.name}/{self.func.__name__}'
32
+
33
+ def __hash__(self):
34
+ return hash(self.full_path)
35
+
36
+ def __eq__(self, other):
37
+ return self.full_path == other.full_path
38
+
39
+
40
+ DB_ID: TypeAlias = str | int
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlalchemy-events-lib
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Event-driven extension for SQLAlchemy that enables listening to database CUD events. This library allows you to react to database changes in real time using a clean, declarative API.
5
5
  Author-email: Alexey Kostarev <normjkeeewm@gmail.com>
6
6
  License: MIT
@@ -1,4 +1,3 @@
1
- import asyncio
2
1
  import os
3
2
 
4
3
  import pytest_asyncio
@@ -7,6 +6,8 @@ from dotenv import load_dotenv
7
6
  from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
8
7
  from sqlalchemy import text
9
8
  from sqlalchemy_events import SQLAlchemyEvents
9
+ from sqlalchemy_events.events import sa_events_strategy
10
+ from sqlalchemy_events.utils import dialect_resolver
10
11
 
11
12
  from tests.test_models import Base
12
13
 
@@ -45,4 +46,10 @@ async def session(engine):
45
46
 
46
47
  async with async_session() as session:
47
48
  yield session
48
- await session.rollback()
49
+ await session.rollback()
50
+
51
+
52
+ @pytest_asyncio.fixture
53
+ async def mock_callback(engine):
54
+ dialect = dialect_resolver(engine)
55
+ yield sa_events_strategy[dialect].callback
@@ -117,18 +117,17 @@ async def test_discover_handlers():
117
117
  assert handlers
118
118
  result_handlers = []
119
119
  for key, handler_list in handlers.items():
120
- assert len(handler_list) == 1
121
120
  if 'insert' in key:
122
121
  assert handler_list[0].func.__name__ == 'insert_handler'
123
- result_handlers.extend(handler_list)
122
+ result_handlers.append(handler_list[0])
124
123
  if 'update' in key:
125
124
  assert handler_list[0].func.__name__ == 'update_handler'
126
- result_handlers.extend(handler_list)
125
+ result_handlers.append(handler_list[0])
127
126
  if 'delete' in key:
128
127
  assert handler_list[0].func.__name__ == 'delete_handler'
129
- result_handlers.extend(handler_list)
128
+ result_handlers.append(handler_list[0])
130
129
 
131
- assert len(result_handlers) == len(handlers)
130
+ assert len(result_handlers) == 3
132
131
 
133
132
 
134
133
  def test_invalid_autodiscover_path():
@@ -0,0 +1,79 @@
1
+ import json
2
+ import pytest
3
+
4
+ from sqlalchemy_events import sa_delete_handler, sa_insert_handler, sa_update_handler
5
+ from tests.test_models import UserModel
6
+
7
+
8
+ def test_invalid_model_in_handler():
9
+ with pytest.raises(RuntimeError):
10
+ @sa_insert_handler(object)
11
+ def test_func():
12
+ pass
13
+
14
+
15
+ def test_handler_signature():
16
+ with pytest.raises(TypeError):
17
+ @sa_insert_handler()
18
+ def test_func():
19
+ pass
20
+
21
+
22
+ class Test:
23
+ def test_method(self):
24
+ pass
25
+
26
+ @classmethod
27
+ def test_class_method(cls):
28
+ pass
29
+
30
+ @staticmethod
31
+ def test_static_method():
32
+ pass
33
+
34
+
35
+ def test_handler_in_class():
36
+ with pytest.raises(RuntimeError):
37
+ sa_insert_handler(UserModel)(Test().test_method)
38
+
39
+ with pytest.raises(RuntimeError):
40
+ sa_update_handler(UserModel)(Test.test_class_method)
41
+
42
+
43
+ @sa_insert_handler(UserModel)
44
+ async def insert_user_handler(rows):
45
+ assert rows == [1, 2, 3]
46
+ return rows
47
+
48
+
49
+ @sa_update_handler(UserModel)
50
+ async def update_user_handler(rows):
51
+ assert rows == [4, 5, 6]
52
+ return rows
53
+
54
+
55
+ @sa_delete_handler(UserModel)
56
+ async def delete_user_handler(rows):
57
+ assert rows == [7, 8, 9]
58
+ return rows
59
+
60
+
61
+ @pytest.mark.parametrize(
62
+ 'op,rows,resp',
63
+ [
64
+ ('insert', [1, 2, 3], [None, [1, 2, 3]]),
65
+ ('update', [4, 5, 6], [None, [4, 5, 6]]),
66
+ ('delete', [7, 8, 9], [None, [7, 8, 9]]),
67
+ ]
68
+ )
69
+ async def test_handlers(mock_callback, op, rows, resp):
70
+ response = await mock_callback.handle(
71
+ None, None, None, json.dumps(
72
+ {
73
+ 'op': op,
74
+ 'table': 'users',
75
+ 'rows': rows
76
+ }
77
+ )
78
+ )
79
+ assert response == resp
@@ -1,26 +0,0 @@
1
- from dataclasses import dataclass
2
- from enum import StrEnum
3
- from typing import Any, Awaitable, Callable, TypeAlias, Union
4
-
5
-
6
- class SaEvent(StrEnum):
7
- INSERT = 'INSERT'
8
- UPDATE = 'UPDATE'
9
- DELETE = 'DELETE'
10
-
11
-
12
- class Dialect(StrEnum):
13
- POSTGRESQL = 'postgresql'
14
- SQLITE = 'sqlite'
15
- MYSQL = 'mysql'
16
- MSSQL = 'mssql'
17
- ORACLE = 'oracle'
18
-
19
-
20
- @dataclass
21
- class Handler:
22
- func: Union[Callable, Awaitable]
23
- args: dict[str, Any]
24
-
25
-
26
- DB_ID: TypeAlias = str | int
@@ -1,17 +0,0 @@
1
- import pytest
2
-
3
- from sqlalchemy_events import sa_insert_handler
4
-
5
-
6
- def test_invalid_model_in_handler():
7
- with pytest.raises(RuntimeError):
8
- @sa_insert_handler(object)
9
- def test_func():
10
- pass
11
-
12
-
13
- def test_handler_signature():
14
- with pytest.raises(TypeError):
15
- @sa_insert_handler()
16
- def test_func():
17
- pass