execsql2 2.16.15__py3-none-any.whl → 2.16.16__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 (31) hide show
  1. execsql/api.py +4 -0
  2. execsql/cli/__init__.py +156 -134
  3. execsql/cli/help.py +10 -1
  4. execsql/cli/run.py +4 -0
  5. execsql/config.py +2 -0
  6. execsql/data/__init__.py +0 -0
  7. execsql/data/execsql.conf.template +327 -0
  8. execsql/db/sqlite.py +47 -43
  9. execsql/metacommands/system.py +10 -9
  10. execsql2-2.16.16.data/data/execsql2_extras/execsql.conf +327 -0
  11. {execsql2-2.16.15.dist-info → execsql2-2.16.16.dist-info}/METADATA +1 -1
  12. {execsql2-2.16.15.dist-info → execsql2-2.16.16.dist-info}/RECORD +30 -28
  13. execsql2-2.16.15.data/data/execsql2_extras/execsql.conf +0 -287
  14. {execsql2-2.16.15.data → execsql2-2.16.16.data}/data/execsql2_extras/README.md +0 -0
  15. {execsql2-2.16.15.data → execsql2-2.16.16.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  16. {execsql2-2.16.15.data → execsql2-2.16.16.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  17. {execsql2-2.16.15.data → execsql2-2.16.16.data}/data/execsql2_extras/make_config_db.sql +0 -0
  18. {execsql2-2.16.15.data → execsql2-2.16.16.data}/data/execsql2_extras/md_compare.sql +0 -0
  19. {execsql2-2.16.15.data → execsql2-2.16.16.data}/data/execsql2_extras/md_glossary.sql +0 -0
  20. {execsql2-2.16.15.data → execsql2-2.16.16.data}/data/execsql2_extras/md_upsert.sql +0 -0
  21. {execsql2-2.16.15.data → execsql2-2.16.16.data}/data/execsql2_extras/pg_compare.sql +0 -0
  22. {execsql2-2.16.15.data → execsql2-2.16.16.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  23. {execsql2-2.16.15.data → execsql2-2.16.16.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  24. {execsql2-2.16.15.data → execsql2-2.16.16.data}/data/execsql2_extras/script_template.sql +0 -0
  25. {execsql2-2.16.15.data → execsql2-2.16.16.data}/data/execsql2_extras/ss_compare.sql +0 -0
  26. {execsql2-2.16.15.data → execsql2-2.16.16.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  27. {execsql2-2.16.15.data → execsql2-2.16.16.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  28. {execsql2-2.16.15.dist-info → execsql2-2.16.16.dist-info}/WHEEL +0 -0
  29. {execsql2-2.16.15.dist-info → execsql2-2.16.16.dist-info}/entry_points.txt +0 -0
  30. {execsql2-2.16.15.dist-info → execsql2-2.16.16.dist-info}/licenses/LICENSE.txt +0 -0
  31. {execsql2-2.16.15.dist-info → execsql2-2.16.16.dist-info}/licenses/NOTICE +0 -0
execsql/api.py CHANGED
@@ -316,6 +316,7 @@ def run(
316
316
  encoding: str = "utf-8",
317
317
  halt_on_error: bool = True,
318
318
  new_db: bool = False,
319
+ allow_system_cmd: bool = True,
319
320
  ) -> ScriptResult:
320
321
  """Execute a SQL script and return the result.
321
322
 
@@ -337,6 +338,8 @@ def run(
337
338
  error. If ``False``, capture errors and continue.
338
339
  new_db: If ``True``, create the database if it does not exist
339
340
  (SQLite, PostgreSQL, DuckDB).
341
+ allow_system_cmd: If ``False``, the SYSTEM_CMD (SHELL) metacommand
342
+ is disabled and will raise an error if encountered.
340
343
 
341
344
  Returns:
342
345
  A :class:`ScriptResult` with execution outcome, timing, errors,
@@ -434,6 +437,7 @@ def run(
434
437
  ctx.subvars = subvars
435
438
  ctx.status = StatObj()
436
439
  ctx.status.halt_on_err = halt_on_error
440
+ conf.allow_system_cmd = allow_system_cmd
437
441
  ctx.conf = conf
438
442
 
439
443
  # Capture output to a buffer (suppress stdout/stderr)
execsql/cli/__init__.py CHANGED
@@ -20,7 +20,7 @@ import typer
20
20
 
21
21
  from execsql import __version__
22
22
  from execsql.cli.dsn import _parse_connection_string, _SCHEME_TO_DBTYPE # noqa: F401 — re-export
23
- from execsql.cli.help import _console, _err_console, _print_encodings, _print_metacommands # noqa: F401 — re-export
23
+ from execsql.cli.help import _console, _err_console, _init_config, _print_encodings, _print_metacommands # noqa: F401 — re-export
24
24
  from execsql.cli.run import _connect_initial_db, _run # noqa: F401 — re-export
25
25
  from execsql.exceptions import ConfigError, ErrInfo
26
26
 
@@ -29,6 +29,7 @@ __all__ = [
29
29
  "_connect_initial_db",
30
30
  "_console",
31
31
  "_err_console",
32
+ "_init_config",
32
33
  "_legacy_main",
33
34
  "_parse_connection_string",
34
35
  "_print_encodings",
@@ -72,28 +73,55 @@ def main(
72
73
  "name (client-server DBs) or a database file path (file-based DBs)."
73
74
  ),
74
75
  ),
75
- # Named options — grouped to mirror the original argparse interface
76
- sub_vars: list[str] | None = typer.Option(
76
+ # -- Connection --------------------------------------------------------
77
+ db_type: str | None = typer.Option(
77
78
  None,
78
- "-a",
79
- "--assign-arg",
80
- metavar="VALUE",
81
- help="Define the replacement string for a substitution variable [cyan]\\$ARG_x[/cyan].",
79
+ "-t",
80
+ "--type",
81
+ metavar="{a,d,p,s,l,m,k,o,f}",
82
+ help=(
83
+ "Database type: [bold]a[/bold]=MS-Access, [bold]p[/bold]=PostgreSQL, "
84
+ "[bold]s[/bold]=SQL Server, [bold]l[/bold]=SQLite, [bold]m[/bold]=MySQL/MariaDB, "
85
+ "[bold]k[/bold]=DuckDB, [bold]o[/bold]=Oracle, [bold]f[/bold]=Firebird, "
86
+ "[bold]d[/bold]=DSN."
87
+ ),
82
88
  ),
83
- boolean_int: str | None = typer.Option(
89
+ dsn: str | None = typer.Option(
84
90
  None,
85
- "-b",
86
- "--boolean-int",
87
- metavar="{0,1,t,f,y,n}",
88
- help="Treat integers 0 and 1 as boolean values.",
91
+ "--dsn",
92
+ "--connection-string",
93
+ metavar="URL",
94
+ help=(
95
+ "Database connection URL, e.g. [cyan]postgresql://user:pass@host:5432/db[/cyan]. "
96
+ "Supported schemes: postgresql, mysql, mssql, oracle, firebird, sqlite, duckdb. "
97
+ "Overrides [cyan]-t[/cyan]/[cyan]-u[/cyan]/[cyan]-p[/cyan] and positional server/db args."
98
+ ),
89
99
  ),
90
- make_dirs: str | None = typer.Option(
100
+ user: str | None = typer.Option(
91
101
  None,
92
- "-d",
93
- "--directories",
94
- metavar="{0,1,t,f,y,n}",
95
- help="Auto-create directories for EXPORT metacommand. [dim]n=no (default), y=yes[/dim]",
102
+ "-u",
103
+ "--user",
104
+ help="Database user name.",
96
105
  ),
106
+ port: int | None = typer.Option(
107
+ None,
108
+ "-p",
109
+ "--port",
110
+ help="Database server port.",
111
+ ),
112
+ no_passwd: bool = typer.Option(
113
+ False,
114
+ "-w",
115
+ "--no-passwd",
116
+ help="Skip password prompt when user is specified.",
117
+ ),
118
+ new_db: bool = typer.Option(
119
+ False,
120
+ "-n",
121
+ "--new-db",
122
+ help="Create a new SQLite or Postgres database if it does not exist.",
123
+ ),
124
+ # -- Encoding ----------------------------------------------------------
97
125
  database_encoding: str | None = typer.Option(
98
126
  None,
99
127
  "-e",
@@ -118,35 +146,13 @@ def main(
118
146
  "--import-encoding",
119
147
  help="Encoding for data files used with IMPORT.",
120
148
  ),
121
- user_logfile: bool = typer.Option(
122
- False,
123
- "-l",
124
- "--user-logfile",
125
- help="Write a log file to [cyan]~/execsql.log[/cyan].",
126
- ),
127
- metacommands: bool = typer.Option(
128
- False,
129
- "-m",
130
- "--metacommands",
131
- help="List metacommands and exit.",
132
- ),
133
- new_db: bool = typer.Option(
134
- False,
135
- "-n",
136
- "--new-db",
137
- help="Create a new SQLite or Postgres database if it does not exist.",
138
- ),
139
- online_help: bool = typer.Option(
140
- False,
141
- "-o",
142
- "--online-help",
143
- help="Open the online documentation in the default browser.",
144
- ),
145
- port: int | None = typer.Option(
149
+ # -- Import/Export -----------------------------------------------------
150
+ import_buffer: int | None = typer.Option(
146
151
  None,
147
- "-p",
148
- "--port",
149
- help="Database server port.",
152
+ "-z",
153
+ "--import-buffer",
154
+ metavar="KB",
155
+ help="Import buffer size in KB. [dim]Default: 32[/dim]",
150
156
  ),
151
157
  scanlines: int | None = typer.Option(
152
158
  None,
@@ -155,59 +161,36 @@ def main(
155
161
  metavar="N",
156
162
  help="Lines to scan for IMPORT format detection. [dim]0 = scan entire file.[/dim]",
157
163
  ),
158
- db_type: str | None = typer.Option(
164
+ boolean_int: str | None = typer.Option(
159
165
  None,
160
- "-t",
161
- "--type",
162
- metavar="{a,d,p,s,l,m,k,o,f}",
163
- help=(
164
- "Database type: [bold]a[/bold]=MS-Access, [bold]p[/bold]=PostgreSQL, "
165
- "[bold]s[/bold]=SQL Server, [bold]l[/bold]=SQLite, [bold]m[/bold]=MySQL/MariaDB, "
166
- "[bold]k[/bold]=DuckDB, [bold]o[/bold]=Oracle, [bold]f[/bold]=Firebird, "
167
- "[bold]d[/bold]=DSN."
168
- ),
166
+ "-b",
167
+ "--boolean-int",
168
+ metavar="{0,1,t,f,y,n}",
169
+ help="Treat integers 0 and 1 as boolean values.",
169
170
  ),
170
- user: str | None = typer.Option(
171
+ make_dirs: str | None = typer.Option(
171
172
  None,
172
- "-u",
173
- "--user",
174
- help="Database user name.",
173
+ "-d",
174
+ "--directories",
175
+ metavar="{0,1,t,f,y,n}",
176
+ help="Auto-create directories for EXPORT metacommand. [dim]n=no (default), y=yes[/dim]",
175
177
  ),
176
- use_gui: str | None = typer.Option(
178
+ output_dir: str | None = typer.Option(
177
179
  None,
178
- "-v",
179
- "--visible-prompts",
180
- metavar="{0,1,2,3}",
180
+ "--output-dir",
181
+ metavar="DIR",
181
182
  help=(
182
- "GUI level: [bold]0[/bold]=none (default), [bold]1[/bold]=GUI for password/pause, "
183
- "[bold]2[/bold]=GUI for password/pause + DB selection, [bold]3[/bold]=full GUI console."
183
+ "Default base directory for EXPORT output files. "
184
+ "Relative paths in EXPORT metacommands are joined to this directory. "
185
+ "Absolute paths and [cyan]stdout[/cyan] are unaffected."
184
186
  ),
185
187
  ),
186
- gui_framework: str | None = typer.Option(
187
- None,
188
- "--gui-framework",
189
- metavar="{tkinter,textual}",
190
- help="GUI framework to use with [cyan]--visible-prompts[/cyan]. [dim]Default: tkinter[/dim]",
191
- ),
192
- no_passwd: bool = typer.Option(
193
- False,
194
- "-w",
195
- "--no-passwd",
196
- help="Skip password prompt when user is specified.",
197
- ),
198
- encodings: bool = typer.Option(
188
+ progress: bool = typer.Option(
199
189
  False,
200
- "-y",
201
- "--encodings",
202
- help="List available encoding names and exit.",
203
- ),
204
- import_buffer: int | None = typer.Option(
205
- None,
206
- "-z",
207
- "--import-buffer",
208
- metavar="KB",
209
- help="Import buffer size in KB. [dim]Default: 32[/dim]",
190
+ "--progress",
191
+ help="Show a progress bar for long-running IMPORT operations.",
210
192
  ),
193
+ # -- Execution ---------------------------------------------------------
211
194
  command: str | None = typer.Option(
212
195
  None,
213
196
  "-c",
@@ -221,7 +204,7 @@ def main(
221
204
  dry_run: bool = typer.Option(
222
205
  False,
223
206
  "--dry-run",
224
- help=("Parse the script and print the command list without connecting to a database or executing anything."),
207
+ help="Parse the script and print the command list without connecting to a database or executing anything.",
225
208
  ),
226
209
  lint: bool = typer.Option(
227
210
  False,
@@ -232,51 +215,23 @@ def main(
232
215
  "and missing INCLUDE files (warnings). Exits 0 if no errors, 1 if errors found."
233
216
  ),
234
217
  ),
235
- ping: bool = typer.Option(
218
+ parse_tree: bool = typer.Option(
236
219
  False,
237
- "--ping",
238
- help=(
239
- "Test database connectivity and exit. "
240
- "Prints connection details and the server version on success (exit 0), "
241
- "or the error message on failure (exit 1). "
242
- "No script file is required."
243
- ),
244
- ),
245
- dsn: str | None = typer.Option(
246
- None,
247
- "--dsn",
248
- "--connection-string",
249
- metavar="URL",
250
- help=(
251
- "Database connection URL, e.g. [cyan]postgresql://user:pass@host:5432/db[/cyan]. "
252
- "Supported schemes: postgresql, mysql, mssql, oracle, firebird, sqlite, duckdb. "
253
- "Overrides [cyan]-t[/cyan]/[cyan]-u[/cyan]/[cyan]-p[/cyan] and positional server/db args."
254
- ),
255
- ),
256
- output_dir: str | None = typer.Option(
257
- None,
258
- "--output-dir",
259
- metavar="DIR",
220
+ "--parse-tree",
260
221
  help=(
261
- "Default base directory for EXPORT output files. "
262
- "Relative paths in EXPORT metacommands are joined to this directory. "
263
- "Absolute paths and [cyan]stdout[/cyan] are unaffected."
222
+ "Parse the script into an abstract syntax tree and print the tree structure. "
223
+ "Does not connect to a database or execute anything."
264
224
  ),
265
225
  ),
266
- progress: bool = typer.Option(
267
- False,
268
- "--progress",
269
- help="Show a progress bar for long-running IMPORT operations.",
270
- ),
271
- dump_keywords: bool = typer.Option(
226
+ debug: bool = typer.Option(
272
227
  False,
273
- "--dump-keywords",
274
- help="Dump all metacommand keywords as JSON and exit.",
228
+ "--debug",
229
+ help="Start in step-through debug mode. The debug REPL pauses before each statement.",
275
230
  ),
276
- list_plugins: bool = typer.Option(
231
+ no_system_cmd: bool = typer.Option(
277
232
  False,
278
- "--list-plugins",
279
- help="List all discovered plugins (metacommands, exporters, importers) and exit.",
233
+ "--no-system-cmd",
234
+ help="Disable the SYSTEM_CMD (SHELL) metacommand. Scripts that use SHELL will fail with an error.",
280
235
  ),
281
236
  profile: bool = typer.Option(
282
237
  False,
@@ -288,6 +243,24 @@ def main(
288
243
  "--profile-limit",
289
244
  help="Number of top statements to show in the --profile timing summary (default: 20).",
290
245
  ),
246
+ # -- GUI ---------------------------------------------------------------
247
+ use_gui: str | None = typer.Option(
248
+ None,
249
+ "-v",
250
+ "--visible-prompts",
251
+ metavar="{0,1,2,3}",
252
+ help=(
253
+ "GUI level: [bold]0[/bold]=none (default), [bold]1[/bold]=GUI for password/pause, "
254
+ "[bold]2[/bold]=GUI for password/pause + DB selection, [bold]3[/bold]=full GUI console."
255
+ ),
256
+ ),
257
+ gui_framework: str | None = typer.Option(
258
+ None,
259
+ "--gui-framework",
260
+ metavar="{tkinter,textual}",
261
+ help="GUI framework to use with [cyan]--visible-prompts[/cyan]. [dim]Default: tkinter[/dim]",
262
+ ),
263
+ # -- Configuration -----------------------------------------------------
291
264
  config_file: str | None = typer.Option(
292
265
  None,
293
266
  "--config",
@@ -298,18 +271,62 @@ def main(
298
271
  "The file may chain additional configs via its [cyan][config][/cyan] section."
299
272
  ),
300
273
  ),
301
- parse_tree: bool = typer.Option(
274
+ init_config: bool = typer.Option(
302
275
  False,
303
- "--parse-tree",
276
+ "--init-config",
277
+ help="Print a default [cyan]execsql.conf[/cyan] template to stdout and exit.",
278
+ ),
279
+ sub_vars: list[str] | None = typer.Option(
280
+ None,
281
+ "-a",
282
+ "--assign-arg",
283
+ metavar="VALUE",
284
+ help="Define the replacement string for a substitution variable [cyan]\\$ARG_x[/cyan].",
285
+ ),
286
+ user_logfile: bool = typer.Option(
287
+ False,
288
+ "-l",
289
+ "--user-logfile",
290
+ help="Write a log file to [cyan]~/execsql.log[/cyan].",
291
+ ),
292
+ # -- Information -------------------------------------------------------
293
+ metacommands: bool = typer.Option(
294
+ False,
295
+ "-m",
296
+ "--metacommands",
297
+ help="List metacommands and exit.",
298
+ ),
299
+ encodings: bool = typer.Option(
300
+ False,
301
+ "-y",
302
+ "--encodings",
303
+ help="List available encoding names and exit.",
304
+ ),
305
+ dump_keywords: bool = typer.Option(
306
+ False,
307
+ "--dump-keywords",
308
+ help="Dump all metacommand keywords as JSON and exit.",
309
+ ),
310
+ list_plugins: bool = typer.Option(
311
+ False,
312
+ "--list-plugins",
313
+ help="List all discovered plugins (metacommands, exporters, importers) and exit.",
314
+ ),
315
+ ping: bool = typer.Option(
316
+ False,
317
+ "--ping",
304
318
  help=(
305
- "Parse the script into an abstract syntax tree and print the tree structure. "
306
- "Does not connect to a database or execute anything."
319
+ "Test database connectivity and exit. "
320
+ "Prints connection details and the server version on success (exit 0), "
321
+ "or the error message on failure (exit 1). "
322
+ "No script file is required."
307
323
  ),
308
324
  ),
309
- debug: bool = typer.Option(
325
+ online_help: bool = typer.Option(
310
326
  False,
311
- "--debug",
312
- help="Start in step-through debug mode. The debug REPL pauses before each statement.",
327
+ "-o",
328
+ "--online-help",
329
+ help="Open the online documentation in the default browser.",
313
330
  ),
314
331
  version: bool | None = typer.Option(
315
332
  None,
@@ -340,6 +357,10 @@ def main(
340
357
  _print_encodings()
341
358
  raise typer.Exit()
342
359
 
360
+ if init_config:
361
+ _init_config()
362
+ raise typer.Exit()
363
+
343
364
  if dump_keywords:
344
365
  import json as _json
345
366
 
@@ -580,6 +601,7 @@ def main(
580
601
  ping=ping,
581
602
  lint=lint,
582
603
  debug=debug,
604
+ no_system_cmd=no_system_cmd,
583
605
  config_file=config_file,
584
606
  )
585
607
 
execsql/cli/help.py CHANGED
@@ -11,7 +11,7 @@ from encodings.aliases import aliases as codec_dict
11
11
  from rich.console import Console
12
12
  from rich.table import Table
13
13
 
14
- __all__ = ["_console", "_err_console", "_print_encodings", "_print_metacommands"]
14
+ __all__ = ["_console", "_err_console", "_init_config", "_print_encodings", "_print_metacommands"]
15
15
 
16
16
  _console = Console()
17
17
  _err_console = Console(stderr=True)
@@ -77,6 +77,15 @@ _SKIP_FROM_DISPATCH = {
77
77
  }
78
78
 
79
79
 
80
+ def _init_config() -> None:
81
+ """Print the default execsql.conf template to stdout."""
82
+ import importlib.resources
83
+ import sys
84
+
85
+ template = importlib.resources.files("execsql.data").joinpath("execsql.conf.template").read_text(encoding="utf-8")
86
+ sys.stdout.write(template)
87
+
88
+
80
89
  def _print_metacommands() -> None:
81
90
  """Print the metacommands table using Rich.
82
91
 
execsql/cli/run.py CHANGED
@@ -532,6 +532,7 @@ def _run(
532
532
  ping: bool = False,
533
533
  lint: bool = False,
534
534
  debug: bool = False,
535
+ no_system_cmd: bool = False,
535
536
  config_file: str | None = None,
536
537
  ) -> None:
537
538
  """Initialise state, connect to the database, load the script, and run it.
@@ -727,6 +728,9 @@ def _run(
727
728
  if debug:
728
729
  _state.step_mode = True
729
730
 
731
+ if no_system_cmd:
732
+ conf.allow_system_cmd = False
733
+
730
734
  if _ast_tree is not None:
731
735
  _execute_script_ast(_ast_tree, conf, profile=profile, profile_limit=profile_limit)
732
736
 
execsql/config.py CHANGED
@@ -288,6 +288,7 @@ class ConfigData:
288
288
  self.include_opt: list = []
289
289
  self.export_output_dir: str | None = None
290
290
  self.dao_flush_delay_secs = 5.0
291
+ self.allow_system_cmd = True
291
292
  self.zip_buffer_mb = 10
292
293
  if os.name == "posix":
293
294
  sys_config_file = str(Path("/etc") / self.config_file_name)
@@ -474,6 +475,7 @@ class ConfigData:
474
475
  self._get_bool(cp, self._CONFIG_SECTION, "log_datavars", "log_datavars")
475
476
  self._get_bool(cp, self._CONFIG_SECTION, "log_sql", "log_sql")
476
477
  self._get_int(cp, self._CONFIG_SECTION, "max_log_size_mb", "max_log_size_mb")
478
+ self._get_bool(cp, self._CONFIG_SECTION, "allow_system_cmd", "allow_system_cmd")
477
479
  # --- [email] ---
478
480
  self._get_str(cp, self._EMAIL_SECTION, "host", "smtp_host")
479
481
  self._get_int(cp, self._EMAIL_SECTION, "port", "smtp_port")
File without changes