sqlalchemy-events-lib 0.2.0__tar.gz → 0.3.0__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 (31) hide show
  1. {sqlalchemy_events_lib-0.2.0 → sqlalchemy_events_lib-0.3.0}/PKG-INFO +6 -7
  2. {sqlalchemy_events_lib-0.2.0 → sqlalchemy_events_lib-0.3.0}/README.md +5 -6
  3. {sqlalchemy_events_lib-0.2.0 → sqlalchemy_events_lib-0.3.0}/pyproject.toml +1 -1
  4. {sqlalchemy_events_lib-0.2.0 → sqlalchemy_events_lib-0.3.0}/src/sqlalchemy_events/__init__.py +1 -1
  5. {sqlalchemy_events_lib-0.2.0 → sqlalchemy_events_lib-0.3.0}/src/sqlalchemy_events/core.py +53 -18
  6. {sqlalchemy_events_lib-0.2.0 → sqlalchemy_events_lib-0.3.0}/src/sqlalchemy_events/decorators.py +4 -0
  7. sqlalchemy_events_lib-0.3.0/src/sqlalchemy_events/default_logger.py +25 -0
  8. {sqlalchemy_events_lib-0.2.0 → sqlalchemy_events_lib-0.3.0}/src/sqlalchemy_events/discovery.py +6 -0
  9. {sqlalchemy_events_lib-0.2.0 → sqlalchemy_events_lib-0.3.0}/src/sqlalchemy_events/handlers.py +3 -0
  10. {sqlalchemy_events_lib-0.2.0 → sqlalchemy_events_lib-0.3.0}/src/sqlalchemy_events_lib.egg-info/PKG-INFO +6 -7
  11. {sqlalchemy_events_lib-0.2.0 → sqlalchemy_events_lib-0.3.0}/src/sqlalchemy_events_lib.egg-info/SOURCES.txt +1 -0
  12. {sqlalchemy_events_lib-0.2.0 → sqlalchemy_events_lib-0.3.0}/src/tests/conftest.py +0 -1
  13. {sqlalchemy_events_lib-0.2.0 → sqlalchemy_events_lib-0.3.0}/LICENSE +0 -0
  14. {sqlalchemy_events_lib-0.2.0 → sqlalchemy_events_lib-0.3.0}/setup.cfg +0 -0
  15. {sqlalchemy_events_lib-0.2.0 → sqlalchemy_events_lib-0.3.0}/src/sqlalchemy_events/callbacks_strategies/__init__.py +0 -0
  16. {sqlalchemy_events_lib-0.2.0 → sqlalchemy_events_lib-0.3.0}/src/sqlalchemy_events/callbacks_strategies/base.py +0 -0
  17. {sqlalchemy_events_lib-0.2.0 → sqlalchemy_events_lib-0.3.0}/src/sqlalchemy_events/callbacks_strategies/postgres_callback.py +0 -0
  18. {sqlalchemy_events_lib-0.2.0 → sqlalchemy_events_lib-0.3.0}/src/sqlalchemy_events/events.py +0 -0
  19. {sqlalchemy_events_lib-0.2.0 → sqlalchemy_events_lib-0.3.0}/src/sqlalchemy_events/exceptions.py +0 -0
  20. {sqlalchemy_events_lib-0.2.0 → sqlalchemy_events_lib-0.3.0}/src/sqlalchemy_events/init_triggers_strategies/__init__.py +0 -0
  21. {sqlalchemy_events_lib-0.2.0 → sqlalchemy_events_lib-0.3.0}/src/sqlalchemy_events/init_triggers_strategies/base.py +0 -0
  22. {sqlalchemy_events_lib-0.2.0 → sqlalchemy_events_lib-0.3.0}/src/sqlalchemy_events/init_triggers_strategies/postgres_init_triggers.py +0 -0
  23. {sqlalchemy_events_lib-0.2.0 → sqlalchemy_events_lib-0.3.0}/src/sqlalchemy_events/registry.py +0 -0
  24. {sqlalchemy_events_lib-0.2.0 → sqlalchemy_events_lib-0.3.0}/src/sqlalchemy_events/types.py +0 -0
  25. {sqlalchemy_events_lib-0.2.0 → sqlalchemy_events_lib-0.3.0}/src/sqlalchemy_events/utils.py +0 -0
  26. {sqlalchemy_events_lib-0.2.0 → sqlalchemy_events_lib-0.3.0}/src/sqlalchemy_events_lib.egg-info/dependency_links.txt +0 -0
  27. {sqlalchemy_events_lib-0.2.0 → sqlalchemy_events_lib-0.3.0}/src/sqlalchemy_events_lib.egg-info/requires.txt +0 -0
  28. {sqlalchemy_events_lib-0.2.0 → sqlalchemy_events_lib-0.3.0}/src/sqlalchemy_events_lib.egg-info/top_level.txt +0 -0
  29. {sqlalchemy_events_lib-0.2.0 → sqlalchemy_events_lib-0.3.0}/src/tests/test_autodiscover.py +0 -0
  30. {sqlalchemy_events_lib-0.2.0 → sqlalchemy_events_lib-0.3.0}/src/tests/test_is_created_triggers.py +0 -0
  31. {sqlalchemy_events_lib-0.2.0 → sqlalchemy_events_lib-0.3.0}/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.2.0
