SQLDbWrpr 5.0.0__py3-none-any.whl → 5.1.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.
- sqldbwrpr/sqldbwrpr.py +475 -148
- {sqldbwrpr-5.0.0.dist-info → sqldbwrpr-5.1.0.dist-info}/METADATA +176 -10
- sqldbwrpr-5.1.0.dist-info/RECORD +6 -0
- {sqldbwrpr-5.0.0.dist-info → sqldbwrpr-5.1.0.dist-info}/WHEEL +1 -1
- sqldbwrpr-5.0.0.dist-info/RECORD +0 -6
- {sqldbwrpr-5.0.0.dist-info → sqldbwrpr-5.1.0.dist-info}/licenses/LICENSE.txt +0 -0
sqldbwrpr/sqldbwrpr.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Database wrapper utilities for MySQL and PostgreSQL.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
SqlDbWrpr creates databases and tables from either the legacy dictionary
|
|
4
|
+
structure or SQLAlchemy metadata, then imports and exports CSV data through a
|
|
5
|
+
small DB-API based wrapper.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import datetime
|
|
@@ -14,9 +14,8 @@ import csvwrpr
|
|
|
14
14
|
import displayfx
|
|
15
15
|
import fixdate
|
|
16
16
|
import mysql.connector
|
|
17
|
-
import
|
|
17
|
+
import psycopg
|
|
18
18
|
from beetools import msg as bm
|
|
19
|
-
from mysql.connector import Error
|
|
20
19
|
from mysql.connector import errorcode
|
|
21
20
|
|
|
22
21
|
# from pathlib import Path
|
|
@@ -27,8 +26,12 @@ from mysql.connector import errorcode
|
|
|
27
26
|
# _PROJ_NAME = _PROJ_PATH.stem
|
|
28
27
|
|
|
29
28
|
|
|
29
|
+
class SchemaSourceError(ValueError):
|
|
30
|
+
"""Raised when no supported database schema source is supplied."""
|
|
31
|
+
|
|
32
|
+
|
|
30
33
|
class SQLDbWrpr:
|
|
31
|
-
"""
|
|
34
|
+
"""Base wrapper for schema creation and CSV import/export operations."""
|
|
32
35
|
|
|
33
36
|
def __init__(
|
|
34
37
|
self,
|
|
@@ -38,6 +41,8 @@ class SQLDbWrpr:
|
|
|
38
41
|
p_recreate_db=False,
|
|
39
42
|
p_db_name="",
|
|
40
43
|
p_db_structure=None,
|
|
44
|
+
p_sqlalchemy_base=None,
|
|
45
|
+
p_sqlalchemy_metadata=None,
|
|
41
46
|
p_batch_size=10000,
|
|
42
47
|
p_bar_len=50,
|
|
43
48
|
p_msg_width=50,
|
|
@@ -47,18 +52,25 @@ class SQLDbWrpr:
|
|
|
47
52
|
p_ssl_key=None,
|
|
48
53
|
p_ssl_cert=None,
|
|
49
54
|
):
|
|
50
|
-
"""
|
|
55
|
+
"""Initialize common wrapper state and resolve the database schema.
|
|
56
|
+
|
|
57
|
+
A legacy `p_db_structure` dictionary takes precedence. If it is not
|
|
58
|
+
supplied, SQLAlchemy metadata is read from `p_sqlalchemy_metadata` or
|
|
59
|
+
from `p_sqlalchemy_base.metadata`. A `SchemaSourceError` is raised when
|
|
60
|
+
none of those schema sources are available.
|
|
51
61
|
|
|
52
62
|
Parameters
|
|
53
|
-
- p_host_name
|
|
54
|
-
- p_user_name
|
|
55
|
-
- p_password
|
|
56
|
-
-
|
|
57
|
-
-
|
|
58
|
-
-
|
|
59
|
-
-
|
|
60
|
-
-
|
|
61
|
-
-
|
|
63
|
+
- p_host_name: Host to connect to.
|
|
64
|
+
- p_user_name: User name for the connection.
|
|
65
|
+
- p_password: Password for the connection.
|
|
66
|
+
- p_recreate_db: Recreate the database when the backend supports it.
|
|
67
|
+
- p_db_name: Database name.
|
|
68
|
+
- p_db_structure: Legacy SqlDbWrpr table/field dictionary.
|
|
69
|
+
- p_sqlalchemy_base: Declarative base that exposes SQLAlchemy metadata.
|
|
70
|
+
- p_sqlalchemy_metadata: SQLAlchemy MetaData instance.
|
|
71
|
+
- p_batch_size: Number of rows committed per import batch.
|
|
72
|
+
- p_bar_len: Length of progress bars.
|
|
73
|
+
- p_msg_width: Width of progress messages.
|
|
62
74
|
"""
|
|
63
75
|
self.logger_name = __name__
|
|
64
76
|
self.logger = logging.getLogger(self.logger_name)
|
|
@@ -70,10 +82,12 @@ class SQLDbWrpr:
|
|
|
70
82
|
self.conn = None
|
|
71
83
|
self.cur = None
|
|
72
84
|
self.db_name = p_db_name
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
85
|
+
self.db_structure = self.resolve_db_structure(
|
|
86
|
+
p_db_structure=p_db_structure,
|
|
87
|
+
p_sqlalchemy_base=p_sqlalchemy_base,
|
|
88
|
+
p_sqlalchemy_metadata=p_sqlalchemy_metadata,
|
|
89
|
+
)
|
|
90
|
+
self.db_error = mysql.connector.Error
|
|
77
91
|
self.delimiter = ","
|
|
78
92
|
self.fkey_ref_act = {
|
|
79
93
|
"C": "CASCADE",
|
|
@@ -82,7 +96,9 @@ class SQLDbWrpr:
|
|
|
82
96
|
"N": "SET NULL",
|
|
83
97
|
}
|
|
84
98
|
self.host_name = p_host_name
|
|
99
|
+
self.identifier_quote = ""
|
|
85
100
|
self.idx_type = {"U": "UNIQUE", "F": "FULLTEXT", "S": "SPATIAL"}
|
|
101
|
+
self.inline_indexes = True
|
|
86
102
|
self.msg_width = p_msg_width
|
|
87
103
|
self.non_char_fields = {}
|
|
88
104
|
self._password = p_password
|
|
@@ -95,10 +111,75 @@ class SQLDbWrpr:
|
|
|
95
111
|
self.db_port = p_db_port
|
|
96
112
|
|
|
97
113
|
def close(self):
|
|
98
|
-
"""Close the
|
|
114
|
+
"""Close the active database connection."""
|
|
99
115
|
if self.conn:
|
|
100
116
|
self.conn.close()
|
|
101
117
|
|
|
118
|
+
def build_column_sql(self, p_field_name, p_field_type, p_field_params, p_field_comment):
|
|
119
|
+
"""Build SQL for one column in a CREATE TABLE statement."""
|
|
120
|
+
sql_str = f"{self.quote_identifier(p_field_name)} {self.render_field_type(p_field_type, p_field_params)}"
|
|
121
|
+
if p_field_params["AI"] == "Y":
|
|
122
|
+
sql_str += " AUTO_INCREMENT"
|
|
123
|
+
if p_field_params["UN"] == "Y" and p_field_params["AI"] != "Y":
|
|
124
|
+
sql_str += " UNSIGNED"
|
|
125
|
+
if p_field_params["NN"] == "Y":
|
|
126
|
+
sql_str += " NOT NULL"
|
|
127
|
+
if p_field_params["ZF"] == "Y":
|
|
128
|
+
sql_str += " ZEROFILL"
|
|
129
|
+
if p_field_params["DEF"]:
|
|
130
|
+
sql_str += self.render_default_sql(p_field_type, p_field_params["DEF"])
|
|
131
|
+
if p_field_comment:
|
|
132
|
+
sql_str += f' COMMENT "{p_field_comment}"'
|
|
133
|
+
return sql_str
|
|
134
|
+
|
|
135
|
+
def build_index_sql(self, p_table_name, p_idx_name, p_idx_fields, p_unique=False):
|
|
136
|
+
"""Build an index clause for the active SQL dialect."""
|
|
137
|
+
idx_type = "UNIQUE INDEX" if p_unique else "INDEX"
|
|
138
|
+
index_fields = []
|
|
139
|
+
for field_det in p_idx_fields:
|
|
140
|
+
index_fields.append(f"{self.quote_identifier(field_det[0])} {self.sort_order[field_det[2]]}")
|
|
141
|
+
return f"{idx_type} {self.quote_identifier(p_idx_name)} ({','.join(index_fields)}) VISIBLE, "
|
|
142
|
+
|
|
143
|
+
def build_insert_sql(self, p_table_name, p_header, p_replace=False):
|
|
144
|
+
"""Build SQL for inserting rows into a table."""
|
|
145
|
+
insert_command = "REPLACE" if p_replace else "INSERT"
|
|
146
|
+
return "{} INTO {} ({}) VALUES ({})".format(
|
|
147
|
+
insert_command,
|
|
148
|
+
self.quote_identifier(p_table_name),
|
|
149
|
+
",".join([self.quote_identifier(str(x)) for x in p_header]),
|
|
150
|
+
",".join([self.param_placeholder() for x in range(len(p_header))]),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def param_placeholder(self):
|
|
154
|
+
"""Return the DB-API placeholder used by this backend."""
|
|
155
|
+
return "%s"
|
|
156
|
+
|
|
157
|
+
def quote_identifier(self, p_identifier):
|
|
158
|
+
"""Quote an SQL identifier for this backend."""
|
|
159
|
+
if not self.identifier_quote:
|
|
160
|
+
return str(p_identifier)
|
|
161
|
+
escaped_identifier = str(p_identifier).replace(self.identifier_quote, self.identifier_quote * 2)
|
|
162
|
+
return f"{self.identifier_quote}{escaped_identifier}{self.identifier_quote}"
|
|
163
|
+
|
|
164
|
+
def quote_identifier_list(self, p_identifiers):
|
|
165
|
+
"""Quote and join SQL identifiers."""
|
|
166
|
+
return ",".join([self.quote_identifier(p_identifier) for p_identifier in p_identifiers])
|
|
167
|
+
|
|
168
|
+
def render_default_sql(self, p_field_type, p_default_value):
|
|
169
|
+
"""Render a column default expression."""
|
|
170
|
+
if p_field_type[0] == "varchar" or p_field_type[0] == "char":
|
|
171
|
+
return f' DEFAULT "{p_default_value}"'
|
|
172
|
+
return f" DEFAULT {p_default_value}"
|
|
173
|
+
|
|
174
|
+
def render_field_type(self, p_field_type, p_field_params):
|
|
175
|
+
"""Render a field type for this backend."""
|
|
176
|
+
field_type = p_field_type[0]
|
|
177
|
+
if field_type == "varchar" or field_type == "char":
|
|
178
|
+
return f"{field_type} ({str(p_field_type[1])})"
|
|
179
|
+
if field_type == "decimal":
|
|
180
|
+
return f"{field_type}({str(p_field_type[1])}, {str(p_field_type[2])})"
|
|
181
|
+
return field_type
|
|
182
|
+
|
|
102
183
|
def create_db(self):
|
|
103
184
|
"""Create the database according to self.db_structure."""
|
|
104
185
|
self.cur.execute("SHOW DATABASES")
|
|
@@ -108,7 +189,7 @@ class SQLDbWrpr:
|
|
|
108
189
|
try:
|
|
109
190
|
self.cur.execute(f"DROP DATABASE {self.db_name}")
|
|
110
191
|
self.conn.commit()
|
|
111
|
-
except
|
|
192
|
+
except self.db_error as err:
|
|
112
193
|
self._print_err_msg(err, "Could not drop the database")
|
|
113
194
|
self.close()
|
|
114
195
|
sys.exit()
|
|
@@ -117,23 +198,23 @@ class SQLDbWrpr:
|
|
|
117
198
|
self.conn.commit()
|
|
118
199
|
self.cur.execute(f"USE {self.db_name}")
|
|
119
200
|
self.conn.commit()
|
|
120
|
-
except
|
|
201
|
+
except self.db_error as err:
|
|
121
202
|
self._print_err_msg(err, "Could not create the database")
|
|
122
203
|
self.close()
|
|
123
204
|
sys.exit()
|
|
124
205
|
return True
|
|
125
206
|
|
|
126
207
|
def create_tables(self):
|
|
127
|
-
"""Create
|
|
208
|
+
"""Create database tables, indexes, and constraints from the resolved schema."""
|
|
128
209
|
|
|
129
210
|
def build_db(p_db_sql_str_set):
|
|
130
|
-
"""
|
|
211
|
+
"""Execute the generated table or index SQL statements."""
|
|
131
212
|
for sql_set in p_db_sql_str_set:
|
|
132
213
|
try:
|
|
133
214
|
self.cur.execute(sql_set[1])
|
|
134
215
|
if self.silent:
|
|
135
216
|
print(f"Created table = {sql_set[0]}")
|
|
136
|
-
except
|
|
217
|
+
except self.db_error as err:
|
|
137
218
|
print(f"Failed creating table = {sql_set[0]}: {err}\nForced termination of program")
|
|
138
219
|
print(f"{sql_set[1]}")
|
|
139
220
|
sys.exit()
|
|
@@ -147,7 +228,7 @@ class SQLDbWrpr:
|
|
|
147
228
|
p_idx_set_up_list,
|
|
148
229
|
p_constraint_set_up_list,
|
|
149
230
|
):
|
|
150
|
-
"""
|
|
231
|
+
"""Combine column, primary-key, index, and constraint SQL."""
|
|
151
232
|
table_set_up_str = p_table_set_up_str
|
|
152
233
|
table_set_up_str += p_primary_key_str
|
|
153
234
|
for idx_str in p_idx_set_up_list:
|
|
@@ -161,7 +242,7 @@ class SQLDbWrpr:
|
|
|
161
242
|
|
|
162
243
|
def build_constraints(p_table_name):
|
|
163
244
|
# noinspection PySingleQuotedDocstring
|
|
164
|
-
"""
|
|
245
|
+
"""Build foreign-key constraint SQL for one table."""
|
|
165
246
|
constraint_list = []
|
|
166
247
|
fkey_nr_list = []
|
|
167
248
|
for field_name in self.db_structure[p_table_name]:
|
|
@@ -173,9 +254,9 @@ class SQLDbWrpr:
|
|
|
173
254
|
"CONSTRAINT fk_{}_{} FOREIGN KEY ({}) REFERENCES {} ({}) ON DELETE {} ON UPDATE {}, ".format(
|
|
174
255
|
fkey["FKeyTable"],
|
|
175
256
|
fkey["RefTable"],
|
|
176
|
-
|
|
177
|
-
fkey["RefTable"],
|
|
178
|
-
|
|
257
|
+
self.quote_identifier_list(fkey["FKeyFlds"]),
|
|
258
|
+
self.quote_identifier(fkey["RefTable"]),
|
|
259
|
+
self.quote_identifier_list(fkey["RefFields"]),
|
|
179
260
|
self.fkey_ref_act[fkey["OnDelete"]],
|
|
180
261
|
self.fkey_ref_act[fkey["OnUpdate"]],
|
|
181
262
|
)
|
|
@@ -187,10 +268,10 @@ class SQLDbWrpr:
|
|
|
187
268
|
# def build_constraints
|
|
188
269
|
|
|
189
270
|
def build_all_indexes(p_table_name):
|
|
190
|
-
"""
|
|
271
|
+
"""Build inline or post-create index SQL for one table."""
|
|
191
272
|
|
|
192
273
|
# def build_primary_key_idx(p_table_name):
|
|
193
|
-
# '''
|
|
274
|
+
# '''Build primary-key index SQL.'''
|
|
194
275
|
# idx_name_list = []
|
|
195
276
|
# idx_str_list = []
|
|
196
277
|
# pkey = get_primary_key(p_table_name)
|
|
@@ -205,7 +286,7 @@ class SQLDbWrpr:
|
|
|
205
286
|
# # end build_primary_key_idx
|
|
206
287
|
|
|
207
288
|
def build_unique_key_idx(p_table_name, p_dx_name_list, p_idx_str_list):
|
|
208
|
-
"""
|
|
289
|
+
"""Build grouped index definitions from legacy field metadata."""
|
|
209
290
|
idx_list = {}
|
|
210
291
|
idx_name_list = p_dx_name_list
|
|
211
292
|
idx_str_list = p_idx_str_list
|
|
@@ -229,14 +310,16 @@ class SQLDbWrpr:
|
|
|
229
310
|
idx_name = f"idx_{idx_name[:-1]}"
|
|
230
311
|
if idx_name not in idx_name_list:
|
|
231
312
|
idx_name_list.append(idx_name)
|
|
232
|
-
|
|
233
|
-
|
|
313
|
+
idx_str = self.build_index_sql(
|
|
314
|
+
p_table_name,
|
|
315
|
+
idx_name,
|
|
316
|
+
idx_instance_order,
|
|
317
|
+
field_det[3] == "U",
|
|
318
|
+
)
|
|
319
|
+
if self.inline_indexes:
|
|
320
|
+
idx_str_list.append(idx_str)
|
|
234
321
|
else:
|
|
235
|
-
|
|
236
|
-
for field_det in idx_instance_order:
|
|
237
|
-
idx_str += f"{field_det[0]} {self.sort_order[field_det[2]]}, "
|
|
238
|
-
idx_str = idx_str[:-2] + ") VISIBLE, "
|
|
239
|
-
idx_str_list.append(idx_str)
|
|
322
|
+
post_create_sql_set.append([idx_name, idx_str])
|
|
240
323
|
return idx_str_list, idx_name_list
|
|
241
324
|
|
|
242
325
|
# end build_unique_key_idx
|
|
@@ -249,47 +332,33 @@ class SQLDbWrpr:
|
|
|
249
332
|
# def build_all_indexes
|
|
250
333
|
|
|
251
334
|
def build_primary_key_sql_str(p_table_name):
|
|
252
|
-
"""
|
|
335
|
+
"""Build the primary-key clause for one table."""
|
|
253
336
|
primary_key_det = get_primary_key(p_table_name)
|
|
254
|
-
sql_str = "PRIMARY KEY ({}), ".format(
|
|
337
|
+
sql_str = "PRIMARY KEY ({}), ".format(self.quote_identifier_list(primary_key_det["Flds"]))
|
|
255
338
|
return sql_str
|
|
256
339
|
|
|
257
340
|
# def build_primary_key_sql_str
|
|
258
341
|
|
|
259
342
|
def build_table_sql_str(p_table_name):
|
|
260
|
-
"""
|
|
261
|
-
sql_str = f"CREATE TABLE {p_table_name} ("
|
|
343
|
+
"""Build the CREATE TABLE prefix and column definitions."""
|
|
344
|
+
sql_str = f"CREATE TABLE {self.quote_identifier(p_table_name)} ("
|
|
262
345
|
for field_name in self.db_structure[p_table_name]:
|
|
263
346
|
field_type_st_ref = self.db_structure[p_table_name][field_name]["Type"]
|
|
264
347
|
field_param_st_ref = self.db_structure[p_table_name][field_name]["Params"]
|
|
265
348
|
field_comment_st_ref = self.db_structure[p_table_name][field_name]["Comment"]
|
|
266
|
-
sql_str +=
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
sql_str += " AUTO_INCREMENT"
|
|
273
|
-
if field_param_st_ref["UN"] == "Y" and field_param_st_ref["AI"] != "Y":
|
|
274
|
-
sql_str += " UNSIGNED"
|
|
275
|
-
if field_param_st_ref["NN"] == "Y":
|
|
276
|
-
sql_str += " NOT NULL"
|
|
277
|
-
if field_param_st_ref["ZF"] == "Y":
|
|
278
|
-
sql_str += " ZEROFILL"
|
|
279
|
-
if field_param_st_ref["DEF"]:
|
|
280
|
-
if field_type_st_ref[0] == "varchar" or field_type_st_ref[0] == "char":
|
|
281
|
-
sql_str += ' DEFAULT "{}"'.format(field_param_st_ref["DEF"])
|
|
282
|
-
else:
|
|
283
|
-
sql_str += " DEFAULT {}".format(field_param_st_ref["DEF"])
|
|
284
|
-
if field_comment_st_ref:
|
|
285
|
-
sql_str += f' COMMENT "{field_comment_st_ref}"'
|
|
349
|
+
sql_str += self.build_column_sql(
|
|
350
|
+
field_name,
|
|
351
|
+
field_type_st_ref,
|
|
352
|
+
field_param_st_ref,
|
|
353
|
+
field_comment_st_ref,
|
|
354
|
+
)
|
|
286
355
|
sql_str += ", "
|
|
287
356
|
return sql_str
|
|
288
357
|
|
|
289
358
|
# end build_table_sql_str
|
|
290
359
|
|
|
291
360
|
def get_foreign_key(p_table_name, p_field_name):
|
|
292
|
-
"""
|
|
361
|
+
"""Return normalized foreign-key metadata for a field."""
|
|
293
362
|
fkey = {
|
|
294
363
|
"Present": False,
|
|
295
364
|
"FKeyFlds": [],
|
|
@@ -327,7 +396,7 @@ class SQLDbWrpr:
|
|
|
327
396
|
# end get_foreign_key
|
|
328
397
|
|
|
329
398
|
def get_primary_key(p_table_name):
|
|
330
|
-
"""
|
|
399
|
+
"""Return normalized primary-key metadata for a table."""
|
|
331
400
|
pkey = {"Present": False, "Flds": (), "SortPairList": [], "SortPairStr": []}
|
|
332
401
|
for field_name in self.db_structure[p_table_name]:
|
|
333
402
|
pkey_field_det = self.db_structure[p_table_name][field_name]
|
|
@@ -351,7 +420,7 @@ class SQLDbWrpr:
|
|
|
351
420
|
# end get_primary_key
|
|
352
421
|
|
|
353
422
|
def order_table_build_list(p_db_sql_str_set, p_constraint_set_up_list):
|
|
354
|
-
"""
|
|
423
|
+
"""Order table creation so referenced tables are created first."""
|
|
355
424
|
db_sql_str_set = p_db_sql_str_set
|
|
356
425
|
ordered = False
|
|
357
426
|
while not ordered:
|
|
@@ -385,13 +454,13 @@ class SQLDbWrpr:
|
|
|
385
454
|
# end order_table_build_list
|
|
386
455
|
|
|
387
456
|
def structure_validation():
|
|
388
|
-
"""
|
|
457
|
+
"""Validate schema relationships before SQL generation."""
|
|
389
458
|
|
|
390
459
|
def check_pkey_fkey_overlap(p_remove_fkey_pkey__overlap=True):
|
|
391
|
-
"""
|
|
460
|
+
"""Detect and optionally remove primary-key/foreign-key overlap."""
|
|
392
461
|
|
|
393
462
|
def partial_overlap(p_fkey, p_pkey):
|
|
394
|
-
"""
|
|
463
|
+
"""Return whether a foreign key partially overlaps a primary key."""
|
|
395
464
|
is_overlap = False
|
|
396
465
|
for field_name in p_fkey["FKeyFlds"]:
|
|
397
466
|
if field_name in p_pkey["Flds"]:
|
|
@@ -401,7 +470,7 @@ class SQLDbWrpr:
|
|
|
401
470
|
# end partial_overlap
|
|
402
471
|
|
|
403
472
|
def remove_fkey(p_fkey):
|
|
404
|
-
"""
|
|
473
|
+
"""Remove a foreign-key definition from all participating fields."""
|
|
405
474
|
for field_name in self.db_structure[p_fkey["FKeyTable"]]:
|
|
406
475
|
if self.db_structure[p_fkey["FKeyTable"]][field_name]["Params"]["FKey"]:
|
|
407
476
|
if (
|
|
@@ -448,6 +517,7 @@ class SQLDbWrpr:
|
|
|
448
517
|
# end structure_validation()
|
|
449
518
|
|
|
450
519
|
success = True
|
|
520
|
+
post_create_sql_set = []
|
|
451
521
|
structure_validation()
|
|
452
522
|
table_set_up_list = ""
|
|
453
523
|
idx_set_up_list = []
|
|
@@ -474,9 +544,11 @@ class SQLDbWrpr:
|
|
|
474
544
|
pass
|
|
475
545
|
db_sql_str_set = order_table_build_list(db_sql_str_set, constraint_set_up_list)
|
|
476
546
|
build_db(db_sql_str_set)
|
|
547
|
+
build_db(post_create_sql_set)
|
|
477
548
|
return success
|
|
478
549
|
|
|
479
550
|
def create_users(self, p_admin_user, p_new_users):
|
|
551
|
+
"""Create MySQL users that do not already exist."""
|
|
480
552
|
c_user_name = 0
|
|
481
553
|
self.cur.execute("SELECT User, Host FROM mysql.user")
|
|
482
554
|
curr_users = self.cur.fetchall()
|
|
@@ -488,7 +560,7 @@ class SQLDbWrpr:
|
|
|
488
560
|
user[0], self.host_name, user[1]
|
|
489
561
|
)
|
|
490
562
|
)
|
|
491
|
-
except
|
|
563
|
+
except self.db_error as err:
|
|
492
564
|
self._print_err_msg(err, "Could not create user")
|
|
493
565
|
self.close()
|
|
494
566
|
sys.exit()
|
|
@@ -505,7 +577,7 @@ class SQLDbWrpr:
|
|
|
505
577
|
if user[c_user_name] in curr_users:
|
|
506
578
|
try:
|
|
507
579
|
self.cur.execute(f"DROP USER '{user[c_user_name]}'@'{user[c_host]}'")
|
|
508
|
-
except
|
|
580
|
+
except self.db_error as err:
|
|
509
581
|
self._print_err_msg(err, "Could not delete user")
|
|
510
582
|
self.close()
|
|
511
583
|
sys.exit()
|
|
@@ -558,12 +630,13 @@ class SQLDbWrpr:
|
|
|
558
630
|
file_name_list = []
|
|
559
631
|
header = p_delimeter.join(self.db_structure[p_table_name])
|
|
560
632
|
prim_key_sql_str = "SELECT "
|
|
561
|
-
|
|
633
|
+
select_header = self.quote_identifier_list(self.db_structure[p_table_name])
|
|
634
|
+
all_sql_str = "SELECT " + select_header + " FROM " + self.quote_identifier(p_table_name) + " WHERE "
|
|
562
635
|
for i, field in enumerate(self.db_structure[p_table_name]):
|
|
563
636
|
if self.db_structure[p_table_name][field]["Params"]["PrimaryKey"][0] == "Y":
|
|
564
|
-
prim_key_sql_str += field + ", "
|
|
565
|
-
all_sql_str += field + " =
|
|
566
|
-
prim_key_sql_str = prim_key_sql_str[:-2] + " FROM " + p_table_name
|
|
637
|
+
prim_key_sql_str += self.quote_identifier(field) + ", "
|
|
638
|
+
all_sql_str += self.quote_identifier(field) + " = " + self.param_placeholder() + " and "
|
|
639
|
+
prim_key_sql_str = prim_key_sql_str[:-2] + " FROM " + self.quote_identifier(p_table_name)
|
|
567
640
|
all_sql_str = all_sql_str[:-5]
|
|
568
641
|
print(f"Collecting {p_table_name} table records")
|
|
569
642
|
self.cur.execute(prim_key_sql_str)
|
|
@@ -626,7 +699,12 @@ class SQLDbWrpr:
|
|
|
626
699
|
file_name_list.append(os.path.split(p_csv_path))
|
|
627
700
|
if not p_sql_query:
|
|
628
701
|
header = p_delimeter.join(self.db_structure[p_table_name])
|
|
629
|
-
sql_str =
|
|
702
|
+
sql_str = (
|
|
703
|
+
"SELECT "
|
|
704
|
+
+ self.quote_identifier_list(self.db_structure[p_table_name])
|
|
705
|
+
+ " FROM "
|
|
706
|
+
+ self.quote_identifier(p_table_name)
|
|
707
|
+
)
|
|
630
708
|
else:
|
|
631
709
|
header = p_delimeter.join(p_sql_query[0])
|
|
632
710
|
sql_str = p_sql_query[1]
|
|
@@ -663,8 +741,8 @@ class SQLDbWrpr:
|
|
|
663
741
|
|
|
664
742
|
file_name_list = None
|
|
665
743
|
try:
|
|
666
|
-
self.cur.execute("SELECT COUNT(*) FROM " + p_table_name)
|
|
667
|
-
except
|
|
744
|
+
self.cur.execute("SELECT COUNT(*) FROM " + self.quote_identifier(p_table_name))
|
|
745
|
+
except self.db_error as err:
|
|
668
746
|
print(f"Err mesg: {err.msg}")
|
|
669
747
|
print(err.msg)
|
|
670
748
|
else:
|
|
@@ -677,7 +755,7 @@ class SQLDbWrpr:
|
|
|
677
755
|
return file_name_list
|
|
678
756
|
|
|
679
757
|
def get_db_field_types(self):
|
|
680
|
-
"""
|
|
758
|
+
"""Populate field type lookup lists used during import/export."""
|
|
681
759
|
for p_table_name in self.db_structure:
|
|
682
760
|
self.char_fields[p_table_name] = []
|
|
683
761
|
self.non_char_fields[p_table_name] = []
|
|
@@ -691,6 +769,7 @@ class SQLDbWrpr:
|
|
|
691
769
|
self.non_char_fields[p_table_name].append(field)
|
|
692
770
|
|
|
693
771
|
def grant_rights(self, p_admin_user, p_user_rights):
|
|
772
|
+
"""Grant configured MySQL rights to users."""
|
|
694
773
|
c_user_name = 0
|
|
695
774
|
# c_password = 1
|
|
696
775
|
c_host = 1
|
|
@@ -718,7 +797,7 @@ class SQLDbWrpr:
|
|
|
718
797
|
)
|
|
719
798
|
self.cur.execute(sql_str)
|
|
720
799
|
self.conn.commit()
|
|
721
|
-
except
|
|
800
|
+
except self.db_error as err:
|
|
722
801
|
self._print_err_msg(err)
|
|
723
802
|
self.close()
|
|
724
803
|
sys.exit()
|
|
@@ -737,41 +816,40 @@ class SQLDbWrpr:
|
|
|
737
816
|
p_verbose=False,
|
|
738
817
|
p_replace=False,
|
|
739
818
|
):
|
|
740
|
-
"""Import
|
|
819
|
+
"""Import CSV rows into a database table.
|
|
741
820
|
|
|
742
821
|
Parameters
|
|
743
822
|
- p_table_name
|
|
744
|
-
Table name to import the
|
|
823
|
+
Table name to import the CSV data into
|
|
745
824
|
- p_csv_file_name = ''
|
|
746
|
-
|
|
825
|
+
CSV file name. Empty when rows are supplied in p_csv_db
|
|
747
826
|
- p_key = ''
|
|
748
827
|
Key used to insert in table
|
|
749
828
|
- p_header = ''
|
|
750
|
-
- Header of
|
|
829
|
+
- Header of CSV files
|
|
751
830
|
- p_del_head = ''
|
|
752
831
|
- Delete the header
|
|
753
832
|
- p_csv_db = ''
|
|
754
|
-
- Contains the
|
|
833
|
+
- Contains the CSV rows directly and makes p_csv_file_name obsolete.
|
|
755
834
|
- p_csv_corr_str_file_name = ''
|
|
756
|
-
-
|
|
757
|
-
file before parsing
|
|
835
|
+
- File containing string replacements to apply before parsing
|
|
758
836
|
- p_vol_type = 'Multi'
|
|
759
837
|
- Multi - Read multiple volume
|
|
760
838
|
- Single - Read single file
|
|
761
839
|
- p_verbose = False
|
|
762
|
-
- Determine
|
|
840
|
+
- Determine whether progress output is written to screen
|
|
763
841
|
- debug = False
|
|
764
|
-
- Switch debug on
|
|
842
|
+
- Switch debug output on
|
|
765
843
|
- p_replace = False
|
|
766
844
|
- False - INSERT into database
|
|
767
845
|
- True - REPLACE into database
|
|
768
846
|
"""
|
|
769
847
|
|
|
770
848
|
def import_volume(p_csv_db, p_header, p_verbose):
|
|
771
|
-
"""
|
|
849
|
+
"""Prepare and write one in-memory CSV volume to the active table."""
|
|
772
850
|
|
|
773
851
|
def convert_str_to_none(p_non_char_fields_idx, p_csv_db):
|
|
774
|
-
"""
|
|
852
|
+
"""Convert blank non-character values to None before insert."""
|
|
775
853
|
rows_to_del = []
|
|
776
854
|
csv_db = p_csv_db
|
|
777
855
|
list_len = len(csv_db)
|
|
@@ -813,7 +891,7 @@ class SQLDbWrpr:
|
|
|
813
891
|
# end convert_str_to_none
|
|
814
892
|
|
|
815
893
|
def find_non_char_field_idx(p_csv_db):
|
|
816
|
-
"""Find
|
|
894
|
+
"""Find non-character field indexes that may contain empty strings."""
|
|
817
895
|
non_char_fields_idx = []
|
|
818
896
|
for header_field_name in self.non_char_fields[p_table_name]:
|
|
819
897
|
for row_idx, data_field_name in enumerate(p_csv_db[0]):
|
|
@@ -825,7 +903,7 @@ class SQLDbWrpr:
|
|
|
825
903
|
# end find_non_char_field_idx
|
|
826
904
|
|
|
827
905
|
def fix_dates(p_csv_db, p_table_name, p_header):
|
|
828
|
-
"""
|
|
906
|
+
"""Normalize date and datetime values before insert."""
|
|
829
907
|
c_field_idx = 0
|
|
830
908
|
c_field_type = 1
|
|
831
909
|
csv_db = p_csv_db
|
|
@@ -879,7 +957,7 @@ class SQLDbWrpr:
|
|
|
879
957
|
# end fix_dates
|
|
880
958
|
|
|
881
959
|
def write_to_table(p_csv_db):
|
|
882
|
-
"""Write
|
|
960
|
+
"""Write prepared rows to the destination table."""
|
|
883
961
|
i = 1
|
|
884
962
|
j = 0 # In case batch size is more than all records
|
|
885
963
|
list_len = len(p_csv_db)
|
|
@@ -893,21 +971,11 @@ class SQLDbWrpr:
|
|
|
893
971
|
p_verbose=p_verbose,
|
|
894
972
|
p_bar_len=self.bar_len,
|
|
895
973
|
)
|
|
896
|
-
|
|
897
|
-
if p_replace:
|
|
898
|
-
sql_str = "REPLACE"
|
|
899
|
-
else:
|
|
900
|
-
sql_str = "INSERT"
|
|
901
|
-
sql_str = "{} INTO {} ({}) VALUES ({})".format(
|
|
902
|
-
sql_str,
|
|
903
|
-
p_table_name,
|
|
904
|
-
",".join([str(x) for x in header]),
|
|
905
|
-
",".join(["%s" for x in range(len(header))]),
|
|
906
|
-
)
|
|
974
|
+
sql_str = self.build_insert_sql(p_table_name, header, p_replace=p_replace)
|
|
907
975
|
for j in range(self.batch_size, list_len, self.batch_size):
|
|
908
976
|
try:
|
|
909
977
|
self.cur.executemany(sql_str, p_csv_db[i : j + 1])
|
|
910
|
-
except
|
|
978
|
+
except self.db_error as err:
|
|
911
979
|
self.logger.error(err)
|
|
912
980
|
self.conn.rollback()
|
|
913
981
|
self._err_broken_rec(sql_str, p_csv_db[i : j + 1])
|
|
@@ -939,7 +1007,7 @@ class SQLDbWrpr:
|
|
|
939
1007
|
# end import_volume
|
|
940
1008
|
|
|
941
1009
|
def import_single_volume(p_csv_db, p_header, p_verbose):
|
|
942
|
-
"""
|
|
1010
|
+
"""Import one supplied in-memory CSV volume."""
|
|
943
1011
|
success = False
|
|
944
1012
|
# if not p_csv_db:
|
|
945
1013
|
# if os.path.isfile(p_csv_file_name):
|
|
@@ -965,7 +1033,7 @@ class SQLDbWrpr:
|
|
|
965
1033
|
# end import_single_volume
|
|
966
1034
|
|
|
967
1035
|
def import_multi_volume(p_verbose, p_header):
|
|
968
|
-
"""
|
|
1036
|
+
"""Import numbered CSV files until the next volume is missing."""
|
|
969
1037
|
vol_cntr = 1
|
|
970
1038
|
success = False
|
|
971
1039
|
vol_csv_file_name = p_csv_file_name
|
|
@@ -1018,7 +1086,7 @@ class SQLDbWrpr:
|
|
|
1018
1086
|
p_verbose=False,
|
|
1019
1087
|
p_debug=False,
|
|
1020
1088
|
):
|
|
1021
|
-
"""
|
|
1089
|
+
"""Split CSV rows into one or more destination table imports.
|
|
1022
1090
|
|
|
1023
1091
|
Parameters
|
|
1024
1092
|
- p_split_struct - { 'Seq01': { 'TableName': Desttable_name1, 'Key': TableKey, 'Replace': False, 'Flds': [[ OrgField1, DestField1, [ Command, Parm1, Parm2, Parm3 ]],
|
|
@@ -1036,8 +1104,8 @@ class SQLDbWrpr:
|
|
|
1036
1104
|
- Replace (boolean): Either use REPLACE or INSERT SQL statement to add records to the table. INSERT will cause
|
|
1037
1105
|
a failure when the record to be added is a duplicate.
|
|
1038
1106
|
- Fields (str): Mandatory key word (in the python dict structure) to list the fields in the table
|
|
1039
|
-
- OrgFieldN (str): Field name from the
|
|
1040
|
-
- DestFieldN (str): Destination
|
|
1107
|
+
- OrgFieldN (str): Field name from the CSV file to copy to the database table
|
|
1108
|
+
- DestFieldN (str): Destination field where OrgFieldN will be copied into
|
|
1041
1109
|
- Command (int): 0 = Copy OrgFieldN to DestFieldN as is
|
|
1042
1110
|
Parm1 = Truncate OrgFieldN at Parm1 if it is a string and insert into DestFieldN. 0 for no truncation. Non 'str' will not be truncated
|
|
1043
1111
|
Parm2 = True if you do not want to add the row if the result is empty, else False
|
|
@@ -1181,8 +1249,158 @@ class SQLDbWrpr:
|
|
|
1181
1249
|
p_replace=p_split_struct[seq]["Replace"],
|
|
1182
1250
|
)
|
|
1183
1251
|
|
|
1252
|
+
@classmethod
|
|
1253
|
+
def from_sqlalchemy_metadata(cls, p_sqlalchemy_metadata):
|
|
1254
|
+
"""Convert SQLAlchemy metadata into the legacy SqlDbWrpr schema structure."""
|
|
1255
|
+
if not getattr(p_sqlalchemy_metadata, "tables", None):
|
|
1256
|
+
raise SchemaSourceError("SQLAlchemy metadata does not define any tables")
|
|
1257
|
+
db_structure = {}
|
|
1258
|
+
for table in p_sqlalchemy_metadata.sorted_tables:
|
|
1259
|
+
db_structure[table.name] = cls._table_to_legacy_structure(table)
|
|
1260
|
+
return db_structure
|
|
1261
|
+
|
|
1262
|
+
@staticmethod
|
|
1263
|
+
def resolve_db_structure(p_db_structure=None, p_sqlalchemy_base=None, p_sqlalchemy_metadata=None):
|
|
1264
|
+
"""Resolve schema from explicit config, SQLAlchemy metadata, or declarative base."""
|
|
1265
|
+
if p_db_structure:
|
|
1266
|
+
return p_db_structure
|
|
1267
|
+
if p_sqlalchemy_metadata is None and p_sqlalchemy_base is not None:
|
|
1268
|
+
p_sqlalchemy_metadata = getattr(p_sqlalchemy_base, "metadata", None)
|
|
1269
|
+
if p_sqlalchemy_metadata is not None:
|
|
1270
|
+
return SQLDbWrpr.from_sqlalchemy_metadata(p_sqlalchemy_metadata)
|
|
1271
|
+
raise SchemaSourceError("Supply p_db_structure, p_sqlalchemy_metadata, or p_sqlalchemy_base")
|
|
1272
|
+
|
|
1273
|
+
@staticmethod
|
|
1274
|
+
def _action_to_legacy_code(p_action):
|
|
1275
|
+
"""Convert SQLAlchemy foreign-key actions to legacy codes."""
|
|
1276
|
+
if p_action is None:
|
|
1277
|
+
return "N"
|
|
1278
|
+
action = p_action.upper().replace(" ", "_")
|
|
1279
|
+
action_map = {
|
|
1280
|
+
"CASCADE": "C",
|
|
1281
|
+
"RESTRICT": "R",
|
|
1282
|
+
"SET_DEFAULT": "D",
|
|
1283
|
+
"SET_NULL": "N",
|
|
1284
|
+
}
|
|
1285
|
+
return action_map[action]
|
|
1286
|
+
|
|
1287
|
+
@staticmethod
|
|
1288
|
+
def _build_default_field_params():
|
|
1289
|
+
"""Build the legacy default parameter block for a field."""
|
|
1290
|
+
return {
|
|
1291
|
+
"PrimaryKey": ["", ""],
|
|
1292
|
+
"FKey": [],
|
|
1293
|
+
"Index": [],
|
|
1294
|
+
"NN": "",
|
|
1295
|
+
"B": "",
|
|
1296
|
+
"UN": "",
|
|
1297
|
+
"ZF": "",
|
|
1298
|
+
"AI": "",
|
|
1299
|
+
"G": "",
|
|
1300
|
+
"DEF": "",
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
@staticmethod
|
|
1304
|
+
def _column_type_to_legacy(p_column):
|
|
1305
|
+
"""Convert a SQLAlchemy column type to the legacy type list."""
|
|
1306
|
+
column_type = p_column.type
|
|
1307
|
+
type_name = column_type.__class__.__name__.lower()
|
|
1308
|
+
if "biginteger" in type_name:
|
|
1309
|
+
return ["bigint"]
|
|
1310
|
+
if "boolean" in type_name:
|
|
1311
|
+
return ["boolean"]
|
|
1312
|
+
if "char" in type_name or "string" in type_name or "varchar" in type_name:
|
|
1313
|
+
if getattr(column_type, "length", None) is None:
|
|
1314
|
+
return ["varchar"]
|
|
1315
|
+
if type_name == "char":
|
|
1316
|
+
return ["char", column_type.length]
|
|
1317
|
+
return ["varchar", column_type.length]
|
|
1318
|
+
if type_name == "date":
|
|
1319
|
+
return ["date"]
|
|
1320
|
+
if "datetime" in type_name:
|
|
1321
|
+
return ["datetime"]
|
|
1322
|
+
if "integer" in type_name:
|
|
1323
|
+
return ["int"]
|
|
1324
|
+
if "largebinary" in type_name or "blob" in type_name or "binary" in type_name:
|
|
1325
|
+
return ["blob"]
|
|
1326
|
+
if "numeric" in type_name or "decimal" in type_name:
|
|
1327
|
+
precision = getattr(column_type, "precision", None)
|
|
1328
|
+
scale = getattr(column_type, "scale", None)
|
|
1329
|
+
if precision is not None and scale is not None:
|
|
1330
|
+
return ["decimal", precision, scale]
|
|
1331
|
+
return ["decimal"]
|
|
1332
|
+
if type_name == "time":
|
|
1333
|
+
return ["time"]
|
|
1334
|
+
return [type_name]
|
|
1335
|
+
|
|
1336
|
+
@staticmethod
|
|
1337
|
+
def _column_to_legacy_field(p_column):
|
|
1338
|
+
"""Convert a SQLAlchemy column to a legacy field definition."""
|
|
1339
|
+
field_type = SQLDbWrpr._column_type_to_legacy(p_column)
|
|
1340
|
+
params = SQLDbWrpr._build_default_field_params()
|
|
1341
|
+
if not p_column.nullable or p_column.primary_key:
|
|
1342
|
+
params["NN"] = "Y"
|
|
1343
|
+
if p_column.autoincrement is True:
|
|
1344
|
+
params["AI"] = "Y"
|
|
1345
|
+
if p_column.default is not None and getattr(p_column.default, "is_scalar", False):
|
|
1346
|
+
params["DEF"] = str(p_column.default.arg)
|
|
1347
|
+
if field_type[0] == "blob":
|
|
1348
|
+
params["B"] = "Y"
|
|
1349
|
+
return {
|
|
1350
|
+
"Type": field_type,
|
|
1351
|
+
"Params": params,
|
|
1352
|
+
"Possible Values": "",
|
|
1353
|
+
"Comment": p_column.comment or "",
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
@staticmethod
|
|
1357
|
+
def _set_foreign_keys(p_table, p_table_structure):
|
|
1358
|
+
"""Set legacy foreign-key metadata on converted table fields."""
|
|
1359
|
+
for fkey_nr, constraint in enumerate(p_table.foreign_key_constraints, start=1):
|
|
1360
|
+
elements = list(constraint.elements)
|
|
1361
|
+
for field_order, element in enumerate(elements, start=1):
|
|
1362
|
+
p_table_structure[element.parent.name]["Params"]["FKey"] = [
|
|
1363
|
+
fkey_nr,
|
|
1364
|
+
field_order,
|
|
1365
|
+
element.column.table.name,
|
|
1366
|
+
element.column.name,
|
|
1367
|
+
SQLDbWrpr._action_to_legacy_code(constraint.ondelete),
|
|
1368
|
+
SQLDbWrpr._action_to_legacy_code(constraint.onupdate),
|
|
1369
|
+
]
|
|
1370
|
+
|
|
1371
|
+
@staticmethod
|
|
1372
|
+
def _set_indexes(p_table, p_table_structure):
|
|
1373
|
+
"""Set legacy index metadata on converted table fields."""
|
|
1374
|
+
indexes = sorted(p_table.indexes, key=lambda p_index: p_index.name or "")
|
|
1375
|
+
for idx_nr, index in enumerate(indexes, start=1):
|
|
1376
|
+
for field_order, column in enumerate(index.columns, start=1):
|
|
1377
|
+
p_table_structure[column.name]["Params"]["Index"] = [
|
|
1378
|
+
idx_nr,
|
|
1379
|
+
field_order,
|
|
1380
|
+
"A",
|
|
1381
|
+
"U" if index.unique else "",
|
|
1382
|
+
]
|
|
1383
|
+
|
|
1384
|
+
@staticmethod
|
|
1385
|
+
def _set_primary_key(p_table, p_table_structure):
|
|
1386
|
+
"""Set legacy primary-key metadata on converted table fields."""
|
|
1387
|
+
for column in p_table.primary_key.columns:
|
|
1388
|
+
p_table_structure[column.name]["Params"]["PrimaryKey"] = ["Y", "A"]
|
|
1389
|
+
|
|
1390
|
+
@staticmethod
|
|
1391
|
+
def _table_to_legacy_structure(p_table):
|
|
1392
|
+
"""Convert a SQLAlchemy table to the legacy table structure."""
|
|
1393
|
+
table_structure = {}
|
|
1394
|
+
for column in p_table.columns:
|
|
1395
|
+
table_structure[column.name] = SQLDbWrpr._column_to_legacy_field(column)
|
|
1396
|
+
SQLDbWrpr._set_foreign_keys(p_table, table_structure)
|
|
1397
|
+
SQLDbWrpr._set_indexes(p_table, table_structure)
|
|
1398
|
+
SQLDbWrpr._set_primary_key(p_table, table_structure)
|
|
1399
|
+
return table_structure
|
|
1400
|
+
|
|
1184
1401
|
@staticmethod
|
|
1185
1402
|
def _print_err_msg(p_err, p_msg=""):
|
|
1403
|
+
"""Print a formatted database error message."""
|
|
1186
1404
|
msg = p_msg
|
|
1187
1405
|
if p_msg:
|
|
1188
1406
|
msg = f"{p_msg}\n"
|
|
@@ -1197,7 +1415,7 @@ class SQLDbWrpr:
|
|
|
1197
1415
|
|
|
1198
1416
|
|
|
1199
1417
|
class MySQL(SQLDbWrpr):
|
|
1200
|
-
"""
|
|
1418
|
+
"""Wrapper for MySQL databases."""
|
|
1201
1419
|
|
|
1202
1420
|
def __init__(
|
|
1203
1421
|
self,
|
|
@@ -1208,6 +1426,8 @@ class MySQL(SQLDbWrpr):
|
|
|
1208
1426
|
p_recreate_db=False,
|
|
1209
1427
|
p_db_name=None,
|
|
1210
1428
|
p_db_structure=None,
|
|
1429
|
+
p_sqlalchemy_base=None,
|
|
1430
|
+
p_sqlalchemy_metadata=None,
|
|
1211
1431
|
p_batch_size=10000,
|
|
1212
1432
|
p_bar_len=50,
|
|
1213
1433
|
p_msg_width=50,
|
|
@@ -1220,7 +1440,7 @@ class MySQL(SQLDbWrpr):
|
|
|
1220
1440
|
# p_ssl_cert=None
|
|
1221
1441
|
**kwargs,
|
|
1222
1442
|
):
|
|
1223
|
-
"""
|
|
1443
|
+
"""Connect to MySQL and optionally recreate or select the target database."""
|
|
1224
1444
|
super().__init__(
|
|
1225
1445
|
p_host_name=p_host_name,
|
|
1226
1446
|
p_user_name=p_user_name,
|
|
@@ -1228,6 +1448,8 @@ class MySQL(SQLDbWrpr):
|
|
|
1228
1448
|
p_db_name=p_db_name,
|
|
1229
1449
|
p_recreate_db=p_recreate_db,
|
|
1230
1450
|
p_db_structure=p_db_structure,
|
|
1451
|
+
p_sqlalchemy_base=p_sqlalchemy_base,
|
|
1452
|
+
p_sqlalchemy_metadata=p_sqlalchemy_metadata,
|
|
1231
1453
|
p_batch_size=p_batch_size,
|
|
1232
1454
|
p_bar_len=p_bar_len,
|
|
1233
1455
|
p_msg_width=p_msg_width,
|
|
@@ -1252,7 +1474,7 @@ class MySQL(SQLDbWrpr):
|
|
|
1252
1474
|
**kwargs,
|
|
1253
1475
|
)
|
|
1254
1476
|
self.cur = self.conn.cursor()
|
|
1255
|
-
except
|
|
1477
|
+
except self.db_error as err:
|
|
1256
1478
|
print(
|
|
1257
1479
|
bm.error(
|
|
1258
1480
|
f"Error {err}:'({self.user_name}'@'{self.host_name}')",
|
|
@@ -1270,7 +1492,7 @@ class MySQL(SQLDbWrpr):
|
|
|
1270
1492
|
auth_plugin="mysql_native_password",
|
|
1271
1493
|
port=self.db_port,
|
|
1272
1494
|
)
|
|
1273
|
-
except
|
|
1495
|
+
except self.db_error as err:
|
|
1274
1496
|
self._print_err_msg(
|
|
1275
1497
|
err,
|
|
1276
1498
|
"Admin user name and/or password not supplied or incorrect",
|
|
@@ -1311,8 +1533,8 @@ class MySQL(SQLDbWrpr):
|
|
|
1311
1533
|
pass
|
|
1312
1534
|
|
|
1313
1535
|
|
|
1314
|
-
class
|
|
1315
|
-
"""
|
|
1536
|
+
class PostgreSQL(SQLDbWrpr):
|
|
1537
|
+
"""Wrapper for PostgreSQL databases."""
|
|
1316
1538
|
|
|
1317
1539
|
def __init__(
|
|
1318
1540
|
self,
|
|
@@ -1322,12 +1544,17 @@ class MSSQL(SQLDbWrpr):
|
|
|
1322
1544
|
p_recreate_db=False,
|
|
1323
1545
|
p_db_name=None,
|
|
1324
1546
|
p_db_structure=None,
|
|
1547
|
+
p_sqlalchemy_base=None,
|
|
1548
|
+
p_sqlalchemy_metadata=None,
|
|
1325
1549
|
p_batch_size=10000,
|
|
1326
1550
|
p_bar_len=50,
|
|
1327
1551
|
p_msg_width=50,
|
|
1328
1552
|
p_verbose=False,
|
|
1553
|
+
p_db_port="5432",
|
|
1554
|
+
p_maintenance_db="postgres",
|
|
1555
|
+
**kwargs,
|
|
1329
1556
|
):
|
|
1330
|
-
"""
|
|
1557
|
+
"""Create a PostgreSQL wrapper and optionally recreate the target database."""
|
|
1331
1558
|
super().__init__(
|
|
1332
1559
|
p_host_name=p_host_name,
|
|
1333
1560
|
p_user_name=p_user_name,
|
|
@@ -1335,36 +1562,136 @@ class MSSQL(SQLDbWrpr):
|
|
|
1335
1562
|
p_db_name=p_db_name,
|
|
1336
1563
|
p_recreate_db=p_recreate_db,
|
|
1337
1564
|
p_db_structure=p_db_structure,
|
|
1565
|
+
p_sqlalchemy_base=p_sqlalchemy_base,
|
|
1566
|
+
p_sqlalchemy_metadata=p_sqlalchemy_metadata,
|
|
1338
1567
|
p_batch_size=p_batch_size,
|
|
1339
1568
|
p_bar_len=p_bar_len,
|
|
1340
1569
|
p_msg_width=p_msg_width,
|
|
1341
1570
|
p_verbose=p_verbose,
|
|
1571
|
+
p_db_port=p_db_port,
|
|
1572
|
+
)
|
|
1573
|
+
self.db_error = psycopg.Error
|
|
1574
|
+
self.identifier_quote = '"'
|
|
1575
|
+
self.inline_indexes = False
|
|
1576
|
+
self.maintenance_db = p_maintenance_db
|
|
1577
|
+
self._connection_kwargs = kwargs
|
|
1578
|
+
connect_db = self.maintenance_db if self.re_create_db else self.db_name
|
|
1579
|
+
self.conn = self._connect(connect_db)
|
|
1580
|
+
self.conn.autocommit = True
|
|
1581
|
+
self.cur = self.conn.cursor()
|
|
1582
|
+
if self.re_create_db:
|
|
1583
|
+
if self.create_db():
|
|
1584
|
+
self.create_tables()
|
|
1585
|
+
self.success = True
|
|
1586
|
+
|
|
1587
|
+
def build_column_sql(self, p_field_name, p_field_type, p_field_params, p_field_comment):
|
|
1588
|
+
"""Build PostgreSQL SQL for one column in a CREATE TABLE statement."""
|
|
1589
|
+
sql_str = f"{self.quote_identifier(p_field_name)} {self.render_field_type(p_field_type, p_field_params)}"
|
|
1590
|
+
if p_field_params["NN"] == "Y" and p_field_params["AI"] != "Y":
|
|
1591
|
+
sql_str += " NOT NULL"
|
|
1592
|
+
if p_field_params["DEF"]:
|
|
1593
|
+
sql_str += self.render_default_sql(p_field_type, p_field_params["DEF"])
|
|
1594
|
+
return sql_str
|
|
1595
|
+
|
|
1596
|
+
def build_index_sql(self, p_table_name, p_idx_name, p_idx_fields, p_unique=False):
|
|
1597
|
+
"""Build a PostgreSQL CREATE INDEX statement."""
|
|
1598
|
+
unique_sql = "UNIQUE " if p_unique else ""
|
|
1599
|
+
index_fields = []
|
|
1600
|
+
for field_det in p_idx_fields:
|
|
1601
|
+
index_fields.append(f"{self.quote_identifier(field_det[0])} {self.sort_order[field_det[2]]}")
|
|
1602
|
+
return "CREATE {}INDEX {} ON {} ({})".format(
|
|
1603
|
+
unique_sql,
|
|
1604
|
+
self.quote_identifier(p_idx_name),
|
|
1605
|
+
self.quote_identifier(p_table_name),
|
|
1606
|
+
",".join(index_fields),
|
|
1607
|
+
)
|
|
1608
|
+
|
|
1609
|
+
def build_insert_sql(self, p_table_name, p_header, p_replace=False):
|
|
1610
|
+
"""Build a PostgreSQL INSERT or upsert statement."""
|
|
1611
|
+
insert_sql = "INSERT INTO {} ({}) VALUES ({})".format(
|
|
1612
|
+
self.quote_identifier(p_table_name),
|
|
1613
|
+
",".join([self.quote_identifier(str(x)) for x in p_header]),
|
|
1614
|
+
",".join([self.param_placeholder() for x in range(len(p_header))]),
|
|
1342
1615
|
)
|
|
1616
|
+
if not p_replace:
|
|
1617
|
+
return insert_sql
|
|
1618
|
+
primary_key_fields = [
|
|
1619
|
+
field_name
|
|
1620
|
+
for field_name in self.db_structure[p_table_name]
|
|
1621
|
+
if self.db_structure[p_table_name][field_name]["Params"]["PrimaryKey"][0] == "Y"
|
|
1622
|
+
]
|
|
1623
|
+
if not primary_key_fields:
|
|
1624
|
+
return f"{insert_sql} ON CONFLICT DO NOTHING"
|
|
1625
|
+
update_fields = [field_name for field_name in p_header if field_name not in primary_key_fields]
|
|
1626
|
+
if not update_fields:
|
|
1627
|
+
return f"{insert_sql} ON CONFLICT ({self.quote_identifier_list(primary_key_fields)}) DO NOTHING"
|
|
1628
|
+
update_sql = ",".join(
|
|
1629
|
+
[
|
|
1630
|
+
f"{self.quote_identifier(field_name)} = EXCLUDED.{self.quote_identifier(field_name)}"
|
|
1631
|
+
for field_name in update_fields
|
|
1632
|
+
]
|
|
1633
|
+
)
|
|
1634
|
+
return f"{insert_sql} ON CONFLICT ({self.quote_identifier_list(primary_key_fields)}) DO UPDATE SET {update_sql}"
|
|
1635
|
+
|
|
1636
|
+
def create_db(self):
|
|
1637
|
+
"""Create the PostgreSQL database according to self.db_structure."""
|
|
1638
|
+
self.cur.execute("SELECT 1 FROM pg_database WHERE datname = %s", (self.db_name,))
|
|
1639
|
+
if self.cur.fetchone():
|
|
1640
|
+
try:
|
|
1641
|
+
self.cur.execute(f"DROP DATABASE {self.quote_identifier(self.db_name)} WITH (FORCE)")
|
|
1642
|
+
except self.db_error as err:
|
|
1643
|
+
self._print_err_msg(err, "Could not drop the database")
|
|
1644
|
+
self.close()
|
|
1645
|
+
sys.exit()
|
|
1343
1646
|
try:
|
|
1344
|
-
self.
|
|
1345
|
-
self.
|
|
1346
|
-
self.
|
|
1347
|
-
self.
|
|
1348
|
-
self.driver = "{ODBC Driver 17 for SQL Server}"
|
|
1349
|
-
# driver = pyodbc.drivers()
|
|
1350
|
-
# con_str_1 = 'DRIVER={};SERVER={};DATABASE={};UID={};PWD={}'.format( self.driver, self.host_name, self.db_name, self.user_name, self._password )
|
|
1351
|
-
con_str_2 = (
|
|
1352
|
-
"DRIVER={ODBC Driver 17 for SQL Server};SERVER="
|
|
1353
|
-
+ self.host_name
|
|
1354
|
-
+ ";DATABASE="
|
|
1355
|
-
+ self.db_name
|
|
1356
|
-
+ ";UID="
|
|
1357
|
-
+ self.user_name
|
|
1358
|
-
+ ";PWD="
|
|
1359
|
-
+ self._password
|
|
1360
|
-
)
|
|
1361
|
-
self.conn = pyodbc.connect(con_str_2)
|
|
1362
|
-
pass
|
|
1363
|
-
except Error as err:
|
|
1364
|
-
self.logger.error(err)
|
|
1365
|
-
self.success = self.conn.is_connected()
|
|
1366
|
-
if self.conn.is_connected():
|
|
1647
|
+
self.cur.execute(f"CREATE DATABASE {self.quote_identifier(self.db_name)}")
|
|
1648
|
+
self.close()
|
|
1649
|
+
self.conn = self._connect(self.db_name)
|
|
1650
|
+
self.conn.autocommit = True
|
|
1367
1651
|
self.cur = self.conn.cursor()
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1652
|
+
except self.db_error as err:
|
|
1653
|
+
self._print_err_msg(err, "Could not create the database")
|
|
1654
|
+
self.close()
|
|
1655
|
+
sys.exit()
|
|
1656
|
+
return True
|
|
1657
|
+
|
|
1658
|
+
def render_default_sql(self, p_field_type, p_default_value):
|
|
1659
|
+
"""Render a PostgreSQL column default expression."""
|
|
1660
|
+
if p_field_type[0] == "varchar" or p_field_type[0] == "char":
|
|
1661
|
+
escaped_value = str(p_default_value).replace("'", "''")
|
|
1662
|
+
return f" DEFAULT '{escaped_value}'"
|
|
1663
|
+
return f" DEFAULT {p_default_value}"
|
|
1664
|
+
|
|
1665
|
+
def render_field_type(self, p_field_type, p_field_params):
|
|
1666
|
+
"""Render a legacy field type as a PostgreSQL type."""
|
|
1667
|
+
field_type = p_field_type[0]
|
|
1668
|
+
if p_field_params["AI"] == "Y":
|
|
1669
|
+
if field_type == "bigint":
|
|
1670
|
+
return "BIGSERIAL"
|
|
1671
|
+
return "SERIAL"
|
|
1672
|
+
type_map = {
|
|
1673
|
+
"bigint": "BIGINT",
|
|
1674
|
+
"blob": "BYTEA",
|
|
1675
|
+
"boolean": "BOOLEAN",
|
|
1676
|
+
"date": "DATE",
|
|
1677
|
+
"datetime": "TIMESTAMP",
|
|
1678
|
+
"int": "INTEGER",
|
|
1679
|
+
"tinyint": "SMALLINT",
|
|
1680
|
+
"time": "TIME",
|
|
1681
|
+
}
|
|
1682
|
+
if field_type == "varchar" or field_type == "char":
|
|
1683
|
+
return f"{field_type.upper()}({str(p_field_type[1])})"
|
|
1684
|
+
if field_type == "decimal":
|
|
1685
|
+
return f"DECIMAL({str(p_field_type[1])}, {str(p_field_type[2])})"
|
|
1686
|
+
return type_map.get(field_type, field_type.upper())
|
|
1687
|
+
|
|
1688
|
+
def _connect(self, p_db_name):
|
|
1689
|
+
"""Open a PostgreSQL connection."""
|
|
1690
|
+
return psycopg.connect(
|
|
1691
|
+
host=self.host_name,
|
|
1692
|
+
user=self.user_name,
|
|
1693
|
+
password=self._password,
|
|
1694
|
+
dbname=p_db_name,
|
|
1695
|
+
port=self.db_port,
|
|
1696
|
+
**self._connection_kwargs,
|
|
1697
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: SQLDbWrpr
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.1.0
|
|
4
4
|
Summary: Wrapper for importing CSV and Text files into MySQL and Postgress
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE.txt
|
|
@@ -8,23 +8,26 @@ Author: Hendrik du Toit
|
|
|
8
8
|
Author-email: hendrik@brightedge.co.za
|
|
9
9
|
Maintainer: Hendrik du Toit
|
|
10
10
|
Maintainer-email: hendrikdt@citiqprepaid.co.za
|
|
11
|
-
Requires-Python: >=3.10
|
|
12
|
-
Classifier: Development Status ::
|
|
13
|
-
Classifier:
|
|
14
|
-
Classifier:
|
|
15
|
-
Classifier: Topic :: Software Development
|
|
16
|
-
Classifier: Topic :: Text Processing
|
|
17
|
-
Classifier: Topic :: Utilities
|
|
11
|
+
Requires-Python: >=3.10,<3.15
|
|
12
|
+
Classifier: Development Status :: 6 - Mature
|
|
13
|
+
Classifier: Framework :: Pydantic :: 2
|
|
14
|
+
Classifier: Framework :: Pytest
|
|
18
15
|
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
17
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
18
|
+
Classifier: Operating System :: iOS
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.10
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.11
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.12
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
25
|
Requires-Dist: csvwrpr (>=1.16.1,<2.0.0)
|
|
24
26
|
Requires-Dist: fixdate (>=2.1.0,<3.0.0)
|
|
25
27
|
Requires-Dist: mysql-connector (>=2.2.9,<3.0.0)
|
|
28
|
+
Requires-Dist: psycopg[binary] (>=3.2.0,<4.0.0)
|
|
26
29
|
Requires-Dist: pydantic-settings (>=2.12.0,<3.0.0)
|
|
27
|
-
Requires-Dist:
|
|
30
|
+
Requires-Dist: sqlalchemy (>=2.0.0,<3.0.0)
|
|
28
31
|
Project-URL: Homepage, https://github.com/BrightEdgeeServices/SqlDbWrpr
|
|
29
32
|
Project-URL: Issues, https://github.com/BrightEdgeeServices/SqlDbWrpr/issues
|
|
30
33
|
Project-URL: Repository, https://github.com/BrightEdgeeServices/SqlDbWrpr.git
|
|
@@ -40,7 +43,170 @@ Description-Content-Type: text/markdown
|
|
|
40
43
|
| PyPI | [![][pypi_release_img]][pypi_release_lnk] [![][pypi_py_versions_img]][pypi_py_versions_lnk] [![][pypi_format_img]][pypi_format_lnk] [![][pypi_downloads_img]][pypi_downloads_lnk] |
|
|
41
44
|
| Github | [![][gh_issues_img]][gh_issues_lnk] [![][gh_language_img]][gh_language_lnk] [![][gh_last_commit_img]][gh_last_commit_lnk] [![][gh_deployment_img]][gh_deployment_lnk] |
|
|
42
45
|
|
|
43
|
-
##
|
|
46
|
+
## Short description
|
|
47
|
+
|
|
48
|
+
SqlDbWrpr is a Python wrapper that streamlines schema creation plus CSV import/export workflows for SQL backends.
|
|
49
|
+
|
|
50
|
+
## Module Overview
|
|
51
|
+
|
|
52
|
+
SqlDbWrpr is a Python utility for creating SQL database schemas and moving CSV data into and out of those schemas. It currently provides wrappers for MySQL and PostgreSQL.
|
|
53
|
+
|
|
54
|
+
Schemas can be supplied in two ways:
|
|
55
|
+
|
|
56
|
+
1. A legacy `db_structure` dictionary.
|
|
57
|
+
2. SQLAlchemy metadata, either directly through `p_sqlalchemy_metadata` or through `p_sqlalchemy_base.metadata`.
|
|
58
|
+
|
|
59
|
+
When both are supplied, `p_db_structure` takes precedence for backward compatibility. If no supported schema source is supplied, `SchemaSourceError` is raised.
|
|
60
|
+
|
|
61
|
+
### Key Features
|
|
62
|
+
|
|
63
|
+
- **Schema Management**: Create databases, tables, primary keys, foreign keys, and indexes from a legacy dictionary or SQLAlchemy metadata.
|
|
64
|
+
- **Data Import/Export**:
|
|
65
|
+
- Import CSV data from files or in-memory rows, including single-volume and numbered multi-volume files.
|
|
66
|
+
- Export full tables or custom SQL query results to CSV, including optional multi-volume exports.
|
|
67
|
+
- **Database Support**: Includes MySQL and PostgreSQL wrappers with dialect-specific SQL rendering.
|
|
68
|
+
- **User and Permission Management**: Create MySQL users and grant database rights.
|
|
69
|
+
- **Batch Processing**: Configure import batch sizes for larger CSV loads.
|
|
70
|
+
|
|
71
|
+
### Project Structure
|
|
72
|
+
|
|
73
|
+
- `src/sqldbwrpr/`: Core library implementation, including MySQL and PostgreSQL wrappers.
|
|
74
|
+
- `tests/`: Unit and integration-oriented test coverage for wrapper behaviour.
|
|
75
|
+
- `scripts/`: SQL setup/bootstrap assets used for database initialization.
|
|
76
|
+
- Root automation scripts (`*.ps1`) and CI configuration under `.github/workflows/` support setup and delivery.
|
|
77
|
+
|
|
78
|
+
## Getting Started
|
|
79
|
+
|
|
80
|
+
### Installation
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pip install SqlDbWrpr
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Quick Start With A Legacy Structure
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from sqldbwrpr.sqldbwrpr import MySQL
|
|
90
|
+
|
|
91
|
+
field_defaults = {
|
|
92
|
+
"PrimaryKey": ["", ""],
|
|
93
|
+
"FKey": [],
|
|
94
|
+
"Index": [],
|
|
95
|
+
"NN": "",
|
|
96
|
+
"B": "",
|
|
97
|
+
"UN": "",
|
|
98
|
+
"ZF": "",
|
|
99
|
+
"AI": "",
|
|
100
|
+
"G": "",
|
|
101
|
+
"DEF": "",
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
db_structure = {
|
|
105
|
+
"Users": {
|
|
106
|
+
"ID": {
|
|
107
|
+
"Type": ["int"],
|
|
108
|
+
"Params": {
|
|
109
|
+
**field_defaults,
|
|
110
|
+
"PrimaryKey": ["Y", "A"],
|
|
111
|
+
"NN": "Y",
|
|
112
|
+
"AI": "Y",
|
|
113
|
+
},
|
|
114
|
+
"Possible Values": "",
|
|
115
|
+
"Comment": "",
|
|
116
|
+
},
|
|
117
|
+
"Username": {
|
|
118
|
+
"Type": ["varchar", 50],
|
|
119
|
+
"Params": {**field_defaults, "NN": "Y"},
|
|
120
|
+
"Possible Values": "",
|
|
121
|
+
"Comment": "",
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
db = MySQL(
|
|
127
|
+
p_host_name="localhost",
|
|
128
|
+
p_user_name="root",
|
|
129
|
+
p_password="yourpassword",
|
|
130
|
+
p_db_name="my_database",
|
|
131
|
+
p_db_structure=db_structure,
|
|
132
|
+
p_recreate_db=True,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
db.import_csv("Users", p_csv_db=[("Username",), ("alice",)], p_vol_type="Single")
|
|
136
|
+
db.export_to_csv("exported_users.csv", "Users")
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Quick Start With SQLAlchemy Metadata
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from sqlalchemy import Column
|
|
143
|
+
from sqlalchemy import Integer
|
|
144
|
+
from sqlalchemy import MetaData
|
|
145
|
+
from sqlalchemy import String
|
|
146
|
+
from sqlalchemy import Table
|
|
147
|
+
|
|
148
|
+
from sqldbwrpr.sqldbwrpr import PostgreSQL
|
|
149
|
+
|
|
150
|
+
metadata = MetaData()
|
|
151
|
+
Table(
|
|
152
|
+
"Users",
|
|
153
|
+
metadata,
|
|
154
|
+
Column("ID", Integer, primary_key=True, autoincrement=True),
|
|
155
|
+
Column("Username", String(50), nullable=False),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
db = PostgreSQL(
|
|
159
|
+
p_host_name="localhost",
|
|
160
|
+
p_user_name="postgres",
|
|
161
|
+
p_password="yourpassword",
|
|
162
|
+
p_db_name="my_database",
|
|
163
|
+
p_sqlalchemy_metadata=metadata,
|
|
164
|
+
p_recreate_db=True,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
db.import_csv("Users", p_csv_db=[("Username",), ("alice",)], p_vol_type="Single")
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
______________________________________________________________________
|
|
171
|
+
|
|
172
|
+
## Updating ReleaseNotes Instructions
|
|
173
|
+
|
|
174
|
+
1. Run the `pushpy.ps1` script or manually commit the current changes.
|
|
175
|
+
2. Generate the release notes
|
|
176
|
+
3. Use one of the following AI propmpts in Notion to generate the release notes.
|
|
177
|
+
|
|
178
|
+
- [Release - Update - General](https://www.notion.so/Release-Update-General-2c0bc8e6c6f38076b4cee82e3cf243fa?v=2c0bc8e6c6f3806e85db000c395f94ce&source=copy_link)
|
|
179
|
+
- [Release - Update - VenvIt](https://www.notion.so/Release-Update-VenvIt-2c0bc8e6c6f380de84a0f3fbb8b5dda5?v=2c0bc8e6c6f3806e85db000c395f94ce&source=copy_link)
|
|
180
|
+
|
|
181
|
+
or
|
|
182
|
+
|
|
183
|
+
1. Use the following template and manually update the ReleaseNotes.md file.
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
# Release ?.?.?
|
|
187
|
+
## Summary of Changes
|
|
188
|
+
- bla, bla, bla
|
|
189
|
+
## Next Heading
|
|
190
|
+
- bla, bla, bla
|
|
191
|
+
---
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
2. You can repeat step 1 multiple times.
|
|
195
|
+
|
|
196
|
+
3. You can repeat step 2 multiple times but update the ReleaseNotes that has not been published.
|
|
197
|
+
|
|
198
|
+
4. Run the `pushpr.ps1` script once you are ready to create the PR to publish the release. TOy can also manually create
|
|
199
|
+
the tag, touch a file, commit and push the changes.
|
|
200
|
+
|
|
201
|
+
5. Merge the PR in GitHub.
|
|
202
|
+
|
|
203
|
+
6. Confirm the following:
|
|
204
|
+
|
|
205
|
+
7. The release update reflects in GitHub
|
|
206
|
+
|
|
207
|
+
8. The release update notification was sent
|
|
208
|
+
|
|
209
|
+
______________________________________________________________________
|
|
44
210
|
|
|
45
211
|
[cicd_codestyle_img]: https://img.shields.io/badge/code%20style-black-000000.svg "Black"
|
|
46
212
|
[cicd_codestyle_lnk]: https://github.com/psf/black "Black"
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
sqldbwrpr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
sqldbwrpr/sqldbwrpr.py,sha256=m17qvof-ffgzAj475f-qmT1_bwCDLVto0sPDcvnINTo,74541
|
|
3
|
+
sqldbwrpr-5.1.0.dist-info/METADATA,sha256=E-RVCjEMgjVdW-mBTEklHtSrGeElGi5zkCeNqZDBV4o,11042
|
|
4
|
+
sqldbwrpr-5.1.0.dist-info/WHEEL,sha256=EGEvSphFYqXKs23-kQBeyNoJP1nrT8ZJKQoi5p5DYL8,88
|
|
5
|
+
sqldbwrpr-5.1.0.dist-info/licenses/LICENSE.txt,sha256=IA9VQhUiFRCmo0yWTjYyjM37Ldm-rjBno-D0AW12UBs,1149
|
|
6
|
+
sqldbwrpr-5.1.0.dist-info/RECORD,,
|
sqldbwrpr-5.0.0.dist-info/RECORD
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
sqldbwrpr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
sqldbwrpr/sqldbwrpr.py,sha256=ufjneZ6W3akmC87_HJ1ZOFOGEmfZD76fBx9dvsQAmIU,59224
|
|
3
|
-
sqldbwrpr-5.0.0.dist-info/METADATA,sha256=SuWw8-yHoQG-ijNe5QDhqbMJfdwlBhvUUGDGZqLdM1E,5919
|
|
4
|
-
sqldbwrpr-5.0.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
5
|
-
sqldbwrpr-5.0.0.dist-info/licenses/LICENSE.txt,sha256=IA9VQhUiFRCmo0yWTjYyjM37Ldm-rjBno-D0AW12UBs,1149
|
|
6
|
-
sqldbwrpr-5.0.0.dist-info/RECORD,,
|
|
File without changes
|