sqlakit 0.22.2__tar.gz → 0.22.4__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.
Files changed (35) hide show
  1. {sqlakit-0.22.2 → sqlakit-0.22.4}/PKG-INFO +1 -1
  2. {sqlakit-0.22.2 → sqlakit-0.22.4}/pyproject.toml +2 -1
  3. {sqlakit-0.22.2 → sqlakit-0.22.4}/pyproject.toml.orig +2 -1
  4. {sqlakit-0.22.2 → sqlakit-0.22.4}/sqlakit/_cli.py +208 -15
  5. sqlakit-0.22.4/sqlakit/_linters.py +375 -0
  6. sqlakit-0.22.4/sqlakit/_render.py +382 -0
  7. sqlakit-0.22.4/sqlakit/_sql_formatter.py +91 -0
  8. sqlakit-0.22.2/sqlakit/_sqruff.py +0 -168
  9. {sqlakit-0.22.2 → sqlakit-0.22.4}/LICENSE +0 -0
  10. {sqlakit-0.22.2 → sqlakit-0.22.4}/README.md +0 -0
  11. {sqlakit-0.22.2 → sqlakit-0.22.4}/sqlakit/__init__.py +0 -0
  12. {sqlakit-0.22.2 → sqlakit-0.22.4}/sqlakit/_base.py +0 -0
  13. {sqlakit-0.22.2 → sqlakit-0.22.4}/sqlakit/_db.py +0 -0
  14. {sqlakit-0.22.2 → sqlakit-0.22.4}/sqlakit/_discovery.py +0 -0
  15. {sqlakit-0.22.2 → sqlakit-0.22.4}/sqlakit/_model.py +0 -0
  16. {sqlakit-0.22.2 → sqlakit-0.22.4}/sqlakit/_project.py +0 -0
  17. {sqlakit-0.22.2 → sqlakit-0.22.4}/sqlakit/_pycharm.py +0 -0
  18. {sqlakit-0.22.2 → sqlakit-0.22.4}/sqlakit/_query.py +0 -0
  19. {sqlakit-0.22.2 → sqlakit-0.22.4}/sqlakit/_recording.py +0 -0
  20. {sqlakit-0.22.2 → sqlakit-0.22.4}/sqlakit/_registry.py +0 -0
  21. {sqlakit-0.22.2 → sqlakit-0.22.4}/sqlakit/_routing.py +0 -0
  22. {sqlakit-0.22.2 → sqlakit-0.22.4}/sqlakit/_sql.py +0 -0
  23. {sqlakit-0.22.2 → sqlakit-0.22.4}/sqlakit/_static.py +0 -0
  24. {sqlakit-0.22.2 → sqlakit-0.22.4}/sqlakit/asyncio/__init__.py +0 -0
  25. {sqlakit-0.22.2 → sqlakit-0.22.4}/sqlakit/asyncio/_db.py +0 -0
  26. {sqlakit-0.22.2 → sqlakit-0.22.4}/sqlakit/asyncio/_registry.py +0 -0
  27. {sqlakit-0.22.2 → sqlakit-0.22.4}/sqlakit/asyncio/orm.py +0 -0
  28. {sqlakit-0.22.2 → sqlakit-0.22.4}/sqlakit/asyncio/sql.py +0 -0
  29. {sqlakit-0.22.2 → sqlakit-0.22.4}/sqlakit/exceptions.py +0 -0
  30. {sqlakit-0.22.2 → sqlakit-0.22.4}/sqlakit/orm.py +0 -0
  31. {sqlakit-0.22.2 → sqlakit-0.22.4}/sqlakit/py.typed +0 -0
  32. {sqlakit-0.22.2 → sqlakit-0.22.4}/sqlakit/pytest_plugin.py +0 -0
  33. {sqlakit-0.22.2 → sqlakit-0.22.4}/sqlakit/sql.py +0 -0
  34. {sqlakit-0.22.2 → sqlakit-0.22.4}/sqlakit/testing.py +0 -0
  35. {sqlakit-0.22.2 → sqlakit-0.22.4}/sqlakit/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlakit
3
- Version: 0.22.2
3
+ Version: 0.22.4
4
4
  Summary: A toolkit for SQLAlchemy applications.
5
5
  Keywords: sqlalchemy,database,orm,sql,asyncio
6
6
  Author: Anton Ruhlov
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlakit"
3
- version = "0.22.2"
3
+ version = "0.22.4"
4
4
  description = "A toolkit for SQLAlchemy applications."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -201,6 +201,7 @@ dev = [
201
201
  "dirty-equals>=0.11",
202
202
  "codespell>=2.4.3",
203
203
  "sqruff>=0.40.0",
204
+ "sqlfluff>=4.3.0",
204
205
  ]
