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.
@@ -0,0 +1,4 @@
1
+ from .database import Database
2
+
3
+
4
+ __all__ = ['Database']
@@ -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
+ )
@@ -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
+ [![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/ds-wizard/engine-tools)](https://github.com/ds-wizard/engine-tools/releases)
29
+ [![PyPI](https://img.shields.io/pypi/v/dsw-database)](https://pypi.org/project/dsw-database/)
30
+ [![LICENSE](https://img.shields.io/github/license/ds-wizard/engine-tools)](LICENSE)
31
+ [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/4975/badge)](https://bestpractices.coreinfrastructure.org/projects/4975)
32
+ [![Python Version](https://img.shields.io/badge/Python-%E2%89%A5%203.7-blue)](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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.9.28
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any