sqlalchemy-events-lib 0.1.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.
- sqlalchemy_events_lib-0.1.0/LICENSE +5 -0
- sqlalchemy_events_lib-0.1.0/PKG-INFO +179 -0
- sqlalchemy_events_lib-0.1.0/README.md +160 -0
- sqlalchemy_events_lib-0.1.0/pyproject.toml +35 -0
- sqlalchemy_events_lib-0.1.0/setup.cfg +4 -0
- sqlalchemy_events_lib-0.1.0/src/sqlalchemy_events/__init__.py +8 -0
- sqlalchemy_events_lib-0.1.0/src/sqlalchemy_events/callbacks_strategies/__init__.py +0 -0
- sqlalchemy_events_lib-0.1.0/src/sqlalchemy_events/callbacks_strategies/base.py +8 -0
- sqlalchemy_events_lib-0.1.0/src/sqlalchemy_events/callbacks_strategies/postgres_callback.py +28 -0
- sqlalchemy_events_lib-0.1.0/src/sqlalchemy_events/core.py +103 -0
- sqlalchemy_events_lib-0.1.0/src/sqlalchemy_events/decorators.py +14 -0
- sqlalchemy_events_lib-0.1.0/src/sqlalchemy_events/discovery.py +24 -0
- sqlalchemy_events_lib-0.1.0/src/sqlalchemy_events/enums.py +15 -0
- sqlalchemy_events_lib-0.1.0/src/sqlalchemy_events/events.py +35 -0
- sqlalchemy_events_lib-0.1.0/src/sqlalchemy_events/exceptions.py +1 -0
- sqlalchemy_events_lib-0.1.0/src/sqlalchemy_events/handler.py +38 -0
- sqlalchemy_events_lib-0.1.0/src/sqlalchemy_events/init_triggers_strategies/__init__.py +0 -0
- sqlalchemy_events_lib-0.1.0/src/sqlalchemy_events/init_triggers_strategies/base.py +13 -0
- sqlalchemy_events_lib-0.1.0/src/sqlalchemy_events/init_triggers_strategies/postgres_init_triggers.py +89 -0
- sqlalchemy_events_lib-0.1.0/src/sqlalchemy_events/registry.py +15 -0
- sqlalchemy_events_lib-0.1.0/src/sqlalchemy_events/utils.py +25 -0
- sqlalchemy_events_lib-0.1.0/src/sqlalchemy_events_lib.egg-info/PKG-INFO +179 -0
- sqlalchemy_events_lib-0.1.0/src/sqlalchemy_events_lib.egg-info/SOURCES.txt +24 -0
- sqlalchemy_events_lib-0.1.0/src/sqlalchemy_events_lib.egg-info/dependency_links.txt +1 -0
- sqlalchemy_events_lib-0.1.0/src/sqlalchemy_events_lib.egg-info/requires.txt +1 -0
- sqlalchemy_events_lib-0.1.0/src/sqlalchemy_events_lib.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sqlalchemy-events-lib
|
|
3
|
+
Version: 0.1.0
|
|
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
|
+
Author-email: Alexey Kostarev <normjkeeewm@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/NormanwOw/SQLAlchemy-Events
|
|
8
|
+
Project-URL: Repository, https://github.com/NormanwOw/SQLAlchemy-Events
|
|
9
|
+
Project-URL: Issues, https://github.com/NormanwOw/SQLAlchemy-Events/issues
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Requires-Python: >=3.12
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: sqlalchemy>=2.0.49
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# SQLAlchemy Events
|
|
21
|
+
|
|
22
|
+
## About
|
|
23
|
+
Event-driven extension for SQLAlchemy that enables listening to database CUD events.
|
|
24
|
+
This library allows you to react to database changes in real time using a clean, declarative API.
|
|
25
|
+
* **Currently supports PostgreSQL only**
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
$ pip install sqlalchemy-events-lib
|
|
32
|
+
```
|
|
33
|
+
## Quick start
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
### Define models with enabled event tracking using **`@with_events`**
|
|
37
|
+
models.py
|
|
38
|
+
```python
|
|
39
|
+
import uuid
|
|
40
|
+
|
|
41
|
+
from sqlalchemy import UUID
|
|
42
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
|
43
|
+
|
|
44
|
+
from sqlalchemy_events import with_events, SaEvent
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Base(DeclarativeBase):
|
|
48
|
+
id: Mapped[uuid.UUID] = mapped_column(
|
|
49
|
+
UUID, nullable=False, primary_key=True, default=uuid.uuid4
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
@with_events([SaEvent.INSERT, SaEvent.UPDATE, SaEvent.DELETE])
|
|
53
|
+
class UserModel(Base):
|
|
54
|
+
__tablename__ = 'users'
|
|
55
|
+
|
|
56
|
+
name: Mapped[str] = mapped_column()
|
|
57
|
+
```
|
|
58
|
+
___
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
### Define event handlers using decorator `@sa_event_handler`
|
|
62
|
+
services/handlers.py
|
|
63
|
+
```python
|
|
64
|
+
from models import UserModel
|
|
65
|
+
from sqlalchemy_events import sa_event_handler
|
|
66
|
+
|
|
67
|
+
@sa_event_handler.on_insert(UserModel)
|
|
68
|
+
async def handle_user_insert():
|
|
69
|
+
print("User inserted!")
|
|
70
|
+
```
|
|
71
|
+
___
|
|
72
|
+
|
|
73
|
+
### Configure SQLAlchemy async engine
|
|
74
|
+
session.py
|
|
75
|
+
```python
|
|
76
|
+
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
|
77
|
+
|
|
78
|
+
from config import DATABASE_URL
|
|
79
|
+
|
|
80
|
+
engine = create_async_engine(DATABASE_URL)
|
|
81
|
+
async_session = async_sessionmaker(engine, expire_on_commit=False, autoflush=False)
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
___
|
|
85
|
+
|
|
86
|
+
### Initialize event system and start listening
|
|
87
|
+
main.py
|
|
88
|
+
```python
|
|
89
|
+
import asyncio
|
|
90
|
+
from sqlalchemy_events import SQLAlchemyEvents
|
|
91
|
+
from models import Base
|
|
92
|
+
from session import engine
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def main():
|
|
96
|
+
asyncio.create_task(
|
|
97
|
+
SQLAlchemyEvents(
|
|
98
|
+
base=Base,
|
|
99
|
+
engine=engine,
|
|
100
|
+
autodiscover_paths=['services']
|
|
101
|
+
).init()
|
|
102
|
+
)
|
|
103
|
+
try:
|
|
104
|
+
await asyncio.sleep(9999)
|
|
105
|
+
except:
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
if __name__ == '__main__':
|
|
109
|
+
asyncio.run(main())
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Configuration
|
|
113
|
+
### SQLAlchemyEvents
|
|
114
|
+
|
|
115
|
+
The SQLAlchemyEvents class accepts the following parameters:
|
|
116
|
+
```python
|
|
117
|
+
SQLAlchemyEvents(
|
|
118
|
+
base,
|
|
119
|
+
engine,
|
|
120
|
+
autodiscover_paths,
|
|
121
|
+
logger=None
|
|
122
|
+
)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Parameters:
|
|
126
|
+
* **base** - SQLAlchemy declarative base class used to discover mapped models.
|
|
127
|
+
* **engine** - SQLAlchemy Engine or AsyncEngine instance.
|
|
128
|
+
* **autodiscover_paths** - List of Python module paths where event handlers are defined.
|
|
129
|
+
These modules are automatically imported so that decorators such as `@sa_event_handler` are executed. Example:`autodiscover_paths=["services", "app.handlers"]`
|
|
130
|
+
|
|
131
|
+
### Important:
|
|
132
|
+
|
|
133
|
+
All modules containing event handlers must be imported through autodiscover
|
|
134
|
+
This ensures decorator registration is executed at startup
|
|
135
|
+
|
|
136
|
+
### logger (optional)
|
|
137
|
+
A standard Python logging.Logger instance.
|
|
138
|
+
|
|
139
|
+
If provided, the library will log internal lifecycle events such as:
|
|
140
|
+
|
|
141
|
+
* successful initialization
|
|
142
|
+
* listener startup
|
|
143
|
+
* trigger setup
|
|
144
|
+
|
|
145
|
+
**Example:**
|
|
146
|
+
```python
|
|
147
|
+
import logging
|
|
148
|
+
|
|
149
|
+
logger = logging.getLogger("sqlalchemy_events")
|
|
150
|
+
logger.setLevel(logging.INFO)
|
|
151
|
+
|
|
152
|
+
SQLAlchemyEvents(
|
|
153
|
+
base=Base,
|
|
154
|
+
engine=engine,
|
|
155
|
+
autodiscover_paths=["services"],
|
|
156
|
+
logger=logger
|
|
157
|
+
)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## How it works
|
|
161
|
+
1. autodiscover_paths modules are imported at startup
|
|
162
|
+
2. Decorators register event handlers into a global registry
|
|
163
|
+
3. Triggers send events via LISTEN/NOTIFY
|
|
164
|
+
4. The library receives notifications and dispatches them to registered handlers
|
|
165
|
+
|
|
166
|
+
## Notes
|
|
167
|
+
* Handlers can be both async and regular functions
|
|
168
|
+
* Only models registered with @with_events will emit events
|
|
169
|
+
* Currently supports LISTEN/NOTIFY
|
|
170
|
+
* Ensure handlers are imported via autodiscover_paths, otherwise they will not be registered
|
|
171
|
+
|
|
172
|
+
## Example flow
|
|
173
|
+
INSERT INTO {table} → DATABASE trigger fires →
|
|
174
|
+
NOTIFY → Python listener receives event →
|
|
175
|
+
handler is executed
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
|
|
179
|
+
MIT
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# SQLAlchemy Events
|
|
2
|
+
|
|
3
|
+
## About
|
|
4
|
+
Event-driven extension for SQLAlchemy that enables listening to database CUD events.
|
|
5
|
+
This library allows you to react to database changes in real time using a clean, declarative API.
|
|
6
|
+
* **Currently supports PostgreSQL only**
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
$ pip install sqlalchemy-events-lib
|
|
13
|
+
```
|
|
14
|
+
## Quick start
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Define models with enabled event tracking using **`@with_events`**
|
|
18
|
+
models.py
|
|
19
|
+
```python
|
|
20
|
+
import uuid
|
|
21
|
+
|
|
22
|
+
from sqlalchemy import UUID
|
|
23
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
|
24
|
+
|
|
25
|
+
from sqlalchemy_events import with_events, SaEvent
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Base(DeclarativeBase):
|
|
29
|
+
id: Mapped[uuid.UUID] = mapped_column(
|
|
30
|
+
UUID, nullable=False, primary_key=True, default=uuid.uuid4
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
@with_events([SaEvent.INSERT, SaEvent.UPDATE, SaEvent.DELETE])
|
|
34
|
+
class UserModel(Base):
|
|
35
|
+
__tablename__ = 'users'
|
|
36
|
+
|
|
37
|
+
name: Mapped[str] = mapped_column()
|
|
38
|
+
```
|
|
39
|
+
___
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
### Define event handlers using decorator `@sa_event_handler`
|
|
43
|
+
services/handlers.py
|
|
44
|
+
```python
|
|
45
|
+
from models import UserModel
|
|
46
|
+
from sqlalchemy_events import sa_event_handler
|
|
47
|
+
|
|
48
|
+
@sa_event_handler.on_insert(UserModel)
|
|
49
|
+
async def handle_user_insert():
|
|
50
|
+
print("User inserted!")
|
|
51
|
+
```
|
|
52
|
+
___
|
|
53
|
+
|
|
54
|
+
### Configure SQLAlchemy async engine
|
|
55
|
+
session.py
|
|
56
|
+
```python
|
|
57
|
+
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
|
58
|
+
|
|
59
|
+
from config import DATABASE_URL
|
|
60
|
+
|
|
61
|
+
engine = create_async_engine(DATABASE_URL)
|
|
62
|
+
async_session = async_sessionmaker(engine, expire_on_commit=False, autoflush=False)
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
___
|
|
66
|
+
|
|
67
|
+
### Initialize event system and start listening
|
|
68
|
+
main.py
|
|
69
|
+
```python
|
|
70
|
+
import asyncio
|
|
71
|
+
from sqlalchemy_events import SQLAlchemyEvents
|
|
72
|
+
from models import Base
|
|
73
|
+
from session import engine
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async def main():
|
|
77
|
+
asyncio.create_task(
|
|
78
|
+
SQLAlchemyEvents(
|
|
79
|
+
base=Base,
|
|
80
|
+
engine=engine,
|
|
81
|
+
autodiscover_paths=['services']
|
|
82
|
+
).init()
|
|
83
|
+
)
|
|
84
|
+
try:
|
|
85
|
+
await asyncio.sleep(9999)
|
|
86
|
+
except:
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
if __name__ == '__main__':
|
|
90
|
+
asyncio.run(main())
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Configuration
|
|
94
|
+
### SQLAlchemyEvents
|
|
95
|
+
|
|
96
|
+
The SQLAlchemyEvents class accepts the following parameters:
|
|
97
|
+
```python
|
|
98
|
+
SQLAlchemyEvents(
|
|
99
|
+
base,
|
|
100
|
+
engine,
|
|
101
|
+
autodiscover_paths,
|
|
102
|
+
logger=None
|
|
103
|
+
)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Parameters:
|
|
107
|
+
* **base** - SQLAlchemy declarative base class used to discover mapped models.
|
|
108
|
+
* **engine** - SQLAlchemy Engine or AsyncEngine instance.
|
|
109
|
+
* **autodiscover_paths** - List of Python module paths where event handlers are defined.
|
|
110
|
+
These modules are automatically imported so that decorators such as `@sa_event_handler` are executed. Example:`autodiscover_paths=["services", "app.handlers"]`
|
|
111
|
+
|
|
112
|
+
### Important:
|
|
113
|
+
|
|
114
|
+
All modules containing event handlers must be imported through autodiscover
|
|
115
|
+
This ensures decorator registration is executed at startup
|
|
116
|
+
|
|
117
|
+
### logger (optional)
|
|
118
|
+
A standard Python logging.Logger instance.
|
|
119
|
+
|
|
120
|
+
If provided, the library will log internal lifecycle events such as:
|
|
121
|
+
|
|
122
|
+
* successful initialization
|
|
123
|
+
* listener startup
|
|
124
|
+
* trigger setup
|
|
125
|
+
|
|
126
|
+
**Example:**
|
|
127
|
+
```python
|
|
128
|
+
import logging
|
|
129
|
+
|
|
130
|
+
logger = logging.getLogger("sqlalchemy_events")
|
|
131
|
+
logger.setLevel(logging.INFO)
|
|
132
|
+
|
|
133
|
+
SQLAlchemyEvents(
|
|
134
|
+
base=Base,
|
|
135
|
+
engine=engine,
|
|
136
|
+
autodiscover_paths=["services"],
|
|
137
|
+
logger=logger
|
|
138
|
+
)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## How it works
|
|
142
|
+
1. autodiscover_paths modules are imported at startup
|
|
143
|
+
2. Decorators register event handlers into a global registry
|
|
144
|
+
3. Triggers send events via LISTEN/NOTIFY
|
|
145
|
+
4. The library receives notifications and dispatches them to registered handlers
|
|
146
|
+
|
|
147
|
+
## Notes
|
|
148
|
+
* Handlers can be both async and regular functions
|
|
149
|
+
* Only models registered with @with_events will emit events
|
|
150
|
+
* Currently supports LISTEN/NOTIFY
|
|
151
|
+
* Ensure handlers are imported via autodiscover_paths, otherwise they will not be registered
|
|
152
|
+
|
|
153
|
+
## Example flow
|
|
154
|
+
INSERT INTO {table} → DATABASE trigger fires →
|
|
155
|
+
NOTIFY → Python listener receives event →
|
|
156
|
+
handler is executed
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
MIT
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[tool.setuptools]
|
|
6
|
+
package-dir = {"" = "src"}
|
|
7
|
+
|
|
8
|
+
[tool.setuptools.packages.find]
|
|
9
|
+
where = ["src"]
|
|
10
|
+
|
|
11
|
+
[project]
|
|
12
|
+
name = "sqlalchemy-events-lib"
|
|
13
|
+
version = "0.1.0"
|
|
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
|
+
requires-python = ">=3.12"
|
|
16
|
+
authors = [
|
|
17
|
+
{name = "Alexey Kostarev", email = "normjkeeewm@gmail.com"}
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"sqlalchemy>=2.0.49",
|
|
21
|
+
]
|
|
22
|
+
readme = {file = "README.md", content-type = "text/markdown"}
|
|
23
|
+
license = {text = "MIT"}
|
|
24
|
+
classifiers = [
|
|
25
|
+
"Programming Language :: Python :: 3",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"License :: OSI Approved :: MIT License",
|
|
28
|
+
"Operating System :: OS Independent",
|
|
29
|
+
]
|
|
30
|
+
keywords = []
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/NormanwOw/SQLAlchemy-Events"
|
|
34
|
+
Repository = "https://github.com/NormanwOw/SQLAlchemy-Events"
|
|
35
|
+
Issues = "https://github.com/NormanwOw/SQLAlchemy-Events/issues"
|
|
File without changes
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from .base import SaEventsCallbacksStrategy
|
|
5
|
+
from ..registry import get_event_handlers
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PostgresCallback(SaEventsCallbacksStrategy):
|
|
9
|
+
|
|
10
|
+
def __init__(self) -> None:
|
|
11
|
+
self.__handlers = get_event_handlers()
|
|
12
|
+
|
|
13
|
+
async def handle(self, *args, **kwargs):
|
|
14
|
+
data = json.loads(args[3] or '{}')
|
|
15
|
+
trig_name = data.get('trigger')
|
|
16
|
+
|
|
17
|
+
if not trig_name:
|
|
18
|
+
return
|
|
19
|
+
|
|
20
|
+
handlers = self.__handlers.get(trig_name, [])
|
|
21
|
+
if not handlers:
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
for handler in handlers:
|
|
25
|
+
if asyncio.iscoroutinefunction(handler):
|
|
26
|
+
await handler()
|
|
27
|
+
else:
|
|
28
|
+
handler()
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional, Type, Union
|
|
5
|
+
import inspect
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import Engine
|
|
8
|
+
from sqlalchemy.ext.asyncio import AsyncEngine
|
|
9
|
+
from sqlalchemy.orm import DeclarativeBase
|
|
10
|
+
|
|
11
|
+
from .discovery import autodiscover
|
|
12
|
+
from .events import SaEventStrategy, sa_events_strategy
|
|
13
|
+
from .registry import get_event_handlers
|
|
14
|
+
from .utils import dialect_resolver
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SQLAlchemyEvents:
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
base: Type[DeclarativeBase],
|
|
22
|
+
engine: Union[AsyncEngine, Engine],
|
|
23
|
+
autodiscover_paths: list[str],
|
|
24
|
+
logger: Optional[logging.Logger] = None
|
|
25
|
+
) -> None:
|
|
26
|
+
self.base = base
|
|
27
|
+
self.engine = engine
|
|
28
|
+
self.autodiscover_paths = autodiscover_paths
|
|
29
|
+
self.logger = logger
|
|
30
|
+
|
|
31
|
+
async def init(self) -> None:
|
|
32
|
+
if not await self.__find_handlers():
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
dialect = dialect_resolver(self.engine)
|
|
36
|
+
event_strategy = sa_events_strategy.get(dialect)
|
|
37
|
+
if not event_strategy:
|
|
38
|
+
raise RuntimeError(f'[SQLAlchemyEvents] Unsupported database {dialect}. '
|
|
39
|
+
f'This library supports only {', '.join(sa_events_strategy.keys())}')
|
|
40
|
+
await self.__start_listen(event_strategy)
|
|
41
|
+
|
|
42
|
+
async def __find_handlers(self):
|
|
43
|
+
autodiscover(self.autodiscover_paths)
|
|
44
|
+
handlers = get_event_handlers()
|
|
45
|
+
if not handlers:
|
|
46
|
+
self.logger.info('[SQLAlchemyEvents] No handlers found')
|
|
47
|
+
return
|
|
48
|
+
res_handlers = []
|
|
49
|
+
for handlers_list in handlers.values():
|
|
50
|
+
res_handlers.extend(handlers_list)
|
|
51
|
+
for handler in res_handlers:
|
|
52
|
+
file_path = inspect.getsourcefile(handler) or inspect.getfile(handler)
|
|
53
|
+
file_func = Path(file_path)
|
|
54
|
+
file_name = file_func.name
|
|
55
|
+
self.logger.info(f'[SQLAlchemyEvents] Registered handler {handler.__name__} '
|
|
56
|
+
f'from {file_func.parent.name}/{file_name}')
|
|
57
|
+
|
|
58
|
+
return res_handlers
|
|
59
|
+
|
|
60
|
+
async def __start_listen(self, event_strategy: SaEventStrategy):
|
|
61
|
+
if isinstance(self.engine, AsyncEngine):
|
|
62
|
+
async with self.engine.connect() as conn:
|
|
63
|
+
raw_conn = await conn.get_raw_connection()
|
|
64
|
+
driver_conn = raw_conn.driver_connection
|
|
65
|
+
|
|
66
|
+
if not hasattr(driver_conn, 'add_listener'):
|
|
67
|
+
raise RuntimeError('[SQLAlchemyEvents] Driver does not support LISTEN/NOTIFY')
|
|
68
|
+
|
|
69
|
+
await event_strategy.init_triggers(
|
|
70
|
+
model_list=self.base.__subclasses__(),
|
|
71
|
+
conn=conn,
|
|
72
|
+
logger=self.logger
|
|
73
|
+
)
|
|
74
|
+
if self.logger:
|
|
75
|
+
self.logger.info('[SQLAlchemyEvents] Start listening')
|
|
76
|
+
await driver_conn.add_listener('sqlalchemy_events', event_strategy.callback.handle)
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
elif isinstance(self.engine, Engine):
|
|
80
|
+
sync_engine_error = ('[SQLAlchemyEvents] Sync Engine driver does not support async LISTEN/NOTIFY. '
|
|
81
|
+
'Use AsyncEngine with asyncpg or psycopg[async]')
|
|
82
|
+
with self.engine.connect() as conn:
|
|
83
|
+
raw_conn = conn.connection
|
|
84
|
+
driver_conn = getattr(raw_conn, 'driver_connection', raw_conn)
|
|
85
|
+
|
|
86
|
+
if not hasattr(driver_conn, 'add_listener'):
|
|
87
|
+
raise RuntimeError(sync_engine_error)
|
|
88
|
+
result = driver_conn.add_listener('sqlalchemy_events', event_strategy.callback.handle)
|
|
89
|
+
|
|
90
|
+
if not asyncio.iscoroutine(result):
|
|
91
|
+
raise RuntimeError(sync_engine_error)
|
|
92
|
+
await event_strategy.init_triggers(
|
|
93
|
+
model_list=self.base.__subclasses__(),
|
|
94
|
+
conn=conn,
|
|
95
|
+
logger=self.logger
|
|
96
|
+
)
|
|
97
|
+
if self.logger:
|
|
98
|
+
self.logger.info('[SQLAlchemyEvents] Start listening')
|
|
99
|
+
await result
|
|
100
|
+
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
raise TypeError('[SQLAlchemyEvents] engine must be AsyncEngine or Engine')
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import pkgutil
|
|
3
|
+
from types import ModuleType
|
|
4
|
+
from typing import Iterable, List
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def autodiscover(paths: Iterable[str]) -> List[ModuleType]:
|
|
8
|
+
modules: List[ModuleType] = []
|
|
9
|
+
|
|
10
|
+
for path in paths:
|
|
11
|
+
module = importlib.import_module(path)
|
|
12
|
+
modules.append(module)
|
|
13
|
+
|
|
14
|
+
if not hasattr(module, '__path__'):
|
|
15
|
+
continue
|
|
16
|
+
|
|
17
|
+
for _, module_name, _ in pkgutil.walk_packages(
|
|
18
|
+
module.__path__,
|
|
19
|
+
module.__name__ + '.',
|
|
20
|
+
):
|
|
21
|
+
submodule = importlib.import_module(module_name)
|
|
22
|
+
modules.append(submodule)
|
|
23
|
+
|
|
24
|
+
return modules
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from enum import StrEnum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SaEvent(StrEnum):
|
|
5
|
+
INSERT = 'INSERT'
|
|
6
|
+
UPDATE = 'UPDATE'
|
|
7
|
+
DELETE = 'DELETE'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Dialect(StrEnum):
|
|
11
|
+
POSTGRESQL = 'postgresql'
|
|
12
|
+
SQLITE = 'sqlite'
|
|
13
|
+
MYSQL = 'mysql'
|
|
14
|
+
MSSQL = 'mssql'
|
|
15
|
+
ORACLE = 'oracle'
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from .callbacks_strategies.base import SaEventsCallbacksStrategy
|
|
4
|
+
from .callbacks_strategies.postgres_callback import PostgresCallback
|
|
5
|
+
from .enums import Dialect, SaEvent
|
|
6
|
+
from .init_triggers_strategies.base import InitTriggersStrategy
|
|
7
|
+
from .init_triggers_strategies.postgres_init_triggers import PostgresInitTriggers
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SaEvents:
|
|
11
|
+
|
|
12
|
+
def __init__(self, events: list[SaEvent]):
|
|
13
|
+
self.events = events
|
|
14
|
+
for e in events:
|
|
15
|
+
setattr(self, e.name, e)
|
|
16
|
+
|
|
17
|
+
def __iter__(self):
|
|
18
|
+
return iter(self.__dict__.values())
|
|
19
|
+
|
|
20
|
+
def __len__(self):
|
|
21
|
+
return len(self.events)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class SaEventStrategy:
|
|
26
|
+
init_triggers: InitTriggersStrategy
|
|
27
|
+
callback: SaEventsCallbacksStrategy
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
sa_events_strategy: dict[Dialect, SaEventStrategy] = {
|
|
31
|
+
Dialect.POSTGRESQL: SaEventStrategy(
|
|
32
|
+
init_triggers=PostgresInitTriggers(),
|
|
33
|
+
callback=PostgresCallback()
|
|
34
|
+
)
|
|
35
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
class InvalidModelEventError(ValueError): ...
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from typing import Callable, Type
|
|
2
|
+
from sqlalchemy.orm import DeclarativeBase
|
|
3
|
+
|
|
4
|
+
from .enums import SaEvent
|
|
5
|
+
from .registry import get_event_handlers
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SaEventHandler:
|
|
9
|
+
|
|
10
|
+
def __init__(self):
|
|
11
|
+
self.__handlers = get_event_handlers()
|
|
12
|
+
|
|
13
|
+
def __sa_event_handler(self, model: Type[DeclarativeBase], event: SaEvent):
|
|
14
|
+
if event not in getattr(model, '__events__', set()):
|
|
15
|
+
raise ValueError(
|
|
16
|
+
f'[SQLAlchemyEvents] Event {event} is not registered for model {model.__name__}'
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
def decorator(func: Callable):
|
|
20
|
+
trig_name = f'sa_{model.__tablename__}_{event.lower()}_notify'
|
|
21
|
+
|
|
22
|
+
self.__handlers.setdefault(trig_name, []).append(func)
|
|
23
|
+
|
|
24
|
+
return func
|
|
25
|
+
|
|
26
|
+
return decorator
|
|
27
|
+
|
|
28
|
+
def on_insert(self, model: Type[DeclarativeBase]):
|
|
29
|
+
return self.__sa_event_handler(model, SaEvent.INSERT)
|
|
30
|
+
|
|
31
|
+
def on_update(self, model: Type[DeclarativeBase]):
|
|
32
|
+
return self.__sa_event_handler(model, SaEvent.UPDATE)
|
|
33
|
+
|
|
34
|
+
def on_delete(self, model: Type[DeclarativeBase]):
|
|
35
|
+
return self.__sa_event_handler(model, SaEvent.DELETE)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
sa_event_handler = SaEventHandler()
|
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Type
|
|
3
|
+
|
|
4
|
+
from sqlalchemy.orm import DeclarativeBase
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class InitTriggersStrategy(ABC):
|
|
8
|
+
|
|
9
|
+
@abstractmethod
|
|
10
|
+
async def __call__(self, model_list: list[Type[DeclarativeBase]], conn, logger):
|
|
11
|
+
raise NotImplementedError
|
|
12
|
+
|
|
13
|
+
|
sqlalchemy_events_lib-0.1.0/src/sqlalchemy_events/init_triggers_strategies/postgres_init_triggers.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from typing import Type
|
|
2
|
+
|
|
3
|
+
from sqlalchemy import text
|
|
4
|
+
from sqlalchemy.orm import DeclarativeBase
|
|
5
|
+
|
|
6
|
+
from ..enums import SaEvent
|
|
7
|
+
from .base import InitTriggersStrategy
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PostgresInitTriggers(InitTriggersStrategy):
|
|
11
|
+
|
|
12
|
+
async def __call__(self, model_list: list[Type[DeclarativeBase]], conn, logger):
|
|
13
|
+
await conn.execute(
|
|
14
|
+
text("""
|
|
15
|
+
CREATE OR REPLACE FUNCTION sqlalchemy_events()
|
|
16
|
+
RETURNS trigger AS $$
|
|
17
|
+
BEGIN PERFORM pg_notify(
|
|
18
|
+
'sqlalchemy_events', json_build_object(
|
|
19
|
+
'table', TG_TABLE_NAME, 'event', TG_OP, 'trigger', TG_NAME
|
|
20
|
+
)::text);
|
|
21
|
+
RETURN NEW; END; $$ LANGUAGE plpgsql;
|
|
22
|
+
""")
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
for cls in model_list:
|
|
26
|
+
events = getattr(cls, '__events__', None)
|
|
27
|
+
|
|
28
|
+
if not events or not all(isinstance(e, SaEvent) for e in events):
|
|
29
|
+
continue
|
|
30
|
+
|
|
31
|
+
table_name = cls.__tablename__
|
|
32
|
+
|
|
33
|
+
existing_triggers = set()
|
|
34
|
+
|
|
35
|
+
table_exists = await conn.scalar(
|
|
36
|
+
text("""
|
|
37
|
+
SELECT EXISTS (SELECT 1
|
|
38
|
+
FROM pg_class
|
|
39
|
+
WHERE relname = :tbl
|
|
40
|
+
AND relkind = 'r')
|
|
41
|
+
"""),
|
|
42
|
+
{'tbl': table_name},
|
|
43
|
+
)
|
|
44
|
+
if not table_exists:
|
|
45
|
+
if logger:
|
|
46
|
+
logger.warning(f"[SQLAlchemyEvents] {cls.__name__} has "
|
|
47
|
+
f"{'event' if len(events) == 1 else 'events'} {', '.join(events)}, "
|
|
48
|
+
f"but relation '{table_name}' does not exist")
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
if table_exists:
|
|
52
|
+
tg_sql = text("""
|
|
53
|
+
SELECT tgname
|
|
54
|
+
FROM pg_trigger
|
|
55
|
+
WHERE tgrelid = to_regclass(:tbl)
|
|
56
|
+
AND NOT tgisinternal
|
|
57
|
+
""")
|
|
58
|
+
|
|
59
|
+
for row in await conn.execute(tg_sql, {'tbl': table_name}):
|
|
60
|
+
existing_triggers.add(row[0])
|
|
61
|
+
|
|
62
|
+
added_events = []
|
|
63
|
+
for event in sorted(events):
|
|
64
|
+
l_event = event.lower()
|
|
65
|
+
trig_name = f'sa_{table_name}_{l_event}_notify'
|
|
66
|
+
|
|
67
|
+
if trig_name in existing_triggers:
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
added_events.append(event)
|
|
71
|
+
await conn.execute(
|
|
72
|
+
text(f"DROP TRIGGER IF EXISTS {trig_name} ON {table_name};")
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
await conn.execute(
|
|
76
|
+
text(f"""
|
|
77
|
+
CREATE TRIGGER {trig_name}
|
|
78
|
+
AFTER {event} ON {table_name}
|
|
79
|
+
FOR EACH ROW EXECUTE FUNCTION sqlalchemy_events();
|
|
80
|
+
""")
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if added_events and logger:
|
|
84
|
+
logger.info(
|
|
85
|
+
f"Detected added {'event' if len(added_events) == 1 else 'events'} "
|
|
86
|
+
f"{', '.join(added_events)} for table '{table_name}'"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
await conn.commit()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
_GLOBAL_KEY = 'sqlalchemy_events._global_registry'
|
|
4
|
+
|
|
5
|
+
if _GLOBAL_KEY not in sys.modules:
|
|
6
|
+
class _Registry:
|
|
7
|
+
handlers = {}
|
|
8
|
+
|
|
9
|
+
sys.modules[_GLOBAL_KEY] = _Registry()
|
|
10
|
+
|
|
11
|
+
_registry = sys.modules[_GLOBAL_KEY]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_event_handlers():
|
|
15
|
+
return _registry.handlers
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from typing import Union
|
|
2
|
+
from sqlalchemy.engine import Engine
|
|
3
|
+
from sqlalchemy.ext.asyncio import AsyncEngine
|
|
4
|
+
|
|
5
|
+
from .enums import Dialect
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def dialect_resolver(engine: Union[AsyncEngine, Engine]) -> Dialect:
|
|
9
|
+
if isinstance(engine, AsyncEngine):
|
|
10
|
+
dialect_name = engine.sync_engine.dialect.name.lower()
|
|
11
|
+
else:
|
|
12
|
+
dialect_name = engine.dialect.name.lower()
|
|
13
|
+
|
|
14
|
+
if dialect_name.startswith('postgres'):
|
|
15
|
+
return Dialect.POSTGRESQL
|
|
16
|
+
elif dialect_name.startswith('sqlite'):
|
|
17
|
+
return Dialect.SQLITE
|
|
18
|
+
elif dialect_name.startswith('mysql'):
|
|
19
|
+
return Dialect.MYSQL
|
|
20
|
+
elif dialect_name in ('mssql', 'sqlserver'):
|
|
21
|
+
return Dialect.MSSQL
|
|
22
|
+
elif dialect_name.startswith('oracle'):
|
|
23
|
+
return Dialect.ORACLE
|
|
24
|
+
|
|
25
|
+
raise ValueError(f'Unsupported dialect: {dialect_name}')
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sqlalchemy-events-lib
|
|
3
|
+
Version: 0.1.0
|
|
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
|
+
Author-email: Alexey Kostarev <normjkeeewm@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/NormanwOw/SQLAlchemy-Events
|
|
8
|
+
Project-URL: Repository, https://github.com/NormanwOw/SQLAlchemy-Events
|
|
9
|
+
Project-URL: Issues, https://github.com/NormanwOw/SQLAlchemy-Events/issues
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Requires-Python: >=3.12
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: sqlalchemy>=2.0.49
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# SQLAlchemy Events
|
|
21
|
+
|
|
22
|
+
## About
|
|
23
|
+
Event-driven extension for SQLAlchemy that enables listening to database CUD events.
|
|
24
|
+
This library allows you to react to database changes in real time using a clean, declarative API.
|
|
25
|
+
* **Currently supports PostgreSQL only**
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
$ pip install sqlalchemy-events-lib
|
|
32
|
+
```
|
|
33
|
+
## Quick start
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
### Define models with enabled event tracking using **`@with_events`**
|
|
37
|
+
models.py
|
|
38
|
+
```python
|
|
39
|
+
import uuid
|
|
40
|
+
|
|
41
|
+
from sqlalchemy import UUID
|
|
42
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
|
43
|
+
|
|
44
|
+
from sqlalchemy_events import with_events, SaEvent
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Base(DeclarativeBase):
|
|
48
|
+
id: Mapped[uuid.UUID] = mapped_column(
|
|
49
|
+
UUID, nullable=False, primary_key=True, default=uuid.uuid4
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
@with_events([SaEvent.INSERT, SaEvent.UPDATE, SaEvent.DELETE])
|
|
53
|
+
class UserModel(Base):
|
|
54
|
+
__tablename__ = 'users'
|
|
55
|
+
|
|
56
|
+
name: Mapped[str] = mapped_column()
|
|
57
|
+
```
|
|
58
|
+
___
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
### Define event handlers using decorator `@sa_event_handler`
|
|
62
|
+
services/handlers.py
|
|
63
|
+
```python
|
|
64
|
+
from models import UserModel
|
|
65
|
+
from sqlalchemy_events import sa_event_handler
|
|
66
|
+
|
|
67
|
+
@sa_event_handler.on_insert(UserModel)
|
|
68
|
+
async def handle_user_insert():
|
|
69
|
+
print("User inserted!")
|
|
70
|
+
```
|
|
71
|
+
___
|
|
72
|
+
|
|
73
|
+
### Configure SQLAlchemy async engine
|
|
74
|
+
session.py
|
|
75
|
+
```python
|
|
76
|
+
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
|
77
|
+
|
|
78
|
+
from config import DATABASE_URL
|
|
79
|
+
|
|
80
|
+
engine = create_async_engine(DATABASE_URL)
|
|
81
|
+
async_session = async_sessionmaker(engine, expire_on_commit=False, autoflush=False)
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
___
|
|
85
|
+
|
|
86
|
+
### Initialize event system and start listening
|
|
87
|
+
main.py
|
|
88
|
+
```python
|
|
89
|
+
import asyncio
|
|
90
|
+
from sqlalchemy_events import SQLAlchemyEvents
|
|
91
|
+
from models import Base
|
|
92
|
+
from session import engine
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def main():
|
|
96
|
+
asyncio.create_task(
|
|
97
|
+
SQLAlchemyEvents(
|
|
98
|
+
base=Base,
|
|
99
|
+
engine=engine,
|
|
100
|
+
autodiscover_paths=['services']
|
|
101
|
+
).init()
|
|
102
|
+
)
|
|
103
|
+
try:
|
|
104
|
+
await asyncio.sleep(9999)
|
|
105
|
+
except:
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
if __name__ == '__main__':
|
|
109
|
+
asyncio.run(main())
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Configuration
|
|
113
|
+
### SQLAlchemyEvents
|
|
114
|
+
|
|
115
|
+
The SQLAlchemyEvents class accepts the following parameters:
|
|
116
|
+
```python
|
|
117
|
+
SQLAlchemyEvents(
|
|
118
|
+
base,
|
|
119
|
+
engine,
|
|
120
|
+
autodiscover_paths,
|
|
121
|
+
logger=None
|
|
122
|
+
)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Parameters:
|
|
126
|
+
* **base** - SQLAlchemy declarative base class used to discover mapped models.
|
|
127
|
+
* **engine** - SQLAlchemy Engine or AsyncEngine instance.
|
|
128
|
+
* **autodiscover_paths** - List of Python module paths where event handlers are defined.
|
|
129
|
+
These modules are automatically imported so that decorators such as `@sa_event_handler` are executed. Example:`autodiscover_paths=["services", "app.handlers"]`
|
|
130
|
+
|
|
131
|
+
### Important:
|
|
132
|
+
|
|
133
|
+
All modules containing event handlers must be imported through autodiscover
|
|
134
|
+
This ensures decorator registration is executed at startup
|
|
135
|
+
|
|
136
|
+
### logger (optional)
|
|
137
|
+
A standard Python logging.Logger instance.
|
|
138
|
+
|
|
139
|
+
If provided, the library will log internal lifecycle events such as:
|
|
140
|
+
|
|
141
|
+
* successful initialization
|
|
142
|
+
* listener startup
|
|
143
|
+
* trigger setup
|
|
144
|
+
|
|
145
|
+
**Example:**
|
|
146
|
+
```python
|
|
147
|
+
import logging
|
|
148
|
+
|
|
149
|
+
logger = logging.getLogger("sqlalchemy_events")
|
|
150
|
+
logger.setLevel(logging.INFO)
|
|
151
|
+
|
|
152
|
+
SQLAlchemyEvents(
|
|
153
|
+
base=Base,
|
|
154
|
+
engine=engine,
|
|
155
|
+
autodiscover_paths=["services"],
|
|
156
|
+
logger=logger
|
|
157
|
+
)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## How it works
|
|
161
|
+
1. autodiscover_paths modules are imported at startup
|
|
162
|
+
2. Decorators register event handlers into a global registry
|
|
163
|
+
3. Triggers send events via LISTEN/NOTIFY
|
|
164
|
+
4. The library receives notifications and dispatches them to registered handlers
|
|
165
|
+
|
|
166
|
+
## Notes
|
|
167
|
+
* Handlers can be both async and regular functions
|
|
168
|
+
* Only models registered with @with_events will emit events
|
|
169
|
+
* Currently supports LISTEN/NOTIFY
|
|
170
|
+
* Ensure handlers are imported via autodiscover_paths, otherwise they will not be registered
|
|
171
|
+
|
|
172
|
+
## Example flow
|
|
173
|
+
INSERT INTO {table} → DATABASE trigger fires →
|
|
174
|
+
NOTIFY → Python listener receives event →
|
|
175
|
+
handler is executed
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
|
|
179
|
+
MIT
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/sqlalchemy_events/__init__.py
|
|
5
|
+
src/sqlalchemy_events/core.py
|
|
6
|
+
src/sqlalchemy_events/decorators.py
|
|
7
|
+
src/sqlalchemy_events/discovery.py
|
|
8
|
+
src/sqlalchemy_events/enums.py
|
|
9
|
+
src/sqlalchemy_events/events.py
|
|
10
|
+
src/sqlalchemy_events/exceptions.py
|
|
11
|
+
src/sqlalchemy_events/handler.py
|
|
12
|
+
src/sqlalchemy_events/registry.py
|
|
13
|
+
src/sqlalchemy_events/utils.py
|
|
14
|
+
src/sqlalchemy_events/callbacks_strategies/__init__.py
|
|
15
|
+
src/sqlalchemy_events/callbacks_strategies/base.py
|
|
16
|
+
src/sqlalchemy_events/callbacks_strategies/postgres_callback.py
|
|
17
|
+
src/sqlalchemy_events/init_triggers_strategies/__init__.py
|
|
18
|
+
src/sqlalchemy_events/init_triggers_strategies/base.py
|
|
19
|
+
src/sqlalchemy_events/init_triggers_strategies/postgres_init_triggers.py
|
|
20
|
+
src/sqlalchemy_events_lib.egg-info/PKG-INFO
|
|
21
|
+
src/sqlalchemy_events_lib.egg-info/SOURCES.txt
|
|
22
|
+
src/sqlalchemy_events_lib.egg-info/dependency_links.txt
|
|
23
|
+
src/sqlalchemy_events_lib.egg-info/requires.txt
|
|
24
|
+
src/sqlalchemy_events_lib.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sqlalchemy>=2.0.49
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sqlalchemy_events
|