execsql2 2.1.2__py3-none-any.whl → 2.4.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.
Files changed (94) hide show
  1. execsql/cli/__init__.py +436 -0
  2. execsql/cli/dsn.py +86 -0
  3. execsql/cli/help.py +140 -0
  4. execsql/{cli.py → cli/run.py} +14 -589
  5. execsql/config.py +65 -1
  6. execsql/db/access.py +27 -15
  7. execsql/db/base.py +328 -215
  8. execsql/db/dsn.py +10 -5
  9. execsql/db/duckdb.py +6 -2
  10. execsql/db/factory.py +21 -0
  11. execsql/db/firebird.py +27 -19
  12. execsql/db/mysql.py +12 -7
  13. execsql/db/oracle.py +15 -11
  14. execsql/db/postgres.py +31 -16
  15. execsql/db/sqlite.py +15 -11
  16. execsql/db/sqlserver.py +16 -5
  17. execsql/exceptions.py +25 -7
  18. execsql/exporters/base.py +12 -1
  19. execsql/exporters/delimited.py +80 -35
  20. execsql/exporters/duckdb.py +6 -2
  21. execsql/exporters/feather.py +10 -6
  22. execsql/exporters/html.py +89 -69
  23. execsql/exporters/json.py +52 -45
  24. execsql/exporters/latex.py +37 -27
  25. execsql/exporters/ods.py +32 -11
  26. execsql/exporters/parquet.py +5 -2
  27. execsql/exporters/pretty.py +16 -9
  28. execsql/exporters/raw.py +22 -16
  29. execsql/exporters/sqlite.py +6 -2
  30. execsql/exporters/templates.py +39 -21
  31. execsql/exporters/values.py +26 -20
  32. execsql/exporters/xls.py +30 -11
  33. execsql/exporters/xml.py +31 -13
  34. execsql/exporters/zip.py +15 -0
  35. execsql/importers/base.py +6 -4
  36. execsql/importers/csv.py +8 -6
  37. execsql/importers/feather.py +6 -4
  38. execsql/importers/ods.py +6 -4
  39. execsql/importers/xls.py +6 -4
  40. execsql/metacommands/__init__.py +208 -1548
  41. execsql/metacommands/conditions.py +101 -27
  42. execsql/metacommands/control.py +8 -4
  43. execsql/metacommands/data.py +6 -6
  44. execsql/metacommands/debug.py +6 -2
  45. execsql/metacommands/dispatch.py +2011 -0
  46. execsql/metacommands/io.py +67 -1310
  47. execsql/metacommands/io_export.py +442 -0
  48. execsql/metacommands/io_fileops.py +287 -0
  49. execsql/metacommands/io_import.py +398 -0
  50. execsql/metacommands/io_write.py +248 -0
  51. execsql/metacommands/prompt.py +22 -66
  52. execsql/metacommands/system.py +7 -2
  53. execsql/models.py +7 -0
  54. execsql/parser.py +10 -0
  55. execsql/py.typed +0 -0
  56. execsql/script/__init__.py +95 -0
  57. execsql/script/control.py +162 -0
  58. execsql/{script.py → script/engine.py} +184 -402
  59. execsql/script/variables.py +281 -0
  60. execsql/types.py +49 -20
  61. execsql/utils/auth.py +2 -0
  62. execsql/utils/crypto.py +4 -6
  63. execsql/utils/datetime.py +1 -0
  64. execsql/utils/errors.py +11 -0
  65. execsql/utils/fileio.py +33 -8
  66. execsql/utils/gui.py +46 -0
  67. execsql/utils/mail.py +7 -17
  68. execsql/utils/numeric.py +2 -0
  69. execsql/utils/regex.py +9 -0
  70. execsql/utils/strings.py +16 -0
  71. execsql/utils/timer.py +2 -0
  72. execsql2-2.4.0.data/data/execsql2_extras/README.md +65 -0
  73. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/execsql.conf +1 -1
  74. {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/METADATA +13 -6
  75. execsql2-2.4.0.dist-info/RECORD +108 -0
  76. execsql2-2.1.2.data/data/execsql2_extras/READ_ME.rst +0 -127
  77. execsql2-2.1.2.dist-info/RECORD +0 -96
  78. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  79. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  80. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
  81. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/md_compare.sql +0 -0
  82. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
  83. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
  84. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
  85. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  86. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  87. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/script_template.sql +0 -0
  88. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
  89. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  90. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  91. {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/WHEEL +0 -0
  92. {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/entry_points.txt +0 -0
  93. {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/licenses/LICENSE.txt +0 -0
  94. {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/licenses/NOTICE +0 -0
@@ -0,0 +1,162 @@
1
+ from __future__ import annotations
2
+
3
+ """Control-flow stack structures for execsql script execution.
4
+
5
+ Classes:
6
+ - :class:`BatchLevels` — tracks which databases are used in nested BEGIN/END BATCH blocks.
7
+ - :class:`IfItem` — one level of a nested IF/ELSE/ENDIF condition.
8
+ - :class:`IfLevels` — stack of boolean IF-level states.
9
+ """
10
+
11
+ from typing import Any
12
+
13
+ from execsql.exceptions import ErrInfo
14
+
15
+
16
+ # ---------------------------------------------------------------------------
17
+ # BatchLevels
18
+ # ---------------------------------------------------------------------------
19
+
20
+
21
+ class BatchLevels:
22
+ """Track the databases used within nested BEGIN/END BATCH blocks.
23
+
24
+ Maintains a stack of :class:`Batch` objects so that each nesting level
25
+ records its own set of active database connections for commit/rollback.
26
+ """
27
+
28
+ # A stack to keep a record of the databases used in nested batches.
29
+ class Batch:
30
+ def __init__(self) -> None:
31
+ self.dbs_used: list[Any] = []
32
+
33
+ def __init__(self) -> None:
34
+ self.batchlevels: list[BatchLevels.Batch] = []
35
+
36
+ def in_batch(self) -> bool:
37
+ """Return True if execution is currently inside at least one BATCH block."""
38
+ return len(self.batchlevels) > 0
39
+
40
+ def new_batch(self) -> None:
41
+ """Push a new empty batch level onto the stack."""
42
+ self.batchlevels.append(self.Batch())
43
+
44
+ def using_db(self, db: Any) -> None:
45
+ """Register *db* as used within the innermost active batch."""
46
+ if len(self.batchlevels) > 0 and db not in self.batchlevels[-1].dbs_used:
47
+ self.batchlevels[-1].dbs_used.append(db)
48
+
49
+ def uses_db(self, db: Any) -> bool:
50
+ """Return True if *db* is registered in any active batch level."""
51
+ if len(self.batchlevels) == 0:
52
+ return False
53
+ return any(db in batch.dbs_used for batch in self.batchlevels)
54
+
55
+ def rollback_batch(self) -> None:
56
+ """Roll back all databases registered in the innermost batch level."""
57
+ if len(self.batchlevels) > 0:
58
+ b = self.batchlevels[-1]
59
+ for db in b.dbs_used:
60
+ db.rollback()
61
+
62
+ def end_batch(self) -> None:
63
+ """Commit all databases in the innermost batch level and pop the stack."""
64
+ b = self.batchlevels.pop()
65
+ for db in b.dbs_used:
66
+ db.commit()
67
+
68
+
69
+ # ---------------------------------------------------------------------------
70
+ # IfItem / IfLevels
71
+ # ---------------------------------------------------------------------------
72
+
73
+
74
+ class IfItem:
75
+ """One level of a nested IF/ELSE/ENDIF condition, paired with its source location."""
76
+
77
+ # An object representing an 'if' level, with context data.
78
+ def __init__(self, tf_value: bool) -> None:
79
+ self.tf_value = tf_value
80
+ # Import from the package (not engine directly) so that test patches on
81
+ # execsql.script.current_script_line are effective.
82
+ from execsql.script import current_script_line
83
+
84
+ self.scriptname, self.scriptline = current_script_line()
85
+
86
+ def value(self) -> bool:
87
+ return self.tf_value
88
+
89
+ def invert(self) -> None:
90
+ self.tf_value = not self.tf_value
91
+
92
+ def change_to(self, tf_value: bool) -> None:
93
+ self.tf_value = tf_value
94
+
95
+ def script_line(self) -> tuple:
96
+ return (self.scriptname, self.scriptline)
97
+
98
+
99
+ class IfLevels:
100
+ """Stack of boolean IF-level states for nested conditional execution.
101
+
102
+ Each :meth:`nest` call corresponds to an IF statement; each
103
+ :meth:`unnest` call corresponds to an ENDIF. :meth:`all_true` drives
104
+ the execution gate — commands are skipped unless every level is ``True``.
105
+ """
106
+
107
+ # A stack of True/False values corresponding to a nested set of conditionals,
108
+ # with methods to manipulate and query the set of conditional states.
109
+ def __init__(self) -> None:
110
+ self.if_levels: list[IfItem] = []
111
+
112
+ def nest(self, tf_value: bool) -> None:
113
+ """Push a new IF level onto the stack with the given boolean value."""
114
+ self.if_levels.append(IfItem(tf_value))
115
+
116
+ def unnest(self) -> None:
117
+ """Pop the innermost IF level; raise ErrInfo if the stack is empty."""
118
+ if len(self.if_levels) == 0:
119
+ raise ErrInfo(type="error", other_msg="Can't exit an IF block; no IF block is active.")
120
+ else:
121
+ self.if_levels.pop()
122
+
123
+ def invert(self) -> None:
124
+ if len(self.if_levels) == 0:
125
+ raise ErrInfo(type="error", other_msg="Can't change the IF state; no IF block is active.")
126
+ else:
127
+ self.if_levels[-1].invert()
128
+
129
+ def replace(self, tf_value: bool) -> None:
130
+ if len(self.if_levels) == 0:
131
+ raise ErrInfo(type="error", other_msg="Can't change the IF state; no IF block is active.")
132
+ else:
133
+ self.if_levels[-1].change_to(tf_value)
134
+
135
+ def current(self) -> bool:
136
+ if len(self.if_levels) == 0:
137
+ raise ErrInfo(type="error", other_msg="No IF block is active.")
138
+ else:
139
+ return self.if_levels[-1].value()
140
+
141
+ def all_true(self) -> bool:
142
+ """Return True if every active IF level is true (or the stack is empty)."""
143
+ if self.if_levels == []:
144
+ return True
145
+ return all(tf.value() for tf in self.if_levels)
146
+
147
+ def only_current_false(self) -> bool:
148
+ # Returns True if the current if level is false and all higher levels are True.
149
+ if len(self.if_levels) == 0:
150
+ return False
151
+ elif len(self.if_levels) == 1:
152
+ return not self.if_levels[-1].value()
153
+ else:
154
+ return not self.if_levels[-1].value() and all(tf.value() for tf in self.if_levels[:-1])
155
+
156
+ def script_lines(self, top_n: int) -> list[tuple]:
157
+ # Returns a list of tuples containing the script name and line number
158
+ # for the topmost 'top_n' if levels, in bottom-up order.
159
+ if len(self.if_levels) < top_n:
160
+ raise ErrInfo(type="error", other_msg="Invalid IF stack depth reference.")
161
+ levels = self.if_levels[len(self.if_levels) - top_n :]
162
+ return [lvl.script_line() for lvl in levels]