205
206
  docs = [
206
207
  "mkdocs>=1.6.1,<2",
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlakit"
3
- version = "0.22.2"
3
+ version = "0.22.4"
4
4
  description = "A toolkit for SQLAlchemy applications."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -77,6 +77,7 @@ dev = [
77
77
  "dirty-equals>=0.11",
78
78
  "codespell>=2.4.3",
79
79
  "sqruff>=0.40.0",
80
+ "sqlfluff>=4.3.0",
80
81
  ]
81
82
  docs = [
82
83
  "mkdocs>=1.6.1,<2",
@@ -5,17 +5,20 @@ from __future__ import annotations
5
5
  import argparse
6
6
  import json
7
7
  import os
8
+ import shutil
8
9
  import sys
9
10
  from pathlib import Path
10
11
 
12
+ from . import _sql_formatter
13
+ from ._linters import LINTERS, dialect_of, settings, stale
11
14
  from ._project import Problem, Project, load_project
12
15
  from ._pycharm import DIALECTS, ddl, dialects
16
+ from ._render import import_macros, rendered, unparsed
13
17
  from ._sql import NAMESPACE, registered, signature_of
14
- from ._sqruff import settings, stale
15
18
  from .exceptions import MacroDefinitionError, ProjectConfigError, UnknownImportPathError
16
19
 
17
20
 
18
- def main(argv: list[str] | None = None) -> int:
21
+ def main(argv: list[str] | None = None) -> int: # noqa: PLR0911 - one return a command
19
22
  """Run a command, and return what the shell should make of it."""
20
23
  parser = argparse.ArgumentParser(prog="sqlakit", description="SQLAKit.")
21
24
  commands = parser.add_subparsers(dest="command", required=True)
@@ -51,11 +54,36 @@ def main(argv: list[str] | None = None) -> int:
51
54
  help="a directory in the project, the current one by default",
52
55
  )
53
56
  check.add_argument("--format", choices=("text", "json"), default="text")
57
+ check.add_argument(
58
+ "--lint",
59
+ choices=tuple(LINTERS),
60
+ help="also render every template, running the project's macros, and "
61
+ "fail where the linter cannot parse the SQL",
62
+ )
63
+ check.add_argument(
64
+ "--dialect", help="the dialect to render for, in place of the project's"
65
+ )
66
+
67
+ render = commands.add_parser(
68
+ "render",
69
+ help="write every template as the SQL it renders, for a linter to read",
70
+ )
71
+ render.add_argument(
72
+ "--out", required=True, help="the directory to write the rendered SQL into"
73
+ )
74
+ render.add_argument(
75
+ "--project",
76
+ default=".",
77
+ help="a directory in the project, the current one by default",
78
+ )
79
+ render.add_argument(
80
+ "--dialect", help="the dialect to render for, in place of the project's"
81
+ )
54
82
 
55
83
  export = commands.add_parser(
56
84
  "export", help="write what another tool needs to read the templates"
57
85
  )
58
- export.add_argument("tool", choices=("sqruff", "pycharm"))
86
+ export.add_argument("tool", choices=(*LINTERS, "sql-formatter", "pycharm"))
59
87
  export.add_argument(
60
88
  "--project",
61
89
  default=".",
@@ -72,11 +100,34 @@ def main(argv: list[str] | None = None) -> int:
72
100
 
73
101
  arguments = parser.parse_args(argv)
74
102
  if arguments.command == "check":
75
- return _check(Path(arguments.project), json_output=arguments.format == "json")
103
+ return _check(
104
+ Path(arguments.project),
105
+ json_output=arguments.format == "json",
106
+ lint=arguments.lint,
107
+ dialect=arguments.dialect,
108
+ )
109
+ if arguments.command == "render":
110
+ return _render(
111
+ Path(arguments.project), Path(arguments.out), dialect=arguments.dialect
112
+ )
76
113
  if arguments.command == "export":
77
- export = _export_pycharm if arguments.tool == "pycharm" else _export_sqruff
78
- return export(
79
- Path(arguments.project), dialect=arguments.dialect, check=arguments.check
114
+ if arguments.tool == "sql-formatter":
115
+ return _export_sql_formatter(
116
+ Path(arguments.project),
117
+ dialect=arguments.dialect,
118
+ check=arguments.check,
119
+ )
120
+ if arguments.tool == "pycharm":
121
+ return _export_pycharm(
122
+ Path(arguments.project),
123
+ dialect=arguments.dialect,
124
+ check=arguments.check,
125
+ )
126
+ return _export_linter(
127
+ arguments.tool,
128
+ Path(arguments.project),
129
+ dialect=arguments.dialect,
130
+ check=arguments.check,
80
131
  )
81
132
  if arguments.command == "macros":
82
133
  return _macros(
@@ -133,11 +184,14 @@ def _load(directory: Path, *, json_output: bool = False) -> Project | None:
133
184
  return None
134
185
 
135
186
 
136
- def _check(directory: Path, *, json_output: bool) -> int:
187
+ def _check(
188
+ directory: Path, *, json_output: bool, lint: str | None, dialect: str | None
189
+ ) -> int:
137
190
  """Print what is wrong with the project's templates, and fail if anything is.
138
191
 
139
- The exit is 1 for problems in the templates, and 2 for a project that cannot
140
- be read, or holds no template to check.
192
+ With a linter, every template is rendered too, and what it cannot parse is
193
+ a problem. The exit is 1 for problems in the templates, and 2 for a project
194
+ that cannot be read, holds no template to check, or cannot be linted.
141
195
  """
142
196
  project = _load(directory, json_output=json_output)
143
197
  if project is None:
@@ -148,6 +202,12 @@ def _check(directory: Path, *, json_output: bool) -> int:
148
202
  _say(json.dumps({"error": message}) if json_output else message)
149
203
  return 2
150
204
  problems = list(project.problems())
205
+ if lint is not None:
206
+ try:
207
+ problems.extend(_lint(project, lint, dialect))
208
+ except _CannotRenderError as error:
209
+ _say(json.dumps({"error": str(error)}) if json_output else str(error))
210
+ return 2
151
211
  if json_output:
152
212
  _say(json.dumps([_as_json(problem) for problem in problems], indent=2))
153
213
  else:
@@ -165,21 +225,132 @@ def _check(directory: Path, *, json_output: bool) -> int:
165
225
  return 1 if problems else 0
166
226
 
167
227
 
168
- def _export_sqruff(directory: Path, *, dialect: str | None, check: bool) -> int:
169
- """Write the `sqruff` settings that read the templates into `pyproject.toml`."""
228
+ _WRITTEN = {"given": "every parameter given", "not_given": "no parameter given"}
229
+
230
+
231
+ class _CannotRenderError(Exception):
232
+ """Why the templates cannot be rendered, or the rendering linted."""
233
+
234
+
235
+ def _lint(project: Project, name: str, given: str | None) -> list[Problem]:
236
+ """Return where the linter cannot parse what the templates render.
237
+
238
+ Raises:
239
+ _CannotRenderError: if the linter cannot read the templates as the
240
+ project is set up.
241
+
242
+ """
243
+ linter = LINTERS[name]
244
+ if shutil.which(name) is None:
245
+ problem = f"`{name}` is not installed, so the rendered SQL cannot be linted"
246
+ raise _CannotRenderError(problem)
247
+ pyproject = project.root / "pyproject.toml"
248
+ text = pyproject.read_text(encoding="utf-8") if pyproject.exists() else ""
249
+ if stale(linter, project, text):
250
+ problem = (
251
+ f"the `{name}` settings do not read the templates: run "
252
+ f"`sqlakit export {name}` first"
253
+ )
254
+ raise _CannotRenderError(problem)
255
+ configured = dialect_of(linter, text)
256
+ chosen = _dialect_to_render(project, given, configured)
257
+ if configured is not None and configured != chosen:
258
+ problem = (
259
+ f"the templates render on {chosen}, and `{name}` reads {configured}: "
260
+ f"pass --dialect {configured}, or set the dialect in `[tool.{name}.core]`"
261
+ )
262
+ raise _CannotRenderError(problem)
263
+ try:
264
+ import_macros(project)
265
+ except Exception as error:
266
+ problem = (
267
+ f"the project's macros cannot be imported: {type(error).__name__}: {error}"
268
+ )
269
+ raise _CannotRenderError(problem) from error
270
+ return unparsed(project, linter, chosen)
271
+
272
+
273
+ def _dialect_to_render(
274
+ project: Project, given: str | None, configured: str | None = None
275
+ ) -> str:
276
+ """Return the dialect to render the templates on.
277
+
278
+ `--dialect` wins, then the one the project's database URL names, then the
279
+ one a linter's settings read in. A macro writes by `ctx.dialect`, so a
280
+ guess would render SQL the database never gets.
281
+
282
+ Raises:
283
+ _CannotRenderError: if nothing names one.
284
+
285
+ """
286
+ chosen = given or project.dialect or configured
287
+ if chosen is None:
288
+ problem = (
289
+ "the code names no database URL to take the dialect from: pass --dialect"
290
+ )
291
+ raise _CannotRenderError(problem)
292
+ return chosen.lower()
293
+
294
+
295
+ def _render(directory: Path, out: Path, *, dialect: str | None) -> int:
296
+ """Write each template twice, rendered with its parameters given and not.
297
+
298
+ The project's Python macros are imported and run, so what they write is
299
+ in the SQL. Exit 1 when a template cannot be rendered.
300
+ """
301
+ project = _load(directory)
302
+ if project is None:
303
+ return 2
304
+ try:
305
+ import_macros(project)
306
+ except Exception as error: # noqa: BLE001 - the project's code raises anything
307
+ _say(
308
+ f"the project's macros cannot be imported: {type(error).__name__}: {error}"
309
+ )
310
+ return 2
311
+ try:
312
+ chosen = _dialect_to_render(project, dialect)
313
+ except _CannotRenderError as error:
314
+ _say(str(error))
315
+ return 2
316
+ problems = 0
317
+ count = 0
318
+ for one in rendered(project, chosen):
319
+ if one.sql is None:
320
+ problems += 1
321
+ _say(f"{one.name} ({one.variant}): {one.problem}")
322
+ continue
323
+ target = out / f"{one.name.removesuffix('.sql')}.{one.variant}.sql"
324
+ target.parent.mkdir(parents=True, exist_ok=True)
325
+ header = [f"-- {one.name} on {chosen}, {_WRITTEN[one.variant]}"]
326
+ if one.kept:
327
+ header.append(
328
+ f"-- {', '.join(one.kept)} could not be made, and stays a call"
329
+ )
330
+ target.write_text("\n".join(header) + "\n" + one.sql, encoding="utf-8")
331
+ count += 1
332
+ _say(f"wrote {count} files to {_relative(out)}")
333
+ return 1 if problems else 0
334
+
335
+
336
+ def _export_linter(
337
+ name: str, directory: Path, *, dialect: str | None, check: bool
338
+ ) -> int:
339
+ """Write the settings of `sqruff` or `sqlfluff` that read the templates."""
340
+ linter = LINTERS[name]
170
341
  project = _load(directory)
171
342
  if project is None:
172
343
  return 2
173
344
  pyproject = project.root / "pyproject.toml"
174
345
  text = pyproject.read_text(encoding="utf-8") if pyproject.exists() else ""
175
346
  if check:
176
- tables = stale(project, text)
347
+ tables = stale(linter, project, text)
177
348
  if tables:
178
- _say(f"{', '.join(tables)} out of date: run `sqlakit export sqruff`")
349
+ _say(f"{', '.join(tables)} out of date: run `sqlakit export {name}`")
179
350
  return 1
180
351
  return 0
181
352
  try:
182
- written = settings(project, text, dialect)
353
+ written = settings(linter, project, text, dialect)
183
354
  except ProjectConfigError as error:
184
355
  _say(str(error))
185
356
  return 2
@@ -188,6 +359,28 @@ def _export_sqruff(directory: Path, *, dialect: str | None, check: bool) -> int:
188
359
  return 0
189
360
 
190
361
 
362
+ def _export_sql_formatter(directory: Path, *, dialect: str | None, check: bool) -> int:
363
+ """Write the `.sql-formatter.json` that reads `:name` as a parameter."""
364
+ project = _load(directory)
365
+ if project is None:
366
+ return 2
367
+ path = project.root / _sql_formatter.FILE
368
+ written = path.read_text(encoding="utf-8") if path.exists() else None
369
+ if check:
370
+ if _sql_formatter.stale(written):
371
+ _say(f"{_relative(path)} out of date: run `sqlakit export sql-formatter`")
372
+ return 1
373
+ return 0
374
+ try:
375
+ text = _sql_formatter.config(project, written, dialect)
376
+ except ProjectConfigError as error:
377
+ _say(str(error))
378
+ return 2
379
+ path.write_text(text, encoding="utf-8")
380
+ _say(f"wrote {_relative(path)}")
381
+ return 0
382
+
383
+
191
384
  def _export_pycharm(directory: Path, *, dialect: str | None, check: bool) -> int:
192
385
  """Write what PyCharm needs into `.idea/`: the macros as DDL, and the dialect."""
193
386
  project = _load(directory)