execsql2 2.15.8__py3-none-any.whl → 2.16.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 (66) hide show
  1. execsql/__init__.py +8 -3
  2. execsql/api.py +580 -0
  3. execsql/cli/__init__.py +123 -0
  4. execsql/cli/lint_ast.py +439 -0
  5. execsql/cli/run.py +113 -102
  6. execsql/config.py +29 -4
  7. execsql/db/access.py +1 -0
  8. execsql/db/base.py +4 -1
  9. execsql/db/dsn.py +3 -2
  10. execsql/db/duckdb.py +1 -1
  11. execsql/db/factory.py +3 -0
  12. execsql/db/firebird.py +2 -1
  13. execsql/db/mysql.py +2 -1
  14. execsql/db/oracle.py +2 -1
  15. execsql/db/postgres.py +2 -1
  16. execsql/db/sqlite.py +1 -1
  17. execsql/db/sqlserver.py +3 -2
  18. execsql/debug/repl.py +27 -10
  19. execsql/exporters/base.py +6 -4
  20. execsql/exporters/delimited.py +11 -3
  21. execsql/exporters/pretty.py +9 -12
  22. execsql/gui/tui.py +59 -2
  23. execsql/metacommands/__init__.py +3 -0
  24. execsql/metacommands/conditions.py +20 -2
  25. execsql/metacommands/connect.py +1 -1
  26. execsql/metacommands/control.py +8 -14
  27. execsql/metacommands/debug.py +6 -4
  28. execsql/metacommands/io_export.py +117 -315
  29. execsql/metacommands/io_fileops.py +7 -13
  30. execsql/metacommands/io_write.py +1 -1
  31. execsql/metacommands/script_ext.py +8 -5
  32. execsql/metacommands/upsert.py +40 -0
  33. execsql/models.py +8 -12
  34. execsql/plugins.py +414 -0
  35. execsql/script/__init__.py +36 -12
  36. execsql/script/ast.py +562 -0
  37. execsql/script/engine.py +59 -368
  38. execsql/script/executor.py +833 -0
  39. execsql/script/parser.py +663 -0
  40. execsql/script/variables.py +11 -0
  41. execsql/state.py +55 -2
  42. execsql/utils/crypto.py +14 -10
  43. execsql/utils/errors.py +31 -8
  44. execsql/utils/gui.py +139 -17
  45. execsql/utils/mail.py +15 -12
  46. {execsql2-2.15.8.dist-info → execsql2-2.16.0.dist-info}/METADATA +59 -1
  47. {execsql2-2.15.8.dist-info → execsql2-2.16.0.dist-info}/RECORD +66 -60
  48. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/README.md +0 -0
  49. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  50. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  51. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/execsql.conf +0 -0
  52. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
  53. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/md_compare.sql +0 -0
  54. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
  55. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
  56. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
  57. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  58. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  59. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/script_template.sql +0 -0
  60. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
  61. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  62. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  63. {execsql2-2.15.8.dist-info → execsql2-2.16.0.dist-info}/WHEEL +0 -0
  64. {execsql2-2.15.8.dist-info → execsql2-2.16.0.dist-info}/entry_points.txt +0 -0
  65. {execsql2-2.15.8.dist-info → execsql2-2.16.0.dist-info}/licenses/LICENSE.txt +0 -0
  66. {execsql2-2.15.8.dist-info → execsql2-2.16.0.dist-info}/licenses/NOTICE +0 -0
