sqlnotify 0.1.0__tar.gz → 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. {sqlnotify-0.1.0/src/sqlnotify.egg-info → sqlnotify-0.1.1}/PKG-INFO +3 -3
  2. {sqlnotify-0.1.0 → sqlnotify-0.1.1}/README.md +2 -2
  3. {sqlnotify-0.1.0 → sqlnotify-0.1.1}/pyproject.toml +4 -5
  4. {sqlnotify-0.1.0 → sqlnotify-0.1.1}/src/sqlnotify/constants.py +2 -2
  5. {sqlnotify-0.1.0 → sqlnotify-0.1.1}/src/sqlnotify/notifiers/notifier.py +10 -2
  6. {sqlnotify-0.1.0 → sqlnotify-0.1.1}/src/sqlnotify/utils.py +71 -3
  7. {sqlnotify-0.1.0 → sqlnotify-0.1.1/src/sqlnotify.egg-info}/PKG-INFO +3 -3
  8. {sqlnotify-0.1.0 → sqlnotify-0.1.1}/LICENSE +0 -0
  9. {sqlnotify-0.1.0 → sqlnotify-0.1.1}/setup.cfg +0 -0
  10. {sqlnotify-0.1.0 → sqlnotify-0.1.1}/src/sqlnotify/__init__.py +0 -0
  11. {sqlnotify-0.1.0 → sqlnotify-0.1.1}/src/sqlnotify/adapters/__init__.py +0 -0
  12. {sqlnotify-0.1.0 → sqlnotify-0.1.1}/src/sqlnotify/adapters/asgi.py +0 -0
  13. {sqlnotify-0.1.0 → sqlnotify-0.1.1}/src/sqlnotify/dialects/__init__.py +0 -0
  14. {sqlnotify-0.1.0 → sqlnotify-0.1.1}/src/sqlnotify/dialects/base.py +0 -0
  15. {sqlnotify-0.1.0 → sqlnotify-0.1.1}/src/sqlnotify/dialects/postgresql.py +0 -0
  16. {sqlnotify-0.1.0 → sqlnotify-0.1.1}/src/sqlnotify/dialects/sqlite.py +0 -0
  17. {sqlnotify-0.1.0 → sqlnotify-0.1.1}/src/sqlnotify/dialects/utils.py +0 -0
  18. {sqlnotify-0.1.0 → sqlnotify-0.1.1}/src/sqlnotify/exceptions.py +0 -0
  19. {sqlnotify-0.1.0 → sqlnotify-0.1.1}/src/sqlnotify/logger.py +0 -0
  20. {sqlnotify-0.1.0 → sqlnotify-0.1.1}/src/sqlnotify/notifiers/__init__.py +0 -0
  21. {sqlnotify-0.1.0 → sqlnotify-0.1.1}/src/sqlnotify/notifiers/base.py +0 -0
  22. {sqlnotify-0.1.0 → sqlnotify-0.1.1}/src/sqlnotify/types.py +0 -0
  23. {sqlnotify-0.1.0 → sqlnotify-0.1.1}/src/sqlnotify/watcher.py +0 -0
  24. {sqlnotify-0.1.0 → sqlnotify-0.1.1}/src/sqlnotify.egg-info/SOURCES.txt +0 -0
  25. {sqlnotify-0.1.0 → sqlnotify-0.1.1}/src/sqlnotify.egg-info/dependency_links.txt +0 -0
  26. {sqlnotify-0.1.0 → sqlnotify-0.1.1}/src/sqlnotify.egg-info/entry_points.txt +0 -0
  27. {sqlnotify-0.1.0 → sqlnotify-0.1.1}/src/sqlnotify.egg-info/requires.txt +0 -0
  28. {sqlnotify-0.1.0 → sqlnotify-0.1.1}/src/sqlnotify.egg-info/top_level.txt +0 -0
  29. {sqlnotify-0.1.0 → sqlnotify-0.1.1}/tests/test_postgres_dialect.py +0 -0
  30. {sqlnotify-0.1.0 → sqlnotify-0.1.1}/tests/test_postgres_notifier.py +0 -0
  31. {sqlnotify-0.1.0 → sqlnotify-0.1.1}/tests/test_postgres_notifier_asgi_adapter.py +0 -0
  32. {sqlnotify-0.1.0 → sqlnotify-0.1.1}/tests/test_sqlite_dialect.py +0 -0
  33. {sqlnotify-0.1.0 → sqlnotify-0.1.1}/tests/test_sqlite_notifier.py +0 -0
  34. {sqlnotify-0.1.0 → sqlnotify-0.1.1}/tests/test_sqlite_notifier_asgi_adapter.py +0 -0
  35. {sqlnotify-0.1.0 → sqlnotify-0.1.1}/tests/test_watcher.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlnotify
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: A near real-time SQL notification library for database changes, supporting PostgreSQL and SQLite.
5
5
  Author-email: Daniel Brai <danielbrai.dev@gmail.com>
