iddqueue 0.13.0rc1__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.
iddqueue/utils.py ADDED
@@ -0,0 +1,206 @@
1
+ import functools
2
+ import json
3
+ import logging
4
+ from contextlib import ExitStack, contextmanager
5
+ from hashlib import sha256
6
+ from urllib.parse import parse_qsl, urlparse
7
+
8
+ import tenacity
9
+ from dramatiq import Message, MessageProxy, get_encoder
10
+ from dramatiq.errors import BrokerConnectionError
11
+ from psycopg import InterfaceError, OperationalError, sql
12
+ from psycopg.conninfo import conninfo_to_dict, make_conninfo
13
+ from psycopg.errors import AdminShutdown
14
+ from psycopg_pool import ConnectionPool
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ DISCONNECT_ERRORS = (
20
+ AdminShutdown,
21
+ InterfaceError,
22
+ OperationalError,
23
+ )
24
+
25
+
26
+ retry_pg = tenacity.retry(
27
+ retry=tenacity.retry_if_exception_type(
28
+ DISCONNECT_ERRORS + (BrokerConnectionError,)
29
+ ),
30
+ reraise=True,
31
+ wait=tenacity.wait_random_exponential(multiplier=1, max=30),
32
+ stop=tenacity.stop_after_attempt(10),
33
+ before_sleep=tenacity.before_sleep_log(logger, logging.INFO),
34
+ )
35
+
36
+
37
+ def check_conn(conn):
38
+ try:
39
+ ConnectionPool.check_connection(conn)
40
+ except DISCONNECT_ERRORS as e:
41
+ if not conn.closed:
42
+ logger.debug("Closing connexion due to error: %s", e)
43
+ try:
44
+ conn.close()
45
+ except Exception as close_e:
46
+ logger.debug("Failed to close connexion: %s", close_e)
47
+ raise BrokerConnectionError(str(e)) from None
48
+ return conn
49
+
50
+
51
+ @retry_pg
52
+ def getconn(pool):
53
+ # Get a reliable connection to Postgres.
54
+ if pool.closed:
55
+ pool.open()
56
+ conn = pool.getconn()
57
+ try:
58
+ check_conn(conn)
59
+ except BrokerConnectionError:
60
+ pool.putconn(conn)
61
+ raise # Let tenacity control retry.
62
+ return conn
63
+
64
+
65
+ def make_pool(url, maxconn=16):
66
+ if isinstance(url, str):
67
+ if "://" in url:
68
+ parts = urlparse(url)
69
+ kwargs = dict(parse_qsl(parts.query))
70
+ conninfo = url.split("?", 1)[0]
71
+ pool_options = {
72
+ key: kwargs.pop(key) for key in ("minconn", "maxconn") if key in kwargs
73
+ }
74
+ kwargs = conninfo_to_dict(conninfo, **kwargs)
75
+ kwargs.update(pool_options)
76
+ else:
77
+ kwargs = conninfo_to_dict(url)
78
+ else:
79
+ kwargs = dict(url)
80
+
81
+ maxconn = int(kwargs.pop("maxconn", maxconn))
82
+ minconn = int(kwargs.pop("minconn", 0))
83
+ kwargs.setdefault("application_name", "iddqueue")
84
+ kwargs.setdefault("keepalives", "1")
85
+ kwargs.setdefault("keepalives_count", "2")
86
+ kwargs.setdefault("keepalives_idle", "5")
87
+ kwargs.setdefault("keepalives_interval", "2")
88
+ return ConnectionPool(
89
+ make_conninfo(**kwargs),
90
+ min_size=minconn,
91
+ max_size=maxconn,
92
+ kwargs={"autocommit": True},
93
+ open=False,
94
+ check=ConnectionPool.check_connection,
95
+ )
96
+
97
+
98
+ def raise_connection_error(fn):
99
+ # Raises Dramatiq connection error on Psycopg error
100
+
101
+ @functools.wraps(fn)
102
+ def wrapper(*a, **kw):
103
+ try:
104
+ return fn(*a, **kw)
105
+ except DISCONNECT_ERRORS as e:
106
+ raise BrokerConnectionError(str(e))
107
+
108
+ return wrapper
109
+
110
+
111
+ def quote_ident(raw):
112
+ # Quote an SQL identifier, free from a connection object.
113
+ return '"%s"' % raw.replace('"', '""')
114
+
115
+
116
+ def unlisten_all(conn):
117
+ if not conn.closed:
118
+ try:
119
+ with conn.cursor() as cur:
120
+ cur.execute("UNLISTEN *")
121
+ # Discard notifications buffered before UNLISTEN.
122
+ list(conn.notifies(timeout=0))
123
+ except DISCONNECT_ERRORS:
124
+ conn.close()
125
+
126
+
127
+ @contextmanager
128
+ def transaction(conn_or_pool, listen=None):
129
+ with ExitStack() as defer:
130
+ if hasattr(conn_or_pool, "getconn"):
131
+ conn = getconn(conn_or_pool)
132
+ defer.callback(conn_or_pool.putconn, conn)
133
+ else:
134
+ conn = conn_or_pool
135
+
136
+ if listen:
137
+ autocommit = conn.autocommit
138
+ conn.autocommit = True
139
+ try:
140
+ with conn.cursor() as curs:
141
+ curs.execute(sql.SQL("LISTEN {}").format(sql.Identifier(listen)))
142
+ try:
143
+ yield curs
144
+ finally:
145
+ unlisten_all(conn)
146
+ finally:
147
+ if not conn.closed:
148
+ conn.autocommit = autocommit
149
+ else:
150
+ with conn.transaction(), conn.cursor() as curs:
151
+ yield curs
152
+
153
+
154
+ def wait_for_notifies(conn, timeout=1):
155
+ # Receive one batch without waiting for the entire timeout after a notify.
156
+ return list(conn.notifies(timeout=timeout, stop_after=1))
157
+
158
+
159
+
160
+ def storage_namespace(schema="dramatiq", prefix=""):
161
+ # PostgreSQL identifiers cannot contain NUL, so components cannot overlap.
162
+ return schema + "\0" + prefix + "\0"
163
+
164
+
165
+ def notification_channel(key, event, *, schema="dramatiq", prefix=""):
166
+ legacy = f"dramatiq.{key}.{event}"
167
+ if schema == "dramatiq" and not prefix and len(legacy.encode()) <= 63:
168
+ return legacy
169
+ digest = sha256((storage_namespace(schema, prefix) + str(key) + "\0" + event).encode()).hexdigest()
170
+ return "dpg." + digest[:48]
171
+
172
+ class QueryManager:
173
+ def __init__(self, queries, schema="dramatiq", prefix=""):
174
+ self.queries = queries
175
+ self.schema = schema
176
+ self.prefix = prefix
177
+ self.build_queries(schema, prefix)
178
+
179
+ def channel(self, key, event):
180
+ return notification_channel(key, event, schema=self.schema, prefix=self.prefix)
181
+
182
+ def build_queries(self, schema=None, prefix=None):
183
+ schema = self.schema if schema is None else schema
184
+ prefix = self.prefix if prefix is None else prefix
185
+ self.schema, self.prefix = schema, prefix
186
+
187
+ for name, query in self.queries.items():
188
+ setattr(
189
+ self,
190
+ name,
191
+ query.format(
192
+ schema=quote_ident(schema),
193
+ tablename=quote_ident(prefix + "queue"),
194
+ ),
195
+ )
196
+
197
+
198
+ def tidy4json(data):
199
+ if isinstance(data, (Message, MessageProxy)):
200
+ # Translate python data into decoded json.
201
+ # Encode message using Dramatiq encoder. But immediatly decode it as
202
+ # standard json to send native json to PostgreSQL.
203
+ # e.g. date formating problem
204
+ return json.loads(data.encode())
205
+ else:
206
+ return json.loads(get_encoder().encode(data))