execsql/plugins.py ADDED
@@ -0,0 +1,414 @@
1
+ """Plugin discovery and registration for execsql.
2
+
3
+ Discovers and loads plugins via Python `entry_points`_. Plugins can
4
+ provide custom metacommands, export formats, and import formats.
5
+
6
+ Entry point groups:
7
+
8
+ - ``execsql.metacommands`` — custom metacommand handlers
9
+ - ``execsql.exporters`` — custom export formats
10
+ - ``execsql.importers`` — custom import formats
11
+
12
+ Each entry point should reference a **registration function** that
13
+ receives the appropriate registry and adds its entries.
14
+
15
+ Metacommand plugin example
16
+ --------------------------
17
+
18
+ In the plugin package's ``pyproject.toml``::
19
+
20
+ [project.entry-points."execsql.metacommands"]
21
+ my_plugin = "my_package.execsql_plugin:register"
22
+
23
+ The registration function receives a
24
+ :class:`~execsql.script.engine.MetaCommandList` and adds entries::
25
+
26
+ def register(mcl):
27
+ mcl.add(
28
+ r"^\\s*MY_COMMAND\\s+(?P<arg>.+)\\s*$",
29
+ my_handler,
30
+ description="MY_COMMAND",
31
+ category="action",
32
+ )
33
+
34
+ def my_handler(**kwargs):
35
+ import execsql.state as _state
36
+ arg = kwargs["arg"]
37
+ # ... do work ...
38
+
39
+ Exporter plugin example
40
+ -----------------------
41
+
42
+ In ``pyproject.toml``::
43
+
44
+ [project.entry-points."execsql.exporters"]
45
+ my_format = "my_package.execsql_export:register"
46
+
47
+ The registration function receives an :class:`ExporterRegistry` and
48
+ adds entries::
49
+
50
+ def register(registry):
51
+ registry.add(
52
+ format_name="myformat",
53
+ query_fn=my_query_exporter,
54
+ table_fn=my_table_exporter, # optional
55
+ description="My custom format",
56
+ )
57
+
58
+ .. _entry_points: https://packaging.python.org/en/latest/specifications/entry-points/
59
+ """
60
+
61
+ from __future__ import annotations
62
+
63
+ import logging
64
+ from collections.abc import Callable
65
+ from importlib.metadata import entry_points
66
+ from typing import Any
67
+
68
+ __all__ = [
69
+ "ExporterEntry",
70
+ "ExporterRegistry",
71
+ "ImporterEntry",
72
+ "ImporterRegistry",
73
+ "discover_metacommand_plugins",
74
+ "discover_exporter_plugins",
75
+ "discover_importer_plugins",
76
+ "discover_all_plugins",
77
+ "get_exporter_registry",
78
+ "get_importer_registry",
79
+ ]
80
+
81
+ _log = logging.getLogger(__name__)
82
+
83
+
84
+ # ---------------------------------------------------------------------------
85
+ # Entry point group names
86
+ # ---------------------------------------------------------------------------
87
+
88
+ METACOMMAND_GROUP = "execsql.metacommands"
89
+ EXPORTER_GROUP = "execsql.exporters"
90
+ IMPORTER_GROUP = "execsql.importers"
91
+
92
+
93
+ # ---------------------------------------------------------------------------
94
+ # Exporter registry
95
+ # ---------------------------------------------------------------------------
96
+
97
+
98
+ class ExporterEntry:
99
+ """A registered export format.
100
+
101
+ Attributes:
102
+ format_name: The format keyword (e.g. ``"csv"``, ``"myformat"``).
103
+ query_fn: Function for exporting query results.
104
+ table_fn: Function for exporting full tables (optional).
105
+ description: Human-readable description.
106
+ plugin_name: Name of the plugin that registered this entry, or
107
+ ``"built-in"`` for formats shipped with execsql.
108
+ """
109
+
110
+ __slots__ = ("format_name", "query_fn", "table_fn", "description", "plugin_name")
111
+
112
+ def __init__(
113
+ self,
114
+ format_name: str,
115
+ query_fn: Callable | None = None,
116
+ table_fn: Callable | None = None,
117
+ description: str = "",
118
+ plugin_name: str = "built-in",
119
+ ) -> None:
120
+ self.format_name = format_name.upper()
121
+ self.query_fn = query_fn
122
+ self.table_fn = table_fn
123
+ self.description = description
124
+ self.plugin_name = plugin_name
125
+
126
+ def __repr__(self) -> str:
127
+ return f"ExporterEntry({self.format_name!r}, plugin={self.plugin_name!r})"
128
+
129
+
130
+ class ExporterRegistry:
131
+ """Registry of available export formats.
132
+
133
+ Built-in formats are registered during initialization. Plugins add
134
+ their formats via :meth:`add`.
135
+ """
136
+
137
+ def __init__(self) -> None:
138
+ self._entries: dict[str, ExporterEntry] = {}
139
+
140
+ def add(
141
+ self,
142
+ format_name: str,
143
+ query_fn: Callable | None = None,
144
+ table_fn: Callable | None = None,
145
+ description: str = "",
146
+ plugin_name: str = "built-in",
147
+ ) -> None:
148
+ """Register an export format.
149
+
150
+ If a format with the same name already exists, the new entry
151
+ overwrites it (plugins can override built-in formats).
152
+ """
153
+ key = format_name.upper()
154
+ if key in self._entries and self._entries[key].plugin_name != plugin_name:
155
+ _log.info(
156
+ "Plugin %r overrides export format %r (was %r)",
157
+ plugin_name,
158
+ key,
159
+ self._entries[key].plugin_name,
160
+ )
161
+ self._entries[key] = ExporterEntry(
162
+ format_name=key,
163
+ query_fn=query_fn,
164
+ table_fn=table_fn,
165
+ description=description,
166
+ plugin_name=plugin_name,
167
+ )
168
+
169
+ def get(self, format_name: str) -> ExporterEntry | None:
170
+ """Look up an export format by name (case-insensitive)."""
171
+ return self._entries.get(format_name.upper())
172
+
173
+ def formats(self) -> list[str]:
174
+ """Return sorted list of registered format names."""
175
+ return sorted(self._entries)
176
+
177
+ def entries(self) -> list[ExporterEntry]:
178
+ """Return all registered entries."""
179
+ return list(self._entries.values())
180
+
181
+ def __contains__(self, format_name: str) -> bool:
182
+ return format_name.upper() in self._entries
183
+
184
+ def __len__(self) -> int:
185
+ return len(self._entries)
186
+
187
+
188
+ # ---------------------------------------------------------------------------
189
+ # Importer registry
190
+ # ---------------------------------------------------------------------------
191
+
192
+
193
+ class ImporterEntry:
194
+ """A registered import format.
195
+
196
+ Attributes:
197
+ format_name: The format keyword (e.g. ``"csv"``, ``"json"``).
198
+ import_fn: Function for importing data.
199
+ description: Human-readable description.
200
+ plugin_name: Name of the plugin that registered this entry.
201
+ """
202
+
203
+ __slots__ = ("format_name", "import_fn", "description", "plugin_name")
204
+
205
+ def __init__(
206
+ self,
207
+ format_name: str,
208
+ import_fn: Callable | None = None,
209
+ description: str = "",
210
+ plugin_name: str = "built-in",
211
+ ) -> None:
212
+ self.format_name = format_name.upper()
213
+ self.import_fn = import_fn
214
+ self.description = description
215
+ self.plugin_name = plugin_name
216
+
217
+ def __repr__(self) -> str:
218
+ return f"ImporterEntry({self.format_name!r}, plugin={self.plugin_name!r})"
219
+
220
+
221
+ class ImporterRegistry:
222
+ """Registry of available import formats."""
223
+
224
+ def __init__(self) -> None:
225
+ self._entries: dict[str, ImporterEntry] = {}
226
+
227
+ def add(
228
+ self,
229
+ format_name: str,
230
+ import_fn: Callable | None = None,
231
+ description: str = "",
232
+ plugin_name: str = "built-in",
233
+ ) -> None:
234
+ """Register an import format."""
235
+ key = format_name.upper()
236
+ if key in self._entries and self._entries[key].plugin_name != plugin_name:
237
+ _log.info(
238
+ "Plugin %r overrides import format %r (was %r)",
239
+ plugin_name,
240
+ key,
241
+ self._entries[key].plugin_name,
242
+ )
243
+ self._entries[key] = ImporterEntry(
244
+ format_name=key,
245
+ import_fn=import_fn,
246
+ description=description,
247
+ plugin_name=plugin_name,
248
+ )
249
+
250
+ def get(self, format_name: str) -> ImporterEntry | None:
251
+ return self._entries.get(format_name.upper())
252
+
253
+ def formats(self) -> list[str]:
254
+ return sorted(self._entries)
255
+
256
+ def entries(self) -> list[ImporterEntry]:
257
+ return list(self._entries.values())
258
+
259
+ def __contains__(self, format_name: str) -> bool:
260
+ return format_name.upper() in self._entries
261
+
262
+ def __len__(self) -> int:
263
+ return len(self._entries)
264
+
265
+
266
+ # ---------------------------------------------------------------------------
267
+ # Module-level singletons
268
+ # ---------------------------------------------------------------------------
269
+
270
+ _exporter_registry: ExporterRegistry | None = None
271
+ _importer_registry: ImporterRegistry | None = None
272
+
273
+
274
+ def get_exporter_registry() -> ExporterRegistry:
275
+ """Return the global exporter registry, creating it on first access."""
276
+ global _exporter_registry
277
+ if _exporter_registry is None:
278
+ _exporter_registry = ExporterRegistry()
279
+ return _exporter_registry
280
+
281
+
282
+ def get_importer_registry() -> ImporterRegistry:
283
+ """Return the global importer registry, creating it on first access."""
284
+ global _importer_registry
285
+ if _importer_registry is None:
286
+ _importer_registry = ImporterRegistry()
287
+ return _importer_registry
288
+
289
+
290
+ # ---------------------------------------------------------------------------
291
+ # Discovery functions
292
+ # ---------------------------------------------------------------------------
293
+
294
+
295
+ def _load_entry_points(group: str) -> list[tuple[str, Any]]:
296
+ """Load all entry points for a group, returning (name, loaded_object) pairs.
297
+
298
+ Errors during loading are logged and skipped — a broken plugin should
299
+ not prevent execsql from starting.
300
+ """
301
+ results = []
302
+ try:
303
+ eps = entry_points(group=group)
304
+ except Exception:
305
+ _log.warning("Failed to query entry points for group %r", group, exc_info=True)
306
+ return results
307
+
308
+ for ep in eps:
309
+ try:
310
+ obj = ep.load()
311
+ results.append((ep.name, obj))
312
+ except Exception:
313
+ _log.warning(
314
+ "Failed to load plugin %r from group %r: %s",
315
+ ep.name,
316
+ group,
317
+ ep.value,
318
+ exc_info=True,
319
+ )
320
+ return results
321
+
322
+
323
+ def discover_metacommand_plugins(mcl: Any) -> int:
324
+ """Discover and register metacommand plugins.
325
+
326
+ Each entry point should be a callable that receives a
327
+ :class:`~execsql.script.engine.MetaCommandList` and calls ``mcl.add()``
328
+ to register its commands.
329
+
330
+ Args:
331
+ mcl: The metacommand dispatch table to register into.
332
+
333
+ Returns:
334
+ Number of plugins successfully loaded.
335
+ """
336
+ loaded = 0
337
+ for name, register_fn in _load_entry_points(METACOMMAND_GROUP):
338
+ try:
339
+ register_fn(mcl)
340
+ _log.info("Loaded metacommand plugin: %s", name)
341
+ loaded += 1
342
+ except Exception:
343
+ _log.warning("Metacommand plugin %r failed during registration", name, exc_info=True)
344
+ return loaded
345
+
346
+
347
+ def discover_exporter_plugins(registry: ExporterRegistry | None = None) -> int:
348
+ """Discover and register exporter plugins.
349
+
350
+ Each entry point should be a callable that receives an
351
+ :class:`ExporterRegistry` and calls ``registry.add()`` to register
352
+ its formats.
353
+
354
+ Args:
355
+ registry: The exporter registry. Defaults to the global singleton.
356
+
357
+ Returns:
358
+ Number of plugins successfully loaded.
359
+ """
360
+ if registry is None:
361
+ registry = get_exporter_registry()
362
+ loaded = 0
363
+ for name, register_fn in _load_entry_points(EXPORTER_GROUP):
364
+ try:
365
+ register_fn(registry)
366
+ _log.info("Loaded exporter plugin: %s", name)
367
+ loaded += 1
368
+ except Exception:
369
+ _log.warning("Exporter plugin %r failed during registration", name, exc_info=True)
370
+ return loaded
371
+
372
+
373
+ def discover_importer_plugins(registry: ImporterRegistry | None = None) -> int:
374
+ """Discover and register importer plugins.
375
+
376
+ Each entry point should be a callable that receives an
377
+ :class:`ImporterRegistry` and calls ``registry.add()`` to register
378
+ its formats.
379
+
380
+ Args:
381
+ registry: The importer registry. Defaults to the global singleton.
382
+
383
+ Returns:
384
+ Number of plugins successfully loaded.
385
+ """
386
+ if registry is None:
387
+ registry = get_importer_registry()
388
+ loaded = 0
389
+ for name, register_fn in _load_entry_points(IMPORTER_GROUP):
390
+ try:
391
+ register_fn(registry)
392
+ _log.info("Loaded importer plugin: %s", name)
393
+ loaded += 1
394
+ except Exception:
395
+ _log.warning("Importer plugin %r failed during registration", name, exc_info=True)
396
+ return loaded
397
+
398
+
399
+ def discover_all_plugins(mcl: Any = None) -> dict[str, int]:
400
+ """Discover and load all plugin types.
401
+
402
+ Args:
403
+ mcl: The metacommand dispatch table. If ``None``, metacommand
404
+ plugins are skipped.
405
+
406
+ Returns:
407
+ Dict of ``{group_name: count}`` for each plugin type loaded.
408
+ """
409
+ results = {}
410
+ if mcl is not None:
411
+ results[METACOMMAND_GROUP] = discover_metacommand_plugins(mcl)
412
+ results[EXPORTER_GROUP] = discover_exporter_plugins()
413
+ results[IMPORTER_GROUP] = discover_importer_plugins()
414
+ return results
@@ -50,24 +50,36 @@ Key functions:
50
50
  from execsql.script.control import BatchLevels, IfItem, IfLevels
