sqlalchemy-events-lib 0.4.0__tar.gz → 0.4.1__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.4.0 → sqlalchemy_events_lib-0.4.1}/PKG-INFO +1 -1
- {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/pyproject.toml +1 -1
- {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/__init__.py +1 -1
- {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/core.py +122 -52
- {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events_lib.egg-info/PKG-INFO +1 -1
- {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/LICENSE +0 -0
- {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/README.md +0 -0
- {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/setup.cfg +0 -0
- {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/callbacks_strategies/__init__.py +0 -0
- {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/callbacks_strategies/base.py +0 -0
- {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/callbacks_strategies/postgres_callback.py +0 -0
- {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/decorators.py +0 -0
- {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/default_logger.py +0 -0
- {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/discovery.py +0 -0
- {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/events.py +0 -0
- {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/exceptions.py +0 -0
- {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/handlers.py +0 -0
- {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/init_triggers_strategies/__init__.py +0 -0
- {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/init_triggers_strategies/base.py +0 -0
- {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/init_triggers_strategies/postgres_init_triggers.py +0 -0
- {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/py.typed +0 -0
- {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/registry.py +0 -0
- {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/types.py +0 -0
- {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/utils.py +0 -0
- {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events_lib.egg-info/SOURCES.txt +0 -0
- {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events_lib.egg-info/dependency_links.txt +0 -0
- {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events_lib.egg-info/requires.txt +0 -0
- {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events_lib.egg-info/top_level.txt +0 -0
- {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/tests/conftest.py +0 -0
- {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/tests/test_autodiscover.py +0 -0
- {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/tests/test_handlers.py +0 -0
- {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/tests/test_is_created_triggers.py +0 -0
- {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/tests/test_models.py +0 -0
- {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/tests/test_sa_events.py +0 -0
- {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/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.4.
|
|
3
|
+
Version: 0.4.1
|
|
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.4.
|
|
13
|
+
version = "0.4.1"
|
|
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 = [
|
|
@@ -6,9 +6,13 @@ from collections import defaultdict
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Optional, Union
|
|
8
8
|
|
|
9
|
-
from sqlalchemy.orm import DeclarativeBase
|
|
10
9
|
from sqlalchemy import Engine
|
|
11
|
-
from sqlalchemy.ext.asyncio import
|
|
10
|
+
from sqlalchemy.ext.asyncio import (
|
|
11
|
+
AsyncEngine,
|
|
12
|
+
create_async_engine,
|
|
13
|
+
)
|
|
14
|
+
from sqlalchemy.pool import NullPool
|
|
15
|
+
from sqlalchemy.orm import DeclarativeBase
|
|
12
16
|
|
|
13
17
|
from .default_logger import DefaultLogger
|
|
14
18
|
from .discovery import autodiscover
|
|
@@ -22,8 +26,8 @@ class SQLAlchemyEvents:
|
|
|
22
26
|
|
|
23
27
|
CHANNEL = 'sqlalchemy_events'
|
|
24
28
|
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
RETRY_DELAY = 2
|
|
30
|
+
LOG_INTERVAL = 30
|
|
27
31
|
|
|
28
32
|
def __init__(
|
|
29
33
|
self,
|
|
@@ -41,6 +45,7 @@ class SQLAlchemyEvents:
|
|
|
41
45
|
|
|
42
46
|
self._stop_event = asyncio.Event()
|
|
43
47
|
self._listener_task: asyncio.Task | None = None
|
|
48
|
+
self._listener_engine: AsyncEngine | None = None
|
|
44
49
|
|
|
45
50
|
async def __call__(self) -> None:
|
|
46
51
|
if not isinstance(self.engine, (AsyncEngine, Engine)):
|
|
@@ -59,6 +64,12 @@ class SQLAlchemyEvents:
|
|
|
59
64
|
)
|
|
60
65
|
return
|
|
61
66
|
|
|
67
|
+
if not isinstance(self.engine, AsyncEngine):
|
|
68
|
+
raise RuntimeError(
|
|
69
|
+
'[SQLAlchemyEvents] Sync Engine driver does not support '
|
|
70
|
+
'async LISTEN/NOTIFY. Use AsyncEngine with asyncpg'
|
|
71
|
+
)
|
|
72
|
+
|
|
62
73
|
handlers = await self.__find_handlers()
|
|
63
74
|
|
|
64
75
|
if not handlers:
|
|
@@ -75,24 +86,35 @@ class SQLAlchemyEvents:
|
|
|
75
86
|
f'{", ".join(sa_events_strategy.keys())}'
|
|
76
87
|
)
|
|
77
88
|
|
|
78
|
-
|
|
89
|
+
self.__create_listener_engine()
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
await self.__start_listen(
|
|
93
|
+
event_strategy,
|
|
94
|
+
handlers,
|
|
95
|
+
)
|
|
96
|
+
except Exception:
|
|
97
|
+
await self.__dispose_listener_engine()
|
|
98
|
+
raise
|
|
79
99
|
|
|
80
100
|
async def stop(self) -> None:
|
|
81
101
|
self._stop_event.set()
|
|
82
102
|
|
|
83
|
-
|
|
84
|
-
return
|
|
103
|
+
listener_task = self._listener_task
|
|
85
104
|
|
|
86
|
-
|
|
105
|
+
if listener_task is not None:
|
|
106
|
+
listener_task.cancel()
|
|
87
107
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
108
|
+
try:
|
|
109
|
+
await listener_task
|
|
110
|
+
except asyncio.CancelledError:
|
|
111
|
+
pass
|
|
112
|
+
finally:
|
|
113
|
+
self._listener_task = None
|
|
94
114
|
|
|
95
|
-
|
|
115
|
+
await self.__dispose_listener_engine()
|
|
116
|
+
|
|
117
|
+
async def __find_handlers(self) -> list[Handler] | None:
|
|
96
118
|
autodiscover(self.autodiscover_paths)
|
|
97
119
|
|
|
98
120
|
handlers = get_event_handlers()
|
|
@@ -102,16 +124,16 @@ class SQLAlchemyEvents:
|
|
|
102
124
|
self.logger.info(
|
|
103
125
|
'[SQLAlchemyEvents] No handlers found'
|
|
104
126
|
)
|
|
105
|
-
return
|
|
127
|
+
return None
|
|
106
128
|
|
|
107
|
-
res_handlers = []
|
|
129
|
+
res_handlers: list[Handler] = []
|
|
108
130
|
|
|
109
131
|
for handlers_list in handlers.values():
|
|
110
132
|
res_handlers.extend(handlers_list)
|
|
111
133
|
|
|
112
|
-
filtered_handlers = []
|
|
113
|
-
handler_paths = set()
|
|
114
|
-
handlers_qty = defaultdict(int)
|
|
134
|
+
filtered_handlers: list[Handler] = []
|
|
135
|
+
handler_paths: set[str] = set()
|
|
136
|
+
handlers_qty: defaultdict[str, int] = defaultdict(int)
|
|
115
137
|
|
|
116
138
|
for handler in res_handlers:
|
|
117
139
|
file_path = (
|
|
@@ -146,6 +168,25 @@ class SQLAlchemyEvents:
|
|
|
146
168
|
|
|
147
169
|
return filtered_handlers
|
|
148
170
|
|
|
171
|
+
def __create_listener_engine(self) -> None:
|
|
172
|
+
if self._listener_engine is not None:
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
self._listener_engine = create_async_engine(
|
|
176
|
+
self.engine.url,
|
|
177
|
+
poolclass=NullPool,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
async def __dispose_listener_engine(self) -> None:
|
|
181
|
+
listener_engine = self._listener_engine
|
|
182
|
+
|
|
183
|
+
if listener_engine is None:
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
self._listener_engine = None
|
|
187
|
+
|
|
188
|
+
await listener_engine.dispose()
|
|
189
|
+
|
|
149
190
|
async def __start_listen(
|
|
150
191
|
self,
|
|
151
192
|
event_strategy: SaEventStrategy,
|
|
@@ -153,10 +194,11 @@ class SQLAlchemyEvents:
|
|
|
153
194
|
) -> None:
|
|
154
195
|
base = self.__get_base(handlers)
|
|
155
196
|
|
|
156
|
-
|
|
197
|
+
listener_engine = self._listener_engine
|
|
198
|
+
|
|
199
|
+
if listener_engine is None:
|
|
157
200
|
raise RuntimeError(
|
|
158
|
-
'[SQLAlchemyEvents]
|
|
159
|
-
'async LISTEN/NOTIFY. Use AsyncEngine with asyncpg'
|
|
201
|
+
'[SQLAlchemyEvents] Listener engine is not initialized'
|
|
160
202
|
)
|
|
161
203
|
|
|
162
204
|
async with self.engine.connect() as conn:
|
|
@@ -169,6 +211,9 @@ class SQLAlchemyEvents:
|
|
|
169
211
|
|
|
170
212
|
self._stop_event.clear()
|
|
171
213
|
|
|
214
|
+
if self._listener_task is not None:
|
|
215
|
+
return
|
|
216
|
+
|
|
172
217
|
self._listener_task = asyncio.create_task(
|
|
173
218
|
self.__listen_loop(event_strategy),
|
|
174
219
|
name='sqlalchemy-events-listener',
|
|
@@ -180,27 +225,28 @@ class SQLAlchemyEvents:
|
|
|
180
225
|
):
|
|
181
226
|
try:
|
|
182
227
|
model = handlers[0].args['model']
|
|
228
|
+
|
|
183
229
|
for cls in model.__mro__:
|
|
184
230
|
if (
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
231
|
+
isinstance(cls, type)
|
|
232
|
+
and issubclass(cls, DeclarativeBase)
|
|
233
|
+
and DeclarativeBase in cls.__bases__
|
|
188
234
|
):
|
|
189
235
|
return cls
|
|
190
236
|
|
|
191
237
|
except Exception:
|
|
192
|
-
raise RuntimeError(
|
|
238
|
+
raise RuntimeError(
|
|
239
|
+
'[SQLAlchemyEvents] No Base found in Registered handlers'
|
|
240
|
+
)
|
|
193
241
|
|
|
194
242
|
raise RuntimeError(
|
|
195
243
|
'[SQLAlchemyEvents] No Base found in Registered handlers'
|
|
196
244
|
)
|
|
197
245
|
|
|
198
246
|
async def __listen_loop(
|
|
199
|
-
|
|
200
|
-
|
|
247
|
+
self,
|
|
248
|
+
event_strategy: SaEventStrategy,
|
|
201
249
|
) -> None:
|
|
202
|
-
retry_delay = 2
|
|
203
|
-
log_interval = 30
|
|
204
250
|
last_log_time = 0.0
|
|
205
251
|
was_connected = False
|
|
206
252
|
|
|
@@ -208,6 +254,8 @@ class SQLAlchemyEvents:
|
|
|
208
254
|
try:
|
|
209
255
|
await self.__listen_once(event_strategy)
|
|
210
256
|
|
|
257
|
+
was_connected = True
|
|
258
|
+
|
|
211
259
|
except asyncio.CancelledError:
|
|
212
260
|
raise
|
|
213
261
|
|
|
@@ -217,48 +265,62 @@ class SQLAlchemyEvents:
|
|
|
217
265
|
|
|
218
266
|
now = asyncio.get_running_loop().time()
|
|
219
267
|
|
|
220
|
-
if
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
self.verbose
|
|
226
|
-
and now - last_log_time >= log_interval
|
|
227
|
-
):
|
|
228
|
-
self.logger.warning(
|
|
229
|
-
f'[SQLAlchemyEvents] '
|
|
230
|
-
f'Unable to connect publisher: {ex}'
|
|
268
|
+
if (
|
|
269
|
+
self.verbose
|
|
270
|
+
and (
|
|
271
|
+
last_log_time == 0.0
|
|
272
|
+
or now - last_log_time >= self.LOG_INTERVAL
|
|
231
273
|
)
|
|
274
|
+
):
|
|
275
|
+
if was_connected:
|
|
276
|
+
self.logger.warning(
|
|
277
|
+
'[SQLAlchemyEvents] '
|
|
278
|
+
f'Listener connection lost: {ex}'
|
|
279
|
+
)
|
|
280
|
+
else:
|
|
281
|
+
self.logger.warning(
|
|
282
|
+
'[SQLAlchemyEvents] '
|
|
283
|
+
f'Unable to connect listener: {ex}'
|
|
284
|
+
)
|
|
285
|
+
|
|
232
286
|
self.logger.warning(
|
|
233
287
|
'[SQLAlchemyEvents] '
|
|
234
|
-
'Trying to connect
|
|
288
|
+
'Trying to connect listener every '
|
|
289
|
+
f'{self.RETRY_DELAY} seconds...'
|
|
235
290
|
)
|
|
236
291
|
|
|
237
292
|
last_log_time = now
|
|
238
293
|
|
|
294
|
+
was_connected = False
|
|
295
|
+
|
|
239
296
|
if self._stop_event.is_set():
|
|
240
297
|
break
|
|
241
298
|
|
|
242
|
-
await self.engine.dispose()
|
|
243
|
-
|
|
244
299
|
try:
|
|
245
300
|
await asyncio.wait_for(
|
|
246
301
|
self._stop_event.wait(),
|
|
247
|
-
timeout=
|
|
302
|
+
timeout=self.RETRY_DELAY,
|
|
248
303
|
)
|
|
249
304
|
except asyncio.TimeoutError:
|
|
250
305
|
pass
|
|
251
306
|
|
|
252
307
|
if self.verbose:
|
|
253
308
|
self.logger.info(
|
|
254
|
-
'[SQLAlchemyEvents]
|
|
309
|
+
'[SQLAlchemyEvents] Listener stopped'
|
|
255
310
|
)
|
|
256
311
|
|
|
257
312
|
async def __listen_once(
|
|
258
313
|
self,
|
|
259
314
|
event_strategy: SaEventStrategy,
|
|
260
315
|
) -> None:
|
|
261
|
-
|
|
316
|
+
listener_engine = self._listener_engine
|
|
317
|
+
|
|
318
|
+
if listener_engine is None:
|
|
319
|
+
raise RuntimeError(
|
|
320
|
+
'[SQLAlchemyEvents] Listener engine is not initialized'
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
async with listener_engine.connect() as conn:
|
|
262
324
|
raw_conn = await conn.get_raw_connection()
|
|
263
325
|
driver_conn = raw_conn.driver_connection
|
|
264
326
|
|
|
@@ -276,21 +338,28 @@ class SQLAlchemyEvents:
|
|
|
276
338
|
driver_conn.add_termination_listener(
|
|
277
339
|
on_disconnect
|
|
278
340
|
)
|
|
341
|
+
|
|
279
342
|
listener_registered = False
|
|
343
|
+
|
|
280
344
|
try:
|
|
281
345
|
await driver_conn.add_listener(
|
|
282
346
|
self.CHANNEL,
|
|
283
347
|
event_strategy.callback.handle,
|
|
284
348
|
)
|
|
349
|
+
|
|
285
350
|
listener_registered = True
|
|
351
|
+
|
|
286
352
|
if self.verbose:
|
|
287
353
|
self.logger.info(
|
|
288
|
-
'[SQLAlchemyEvents]
|
|
354
|
+
'[SQLAlchemyEvents] Listener connected'
|
|
289
355
|
)
|
|
290
356
|
|
|
291
357
|
await disconnected.wait()
|
|
358
|
+
|
|
292
359
|
if not self._stop_event.is_set():
|
|
293
|
-
raise ConnectionError(
|
|
360
|
+
raise ConnectionError(
|
|
361
|
+
'Database connection lost'
|
|
362
|
+
)
|
|
294
363
|
|
|
295
364
|
finally:
|
|
296
365
|
driver_conn.remove_termination_listener(
|
|
@@ -326,8 +395,9 @@ class SQLAlchemyEvents:
|
|
|
326
395
|
if inspect.isawaitable(result):
|
|
327
396
|
await result
|
|
328
397
|
|
|
329
|
-
except Exception as
|
|
398
|
+
except Exception as ex:
|
|
330
399
|
if self.verbose:
|
|
331
400
|
self.logger.warning(
|
|
332
|
-
|
|
401
|
+
'[SQLAlchemyEvents] '
|
|
402
|
+
f'Failed to remove listener: {ex}',
|
|
333
403
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlalchemy-events-lib
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.1
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/decorators.py
RENAMED
|
File without changes
|
{sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/default_logger.py
RENAMED
|
File without changes
|
{sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/discovery.py
RENAMED
|
File without changes
|
|
File without changes
|
{sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/exceptions.py
RENAMED
|
File without changes
|
{sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/handlers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/registry.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
|
|
File without changes
|
|
File without changes
|
{sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/tests/test_is_created_triggers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/tests/test_with_events_deco.py
RENAMED
|
File without changes
|