3
+ Version: 0.3.0
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
@@ -110,13 +110,11 @@ main.py
110
110
  ```python
111
111
  import asyncio
112
112
  from sqlalchemy_events import SQLAlchemyEvents
113
- from models import Base
114
113
  from session import engine
115
114
 
116
115
 
117
116
  async def main():
118
117
  SQLAlchemyEvents(
119
- base=Base,
120
118
  engine=engine,
121
119
  autodiscover_paths=['services']
122
120
  )
@@ -133,22 +131,23 @@ if __name__ == '__main__':
133
131
  The SQLAlchemyEvents class accepts the following parameters:
134
132
  ```python
135
133
  SQLAlchemyEvents(
136
- base,
137
134
  engine,
138
135
  autodiscover_paths,
139
- logger=None
136
+ logger=None,
137
+ verbose=True
140
138
  )
141
139
  ```
142
140
 
143
141
  Parameters:
144
- * **base** - SQLAlchemy declarative base class used to discover mapped models.
145
142
  * **engine** - SQLAlchemy Engine or AsyncEngine instance.
146
143
  * **autodiscover_paths** - List of Python module paths where event handlers are defined.
147
144
  These modules are automatically imported so that decorators such as
148
- `@sa_insert_handler`, `@sa_update_handler`, `@sa_delete_handler` are executed.
145
+ `@sa_insert_handler`, `@sa_update_handler`, `@sa_delete_handler` are executed.
149
146
  **Example:**
150
147
  `autodiscover_paths=["services", "app.handlers"]`
151
148
 
149
+ * **verbose** - Enables detailed logging output. When set to True, the library will log additional informational and warning messages to help with debugging and configuration.
150
+
152
151
  ### Important:
153
152
 
154
153
  All modules containing event handlers must be imported through autodiscover
@@ -88,13 +88,11 @@ main.py
88
88
  ```python
89
89
  import asyncio
90
90
  from sqlalchemy_events import SQLAlchemyEvents
91
- from models import Base
92
91
  from session import engine
93
92
 
94
93
 
95
94
  async def main():
96
95
  SQLAlchemyEvents(
97
- base=Base,
98
96
  engine=engine,
99
97
  autodiscover_paths=['services']
100
98
  )
@@ -111,22 +109,23 @@ if __name__ == '__main__':
111
109
  The SQLAlchemyEvents class accepts the following parameters:
112
110
  ```python
113
111
  SQLAlchemyEvents(
114
- base,
115
112
  engine,
116
113
  autodiscover_paths,
117
- logger=None
114
+ logger=None,
115
+ verbose=True
118
116
  )
119
117
  ```
120
118
 
121
119
  Parameters:
122
- * **base** - SQLAlchemy declarative base class used to discover mapped models.
123
120
  * **engine** - SQLAlchemy Engine or AsyncEngine instance.
124
121
  * **autodiscover_paths** - List of Python module paths where event handlers are defined.
125
122
  These modules are automatically imported so that decorators such as
126
- `@sa_insert_handler`, `@sa_update_handler`, `@sa_delete_handler` are executed.
123
+ `@sa_insert_handler`, `@sa_update_handler`, `@sa_delete_handler` are executed.
127
124
  **Example:**
