dsw-database 4.27.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.
- dsw/database/__init__.py +4 -0
- dsw/database/build_info.py +17 -0
- dsw/database/database.py +640 -0
- dsw/database/model.py +478 -0
- dsw/database/py.typed +0 -0
- dsw_database-4.27.0.dist-info/METADATA +44 -0
- dsw_database-4.27.0.dist-info/RECORD +8 -0
- dsw_database-4.27.0.dist-info/WHEEL +4 -0
dsw/database/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Generated file
|
|
2
|
+
# - do not overwrite
|
|
3
|
+
# - do not include in git
|
|
4
|
+
from collections import namedtuple
|
|
5
|
+
|
|
6
|
+
BuildInfo = namedtuple(
|
|
7
|
+
'BuildInfo',
|
|
8
|
+
['version', 'built_at', 'sha', 'branch', 'tag'],
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
BUILD_INFO = BuildInfo(
|
|
12
|
+
version='v4.27.0~8ec71bd',
|
|
13
|
+
built_at='2026-02-03 08:44:49Z',
|
|
14
|
+
sha='8ec71bd85dfbea66adedb6590f7d76ae5143bbaa',
|
|
15
|
+
branch='HEAD',
|
|
16
|
+
tag='v4.27.0',
|
|
17
|
+
)
|
dsw/database/database.py
ADDED
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
# pylint: disable=no-member
|
|
2
|
+
import datetime
|
|
3
|
+
import logging
|
|
4
|
+
import typing
|
|
5
|
+
|
|
6
|
+
import psycopg
|
|
7
|
+
import psycopg.conninfo
|
|
8
|
+
import psycopg.rows
|
|
9
|
+
import psycopg.types.json
|
|
10
|
+
import tenacity
|
|
11
|
+
|
|
12
|
+
from dsw.config.model import DatabaseConfig
|
|
13
|
+
|
|
14
|
+
from . import model
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
LOG = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
RETRY_QUERY_MULTIPLIER = 0.5
|
|
20
|
+
RETRY_QUERY_TRIES = 3
|
|
21
|
+
|
|
22
|
+
RETRY_CONNECT_MULTIPLIER = 0.2
|
|
23
|
+
RETRY_CONNECT_TRIES = 10
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def wrap_json_data(data: dict):
|
|
27
|
+
return psycopg.types.json.Json(data)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# pylint: disable-next=too-many-public-methods
|
|
31
|
+
class Database:
|
|
32
|
+
|
|
33
|
+
SELECT_DOCUMENT = ('SELECT * FROM document '
|
|
34
|
+
'WHERE uuid = %s AND tenant_uuid = %s LIMIT 1;')
|
|
35
|
+
SELECT_DOCUMENTS = ('SELECT * FROM document '
|
|
36
|
+
'WHERE project_uuid = %s AND tenant_uuid = %s;')
|
|
37
|
+
SELECT_DOCUMENT_SUBMISSIONS = ('SELECT * FROM submission '
|
|
38
|
+
'WHERE document_uuid = %s AND tenant_uuid = %s;')
|
|
39
|
+
SELECT_PROJECT_SUBMISSIONS = ('SELECT s.* '
|
|
40
|
+
'FROM document d JOIN submission s ON d.uuid = s.document_uuid '
|
|
41
|
+
'WHERE d.project_uuid = %s AND d.tenant_uuid = %s;')
|
|
42
|
+
SELECT_PROJECT_SIMPLE = ('SELECT p.* FROM project p '
|
|
43
|
+
'WHERE p.uuid = %s AND p.tenant_uuid = %s;')
|
|
44
|
+
SELECT_TENANT_LIMIT = ('SELECT uuid, storage FROM tenant_limit_bundle '
|
|
45
|
+
'WHERE uuid = %(tenant_uuid)s LIMIT 1;')
|
|
46
|
+
UPDATE_DOCUMENT_STATE = 'UPDATE document SET state = %s, worker_log = %s WHERE uuid = %s;'
|
|
47
|
+
UPDATE_DOCUMENT_RETRIEVED = 'UPDATE document SET retrieved_at = %s, state = %s WHERE uuid = %s;'
|
|
48
|
+
UPDATE_DOCUMENT_FINISHED = ('UPDATE document SET finished_at = %s, state = %s, '
|
|
49
|
+
'file_name = %s, content_type = %s, worker_log = %s, '
|
|
50
|
+
'file_size = %s WHERE uuid = %s;')
|
|
51
|
+
SELECT_TEMPLATE = ('SELECT * FROM document_template '
|
|
52
|
+
'WHERE id = %s AND tenant_uuid = %s LIMIT 1;')
|
|
53
|
+
SELECT_TEMPLATE_FORMATS = ('SELECT * FROM document_template_format '
|
|
54
|
+
'WHERE document_template_id = %s AND tenant_uuid = %s;')
|
|
55
|
+
SELECT_TEMPLATE_STEPS = ('SELECT * FROM document_template_format_step '
|
|
56
|
+
'WHERE document_template_id = %s AND tenant_uuid = %s;')
|
|
57
|
+
SELECT_TEMPLATE_FILES = ('SELECT * FROM document_template_file '
|
|
58
|
+
'WHERE document_template_id = %s AND tenant_uuid = %s;')
|
|
59
|
+
SELECT_TEMPLATE_ASSETS = ('SELECT * FROM document_template_asset '
|
|
60
|
+
'WHERE document_template_id = %s AND tenant_uuid = %s;')
|
|
61
|
+
CHECK_TABLE_EXISTS = ('SELECT EXISTS(SELECT * FROM information_schema.tables'
|
|
62
|
+
' WHERE table_name = %(table_name)s)')
|
|
63
|
+
SELECT_MAIL_CONFIG = ('SELECT * FROM instance_config_mail '
|
|
64
|
+
'WHERE uuid = %(mail_config_uuid)s;')
|
|
65
|
+
UPDATE_COMPONENT_INFO = ('INSERT INTO component '
|
|
66
|
+
'(name, version, built_at, created_at, updated_at) '
|
|
67
|
+
'VALUES (%(name)s, %(version)s, %(built_at)s, '
|
|
68
|
+
'%(created_at)s, %(updated_at)s)'
|
|
69
|
+
'ON CONFLICT (name) DO '
|
|
70
|
+
'UPDATE SET version = %(version)s, built_at = %(built_at)s, '
|
|
71
|
+
'updated_at = %(updated_at)s;')
|
|
72
|
+
SELECT_COMPONENT_INFO = 'SELECT * FROM component WHERE name = %(name)s;'
|
|
73
|
+
SUM_FILE_SIZES = ('SELECT (SELECT COALESCE(SUM(file_size)::bigint, 0) '
|
|
74
|
+
'FROM document WHERE tenant_uuid = %(tenant_uuid)s) '
|
|
75
|
+
'+ (SELECT COALESCE(SUM(file_size)::bigint, 0) '
|
|
76
|
+
'FROM document_template_asset WHERE tenant_uuid = %(tenant_uuid)s) '
|
|
77
|
+
'+ (SELECT COALESCE(SUM(file_size)::bigint, 0) '
|
|
78
|
+
'FROM project_file WHERE tenant_uuid = %(tenant_uuid)s) '
|
|
79
|
+
'AS result;')
|
|
80
|
+
SELECT_USER = ('SELECT * FROM user_entity '
|
|
81
|
+
'WHERE uuid = %(user_uuid)s AND tenant_uuid = %(tenant_uuid)s;')
|
|
82
|
+
SELECT_DEFAULT_LOCALE = ('SELECT * FROM locale '
|
|
83
|
+
'WHERE default_locale IS TRUE AND '
|
|
84
|
+
' enabled is TRUE AND '
|
|
85
|
+
' tenant_uuid = %(tenant_uuid)s;')
|
|
86
|
+
SELECT_LOCALE = ('SELECT * FROM locale '
|
|
87
|
+
'WHERE uuid = %(locale_uuid)s AND tenant_uuid = %(tenant_uuid)s;')
|
|
88
|
+
|
|
89
|
+
def __init__(self, cfg: DatabaseConfig, connect: bool = True,
|
|
90
|
+
with_queue: bool = True):
|
|
91
|
+
self.cfg = cfg
|
|
92
|
+
LOG.info('Preparing PostgreSQL connection for QUERY')
|
|
93
|
+
self.conn_query = PostgresConnection(
|
|
94
|
+
name='query',
|
|
95
|
+
dsn=self.cfg.connection_string,
|
|
96
|
+
timeout=self.cfg.connection_timeout,
|
|
97
|
+
autocommit=False,
|
|
98
|
+
)
|
|
99
|
+
if connect:
|
|
100
|
+
self.conn_query.connect()
|
|
101
|
+
self.with_queue = with_queue
|
|
102
|
+
if with_queue:
|
|
103
|
+
LOG.info('Preparing PostgreSQL connection for QUEUE')
|
|
104
|
+
self.conn_queue = PostgresConnection(
|
|
105
|
+
name='queue',
|
|
106
|
+
dsn=self.cfg.connection_string,
|
|
107
|
+
timeout=self.cfg.connection_timeout,
|
|
108
|
+
autocommit=True,
|
|
109
|
+
)
|
|
110
|
+
if connect:
|
|
111
|
+
self.conn_queue.connect()
|
|
112
|
+
|
|
113
|
+
def connect(self):
|
|
114
|
+
self.conn_query.connect()
|
|
115
|
+
if self.with_queue:
|
|
116
|
+
self.conn_queue.connect()
|
|
117
|
+
|
|
118
|
+
@tenacity.retry(
|
|
119
|
+
reraise=True,
|
|
120
|
+
wait=tenacity.wait_exponential(multiplier=RETRY_QUERY_MULTIPLIER),
|
|
121
|
+
stop=tenacity.stop_after_attempt(RETRY_QUERY_TRIES),
|
|
122
|
+
before=tenacity.before_log(LOG, logging.DEBUG),
|
|
123
|
+
after=tenacity.after_log(LOG, logging.DEBUG),
|
|
124
|
+
)
|
|
125
|
+
def _check_table_exists(self, table_name: str) -> bool:
|
|
126
|
+
with self.conn_query.new_cursor() as cursor:
|
|
127
|
+
try:
|
|
128
|
+
cursor.execute(
|
|
129
|
+
query=self.CHECK_TABLE_EXISTS,
|
|
130
|
+
params={'table_name': table_name},
|
|
131
|
+
)
|
|
132
|
+
return cursor.fetchone()[0]
|
|
133
|
+
except Exception:
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
@tenacity.retry(
|
|
137
|
+
reraise=True,
|
|
138
|
+
wait=tenacity.wait_exponential(multiplier=RETRY_QUERY_MULTIPLIER),
|
|
139
|
+
stop=tenacity.stop_after_attempt(RETRY_QUERY_TRIES),
|
|
140
|
+
before=tenacity.before_log(LOG, logging.DEBUG),
|
|
141
|
+
after=tenacity.after_log(LOG, logging.DEBUG),
|
|
142
|
+
)
|
|
143
|
+
def fetch_document(self, document_uuid: str, tenant_uuid: str) -> model.DBDocument | None:
|
|
144
|
+
with self.conn_query.new_cursor(use_dict=True) as cursor:
|
|
145
|
+
cursor.execute(
|
|
146
|
+
query=self.SELECT_DOCUMENT,
|
|
147
|
+
params=(document_uuid, tenant_uuid),
|
|
148
|
+
)
|
|
149
|
+
result = cursor.fetchall()
|
|
150
|
+
if len(result) != 1:
|
|
151
|
+
return None
|
|
152
|
+
return model.DBDocument.from_dict_row(result[0])
|
|
153
|
+
|
|
154
|
+
@tenacity.retry(
|
|
155
|
+
reraise=True,
|
|
156
|
+
wait=tenacity.wait_exponential(multiplier=RETRY_QUERY_MULTIPLIER),
|
|
157
|
+
stop=tenacity.stop_after_attempt(RETRY_QUERY_TRIES),
|
|
158
|
+
before=tenacity.before_log(LOG, logging.DEBUG),
|
|
159
|
+
after=tenacity.after_log(LOG, logging.DEBUG),
|
|
160
|
+
)
|
|
161
|
+
def fetch_tenant_limits(self, tenant_uuid: str) -> model.DBTenantLimits | None:
|
|
162
|
+
with self.conn_query.new_cursor(use_dict=True) as cursor:
|
|
163
|
+
cursor.execute(
|
|
164
|
+
query=self.SELECT_TENANT_LIMIT,
|
|
165
|
+
params={'tenant_uuid': tenant_uuid},
|
|
166
|
+
)
|
|
167
|
+
result = cursor.fetchall()
|
|
168
|
+
if len(result) != 1:
|
|
169
|
+
return None
|
|
170
|
+
return model.DBTenantLimits.from_dict_row(result[0])
|
|
171
|
+
|
|
172
|
+
@tenacity.retry(
|
|
173
|
+
reraise=True,
|
|
174
|
+
wait=tenacity.wait_exponential(multiplier=RETRY_QUERY_MULTIPLIER),
|
|
175
|
+
stop=tenacity.stop_after_attempt(RETRY_QUERY_TRIES),
|
|
176
|
+
before=tenacity.before_log(LOG, logging.DEBUG),
|
|
177
|
+
after=tenacity.after_log(LOG, logging.DEBUG),
|
|
178
|
+
)
|
|
179
|
+
def fetch_template(
|
|
180
|
+
self, template_id: str, tenant_uuid: str,
|
|
181
|
+
) -> model.DBDocumentTemplate | None:
|
|
182
|
+
with self.conn_query.new_cursor(use_dict=True) as cursor:
|
|
183
|
+
cursor.execute(
|
|
184
|
+
query=self.SELECT_TEMPLATE,
|
|
185
|
+
params=(template_id, tenant_uuid),
|
|
186
|
+
)
|
|
187
|
+
dt_result = cursor.fetchall()
|
|
188
|
+
if len(dt_result) != 1:
|
|
189
|
+
return None
|
|
190
|
+
template = model.DBDocumentTemplate.from_dict_row(dt_result[0])
|
|
191
|
+
|
|
192
|
+
cursor.execute(
|
|
193
|
+
query=self.SELECT_TEMPLATE_FORMATS,
|
|
194
|
+
params=(template_id, tenant_uuid),
|
|
195
|
+
)
|
|
196
|
+
formats_result = cursor.fetchall()
|
|
197
|
+
formats = sorted([
|
|
198
|
+
model.DBDocumentTemplateFormat.from_dict_row(x) for x in formats_result
|
|
199
|
+
], key=lambda x: x.name)
|
|
200
|
+
cursor.execute(
|
|
201
|
+
query=self.SELECT_TEMPLATE_STEPS,
|
|
202
|
+
params=(template_id, tenant_uuid),
|
|
203
|
+
)
|
|
204
|
+
steps_result = cursor.fetchall()
|
|
205
|
+
steps = sorted([
|
|
206
|
+
model.DBDocumentTemplateStep.from_dict_row(x) for x in steps_result
|
|
207
|
+
], key=lambda x: x.position)
|
|
208
|
+
steps_dict: dict[str, list[dict]] = {}
|
|
209
|
+
for step in steps:
|
|
210
|
+
if step.format_uuid not in steps_dict:
|
|
211
|
+
steps_dict[step.format_uuid] = []
|
|
212
|
+
steps_dict[step.format_uuid].append({
|
|
213
|
+
'name': step.name,
|
|
214
|
+
'options': step.options,
|
|
215
|
+
})
|
|
216
|
+
for format_obj in formats:
|
|
217
|
+
template.formats.append({
|
|
218
|
+
'uuid': format_obj.uuid,
|
|
219
|
+
'name': format_obj.name,
|
|
220
|
+
'steps': steps_dict.get(format_obj.uuid, []),
|
|
221
|
+
})
|
|
222
|
+
return template
|
|
223
|
+
|
|
224
|
+
@tenacity.retry(
|
|
225
|
+
reraise=True,
|
|
226
|
+
wait=tenacity.wait_exponential(multiplier=RETRY_QUERY_MULTIPLIER),
|
|
227
|
+
stop=tenacity.stop_after_attempt(RETRY_QUERY_TRIES),
|
|
228
|
+
before=tenacity.before_log(LOG, logging.DEBUG),
|
|
229
|
+
after=tenacity.after_log(LOG, logging.DEBUG),
|
|
230
|
+
)
|
|
231
|
+
def fetch_template_files(
|
|
232
|
+
self, template_id: str, tenant_uuid: str,
|
|
233
|
+
) -> list[model.DBDocumentTemplateFile]:
|
|
234
|
+
with self.conn_query.new_cursor(use_dict=True) as cursor:
|
|
235
|
+
cursor.execute(
|
|
236
|
+
query=self.SELECT_TEMPLATE_FILES,
|
|
237
|
+
params=(template_id, tenant_uuid),
|
|
238
|
+
)
|
|
239
|
+
return [model.DBDocumentTemplateFile.from_dict_row(x) for x in cursor.fetchall()]
|
|
240
|
+
|
|
241
|
+
@tenacity.retry(
|
|
242
|
+
reraise=True,
|
|
243
|
+
wait=tenacity.wait_exponential(multiplier=RETRY_QUERY_MULTIPLIER),
|
|
244
|
+
stop=tenacity.stop_after_attempt(RETRY_QUERY_TRIES),
|
|
245
|
+
before=tenacity.before_log(LOG, logging.DEBUG),
|
|
246
|
+
after=tenacity.after_log(LOG, logging.DEBUG),
|
|
247
|
+
)
|
|
248
|
+
def fetch_template_assets(
|
|
249
|
+
self, template_id: str, tenant_uuid: str,
|
|
250
|
+
) -> list[model.DBDocumentTemplateAsset]:
|
|
251
|
+
with self.conn_query.new_cursor(use_dict=True) as cursor:
|
|
252
|
+
cursor.execute(
|
|
253
|
+
query=self.SELECT_TEMPLATE_ASSETS,
|
|
254
|
+
params=(template_id, tenant_uuid),
|
|
255
|
+
)
|
|
256
|
+
return [model.DBDocumentTemplateAsset.from_dict_row(x) for x in cursor.fetchall()]
|
|
257
|
+
|
|
258
|
+
@tenacity.retry(
|
|
259
|
+
reraise=True,
|
|
260
|
+
wait=tenacity.wait_exponential(multiplier=RETRY_QUERY_MULTIPLIER),
|
|
261
|
+
stop=tenacity.stop_after_attempt(RETRY_QUERY_TRIES),
|
|
262
|
+
before=tenacity.before_log(LOG, logging.DEBUG),
|
|
263
|
+
after=tenacity.after_log(LOG, logging.DEBUG),
|
|
264
|
+
)
|
|
265
|
+
def fetch_project_documents(self, project_uuid: str,
|
|
266
|
+
tenant_uuid: str) -> list[model.DBDocument]:
|
|
267
|
+
with self.conn_query.new_cursor(use_dict=True) as cursor:
|
|
268
|
+
cursor.execute(
|
|
269
|
+
query=self.SELECT_DOCUMENTS,
|
|
270
|
+
params=(project_uuid, tenant_uuid),
|
|
271
|
+
)
|
|
272
|
+
return [model.DBDocument.from_dict_row(x) for x in cursor.fetchall()]
|
|
273
|
+
|
|
274
|
+
@tenacity.retry(
|
|
275
|
+
reraise=True,
|
|
276
|
+
wait=tenacity.wait_exponential(multiplier=RETRY_QUERY_MULTIPLIER),
|
|
277
|
+
stop=tenacity.stop_after_attempt(RETRY_QUERY_TRIES),
|
|
278
|
+
before=tenacity.before_log(LOG, logging.DEBUG),
|
|
279
|
+
after=tenacity.after_log(LOG, logging.DEBUG),
|
|
280
|
+
)
|
|
281
|
+
def fetch_document_submissions(self, document_uuid: str,
|
|
282
|
+
tenant_uuid: str) -> list[model.DBSubmission]:
|
|
283
|
+
with self.conn_query.new_cursor(use_dict=True) as cursor:
|
|
284
|
+
cursor.execute(
|
|
285
|
+
query=self.SELECT_DOCUMENT_SUBMISSIONS,
|
|
286
|
+
params=(document_uuid, tenant_uuid),
|
|
287
|
+
)
|
|
288
|
+
return [model.DBSubmission.from_dict_row(x) for x in cursor.fetchall()]
|
|
289
|
+
|
|
290
|
+
@tenacity.retry(
|
|
291
|
+
reraise=True,
|
|
292
|
+
wait=tenacity.wait_exponential(multiplier=RETRY_QUERY_MULTIPLIER),
|
|
293
|
+
stop=tenacity.stop_after_attempt(RETRY_QUERY_TRIES),
|
|
294
|
+
before=tenacity.before_log(LOG, logging.DEBUG),
|
|
295
|
+
after=tenacity.after_log(LOG, logging.DEBUG),
|
|
296
|
+
)
|
|
297
|
+
def fetch_project_submissions(self, project_uuid: str,
|
|
298
|
+
tenant_uuid: str) -> list[model.DBSubmission]:
|
|
299
|
+
with self.conn_query.new_cursor(use_dict=True) as cursor:
|
|
300
|
+
cursor.execute(
|
|
301
|
+
query=self.SELECT_PROJECT_SUBMISSIONS,
|
|
302
|
+
params=(project_uuid, tenant_uuid),
|
|
303
|
+
)
|
|
304
|
+
return [model.DBSubmission.from_dict_row(x) for x in cursor.fetchall()]
|
|
305
|
+
|
|
306
|
+
@tenacity.retry(
|
|
307
|
+
reraise=True,
|
|
308
|
+
wait=tenacity.wait_exponential(multiplier=RETRY_QUERY_MULTIPLIER),
|
|
309
|
+
stop=tenacity.stop_after_attempt(RETRY_QUERY_TRIES),
|
|
310
|
+
before=tenacity.before_log(LOG, logging.DEBUG),
|
|
311
|
+
after=tenacity.after_log(LOG, logging.DEBUG),
|
|
312
|
+
)
|
|
313
|
+
def fetch_project_simple(self, project_uuid: str,
|
|
314
|
+
tenant_uuid: str) -> model.DBProjectSimple:
|
|
315
|
+
with self.conn_query.new_cursor(use_dict=True) as cursor:
|
|
316
|
+
cursor.execute(
|
|
317
|
+
query=self.SELECT_PROJECT_SIMPLE,
|
|
318
|
+
params=(project_uuid, tenant_uuid),
|
|
319
|
+
)
|
|
320
|
+
return model.DBProjectSimple.from_dict_row(cursor.fetchone())
|
|
321
|
+
|
|
322
|
+
@tenacity.retry(
|
|
323
|
+
reraise=True,
|
|
324
|
+
wait=tenacity.wait_exponential(multiplier=RETRY_QUERY_MULTIPLIER),
|
|
325
|
+
stop=tenacity.stop_after_attempt(RETRY_QUERY_TRIES),
|
|
326
|
+
before=tenacity.before_log(LOG, logging.DEBUG),
|
|
327
|
+
after=tenacity.after_log(LOG, logging.DEBUG),
|
|
328
|
+
)
|
|
329
|
+
def update_document_state(self, document_uuid: str, worker_log: str, state: str) -> bool:
|
|
330
|
+
with self.conn_query.new_cursor() as cursor:
|
|
331
|
+
cursor.execute(
|
|
332
|
+
query=self.UPDATE_DOCUMENT_STATE,
|
|
333
|
+
params=(state, worker_log, document_uuid),
|
|
334
|
+
)
|
|
335
|
+
return cursor.rowcount == 1
|
|
336
|
+
|
|
337
|
+
@tenacity.retry(
|
|
338
|
+
reraise=True,
|
|
339
|
+
wait=tenacity.wait_exponential(multiplier=RETRY_QUERY_MULTIPLIER),
|
|
340
|
+
stop=tenacity.stop_after_attempt(RETRY_QUERY_TRIES),
|
|
341
|
+
before=tenacity.before_log(LOG, logging.DEBUG),
|
|
342
|
+
after=tenacity.after_log(LOG, logging.DEBUG),
|
|
343
|
+
)
|
|
344
|
+
def update_document_retrieved(self, retrieved_at: datetime.datetime,
|
|
345
|
+
document_uuid: str) -> bool:
|
|
346
|
+
with self.conn_query.new_cursor() as cursor:
|
|
347
|
+
cursor.execute(
|
|
348
|
+
query=self.UPDATE_DOCUMENT_RETRIEVED,
|
|
349
|
+
params=(
|
|
350
|
+
retrieved_at,
|
|
351
|
+
model.DocumentState.PROCESSING.value,
|
|
352
|
+
document_uuid,
|
|
353
|
+
),
|
|
354
|
+
)
|
|
355
|
+
return cursor.rowcount == 1
|
|
356
|
+
|
|
357
|
+
@tenacity.retry(
|
|
358
|
+
reraise=True,
|
|
359
|
+
wait=tenacity.wait_exponential(multiplier=RETRY_QUERY_MULTIPLIER),
|
|
360
|
+
stop=tenacity.stop_after_attempt(RETRY_QUERY_TRIES),
|
|
361
|
+
before=tenacity.before_log(LOG, logging.DEBUG),
|
|
362
|
+
after=tenacity.after_log(LOG, logging.DEBUG),
|
|
363
|
+
)
|
|
364
|
+
def update_document_finished(
|
|
365
|
+
self, *, finished_at: datetime.datetime, file_name: str, file_size: int,
|
|
366
|
+
content_type: str, worker_log: str, document_uuid: str,
|
|
367
|
+
) -> bool:
|
|
368
|
+
with self.conn_query.new_cursor() as cursor:
|
|
369
|
+
cursor.execute(
|
|
370
|
+
query=self.UPDATE_DOCUMENT_FINISHED,
|
|
371
|
+
params=(
|
|
372
|
+
finished_at,
|
|
373
|
+
model.DocumentState.FINISHED.value,
|
|
374
|
+
file_name,
|
|
375
|
+
content_type,
|
|
376
|
+
worker_log,
|
|
377
|
+
file_size,
|
|
378
|
+
document_uuid,
|
|
379
|
+
),
|
|
380
|
+
)
|
|
381
|
+
return cursor.rowcount == 1
|
|
382
|
+
|
|
383
|
+
@tenacity.retry(
|
|
384
|
+
reraise=True,
|
|
385
|
+
wait=tenacity.wait_exponential(multiplier=RETRY_QUERY_MULTIPLIER),
|
|
386
|
+
stop=tenacity.stop_after_attempt(RETRY_QUERY_TRIES),
|
|
387
|
+
before=tenacity.before_log(LOG, logging.DEBUG),
|
|
388
|
+
after=tenacity.after_log(LOG, logging.DEBUG),
|
|
389
|
+
)
|
|
390
|
+
def get_currently_used_size(self, tenant_uuid: str):
|
|
391
|
+
with self.conn_query.new_cursor() as cursor:
|
|
392
|
+
cursor.execute(
|
|
393
|
+
query=self.SUM_FILE_SIZES,
|
|
394
|
+
params={'tenant_uuid': tenant_uuid},
|
|
395
|
+
)
|
|
396
|
+
row = cursor.fetchone()
|
|
397
|
+
return row[0]
|
|
398
|
+
|
|
399
|
+
@tenacity.retry(
|
|
400
|
+
reraise=True,
|
|
401
|
+
wait=tenacity.wait_exponential(multiplier=RETRY_QUERY_MULTIPLIER),
|
|
402
|
+
stop=tenacity.stop_after_attempt(RETRY_QUERY_TRIES),
|
|
403
|
+
before=tenacity.before_log(LOG, logging.DEBUG),
|
|
404
|
+
after=tenacity.after_log(LOG, logging.DEBUG),
|
|
405
|
+
)
|
|
406
|
+
def get_mail_config(self, mail_config_uuid: str) -> model.DBInstanceConfigMail | None:
|
|
407
|
+
with self.conn_query.new_cursor(use_dict=True) as cursor:
|
|
408
|
+
if not self._check_table_exists(table_name='instance_config_mail'):
|
|
409
|
+
return None
|
|
410
|
+
try:
|
|
411
|
+
cursor.execute(
|
|
412
|
+
query=self.SELECT_MAIL_CONFIG,
|
|
413
|
+
params={'mail_config_uuid': mail_config_uuid},
|
|
414
|
+
)
|
|
415
|
+
result = cursor.fetchone()
|
|
416
|
+
if result is None:
|
|
417
|
+
return None
|
|
418
|
+
return model.DBInstanceConfigMail.from_dict_row(data=result)
|
|
419
|
+
except Exception as e:
|
|
420
|
+
LOG.warning('Could not retrieve instance_config_mail "%s": %s',
|
|
421
|
+
mail_config_uuid, str(e))
|
|
422
|
+
return None
|
|
423
|
+
|
|
424
|
+
@tenacity.retry(
|
|
425
|
+
reraise=True,
|
|
426
|
+
wait=tenacity.wait_exponential(multiplier=RETRY_QUERY_MULTIPLIER),
|
|
427
|
+
stop=tenacity.stop_after_attempt(RETRY_QUERY_TRIES),
|
|
428
|
+
before=tenacity.before_log(LOG, logging.DEBUG),
|
|
429
|
+
after=tenacity.after_log(LOG, logging.DEBUG),
|
|
430
|
+
)
|
|
431
|
+
def get_user(self, user_uuid: str, tenant_uuid: str) -> model.DBUserEntity | None:
|
|
432
|
+
if not self._check_table_exists(table_name='user_entity'):
|
|
433
|
+
return None
|
|
434
|
+
with self.conn_query.new_cursor(use_dict=True) as cursor:
|
|
435
|
+
try:
|
|
436
|
+
cursor.execute(
|
|
437
|
+
query=self.SELECT_USER,
|
|
438
|
+
params={'user_uuid': user_uuid, 'tenant_uuid': tenant_uuid},
|
|
439
|
+
)
|
|
440
|
+
result = cursor.fetchone()
|
|
441
|
+
if result is None:
|
|
442
|
+
return None
|
|
443
|
+
return model.DBUserEntity.from_dict_row(data=result)
|
|
444
|
+
except Exception as e:
|
|
445
|
+
LOG.warning('Could not retrieve user "%s" for tenant "%s": %s',
|
|
446
|
+
user_uuid, tenant_uuid, str(e))
|
|
447
|
+
return None
|
|
448
|
+
|
|
449
|
+
@tenacity.retry(
|
|
450
|
+
reraise=True,
|
|
451
|
+
wait=tenacity.wait_exponential(multiplier=RETRY_QUERY_MULTIPLIER),
|
|
452
|
+
stop=tenacity.stop_after_attempt(RETRY_QUERY_TRIES),
|
|
453
|
+
before=tenacity.before_log(LOG, logging.DEBUG),
|
|
454
|
+
after=tenacity.after_log(LOG, logging.DEBUG),
|
|
455
|
+
)
|
|
456
|
+
def get_default_locale(self, tenant_uuid: str) -> model.DBLocale | None:
|
|
457
|
+
if not self._check_table_exists(table_name='locale'):
|
|
458
|
+
return None
|
|
459
|
+
with self.conn_query.new_cursor(use_dict=True) as cursor:
|
|
460
|
+
try:
|
|
461
|
+
cursor.execute(
|
|
462
|
+
query=self.SELECT_DEFAULT_LOCALE,
|
|
463
|
+
params={'tenant_uuid': tenant_uuid},
|
|
464
|
+
)
|
|
465
|
+
result = cursor.fetchone()
|
|
466
|
+
if result is None:
|
|
467
|
+
return None
|
|
468
|
+
return model.DBLocale.from_dict_row(data=result)
|
|
469
|
+
except Exception as e:
|
|
470
|
+
LOG.warning('Could not retrieve default locale for tenant "%s": %s',
|
|
471
|
+
tenant_uuid, str(e))
|
|
472
|
+
return None
|
|
473
|
+
|
|
474
|
+
@tenacity.retry(
|
|
475
|
+
reraise=True,
|
|
476
|
+
wait=tenacity.wait_exponential(multiplier=RETRY_QUERY_MULTIPLIER),
|
|
477
|
+
stop=tenacity.stop_after_attempt(RETRY_QUERY_TRIES),
|
|
478
|
+
before=tenacity.before_log(LOG, logging.DEBUG),
|
|
479
|
+
after=tenacity.after_log(LOG, logging.DEBUG),
|
|
480
|
+
)
|
|
481
|
+
def get_locale(self, locale_uuid: str, tenant_uuid: str) -> model.DBLocale | None:
|
|
482
|
+
if not self._check_table_exists(table_name='locale'):
|
|
483
|
+
return None
|
|
484
|
+
with self.conn_query.new_cursor(use_dict=True) as cursor:
|
|
485
|
+
try:
|
|
486
|
+
cursor.execute(
|
|
487
|
+
query=self.SELECT_LOCALE,
|
|
488
|
+
params={'locale_uuid': locale_uuid, 'tenant_uuid': tenant_uuid},
|
|
489
|
+
)
|
|
490
|
+
result = cursor.fetchone()
|
|
491
|
+
if result is None:
|
|
492
|
+
return None
|
|
493
|
+
return model.DBLocale.from_dict_row(data=result)
|
|
494
|
+
except Exception as e:
|
|
495
|
+
LOG.warning('Could not retrieve locale "%s" for tenant "%s": %s',
|
|
496
|
+
locale_uuid, tenant_uuid, str(e))
|
|
497
|
+
return None
|
|
498
|
+
|
|
499
|
+
@tenacity.retry(
|
|
500
|
+
reraise=True,
|
|
501
|
+
wait=tenacity.wait_exponential(multiplier=RETRY_QUERY_MULTIPLIER),
|
|
502
|
+
stop=tenacity.stop_after_attempt(RETRY_QUERY_TRIES),
|
|
503
|
+
before=tenacity.before_log(LOG, logging.DEBUG),
|
|
504
|
+
after=tenacity.after_log(LOG, logging.DEBUG),
|
|
505
|
+
)
|
|
506
|
+
def update_component_info(self, name: str, version: str, built_at: datetime.datetime):
|
|
507
|
+
with self.conn_query.new_cursor(use_dict=True) as cursor:
|
|
508
|
+
if not self._check_table_exists(table_name='component'):
|
|
509
|
+
return
|
|
510
|
+
ts_now = datetime.datetime.now(tz=datetime.UTC)
|
|
511
|
+
try:
|
|
512
|
+
cursor.execute(
|
|
513
|
+
query=self.UPDATE_COMPONENT_INFO,
|
|
514
|
+
params={
|
|
515
|
+
'name': name,
|
|
516
|
+
'version': version,
|
|
517
|
+
'built_at': built_at,
|
|
518
|
+
'created_at': ts_now,
|
|
519
|
+
'updated_at': ts_now,
|
|
520
|
+
},
|
|
521
|
+
)
|
|
522
|
+
self.conn_query.connection.commit()
|
|
523
|
+
except Exception as e:
|
|
524
|
+
LOG.warning('Could not update component info: %s', str(e))
|
|
525
|
+
|
|
526
|
+
@tenacity.retry(
|
|
527
|
+
reraise=True,
|
|
528
|
+
wait=tenacity.wait_exponential(multiplier=RETRY_QUERY_MULTIPLIER),
|
|
529
|
+
stop=tenacity.stop_after_attempt(RETRY_QUERY_TRIES),
|
|
530
|
+
before=tenacity.before_log(LOG, logging.DEBUG),
|
|
531
|
+
after=tenacity.after_log(LOG, logging.DEBUG),
|
|
532
|
+
)
|
|
533
|
+
def get_component_info(self, name: str) -> model.DBComponent | None:
|
|
534
|
+
if not self._check_table_exists(table_name='component'):
|
|
535
|
+
return None
|
|
536
|
+
with self.conn_query.new_cursor(use_dict=True) as cursor:
|
|
537
|
+
try:
|
|
538
|
+
cursor.execute(
|
|
539
|
+
query=self.SELECT_COMPONENT_INFO,
|
|
540
|
+
params={'name': name},
|
|
541
|
+
)
|
|
542
|
+
result = cursor.fetchone()
|
|
543
|
+
if result is None:
|
|
544
|
+
return None
|
|
545
|
+
return model.DBComponent.from_dict_row(data=result)
|
|
546
|
+
except Exception as e:
|
|
547
|
+
LOG.warning('Could not get component info: %s', str(e))
|
|
548
|
+
return None
|
|
549
|
+
|
|
550
|
+
@tenacity.retry(
|
|
551
|
+
reraise=True,
|
|
552
|
+
wait=tenacity.wait_exponential(multiplier=RETRY_QUERY_MULTIPLIER),
|
|
553
|
+
stop=tenacity.stop_after_attempt(RETRY_QUERY_TRIES),
|
|
554
|
+
before=tenacity.before_log(LOG, logging.DEBUG),
|
|
555
|
+
after=tenacity.after_log(LOG, logging.DEBUG),
|
|
556
|
+
)
|
|
557
|
+
def execute_queries(self, queries: typing.Iterable[str]):
|
|
558
|
+
with self.conn_query.new_cursor(use_dict=True) as cursor:
|
|
559
|
+
for query in queries:
|
|
560
|
+
cursor.execute(query=query)
|
|
561
|
+
|
|
562
|
+
@tenacity.retry(
|
|
563
|
+
reraise=True,
|
|
564
|
+
wait=tenacity.wait_exponential(multiplier=RETRY_QUERY_MULTIPLIER),
|
|
565
|
+
stop=tenacity.stop_after_attempt(RETRY_QUERY_TRIES),
|
|
566
|
+
before=tenacity.before_log(LOG, logging.DEBUG),
|
|
567
|
+
after=tenacity.after_log(LOG, logging.DEBUG),
|
|
568
|
+
)
|
|
569
|
+
def execute_query(self, query: str, **kwargs):
|
|
570
|
+
with self.conn_query.new_cursor(use_dict=True) as cursor:
|
|
571
|
+
cursor.execute(query=query, params=kwargs)
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
class PostgresConnection:
|
|
575
|
+
|
|
576
|
+
def __init__(self, name: str, dsn: str, timeout=30000, autocommit=False):
|
|
577
|
+
self.name = name
|
|
578
|
+
self.listening = False
|
|
579
|
+
self.dsn = psycopg.conninfo.make_conninfo(
|
|
580
|
+
conninfo=dsn,
|
|
581
|
+
connect_timeout=timeout,
|
|
582
|
+
)
|
|
583
|
+
self.autocommit = autocommit
|
|
584
|
+
self._connection: psycopg.Connection | None = None
|
|
585
|
+
|
|
586
|
+
@tenacity.retry(
|
|
587
|
+
reraise=True,
|
|
588
|
+
wait=tenacity.wait_exponential(multiplier=RETRY_CONNECT_MULTIPLIER),
|
|
589
|
+
stop=tenacity.stop_after_attempt(RETRY_CONNECT_TRIES),
|
|
590
|
+
before=tenacity.before_log(LOG, logging.DEBUG),
|
|
591
|
+
after=tenacity.after_log(LOG, logging.DEBUG),
|
|
592
|
+
)
|
|
593
|
+
def _connect_db(self):
|
|
594
|
+
LOG.info('Creating connection to PostgreSQL database "%s"', self.name)
|
|
595
|
+
try:
|
|
596
|
+
connection: psycopg.Connection = psycopg.connect(
|
|
597
|
+
conninfo=self.dsn,
|
|
598
|
+
autocommit=self.autocommit,
|
|
599
|
+
)
|
|
600
|
+
except Exception as e:
|
|
601
|
+
LOG.error('Failed to connect to PostgreSQL database "%s": %s',
|
|
602
|
+
self.name, str(e))
|
|
603
|
+
raise e
|
|
604
|
+
# test connection
|
|
605
|
+
cursor = connection.cursor()
|
|
606
|
+
cursor.execute(query='SELECT 1;')
|
|
607
|
+
result = cursor.fetchone()
|
|
608
|
+
if result is None:
|
|
609
|
+
raise RuntimeError('Failed to verify DB connection')
|
|
610
|
+
LOG.debug('DB connection verified (result=%s)', result[0])
|
|
611
|
+
cursor.close()
|
|
612
|
+
connection.commit()
|
|
613
|
+
self._connection = connection
|
|
614
|
+
self.listening = False
|
|
615
|
+
|
|
616
|
+
def connect(self):
|
|
617
|
+
if not self._connection or self._connection.closed != 0:
|
|
618
|
+
self._connect_db()
|
|
619
|
+
|
|
620
|
+
@property
|
|
621
|
+
def connection(self) -> psycopg.Connection:
|
|
622
|
+
self.connect()
|
|
623
|
+
if not self._connection:
|
|
624
|
+
raise RuntimeError('Connection is not established')
|
|
625
|
+
return self._connection
|
|
626
|
+
|
|
627
|
+
def new_cursor(self, use_dict: bool = False):
|
|
628
|
+
return self.connection.cursor(
|
|
629
|
+
row_factory=psycopg.rows.dict_row if use_dict else psycopg.rows.tuple_row,
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
def reset(self):
|
|
633
|
+
self.close()
|
|
634
|
+
self.connect()
|
|
635
|
+
|
|
636
|
+
def close(self):
|
|
637
|
+
if self._connection:
|
|
638
|
+
LOG.info('Closing connection to PostgreSQL database "%s"', self.name)
|
|
639
|
+
self._connection.close()
|
|
640
|
+
self._connection = None
|
dsw/database/model.py
ADDED
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import datetime
|
|
3
|
+
import enum
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
NULL_UUID = '00000000-0000-0000-0000-000000000000'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DocumentState(enum.Enum):
|
|
11
|
+
QUEUED = 'QueuedDocumentState'
|
|
12
|
+
PROCESSING = 'InProgressDocumentState'
|
|
13
|
+
FAILED = 'ErrorDocumentState'
|
|
14
|
+
FINISHED = 'DoneDocumentState'
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DocumentTemplatePhase(enum.Enum):
|
|
18
|
+
RELEASED = 'ReleasedTemplatePhase'
|
|
19
|
+
DEPRECATED = 'DeprecatedTemplatePhase'
|
|
20
|
+
DRAFT = 'DraftTemplatePhase'
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclasses.dataclass
|
|
24
|
+
class DBComponent:
|
|
25
|
+
name: str
|
|
26
|
+
version: str
|
|
27
|
+
built_at: datetime.datetime
|
|
28
|
+
created_at: datetime.datetime
|
|
29
|
+
updated_at: datetime.datetime
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def from_dict_row(data: dict):
|
|
33
|
+
return DBComponent(
|
|
34
|
+
name=data['name'],
|
|
35
|
+
version=data['version'],
|
|
36
|
+
built_at=data['built_at'],
|
|
37
|
+
created_at=data['created_at'],
|
|
38
|
+
updated_at=data['updated_at'],
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclasses.dataclass
|
|
43
|
+
class DBDocument:
|
|
44
|
+
uuid: str
|
|
45
|
+
name: str
|
|
46
|
+
state: str
|
|
47
|
+
durability: str
|
|
48
|
+
project_uuid: str | None
|
|
49
|
+
project_event_uuid: str | None
|
|
50
|
+
project_replies_hash: str
|
|
51
|
+
document_template_id: str
|
|
52
|
+
format_uuid: str
|
|
53
|
+
file_name: str
|
|
54
|
+
content_type: str
|
|
55
|
+
worker_log: str
|
|
56
|
+
created_by: str
|
|
57
|
+
retrieved_at: datetime.datetime | None
|
|
58
|
+
finished_at: datetime.datetime | None
|
|
59
|
+
created_at: datetime.datetime
|
|
60
|
+
tenant_uuid: str
|
|
61
|
+
file_size: int
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def from_dict_row(data: dict):
|
|
65
|
+
project_uuid = data['project_uuid']
|
|
66
|
+
event_uuid = data['project_event_uuid']
|
|
67
|
+
return DBDocument(
|
|
68
|
+
uuid=str(data['uuid']),
|
|
69
|
+
name=data['name'],
|
|
70
|
+
state=data['state'],
|
|
71
|
+
durability=data['durability'],
|
|
72
|
+
project_uuid=str(project_uuid) if project_uuid else None,
|
|
73
|
+
project_event_uuid=str(event_uuid) if event_uuid else None,
|
|
74
|
+
project_replies_hash=data['project_replies_hash'],
|
|
75
|
+
document_template_id=data['document_template_id'],
|
|
76
|
+
format_uuid=str(data['format_uuid']),
|
|
77
|
+
created_by=str(data['created_by']),
|
|
78
|
+
retrieved_at=data['retrieved_at'],
|
|
79
|
+
finished_at=data['finished_at'],
|
|
80
|
+
created_at=data['created_at'],
|
|
81
|
+
file_name=data['file_name'],
|
|
82
|
+
content_type=data['content_type'],
|
|
83
|
+
worker_log=data['worker_log'],
|
|
84
|
+
tenant_uuid=str(data.get('tenant_uuid', NULL_UUID)),
|
|
85
|
+
file_size=data['file_size'],
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclasses.dataclass
|
|
90
|
+
class DBDocumentTemplate:
|
|
91
|
+
id: str
|
|
92
|
+
name: str
|
|
93
|
+
organization_id: str
|
|
94
|
+
template_id: str
|
|
95
|
+
version: str
|
|
96
|
+
metamodel_version: int
|
|
97
|
+
description: str
|
|
98
|
+
readme: str
|
|
99
|
+
license: str
|
|
100
|
+
allowed_packages: dict
|
|
101
|
+
formats: list
|
|
102
|
+
phase: str
|
|
103
|
+
created_at: datetime.datetime
|
|
104
|
+
updated_at: datetime.datetime
|
|
105
|
+
tenant_uuid: str
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def is_draft(self):
|
|
109
|
+
return self.phase == DocumentTemplatePhase.DRAFT
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def is_released(self):
|
|
113
|
+
return self.phase == DocumentTemplatePhase.RELEASED
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def is_deprecated(self):
|
|
117
|
+
return self.phase == DocumentTemplatePhase.DEPRECATED
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def from_dict_row(data: dict) -> 'DBDocumentTemplate':
|
|
121
|
+
return DBDocumentTemplate(
|
|
122
|
+
id=data['id'],
|
|
123
|
+
name=data['name'],
|
|
124
|
+
organization_id=data['organization_id'],
|
|
125
|
+
template_id=data['template_id'],
|
|
126
|
+
version=data['version'],
|
|
127
|
+
metamodel_version=data['metamodel_version'],
|
|
128
|
+
description=data['description'],
|
|
129
|
+
readme=data['readme'],
|
|
130
|
+
license=data['license'],
|
|
131
|
+
allowed_packages=data['allowed_packages'],
|
|
132
|
+
formats=[],
|
|
133
|
+
phase=data['phase'],
|
|
134
|
+
created_at=data['created_at'],
|
|
135
|
+
updated_at=data['updated_at'],
|
|
136
|
+
tenant_uuid=str(data.get('tenant_uuid', NULL_UUID)),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclasses.dataclass
|
|
141
|
+
class DBDocumentTemplateFormat:
|
|
142
|
+
document_template_id: str
|
|
143
|
+
uuid: str
|
|
144
|
+
name: str
|
|
145
|
+
icon: str
|
|
146
|
+
created_at: datetime.datetime
|
|
147
|
+
updated_at: datetime.datetime
|
|
148
|
+
tenant_uuid: str
|
|
149
|
+
|
|
150
|
+
@staticmethod
|
|
151
|
+
def from_dict_row(data: dict) -> 'DBDocumentTemplateFormat':
|
|
152
|
+
return DBDocumentTemplateFormat(
|
|
153
|
+
document_template_id=data['document_template_id'],
|
|
154
|
+
uuid=str(data['uuid']),
|
|
155
|
+
name=data['name'],
|
|
156
|
+
icon=data['icon'],
|
|
157
|
+
created_at=data['created_at'],
|
|
158
|
+
updated_at=data['updated_at'],
|
|
159
|
+
tenant_uuid=str(data.get('tenant_uuid', NULL_UUID)),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclasses.dataclass
|
|
164
|
+
class DBDocumentTemplateStep:
|
|
165
|
+
document_template_id: str
|
|
166
|
+
format_uuid: str
|
|
167
|
+
position: int
|
|
168
|
+
name: str
|
|
169
|
+
options: dict[str, str]
|
|
170
|
+
created_at: datetime.datetime
|
|
171
|
+
updated_at: datetime.datetime
|
|
172
|
+
tenant_uuid: str
|
|
173
|
+
|
|
174
|
+
@staticmethod
|
|
175
|
+
def from_dict_row(data: dict) -> 'DBDocumentTemplateStep':
|
|
176
|
+
return DBDocumentTemplateStep(
|
|
177
|
+
document_template_id=data['document_template_id'],
|
|
178
|
+
format_uuid=str(data['format_uuid']),
|
|
179
|
+
position=data['position'],
|
|
180
|
+
name=data['name'],
|
|
181
|
+
options=data['options'],
|
|
182
|
+
created_at=data['created_at'],
|
|
183
|
+
updated_at=data['updated_at'],
|
|
184
|
+
tenant_uuid=str(data.get('tenant_uuid', NULL_UUID)),
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@dataclasses.dataclass
|
|
189
|
+
class DBDocumentTemplateFile:
|
|
190
|
+
document_template_id: str
|
|
191
|
+
uuid: str
|
|
192
|
+
file_name: str
|
|
193
|
+
content: str
|
|
194
|
+
created_at: datetime.datetime
|
|
195
|
+
updated_at: datetime.datetime
|
|
196
|
+
tenant_uuid: str
|
|
197
|
+
|
|
198
|
+
@staticmethod
|
|
199
|
+
def from_dict_row(data: dict) -> 'DBDocumentTemplateFile':
|
|
200
|
+
return DBDocumentTemplateFile(
|
|
201
|
+
document_template_id=data['document_template_id'],
|
|
202
|
+
uuid=str(data['uuid']),
|
|
203
|
+
file_name=data['file_name'],
|
|
204
|
+
content=data['content'],
|
|
205
|
+
created_at=data['created_at'],
|
|
206
|
+
updated_at=data['updated_at'],
|
|
207
|
+
tenant_uuid=str(data.get('tenant_uuid', NULL_UUID)),
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@dataclasses.dataclass
|
|
212
|
+
class DBDocumentTemplateAsset:
|
|
213
|
+
document_template_id: str
|
|
214
|
+
uuid: str
|
|
215
|
+
file_name: str
|
|
216
|
+
content_type: str
|
|
217
|
+
file_size: int
|
|
218
|
+
created_at: datetime.datetime
|
|
219
|
+
updated_at: datetime.datetime
|
|
220
|
+
tenant_uuid: str
|
|
221
|
+
|
|
222
|
+
@staticmethod
|
|
223
|
+
def from_dict_row(data: dict) -> 'DBDocumentTemplateAsset':
|
|
224
|
+
return DBDocumentTemplateAsset(
|
|
225
|
+
document_template_id=data['document_template_id'],
|
|
226
|
+
uuid=str(data['uuid']),
|
|
227
|
+
file_name=data['file_name'],
|
|
228
|
+
content_type=data['content_type'],
|
|
229
|
+
file_size=data['file_size'],
|
|
230
|
+
created_at=data['created_at'],
|
|
231
|
+
updated_at=data['updated_at'],
|
|
232
|
+
tenant_uuid=str(data.get('tenant_uuid', NULL_UUID)),
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@dataclasses.dataclass
|
|
237
|
+
class PersistentCommand:
|
|
238
|
+
uuid: str
|
|
239
|
+
state: str
|
|
240
|
+
component: str
|
|
241
|
+
function: str
|
|
242
|
+
body: dict
|
|
243
|
+
last_error_message: str | None
|
|
244
|
+
attempts: int
|
|
245
|
+
max_attempts: int
|
|
246
|
+
tenant_uuid: str
|
|
247
|
+
created_by: str | None
|
|
248
|
+
created_at: datetime.datetime
|
|
249
|
+
updated_at: datetime.datetime
|
|
250
|
+
|
|
251
|
+
@staticmethod
|
|
252
|
+
def from_dict_row(data: dict):
|
|
253
|
+
return PersistentCommand(
|
|
254
|
+
uuid=str(data['uuid']),
|
|
255
|
+
state=data['state'],
|
|
256
|
+
component=data['component'],
|
|
257
|
+
function=data['function'],
|
|
258
|
+
body=json.loads(data['body']),
|
|
259
|
+
last_error_message=data['last_error_message'],
|
|
260
|
+
attempts=data['attempts'],
|
|
261
|
+
max_attempts=data['max_attempts'],
|
|
262
|
+
created_by=str(data['created_by']),
|
|
263
|
+
created_at=data['created_at'],
|
|
264
|
+
updated_at=data['updated_at'],
|
|
265
|
+
tenant_uuid=str(data.get('tenant_uuid', NULL_UUID)),
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
@dataclasses.dataclass
|
|
270
|
+
class DBTenantLimits:
|
|
271
|
+
tenant_uuid: str
|
|
272
|
+
storage: int | None
|
|
273
|
+
|
|
274
|
+
@staticmethod
|
|
275
|
+
def from_dict_row(data: dict):
|
|
276
|
+
return DBTenantLimits(
|
|
277
|
+
tenant_uuid=str(data['uuid']),
|
|
278
|
+
storage=data['storage'],
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@dataclasses.dataclass
|
|
283
|
+
class DBSubmission:
|
|
284
|
+
TABLE_NAME = 'submission'
|
|
285
|
+
|
|
286
|
+
uuid: str
|
|
287
|
+
state: str
|
|
288
|
+
location: str
|
|
289
|
+
returned_data: str
|
|
290
|
+
service_id: str
|
|
291
|
+
document_uuid: str
|
|
292
|
+
created_by: str
|
|
293
|
+
created_at: datetime.datetime
|
|
294
|
+
updated_at: datetime.datetime
|
|
295
|
+
tenant_uuid: str
|
|
296
|
+
|
|
297
|
+
@staticmethod
|
|
298
|
+
def from_dict_row(data: dict):
|
|
299
|
+
return DBSubmission(
|
|
300
|
+
uuid=str(data['uuid']),
|
|
301
|
+
state=data['state'],
|
|
302
|
+
location=data['location'],
|
|
303
|
+
returned_data=data['returned_data'],
|
|
304
|
+
service_id=data['service_id'],
|
|
305
|
+
document_uuid=str(data['document_uuid']),
|
|
306
|
+
created_by=str(data['created_by']),
|
|
307
|
+
created_at=data['created_at'],
|
|
308
|
+
updated_at=data['updated_at'],
|
|
309
|
+
tenant_uuid=str(data.get('tenant_uuid', NULL_UUID)),
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
def to_dict(self) -> dict:
|
|
313
|
+
return {
|
|
314
|
+
'uuid': self.uuid,
|
|
315
|
+
'state': self.state,
|
|
316
|
+
'location': self.location,
|
|
317
|
+
'returned_data': self.returned_data,
|
|
318
|
+
'service_id': self.service_id,
|
|
319
|
+
'document_uuid': self.document_uuid,
|
|
320
|
+
'created_by': self.created_by,
|
|
321
|
+
'created_at': self.created_at.isoformat(timespec='milliseconds'),
|
|
322
|
+
'updated_at': self.updated_at.isoformat(timespec='milliseconds'),
|
|
323
|
+
'tenant_uuid': self.tenant_uuid,
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@dataclasses.dataclass
|
|
328
|
+
class DBProjectSimple:
|
|
329
|
+
# without: events, answered_questions, unanswered_questions,
|
|
330
|
+
# squashed, versions, selected_question_tag_uuids
|
|
331
|
+
TABLE_NAME = 'project'
|
|
332
|
+
|
|
333
|
+
uuid: str
|
|
334
|
+
name: str
|
|
335
|
+
visibility: str
|
|
336
|
+
sharing: str
|
|
337
|
+
package_id: str
|
|
338
|
+
document_template_id: str
|
|
339
|
+
format_uuid: str
|
|
340
|
+
created_by: str
|
|
341
|
+
created_at: datetime.datetime
|
|
342
|
+
updated_at: datetime.datetime
|
|
343
|
+
description: str
|
|
344
|
+
is_template: bool
|
|
345
|
+
project_tags: list[str]
|
|
346
|
+
tenant_uuid: str
|
|
347
|
+
|
|
348
|
+
@staticmethod
|
|
349
|
+
def from_dict_row(data: dict):
|
|
350
|
+
return DBProjectSimple(
|
|
351
|
+
uuid=str(data['uuid']),
|
|
352
|
+
name=data['name'],
|
|
353
|
+
visibility=data['visibility'],
|
|
354
|
+
sharing=data['sharing'],
|
|
355
|
+
package_id=data['package_id'],
|
|
356
|
+
document_template_id=data['document_template_id'],
|
|
357
|
+
format_uuid=str(data['format_uuid']),
|
|
358
|
+
created_by=str(data['created_by']),
|
|
359
|
+
created_at=data['created_at'],
|
|
360
|
+
updated_at=data['updated_at'],
|
|
361
|
+
description=data['description'],
|
|
362
|
+
is_template=data['is_template'],
|
|
363
|
+
project_tags=data['project_tags'],
|
|
364
|
+
tenant_uuid=str(data.get('tenant_uuid', NULL_UUID)),
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
def to_dict(self) -> dict:
|
|
368
|
+
return {
|
|
369
|
+
'uuid': self.uuid,
|
|
370
|
+
'name': self.name,
|
|
371
|
+
'visibility': self.visibility,
|
|
372
|
+
'sharing': self.sharing,
|
|
373
|
+
'package_id': self.package_id,
|
|
374
|
+
'document_template_id': self.document_template_id,
|
|
375
|
+
'format_uuid': self.format_uuid,
|
|
376
|
+
'created_by': self.created_by,
|
|
377
|
+
'created_at': self.created_at.isoformat(timespec='milliseconds'),
|
|
378
|
+
'updated_at': self.updated_at.isoformat(timespec='milliseconds'),
|
|
379
|
+
'description': self.description,
|
|
380
|
+
'is_template': self.is_template,
|
|
381
|
+
'project_tags': self.project_tags,
|
|
382
|
+
'tenant_uuid': self.tenant_uuid,
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
@dataclasses.dataclass
|
|
387
|
+
class DBUserEntity:
|
|
388
|
+
TABLE_NAME = 'user_entity'
|
|
389
|
+
|
|
390
|
+
uuid: str
|
|
391
|
+
first_name: str
|
|
392
|
+
last_name: str
|
|
393
|
+
email: str
|
|
394
|
+
locale: str | None
|
|
395
|
+
|
|
396
|
+
@staticmethod
|
|
397
|
+
def from_dict_row(data: dict):
|
|
398
|
+
return DBUserEntity(
|
|
399
|
+
uuid=str(data['uuid']),
|
|
400
|
+
first_name=data['first_name'],
|
|
401
|
+
last_name=data['last_name'],
|
|
402
|
+
email=data['email'],
|
|
403
|
+
locale=data['locale'],
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
@dataclasses.dataclass
|
|
408
|
+
class DBLocale:
|
|
409
|
+
TABLE_NAME = 'locale'
|
|
410
|
+
|
|
411
|
+
uuid: str
|
|
412
|
+
organization_id: str
|
|
413
|
+
locale_id: str
|
|
414
|
+
version: str
|
|
415
|
+
name: str
|
|
416
|
+
code: str
|
|
417
|
+
default_locale: bool
|
|
418
|
+
enabled: bool
|
|
419
|
+
|
|
420
|
+
@staticmethod
|
|
421
|
+
def from_dict_row(data: dict):
|
|
422
|
+
return DBLocale(
|
|
423
|
+
uuid=str(data['uuid']),
|
|
424
|
+
organization_id=data['organization_id'],
|
|
425
|
+
locale_id=data['locale_id'],
|
|
426
|
+
version=data['version'],
|
|
427
|
+
name=data['name'],
|
|
428
|
+
code=data['code'],
|
|
429
|
+
default_locale=data['default_locale'],
|
|
430
|
+
enabled=data['enabled'],
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
@property
|
|
434
|
+
def id(self) -> str:
|
|
435
|
+
return f'{self.organization_id}:{self.locale_id}:{self.version}'
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
@dataclasses.dataclass
|
|
439
|
+
class DBInstanceConfigMail:
|
|
440
|
+
TABLE_NAME = 'instance_config_mail'
|
|
441
|
+
|
|
442
|
+
uuid: str
|
|
443
|
+
enabled: bool
|
|
444
|
+
provider: str
|
|
445
|
+
sender_name: str | None
|
|
446
|
+
sender_email: str | None
|
|
447
|
+
smtp_host: str | None
|
|
448
|
+
smtp_port: int | None
|
|
449
|
+
smtp_security: str | None
|
|
450
|
+
smtp_username: str | None
|
|
451
|
+
smtp_password: str | None
|
|
452
|
+
aws_access_key_id: str | None
|
|
453
|
+
aws_secret_access_key: str | None
|
|
454
|
+
aws_region: str | None
|
|
455
|
+
rate_limit_window: int | None
|
|
456
|
+
rate_limit_count: int | None
|
|
457
|
+
timeout: int | None
|
|
458
|
+
|
|
459
|
+
@staticmethod
|
|
460
|
+
def from_dict_row(data: dict):
|
|
461
|
+
return DBInstanceConfigMail(
|
|
462
|
+
uuid=str(data['uuid']),
|
|
463
|
+
enabled=data['enabled'],
|
|
464
|
+
provider=data['provider'],
|
|
465
|
+
sender_name=data['sender_name'],
|
|
466
|
+
sender_email=data['sender_email'],
|
|
467
|
+
smtp_host=data['smtp_host'],
|
|
468
|
+
smtp_port=data['smtp_port'],
|
|
469
|
+
smtp_security=data['smtp_security'],
|
|
470
|
+
smtp_username=data['smtp_username'],
|
|
471
|
+
smtp_password=data['smtp_password'],
|
|
472
|
+
aws_access_key_id=data['aws_access_key_id'],
|
|
473
|
+
aws_secret_access_key=data['aws_secret_access_key'],
|
|
474
|
+
aws_region=data['aws_region'],
|
|
475
|
+
rate_limit_window=data['rate_limit_window'],
|
|
476
|
+
rate_limit_count=data['rate_limit_count'],
|
|
477
|
+
timeout=data['timeout'],
|
|
478
|
+
)
|
dsw/database/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: dsw-database
|
|
3
|
+
Version: 4.27.0
|
|
4
|
+
Summary: Library for managing DSW database
|
|
5
|
+
Keywords: dsw,database
|
|
6
|
+
Author: Marek Suchánek
|
|
7
|
+
Author-email: Marek Suchánek <marek.suchanek@ds-wizard.org>
|
|
8
|
+
License: Apache License 2.0
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
|
+
Classifier: Programming Language :: Python
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Topic :: Database
|
|
15
|
+
Classifier: Topic :: Utilities
|
|
16
|
+
Requires-Dist: psycopg[binary]
|
|
17
|
+
Requires-Dist: tenacity
|
|
18
|
+
Requires-Dist: dsw-config==4.27.0
|
|
19
|
+
Requires-Python: >=3.12, <4
|
|
20
|
+
Project-URL: Homepage, https://ds-wizard.org
|
|
21
|
+
Project-URL: Repository, https://github.com/ds-wizard/engine-tools
|
|
22
|
+
Project-URL: Documentation, https://guide.ds-wizard.org
|
|
23
|
+
Project-URL: Issues, https://github.com/ds-wizard/ds-wizard/issues
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# Data Stewardship Wizard: Database
|
|
27
|
+
|
|
28
|
+
[](https://github.com/ds-wizard/engine-tools/releases)
|
|
29
|
+
[](https://pypi.org/project/dsw-database/)
|
|
30
|
+
[](LICENSE)
|
|
31
|
+
[](https://bestpractices.coreinfrastructure.org/projects/4975)
|
|
32
|
+
[](https://python.org)
|
|
33
|
+
|
|
34
|
+
*Library for working with DSW database*
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
Currently, this library is intended for internal use of DSW tooling only.
|
|
39
|
+
Enhancements for use in custom scripts are planned for future development.
|
|
40
|
+
|
|
41
|
+
## License
|
|
42
|
+
|
|
43
|
+
This project is licensed under the Apache License v2.0 - see the
|
|
44
|
+
[LICENSE](LICENSE) file for more details.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
dsw/database/__init__.py,sha256=vS926jtn7Xz0jaa83T7llc6f4xpK0wqWYs-CgMO6dVk,56
|
|
2
|
+
dsw/database/build_info.py,sha256=7FfJmNzkLR73Ykl52_X9BmhPIdFBrJmU7vc_pz9IxY8,381
|
|
3
|
+
dsw/database/database.py,sha256=s9E_TTx46GY-rrTs06M-hexegHdKz9s8zsWzptdTbes,27076
|
|
4
|
+
dsw/database/model.py,sha256=BNBK-hAjgUKFAFTIFrO_8fjH0slrGlXJVQlz7SD6EhM,13923
|
|
5
|
+
dsw/database/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
dsw_database-4.27.0.dist-info/WHEEL,sha256=fAguSjoiATBe7TNBkJwOjyL1Tt4wwiaQGtNtjRPNMQA,80
|
|
7
|
+
dsw_database-4.27.0.dist-info/METADATA,sha256=zEsK2DVPdfDIBLrB6ubz8OxJ4cnXso8xCuUZcmSb0no,1891
|
|
8
|
+
dsw_database-4.27.0.dist-info/RECORD,,
|