51
51
  from execsql.script.engine import (
52
52
  CommandList,
53
- CommandListUntilLoop,
54
- CommandListWhileLoop,
55
53
  MetaCommand,
56
54
  MetaCommandList,
57
55
  MetacommandStmt,
58
56
  ScriptCmd,
59
57
  ScriptExecSpec,
60
- ScriptFile,
61
58
  SqlStmt,
62
59
  current_script_line,
63
- read_sqlfile,
64
- read_sqlstring,
65
- runscripts,
66
60
  set_dynamic_system_vars,
67
61
  set_static_system_vars,
68
62
  set_system_vars,
69
63
  substitute_vars,
70
64
  )
65
+ from execsql.script.ast import (
66
+ BatchBlock,
67
+ Comment,
68
+ ConditionModifier,
69
+ ElseIfClause,
70
+ IfBlock,
71
+ IncludeDirective,
72
+ LoopBlock,
73
+ MetaCommandStatement as AstMetaCommand,
74
+ Node,
75
+ Script,
76
+ ScriptBlock,
77
+ SourceSpan,
78
+ SqlBlock,
79
+ SqlStatement as AstSqlStatement,
80
+ format_tree,
81
+ )
82
+ from execsql.script.parser import parse_script, parse_string
71
83
  from execsql.script.variables import CounterVars, LocalSubVarSet, ScriptArgSubVarSet, SubVarSet
72
84
 
73
85
  __all__ = [
@@ -84,16 +96,28 @@ __all__ = [
84
96
  "MetacommandStmt",
85
97
  "ScriptCmd",
86
98
  "CommandList",
87
- "CommandListWhileLoop",
88
- "CommandListUntilLoop",
89
- "ScriptFile",
90
99
  "ScriptExecSpec",
91
100
  "set_dynamic_system_vars",
92
101
  "set_static_system_vars",
93
102
  "set_system_vars",
94
103
  "substitute_vars",
95
- "runscripts",
96
104
  "current_script_line",
97
- "read_sqlfile",
98
- "read_sqlstring",
105
+ # AST nodes and parser
106
+ "Node",
107
+ "SourceSpan",
108
+ "AstSqlStatement",
109
+ "AstMetaCommand",
110
+ "Comment",
111
+ "ConditionModifier",
112
+ "ElseIfClause",
113
+ "IfBlock",
114
+ "LoopBlock",
115
+ "BatchBlock",
116
+ "ScriptBlock",
117
+ "SqlBlock",
118
+ "IncludeDirective",
119
+ "Script",
120
+ "format_tree",
121
+ "parse_script",
122
+ "parse_string",
99
123
  ]