6
6
  License-Expression: MIT
@@ -359,7 +359,7 @@ await notifier.anotify(
359
359
  # notifier.notify(User, Operation.UPDATE, payload={"id": 123})
360
360
  ```
361
361
 
362
- **Note**: The `notify`/`anotify` methods validate payload size and can use overflow tables for large payloads. If `use_overflow_table=True`, payloads exceeding NOTIFY limit are automatically stored in the overflow table, and only an overflow ID is sent through the notification channel.
362
+ **Note**: The `notify`/`anotify` methods validate payload size and can use overflow tables for large payloads. If `use_overflow_table=True`, payloads exceeding SQLNotify limit are automatically stored in the overflow table, and only an overflow ID is sent through the notification channel.
363
363
 
364
364
  ### Model Change Detection
365
365
 
@@ -584,7 +584,7 @@ notifier.start() # Synchronous start
584
584
  2. **Trigger Creation** - The dialect creates database-specific triggers on your tables
585
585
  3. **Change Detection** - When data changes, triggers fire and call notification functions
586
586
  4. **NOTIFY** - Functions use database-native notification mechanisms (e.g., PostgreSQL's NOTIFY)
587
- 5. **LISTEN** - SQLNotify maintains a dedicated connection listening for notifications
587
+ 5. **LISTEN** - SQLNotify maintains a dedicated connection listening for notifications or polls for changes (SQLite)
588
588
  6. **Event Distribution** - Incoming notifications are routed to subscribed callbacks
589
589
  7. **Callback Execution** - Your subscriber functions are called with change events
590
590
 
@@ -323,7 +323,7 @@ await notifier.anotify(
323
323
  # notifier.notify(User, Operation.UPDATE, payload={"id": 123})
324
324
  ```
325
325
 
326
- **Note**: The `notify`/`anotify` methods validate payload size and can use overflow tables for large payloads. If `use_overflow_table=True`, payloads exceeding NOTIFY limit are automatically stored in the overflow table, and only an overflow ID is sent through the notification channel.
326
+ **Note**: The `notify`/`anotify` methods validate payload size and can use overflow tables for large payloads. If `use_overflow_table=True`, payloads exceeding SQLNotify limit are automatically stored in the overflow table, and only an overflow ID is sent through the notification channel.
327
327
 
328
328
  ### Model Change Detection
329
329
 
@@ -548,7 +548,7 @@ notifier.start() # Synchronous start
548
548
  2. **Trigger Creation** - The dialect creates database-specific triggers on your tables
549
549
  3. **Change Detection** - When data changes, triggers fire and call notification functions
550
550
  4. **NOTIFY** - Functions use database-native notification mechanisms (e.g., PostgreSQL's NOTIFY)
551
- 5. **LISTEN** - SQLNotify maintains a dedicated connection listening for notifications
551
+ 5. **LISTEN** - SQLNotify maintains a dedicated connection listening for notifications or polls for changes (SQLite)
552
552
  6. **Event Distribution** - Incoming notifications are routed to subscribed callbacks
553
553
  7. **Callback Execution** - Your subscriber functions are called with change events
554
554
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlnotify"
3
- version = "0.1.0"
3
+ version = "0.1.1"
4
4
  description = "A near real-time SQL notification library for database changes, supporting PostgreSQL and SQLite."
5
5
  readme = { file = "README.md", content-type = "text/markdown" }
6
6
  license = "MIT"
@@ -44,14 +44,13 @@ dependencies = ["sqlalchemy>=2.0.0", "asyncpg>=0.31.0"]
44
44
  [dependency-groups]
45
45
  test = [
46
46
  "pre-commit>=4.5.1",
47
- "coverage>=7.13.3",
47
+ "coverage>=7.13.4",
48
48
  "pytest>=9.0.2",
49
49
  "pytest-cov>=7.0.0",
50
50
  "pytest-xdist>=3.8.0",
51
51
  "pytest-asyncio>=1.3.0",
52
- "psycopg[binary]>=3.3.2",
53
- "sqlmodel>=0.0.25",
54
- "httpx>=0.28.0",
52
+ "psycopg[binary]==3.3.2",
53
+ "sqlmodel==0.0.33",
55
54
  ]
56
55
 
57
56
 
@@ -1,8 +1,8 @@
1
1
  PACKAGE_NAME = "sqlnotify"
2
2
 
3
- MAX_SQLNOTIFY_PAYLOAD_BYTES = 7999 # PostgreSQL NOTIFY payload limit (8000 - 1 for terminator)
3
+ MAX_SQLNOTIFY_PAYLOAD_BYTES = 7999 # SQLNotify payload limit (8000 - 1 for terminator)
4
4
 
5
- MAX_SQLNOTIFY_IDENTIFER_BYTES = 63 # PostgreSQL identifier limit (63 bytes)
5
+ MAX_SQLNOTIFY_IDENTIFER_BYTES = 63 # SQLNotify identifier limit (63 bytes)
6
6
 
7
7
  MAX_SQLNOTIFY_EXTRA_COLUMNS = 5 # Limit extra columns to help stay within payload size limit, but this is not a hard limit since column data size can vary greatly
8
8
 
@@ -10,7 +10,7 @@ from ..dialects import BaseDialect, get_dialect_for_engine
10
10
  from ..exceptions import SQLNotifyConfigurationError
11
11
  from ..logger import get_logger
12
12
  from ..types import ChangeEvent, Operation
13
- from ..utils import extract_database_url, strip_database_query_params
13
+ from ..utils import extract_database_url, strip_database_query_params, wrap_unhandled_error
14
14
  from ..watcher import Watcher
15
15
  from .base import BaseNotifier
16
16
 
@@ -135,6 +135,7 @@ class Notifier(BaseNotifier):
135
135
  def is_running(self) -> bool:
136
136
  return self._running
137
137
 
138
+ @wrap_unhandled_error(lambda self: self._logger)
138
139
  def start(self) -> None:
139
140
  """
140
141
  Start watching for database changes synchronously.
@@ -151,6 +152,7 @@ class Notifier(BaseNotifier):
151
152
 
152
153
  self._start_sync()
153
154
 
155
+ @wrap_unhandled_error(lambda self: self._logger)
154
156
  async def astart(self) -> None:
155
157
  """
156
158
  Start watching for database changes asynchronously.
@@ -203,6 +205,7 @@ class Notifier(BaseNotifier):
203
205
  if self._logger:
204
206
  self._logger.info(f"SQLNotify Notifier started (sync mode, dialect: {self.dialect_name})")
205
207
 
208
+ @wrap_unhandled_error(lambda self: self._logger)
206
209
  def stop(self) -> None:
207
210
  """
208
211
  Stop watching for database changes synchronously
@@ -219,6 +222,7 @@ class Notifier(BaseNotifier):
219
222
 
220
223
  self._stop_sync()
221
224
 
225
+ @wrap_unhandled_error(lambda self: self._logger)
222
226
  async def astop(self) -> None:
223
227
  """
224
228
  Stop watching for database changes asynchronously.
@@ -261,6 +265,7 @@ class Notifier(BaseNotifier):
261
265
  if self._logger:
262
266
  self._logger.info("SQLNotify Notifier stopped (sync mode)")
263
267
 
268
+ @wrap_unhandled_error(lambda self: self._logger, reraise=False)
264
269
  def cleanup(self) -> None:
265
270
  """
266
271
  Remove all triggers and functions from the database synchronously
@@ -277,6 +282,7 @@ class Notifier(BaseNotifier):
277
282
 
278
283
  self._cleanup_sync()
279
284
 
285
+ @wrap_unhandled_error(lambda self: self._logger, reraise=False)
280
286
  async def acleanup(self) -> None:
281
287
  """
282
288
  Remove all triggers and functions from the database asynchronously.
@@ -311,6 +317,7 @@ class Notifier(BaseNotifier):
311
317
  if self._logger:
312
318
  self._logger.info("SQLNotify Notifier cleaned up all triggers and functions (sync mode)")
313
319
 
320
+ @wrap_unhandled_error(lambda self: self._logger)
314
321
  def notify(
315
322
  self,
316
323
  model: type | str,
@@ -345,6 +352,7 @@ class Notifier(BaseNotifier):
345
352
 
346
353
  self._notify_sync(watcher, payload, use_overflow_table)
347
354
 
355
+ @wrap_unhandled_error(lambda self: self._logger)
348
356
  async def anotify(
349
357
  self,
350
358
  model: type | str,
@@ -580,7 +588,7 @@ class Notifier(BaseNotifier):
580
588
 
581
589
  Args:
582
590
  watcher (Watcher): The watcher that triggered
583
- payload (dict): The notification payload
591
+ payload (dict[str, Any]): The notification payload
584
592
  """
585
593
 
586
594
  if len(watcher.primary_keys) == 1:
@@ -1,21 +1,27 @@
1
+ import inspect
2
+ import logging
3
+ from collections.abc import Callable
4
+ from functools import wraps
5
+ from typing import Any
6
+
1
7
  from sqlalchemy.engine import Engine
2
8
  from sqlalchemy.ext.asyncio import AsyncEngine
3
9
 
4
10
  from .constants import MAX_SQLNOTIFY_IDENTIFER_BYTES, MAX_SQLNOTIFY_PAYLOAD_BYTES
5
- from .exceptions import SQLNotifyIdentifierSizeError, SQLNotifyPayloadSizeError
11
+ from .exceptions import SQLNotifyException, SQLNotifyIdentifierSizeError, SQLNotifyPayloadSizeError
6
12
 
7
13
 
8
14
  def extract_database_url(engine: AsyncEngine | Engine) -> str:
9
15
  """
10
16
  Extract the database URL from an engine for use with asyncpg LISTEN.
11
17
 
12
- Converts SQLAlchemy URL format to plain PostgreSQL URL suitable for asyncpg.
18
+ Converts SQLAlchemy URL format to raw database DSN.
13
19
 
14
20
  Args:
15
21
  engine (Union[AsyncEngine, Engine]): SQLAlchemy Engine or AsyncEngine
16
22
 
17
23
  Returns:
18
- str: PostgreSQL connection URL suitable for asyncpg
24
+ str: Database connection URL
19
25
  """
20
26
 
21
27
  url = engine.url.render_as_string(hide_password=False)
@@ -163,3 +169,65 @@ def validate_identifier_size(
163
169
  raise SQLNotifyIdentifierSizeError(combined)
164
170
 
165
171
  return True
172
+
173
+
174
+ def wrap_unhandled_error(
175
+ logger_getter: Callable[..., logging.Logger] | None = None,
176
+ reraise=True,
177
+ ):
178
+ """
179
+ Decorator factory that wraps unhandled exceptions and converts them to SQLNotifyException
180
+ """
181
+
182
+ def _get_logger_from_call(*args, **kwargs):
183
+ if callable(logger_getter):
184
+ try:
185
+ return logger_getter(*args, **kwargs)
186
+ except Exception:
187
+ return None
188
+
189
+ return logger_getter
190
+
191
+ def decorator(func: Callable[..., Any]):
192
+
193
+ if inspect.iscoroutinefunction(func):
194
+
195
+ @wraps(func)
196
+ async def async_wrapper(*args, **kwargs):
197
+ try:
198
+ return await func(*args, **kwargs)
199
+ except SQLNotifyException:
200
+ raise
201
+ except Exception as e:
202
+ logger = _get_logger_from_call(*args, **kwargs)
203
+ if logger:
204
+ logger.exception(f"Unhandled exception in {func.__name__}")
205
+
206
+ if reraise:
207
+ raise SQLNotifyException(f"SQLNotify unhandled exception caught. Error: {str(e)}") from e
208
+ else:
209
+ return None
210
+
211
+ return async_wrapper
212
+
213
+ else:
214
+
215
+ @wraps(func)
216
+ def sync_wrapper(*args, **kwargs):
217
+ try:
218
+ return func(*args, **kwargs)
219
+ except SQLNotifyException:
220
+ raise
221
+ except Exception as e:
222
+ logger = _get_logger_from_call(*args, **kwargs)
223
+ if logger:
224
+ logger.exception(f"Unhandled exception in {func.__name__}")
225
+
226
+ if reraise:
227
+ raise SQLNotifyException(f"SQLNotify unhandled exception caught. Error: {str(e)}") from e
228
+ else:
229
+ return None
230
+
231
+ return sync_wrapper
232
+
233
+ return decorator
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlnotify
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: A near real-time SQL notification library for database changes, supporting PostgreSQL and SQLite.
5
5
  Author-email: Daniel Brai <danielbrai.dev@gmail.com>
6
6
  License-Expression: MIT
@@ -359,7 +359,7 @@ await notifier.anotify(
359
359
  # notifier.notify(User, Operation.UPDATE, payload={"id": 123})
360
360
  ```
361
361
 
362
- **Note**: The `notify`/`anotify` methods validate payload size and can use overflow tables for large payloads. If `use_overflow_table=True`, payloads exceeding NOTIFY limit are automatically stored in the overflow table, and only an overflow ID is sent through the notification channel.
362
+ **Note**: The `notify`/`anotify` methods validate payload size and can use overflow tables for large payloads. If `use_overflow_table=True`, payloads exceeding SQLNotify limit are automatically stored in the overflow table, and only an overflow ID is sent through the notification channel.
363
363
 
364
364
  ### Model Change Detection
365
365
 
@@ -584,7 +584,7 @@ notifier.start() # Synchronous start
584
584
  2. **Trigger Creation** - The dialect creates database-specific triggers on your tables
585
585
  3. **Change Detection** - When data changes, triggers fire and call notification functions
586
586
  4. **NOTIFY** - Functions use database-native notification mechanisms (e.g., PostgreSQL's NOTIFY)
587
- 5. **LISTEN** - SQLNotify maintains a dedicated connection listening for notifications
587
+ 5. **LISTEN** - SQLNotify maintains a dedicated connection listening for notifications or polls for changes (SQLite)
588
588
  6. **Event Distribution** - Incoming notifications are routed to subscribed callbacks
589
589
  7. **Callback Execution** - Your subscriber functions are called with change events
590
590
 
File without changes
File without changes