sqlalchemy-events-lib 0.1.0__tar.gz → 0.2.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 → sqlalchemy_events_lib-0.2.0}/LICENSE +0 -0
- {sqlalchemy_events_lib-0.1.0 → sqlalchemy_events_lib-0.2.0}/PKG-INFO +200 -179
- {sqlalchemy_events_lib-0.1.0 → sqlalchemy_events_lib-0.2.0}/README.md +35 -17
- {sqlalchemy_events_lib-0.1.0 → sqlalchemy_events_lib-0.2.0}/pyproject.toml +45 -35
- {sqlalchemy_events_lib-0.1.0 → sqlalchemy_events_lib-0.2.0}/setup.cfg +4 -4
- sqlalchemy_events_lib-0.2.0/src/sqlalchemy_events/__init__.py +14 -0
- {sqlalchemy_events_lib-0.1.0 → sqlalchemy_events_lib-0.2.0}/src/sqlalchemy_events/callbacks_strategies/__init__.py +0 -0
- {sqlalchemy_events_lib-0.1.0 → sqlalchemy_events_lib-0.2.0}/src/sqlalchemy_events/callbacks_strategies/base.py +0 -0
- sqlalchemy_events_lib-0.2.0/src/sqlalchemy_events/callbacks_strategies/postgres_callback.py +69 -0
- {sqlalchemy_events_lib-0.1.0 → sqlalchemy_events_lib-0.2.0}/src/sqlalchemy_events/core.py +44 -22
- {sqlalchemy_events_lib-0.1.0 → sqlalchemy_events_lib-0.2.0}/src/sqlalchemy_events/decorators.py +2 -2
- {sqlalchemy_events_lib-0.1.0 → sqlalchemy_events_lib-0.2.0}/src/sqlalchemy_events/discovery.py +0 -0
- {sqlalchemy_events_lib-0.1.0 → sqlalchemy_events_lib-0.2.0}/src/sqlalchemy_events/events.py +1 -1
- {sqlalchemy_events_lib-0.1.0 → sqlalchemy_events_lib-0.2.0}/src/sqlalchemy_events/exceptions.py +0 -0
- sqlalchemy_events_lib-0.2.0/src/sqlalchemy_events/handlers.py +50 -0
- {sqlalchemy_events_lib-0.1.0 → sqlalchemy_events_lib-0.2.0}/src/sqlalchemy_events/init_triggers_strategies/__init__.py +0 -0
- {sqlalchemy_events_lib-0.1.0 → sqlalchemy_events_lib-0.2.0}/src/sqlalchemy_events/init_triggers_strategies/base.py +0 -0
- {sqlalchemy_events_lib-0.1.0 → sqlalchemy_events_lib-0.2.0}/src/sqlalchemy_events/init_triggers_strategies/postgres_init_triggers.py +30 -16
- {sqlalchemy_events_lib-0.1.0 → sqlalchemy_events_lib-0.2.0}/src/sqlalchemy_events/registry.py +0 -0
- sqlalchemy_events_lib-0.2.0/src/sqlalchemy_events/types.py +26 -0
- {sqlalchemy_events_lib-0.1.0 → sqlalchemy_events_lib-0.2.0}/src/sqlalchemy_events/utils.py +12 -2
- {sqlalchemy_events_lib-0.1.0 → sqlalchemy_events_lib-0.2.0}/src/sqlalchemy_events_lib.egg-info/PKG-INFO +200 -179
- {sqlalchemy_events_lib-0.1.0 → sqlalchemy_events_lib-0.2.0}/src/sqlalchemy_events_lib.egg-info/SOURCES.txt +7 -3
- {sqlalchemy_events_lib-0.1.0 → sqlalchemy_events_lib-0.2.0}/src/sqlalchemy_events_lib.egg-info/dependency_links.txt +0 -0
- sqlalchemy_events_lib-0.2.0/src/sqlalchemy_events_lib.egg-info/requires.txt +5 -0
- {sqlalchemy_events_lib-0.1.0 → sqlalchemy_events_lib-0.2.0}/src/sqlalchemy_events_lib.egg-info/top_level.txt +1 -0
- sqlalchemy_events_lib-0.2.0/src/tests/conftest.py +50 -0
- sqlalchemy_events_lib-0.2.0/src/tests/test_autodiscover.py +129 -0
- sqlalchemy_events_lib-0.2.0/src/tests/test_is_created_triggers.py +42 -0
- sqlalchemy_events_lib-0.2.0/src/tests/test_models.py +16 -0
- sqlalchemy_events_lib-0.1.0/src/sqlalchemy_events/__init__.py +0 -8
- sqlalchemy_events_lib-0.1.0/src/sqlalchemy_events/callbacks_strategies/postgres_callback.py +0 -28
- sqlalchemy_events_lib-0.1.0/src/sqlalchemy_events/enums.py +0 -15
- sqlalchemy_events_lib-0.1.0/src/sqlalchemy_events/handler.py +0 -38
- sqlalchemy_events_lib-0.1.0/src/sqlalchemy_events_lib.egg-info/requires.txt +0 -1
|
File without changes
|
|
@@ -1,179 +1,200 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: sqlalchemy-events-lib
|
|
3
|
-
Version: 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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
from
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
async
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
###
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
**
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sqlalchemy-events-lib
|
|
3
|
+
Version: 0.2.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
|
+
Provides-Extra: test
|
|
19
|
+
Requires-Dist: pytest; extra == "test"
|
|
20
|
+
Requires-Dist: pytest-asyncio; extra == "test"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# SQLAlchemy Events
|
|
24
|
+
|
|
25
|
+
## About
|
|
26
|
+
Event-driven extension for SQLAlchemy that enables listening to database CUD events.
|
|
27
|
+
This library allows you to react to database changes in real time using a clean, declarative API.
|
|
28
|
+
* **Currently supports PostgreSQL only**
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
$ pip install sqlalchemy-events-lib
|
|
35
|
+
```
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
### Define models with enabled event tracking using **`@with_events`**
|
|
40
|
+
models.py
|
|
41
|
+
```python
|
|
42
|
+
import uuid
|
|
43
|
+
|
|
44
|
+
from sqlalchemy import UUID
|
|
45
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
|
46
|
+
|
|
47
|
+
from sqlalchemy_events import with_events, SaEvent
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Base(DeclarativeBase):
|
|
51
|
+
id: Mapped[uuid.UUID] = mapped_column(
|
|
52
|
+
UUID, nullable=False, primary_key=True, default=uuid.uuid4
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
@with_events([SaEvent.INSERT, SaEvent.UPDATE, SaEvent.DELETE])
|
|
56
|
+
class UserModel(Base):
|
|
57
|
+
__tablename__ = 'users'
|
|
58
|
+
|
|
59
|
+
name: Mapped[str] = mapped_column()
|
|
60
|
+
```
|
|
61
|
+
___
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
### Define event handlers using decorators `@sa_insert_handler`, `@sa_update_handler`, `@sa_delete_handler`
|
|
65
|
+
services/handlers.py
|
|
66
|
+
```python
|
|
67
|
+
from models import UserModel
|
|
68
|
+
from sqlalchemy_events import sa_insert_handler
|
|
69
|
+
|
|
70
|
+
@sa_insert_handler(UserModel)
|
|
71
|
+
async def handle_user_insert():
|
|
72
|
+
print('User inserted!')
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Optional Argument**: `rows`
|
|
76
|
+
Handlers can optionally accept a rows argument.
|
|
77
|
+
|
|
78
|
+
If the parameter is declared in the function signature, it will automatically receive a list of affected `row` IDs.
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from models import UserModel
|
|
82
|
+
from sqlalchemy_events import sa_insert_handler
|
|
83
|
+
|
|
84
|
+
@sa_insert_handler(UserModel)
|
|
85
|
+
async def handle_user_insert(rows: list[DB_ID]):
|
|
86
|
+
print('Users inserted!', rows)
|
|
87
|
+
```
|
|
88
|
+
**Output:**
|
|
89
|
+
```
|
|
90
|
+
Users inserted! ['3310aa38-f555-4c05-a57f-5acae32a0a7b', '6aeba1b0-8e80-4369-8adf-cf967ab7ba7a']
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
___
|
|
94
|
+
|
|
95
|
+
### Configure SQLAlchemy async engine
|
|
96
|
+
session.py
|
|
97
|
+
```python
|
|
98
|
+
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
|
99
|
+
|
|
100
|
+
from config import DATABASE_URL
|
|
101
|
+
|
|
102
|
+
engine = create_async_engine(DATABASE_URL)
|
|
103
|
+
async_session = async_sessionmaker(engine, expire_on_commit=False, autoflush=False)
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
___
|
|
107
|
+
|
|
108
|
+
### Initialize event system and start listening
|
|
109
|
+
main.py
|
|
110
|
+
```python
|
|
111
|
+
import asyncio
|
|
112
|
+
from sqlalchemy_events import SQLAlchemyEvents
|
|
113
|
+
from models import Base
|
|
114
|
+
from session import engine
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
async def main():
|
|
118
|
+
SQLAlchemyEvents(
|
|
119
|
+
base=Base,
|
|
120
|
+
engine=engine,
|
|
121
|
+
autodiscover_paths=['services']
|
|
122
|
+
)
|
|
123
|
+
while True:
|
|
124
|
+
await asyncio.sleep(9999)
|
|
125
|
+
|
|
126
|
+
if __name__ == '__main__':
|
|
127
|
+
asyncio.run(main())
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Configuration
|
|
131
|
+
### SQLAlchemyEvents
|
|
132
|
+
|
|
133
|
+
The SQLAlchemyEvents class accepts the following parameters:
|
|
134
|
+
```python
|
|
135
|
+
SQLAlchemyEvents(
|
|
136
|
+
base,
|
|
137
|
+
engine,
|
|
138
|
+
autodiscover_paths,
|
|
139
|
+
logger=None
|
|
140
|
+
)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Parameters:
|
|
144
|
+
* **base** - SQLAlchemy declarative base class used to discover mapped models.
|
|
145
|
+
* **engine** - SQLAlchemy Engine or AsyncEngine instance.
|
|
146
|
+
* **autodiscover_paths** - List of Python module paths where event handlers are defined.
|
|
147
|
+
These modules are automatically imported so that decorators such as
|
|
148
|
+
`@sa_insert_handler`, `@sa_update_handler`, `@sa_delete_handler` are executed.
|
|
149
|
+
**Example:**
|
|
150
|
+
`autodiscover_paths=["services", "app.handlers"]`
|
|
151
|
+
|
|
152
|
+
### Important:
|
|
153
|
+
|
|
154
|
+
All modules containing event handlers must be imported through autodiscover
|
|
155
|
+
This ensures decorator registration is executed at startup
|
|
156
|
+
|
|
157
|
+
### logger (optional)
|
|
158
|
+
A standard Python logging.Logger instance.
|
|
159
|
+
|
|
160
|
+
If provided, the library will log internal lifecycle events such as:
|
|
161
|
+
|
|
162
|
+
* successful initialization
|
|
163
|
+
* listener startup
|
|
164
|
+
* trigger setup
|
|
165
|
+
|
|
166
|
+
**Example:**
|
|
167
|
+
```python
|
|
168
|
+
import logging
|
|
169
|
+
|
|
170
|
+
logger = logging.getLogger('sqlalchemy_events')
|
|
171
|
+
logger.setLevel(logging.INFO)
|
|
172
|
+
|
|
173
|
+
SQLAlchemyEvents(
|
|
174
|
+
base=Base,
|
|
175
|
+
engine=engine,
|
|
176
|
+
autodiscover_paths=['services'],
|
|
177
|
+
logger=logger
|
|
178
|
+
)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## How it works
|
|
182
|
+
1. autodiscover_paths modules are imported at startup
|
|
183
|
+
2. Decorators register event handlers into a global registry
|
|
184
|
+
3. Triggers send events via LISTEN/NOTIFY
|
|
185
|
+
4. The library receives notifications and dispatches them to registered handlers
|
|
186
|
+
|
|
187
|
+
## Notes
|
|
188
|
+
* Handlers can be both async and regular functions
|
|
189
|
+
* Only models registered with @with_events will emit events
|
|
190
|
+
* Currently supports LISTEN/NOTIFY
|
|
191
|
+
* Ensure handlers are imported via autodiscover_paths, otherwise they will not be registered
|
|
192
|
+
|
|
193
|
+
## Example flow
|
|
194
|
+
INSERT INTO/UPDATE/DELETE FROM {table} → DATABASE trigger fires →
|
|
195
|
+
NOTIFY → Python listener receives event →
|
|
196
|
+
handler is executed
|
|
197
|
+
|
|
198
|
+
## License
|
|
199
|
+
|
|
200
|
+
MIT
|
|
@@ -39,16 +39,35 @@ class UserModel(Base):
|
|
|
39
39
|
___
|
|
40
40
|
|
|
41
41
|
|
|
42
|
-
### Define event handlers using
|
|
42
|
+
### Define event handlers using decorators `@sa_insert_handler`, `@sa_update_handler`, `@sa_delete_handler`
|
|
43
43
|
services/handlers.py
|
|
44
44
|
```python
|
|
45
45
|
from models import UserModel
|
|
46
|
-
from sqlalchemy_events import
|
|
46
|
+
from sqlalchemy_events import sa_insert_handler
|
|
47
47
|
|
|
48
|
-
@
|
|
48
|
+
@sa_insert_handler(UserModel)
|
|
49
49
|
async def handle_user_insert():
|
|
50
|
-
print(
|
|
50
|
+
print('User inserted!')
|
|
51
51
|
```
|
|
52
|
+
|
|
53
|
+
**Optional Argument**: `rows`
|
|
54
|
+
Handlers can optionally accept a rows argument.
|
|
55
|
+
|
|
56
|
+
If the parameter is declared in the function signature, it will automatically receive a list of affected `row` IDs.
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from models import UserModel
|
|
60
|
+
from sqlalchemy_events import sa_insert_handler
|
|
61
|
+
|
|
62
|
+
@sa_insert_handler(UserModel)
|
|
63
|
+
async def handle_user_insert(rows: list[DB_ID]):
|
|
64
|
+
print('Users inserted!', rows)
|
|
65
|
+
```
|
|
66
|
+
**Output:**
|
|
67
|
+
```
|
|
68
|
+
Users inserted! ['3310aa38-f555-4c05-a57f-5acae32a0a7b', '6aeba1b0-8e80-4369-8adf-cf967ab7ba7a']
|
|
69
|
+
```
|
|
70
|
+
|
|
52
71
|
___
|
|
53
72
|
|
|
54
73
|
### Configure SQLAlchemy async engine
|
|
@@ -74,17 +93,13 @@ from session import engine
|
|
|
74
93
|
|
|
75
94
|
|
|
76
95
|
async def main():
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
autodiscover_paths=['services']
|
|
82
|
-
).init()
|
|
96
|
+
SQLAlchemyEvents(
|
|
97
|
+
base=Base,
|
|
98
|
+
engine=engine,
|
|
99
|
+
autodiscover_paths=['services']
|
|
83
100
|
)
|
|
84
|
-
|
|
101
|
+
while True:
|
|
85
102
|
await asyncio.sleep(9999)
|
|
86
|
-
except:
|
|
87
|
-
pass
|
|
88
103
|
|
|
89
104
|
if __name__ == '__main__':
|
|
90
105
|
asyncio.run(main())
|
|
@@ -107,7 +122,10 @@ Parameters:
|
|
|
107
122
|
* **base** - SQLAlchemy declarative base class used to discover mapped models.
|
|
108
123
|
* **engine** - SQLAlchemy Engine or AsyncEngine instance.
|
|
109
124
|
* **autodiscover_paths** - List of Python module paths where event handlers are defined.
|
|
110
|
-
These modules are automatically imported so that decorators such as
|
|
125
|
+
These modules are automatically imported so that decorators such as
|
|
126
|
+
`@sa_insert_handler`, `@sa_update_handler`, `@sa_delete_handler` are executed.
|
|
127
|
+
**Example:**
|
|
128
|
+
`autodiscover_paths=["services", "app.handlers"]`
|
|
111
129
|
|
|
112
130
|
### Important:
|
|
113
131
|
|
|
@@ -127,13 +145,13 @@ If provided, the library will log internal lifecycle events such as:
|
|
|
127
145
|
```python
|
|
128
146
|
import logging
|
|
129
147
|
|
|
130
|
-
logger = logging.getLogger(
|
|
148
|
+
logger = logging.getLogger('sqlalchemy_events')
|
|
131
149
|
logger.setLevel(logging.INFO)
|
|
132
150
|
|
|
133
151
|
SQLAlchemyEvents(
|
|
134
152
|
base=Base,
|
|
135
153
|
engine=engine,
|
|
136
|
-
autodiscover_paths=[
|
|
154
|
+
autodiscover_paths=['services'],
|
|
137
155
|
logger=logger
|
|
138
156
|
)
|
|
139
157
|
```
|
|
@@ -151,7 +169,7 @@ SQLAlchemyEvents(
|
|
|
151
169
|
* Ensure handlers are imported via autodiscover_paths, otherwise they will not be registered
|
|
152
170
|
|
|
153
171
|
## Example flow
|
|
154
|
-
INSERT INTO {table} → DATABASE trigger fires →
|
|
172
|
+
INSERT INTO/UPDATE/DELETE FROM {table} → DATABASE trigger fires →
|
|
155
173
|
NOTIFY → Python listener receives event →
|
|
156
174
|
handler is executed
|
|
157
175
|
|
|
@@ -1,35 +1,45 @@
|
|
|
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.
|
|
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
|
-
[
|
|
33
|
-
|
|
34
|
-
|
|
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.2.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
|
+
[project.optional-dependencies]
|
|
32
|
+
test = [
|
|
33
|
+
"pytest",
|
|
34
|
+
"pytest-asyncio",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://github.com/NormanwOw/SQLAlchemy-Events"
|
|
39
|
+
Repository = "https://github.com/NormanwOw/SQLAlchemy-Events"
|
|
40
|
+
Issues = "https://github.com/NormanwOw/SQLAlchemy-Events/issues"
|
|
41
|
+
|
|
42
|
+
[tool.pytest.ini_options]
|
|
43
|
+
asyncio_mode = "auto"
|
|
44
|
+
testpaths = ["src/tests"]
|
|
45
|
+
pythonpath = ["src"]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
[egg_info]
|
|
2
|
-
tag_build =
|
|
3
|
-
tag_date = 0
|
|
4
|
-
|
|
1
|
+
[egg_info]
|
|
2
|
+
tag_build =
|
|
3
|
+
tag_date = 0
|
|
4
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from .core import SQLAlchemyEvents
|
|
2
|
+
from .decorators import with_events
|
|
3
|
+
from .handlers import sa_insert_handler, sa_update_handler, sa_delete_handler
|
|
4
|
+
from .types import SaEvent
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
'SQLAlchemyEvents',
|
|
8
|
+
'sa_insert_handler',
|
|
9
|
+
'sa_update_handler',
|
|
10
|
+
'sa_delete_handler',
|
|
11
|
+
'with_events',
|
|
12
|
+
'SaEvent'
|
|
13
|
+
]
|
|
14
|
+
__version__ = '0.2.0'
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import inspect
|
|
4
|
+
import signal
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from .base import SaEventsCallbacksStrategy
|
|
8
|
+
from ..registry import get_event_handlers
|
|
9
|
+
from ..types import Handler
|
|
10
|
+
from ..utils import _process_pool
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
signal.signal(signal.SIGINT, lambda sig, frame: sys.exit(0))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PostgresCallback(SaEventsCallbacksStrategy):
|
|
17
|
+
|
|
18
|
+
def __init__(self) -> None:
|
|
19
|
+
self.__handlers = get_event_handlers()
|
|
20
|
+
|
|
21
|
+
async def handle(self, *args, **kwargs):
|
|
22
|
+
data = json.loads(args[3] or '{}')
|
|
23
|
+
trigger = data['op']
|
|
24
|
+
rows = data['rows']
|
|
25
|
+
trig_name = f'sa_{data['table']}_{trigger}_notify'
|
|
26
|
+
handlers: list[Handler] = self.__handlers.get(trig_name, [])
|
|
27
|
+
if not handlers:
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
loop = asyncio.get_running_loop()
|
|
31
|
+
tasks = []
|
|
32
|
+
|
|
33
|
+
for handler in handlers:
|
|
34
|
+
func = handler.func
|
|
35
|
+
sig = inspect.signature(func)
|
|
36
|
+
|
|
37
|
+
params = list(sig.parameters.values())
|
|
38
|
+
rows_index = None
|
|
39
|
+
|
|
40
|
+
for i, p in enumerate(params):
|
|
41
|
+
if p.name == 'rows':
|
|
42
|
+
rows_index = i
|
|
43
|
+
break
|
|
44
|
+
|
|
45
|
+
def build_args():
|
|
46
|
+
args = []
|
|
47
|
+
kwargs = {}
|
|
48
|
+
|
|
49
|
+
if rows_index is not None:
|
|
50
|
+
if not any(p.kind == inspect.Parameter.VAR_POSITIONAL for p in params):
|
|
51
|
+
args.insert(rows_index, rows)
|
|
52
|
+
else:
|
|
53
|
+
kwargs['rows'] = rows
|
|
54
|
+
return args, kwargs
|
|
55
|
+
|
|
56
|
+
args, kwargs = build_args()
|
|
57
|
+
|
|
58
|
+
if asyncio.iscoroutinefunction(func):
|
|
59
|
+
tasks.append(func(*args, **kwargs))
|
|
60
|
+
else:
|
|
61
|
+
tasks.append(
|
|
62
|
+
loop.run_in_executor(
|
|
63
|
+
_process_pool,
|
|
64
|
+
func,
|
|
65
|
+
*args,
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
await asyncio.gather(*tasks)
|