execsql2 2.0.1__py3-none-any.whl → 2.1.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/cli.py +322 -108
- execsql/config.py +134 -114
- execsql/db/access.py +89 -65
- execsql/db/base.py +97 -68
- execsql/db/dsn.py +45 -29
- execsql/db/duckdb.py +4 -5
- execsql/db/factory.py +27 -27
- execsql/db/firebird.py +30 -18
- execsql/db/mysql.py +38 -14
- execsql/db/oracle.py +58 -33
- execsql/db/postgres.py +68 -28
- execsql/db/sqlite.py +36 -27
- execsql/db/sqlserver.py +45 -30
- execsql/exceptions.py +68 -64
- execsql/exporters/__init__.py +1 -1
- execsql/exporters/base.py +42 -17
- execsql/exporters/delimited.py +60 -59
- execsql/exporters/duckdb.py +8 -12
- execsql/exporters/feather.py +32 -24
- execsql/exporters/html.py +33 -30
- execsql/exporters/json.py +18 -17
- execsql/exporters/latex.py +11 -13
- execsql/exporters/ods.py +50 -46
- execsql/exporters/parquet.py +32 -0
- execsql/exporters/pretty.py +16 -15
- execsql/exporters/raw.py +9 -11
- execsql/exporters/sqlite.py +38 -38
- execsql/exporters/templates.py +15 -72
- execsql/exporters/values.py +13 -12
- execsql/exporters/xls.py +26 -26
- execsql/exporters/xml.py +12 -12
- execsql/exporters/zip.py +0 -3
- execsql/gui/__init__.py +2 -2
- execsql/gui/console.py +0 -1
- execsql/gui/desktop.py +6 -7
- execsql/gui/tui.py +8 -14
- execsql/importers/base.py +6 -9
- execsql/importers/csv.py +10 -17
- execsql/importers/feather.py +16 -22
- execsql/importers/ods.py +3 -4
- execsql/importers/xls.py +5 -6
- execsql/metacommands/__init__.py +8 -8
- execsql/metacommands/conditions.py +41 -33
- execsql/metacommands/connect.py +113 -99
- execsql/metacommands/control.py +38 -26
- execsql/metacommands/data.py +35 -33
- execsql/metacommands/debug.py +13 -9
- execsql/metacommands/io.py +288 -229
- execsql/metacommands/prompt.py +179 -157
- execsql/metacommands/script_ext.py +11 -9
- execsql/metacommands/system.py +44 -25
- execsql/models.py +9 -16
- execsql/parser.py +10 -10
- execsql/script.py +183 -157
- execsql/state.py +170 -208
- execsql/types.py +46 -81
- execsql/utils/auth.py +114 -14
- execsql/utils/crypto.py +31 -4
- execsql/utils/datetime.py +7 -7
- execsql/utils/errors.py +34 -29
- execsql/utils/fileio.py +90 -55
- execsql/utils/gui.py +22 -23
- execsql/utils/mail.py +15 -17
- execsql/utils/numeric.py +2 -3
- execsql/utils/regex.py +9 -12
- execsql/utils/strings.py +10 -12
- execsql/utils/timer.py +0 -2
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/execsql.conf +1 -1
- execsql2-2.1.2.dist-info/METADATA +300 -0
- execsql2-2.1.2.dist-info/RECORD +96 -0
- execsql2-2.0.1.dist-info/METADATA +0 -406
- execsql2-2.0.1.dist-info/RECORD +0 -95
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/READ_ME.rst +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/WHEEL +0 -0
- {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/entry_points.txt +0 -0
- {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/licenses/NOTICE +0 -0
execsql/types.py
CHANGED
|
@@ -84,7 +84,7 @@ class DataType:
|
|
|
84
84
|
# This method may be overridden in child classes.
|
|
85
85
|
if data is None:
|
|
86
86
|
raise DataTypeError(self.data_type_name, self._CONV_ERR % "NULL")
|
|
87
|
-
if type(data)
|
|
87
|
+
if type(data) is self.data_type:
|
|
88
88
|
return data
|
|
89
89
|
try:
|
|
90
90
|
i = self.data_type(data)
|
|
@@ -115,10 +115,8 @@ class DT_TimestampTZ(DataType):
|
|
|
115
115
|
def _is_match(self, data: object) -> bool:
|
|
116
116
|
if data is None:
|
|
117
117
|
return False
|
|
118
|
-
if
|
|
119
|
-
|
|
120
|
-
return True
|
|
121
|
-
return False
|
|
118
|
+
if isinstance(data, datetime.datetime):
|
|
119
|
+
return bool(data.tzinfo is not None and data.tzinfo.utcoffset(data) is not None)
|
|
122
120
|
if not isinstance(data, str):
|
|
123
121
|
return False
|
|
124
122
|
try:
|
|
@@ -183,17 +181,20 @@ class DT_Date(DataType):
|
|
|
183
181
|
data_type_name = "date"
|
|
184
182
|
data_type = datetime.date
|
|
185
183
|
|
|
184
|
+
def __init__(self) -> None:
|
|
185
|
+
self._date_fmts = collections.deque(date_fmts)
|
|
186
|
+
|
|
186
187
|
def __repr__(self) -> str:
|
|
187
188
|
return "DT_Date()"
|
|
188
189
|
|
|
189
190
|
def _from_data(self, data: object) -> object:
|
|
190
191
|
if data is None:
|
|
191
192
|
raise DataTypeError(self.data_type_name, self._CONV_ERR % "NULL")
|
|
192
|
-
if type(data)
|
|
193
|
+
if type(data) is self.data_type:
|
|
193
194
|
return data
|
|
194
195
|
if not isinstance(data, str):
|
|
195
196
|
raise DataTypeError(self.data_type_name, self._CONV_ERR % data)
|
|
196
|
-
for i, f in enumerate(
|
|
197
|
+
for i, f in enumerate(self._date_fmts): # noqa: B007
|
|
197
198
|
try:
|
|
198
199
|
dt = datetime.datetime.strptime(data, f)
|
|
199
200
|
dtt = datetime.date(dt.year, dt.month, dt.day)
|
|
@@ -203,8 +204,8 @@ class DT_Date(DataType):
|
|
|
203
204
|
else:
|
|
204
205
|
raise DataTypeError(self.data_type_name, self._CONV_ERR % data)
|
|
205
206
|
if i:
|
|
206
|
-
del
|
|
207
|
-
|
|
207
|
+
del self._date_fmts[i]
|
|
208
|
+
self._date_fmts.appendleft(f)
|
|
208
209
|
return dtt
|
|
209
210
|
|
|
210
211
|
|
|
@@ -232,9 +233,9 @@ class DT_Time(DataType):
|
|
|
232
233
|
def _from_data(self, data: object) -> object:
|
|
233
234
|
if data is None:
|
|
234
235
|
raise DataTypeError(self.data_type_name, self._CONV_ERR % "NULL")
|
|
235
|
-
if type(data)
|
|
236
|
+
if type(data) is self.data_type:
|
|
236
237
|
return data
|
|
237
|
-
if
|
|
238
|
+
if isinstance(data, datetime.datetime):
|
|
238
239
|
return datetime.time(data.hour, data.minute, data.second, data.microsecond)
|
|
239
240
|
if not isinstance(data, str):
|
|
240
241
|
raise DataTypeError(self.data_type_name, self._CONV_ERR % data)
|
|
@@ -250,48 +251,11 @@ class DT_Time(DataType):
|
|
|
250
251
|
return t
|
|
251
252
|
|
|
252
253
|
|
|
253
|
-
class DT_Time_Oracle(
|
|
254
|
-
|
|
255
|
-
|
|
254
|
+
class DT_Time_Oracle(DT_Time):
|
|
255
|
+
"""Oracle-specific time type stored as VARCHAR2 with length specification."""
|
|
256
|
+
|
|
256
257
|
lenspec = True
|
|
257
258
|
varlen = True
|
|
258
|
-
time_fmts = (
|
|
259
|
-
"%H:%M",
|
|
260
|
-
"%H%M:%S",
|
|
261
|
-
"%H%M:%S.%f",
|
|
262
|
-
"%H:%M:%S",
|
|
263
|
-
"%H:%M:%S.%f",
|
|
264
|
-
"%I:%M%p",
|
|
265
|
-
"%I:%M:%S%p",
|
|
266
|
-
"%I:%M:%S.%f%p",
|
|
267
|
-
"%I:%M %p",
|
|
268
|
-
"%I:%M:%S %p",
|
|
269
|
-
"%I:%M:%S.%f %p",
|
|
270
|
-
"%X",
|
|
271
|
-
)
|
|
272
|
-
|
|
273
|
-
def __repr__(self) -> str:
|
|
274
|
-
return "DT_Time()"
|
|
275
|
-
|
|
276
|
-
def _from_data(self, data: object) -> object:
|
|
277
|
-
if data is None:
|
|
278
|
-
raise DataTypeError(self.data_type_name, self._CONV_ERR % "NULL")
|
|
279
|
-
if type(data) == self.data_type:
|
|
280
|
-
return data
|
|
281
|
-
if type(data) == datetime.datetime:
|
|
282
|
-
return datetime.time(data.hour, data.minute, data.second, data.microsecond)
|
|
283
|
-
if not isinstance(data, str):
|
|
284
|
-
raise DataTypeError(self.data_type_name, self._CONV_ERR % data)
|
|
285
|
-
for f in self.time_fmts:
|
|
286
|
-
try:
|
|
287
|
-
dt = datetime.datetime.strptime(data, f)
|
|
288
|
-
t = datetime.time(dt.hour, dt.minute, dt.second, dt.microsecond)
|
|
289
|
-
except Exception:
|
|
290
|
-
continue
|
|
291
|
-
break
|
|
292
|
-
else:
|
|
293
|
-
raise DataTypeError(self.data_type_name, self._CONV_ERR % data)
|
|
294
|
-
return t
|
|
295
259
|
|
|
296
260
|
|
|
297
261
|
class DT_Boolean(DataType):
|
|
@@ -322,13 +286,14 @@ class DT_Boolean(DataType):
|
|
|
322
286
|
if data is None:
|
|
323
287
|
return False
|
|
324
288
|
self.set_bool_matches()
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
289
|
+
return bool(
|
|
290
|
+
isinstance(data, bool)
|
|
291
|
+
or conf.boolean_int
|
|
292
|
+
and type(data) is int
|
|
293
|
+
and data in (0, 1)
|
|
294
|
+
or isinstance(data, str)
|
|
295
|
+
and data.lower() in self.bool_repr,
|
|
296
|
+
)
|
|
332
297
|
|
|
333
298
|
def _from_data(self, data: object) -> object:
|
|
334
299
|
import execsql.state as _state
|
|
@@ -337,9 +302,9 @@ class DT_Boolean(DataType):
|
|
|
337
302
|
if data is None:
|
|
338
303
|
raise DataTypeError(self.data_type_name, self._CONV_ERR % "NULL")
|
|
339
304
|
self.set_bool_matches()
|
|
340
|
-
if
|
|
305
|
+
if isinstance(data, bool):
|
|
341
306
|
return data
|
|
342
|
-
elif conf.boolean_int and type(data)
|
|
307
|
+
elif conf.boolean_int and type(data) is int and data in (0, 1):
|
|
343
308
|
return data == 1
|
|
344
309
|
elif isinstance(data, str) and data.lower() in self.bool_repr:
|
|
345
310
|
return data.lower() in self.true
|
|
@@ -358,9 +323,9 @@ class DT_Integer(DataType):
|
|
|
358
323
|
import execsql.state as _state
|
|
359
324
|
|
|
360
325
|
conf = _state.conf
|
|
361
|
-
if type(data)
|
|
326
|
+
if type(data) is int:
|
|
362
327
|
return data <= conf.max_int and data >= -1 * conf.max_int - 1
|
|
363
|
-
elif
|
|
328
|
+
elif isinstance(data, float):
|
|
364
329
|
return False
|
|
365
330
|
elif isinstance(data, str):
|
|
366
331
|
if leading_zero_num(data):
|
|
@@ -378,9 +343,9 @@ class DT_Integer(DataType):
|
|
|
378
343
|
def _from_data(self, data: object) -> object:
|
|
379
344
|
if data is None:
|
|
380
345
|
raise DataTypeError(self.data_type_name, self._CONV_ERR % "NULL")
|
|
381
|
-
if type(data)
|
|
346
|
+
if type(data) is int:
|
|
382
347
|
return data
|
|
383
|
-
if
|
|
348
|
+
if isinstance(data, float):
|
|
384
349
|
if int(data) == data:
|
|
385
350
|
return int(data)
|
|
386
351
|
else:
|
|
@@ -408,16 +373,16 @@ class DT_Long(DataType):
|
|
|
408
373
|
|
|
409
374
|
if data is None:
|
|
410
375
|
raise DataTypeError(self.data_type_name, self._CONV_ERR % "NULL")
|
|
411
|
-
if type(data)
|
|
376
|
+
if type(data) is int:
|
|
412
377
|
return data
|
|
413
|
-
if
|
|
378
|
+
if isinstance(data, float):
|
|
414
379
|
if _math.isnan(data):
|
|
415
380
|
return None
|
|
416
381
|
else:
|
|
417
382
|
if int(data) == data:
|
|
418
383
|
return int(data)
|
|
419
384
|
raise DataTypeError(self.data_type_name, self._CONV_ERR % data)
|
|
420
|
-
if
|
|
385
|
+
if isinstance(data, Decimal):
|
|
421
386
|
raise DataTypeError(self.data_type_name, self._CONV_ERR % data)
|
|
422
387
|
if leading_zero_num(data):
|
|
423
388
|
raise DataTypeError(self.data_type_name, self._CONV_ERR % data)
|
|
@@ -440,14 +405,14 @@ class DT_Float(DataType):
|
|
|
440
405
|
def _is_match(self, data: object) -> bool:
|
|
441
406
|
if data is None:
|
|
442
407
|
return False
|
|
443
|
-
if
|
|
408
|
+
if isinstance(data, float):
|
|
444
409
|
return True
|
|
445
410
|
if leading_zero_num(data):
|
|
446
411
|
return False
|
|
447
412
|
if isinstance(data, str) and not re.match(r"^[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?$", data):
|
|
448
413
|
return False
|
|
449
414
|
try:
|
|
450
|
-
|
|
415
|
+
float(data)
|
|
451
416
|
except Exception:
|
|
452
417
|
return False
|
|
453
418
|
return True
|
|
@@ -455,7 +420,7 @@ class DT_Float(DataType):
|
|
|
455
420
|
def _from_data(self, data: object) -> object:
|
|
456
421
|
if data is None:
|
|
457
422
|
raise DataTypeError(self.data_type_name, self._CONV_ERR % "NULL")
|
|
458
|
-
if
|
|
423
|
+
if isinstance(data, float):
|
|
459
424
|
return data
|
|
460
425
|
if leading_zero_num(data):
|
|
461
426
|
raise DataTypeError(self.data_type_name, self._CONV_ERR % data)
|
|
@@ -491,7 +456,7 @@ class DT_Decimal(DataType):
|
|
|
491
456
|
raise DataTypeError(self.data_type_name, self._CONV_ERR % "NULL")
|
|
492
457
|
if leading_zero_num(data):
|
|
493
458
|
raise DataTypeError(self.data_type_name, self._CONV_ERR % data)
|
|
494
|
-
if
|
|
459
|
+
if isinstance(data, Decimal):
|
|
495
460
|
self.set_scale_prec(data)
|
|
496
461
|
return data
|
|
497
462
|
elif isinstance(data, str):
|
|
@@ -514,9 +479,9 @@ class DT_Character(DataType):
|
|
|
514
479
|
return "DT_Character()"
|
|
515
480
|
|
|
516
481
|
def _is_match(self, data: object) -> bool:
|
|
517
|
-
if
|
|
482
|
+
if isinstance(data, bytearray):
|
|
518
483
|
return False
|
|
519
|
-
return super(
|
|
484
|
+
return super()._is_match(data)
|
|
520
485
|
|
|
521
486
|
def _from_data(self, data: object) -> object:
|
|
522
487
|
# data must be non-null.
|
|
@@ -544,9 +509,9 @@ class DT_Varchar(DataType):
|
|
|
544
509
|
return "DT_Varchar()"
|
|
545
510
|
|
|
546
511
|
def _is_match(self, data: object) -> bool:
|
|
547
|
-
if
|
|
512
|
+
if isinstance(data, bytearray):
|
|
548
513
|
return False
|
|
549
|
-
return super(
|
|
514
|
+
return super()._is_match(data)
|
|
550
515
|
|
|
551
516
|
def _from_data(self, data: object) -> object:
|
|
552
517
|
# This varchar data type is the same as the character data type.
|
|
@@ -570,9 +535,9 @@ class DT_Text(DataType):
|
|
|
570
535
|
return "DT_Text()"
|
|
571
536
|
|
|
572
537
|
def _is_match(self, data: object) -> bool:
|
|
573
|
-
if
|
|
538
|
+
if isinstance(data, bytearray):
|
|
574
539
|
return False
|
|
575
|
-
return super(
|
|
540
|
+
return super()._is_match(data)
|
|
576
541
|
|
|
577
542
|
def _from_data(self, data: object) -> object:
|
|
578
543
|
if data is None:
|
|
@@ -724,10 +689,10 @@ dbt_sqlite.name_datatype(DT_Text, "varchar")
|
|
|
724
689
|
dbt_sqlite.name_datatype(DT_Binary, "blob")
|
|
725
690
|
|
|
726
691
|
dbt_duckdb = DbType("DuckDB")
|
|
727
|
-
dbt_duckdb.name_datatype(DT_TimestampTZ, "
|
|
728
|
-
dbt_duckdb.name_datatype(DT_Timestamp, "
|
|
729
|
-
dbt_duckdb.name_datatype(DT_Date, "
|
|
730
|
-
dbt_duckdb.name_datatype(DT_Time, "
|
|
692
|
+
dbt_duckdb.name_datatype(DT_TimestampTZ, "TIMESTAMPTZ")
|
|
693
|
+
dbt_duckdb.name_datatype(DT_Timestamp, "TIMESTAMP")
|
|
694
|
+
dbt_duckdb.name_datatype(DT_Date, "DATE")
|
|
695
|
+
dbt_duckdb.name_datatype(DT_Time, "TIME")
|
|
731
696
|
dbt_duckdb.name_datatype(DT_Integer, "INTEGER")
|
|
732
697
|
dbt_duckdb.name_datatype(DT_Long, "BIGINT")
|
|
733
698
|
dbt_duckdb.name_datatype(DT_Float, "REAL")
|
execsql/utils/auth.py
CHANGED
|
@@ -8,25 +8,121 @@ password on the terminal (via :func:`getpass.getpass`) or through the
|
|
|
8
8
|
GUI console when running in GUI mode. The last entered password is
|
|
9
9
|
cached in ``_state.upass`` so that re-prompting is suppressed when
|
|
10
10
|
the same password is needed again within the same session.
|
|
11
|
+
|
|
12
|
+
When the ``keyring`` package is installed, :func:`get_password` checks
|
|
13
|
+
the OS credential store first (macOS Keychain, Windows Credential
|
|
14
|
+
Manager, or Linux SecretService). If a stored password is found it is
|
|
15
|
+
returned without prompting. After a successful interactive prompt the
|
|
16
|
+
password is offered for storage in the keyring for future use (console
|
|
17
|
+
mode only; GUI mode stores silently).
|
|
18
|
+
|
|
19
|
+
If the stored credential turns out to be stale (e.g. after a password
|
|
20
|
+
change), callers should use :func:`password_from_keyring` to detect
|
|
21
|
+
this, call :func:`clear_stored_password` to remove the bad entry, and
|
|
22
|
+
re-prompt with ``skip_keyring=True``.
|
|
23
|
+
|
|
24
|
+
Keyring service names follow the pattern
|
|
25
|
+
``execsql/<db_type>/<server_or_file>/<database>``.
|
|
11
26
|
"""
|
|
12
27
|
|
|
13
28
|
import getpass
|
|
14
|
-
from typing import Optional
|
|
15
29
|
|
|
16
30
|
import execsql.state as _state
|
|
17
31
|
|
|
32
|
+
# Tracks whether the most recent get_password() call returned a keyring-stored value.
|
|
33
|
+
_last_from_keyring: bool = False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _keyring_service(dbms_name: str, database_name: str, server_name: str | None) -> str:
|
|
37
|
+
"""Build a keyring service name from connection parameters."""
|
|
38
|
+
server_part = server_name or "local"
|
|
39
|
+
return f"execsql/{dbms_name}/{server_part}/{database_name}"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _keyring_get(service: str, username: str) -> str | None:
|
|
43
|
+
"""Try to retrieve a password from the OS keyring. Returns None on failure."""
|
|
44
|
+
try:
|
|
45
|
+
import keyring
|
|
46
|
+
|
|
47
|
+
return keyring.get_password(service, username)
|
|
48
|
+
except Exception:
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _keyring_set(service: str, username: str, password: str) -> bool:
|
|
53
|
+
"""Try to store a password in the OS keyring. Returns True on success."""
|
|
54
|
+
try:
|
|
55
|
+
import keyring
|
|
56
|
+
|
|
57
|
+
keyring.set_password(service, username, password)
|
|
58
|
+
return True
|
|
59
|
+
except Exception:
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _keyring_delete(service: str, username: str) -> bool:
|
|
64
|
+
"""Try to remove a password from the OS keyring. Returns True on success."""
|
|
65
|
+
try:
|
|
66
|
+
import keyring
|
|
67
|
+
|
|
68
|
+
keyring.delete_password(service, username)
|
|
69
|
+
return True
|
|
70
|
+
except Exception:
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def password_from_keyring() -> bool:
|
|
75
|
+
"""Return True if the last :func:`get_password` call used a keyring-stored value."""
|
|
76
|
+
return _last_from_keyring
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def clear_stored_password(
|
|
80
|
+
dbms_name: str,
|
|
81
|
+
database_name: str,
|
|
82
|
+
user_name: str,
|
|
83
|
+
server_name: str | None = None,
|
|
84
|
+
) -> bool:
|
|
85
|
+
"""Remove a stored password from the OS keyring. Returns True on success."""
|
|
86
|
+
service = _keyring_service(dbms_name, database_name, server_name)
|
|
87
|
+
return _keyring_delete(service, user_name)
|
|
88
|
+
|
|
18
89
|
|
|
19
90
|
def get_password(
|
|
20
91
|
dbms_name: str,
|
|
21
92
|
database_name: str,
|
|
22
93
|
user_name: str,
|
|
23
|
-
server_name:
|
|
24
|
-
other_msg:
|
|
94
|
+
server_name: str | None = None,
|
|
95
|
+
other_msg: str | None = None,
|
|
96
|
+
skip_keyring: bool = False,
|
|
25
97
|
) -> str:
|
|
26
|
-
"""Prompt the user for a database password, using the GUI if available.
|
|
98
|
+
"""Prompt the user for a database password, using the GUI if available.
|
|
99
|
+
|
|
100
|
+
When ``keyring`` is installed the OS credential store is checked first.
|
|
101
|
+
If a stored credential is found it is returned immediately.
|
|
102
|
+
|
|
103
|
+
Parameters
|
|
104
|
+
----------
|
|
105
|
+
skip_keyring:
|
|
106
|
+
If True, bypass the keyring lookup and always prompt interactively.
|
|
107
|
+
Useful when a previously stored password is known to be invalid.
|
|
108
|
+
"""
|
|
109
|
+
global _last_from_keyring
|
|
27
110
|
# Deferred imports to avoid circular dependencies at import time.
|
|
28
111
|
from execsql.utils.errors import exit_now
|
|
29
112
|
|
|
113
|
+
_last_from_keyring = False
|
|
114
|
+
|
|
115
|
+
# --- Keyring lookup (before any prompting) ---
|
|
116
|
+
conf = _state.conf
|
|
117
|
+
use_keyring = conf is None or getattr(conf, "use_keyring", True)
|
|
118
|
+
service = _keyring_service(dbms_name, database_name, server_name)
|
|
119
|
+
if use_keyring and not skip_keyring:
|
|
120
|
+
stored = _keyring_get(service, user_name)
|
|
121
|
+
if stored is not None:
|
|
122
|
+
_last_from_keyring = True
|
|
123
|
+
_state.upass = stored
|
|
124
|
+
return stored
|
|
125
|
+
|
|
30
126
|
script_name = ""
|
|
31
127
|
prompt = f"The execsql script {script_name} wants the {dbms_name} password for"
|
|
32
128
|
if server_name is not None:
|
|
@@ -50,7 +146,7 @@ def get_password(
|
|
|
50
146
|
user_response = return_queue.get(block=True)
|
|
51
147
|
use_gui = user_response["console_running"]
|
|
52
148
|
except Exception:
|
|
53
|
-
pass
|
|
149
|
+
pass # GUI query failed; fall back to non-GUI password prompt.
|
|
54
150
|
|
|
55
151
|
conf = _state.conf
|
|
56
152
|
if use_gui or (conf is not None and conf.gui_level > 0):
|
|
@@ -73,15 +169,14 @@ def get_password(
|
|
|
73
169
|
user_response = return_queue.get(block=True)
|
|
74
170
|
btn = user_response["button"]
|
|
75
171
|
passwd = user_response["return_value"]
|
|
76
|
-
if not btn:
|
|
77
|
-
if _state.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
exit_now(2, None)
|
|
172
|
+
if not btn and _state.status and _state.status.cancel_halt:
|
|
173
|
+
if _state.exec_log:
|
|
174
|
+
_state.exec_log.log_exit_halt(
|
|
175
|
+
script_name,
|
|
176
|
+
0,
|
|
177
|
+
f"Canceled on password prompt for {dbms_name} database {database_name}, user {user_name}",
|
|
178
|
+
)
|
|
179
|
+
exit_now(2, None)
|
|
85
180
|
except Exception:
|
|
86
181
|
prompt_text = prompt.replace("\n", " ", 1).replace("\n", ", ") + " >"
|
|
87
182
|
passwd = getpass.getpass(str(prompt_text))
|
|
@@ -90,4 +185,9 @@ def get_password(
|
|
|
90
185
|
passwd = getpass.getpass(str(prompt_text))
|
|
91
186
|
|
|
92
187
|
_state.upass = passwd
|
|
188
|
+
|
|
189
|
+
# --- Offer to store in keyring after interactive prompt ---
|
|
190
|
+
if use_keyring and passwd:
|
|
191
|
+
_keyring_set(service, user_name, passwd)
|
|
192
|
+
|
|
93
193
|
return passwd
|
execsql/utils/crypto.py
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
"""
|
|
4
|
-
Simple reversible
|
|
4
|
+
Simple reversible obfuscation for execsql configuration credentials.
|
|
5
|
+
|
|
6
|
+
.. warning::
|
|
7
|
+
|
|
8
|
+
**This is obfuscation, not encryption.**
|
|
9
|
+
|
|
10
|
+
* The XOR keys are hardcoded in this source file; anyone with access to
|
|
11
|
+
the source code (or the installed package) can decode any password
|
|
12
|
+
produced by this module.
|
|
13
|
+
* Passwords stored via ``enc_password`` in ``execsql.conf`` should be
|
|
14
|
+
treated as **plaintext-equivalent**.
|
|
15
|
+
* For production deployments, prefer OS credential stores (e.g. macOS
|
|
16
|
+
Keychain, Windows Credential Manager, ``secret-tool`` on Linux) or
|
|
17
|
+
environment variables rather than relying on this obfuscation.
|
|
5
18
|
|
|
6
19
|
Provides :class:`Encrypt` with ``encrypt()`` / ``decrypt()`` methods
|
|
7
20
|
that use XOR against a fixed key table followed by base64 encoding.
|
|
8
|
-
This is *not* cryptographically secure — it is intended only to prevent
|
|
9
|
-
plaintext passwords from appearing verbatim in ``execsql.conf`` files.
|
|
10
21
|
The monolith (line 2301) called this "SIMPLE ENCRYPTION".
|
|
11
22
|
"""
|
|
12
23
|
|
|
@@ -15,6 +26,21 @@ import uuid
|
|
|
15
26
|
|
|
16
27
|
|
|
17
28
|
class Encrypt:
|
|
29
|
+
"""Reversible XOR-based obfuscation for configuration file passwords.
|
|
30
|
+
|
|
31
|
+
.. warning::
|
|
32
|
+
|
|
33
|
+
This class provides **obfuscation only** — not real encryption.
|
|
34
|
+
The XOR keys are embedded in the source code, so any password
|
|
35
|
+
encrypted with this class can be trivially reversed by anyone who
|
|
36
|
+
can read the source or the installed package. Passwords stored in
|
|
37
|
+
``execsql.conf`` using ``enc_password`` should be considered
|
|
38
|
+
plaintext-equivalent.
|
|
39
|
+
|
|
40
|
+
For meaningful credential protection, use OS-level credential
|
|
41
|
+
stores or environment variables instead.
|
|
42
|
+
"""
|
|
43
|
+
|
|
18
44
|
ky: dict = {}
|
|
19
45
|
ky["0"] = "6f2bba010bdf450a99c1c324ace5d765"
|
|
20
46
|
ky["3"] = "4a69dd15b6304ed491f10d0ebc7498cf"
|
|
@@ -32,7 +58,8 @@ class Encrypt:
|
|
|
32
58
|
def __init__(self) -> None:
|
|
33
59
|
global itertools
|
|
34
60
|
global base64
|
|
35
|
-
import itertools
|
|
61
|
+
import itertools
|
|
62
|
+
import base64
|
|
36
63
|
|
|
37
64
|
def xor(self, text: str, enckey: str) -> str:
|
|
38
65
|
return "".join(chr(ord(t) ^ ord(k)) for t, k in zip(text, itertools.cycle(enckey)))
|
execsql/utils/datetime.py
CHANGED
|
@@ -14,7 +14,7 @@ scanning imported data for type inference.
|
|
|
14
14
|
import collections
|
|
15
15
|
import datetime
|
|
16
16
|
import re
|
|
17
|
-
from typing import Any
|
|
17
|
+
from typing import Any
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
dt_fmts = collections.deque(
|
|
@@ -95,8 +95,8 @@ dt_fmts = collections.deque(
|
|
|
95
95
|
)
|
|
96
96
|
|
|
97
97
|
|
|
98
|
-
def parse_datetime(datestr: Any) ->
|
|
99
|
-
if
|
|
98
|
+
def parse_datetime(datestr: Any) -> datetime.datetime | None:
|
|
99
|
+
if isinstance(datestr, datetime.datetime):
|
|
100
100
|
return datestr
|
|
101
101
|
if not isinstance(datestr, str):
|
|
102
102
|
try:
|
|
@@ -104,7 +104,7 @@ def parse_datetime(datestr: Any) -> Optional[datetime.datetime]:
|
|
|
104
104
|
except Exception:
|
|
105
105
|
return None
|
|
106
106
|
dt = None
|
|
107
|
-
for i, f in enumerate(dt_fmts):
|
|
107
|
+
for i, f in enumerate(dt_fmts): # noqa: B007
|
|
108
108
|
try:
|
|
109
109
|
dt = datetime.datetime.strptime(datestr, f)
|
|
110
110
|
except Exception:
|
|
@@ -238,11 +238,11 @@ timestamptz_fmts = collections.deque(
|
|
|
238
238
|
)
|
|
239
239
|
|
|
240
240
|
|
|
241
|
-
def parse_datetimetz(data: Any) ->
|
|
241
|
+
def parse_datetimetz(data: Any) -> datetime.datetime | None:
|
|
242
242
|
# Import Tz locally to avoid circular imports
|
|
243
243
|
from execsql.types import Tz
|
|
244
244
|
|
|
245
|
-
if
|
|
245
|
+
if isinstance(data, datetime.datetime):
|
|
246
246
|
if data.tzinfo is None or data.tzinfo.utcoffset(data) is None:
|
|
247
247
|
return None
|
|
248
248
|
return data
|
|
@@ -267,7 +267,7 @@ def parse_datetimetz(data: Any) -> Optional[datetime.datetime]:
|
|
|
267
267
|
)
|
|
268
268
|
except Exception:
|
|
269
269
|
# Check for alphabetic timezone
|
|
270
|
-
for i, f in enumerate(timestamptz_fmts):
|
|
270
|
+
for i, f in enumerate(timestamptz_fmts): # noqa: B007
|
|
271
271
|
try:
|
|
272
272
|
dt = datetime.datetime.strptime(data, f)
|
|
273
273
|
except Exception:
|