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.
- {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/PKG-INFO +18 -9
- {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/README.md +17 -8
- {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/pyproject.toml +1 -1
- {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/__init__.py +1 -1
- {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/callbacks_strategies/postgres_callback.py +1 -1
- {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/core.py +1 -2
- {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/decorators.py +7 -0
- {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/default_logger.py +1 -1
- sqlalchemy_events_lib-0.3.2/src/sqlalchemy_events/discovery.py +44 -0
- {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/handlers.py +11 -14
- {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/registry.py +1 -1
- sqlalchemy_events_lib-0.3.2/src/sqlalchemy_events/types.py +40 -0
- {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events_lib.egg-info/PKG-INFO +18 -9
- {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events_lib.egg-info/SOURCES.txt +4 -1
- {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/tests/conftest.py +11 -5
- {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/tests/test_autodiscover.py +23 -5
- sqlalchemy_events_lib-0.3.2/src/tests/test_handlers.py +79 -0
- sqlalchemy_events_lib-0.3.2/src/tests/test_sa_events.py +27 -0
- sqlalchemy_events_lib-0.3.2/src/tests/test_with_events_deco.py +45 -0
- sqlalchemy_events_lib-0.3.0/src/sqlalchemy_events/discovery.py +0 -30
- sqlalchemy_events_lib-0.3.0/src/sqlalchemy_events/types.py +0 -26
- {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/LICENSE +0 -0
- {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/setup.cfg +0 -0
- {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/callbacks_strategies/__init__.py +0 -0
- {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/callbacks_strategies/base.py +0 -0
- {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/events.py +0 -0
- {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/exceptions.py +0 -0
- {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/init_triggers_strategies/__init__.py +0 -0
- {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/init_triggers_strategies/base.py +0 -0
- {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/init_triggers_strategies/postgres_init_triggers.py +0 -0
- {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/utils.py +0 -0
- {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events_lib.egg-info/dependency_links.txt +0 -0
- {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events_lib.egg-info/requires.txt +0 -0
- {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events_lib.egg-info/top_level.txt +0 -0
- {sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/tests/test_is_created_triggers.py +0 -0
- {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.
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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.
|
|
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 = [
|
|
@@ -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
|
|
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 '
|
{sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/decorators.py
RENAMED
|
@@ -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:
|
|
@@ -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
|
{sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/handlers.py
RENAMED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
|
@@ -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.
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/sqlalchemy_events/exceptions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sqlalchemy_events_lib-0.3.0 → sqlalchemy_events_lib-0.3.2}/src/tests/test_is_created_triggers.py
RENAMED
|
File without changes
|
|
File without changes
|