sqlalchemy-events-lib 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,8 @@
1
+ from .core import SQLAlchemyEvents
2
+ from .decorators import with_events
3
+ from .enums import SaEvent
4
+ from .handler import sa_event_handler
5
+
6
+ __all__ = ['SQLAlchemyEvents', 'sa_event_handler', 'with_events', 'SaEvent']
7
+ __version__ = '0.1.0'
8
+
File without changes
@@ -0,0 +1,8 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+
4
+ class SaEventsCallbacksStrategy(ABC):
5
+
6
+ @abstractmethod
7
+ async def handle(self, *args, **kwargs):
8
+ raise NotImplementedError
@@ -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,14 @@
1
+ from .enums import SaEvent
2
+
3
+
4
+ def with_events(events: list[SaEvent]):
5
+ def wrapper(cls):
6
+ cls.__events__ = set(events)
7
+
8
+ class Events:
9
+ for e in events:
10
+ locals()[e.name] = e.value
11
+
12
+ cls.events = Events
13
+ return cls
14
+ return wrapper
@@ -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
+
@@ -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,21 @@
1
+ sqlalchemy_events/__init__.py,sha256=XTsL2UXIGOeR1t23L_BWmV3avg9BpVTjLQHcU9iKzr4,245
2
+ sqlalchemy_events/core.py,sha256=EsR7rxCiCup66Xx8DVMyGr9cvLMRkfV8wG7wvPbbziA,4165
3
+ sqlalchemy_events/decorators.py,sha256=uNIHMaReqUCB_y_ndkaYa2YLQj4KZ6EGDuhzsWMaBFE,302
4
+ sqlalchemy_events/discovery.py,sha256=NjiLWxdVvlM8LWdhLRXtNHvLDDxrtQ-Upuy1Ha-2zRM,633
5
+ sqlalchemy_events/enums.py,sha256=NC-yvTJ1xB_RdPg0uI-z8-C0EQKQLSn9l2om5Lo46_4,270
6
+ sqlalchemy_events/events.py,sha256=F1CEQI3vmaJpfxP2bCqATersNFx_TB6sbHLFz6GfsbA,983
7
+ sqlalchemy_events/exceptions.py,sha256=CRUdzA-V1gAQYGITwwwRJhauosG6iwjVAMG0QCB48gA,45
8
+ sqlalchemy_events/handler.py,sha256=mP04ykFUAKJ3651LWjg8JfBozOFh_y3VFv4OMuONQPQ,1187
9
+ sqlalchemy_events/registry.py,sha256=8a9yqk0oC0DzHOQ26fh6o9T9cZCWnOPz1vQdJnkzYDs,295
10
+ sqlalchemy_events/utils.py,sha256=bWDCouY-FJmK359q_DBSOXfeo1YdyyypPmZCyocEExM,834
11
+ sqlalchemy_events/callbacks_strategies/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ sqlalchemy_events/callbacks_strategies/base.py,sha256=FLnRvmpnAE99qyGx5dXH3Egtn7cvkuhbC64IwJCrkvw,182
13
+ sqlalchemy_events/callbacks_strategies/postgres_callback.py,sha256=87_bT25xoTDEKdp6ASYot4bBIDDCY6BYlVntoevzn5Y,710
14
+ sqlalchemy_events/init_triggers_strategies/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ sqlalchemy_events/init_triggers_strategies/base.py,sha256=nuUL_HTJW1Shf7RSD0-PjDabNHoCtZffcS33e6doGuE,294
16
+ sqlalchemy_events/init_triggers_strategies/postgres_init_triggers.py,sha256=gv873n5tCyrih397hmfJVdj4PCnoCn4MZJvRDmWinLg,3266
17
+ sqlalchemy_events_lib-0.1.0.dist-info/licenses/LICENSE,sha256=-4dAsEvyIMLC8pS68pJqK6ks5sRpNrtBzTBxltHcaCE,132
18
+ sqlalchemy_events_lib-0.1.0.dist-info/METADATA,sha256=CUHfeoAQRy_5T6rLC72RWhkewOFrZwvAkBYqZz2GBuk,4883
19
+ sqlalchemy_events_lib-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
20
+ sqlalchemy_events_lib-0.1.0.dist-info/top_level.txt,sha256=fYKcnbBujkHy1oF2GEJJyxGw0Kr0-ZwLbU35PDbrE3w,18
21
+ sqlalchemy_events_lib-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,5 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alexey Kostarev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy...
@@ -0,0 +1 @@
1
+ sqlalchemy_events