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.
Files changed (35) hide show
  1. {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/PKG-INFO +1 -1
  2. {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/pyproject.toml +1 -1
  3. {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/__init__.py +1 -1
  4. {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/core.py +122 -52
  5. {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events_lib.egg-info/PKG-INFO +1 -1
  6. {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/LICENSE +0 -0
  7. {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/README.md +0 -0
  8. {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/setup.cfg +0 -0
  9. {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/callbacks_strategies/__init__.py +0 -0
  10. {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/callbacks_strategies/base.py +0 -0
  11. {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/callbacks_strategies/postgres_callback.py +0 -0
  12. {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/decorators.py +0 -0
  13. {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/default_logger.py +0 -0
  14. {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/discovery.py +0 -0
  15. {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/events.py +0 -0
  16. {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/exceptions.py +0 -0
  17. {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/handlers.py +0 -0
  18. {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/init_triggers_strategies/__init__.py +0 -0
  19. {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/init_triggers_strategies/base.py +0 -0
  20. {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/init_triggers_strategies/postgres_init_triggers.py +0 -0
  21. {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/py.typed +0 -0
  22. {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/registry.py +0 -0
  23. {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/types.py +0 -0
  24. {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events/utils.py +0 -0
  25. {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events_lib.egg-info/SOURCES.txt +0 -0
  26. {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events_lib.egg-info/dependency_links.txt +0 -0
  27. {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events_lib.egg-info/requires.txt +0 -0
  28. {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/sqlalchemy_events_lib.egg-info/top_level.txt +0 -0
  29. {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/tests/conftest.py +0 -0
  30. {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/tests/test_autodiscover.py +0 -0
  31. {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/tests/test_handlers.py +0 -0
  32. {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/tests/test_is_created_triggers.py +0 -0
  33. {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/tests/test_models.py +0 -0
  34. {sqlalchemy_events_lib-0.4.0 → sqlalchemy_events_lib-0.4.1}/src/tests/test_sa_events.py +0 -0
  35. {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.0
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.0"
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 = [
@@ -11,4 +11,4 @@ __all__ = [
11
11
  'with_events',
12
12
  'SaEvent'
13
13
  ]
14
- __version__ = '0.4.0'
14
+ __version__ = '0.4.1'
@@ -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 AsyncEngine
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
- INITIAL_RETRY_DELAY = 1
26
- MAX_RETRY_DELAY = 30
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
- await self.__start_listen(event_strategy, handlers)
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
- if self._listener_task is None:
84
- return
103
+ listener_task = self._listener_task
85
104
 
86
- self._listener_task.cancel()
105
+ if listener_task is not None:
106
+ listener_task.cancel()
87
107
 
88
- try:
89
- await self._listener_task
90
- except asyncio.CancelledError:
91
- pass
92
- finally:
93
- self._listener_task = None
108
+ try:
109
+ await listener_task
110
+ except asyncio.CancelledError:
111
+ pass
112
+ finally:
113
+ self._listener_task = None
94
114
 
95
- async def __find_handlers(self):
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
- if not isinstance(self.engine, AsyncEngine):
197
+ listener_engine = self._listener_engine
198
+
199
+ if listener_engine is None:
157
200
  raise RuntimeError(
158
- '[SQLAlchemyEvents] Sync Engine driver does not support '
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
- isinstance(cls, type)
186
- and issubclass(cls, DeclarativeBase)
187
- and DeclarativeBase in cls.__bases__
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('[SQLAlchemyEvents] No Base found in Registered handlers')
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
- self,
200
- event_strategy: SaEventStrategy,
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 was_connected:
221
- was_connected = False
222
- last_log_time = now
223
-
224
- elif (
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 publisher every 2 seconds...'
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=retry_delay,
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] Publisher stopped'
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
- async with self.engine.connect() as conn:
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] Publisher connected'
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('Database connection lost')
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 e:
398
+ except Exception as ex:
330
399
  if self.verbose:
331
400
  self.logger.warning(
332
- f'[SQLAlchemyEvents] Failed to remove publisher: {e}',
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.0
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