128
125
  `autodiscover_paths=["services", "app.handlers"]`
129
126
 
127
+ * **verbose** - Enables detailed logging output. When set to True, the library will log additional informational and warning messages to help with debugging and configuration.
128
+
130
129
  ### Important:
131
130
 
132
131
  All modules containing event handlers must be imported through autodiscover
@@ -10,7 +10,7 @@ where = ["src"]
10
10
 
11
11
  [project]
12
12
  name = "sqlalchemy-events-lib"
13
- version = "0.2.0"
13
+ version = "0.3.0"
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.2.0'
14
+ __version__ = '0.3.0'
@@ -1,16 +1,19 @@
1
1
  import asyncio
2
2
  import logging
3
+ from collections import defaultdict
3
4
  from pathlib import Path
4
- from typing import Optional, Type, Union
5
+ from typing import Optional, Union
5
6
  import inspect
6
7
 
7
8
  from sqlalchemy import Engine
8
9
  from sqlalchemy.ext.asyncio import AsyncEngine
9
10
  from sqlalchemy.orm import DeclarativeBase
10
11
 
12
+ from .default_logger import DefaultLogger
11
13
  from .discovery import autodiscover
12
14
  from .events import SaEventStrategy, sa_events_strategy
13
15
  from .registry import get_event_handlers
16
+ from .types import Handler
14
17
  from .utils import dialect_resolver
15
18
 
16
19
 
@@ -18,19 +21,35 @@ class SQLAlchemyEvents:
18
21
 
19
22
  def __init__(
20
23
  self,
21
- base: Type[DeclarativeBase],
22
24
  engine: Union[AsyncEngine, Engine],
23
- autodiscover_paths: list[str],
24
- logger: Optional[logging.Logger] = None
25
+ autodiscover_paths: list[str | Path],
26
+ logger: Optional[logging.Logger] = None,
27
+ verbose: bool = True
25
28
  ) -> None:
26
- self.base = base
27
29
  self.engine = engine
28
30
  self.autodiscover_paths = autodiscover_paths
29
- self.logger = logger
31
+ self.logger = logger or DefaultLogger() if verbose else None
32
+ self.verbose = verbose
30
33
  asyncio.create_task(self.__init())
31
34
 
32
35
  async def __init(self) -> None:
33
- if not await self.__find_handlers():
36
+ if not isinstance(self.engine, (AsyncEngine, Engine)):
37
+ raise RuntimeError(
38
+ '[SQLAlchemyEvents] \'engine\' must be an instance of '
39
+ 'sqlalchemy.Engine or sqlalchemy.ext.asyncio.AsyncEngine.'
40
+ )
41
+
42
+ if not self.autodiscover_paths:
43
+ if self.verbose:
44
+ self.logger.warning(
45
+ '[SQLAlchemyEvents] No autodiscover paths specified. '
46
+ 'Please provide at least one module path (e.g., \'app.handlers\') '
47
+ 'in \'autodiscover_paths\' during initialization.'
48
+ )
49
+ return
50
+
51
+ handlers = await self.__find_handlers()
52
+ if not handlers:
34
53
  return
35
54
 
36
55
  dialect = dialect_resolver(self.engine)
@@ -38,13 +57,13 @@ class SQLAlchemyEvents:
38
57
  if not event_strategy:
39
58
  raise RuntimeError(f'[SQLAlchemyEvents] Unsupported database {dialect}. '
40
59
  f'This library supports only {', '.join(sa_events_strategy.keys())}')
41
- await self.__start_listen(event_strategy)
60
+ await self.__start_listen(event_strategy, handlers)
42
61
 
43
62
  async def __find_handlers(self):
44
63
  autodiscover(self.autodiscover_paths)
45
64
  handlers = get_event_handlers()
46
65
  if not handlers:
47
- if self.logger:
66
+ if self.verbose:
48
67
  self.logger.info('[SQLAlchemyEvents] No handlers found')
49
68
  return
50
69
  res_handlers = []
@@ -53,6 +72,7 @@ class SQLAlchemyEvents:
53
72
 
54
73
  filtered_handlers = []
55
74
  handler_paths = set()
