postgresql-notification-listener 1.0.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.
- postgresql_notification_listener-1.0.0/LICENSE +7 -0
- postgresql_notification_listener-1.0.0/PKG-INFO +53 -0
- postgresql_notification_listener-1.0.0/README.md +34 -0
- postgresql_notification_listener-1.0.0/postgresql_notification_listener/__init__.py +3 -0
- postgresql_notification_listener-1.0.0/postgresql_notification_listener/listener.py +130 -0
- postgresql_notification_listener-1.0.0/postgresql_notification_listener/types.py +4 -0
- postgresql_notification_listener-1.0.0/pyproject.toml +37 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright (c) 2024 Falcony.
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: postgresql-notification-listener
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Execute functions on PostgreSQL notifications
|
|
5
|
+
Home-page: https://github.com/tvuotila/postgresql-notification-listener
|
|
6
|
+
License: MIT
|
|
7
|
+
Author: Tero Vuotila
|
|
8
|
+
Author-email: tero.vuotila@falcony.io
|
|
9
|
+
Requires-Python: >=3.10,<4.0
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Requires-Dist: psycopg
|
|
16
|
+
Project-URL: Repository, https://github.com/tvuotila/postgresql-notification-listener
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# PostgreSQL Notification Listener
|
|
20
|
+
|
|
21
|
+
This project is a Python library that listens to notifications from a PostgreSQL database.
|
|
22
|
+
It provides a simple way to execute functions when specific events happen in the database.
|
|
23
|
+
|
|
24
|
+
## How it works
|
|
25
|
+
|
|
26
|
+
The listener connects to the PostgreSQL database and sets up a notification channel. You can then attach callbacks to this channel, which will be executed whenever a notification is received.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
To install the library, run:
|
|
31
|
+
pip install postgresql-notification-listener
|
|
32
|
+
|
|
33
|
+
### Usage
|
|
34
|
+
|
|
35
|
+
To use this library, follow these steps:
|
|
36
|
+
|
|
37
|
+
* Import the library in your Python script: `from postgresql_notification_listener import NotificationListener`
|
|
38
|
+
* Create instance of the listener `listener = NotificationListener("host=your_host port=your_port dbname=your_database user=your_username password=your_password")`
|
|
39
|
+
* Define a callback function that will be executed when a notification is received.
|
|
40
|
+
* Use the `subscribe_to_channel` method to attach your callback function to the notification channel: `listener.subscribe_to_channel("channel_to_listen", callback_function)`
|
|
41
|
+
* Start listening for notifications by calling the `start` method: `listener.start()`
|
|
42
|
+
* You can trigger a notification from PostgreSQL by `NOTIFY channel_to_listen` statement.
|
|
43
|
+
* The `start` method will call all attached callbacks once when called. If you don't want this behaviour, pass the `initial_run=False` argument to the start method: `listener.start(initial_run=False)`
|
|
44
|
+
* You can get the notification that caused the callback from the `last_notification` attribute on the listener instance `listener.last_notification`
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
### API
|
|
48
|
+
* **NotificationListener**: The main class of this library. It is responsible for setting up a notification channel and managing callbacks.
|
|
49
|
+
+ **subscribe_to_channel** : Attaches a callback function to a specific channel. The `subscribe_to_channel` method takes two required parameters: the name of the channel to listen to and the callback function to execute when a notification is received.
|
|
50
|
+
+ **start** : Starts listening for notifications. If you don't want all attached callbacks to be called once when called, pass `initial_run=False` as an argument.
|
|
51
|
+
+ **last_notification**: Returns the latest notification that caused a callback.
|
|
52
|
+
|
|
53
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# PostgreSQL Notification Listener
|
|
2
|
+
|
|
3
|
+
This project is a Python library that listens to notifications from a PostgreSQL database.
|
|
4
|
+
It provides a simple way to execute functions when specific events happen in the database.
|
|
5
|
+
|
|
6
|
+
## How it works
|
|
7
|
+
|
|
8
|
+
The listener connects to the PostgreSQL database and sets up a notification channel. You can then attach callbacks to this channel, which will be executed whenever a notification is received.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
To install the library, run:
|
|
13
|
+
pip install postgresql-notification-listener
|
|
14
|
+
|
|
15
|
+
### Usage
|
|
16
|
+
|
|
17
|
+
To use this library, follow these steps:
|
|
18
|
+
|
|
19
|
+
* Import the library in your Python script: `from postgresql_notification_listener import NotificationListener`
|
|
20
|
+
* Create instance of the listener `listener = NotificationListener("host=your_host port=your_port dbname=your_database user=your_username password=your_password")`
|
|
21
|
+
* Define a callback function that will be executed when a notification is received.
|
|
22
|
+
* Use the `subscribe_to_channel` method to attach your callback function to the notification channel: `listener.subscribe_to_channel("channel_to_listen", callback_function)`
|
|
23
|
+
* Start listening for notifications by calling the `start` method: `listener.start()`
|
|
24
|
+
* You can trigger a notification from PostgreSQL by `NOTIFY channel_to_listen` statement.
|
|
25
|
+
* The `start` method will call all attached callbacks once when called. If you don't want this behaviour, pass the `initial_run=False` argument to the start method: `listener.start(initial_run=False)`
|
|
26
|
+
* You can get the notification that caused the callback from the `last_notification` attribute on the listener instance `listener.last_notification`
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
### API
|
|
30
|
+
* **NotificationListener**: The main class of this library. It is responsible for setting up a notification channel and managing callbacks.
|
|
31
|
+
+ **subscribe_to_channel** : Attaches a callback function to a specific channel. The `subscribe_to_channel` method takes two required parameters: the name of the channel to listen to and the callback function to execute when a notification is received.
|
|
32
|
+
+ **start** : Starts listening for notifications. If you don't want all attached callbacks to be called once when called, pass `initial_run=False` as an argument.
|
|
33
|
+
+ **last_notification**: Returns the latest notification that caused a callback.
|
|
34
|
+
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from types import TracebackType
|
|
2
|
+
from typing import NoReturn, Self
|
|
3
|
+
|
|
4
|
+
import psycopg
|
|
5
|
+
from psycopg import sql, Notify
|
|
6
|
+
|
|
7
|
+
from .types import Callback
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NotificationListener:
|
|
11
|
+
"""
|
|
12
|
+
NotificationListener listens to notifications from a PostgreSQL database.
|
|
13
|
+
|
|
14
|
+
Callbacks can be attached to the listener to be called when notification is received.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, database_url: str) -> None:
|
|
18
|
+
# Stores latest Notify object in case callbacks need it
|
|
19
|
+
self.last_notification: Notify | None = None
|
|
20
|
+
self.connection = psycopg.connect(database_url, autocommit=True)
|
|
21
|
+
self.callbacks: dict[str, set[Callback]] = {}
|
|
22
|
+
|
|
23
|
+
## Context manager methods ##
|
|
24
|
+
|
|
25
|
+
def __enter__(self) -> Self:
|
|
26
|
+
return self
|
|
27
|
+
|
|
28
|
+
def __exit__(
|
|
29
|
+
self,
|
|
30
|
+
exc_type: type[BaseException] | None,
|
|
31
|
+
exc_value: BaseException | None,
|
|
32
|
+
traceback: TracebackType | None,
|
|
33
|
+
) -> None:
|
|
34
|
+
self.close()
|
|
35
|
+
|
|
36
|
+
def close(self) -> None:
|
|
37
|
+
self.connection.close()
|
|
38
|
+
|
|
39
|
+
## Subscription methods ##
|
|
40
|
+
|
|
41
|
+
def _get_or_create_listening_channel(self, channel: str) -> set[Callback]:
|
|
42
|
+
if channel not in self.callbacks:
|
|
43
|
+
self.callbacks[channel] = set()
|
|
44
|
+
self.connection.execute(
|
|
45
|
+
sql.SQL("LISTEN {}").format(sql.Identifier(channel))
|
|
46
|
+
)
|
|
47
|
+
return self.callbacks[channel]
|
|
48
|
+
|
|
49
|
+
def subscribe_to_channel(
|
|
50
|
+
self,
|
|
51
|
+
channel: str,
|
|
52
|
+
callback: Callback,
|
|
53
|
+
) -> None:
|
|
54
|
+
"""
|
|
55
|
+
Subscribes to a channel and associates a callback function with it.
|
|
56
|
+
If callback is already associated with the channel, old association will be overwritten.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
channel (str): The name of the channel to subscribe to.
|
|
60
|
+
callback (Callable): The callback function to be executed when a notification is received.
|
|
61
|
+
"""
|
|
62
|
+
self._get_or_create_listening_channel(channel).add(callback)
|
|
63
|
+
|
|
64
|
+
## Unsubscription methods ##
|
|
65
|
+
|
|
66
|
+
def unsubscribe_from_channel(self, channel: str, callback: Callback) -> None:
|
|
67
|
+
"""
|
|
68
|
+
Unsubscribe the specified callback function from a channel.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
channel (str): The channel to unsubscribe from.
|
|
72
|
+
callback (Callable): The callback function to remove.
|
|
73
|
+
|
|
74
|
+
Raises: KeyError if channel or callback is not subscribed
|
|
75
|
+
"""
|
|
76
|
+
self.callbacks[channel].remove(callback)
|
|
77
|
+
if not self.callbacks[channel]:
|
|
78
|
+
self.unsubscribe_channel(channel)
|
|
79
|
+
|
|
80
|
+
def unsubscribe_channel(self, channel: str) -> None:
|
|
81
|
+
"""
|
|
82
|
+
Unsubscribe all callback functions from a channel.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
channel (str): The channel to unsubscribe from.
|
|
86
|
+
|
|
87
|
+
Raises: KeyError if channel is not subscribed
|
|
88
|
+
"""
|
|
89
|
+
del self.callbacks[channel]
|
|
90
|
+
self.connection.execute(sql.SQL("UNLISTEN {}").format(sql.Identifier(channel)))
|
|
91
|
+
|
|
92
|
+
def unsubscribe_all(self) -> None:
|
|
93
|
+
"""
|
|
94
|
+
Unsubscribe all callback functions from all channels.
|
|
95
|
+
"""
|
|
96
|
+
for channel in list(self.callbacks):
|
|
97
|
+
self.unsubscribe_channel(channel)
|
|
98
|
+
|
|
99
|
+
## Listening methods ##
|
|
100
|
+
|
|
101
|
+
def start(self, initial_run: bool = True) -> NoReturn: # type: ignore[misc] # We never return
|
|
102
|
+
"""
|
|
103
|
+
Starts the notification listener and executes the callbacks for each received notification.
|
|
104
|
+
Duplicate notifications for a channel are ignored.
|
|
105
|
+
This function will never return.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
initial_run (bool): Execute all callbacks before listening starts.
|
|
109
|
+
This makes sure that no notifications are left unporcessed
|
|
110
|
+
while the notification listened was not running.
|
|
111
|
+
(default: True)
|
|
112
|
+
"""
|
|
113
|
+
if initial_run:
|
|
114
|
+
self.execute_all_callbacks()
|
|
115
|
+
self._notification_generator = self.connection.notifies()
|
|
116
|
+
for notification in self._notification_generator:
|
|
117
|
+
self.last_notification = notification
|
|
118
|
+
self.execute_callbacks(notification.channel)
|
|
119
|
+
|
|
120
|
+
## Execution methods ##
|
|
121
|
+
def execute_all_callbacks(self) -> None:
|
|
122
|
+
for channel in self.callbacks:
|
|
123
|
+
self.execute_callbacks(channel)
|
|
124
|
+
|
|
125
|
+
def execute_callbacks(self, channel: str) -> None:
|
|
126
|
+
if channel not in self.callbacks:
|
|
127
|
+
return
|
|
128
|
+
# Use a copy to allow callbacks to be removed during iteration
|
|
129
|
+
for callback in self.callbacks[channel].copy():
|
|
130
|
+
callback()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "postgresql-notification-listener"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Execute functions on PostgreSQL notifications"
|
|
5
|
+
authors = ["Tero Vuotila <tero.vuotila@falcony.io>"]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
repository = "https://github.com/tvuotila/postgresql-notification-listener"
|
|
9
|
+
|
|
10
|
+
[tool.poetry.dependencies]
|
|
11
|
+
python = "^3.10"
|
|
12
|
+
psycopg = "*"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
[tool.poetry.group.dev.dependencies]
|
|
16
|
+
mypy = "*"
|
|
17
|
+
ruff = "*"
|
|
18
|
+
black = "*"
|
|
19
|
+
pytest = "*"
|
|
20
|
+
pytest-timeout = "*"
|
|
21
|
+
|
|
22
|
+
[build-system]
|
|
23
|
+
requires = ["poetry-core"]
|
|
24
|
+
build-backend = "poetry.core.masonry.api"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
[tool.mypy]
|
|
28
|
+
exclude = ["dist"]
|
|
29
|
+
warn_unused_ignores = true
|
|
30
|
+
show_error_codes = true
|
|
31
|
+
strict = true
|
|
32
|
+
|
|
33
|
+
[[tool.mypy.overrides]]
|
|
34
|
+
module = "tests.*"
|
|
35
|
+
check_untyped_defs = true
|
|
36
|
+
disallow_untyped_defs = false
|
|
37
|
+
disallow_untyped_calls = false
|