sqlalchemy-events-lib 0.3.0__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.0 → sqlalchemy_events_lib-0.3.2}/PKG-INFO +18 -9
  2. {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/README.md +17 -8
  3. {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/pyproject.toml +1 -1
  4. {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/__init__.py +1 -1
  5. {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/callbacks_strategies/postgres_callback.py +1 -1
  6. {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/core.py +1 -2
  7. {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/decorators.py +7 -0
  8. {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/default_logger.py +1 -1
  9. sqlalchemy_events_lib-0.3.2/src/sqlalchemy_events/discovery.py +44 -0
  10. {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/handlers.py +11 -14
  11. {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/registry.py +1 -1
  12. sqlalchemy_events_lib-0.3.2/src/sqlalchemy_events/types.py +40 -0
  13. {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events_lib.egg-info/PKG-INFO +18 -9
  14. {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events_lib.egg-info/SOURCES.txt +4 -1
  15. {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/tests/conftest.py +11 -5
  16. {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/tests/test_autodiscover.py +23 -5
  17. sqlalchemy_events_lib-0.3.2/src/tests/test_handlers.py +79 -0
  18. sqlalchemy_events_lib-0.3.2/src/tests/test_sa_events.py +27 -0
  19. sqlalchemy_events_lib-0.3.2/src/tests/test_with_events_deco.py +45 -0
  20. sqlalchemy_events_lib-0.3.0/src/sqlalchemy_events/discovery.py +0 -30
  21. sqlalchemy_events_lib-0.3.0/src/sqlalchemy_events/types.py +0 -26
  22. {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/LICENSE +0 -0
  23. {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/setup.cfg +0 -0
  24. {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/callbacks_strategies/__init__.py +0 -0
  25. {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/callbacks_strategies/base.py +0 -0
  26. {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/events.py +0 -0
  27. {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/exceptions.py +0 -0
  28. {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/init_triggers_strategies/__init__.py +0 -0
  29. {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/init_triggers_strategies/base.py +0 -0
  30. {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/init_triggers_strategies/postgres_init_triggers.py +0 -0
  31. {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/utils.py +0 -0
  32. {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events_lib.egg-info/dependency_links.txt +0 -0
  33. {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events_lib.egg-info/requires.txt +0 -0
  34. {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events_lib.egg-info/top_level.txt +0 -0
  35. {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/tests/test_is_created_triggers.py +0 -0
  36. {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/tests/test_models.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlalchemy-events-lib
3
- Version: 0.3.0
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
@@ -96,7 +96,6 @@ ___
96
96
  session.py
97
97
  ```python
98
98
  from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
99
-
100
99
  from config import DATABASE_URL
101
100
 
102
101
  engine = create_async_engine(DATABASE_URL)
@@ -114,10 +113,11 @@ from session import engine
114
113
 
115
114
 
116
115
  async def main():
117
- SQLAlchemyEvents(
116
+ sa_events = SQLAlchemyEvents(
118
117
  engine=engine,
119
118
  autodiscover_paths=['services']
120
119
  )
120
+ await sa_events()
121
121
  while True:
122
122
  await asyncio.sleep(9999)
123
123
 
@@ -165,16 +165,25 @@ If provided, the library will log internal lifecycle events such as:
165
165
  **Example:**
166
166
  ```python
167
167
  import logging
168
+ import asyncio
169
+ from sqlalchemy_events import SQLAlchemyEvents
170
+ from session import engine
168
171
 
169
172
  logger = logging.getLogger('sqlalchemy_events')
170
173
  logger.setLevel(logging.INFO)
171
174
 
172
- SQLAlchemyEvents(
173
- base=Base,
174
- engine=engine,
175
- autodiscover_paths=['services'],
176
- logger=logger
177
- )
175
+ async def main():
176
+ sa_events = SQLAlchemyEvents(
177
+ engine=engine,
178
+ autodiscover_paths=['services'],
179
+ logger=logger
180
+ )
181
+ await sa_events()
182
+ while True:
183
+ await asyncio.sleep(9999)
184
+
185
+ if __name__ == '__main__':
186
+ asyncio.run(main())
178
187
  ```
179
188
 
180
189
  ## How it works
@@ -74,7 +74,6 @@ ___
74
74
  session.py
75
75
  ```python
76
76
  from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
77
-
78
77
  from config import DATABASE_URL
79
78
 
80
79
  engine = create_async_engine(DATABASE_URL)
@@ -92,10 +91,11 @@ from session import engine
92
91
 
93
92
 
94
93
  async def main():
95
- SQLAlchemyEvents(
94
+ sa_events = SQLAlchemyEvents(
96
95
  engine=engine,
97
96
  autodiscover_paths=['services']
98
97
  )
98
+ await sa_events()
99
99
  while True:
100
100
  await asyncio.sleep(9999)
101
101
 
@@ -143,16 +143,25 @@ If provided, the library will log internal lifecycle events such as:
143
143
  **Example:**
144
144
  ```python
145
145
  import logging
146
+ import asyncio
147
+ from sqlalchemy_events import SQLAlchemyEvents
148
+ from session import engine
146
149
 
147
150
  logger = logging.getLogger('sqlalchemy_events')
148
151
  logger.setLevel(logging.INFO)
149
152
 
150
- SQLAlchemyEvents(
151
- base=Base,
152
- engine=engine,
153
- autodiscover_paths=['services'],
154
- logger=logger
155
- )
153
+ async def main():
154
+ sa_events = SQLAlchemyEvents(
155
+ engine=engine,
156
+ autodiscover_paths=['services'],
157
+ logger=logger
158
+ )
159
+ await sa_events()
160
+ while True:
161
+ await asyncio.sleep(9999)
162
+
163
+ if __name__ == '__main__':
164
+ asyncio.run(main())
156
165
  ```
157
166
 
158
167
  ## How it works
@@ -10,7 +10,7 @@ where = ["src"]
10
10
 
11
11
  [project]
12
12
  name = "sqlalchemy-events-lib"
13
- version = "0.3.0"
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.0'
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)
@@ -30,9 +30,8 @@ class SQLAlchemyEvents:
30
30
  self.autodiscover_paths = autodiscover_paths
31
31
  self.logger = logger or DefaultLogger() if verbose else None
32
32
  self.verbose = verbose
33
- asyncio.create_task(self.__init())
34
33
 
35
- async def __init(self) -> None:
34
+ async def __call__(self) -> None:
36
35
  if not isinstance(self.engine, (AsyncEngine, Engine)):
37
36
  raise RuntimeError(
38
37
  '[SQLAlchemyEvents] \'engine\' must be an instance of '
@@ -1,12 +1,19 @@
1
+ from sqlalchemy.orm import DeclarativeBase
2
+
1
3
  from .types import SaEvent
2
4
 
3
5
 
4
6
  def with_events(events: list[SaEvent]):
7
+ if not isinstance(events, list) or not events:
8
+ raise RuntimeError('Events must be a list with at least one SeEvent element')
9
+
5
10
  for e in events:
6
11
  if not isinstance(e, SaEvent):
7
12
  raise RuntimeError(f'Event must be a SaEvent instance, not {type(e)}')
8
13
 
9
14
  def wrapper(cls):
15
+ if not issubclass(cls, DeclarativeBase):
16
+ raise RuntimeError('Model must inherit from DeclarativeBase')
10
17
  cls.__events__ = set(events)
11
18
 
12
19
  class Events:
@@ -3,7 +3,7 @@ import logging
3
3
 
4
4
  class DefaultLogger:
5
5
  def __init__(self) -> None:
6
- self.__logger = logging.getLogger()
6
+ self.__logger = logging.getLogger('SQLAlchemyEvents')
7
7
  self.__logger.setLevel(logging.INFO)
8
8
 
9
9
  if not self.__logger.handlers:
@@ -0,0 +1,44 @@
1
+ import importlib
2
+ import pkgutil
3
+ from pathlib import Path
4
+ from types import ModuleType
5
+ from typing import Iterable, List
6
+
7
+
8
+ def autodiscover(paths: Iterable[str | Path]) -> List[ModuleType]:
9
+ modules: List[ModuleType] = []
10
+ seen: set[str] = set()
11
+
12
+ def add_module(module: ModuleType):
13
+ if module.__name__ in seen:
14
+ return
15
+ seen.add(module.__name__)
16
+ modules.append(module)
17
+
18
+ for path in paths:
19
+ is_file = False
20
+
21
+ if isinstance(path, Path):
22
+ is_file = path.suffix == '.py'
23
+ path = str(path)
24
+
25
+ if isinstance(path, str) and path.endswith('.py'):
26
+ is_file = True
27
+ path = path[:-3]
28
+
29
+ path = path.replace('\\', '.').replace('/', '.').lstrip('.')
30
+
31
+ module = importlib.import_module(path)
32
+ add_module(module)
33
+
34
+ if is_file or not hasattr(module, '__path__'):
35
+ continue
36
+
37
+ for _, module_name, _ in pkgutil.walk_packages(
38
+ module.__path__,
39
+ module.__name__ + '.',
40
+ ):
41
+ submodule = importlib.import_module(module_name)
42
+ add_module(submodule)
43
+
44
+ return modules
@@ -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
@@ -10,24 +8,23 @@ from .types import Handler
10
8
 
11
9
 
12
10
  def __inner(func: Callable, sa_event: SaEvent, model: Type[DeclarativeBase]):
13
- if not isinstance(model, type) or not issubclass(model, DeclarativeBase):
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.0
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
@@ -96,7 +96,6 @@ ___
96
96
  session.py
97
97
  ```python
98
98
  from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
99
-
100
99
  from config import DATABASE_URL
101
100
 
102
101
  engine = create_async_engine(DATABASE_URL)
@@ -114,10 +113,11 @@ from session import engine
114
113
 
115
114
 
116
115
  async def main():
117
- SQLAlchemyEvents(
116
+ sa_events = SQLAlchemyEvents(
118
117
  engine=engine,
119
118
  autodiscover_paths=['services']
120
119
  )
120
+ await sa_events()
121
121
  while True:
122
122
  await asyncio.sleep(9999)
123
123
 
@@ -165,16 +165,25 @@ If provided, the library will log internal lifecycle events such as:
165
165
  **Example:**
166
166
  ```python
167
167
  import logging
168
+ import asyncio
169
+ from sqlalchemy_events import SQLAlchemyEvents
170
+ from session import engine
168
171
 
169
172
  logger = logging.getLogger('sqlalchemy_events')
170
173
  logger.setLevel(logging.INFO)
171
174
 
172
- SQLAlchemyEvents(
173
- base=Base,
174
- engine=engine,
175
- autodiscover_paths=['services'],
176
- logger=logger
177
- )
175
+ async def main():
176
+ sa_events = SQLAlchemyEvents(
177
+ engine=engine,
178
+ autodiscover_paths=['services'],
179
+ logger=logger
180
+ )
181
+ await sa_events()
182
+ while True:
183
+ await asyncio.sleep(9999)
184
+
185
+ if __name__ == '__main__':
186
+ asyncio.run(main())
178
187
  ```
179
188
 
180
189
  ## How it works
@@ -25,5 +25,8 @@ src/sqlalchemy_events_lib.egg-info/requires.txt
25
25
  src/sqlalchemy_events_lib.egg-info/top_level.txt
26
26
  src/tests/conftest.py
27
27
  src/tests/test_autodiscover.py
28
+ src/tests/test_handlers.py
28
29
  src/tests/test_is_created_triggers.py
29
- src/tests/test_models.py
30
+ src/tests/test_models.py
31
+ src/tests/test_sa_events.py
32
+ src/tests/test_with_events_deco.py
@@ -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
 
@@ -32,11 +33,10 @@ async def prepared_db(engine):
32
33
  await conn.execute(text('CREATE SCHEMA public'))
33
34
  await conn.run_sync(Base.metadata.create_all)
34
35
 
35
- SQLAlchemyEvents(
36
+ await SQLAlchemyEvents(
36
37
  engine=engine,
37
38
  autodiscover_paths=['tests']
38
- )
39
- await asyncio.sleep(0.1)
39
+ )()
40
40
  IS_DB = True
41
41
 
42
42
 
@@ -46,4 +46,10 @@ async def session(engine):
46
46
 
47
47
  async with async_session() as session:
48
48
  yield session
49
- 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
@@ -1,6 +1,8 @@
1
1
  import sys
2
2
  import types
3
3
 
4
+ import pytest
5
+
4
6
  from sqlalchemy_events.discovery import autodiscover
5
7
  from sqlalchemy_events import sa_insert_handler, sa_update_handler, sa_delete_handler
6
8
  from sqlalchemy_events.registry import get_event_handlers
@@ -115,15 +117,31 @@ async def test_discover_handlers():
115
117
  assert handlers
116
118
  result_handlers = []
117
119
  for key, handler_list in handlers.items():
118
- assert len(handler_list) == 1
119
120
  if 'insert' in key:
120
121
  assert handler_list[0].func.__name__ == 'insert_handler'
121
- result_handlers.extend(handler_list)
122
+ result_handlers.append(handler_list[0])
122
123
  if 'update' in key:
123
124
  assert handler_list[0].func.__name__ == 'update_handler'
124
- result_handlers.extend(handler_list)
125
+ result_handlers.append(handler_list[0])
125
126
  if 'delete' in key:
126
127
  assert handler_list[0].func.__name__ == 'delete_handler'
127
- result_handlers.extend(handler_list)
128
+ result_handlers.append(handler_list[0])
129
+
130
+ assert len(result_handlers) == 3
131
+
132
+
133
+ def test_invalid_autodiscover_path():
134
+ with pytest.raises(ModuleNotFoundError):
135
+ autodiscover(['not.existing.module'])
136
+
137
+
138
+ def test_autodiscover_no_duplicates():
139
+ modules = autodiscover(['tests', 'tests.test_autodiscover.py'])
140
+
141
+ names = [m.__name__ for m in modules]
142
+ assert len(names) == len(set(names))
143
+
128
144
 
129
- assert len(result_handlers) == len(handlers)
145
+ def test_autodiscover_import_error_is_handled():
146
+ with pytest.raises(Exception):
147
+ autodiscover(['app.broken_module'])
@@ -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
@@ -0,0 +1,27 @@
1
+ import logging
2
+
3
+ import pytest
4
+
5
+ from sqlalchemy_events import SQLAlchemyEvents
6
+
7
+
8
+ async def test_invalid_engine_type():
9
+ with pytest.raises(RuntimeError):
10
+ await SQLAlchemyEvents(engine='not_engine', autodiscover_paths=[])()
11
+
12
+
13
+ async def test_empty_autodiscover_paths_logs_warning(caplog, engine):
14
+ caplog.set_level('WARNING', logger='SQLAlchemyEvents')
15
+ await SQLAlchemyEvents(engine=engine, autodiscover_paths=[], verbose=True)()
16
+ assert 'No autodiscover paths' in caplog.text
17
+
18
+
19
+ async def test_no_logs_when_verbose_disabled(caplog, engine):
20
+ await SQLAlchemyEvents(engine=engine, autodiscover_paths=[], verbose=False)()
21
+ assert caplog.text == ''
22
+
23
+
24
+ async def test_custom_logger_used(caplog, engine):
25
+ logger = logging.getLogger('test')
26
+ await SQLAlchemyEvents(engine=engine, autodiscover_paths=[], logger=logger)()
27
+ assert 'SQLAlchemyEvents' in caplog.text
@@ -0,0 +1,45 @@
1
+ import pytest
2
+ from sqlalchemy import Column, Integer, String
3
+
4
+ from sqlalchemy_events import SaEvent, with_events
5
+ from tests.test_models import Base, UserModel
6
+
7
+
8
+ def test_with_events_on_incorrect_class_type():
9
+ with pytest.raises(RuntimeError):
10
+ @with_events([SaEvent.INSERT])
11
+ class Test: ...
12
+
13
+
14
+ def test_incorrect_event_type():
15
+ with pytest.raises(RuntimeError):
16
+ @with_events(['incorrect_event_type'])
17
+ class UserModel(Base):
18
+ __tablename__ = 'test_model'
19
+
20
+ id = Column(Integer, primary_key=True)
21
+ name = Column(String)
22
+
23
+
24
+ def test_empty_events():
25
+ with pytest.raises(TypeError):
26
+ @with_events()
27
+ class UserModel(Base):
28
+ __tablename__ = 'test_model'
29
+
30
+ id = Column(Integer, primary_key=True)
31
+ name = Column(String)
32
+
33
+ with pytest.raises(RuntimeError):
34
+ @with_events([])
35
+ class UserModel(Base):
36
+ __tablename__ = 'test_model'
37
+
38
+ id = Column(Integer, primary_key=True)
39
+ name = Column(String)
40
+
41
+
42
+ def test_events_in_model():
43
+ assert UserModel.events.INSERT
44
+ assert UserModel.events.UPDATE
45
+ assert UserModel.events.DELETE
@@ -1,30 +0,0 @@
1
- import importlib
2
- import pkgutil
3
- from pathlib import Path
4
- from types import ModuleType
5
- from typing import Iterable, List
6
-
7
-
8
- def autodiscover(paths: Iterable[str]) -> List[ModuleType]:
9
- modules: List[ModuleType] = []
10
-
11
- for path in paths:
12
- if isinstance(path, Path):
13
- path = str(path)
14
- path = path.replace('\\', '.')
15
-
16
- path = path.strip('/').replace('/', '.').lstrip('.')
17
- module = importlib.import_module(path)
18
- modules.append(module)
19
-
20
- if not hasattr(module, '__path__'):
21
- continue
22
-
23
- for _, module_name, _ in pkgutil.walk_packages(
24
- module.__path__,
25
- module.__name__ + '.',
26
- ):
27
- submodule = importlib.import_module(module_name)
28
- modules.append(submodule)
29
-
30
- return modules
@@ -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