execsql2 2.15.0__py3-none-any.whl → 2.15.2__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.
- execsql/config.py +238 -310
- execsql/db/base.py +0 -1
- execsql/db/duckdb.py +6 -7
- execsql/db/sqlite.py +47 -47
- execsql/gui/base.py +173 -28
- execsql/gui/console.py +50 -13
- execsql/gui/desktop.py +70 -28
- execsql/gui/tui.py +57 -32
- execsql/metacommands/conditions.py +0 -24
- execsql/metacommands/io_export.py +6 -0
- execsql/metacommands/io_import.py +5 -5
- execsql/metacommands/upsert.py +17 -33
- execsql/models.py +0 -1
- execsql/parser.py +22 -23
- execsql/script/engine.py +2 -0
- execsql/types.py +28 -30
- execsql/utils/datetime.py +52 -246
- execsql/utils/errors.py +0 -19
- execsql/utils/fileio.py +0 -8
- {execsql2-2.15.0.dist-info → execsql2-2.15.2.dist-info}/METADATA +2 -1
- {execsql2-2.15.0.dist-info → execsql2-2.15.2.dist-info}/RECORD +40 -41
- execsql/constants.py +0 -370
- {execsql2-2.15.0.data → execsql2-2.15.2.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.15.0.data → execsql2-2.15.2.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.15.0.data → execsql2-2.15.2.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.15.0.data → execsql2-2.15.2.data}/data/execsql2_extras/execsql.conf +0 -0
- {execsql2-2.15.0.data → execsql2-2.15.2.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.15.0.data → execsql2-2.15.2.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.15.0.data → execsql2-2.15.2.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.15.0.data → execsql2-2.15.2.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.15.0.data → execsql2-2.15.2.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.15.0.data → execsql2-2.15.2.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.15.0.data → execsql2-2.15.2.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.15.0.data → execsql2-2.15.2.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.15.0.data → execsql2-2.15.2.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.15.0.data → execsql2-2.15.2.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.15.0.data → execsql2-2.15.2.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.15.0.dist-info → execsql2-2.15.2.dist-info}/WHEEL +0 -0
- {execsql2-2.15.0.dist-info → execsql2-2.15.2.dist-info}/entry_points.txt +0 -0
- {execsql2-2.15.0.dist-info → execsql2-2.15.2.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.15.0.dist-info → execsql2-2.15.2.dist-info}/licenses/NOTICE +0 -0
execsql/config.py
CHANGED
|
@@ -74,6 +74,130 @@ class ConfigData:
|
|
|
74
74
|
_INCLUDE_REQ_SECTION = "include_required"
|
|
75
75
|
_INCLUDE_OPT_SECTION = "include_optional"
|
|
76
76
|
|
|
77
|
+
def _get_str(self, cp: ConfigParser, section: str, key: str, attr: str, *, required: bool = False) -> None:
|
|
78
|
+
"""Read a string option and set ``self.<attr>``.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
cp: ConfigParser instance to read from.
|
|
82
|
+
section: INI section name.
|
|
83
|
+
key: Option key within the section.
|
|
84
|
+
attr: Attribute name to set on ``self``.
|
|
85
|
+
required: If ``True``, raise :class:`ConfigError` when the value is ``None``.
|
|
86
|
+
"""
|
|
87
|
+
if cp.has_option(section, key):
|
|
88
|
+
val = cp.get(section, key)
|
|
89
|
+
if required and val is None:
|
|
90
|
+
raise ConfigError(f"The {key} cannot be missing.")
|
|
91
|
+
setattr(self, attr, val)
|
|
92
|
+
|
|
93
|
+
def _get_enum(
|
|
94
|
+
self,
|
|
95
|
+
cp: ConfigParser,
|
|
96
|
+
section: str,
|
|
97
|
+
key: str,
|
|
98
|
+
attr: str,
|
|
99
|
+
choices: tuple,
|
|
100
|
+
*,
|
|
101
|
+
lower: bool = True,
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Read a string option, validate against ``choices``, and set ``self.<attr>``.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
cp: ConfigParser instance to read from.
|
|
107
|
+
section: INI section name.
|
|
108
|
+
key: Option key within the section.
|
|
109
|
+
attr: Attribute name to set on ``self``.
|
|
110
|
+
choices: Tuple of permitted values.
|
|
111
|
+
lower: If ``True`` (default), lower-case the raw value before validation.
|
|
112
|
+
"""
|
|
113
|
+
if cp.has_option(section, key):
|
|
114
|
+
val = cp.get(section, key)
|
|
115
|
+
if lower:
|
|
116
|
+
val = val.lower()
|
|
117
|
+
if val not in choices:
|
|
118
|
+
raise ConfigError(f"Invalid argument to {key}: {val}")
|
|
119
|
+
setattr(self, attr, val)
|
|
120
|
+
|
|
121
|
+
def _get_bool(self, cp: ConfigParser, section: str, key: str, attr: str) -> None:
|
|
122
|
+
"""Read a boolean option and set ``self.<attr>``.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
cp: ConfigParser instance to read from.
|
|
126
|
+
section: INI section name.
|
|
127
|
+
key: Option key within the section.
|
|
128
|
+
attr: Attribute name to set on ``self``.
|
|
129
|
+
|
|
130
|
+
Raises:
|
|
131
|
+
ConfigError: If the value cannot be parsed as a boolean.
|
|
132
|
+
"""
|
|
133
|
+
if cp.has_option(section, key):
|
|
134
|
+
try:
|
|
135
|
+
setattr(self, attr, cp.getboolean(section, key))
|
|
136
|
+
except Exception as e:
|
|
137
|
+
raise ConfigError(f"Invalid argument for {key}.") from e
|
|
138
|
+
|
|
139
|
+
def _get_int(
|
|
140
|
+
self,
|
|
141
|
+
cp: ConfigParser,
|
|
142
|
+
section: str,
|
|
143
|
+
key: str,
|
|
144
|
+
attr: str,
|
|
145
|
+
*,
|
|
146
|
+
multiply: int = 1,
|
|
147
|
+
min_val: int | None = None,
|
|
148
|
+
) -> None:
|
|
149
|
+
"""Read an integer option, optionally multiply it, and set ``self.<attr>``.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
cp: ConfigParser instance to read from.
|
|
153
|
+
section: INI section name.
|
|
154
|
+
key: Option key within the section.
|
|
155
|
+
attr: Attribute name to set on ``self``.
|
|
156
|
+
multiply: Multiply the parsed integer by this factor (default 1).
|
|
157
|
+
min_val: If given, clamp the result to at least this value.
|
|
158
|
+
|
|
159
|
+
Raises:
|
|
160
|
+
ConfigError: If the value cannot be parsed as an integer.
|
|
161
|
+
"""
|
|
162
|
+
if cp.has_option(section, key):
|
|
163
|
+
try:
|
|
164
|
+
val = cp.getint(section, key) * multiply
|
|
165
|
+
except Exception as e:
|
|
166
|
+
raise ConfigError(f"Invalid argument for {key}.") from e
|
|
167
|
+
if min_val is not None:
|
|
168
|
+
val = max(min_val, val)
|
|
169
|
+
setattr(self, attr, val)
|
|
170
|
+
|
|
171
|
+
def _get_float(
|
|
172
|
+
self,
|
|
173
|
+
cp: ConfigParser,
|
|
174
|
+
section: str,
|
|
175
|
+
key: str,
|
|
176
|
+
attr: str,
|
|
177
|
+
*,
|
|
178
|
+
min_val: float | None = None,
|
|
179
|
+
) -> None:
|
|
180
|
+
"""Read a float option and set ``self.<attr>``.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
cp: ConfigParser instance to read from.
|
|
184
|
+
section: INI section name.
|
|
185
|
+
key: Option key within the section.
|
|
186
|
+
attr: Attribute name to set on ``self``.
|
|
187
|
+
min_val: If given, raise :class:`ConfigError` when the value is below this minimum.
|
|
188
|
+
|
|
189
|
+
Raises:
|
|
190
|
+
ConfigError: If the value cannot be parsed as a float, or is below ``min_val``.
|
|
191
|
+
"""
|
|
192
|
+
if cp.has_option(section, key):
|
|
193
|
+
try:
|
|
194
|
+
val = cp.getfloat(section, key)
|
|
195
|
+
except Exception as e:
|
|
196
|
+
raise ConfigError(f"Invalid argument for {key}.") from e
|
|
197
|
+
if min_val is not None and val < min_val:
|
|
198
|
+
raise ConfigError(f"Invalid {key}: {val}; must be >= {min_val}.")
|
|
199
|
+
setattr(self, attr, val)
|
|
200
|
+
|
|
77
201
|
def __init__(self, script_path: str, variable_pool: object) -> None:
|
|
78
202
|
"""Load and merge all discoverable execsql.conf files for the given script path.
|
|
79
203
|
|
|
@@ -172,245 +296,89 @@ class ConfigData:
|
|
|
172
296
|
self.files_read.append(configfile)
|
|
173
297
|
cp = ConfigParser()
|
|
174
298
|
cp.read(configfile)
|
|
299
|
+
# --- [connect] ---
|
|
175
300
|
if cp.has_option(self._CONNECT_SECTION, "db_type"):
|
|
176
301
|
t = cp.get(self._CONNECT_SECTION, "db_type").lower()
|
|
177
|
-
if
|
|
302
|
+
if t not in ("a", "d", "f", "k", "l", "m", "o", "p", "s"):
|
|
178
303
|
raise ConfigError(f"Invalid database type: {t}")
|
|
179
304
|
self.db_type = t
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
raise ConfigError("Invalid argument to boolean_words.") from e
|
|
258
|
-
if cp.has_option(self._INPUT_SECTION, "empty_strings"):
|
|
259
|
-
try:
|
|
260
|
-
self.empty_strings = cp.getboolean(self._INPUT_SECTION, "empty_strings")
|
|
261
|
-
except Exception as e:
|
|
262
|
-
raise ConfigError("Invalid argument to empty_strings.") from e
|
|
263
|
-
if cp.has_option(self._INPUT_SECTION, "only_strings"):
|
|
264
|
-
try:
|
|
265
|
-
self.only_strings = cp.getboolean(self._INPUT_SECTION, "only_strings")
|
|
266
|
-
except Exception as e:
|
|
267
|
-
raise ConfigError("Invalid argument to only_strings.") from e
|
|
268
|
-
if cp.has_option(self._INPUT_SECTION, "empty_rows"):
|
|
269
|
-
try:
|
|
270
|
-
self.empty_rows = cp.getboolean(self._INPUT_SECTION, "empty_rows")
|
|
271
|
-
except Exception as e:
|
|
272
|
-
raise ConfigError("Invalid argument to empty_rows.") from e
|
|
273
|
-
if cp.has_option(self._INPUT_SECTION, "delete_empty_columns"):
|
|
274
|
-
try:
|
|
275
|
-
self.del_empty_cols = cp.getboolean(self._INPUT_SECTION, "delete_empty_columns")
|
|
276
|
-
except Exception as e:
|
|
277
|
-
raise ConfigError("Invalid argument to delete_empty_columns.") from e
|
|
278
|
-
if cp.has_option(self._INPUT_SECTION, "create_column_headers"):
|
|
279
|
-
try:
|
|
280
|
-
self.create_col_hdrs = cp.getboolean(self._INPUT_SECTION, "create_column_headers")
|
|
281
|
-
except Exception as e:
|
|
282
|
-
raise ConfigError("Invalid argument to create_column_headers.") from e
|
|
283
|
-
if cp.has_option(self._INPUT_SECTION, "trim_column_headers"):
|
|
284
|
-
try:
|
|
285
|
-
self.trim_col_hdrs = cp.get(self._INPUT_SECTION, "trim_column_headers").lower()
|
|
286
|
-
except Exception as e:
|
|
287
|
-
raise ConfigError("Invalid argument to trim_column_headers.") from e
|
|
288
|
-
if self.trim_col_hdrs not in ("none", "both", "left", "right"):
|
|
289
|
-
raise ConfigError(f"Invalid argument to trim_column_headers: {self.trim_col_hdrs}.")
|
|
290
|
-
if cp.has_option(self._INPUT_SECTION, "clean_column_headers"):
|
|
291
|
-
try:
|
|
292
|
-
self.clean_col_hdrs = cp.getboolean(self._INPUT_SECTION, "clean_column_headers")
|
|
293
|
-
except Exception as e:
|
|
294
|
-
raise ConfigError("Invalid argument to clean_column_headers.") from e
|
|
295
|
-
if cp.has_option(self._INPUT_SECTION, "fold_column_headers"):
|
|
296
|
-
foldspec = cp.get(self._INPUT_SECTION, "fold_column_headers").lower()
|
|
297
|
-
if foldspec not in ("no", "lower", "upper"):
|
|
298
|
-
raise ConfigError(f"Invalid argument to fold_column_headers: {foldspec}.")
|
|
299
|
-
self.fold_col_hdrs = foldspec
|
|
300
|
-
if cp.has_option(self._INPUT_SECTION, "dedup_column_headers"):
|
|
301
|
-
try:
|
|
302
|
-
self.dedup_col_hdrs = cp.getboolean(self._INPUT_SECTION, "dedup_column_headers")
|
|
303
|
-
except Exception as e:
|
|
304
|
-
raise ConfigError("Invalid argument to dedup_column_headers.") from e
|
|
305
|
-
if cp.has_option(self._INPUT_SECTION, "trim_strings"):
|
|
306
|
-
try:
|
|
307
|
-
self.trim_strings = cp.getboolean(self._INPUT_SECTION, "trim_strings")
|
|
308
|
-
except Exception as e:
|
|
309
|
-
raise ConfigError("Invalid argument to trim_strings.") from e
|
|
310
|
-
if cp.has_option(self._INPUT_SECTION, "replace_newlines"):
|
|
311
|
-
try:
|
|
312
|
-
self.replace_newlines = cp.getboolean(self._INPUT_SECTION, "replace_newlines")
|
|
313
|
-
except Exception as e:
|
|
314
|
-
raise ConfigError("Invalid argument to replace_newlines.") from e
|
|
315
|
-
if cp.has_option(self._INPUT_SECTION, "import_row_buffer"):
|
|
316
|
-
try:
|
|
317
|
-
self.import_row_buffer = cp.getint(self._INPUT_SECTION, "import_row_buffer")
|
|
318
|
-
except Exception as e:
|
|
319
|
-
raise ConfigError("Invalid argument for import_row_buffer.") from e
|
|
320
|
-
if cp.has_option(self._INPUT_SECTION, "import_progress_interval"):
|
|
321
|
-
try:
|
|
322
|
-
self.import_progress_interval = cp.getint(self._INPUT_SECTION, "import_progress_interval")
|
|
323
|
-
except Exception as e:
|
|
324
|
-
raise ConfigError("Invalid argument for import_progress_interval.") from e
|
|
325
|
-
if cp.has_option(self._INPUT_SECTION, "show_progress"):
|
|
326
|
-
try:
|
|
327
|
-
self.show_progress = cp.getboolean(self._INPUT_SECTION, "show_progress")
|
|
328
|
-
except Exception as e:
|
|
329
|
-
raise ConfigError("Invalid argument for show_progress.") from e
|
|
330
|
-
if cp.has_option(self._INPUT_SECTION, "access_use_numeric"):
|
|
331
|
-
try:
|
|
332
|
-
self.access_use_numeric = cp.getboolean(self._INPUT_SECTION, "access_use_numeric")
|
|
333
|
-
except Exception as e:
|
|
334
|
-
raise ConfigError("Invalid argument to access_use_numeric.") from e
|
|
335
|
-
if cp.has_option(self._INPUT_SECTION, "import_only_common_columns"):
|
|
336
|
-
try:
|
|
337
|
-
self.import_common_cols_only = cp.getboolean(
|
|
338
|
-
self._INPUT_SECTION,
|
|
339
|
-
"import_only_common_columns",
|
|
340
|
-
)
|
|
341
|
-
except Exception as e:
|
|
342
|
-
raise ConfigError("Invalid argument to import_only_common_columns.") from e
|
|
343
|
-
if cp.has_option(self._INPUT_SECTION, "import_common_columns_only"):
|
|
344
|
-
try:
|
|
345
|
-
self.import_common_cols_only = cp.getboolean(
|
|
346
|
-
self._INPUT_SECTION,
|
|
347
|
-
"import_common_columns_only",
|
|
348
|
-
)
|
|
349
|
-
except Exception as e:
|
|
350
|
-
raise ConfigError("Invalid argument to import_common_columns_only.") from e
|
|
351
|
-
if cp.has_option(self._INPUT_SECTION, "scan_lines"):
|
|
352
|
-
try:
|
|
353
|
-
self.scan_lines = cp.getint(self._INPUT_SECTION, "scan_lines")
|
|
354
|
-
except Exception as e:
|
|
355
|
-
raise ConfigError("Invalid argument to scan_lines.") from e
|
|
356
|
-
if cp.has_option(self._INPUT_SECTION, "import_buffer"):
|
|
357
|
-
try:
|
|
358
|
-
self.import_buffer = cp.getint(self._INPUT_SECTION, "import_buffer") * 1024
|
|
359
|
-
except Exception as e:
|
|
360
|
-
raise ConfigError("Invalid argument for import_buffer.") from e
|
|
361
|
-
if cp.has_option(self._OUTPUT_SECTION, "log_write_messages"):
|
|
362
|
-
try:
|
|
363
|
-
self.tee_write_log = cp.getboolean(self._OUTPUT_SECTION, "log_write_messages")
|
|
364
|
-
except Exception as e:
|
|
365
|
-
raise ConfigError("Invalid argument to log_write_messages") from e
|
|
366
|
-
if cp.has_option(self._OUTPUT_SECTION, "hdf5_text_len"):
|
|
367
|
-
try:
|
|
368
|
-
self.hdf5_text_len = cp.getint(self._OUTPUT_SECTION, "hdf5_text_len")
|
|
369
|
-
except Exception as e:
|
|
370
|
-
raise ConfigError("Invalid argument to log_write_messages") from e
|
|
371
|
-
if cp.has_option(self._OUTPUT_SECTION, "css_file"):
|
|
372
|
-
self.css_file = cp.get(self._OUTPUT_SECTION, "css_file")
|
|
373
|
-
if self.css_file is None:
|
|
374
|
-
raise ConfigError("The css_file name is missing.")
|
|
375
|
-
if cp.has_option(self._OUTPUT_SECTION, "css_styles"):
|
|
376
|
-
self.css_styles = cp.get(self._OUTPUT_SECTION, "css_styles")
|
|
377
|
-
if self.css_styles is None:
|
|
378
|
-
raise ConfigError("The css_styles are missing.")
|
|
379
|
-
if cp.has_option(self._OUTPUT_SECTION, "make_export_dirs"):
|
|
380
|
-
try:
|
|
381
|
-
self.make_export_dirs = cp.getboolean(self._OUTPUT_SECTION, "make_export_dirs")
|
|
382
|
-
except Exception as e:
|
|
383
|
-
raise ConfigError("Invalid argument for make_export_dirs.") from e
|
|
384
|
-
if cp.has_option(self._OUTPUT_SECTION, "quote_all_text"):
|
|
385
|
-
try:
|
|
386
|
-
self.quote_all_text = cp.getboolean(self._OUTPUT_SECTION, "quote_all_text")
|
|
387
|
-
except Exception as e:
|
|
388
|
-
raise ConfigError("Invalid argument for make_export_dirs.") from e
|
|
389
|
-
if cp.has_option(self._OUTPUT_SECTION, "outfile_open_timeout"):
|
|
390
|
-
try:
|
|
391
|
-
self.outfile_open_timeout = cp.getint(self._OUTPUT_SECTION, "outfile_open_timeout")
|
|
392
|
-
except Exception as e:
|
|
393
|
-
raise ConfigError("Invalid argument for outfile_open_timeout.") from e
|
|
394
|
-
if cp.has_option(self._OUTPUT_SECTION, "export_row_buffer"):
|
|
395
|
-
try:
|
|
396
|
-
self.export_row_buffer = cp.getint(self._OUTPUT_SECTION, "export_row_buffer")
|
|
397
|
-
except Exception as e:
|
|
398
|
-
raise ConfigError("Invalid argument for export_row_buffer.") from e
|
|
399
|
-
if cp.has_option(self._OUTPUT_SECTION, "template_processor"):
|
|
400
|
-
tp = cp.get(self._OUTPUT_SECTION, "template_processor").lower()
|
|
401
|
-
if tp not in ("jinja",):
|
|
402
|
-
raise ConfigError(f"Invalid template processor name: {tp}")
|
|
403
|
-
self.template_processor = tp
|
|
404
|
-
if cp.has_option(self._OUTPUT_SECTION, "zip_buffer_mb"):
|
|
405
|
-
try:
|
|
406
|
-
self.zip_buffer_mb = cp.getint(self._OUTPUT_SECTION, "zip_buffer_mb")
|
|
407
|
-
except Exception as e:
|
|
408
|
-
raise ConfigError("Invalid argument for zip_buffer_mb.") from e
|
|
409
|
-
if cp.has_option(self._INTERFACE_SECTION, "write_warnings"):
|
|
410
|
-
try:
|
|
411
|
-
self.write_warnings = cp.getboolean(self._INTERFACE_SECTION, "write_warnings")
|
|
412
|
-
except Exception as e:
|
|
413
|
-
raise ConfigError("Invalid argument to write_warnings.") from e
|
|
305
|
+
self._get_str(cp, self._CONNECT_SECTION, "server", "server", required=True)
|
|
306
|
+
self._get_str(cp, self._CONNECT_SECTION, "db", "db", required=True)
|
|
307
|
+
self._get_int(cp, self._CONNECT_SECTION, "port", "port")
|
|
308
|
+
self._get_str(cp, self._CONNECT_SECTION, "database", "db", required=True)
|
|
309
|
+
self._get_str(cp, self._CONNECT_SECTION, "db_file", "db_file", required=True)
|
|
310
|
+
self._get_str(cp, self._CONNECT_SECTION, "username", "username", required=True)
|
|
311
|
+
self._get_str(cp, self._CONNECT_SECTION, "access_username", "access_username")
|
|
312
|
+
self._get_bool(cp, self._CONNECT_SECTION, "password_prompt", "passwd_prompt")
|
|
313
|
+
self._get_bool(cp, self._CONNECT_SECTION, "use_keyring", "use_keyring")
|
|
314
|
+
self._get_bool(cp, self._CONNECT_SECTION, "new_db", "new_db")
|
|
315
|
+
# --- [encoding] ---
|
|
316
|
+
self._get_str(cp, self._ENCODING_SECTION, "database", "db_encoding")
|
|
317
|
+
self._get_str(cp, self._ENCODING_SECTION, "script", "script_encoding", required=True)
|
|
318
|
+
self._get_str(cp, self._ENCODING_SECTION, "import", "import_encoding", required=True)
|
|
319
|
+
self._get_str(cp, self._ENCODING_SECTION, "output", "output_encoding", required=True)
|
|
320
|
+
self._get_enum(
|
|
321
|
+
cp,
|
|
322
|
+
self._ENCODING_SECTION,
|
|
323
|
+
"error_response",
|
|
324
|
+
"enc_err_disposition",
|
|
325
|
+
("ignore", "replace", "xmlcharrefreplace", "backslashreplace"),
|
|
326
|
+
)
|
|
327
|
+
# --- [input] ---
|
|
328
|
+
self._get_int(cp, self._INPUT_SECTION, "max_int", "max_int")
|
|
329
|
+
self._get_bool(cp, self._INPUT_SECTION, "boolean_int", "boolean_int")
|
|
330
|
+
self._get_bool(cp, self._INPUT_SECTION, "boolean_words", "boolean_words")
|
|
331
|
+
self._get_bool(cp, self._INPUT_SECTION, "empty_strings", "empty_strings")
|
|
332
|
+
self._get_bool(cp, self._INPUT_SECTION, "only_strings", "only_strings")
|
|
333
|
+
self._get_bool(cp, self._INPUT_SECTION, "empty_rows", "empty_rows")
|
|
334
|
+
self._get_bool(cp, self._INPUT_SECTION, "delete_empty_columns", "del_empty_cols")
|
|
335
|
+
self._get_bool(cp, self._INPUT_SECTION, "create_column_headers", "create_col_hdrs")
|
|
336
|
+
self._get_enum(
|
|
337
|
+
cp,
|
|
338
|
+
self._INPUT_SECTION,
|
|
339
|
+
"trim_column_headers",
|
|
340
|
+
"trim_col_hdrs",
|
|
341
|
+
("none", "both", "left", "right"),
|
|
342
|
+
)
|
|
343
|
+
self._get_bool(cp, self._INPUT_SECTION, "clean_column_headers", "clean_col_hdrs")
|
|
344
|
+
self._get_enum(
|
|
345
|
+
cp,
|
|
346
|
+
self._INPUT_SECTION,
|
|
347
|
+
"fold_column_headers",
|
|
348
|
+
"fold_col_hdrs",
|
|
349
|
+
("no", "lower", "upper"),
|
|
350
|
+
)
|
|
351
|
+
self._get_bool(cp, self._INPUT_SECTION, "dedup_column_headers", "dedup_col_hdrs")
|
|
352
|
+
self._get_bool(cp, self._INPUT_SECTION, "trim_strings", "trim_strings")
|
|
353
|
+
self._get_bool(cp, self._INPUT_SECTION, "replace_newlines", "replace_newlines")
|
|
354
|
+
self._get_int(cp, self._INPUT_SECTION, "import_row_buffer", "import_row_buffer")
|
|
355
|
+
self._get_int(cp, self._INPUT_SECTION, "import_progress_interval", "import_progress_interval")
|
|
356
|
+
self._get_bool(cp, self._INPUT_SECTION, "show_progress", "show_progress")
|
|
357
|
+
self._get_bool(cp, self._INPUT_SECTION, "access_use_numeric", "access_use_numeric")
|
|
358
|
+
self._get_bool(cp, self._INPUT_SECTION, "import_only_common_columns", "import_common_cols_only")
|
|
359
|
+
self._get_bool(cp, self._INPUT_SECTION, "import_common_columns_only", "import_common_cols_only")
|
|
360
|
+
self._get_int(cp, self._INPUT_SECTION, "scan_lines", "scan_lines")
|
|
361
|
+
self._get_int(cp, self._INPUT_SECTION, "import_buffer", "import_buffer", multiply=1024)
|
|
362
|
+
# --- [output] ---
|
|
363
|
+
self._get_bool(cp, self._OUTPUT_SECTION, "log_write_messages", "tee_write_log")
|
|
364
|
+
self._get_int(cp, self._OUTPUT_SECTION, "hdf5_text_len", "hdf5_text_len")
|
|
365
|
+
self._get_str(cp, self._OUTPUT_SECTION, "css_file", "css_file", required=True)
|
|
366
|
+
self._get_str(cp, self._OUTPUT_SECTION, "css_styles", "css_styles", required=True)
|
|
367
|
+
self._get_bool(cp, self._OUTPUT_SECTION, "make_export_dirs", "make_export_dirs")
|
|
368
|
+
self._get_bool(cp, self._OUTPUT_SECTION, "quote_all_text", "quote_all_text")
|
|
369
|
+
self._get_int(cp, self._OUTPUT_SECTION, "outfile_open_timeout", "outfile_open_timeout")
|
|
370
|
+
self._get_int(cp, self._OUTPUT_SECTION, "export_row_buffer", "export_row_buffer")
|
|
371
|
+
self._get_enum(
|
|
372
|
+
cp,
|
|
373
|
+
self._OUTPUT_SECTION,
|
|
374
|
+
"template_processor",
|
|
375
|
+
"template_processor",
|
|
376
|
+
("jinja",),
|
|
377
|
+
)
|
|
378
|
+
self._get_int(cp, self._OUTPUT_SECTION, "zip_buffer_mb", "zip_buffer_mb")
|
|
379
|
+
# --- [interface] ---
|
|
380
|
+
self._get_bool(cp, self._INTERFACE_SECTION, "write_warnings", "write_warnings")
|
|
381
|
+
# write_prefix / write_suffix have special "clear" → None handling
|
|
414
382
|
if cp.has_option(self._INTERFACE_SECTION, "write_prefix"):
|
|
415
383
|
try:
|
|
416
384
|
self.write_prefix = cp.get(self._INTERFACE_SECTION, "write_prefix")
|
|
@@ -425,38 +393,23 @@ class ConfigData:
|
|
|
425
393
|
raise ConfigError("Invalid or missing argument to write_suffix.") from e
|
|
426
394
|
if self.write_suffix.lower() == "clear":
|
|
427
395
|
self.write_suffix = None
|
|
396
|
+
# gui_level is an integer enum — keep inline to preserve exact error message
|
|
428
397
|
if cp.has_option(self._INTERFACE_SECTION, "gui_level"):
|
|
429
398
|
self.gui_level = cp.getint(self._INTERFACE_SECTION, "gui_level")
|
|
430
399
|
if self.gui_level not in (0, 1, 2, 3):
|
|
431
400
|
raise ConfigError(f"Invalid GUI level: {self.gui_level}")
|
|
401
|
+
# gui_framework has a specific error message — keep inline
|
|
432
402
|
if cp.has_option(self._INTERFACE_SECTION, "gui_framework"):
|
|
433
403
|
fw = cp.get(self._INTERFACE_SECTION, "gui_framework").lower()
|
|
434
404
|
if fw not in ("tkinter", "textual"):
|
|
435
405
|
raise ConfigError("gui_framework must be 'tkinter' or 'textual'.")
|
|
436
406
|
self.gui_framework = fw
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
try:
|
|
444
|
-
self.gui_console_width = max(20, cp.getint(self._INTERFACE_SECTION, "console_width"))
|
|
445
|
-
except Exception as e:
|
|
446
|
-
raise ConfigError("Invalid argument for console_width.") from e
|
|
447
|
-
if cp.has_option(self._INTERFACE_SECTION, "console_wait_when_done"):
|
|
448
|
-
try:
|
|
449
|
-
self.gui_wait_on_exit = cp.getboolean(self._INTERFACE_SECTION, "console_wait_when_done")
|
|
450
|
-
except Exception as e:
|
|
451
|
-
raise ConfigError("Invalid argument for console_wait_when_done.") from e
|
|
452
|
-
if cp.has_option(self._INTERFACE_SECTION, "console_wait_when_error_halt"):
|
|
453
|
-
try:
|
|
454
|
-
self.gui_wait_on_error_halt = cp.getboolean(
|
|
455
|
-
self._INTERFACE_SECTION,
|
|
456
|
-
"console_wait_when_error_halt",
|
|
457
|
-
)
|
|
458
|
-
except Exception as e:
|
|
459
|
-
raise ConfigError("Invalid argument for console_wait_when_error_halt.") from e
|
|
407
|
+
self._get_int(cp, self._INTERFACE_SECTION, "console_height", "gui_console_height", min_val=5)
|
|
408
|
+
self._get_int(cp, self._INTERFACE_SECTION, "console_width", "gui_console_width", min_val=20)
|
|
409
|
+
self._get_bool(cp, self._INTERFACE_SECTION, "console_wait_when_done", "gui_wait_on_exit")
|
|
410
|
+
self._get_bool(cp, self._INTERFACE_SECTION, "console_wait_when_error_halt", "gui_wait_on_error_halt")
|
|
411
|
+
# --- [config] ---
|
|
412
|
+
# config_file / OS-specific config files retain special chaining logic
|
|
460
413
|
if cp.has_option(self._CONFIG_SECTION, "config_file"):
|
|
461
414
|
conffile = cp.get(self._CONFIG_SECTION, "config_file")
|
|
462
415
|
if os.name == "posix" and conffile[0] == "~":
|
|
@@ -470,9 +423,17 @@ class ConfigData:
|
|
|
470
423
|
if Path(conffile).is_file():
|
|
471
424
|
# Silently ignore a non-existent file, for cross-OS compatibility.
|
|
472
425
|
config_files.insert(ix + 1, conffile)
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
426
|
+
# OS-specific additional config files.
|
|
427
|
+
_os_config_key: str | None = None
|
|
428
|
+
if sys.platform == "linux" and cp.has_option(self._CONFIG_SECTION, "linux_config_file"):
|
|
429
|
+
_os_config_key = "linux_config_file"
|
|
430
|
+
elif sys.platform == "darwin" and cp.has_option(self._CONFIG_SECTION, "macos_config_file"):
|
|
431
|
+
_os_config_key = "macos_config_file"
|
|
432
|
+
elif os.name == "nt" and cp.has_option(self._CONFIG_SECTION, "win_config_file"):
|
|
433
|
+
_os_config_key = "win_config_file"
|
|
434
|
+
if _os_config_key:
|
|
435
|
+
conffile = cp.get(self._CONFIG_SECTION, _os_config_key)
|
|
436
|
+
if conffile and conffile[0] == "~":
|
|
476
437
|
if len(conffile) == 1:
|
|
477
438
|
conffile = str(Path("~").expanduser())
|
|
478
439
|
elif len(conffile) > 1 and conffile[1] == os.sep:
|
|
@@ -482,67 +443,34 @@ class ConfigData:
|
|
|
482
443
|
conffile = str(Path(conffile) / self.config_file_name)
|
|
483
444
|
if Path(conffile).is_file():
|
|
484
445
|
config_files.insert(ix + 1, conffile)
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
conffile = variable_pool.substitute(conffile)[0]
|
|
488
|
-
if not Path(conffile).is_file():
|
|
489
|
-
conffile = str(Path(conffile) / self.config_file_name)
|
|
490
|
-
if Path(conffile).is_file():
|
|
491
|
-
config_files.insert(ix + 1, conffile)
|
|
492
|
-
if cp.has_option(self._CONFIG_SECTION, "user_logfile"):
|
|
493
|
-
self.user_logfile = cp.getboolean(self._CONFIG_SECTION, "user_logfile")
|
|
446
|
+
self._get_bool(cp, self._CONFIG_SECTION, "user_logfile", "user_logfile")
|
|
447
|
+
# dao_flush_delay_secs has a specific error message — keep inline
|
|
494
448
|
if cp.has_option(self._CONFIG_SECTION, "dao_flush_delay_secs"):
|
|
495
449
|
self.dao_flush_delay_secs = cp.getfloat(self._CONFIG_SECTION, "dao_flush_delay_secs")
|
|
496
450
|
if self.dao_flush_delay_secs < 5.0:
|
|
497
451
|
raise ConfigError(
|
|
498
452
|
f"Invalid DAO flush delay: {self.dao_flush_delay_secs}; must be >= 5.0.",
|
|
499
453
|
)
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
raise ConfigError("Invalid argument to log_sql setting.") from e
|
|
510
|
-
if cp.has_option(self._CONFIG_SECTION, "max_log_size_mb"):
|
|
511
|
-
try:
|
|
512
|
-
self.max_log_size_mb = cp.getint(self._CONFIG_SECTION, "max_log_size_mb")
|
|
513
|
-
except Exception as e:
|
|
514
|
-
raise ConfigError("Invalid argument to max_log_size_mb setting.") from e
|
|
515
|
-
if cp.has_option(self._EMAIL_SECTION, "host"):
|
|
516
|
-
self.smtp_host = cp.get(self._EMAIL_SECTION, "host")
|
|
517
|
-
if cp.has_option(self._EMAIL_SECTION, "port"):
|
|
518
|
-
self.smtp_port = cp.get(self._EMAIL_SECTION, "port")
|
|
519
|
-
try:
|
|
520
|
-
self.smtp_port = cp.getint(self._EMAIL_SECTION, "port")
|
|
521
|
-
except Exception as e:
|
|
522
|
-
raise ConfigError("Invalid argument for email port.") from e
|
|
523
|
-
if cp.has_option(self._EMAIL_SECTION, "username"):
|
|
524
|
-
self.smtp_username = cp.get(self._EMAIL_SECTION, "username")
|
|
525
|
-
if cp.has_option(self._EMAIL_SECTION, "password"):
|
|
526
|
-
self.smtp_password = cp.get(self._EMAIL_SECTION, "password")
|
|
454
|
+
self._get_bool(cp, self._CONFIG_SECTION, "log_datavars", "log_datavars")
|
|
455
|
+
self._get_bool(cp, self._CONFIG_SECTION, "log_sql", "log_sql")
|
|
456
|
+
self._get_int(cp, self._CONFIG_SECTION, "max_log_size_mb", "max_log_size_mb")
|
|
457
|
+
# --- [email] ---
|
|
458
|
+
self._get_str(cp, self._EMAIL_SECTION, "host", "smtp_host")
|
|
459
|
+
self._get_int(cp, self._EMAIL_SECTION, "port", "smtp_port")
|
|
460
|
+
self._get_str(cp, self._EMAIL_SECTION, "username", "smtp_username")
|
|
461
|
+
self._get_str(cp, self._EMAIL_SECTION, "password", "smtp_password")
|
|
462
|
+
# enc_password has special decryption logic — keep inline
|
|
527
463
|
if cp.has_option(self._EMAIL_SECTION, "enc_password"):
|
|
528
464
|
self.smtp_password = Encrypt().decrypt(cp.get(self._EMAIL_SECTION, "enc_password"))
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
except Exception as e:
|
|
533
|
-
raise ConfigError("Invalid argument for email use_ssl.") from e
|
|
534
|
-
if cp.has_option(self._EMAIL_SECTION, "use_tls"):
|
|
535
|
-
try:
|
|
536
|
-
self.smtp_tls = cp.getboolean(self._EMAIL_SECTION, "use_tls")
|
|
537
|
-
except Exception as e:
|
|
538
|
-
raise ConfigError("Invalid argument for email use_tls.") from e
|
|
465
|
+
self._get_bool(cp, self._EMAIL_SECTION, "use_ssl", "smtp_ssl")
|
|
466
|
+
self._get_bool(cp, self._EMAIL_SECTION, "use_tls", "smtp_tls")
|
|
467
|
+
# email_format has a specific error message — keep inline
|
|
539
468
|
if cp.has_option(self._EMAIL_SECTION, "email_format"):
|
|
540
469
|
fmt = cp.get(self._EMAIL_SECTION, "email_format").lower()
|
|
541
470
|
if fmt not in ("plain", "html"):
|
|
542
471
|
raise ConfigError(f"Invalid email format: {fmt}")
|
|
543
472
|
self.email_format = fmt
|
|
544
|
-
|
|
545
|
-
self.email_css = cp.get(self._EMAIL_SECTION, "message_css")
|
|
473
|
+
self._get_str(cp, self._EMAIL_SECTION, "message_css", "email_css")
|
|
546
474
|
if cp.has_section(self._VARIABLES_SECTION) and variable_pool:
|
|
547
475
|
varsect = cp.items(self._VARIABLES_SECTION)
|
|
548
476
|
for sub, repl in varsect:
|
|
@@ -641,7 +569,7 @@ class WriteHooks:
|
|
|
641
569
|
|
|
642
570
|
def write_err(self, strval: str) -> None:
|
|
643
571
|
"""Write an error string to the error-output hook, or to sys.stderr if unset."""
|
|
644
|
-
if strval
|
|
572
|
+
if not strval.endswith("\n"):
|
|
645
573
|
strval += "\n"
|
|
646
574
|
if self.err_func:
|
|
647
575
|
self.err_func(strval)
|