75
+ handlers_qty = defaultdict(int)
56
76
  for handler in res_handlers:
57
77
  file_path = inspect.getsourcefile(handler.func) or inspect.getfile(handler.func)
58
78
 
@@ -64,13 +84,28 @@ class SQLAlchemyEvents:
64
84
 
65
85
  handler_paths.add(handler_path)
66
86
  filtered_handlers.append(handler)
67
- if self.logger:
68
- self.logger.info(f'[SQLAlchemyEvents] Registered handler {handler.func.__name__} '
69
- f'from {file_func.parent.name}/{file_name}')
87
+ handlers_qty[f'{file_func.parent.name}/{file_name}'] += 1
88
+
89
+ if self.verbose:
90
+ for path, qty in handlers_qty.items():
91
+ self.logger.info(f'[SQLAlchemyEvents] Registered {qty} {'handler' if qty == 1 else 'handlers'} '
92
+ f'from \'{path}\'')
70
93
 
71
94
  return filtered_handlers
72
95
 
73
- async def __start_listen(self, event_strategy: SaEventStrategy):
96
+ async def __start_listen(self, event_strategy: SaEventStrategy, handlers: list[Handler]):
97
+ base = None
98
+ try:
99
+ model = handlers[0].args['model']
100
+ for cls in model.__mro__:
101
+ if issubclass(cls, DeclarativeBase) and cls is not DeclarativeBase:
102
+ base = cls
103
+
104
+ if not base:
105
+ raise Exception
106
+ except Exception:
107
+ raise RuntimeError('[SQLAlchemyEvents] No Base found in Registered handlers')
108
+
74
109
  if isinstance(self.engine, AsyncEngine):
75
110
  async with self.engine.connect() as conn:
76
111
  raw_conn = await conn.get_raw_connection()
@@ -80,11 +115,11 @@ class SQLAlchemyEvents:
80
115
  raise RuntimeError('[SQLAlchemyEvents] Driver does not support LISTEN/NOTIFY')
81
116
 
82
117
  await event_strategy.init_triggers(
83
- model_list=self.base.__subclasses__(),
118
+ model_list=base.__subclasses__(),
84
119
  conn=conn,
85
120
  logger=self.logger
86
121
  )
87
- if self.logger:
122
+ if self.verbose:
88
123
  self.logger.info('[SQLAlchemyEvents] Start listening')
89
124
  await driver_conn.add_listener('sqlalchemy_events', event_strategy.callback.handle)
90
125
  return
@@ -103,11 +138,11 @@ class SQLAlchemyEvents:
103
138
  if not asyncio.iscoroutine(result):
104
139
  raise RuntimeError(sync_engine_error)
105
140
  await event_strategy.init_triggers(
106
- model_list=self.base.__subclasses__(),
141
+ model_list=base.__subclasses__(),
107
142
  conn=conn,
108
143
  logger=self.logger
109
144
  )
110
- if self.logger:
145
+ if self.verbose:
111
146
  self.logger.info('[SQLAlchemyEvents] Start listening')
112
147
  await result
113
148
 
@@ -120,6 +155,6 @@ class SQLAlchemyEvents:
120
155
  try:
121
156
  await driver_conn.wait_for_notify()
122
157
  except Exception as e:
123
- if self.logger:
158
+ if self.verbose:
124
159
  self.logger.error(f'[SQLAlchemyEvents] listener crashed: {e}')
125
160
  await asyncio.sleep(1)
@@ -2,6 +2,10 @@ from .types import SaEvent
2
2
 
3
3
 
4
4
  def with_events(events: list[SaEvent]):
5
+ for e in events:
6
+ if not isinstance(e, SaEvent):
7
+ raise RuntimeError(f'Event must be a SaEvent instance, not {type(e)}')
8
+
5
9
  def wrapper(cls):
6
10
  cls.__events__ = set(events)
7
11
 
