sqlinclude 0.1.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 lukasburski
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.4
2
+ Name: sqlinclude
3
+ Version: 0.1.0
4
+ Summary: Tiny SQL source preprocessor: expand @include directives and @define variables.
5
+ Author-email: lukasburski <lukasbursky@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/lukasbursky/sqlinclude
8
+ Project-URL: Source, https://github.com/lukasbursky/sqlinclude
9
+ Project-URL: Issues, https://github.com/lukasbursky/sqlinclude/issues
10
+ Keywords: sql,preprocessor,include,bigquery,templating
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Database
20
+ Classifier: Topic :: Software Development :: Pre-processors
21
+ Requires-Python: >=3.12
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=8; extra == "dev"
26
+ Requires-Dist: build>=1.2; extra == "dev"
27
+ Dynamic: license-file
28
+
29
+ # sqlinclude
30
+
31
+ Tiny SQL source preprocessor. Expands `@include` directives and `@define`
32
+ variables recursively. No SQL parsing, no opinion about any database. Intended
33
+ to be piped into a query tool:
34
+
35
+ ```console
36
+ sqlinclude analysis.sql | bq query
37
+ bq query "$(sqlinclude analysis.sql)"
38
+ ```
39
+
40
+ Written for Unix and Windows alike: it is a single dependency-free Python
41
+ package and installs a `sqlinclude` console command (a `sqlinclude.exe` on
42
+ Windows).
43
+
44
+ ## Install
45
+
46
+ ```console
47
+ pipx install sqlinclude
48
+ ```
49
+
50
+ or, into the current environment:
51
+
52
+ ```console
53
+ pip install sqlinclude
54
+ ```
55
+
56
+ Requires Python 3.12+.
57
+
58
+ ## Directives
59
+
60
+ Line-oriented, leading whitespace allowed:
61
+
62
+ ```
63
+ @include file.sql
64
+ @include "file.sql"
65
+ @define name = value (the `= ` is optional; value is the rest of the
66
+ line, kept verbatim including quotes)
67
+ ```
68
+
69
+ Include paths are resolved relative to the including file. Includes may nest;
70
+ cycles are detected and reported as errors.
71
+
72
+ Variables form a single environment filled in expansion order: the first
73
+ `@define name ...` seen anywhere in the tree wins, and later definitions of the
74
+ same name are ignored (so a parent file's definition always overrides an
75
+ included fragment's default). Definitions from an included file leak back to
76
+ the including file. A `@name` with no definition passes through unchanged --
77
+ BigQuery's native `@param` syntax is never touched.
78
+
79
+ Each include block is bracketed with `-- #line N "file"` markers (with forward
80
+ slashes on every platform) so error messages from downstream tools point at the
81
+ originating source file and line. Use `-n`/`--no-markers` to suppress them.
82
+
83
+ ## Example
84
+
85
+ `analysis.sql`:
86
+
87
+ ```sql
88
+ @define start = '2024-01-01'
89
+ @define end = '2024-12-31'
90
+
91
+ WITH users AS (
92
+ @include "users.sql"
93
+ ),
94
+
95
+ orders AS (
96
+ @include orders.sql
97
+ )
98
+
99
+ SELECT ... WHERE created BETWEEN @start AND @end
100
+ ```
101
+
102
+ Run:
103
+
104
+ ```console
105
+ sqlinclude analysis.sql | bq query
106
+ sqlinclude --vars start='2024-06-01' analysis.sql | bq query
107
+ sqlinclude --tree analysis.sql # show the include tree, not SQL
108
+ sqlinclude --tree --ascii analysis.sql # ASCII box characters instead of Unicode
109
+ ```
110
+
111
+ ## Options
112
+
113
+ | Option | Description |
114
+ | --- | --- |
115
+ | `file` | SQL file to preprocess (default: read stdin) |
116
+ | `-n`, `--no-markers` | do not emit `-- #line` markers around includes |
117
+ | `--vars name=value` | set a variable (repeatable); wins over `@define` |
118
+ | `-e`, `--edit` | open the editor even when no variable is undefined |
119
+ | `--no-edit` | never open the editor; leave undefined `@name`s as-is |
120
+ | `-t`, `--tree` | print the `@include` dependency tree instead of SQL |
121
+ | `--ascii` | use ASCII box characters in the include tree |
122
+ | `--version` | show the version |
123
+
124
+ ## Editing undefined variables
125
+
126
+ When a variable is undefined, `sqlinclude` opens the editor named by
127
+ `$VISUAL`/`$EDITOR` on the controlling terminal, even when its output is piped.
128
+ Edit values, or delete a line to leave that variable undefined. Blank lines and
129
+ `#` comments are ignored. On Windows the same variables are honored, falling
130
+ back to `notepad`. With `-e`/`--edit` the buffer also shows the `@include`
131
+ tree. Undefined variables are always reported on stderr; pass `--no-edit` to
132
+ silence everything and pass undefined names through untouched.
133
+
134
+ ## Development
135
+
136
+ ```console
137
+ python -m venv .venv
138
+ . .venv/bin/activate
139
+ pip install -e ".[dev]"
140
+ pytest
141
+ ```
142
+
143
+ ## License
144
+
145
+ MIT -- see [LICENSE](LICENSE).
@@ -0,0 +1,117 @@
1
+ # sqlinclude
2
+
3
+ Tiny SQL source preprocessor. Expands `@include` directives and `@define`
4
+ variables recursively. No SQL parsing, no opinion about any database. Intended
5
+ to be piped into a query tool:
6
+
7
+ ```console
8
+ sqlinclude analysis.sql | bq query
9
+ bq query "$(sqlinclude analysis.sql)"
10
+ ```
11
+
12
+ Written for Unix and Windows alike: it is a single dependency-free Python
13
+ package and installs a `sqlinclude` console command (a `sqlinclude.exe` on
14
+ Windows).
15
+
16
+ ## Install
17
+
18
+ ```console
19
+ pipx install sqlinclude
20
+ ```
21
+
22
+ or, into the current environment:
23
+
24
+ ```console
25
+ pip install sqlinclude
26
+ ```
27
+
28
+ Requires Python 3.12+.
29
+
30
+ ## Directives
31
+
32
+ Line-oriented, leading whitespace allowed:
33
+
34
+ ```
35
+ @include file.sql
36
+ @include "file.sql"
37
+ @define name = value (the `= ` is optional; value is the rest of the
38
+ line, kept verbatim including quotes)
39
+ ```
40
+
41
+ Include paths are resolved relative to the including file. Includes may nest;
42
+ cycles are detected and reported as errors.
43
+
44
+ Variables form a single environment filled in expansion order: the first
45
+ `@define name ...` seen anywhere in the tree wins, and later definitions of the
46
+ same name are ignored (so a parent file's definition always overrides an
47
+ included fragment's default). Definitions from an included file leak back to
48
+ the including file. A `@name` with no definition passes through unchanged --
49
+ BigQuery's native `@param` syntax is never touched.
50
+
51
+ Each include block is bracketed with `-- #line N "file"` markers (with forward
52
+ slashes on every platform) so error messages from downstream tools point at the
53
+ originating source file and line. Use `-n`/`--no-markers` to suppress them.
54
+
55
+ ## Example
56
+
57
+ `analysis.sql`:
58
+
59
+ ```sql
60
+ @define start = '2024-01-01'
61
+ @define end = '2024-12-31'
62
+
63
+ WITH users AS (
64
+ @include "users.sql"
65
+ ),
66
+
67
+ orders AS (
68
+ @include orders.sql
69
+ )
70
+
71
+ SELECT ... WHERE created BETWEEN @start AND @end
72
+ ```
73
+
74
+ Run:
75
+
76
+ ```console
77
+ sqlinclude analysis.sql | bq query
78
+ sqlinclude --vars start='2024-06-01' analysis.sql | bq query
79
+ sqlinclude --tree analysis.sql # show the include tree, not SQL
80
+ sqlinclude --tree --ascii analysis.sql # ASCII box characters instead of Unicode
81
+ ```
82
+
83
+ ## Options
84
+
85
+ | Option | Description |
86
+ | --- | --- |
87
+ | `file` | SQL file to preprocess (default: read stdin) |
88
+ | `-n`, `--no-markers` | do not emit `-- #line` markers around includes |
89
+ | `--vars name=value` | set a variable (repeatable); wins over `@define` |
90
+ | `-e`, `--edit` | open the editor even when no variable is undefined |
91
+ | `--no-edit` | never open the editor; leave undefined `@name`s as-is |
92
+ | `-t`, `--tree` | print the `@include` dependency tree instead of SQL |
93
+ | `--ascii` | use ASCII box characters in the include tree |
94
+ | `--version` | show the version |
95
+
96
+ ## Editing undefined variables
97
+
98
+ When a variable is undefined, `sqlinclude` opens the editor named by
99
+ `$VISUAL`/`$EDITOR` on the controlling terminal, even when its output is piped.
100
+ Edit values, or delete a line to leave that variable undefined. Blank lines and
101
+ `#` comments are ignored. On Windows the same variables are honored, falling
102
+ back to `notepad`. With `-e`/`--edit` the buffer also shows the `@include`
103
+ tree. Undefined variables are always reported on stderr; pass `--no-edit` to
104
+ silence everything and pass undefined names through untouched.
105
+
106
+ ## Development
107
+
108
+ ```console
109
+ python -m venv .venv
110
+ . .venv/bin/activate
111
+ pip install -e ".[dev]"
112
+ pytest
113
+ ```
114
+
115
+ ## License
116
+
117
+ MIT -- see [LICENSE](LICENSE).
@@ -0,0 +1,46 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "sqlinclude"
7
+ dynamic = ["version"]
8
+ description = "Tiny SQL source preprocessor: expand @include directives and @define variables."
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "lukasburski", email = "lukasbursky@gmail.com" }]
14
+ keywords = ["sql", "preprocessor", "include", "bigquery", "templating"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Environment :: Console",
18
+ "Intended Audience :: Developers",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Programming Language :: Python :: 3.14",
24
+ "Topic :: Database",
25
+ "Topic :: Software Development :: Pre-processors",
26
+ ]
27
+
28
+ [project.scripts]
29
+ sqlinclude = "sqlinclude.cli:main"
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/lukasbursky/sqlinclude"
33
+ Source = "https://github.com/lukasbursky/sqlinclude"
34
+ Issues = "https://github.com/lukasbursky/sqlinclude/issues"
35
+
36
+ [project.optional-dependencies]
37
+ dev = ["pytest>=8", "build>=1.2"]
38
+
39
+ [tool.setuptools.dynamic]
40
+ version = { attr = "sqlinclude.__version__" }
41
+
42
+ [tool.setuptools.packages.find]
43
+ where = ["src"]
44
+
45
+ [tool.pytest.ini_options]
46
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,7 @@
1
+ """sqlinclude -- tiny SQL source preprocessor.
2
+
3
+ Expands @include directives and @define variables recursively. No SQL
4
+ parsing, no opinion about any database.
5
+ """
6
+
7
+ __version__ = "0.1.0"
@@ -0,0 +1,8 @@
1
+ """Entry point for ``python -m sqlinclude``."""
2
+
3
+ import sys
4
+
5
+ from .cli import main
6
+
7
+ if __name__ == "__main__":
8
+ sys.exit(main())
@@ -0,0 +1,226 @@
1
+ """Command-line interface for sqlinclude."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ import re
8
+ import sys
9
+
10
+ from . import __version__
11
+ from .editor import (
12
+ edit_buffer,
13
+ have_ctrl_tty,
14
+ parse_buffer,
15
+ render_buffer,
16
+ resolve_editor,
17
+ undefined_count,
18
+ )
19
+ from .preprocess import (
20
+ ORIGIN_UNDEFINED,
21
+ STDIN_NAME,
22
+ collect_tree,
23
+ collect_vars,
24
+ render_tree,
25
+ walk,
26
+ )
27
+
28
+
29
+ def _stdout_can_encode(text: str) -> bool:
30
+ enc = getattr(sys.stdout, "encoding", None) or "ascii"
31
+ try:
32
+ text.encode(enc)
33
+ except (UnicodeEncodeError, LookupError):
34
+ return False
35
+ return True
36
+
37
+
38
+ def build_parser() -> argparse.ArgumentParser:
39
+ parser = argparse.ArgumentParser(
40
+ prog="sqlinclude",
41
+ description="Expand @include directives and @define variables in SQL source "
42
+ "(no SQL parsing).",
43
+ formatter_class=argparse.RawDescriptionHelpFormatter,
44
+ epilog=(
45
+ "example (analysis.sql):\n"
46
+ "\n"
47
+ " @define start = '2024-01-01'\n"
48
+ " @define end = '2024-12-31'\n"
49
+ "\n"
50
+ " WITH users AS (\n"
51
+ ' @include "users.sql"\n'
52
+ " ),\n"
53
+ "\n"
54
+ " orders AS (\n"
55
+ " @include orders.sql\n"
56
+ " )\n"
57
+ "\n"
58
+ " SELECT ... WHERE created BETWEEN @start AND @end\n"
59
+ "\n"
60
+ "run:\n"
61
+ "\n"
62
+ " sqlinclude analysis.sql | bq query\n"
63
+ " sqlinclude --vars start='2024-06-01' analysis.sql | bq query\n"
64
+ " sqlinclude --tree analysis.sql # show the include tree, not SQL\n"
65
+ "\n"
66
+ "variables:\n"
67
+ "\n"
68
+ " @define name = value set a variable in a file (first definition wins)\n"
69
+ " --vars name=value set a variable from the command line (repeatable)\n"
70
+ " @name substituted where defined; undefined names pass\n"
71
+ " through unchanged (BigQuery @params stay intact)\n"
72
+ " -e, --edit open the editor even when nothing is undefined\n"
73
+ " --no-edit never open the editor; no stderr notes\n"
74
+ "\n"
75
+ "editing:\n"
76
+ "\n"
77
+ " When a variable is undefined, sqlinclude opens $VISUAL/$EDITOR (or vim,\n"
78
+ " notepad on Windows) on the controlling terminal, even if its output is\n"
79
+ " piped. Edit values; delete a line to leave that variable undefined; blank\n"
80
+ " and # lines are ignored. With -e/--edit the buffer also shows the @include\n"
81
+ " tree. Undefined variables are always reported on stderr -- use\n"
82
+ " --no-edit to silence everything.\n"
83
+ ),
84
+ )
85
+ parser.add_argument(
86
+ "file",
87
+ nargs="?",
88
+ default="-",
89
+ help="SQL file to preprocess (default: read stdin)",
90
+ )
91
+ parser.add_argument(
92
+ "-n",
93
+ "--no-markers",
94
+ action="store_true",
95
+ help="do not emit -- #line markers around includes",
96
+ )
97
+ parser.add_argument(
98
+ "--vars",
99
+ action="append",
100
+ default=[],
101
+ metavar="name=value",
102
+ help="set a variable (repeatable); wins over @define",
103
+ )
104
+ parser.add_argument(
105
+ "-e",
106
+ "--edit",
107
+ action="store_true",
108
+ help="open the editor even when no variable is undefined",
109
+ )
110
+ parser.add_argument(
111
+ "--no-edit",
112
+ action="store_true",
113
+ help="never open the editor; leave undefined @names as-is (no stderr notes)",
114
+ )
115
+ parser.add_argument(
116
+ "-t",
117
+ "--tree",
118
+ action="store_true",
119
+ help="print the @include dependency tree as ASCII on stdout instead of expanding SQL",
120
+ )
121
+ parser.add_argument(
122
+ "--ascii",
123
+ action="store_true",
124
+ help="use ASCII (not Unicode) box characters in the include tree",
125
+ )
126
+ parser.add_argument(
127
+ "--version",
128
+ action="version",
129
+ version=f"%(prog)s {__version__}",
130
+ )
131
+ return parser
132
+
133
+
134
+ def main(argv: list[str] | None = None) -> int:
135
+ if argv is None:
136
+ argv = sys.argv
137
+ args = build_parser().parse_args(argv[1:])
138
+
139
+ cli_vars: dict[str, str] = {}
140
+ for pair in args.vars:
141
+ if "=" not in pair:
142
+ sys.stderr.write(f"sqlinclude: --vars expects name=value, got {pair!r}\n")
143
+ return 2
144
+ name, value = pair.split("=", 1)
145
+ if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", name):
146
+ sys.stderr.write(f"sqlinclude: invalid variable name in --vars: {name!r}\n")
147
+ return 2
148
+ cli_vars[name] = value
149
+
150
+ if args.file == "-":
151
+ source = sys.stdin.read()
152
+ top = None
153
+ else:
154
+ try:
155
+ with open(args.file, encoding="utf-8") as fh:
156
+ source = fh.read()
157
+ except OSError as exc:
158
+ sys.stderr.write(f"sqlinclude: {args.file}: {exc}\n")
159
+ return 1
160
+ top = args.file
161
+ if source and not source.endswith("\n"):
162
+ source += "\n"
163
+
164
+ top_disp = (
165
+ os.path.relpath(os.path.realpath(top), os.getcwd()).replace("\\", "/")
166
+ if top
167
+ else STDIN_NAME
168
+ )
169
+
170
+ if args.tree:
171
+ root = collect_tree(source, top)
172
+ if root is None:
173
+ return 1
174
+ ascii_only = args.ascii or not _stdout_can_encode("└──")
175
+ sys.stdout.write(render_tree(root, ascii_only=ascii_only))
176
+ return 0
177
+
178
+ collected = collect_vars(source, top, cli_vars)
179
+ if collected is None:
180
+ return 1
181
+ order, values, sources = collected
182
+ undefined_present = any(values[n] is None for n in order)
183
+
184
+ if (
185
+ top is not None
186
+ and not args.no_edit
187
+ and (undefined_present or args.edit)
188
+ and have_ctrl_tty()
189
+ ):
190
+ editor = resolve_editor()
191
+ if undefined_present:
192
+ sys.stderr.write(
193
+ f"sqlinclude: opening {editor} for {undefined_count(order, values)} "
194
+ "undefined variable(s)\n"
195
+ )
196
+ tree = None
197
+ if args.edit:
198
+ tree = collect_tree(source, top)
199
+ if tree is None:
200
+ return 1
201
+ edited = edit_buffer(render_buffer(top_disp, order, values, sources, tree), editor)
202
+ if edited is None:
203
+ return 1
204
+ env = parse_buffer(edited)
205
+ blocked = set(order) - set(env)
206
+ else:
207
+ env = dict(cli_vars)
208
+ blocked = set()
209
+ if not args.no_edit and undefined_present:
210
+ names = [n for n in order if values[n] is None]
211
+ sys.stderr.write(
212
+ "sqlinclude: undefined variables "
213
+ f"({len(names)}): {', '.join('@' + n for n in names)} "
214
+ "-- left as @name\n"
215
+ )
216
+
217
+ out: list[str] = []
218
+ stack: list[tuple[str, str]] = []
219
+ if walk(source, top, out, stack, args.no_markers, env, blocked):
220
+ return 1
221
+ sys.stdout.write("".join(out))
222
+ return 0
223
+
224
+
225
+ if __name__ == "__main__":
226
+ sys.exit(main())