workercommon 0.4.1__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.
- workercommon/__init__.py +7 -0
- workercommon/background.py +72 -0
- workercommon/commands.py +42 -0
- workercommon/config.py +81 -0
- workercommon/connectionpool.py +97 -0
- workercommon/database/__init__.py +16 -0
- workercommon/database/base.py +173 -0
- workercommon/database/pg.py +278 -0
- workercommon/database/py.typed +0 -0
- workercommon/locking.py +52 -0
- workercommon/py.typed +0 -0
- workercommon/rabbitmqueue.py +455 -0
- workercommon/test.py +177 -0
- workercommon/worker.py +347 -0
- workercommon-0.4.1.dist-info/METADATA +25 -0
- workercommon-0.4.1.dist-info/RECORD +19 -0
- workercommon-0.4.1.dist-info/WHEEL +5 -0
- workercommon-0.4.1.dist-info/licenses/LICENSE.TXT +9 -0
- workercommon-0.4.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
if sys.version_info < (3, 8):
|
|
5
|
+
raise RuntimeError("At least Python 3.8 is required")
|
|
6
|
+
|
|
7
|
+
from . import base
|
|
8
|
+
from typing import (
|
|
9
|
+
Optional,
|
|
10
|
+
Any,
|
|
11
|
+
List,
|
|
12
|
+
Dict,
|
|
13
|
+
Generator,
|
|
14
|
+
Iterable,
|
|
15
|
+
Tuple,
|
|
16
|
+
Union,
|
|
17
|
+
Iterator,
|
|
18
|
+
Sequence,
|
|
19
|
+
)
|
|
20
|
+
from threading import Lock
|
|
21
|
+
import logging, weakref
|
|
22
|
+
import psycopg2, psycopg2.extensions
|
|
23
|
+
|
|
24
|
+
psycopg2.extensions.register_type(psycopg2.extensions.UNICODE)
|
|
25
|
+
psycopg2.extensions.register_type(psycopg2.extensions.UNICODEARRAY)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class BaseConnection(base.Connection):
|
|
29
|
+
def get(self) -> psycopg2.extensions.connection:
|
|
30
|
+
raise NotImplementedError
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Cursor(base.Cursor):
|
|
34
|
+
def __init__(self, connection: BaseConnection):
|
|
35
|
+
self.connection = connection
|
|
36
|
+
self.cursor = None
|
|
37
|
+
|
|
38
|
+
def get(self) -> psycopg2.extensions.cursor:
|
|
39
|
+
if self.cursor is None:
|
|
40
|
+
raise RuntimeError("Cursor is not available")
|
|
41
|
+
return self.cursor
|
|
42
|
+
|
|
43
|
+
def execute_direct(self, query, *args) -> psycopg2.extensions.cursor:
|
|
44
|
+
cursor = self.get()
|
|
45
|
+
logging.debug(f"execute_direct({query}, {args})")
|
|
46
|
+
cursor.execute(query, args)
|
|
47
|
+
return cursor
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def iter_inner(cls, cursor) -> Generator[Dict[str, Any], None, None]:
|
|
51
|
+
columns = [desc[0] for desc in cursor.description]
|
|
52
|
+
while (row := cursor.fetchone()) is not None:
|
|
53
|
+
yield dict(zip(columns, row))
|
|
54
|
+
|
|
55
|
+
def execute(self, query, *args) -> Iterator[Dict[str, Any]]:
|
|
56
|
+
cursor = self.get()
|
|
57
|
+
logging.debug(f"execute({query}, {args})")
|
|
58
|
+
cursor.execute(query, args)
|
|
59
|
+
return iter(self.iter_inner(cursor))
|
|
60
|
+
|
|
61
|
+
def __iter__(self) -> Iterator[Dict[str, Any]]:
|
|
62
|
+
return iter(self.iter_inner(self.get()))
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def rowcount(self) -> int:
|
|
66
|
+
return self.get().rowcount
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def columns(self) -> List[str]:
|
|
70
|
+
return [desc[0] for desc in self.get().description]
|
|
71
|
+
|
|
72
|
+
def init_cursor(self) -> None:
|
|
73
|
+
self.cursor = self.connection.get().cursor()
|
|
74
|
+
|
|
75
|
+
def __enter__(self) -> Any:
|
|
76
|
+
self.init_cursor()
|
|
77
|
+
return self
|
|
78
|
+
|
|
79
|
+
def __exit__(self, type, value, traceback) -> None:
|
|
80
|
+
if self.cursor is not None:
|
|
81
|
+
self.cursor = None
|
|
82
|
+
if value is None:
|
|
83
|
+
self.connection.commit()
|
|
84
|
+
else:
|
|
85
|
+
self.connection.rollback()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class Connection(BaseConnection):
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
host: Optional[str],
|
|
92
|
+
port: Optional[int],
|
|
93
|
+
username: str,
|
|
94
|
+
password: Optional[str],
|
|
95
|
+
database: str,
|
|
96
|
+
):
|
|
97
|
+
self.lock = Lock()
|
|
98
|
+
port_number = "5432"
|
|
99
|
+
if port is not None and port > 0:
|
|
100
|
+
port_number = str(port)
|
|
101
|
+
self.connection: Optional[psycopg2.extensions.connection] = psycopg2.connect(
|
|
102
|
+
host=host,
|
|
103
|
+
port=port_number,
|
|
104
|
+
user=username,
|
|
105
|
+
password=password,
|
|
106
|
+
database=database,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def get(self) -> psycopg2.extensions.connection:
|
|
110
|
+
with self.lock:
|
|
111
|
+
if self.connection is None:
|
|
112
|
+
raise RuntimeError("Connection has been closed")
|
|
113
|
+
return self.connection
|
|
114
|
+
|
|
115
|
+
def close(self) -> None:
|
|
116
|
+
with self.lock:
|
|
117
|
+
if self.connection is not None:
|
|
118
|
+
self.connection.close()
|
|
119
|
+
self.connection = None
|
|
120
|
+
|
|
121
|
+
def commit(self) -> None:
|
|
122
|
+
self.get().commit()
|
|
123
|
+
|
|
124
|
+
def rollback(self) -> None:
|
|
125
|
+
self.get().rollback()
|
|
126
|
+
|
|
127
|
+
def cursor(self) -> Cursor:
|
|
128
|
+
return Cursor(self)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class BaseTable(base.BaseTable):
|
|
132
|
+
ORDERINGS = frozenset(["ASC", "DESC"])
|
|
133
|
+
|
|
134
|
+
def __init__(
|
|
135
|
+
self,
|
|
136
|
+
name: str,
|
|
137
|
+
primary_key: str,
|
|
138
|
+
columns: Sequence[str],
|
|
139
|
+
ordering: Optional[str] = "ASC",
|
|
140
|
+
):
|
|
141
|
+
super().__init__(name, primary_key, columns, ordering)
|
|
142
|
+
|
|
143
|
+
@staticmethod
|
|
144
|
+
def convert_cursor(cursor: base.Cursor) -> Cursor:
|
|
145
|
+
if not isinstance(cursor, Cursor):
|
|
146
|
+
raise RuntimeError(f"Not a PgCursor: {cursor}")
|
|
147
|
+
return cursor
|
|
148
|
+
|
|
149
|
+
def insert(self, base_cursor: base.Cursor, **input_values: Any) -> Any:
|
|
150
|
+
cursor = self.convert_cursor(base_cursor)
|
|
151
|
+
self.validate(input_values.keys())
|
|
152
|
+
columns, values = self.unpack(input_values)
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
column_string = ", ".join(columns)
|
|
156
|
+
value_string = ", ".join(("%s" for i in columns))
|
|
157
|
+
row = next(
|
|
158
|
+
cursor.execute(
|
|
159
|
+
f"INSERT INTO {self.name}({column_string}) VALUES({value_string}) RETURNING {self.primary_key}",
|
|
160
|
+
*values,
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
return row[self.primary_key]
|
|
164
|
+
|
|
165
|
+
except StopIteration:
|
|
166
|
+
raise RuntimeError(f"Failed to insert values into {self.name}")
|
|
167
|
+
|
|
168
|
+
def upsert_on_column(
|
|
169
|
+
self,
|
|
170
|
+
base_cursor: base.Cursor,
|
|
171
|
+
column_name: str,
|
|
172
|
+
column_value: Any,
|
|
173
|
+
**input_values: Any,
|
|
174
|
+
) -> Any:
|
|
175
|
+
cursor = self.convert_cursor(base_cursor)
|
|
176
|
+
self.validate(input_values.keys())
|
|
177
|
+
|
|
178
|
+
if self.primary_key in input_values:
|
|
179
|
+
input_values[self.primary_key] = column_value
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
if column_value is not None:
|
|
183
|
+
row = next(
|
|
184
|
+
cursor.execute(
|
|
185
|
+
f"SELECT {self.primary_key} FROM {self.name} WHERE {column_name} = %s",
|
|
186
|
+
column_value,
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
else:
|
|
190
|
+
row = next(
|
|
191
|
+
cursor.execute(
|
|
192
|
+
f"SELECT {self.primary_key} FROM {self.name} WHERE {column_name} IS NULL"
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if cursor.rowcount > 1:
|
|
197
|
+
raise RuntimeError(f"Multiple results were found in {self.name}")
|
|
198
|
+
|
|
199
|
+
pkey_value = row[self.primary_key]
|
|
200
|
+
|
|
201
|
+
if not input_values or (
|
|
202
|
+
len(input_values) == 1 and input_values == {column_name: column_value}
|
|
203
|
+
):
|
|
204
|
+
# No changes are necessary
|
|
205
|
+
return pkey_value
|
|
206
|
+
|
|
207
|
+
# Update existing row
|
|
208
|
+
columns, values = self.unpack(input_values)
|
|
209
|
+
set_string = ", ".join((f"{column} = %s" for column in columns))
|
|
210
|
+
cursor.execute(
|
|
211
|
+
f"UPDATE {self.name} SET {set_string} WHERE {self.primary_key} = %s",
|
|
212
|
+
*(values + (pkey_value,)),
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
return input_values[self.primary_key]
|
|
217
|
+
except KeyError:
|
|
218
|
+
return pkey_value
|
|
219
|
+
|
|
220
|
+
except StopIteration:
|
|
221
|
+
if column_name not in input_values:
|
|
222
|
+
# Specifying the column multiple times seems silly.
|
|
223
|
+
input_values[column_name] = column_value
|
|
224
|
+
|
|
225
|
+
return self.insert(cursor, **input_values)
|
|
226
|
+
|
|
227
|
+
def select_by_columns(
|
|
228
|
+
self, base_cursor: base.Cursor, limit: Optional[int] = None, **input_values: Any
|
|
229
|
+
) -> Iterator[Dict[str, Any]]:
|
|
230
|
+
cursor = self.convert_cursor(base_cursor)
|
|
231
|
+
self.validate(input_values.keys())
|
|
232
|
+
fixed_input_values = list(input_values.items())
|
|
233
|
+
columns, values = self.unpack(fixed_input_values)
|
|
234
|
+
|
|
235
|
+
column_string = ", ".join(self.columns)
|
|
236
|
+
where_string = " AND ".join(
|
|
237
|
+
(
|
|
238
|
+
(f"{column} = %s" if value is not None else f"{column} IS NULL")
|
|
239
|
+
for column, value in fixed_input_values
|
|
240
|
+
)
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
cleaned_values = [v for v in values if v is not None]
|
|
244
|
+
|
|
245
|
+
limit_str = f"LIMIT {limit}" if limit and limit >= 0 else ""
|
|
246
|
+
return cursor.execute(
|
|
247
|
+
f"SELECT {column_string} FROM {self.name} WHERE {where_string} ORDER BY {self.primary_key} {self.ordering} {limit_str}",
|
|
248
|
+
*cleaned_values,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
def select_custom_where(
|
|
252
|
+
self, base_cursor: base.Cursor, where: str, *values
|
|
253
|
+
) -> Iterator[Dict[str, Any]]:
|
|
254
|
+
cursor = self.convert_cursor(base_cursor)
|
|
255
|
+
column_string = ", ".join(self.columns)
|
|
256
|
+
return cursor.execute(
|
|
257
|
+
f"SELECT {column_string} FROM {self.name} WHERE {where}", *values
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
def select_by_id(self, base_cursor: base.Cursor, pkey_value: Any) -> Dict[str, Any]:
|
|
261
|
+
cursor = self.convert_cursor(base_cursor)
|
|
262
|
+
column_string = ", ".join(self.columns)
|
|
263
|
+
try:
|
|
264
|
+
return next(
|
|
265
|
+
cursor.execute(
|
|
266
|
+
f"SELECT {column_string} FROM {self.name} WHERE {self.primary_key} = %s",
|
|
267
|
+
pkey_value,
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
except StopIteration:
|
|
271
|
+
raise KeyError(pkey_value)
|
|
272
|
+
|
|
273
|
+
def delete(self, base_cursor: base.Cursor, pkey_value: Any) -> bool:
|
|
274
|
+
cursor = self.convert_cursor(base_cursor)
|
|
275
|
+
cursor.execute(
|
|
276
|
+
f"DELETE FROM {self.name} WHERE {self.primary_key} = %s", pkey_value
|
|
277
|
+
)
|
|
278
|
+
return cursor.rowcount > 0
|
|
File without changes
|
workercommon/locking.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
if sys.version_info < (3, 8):
|
|
5
|
+
raise RuntimeError("At least Python 3.8 is required")
|
|
6
|
+
|
|
7
|
+
from sys import stdout, stderr
|
|
8
|
+
from os import fchmod
|
|
9
|
+
import fcntl, logging
|
|
10
|
+
from time import sleep
|
|
11
|
+
from typing import Union
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LockFile(object):
|
|
16
|
+
def __init__(self, location: Union[str, Path], exclusive: bool = True):
|
|
17
|
+
self.location = str(location)
|
|
18
|
+
self.fd = None
|
|
19
|
+
self.exclusive = exclusive
|
|
20
|
+
|
|
21
|
+
def __enter__(self):
|
|
22
|
+
if self.fd is not None:
|
|
23
|
+
raise RuntimeError("Cannot lock twice")
|
|
24
|
+
self.fd = open(self.location, "w+b")
|
|
25
|
+
if self.fd is None:
|
|
26
|
+
raise RuntimeError(f"Failed to open {self.location}")
|
|
27
|
+
try:
|
|
28
|
+
fchmod(self.fd.fileno(), 0o666)
|
|
29
|
+
except OSError:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
for i in range(10):
|
|
33
|
+
try:
|
|
34
|
+
mode = fcntl.LOCK_NB
|
|
35
|
+
if self.exclusive:
|
|
36
|
+
mode |= fcntl.LOCK_EX
|
|
37
|
+
else:
|
|
38
|
+
mode |= fcntl.LOCK_SH
|
|
39
|
+
|
|
40
|
+
logging.debug(f"Attempting to lock file {self.location}")
|
|
41
|
+
fcntl.lockf(self.fd, mode)
|
|
42
|
+
return
|
|
43
|
+
except IOError:
|
|
44
|
+
logging.warning("Waiting for existing lock...")
|
|
45
|
+
sleep(1)
|
|
46
|
+
continue
|
|
47
|
+
raise RuntimeError("The existing bot did not exit in time; dying")
|
|
48
|
+
|
|
49
|
+
def __exit__(self, type, value, traceback):
|
|
50
|
+
fcntl.lockf(self.fd, fcntl.LOCK_UN)
|
|
51
|
+
self.fd.close()
|
|
52
|
+
self.fd = None
|
workercommon/py.typed
ADDED
|
File without changes
|