@@ -0,0 +1,25 @@
1
+ import logging
2
+
3
+
4
+ class DefaultLogger:
5
+ def __init__(self) -> None:
6
+ self.__logger = logging.getLogger()
7
+ self.__logger.setLevel(logging.INFO)
8
+
9
+ if not self.__logger.handlers:
10
+ formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
11
+ console_handler = logging.StreamHandler()
12
+ console_handler.setFormatter(formatter)
13
+ self.__logger.addHandler(console_handler)
14
+
15
+ def info(self, message: str) -> None:
16
+ self.__logger.info(message)
17
+
18
+ def error(self, message: str, exc_info: bool = True) -> None:
19
+ self.__logger.error(message, exc_info=exc_info)
20
+
21
+ def warning(self, message: str) -> None:
22
+ self.__logger.warning(message)
23
+
24
+ def debug(self, message: str) -> None:
25
+ self.__logger.debug(message)
@@ -1,5 +1,6 @@
1
1
  import importlib
2
2
  import pkgutil
3
+ from pathlib import Path
3
4
  from types import ModuleType
4
5
  from typing import Iterable, List
5
6
 
@@ -8,6 +9,11 @@ def autodiscover(paths: Iterable[str]) -> List[ModuleType]:
8
9
  modules: List[ModuleType] = []
9
10
 
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('.')
11
17
  module = importlib.import_module(path)
12
18
  modules.append(module)
13
19
 
@@ -10,6 +10,9 @@ from .types import Handler
10
10
 
11
11
 
12
12
  def __inner(func: Callable, sa_event: SaEvent, model: Type[DeclarativeBase]):
13
+ if not isinstance(model, type) or not issubclass(model, DeclarativeBase):
14
+ raise RuntimeError('Model must inherit from DeclarativeBase')
15
+
13
16
  event_handlers = get_event_handlers()
14
17
  trig_name = f'sa_{model.__tablename__}_{sa_event.lower()}_notify'
15
18
  func_path = inspect.getsourcefile(func) or inspect.getfile(func)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlalchemy-events-lib
3
- Version: 0.2.0
3
+ Version: 0.3.0
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
@@ -110,13 +110,11 @@ main.py
110
110
  ```python
111
111
  import asyncio
112
112
  from sqlalchemy_events import SQLAlchemyEvents
113
- from models import Base
114
113
  from session import engine
115
114
 
116
115
 
117
116
  async def main():
118
117
  SQLAlchemyEvents(
119
- base=Base,
120
118
  engine=engine,
121
119
  autodiscover_paths=['services']
122
120
  )
@@ -133,22 +131,23 @@ if __name__ == '__main__':
133
131
  The SQLAlchemyEvents class accepts the following parameters:
134
132
  ```python
135
133
  SQLAlchemyEvents(
136
- base,
137
134
  engine,
138
135
  autodiscover_paths,
139
- logger=None
136
+ logger=None,
137
+ verbose=True
140
138
  )
141
139
  ```
142
140
 
143
141
  Parameters:
144
- * **base** - SQLAlchemy declarative base class used to discover mapped models.
145
142
  * **engine** - SQLAlchemy Engine or AsyncEngine instance.
146
143
  * **autodiscover_paths** - List of Python module paths where event handlers are defined.
147
144
  These modules are automatically imported so that decorators such as
148
- `@sa_insert_handler`, `@sa_update_handler`, `@sa_delete_handler` are executed.
145
+ `@sa_insert_handler`, `@sa_update_handler`, `@sa_delete_handler` are executed.
149
146
  **Example:**
150
147
  `autodiscover_paths=["services", "app.handlers"]`
151
148
 
149
+ * **verbose** - Enables detailed logging output. When set to True, the library will log additional informational and warning messages to help with debugging and configuration.
150
+
152
151
  ### Important:
153
152
 
154
153
  All modules containing event handlers must be imported through autodiscover
@@ -4,6 +4,7 @@ pyproject.toml
4
4
  src/sqlalchemy_events/__init__.py
5
5
  src/sqlalchemy_events/core.py
6
6
  src/sqlalchemy_events/decorators.py
7
+ src/sqlalchemy_events/default_logger.py
7
8
  src/sqlalchemy_events/discovery.py
8
9
  src/sqlalchemy_events/events.py
9
10
  src/sqlalchemy_events/exceptions.py
@@ -33,7 +33,6 @@ async def prepared_db(engine):
33
33
  await conn.run_sync(Base.metadata.create_all)
34
34
 
35
35
  SQLAlchemyEvents(
36
- base=Base,
37
36
  engine=engine,
38
37
  autodiscover_paths=['tests']
39
38
  )