sqlakit 0.19.0__tar.gz → 0.21.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.
Files changed (36) hide show
  1. {sqlakit-0.19.0 → sqlakit-0.21.0}/PKG-INFO +21 -15
  2. {sqlakit-0.19.0 → sqlakit-0.21.0}/README.md +18 -10
  3. {sqlakit-0.19.0 → sqlakit-0.21.0}/pyproject.toml +10 -11
  4. {sqlakit-0.19.0 → sqlakit-0.21.0}/pyproject.toml.orig +37 -40
  5. {sqlakit-0.19.0 → sqlakit-0.21.0}/sqlakit/__init__.py +18 -4
  6. {sqlakit-0.19.0 → sqlakit-0.21.0}/sqlakit/_base.py +8 -9
  7. sqlakit-0.21.0/sqlakit/_cli.py +288 -0
  8. {sqlakit-0.19.0 → sqlakit-0.21.0}/sqlakit/_db.py +1 -3
  9. sqlakit-0.21.0/sqlakit/_project.py +422 -0
  10. sqlakit-0.21.0/sqlakit/_pycharm.py +156 -0
  11. {sqlakit-0.19.0 → sqlakit-0.21.0}/sqlakit/_query.py +14 -2
  12. sqlakit-0.21.0/sqlakit/_sql.py +3018 -0
  13. sqlakit-0.21.0/sqlakit/_sqruff.py +106 -0
  14. sqlakit-0.21.0/sqlakit/_static.py +544 -0
  15. {sqlakit-0.19.0 → sqlakit-0.21.0}/sqlakit/asyncio/_db.py +1 -3
  16. {sqlakit-0.19.0 → sqlakit-0.21.0}/sqlakit/asyncio/sql.py +22 -9
  17. {sqlakit-0.19.0 → sqlakit-0.21.0}/sqlakit/exceptions.py +171 -19
  18. {sqlakit-0.19.0 → sqlakit-0.21.0}/sqlakit/pytest_plugin.py +46 -13
  19. {sqlakit-0.19.0 → sqlakit-0.21.0}/sqlakit/sql.py +22 -7
  20. {sqlakit-0.19.0 → sqlakit-0.21.0}/sqlakit/types.py +0 -2
  21. sqlakit-0.19.0/sqlakit/_cli.py +0 -101
  22. sqlakit-0.19.0/sqlakit/_debugserver.py +0 -396
  23. sqlakit-0.19.0/sqlakit/_sql.py +0 -461
  24. sqlakit-0.19.0/sqlakit/debugserver.html +0 -103
  25. {sqlakit-0.19.0 → sqlakit-0.21.0}/LICENSE +0 -0
  26. {sqlakit-0.19.0 → sqlakit-0.21.0}/sqlakit/_discovery.py +0 -0
  27. {sqlakit-0.19.0 → sqlakit-0.21.0}/sqlakit/_model.py +0 -0
  28. {sqlakit-0.19.0 → sqlakit-0.21.0}/sqlakit/_recording.py +0 -0
  29. {sqlakit-0.19.0 → sqlakit-0.21.0}/sqlakit/_registry.py +0 -0
  30. {sqlakit-0.19.0 → sqlakit-0.21.0}/sqlakit/_routing.py +0 -0
  31. {sqlakit-0.19.0 → sqlakit-0.21.0}/sqlakit/asyncio/__init__.py +0 -0
  32. {sqlakit-0.19.0 → sqlakit-0.21.0}/sqlakit/asyncio/_registry.py +0 -0
  33. {sqlakit-0.19.0 → sqlakit-0.21.0}/sqlakit/asyncio/orm.py +0 -0
  34. {sqlakit-0.19.0 → sqlakit-0.21.0}/sqlakit/orm.py +0 -0
  35. {sqlakit-0.19.0 → sqlakit-0.21.0}/sqlakit/py.typed +0 -0
  36. {sqlakit-0.19.0 → sqlakit-0.21.0}/sqlakit/testing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlakit
3
- Version: 0.19.0
3
+ Version: 0.21.0
4
4
  Summary: A toolkit for SQLAlchemy applications.
5
5
  Keywords: sqlalchemy,database,orm,sql,asyncio
6
6
  Author: Anton Ruhlov
@@ -22,13 +22,11 @@ Requires-Dist: sqlalchemy>=2.0.22
22
22
  Requires-Dist: typing-extensions>=4.13
23
23
  Requires-Dist: sqlalchemy[asyncio]>=2.0.22 ; extra == 'asyncio'
24
24
  Requires-Dist: sqlparse>=0.6.0 ; extra == 'debug'
25
- Requires-Dist: jinja2sql>=0.12.0 ; extra == 'sql'
26
25
  Requires-Python: >=3.11
27
- Project-URL: Repository, https://github.com/antonrh/sqlakit
28
- Project-URL: Documentation, https://github.com/antonrh/sqlakit
26
+ Project-URL: Repository, https://github.com/sqlakit/sqlakit
27
+ Project-URL: Documentation, https://sqlakit.readthedocs.io/en/stable/
29
28
  Provides-Extra: asyncio
30
29
  Provides-Extra: debug
31
- Provides-Extra: sql
32
30
  Description-Content-Type: text/markdown
33
31
 
34
32
  # SQLAKit
@@ -92,11 +90,11 @@ with db.autocommit(): # AUTOCOMMIT, no transaction held open
92
90
 
93
91
  ## SQL templates
94
92
 
95
- Templates are `Jinja` files, so they can hold anything from a one-line query to
96
- a report with window functions or a recursive CTE.
97
- [jinja2sql](https://github.com/antonrh/jinja2sql) turns every `{{ name }}` into
98
- a bound parameter (`:name__1`), so values never reach the SQL text and there
99
- is no way to inject anything. Requires the `sqlakit[sql]` extra.
93
+ Templates are SQL files with `:name` parameters, so they hold anything from a
94
+ one-line query to a report with window functions or a recursive CTE, and every
95
+ SQL tool reads them as SQL. Values are bound, so they never reach the SQL
96
+ text. What changes per call, an optional
97
+ condition or a sort order, is a macro such as `tpl.if_set(:team, team = :team)`.
100
98
 
101
99
  ### From a file
102
100
 
@@ -104,7 +102,7 @@ is no way to inject anything. Requires the `sqlakit[sql]` extra.
104
102
  -- reports/by_team.sql
105
103
  SELECT team, count(*) AS members
106
104
  FROM users
107
- WHERE joined_at > {{ since }}
105
+ WHERE joined_at > :since
108
106
  GROUP BY team
109
107
  ```
110
108
 
@@ -131,13 +129,17 @@ type each row is returned as.
131
129
  `SQLAKit` adds the template name to the SQL as a comment, so a slow query log
132
130
  shows the source file of each query right away.
133
131
 
132
+ `sqlakit check` reads every template the project uses and reports a macro call
133
+ that can't work, before the query runs. See [template
134
+ validation](docs/sql.md#template-validation).
135
+
134
136
  ### From a string
135
137
 
136
138
  ```python
137
139
  db.sql.from_string("SELECT count(*) FROM users").scalars().one()
138
140
  ```
139
141
 
140
- The same templating, with no directory to configure.
142
+ The same syntax, with no directory to configure.
141
143
 
142
144
  ## Query builder
143
145
 
@@ -249,16 +251,20 @@ project has `rich`, the output is colored too.
249
251
 
250
252
  ### The debug server
251
253
 
252
- `sqlakit debugserver` serves a page that fills as the recordings arrive:
254
+ [`sqlakit-debugserver`](https://github.com/sqlakit/sqlakit-debugserver) serves a page that fills as the recordings
255
+ arrive. It is a package of its own:
253
256
 
254
257
  ```console
255
- $ sqlakit debugserver
258
+ $ pip install sqlakit-debugserver
259
+ $ sqlakit-debugserver
256
260
 
257
261
  SQLAKit debug server on http://localhost:5555
258
262
 
259
263
  Send recordings to it:
260
264
 
261
- │ with db.recording("GET /users", debugserver=("localhost", 5555)):
265
+ │ from sqlakit_debugserver import DebugServer
266
+ │
267
+ │ with db.recording("GET /users", send_to=DebugServer("localhost", 5555)):
262
268
  │ list_users()
263
269
  ```
264
270
 
@@ -59,11 +59,11 @@ with db.autocommit(): # AUTOCOMMIT, no transaction held open
59
59
 
60
60
  ## SQL templates
61
61
 
62
- Templates are `Jinja` files, so they can hold anything from a one-line query to
63
- a report with window functions or a recursive CTE.
64
- [jinja2sql](https://github.com/antonrh/jinja2sql) turns every `{{ name }}` into
65
- a bound parameter (`:name__1`), so values never reach the SQL text and there
66
- is no way to inject anything. Requires the `sqlakit[sql]` extra.
62
+ Templates are SQL files with `:name` parameters, so they hold anything from a
63
+ one-line query to a report with window functions or a recursive CTE, and every
64
+ SQL tool reads them as SQL. Values are bound, so they never reach the SQL
65
+ text. What changes per call, an optional
66
+ condition or a sort order, is a macro such as `tpl.if_set(:team, team = :team)`.
67
67
 
68
68
  ### From a file
69
69
 
@@ -71,7 +71,7 @@ is no way to inject anything. Requires the `sqlakit[sql]` extra.
71
71
  -- reports/by_team.sql
72
72
  SELECT team, count(*) AS members
73
73
  FROM users
74
- WHERE joined_at > {{ since }}
74
+ WHERE joined_at > :since
75
75
  GROUP BY team
76
76
  ```
77
77
 
@@ -98,13 +98,17 @@ type each row is returned as.
98
98
  `SQLAKit` adds the template name to the SQL as a comment, so a slow query log
99
99
  shows the source file of each query right away.
100
100
 
101
+ `sqlakit check` reads every template the project uses and reports a macro call
102
+ that can't work, before the query runs. See [template
103
+ validation](docs/sql.md#template-validation).
104
+
101
105
  ### From a string
102
106
 
103
107
  ```python
104
108
  db.sql.from_string("SELECT count(*) FROM users").scalars().one()
105
109
  ```
106
110
 
107
- The same templating, with no directory to configure.
111
+ The same syntax, with no directory to configure.
108
112
 
109
113
  ## Query builder
110
114
 
@@ -216,16 +220,20 @@ project has `rich`, the output is colored too.
216
220
 
217
221
  ### The debug server
218
222
 
219
- `sqlakit debugserver` serves a page that fills as the recordings arrive:
223
+ [`sqlakit-debugserver`](https://github.com/sqlakit/sqlakit-debugserver) serves a page that fills as the recordings
224
+ arrive. It is a package of its own:
220
225
 
221
226
  ```console
222
- $ sqlakit debugserver
227
+ $ pip install sqlakit-debugserver
228
+ $ sqlakit-debugserver
223
229
 
224
230
  SQLAKit debug server on http://localhost:5555
225
231
 
226
232
  Send recordings to it:
227
233
 
228
- │ with db.recording("GET /users", debugserver=("localhost", 5555)):
234
+ │ from sqlakit_debugserver import DebugServer
235
+ │
236
+ │ with db.recording("GET /users", send_to=DebugServer("localhost", 5555)):
229
237
  │ list_users()
230
238
  ```
231
239
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlakit"
3
- version = "0.19.0"
3
+ version = "0.21.0"
4
4
  description = "A toolkit for SQLAlchemy applications."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -42,18 +42,13 @@ sqlakit = "sqlakit._cli:main"
42
42
  sqlakit = "sqlakit.pytest_plugin"
43
43
 
44
44
  [project.urls]
45
- Repository = "https://github.com/antonrh/sqlakit"
46
- Documentation = "https://github.com/antonrh/sqlakit"
45
+ Repository = "https://github.com/sqlakit/sqlakit"
46
+ Documentation = "https://sqlakit.readthedocs.io/en/stable/"
47
47
 
48
48
  [project.optional-dependencies]
49
49
  asyncio = ["sqlalchemy[asyncio]>=2.0.22"]
50
- sql = ["jinja2sql>=0.12.0"]
51
50
  debug = ["sqlparse>=0.6.0"]
52
51
 
53
- [build-system]
54
- requires = ["uv_build>=0.11,<0.13"]
55
- build-backend = "uv_build"
56
-
57
52
  [tool.uv]
58
53
  default-groups = [
59
54
  "dev",
@@ -178,12 +173,12 @@ default_item_type = "cmd"
178
173
 
179
174
  [tool.codespell]
180
175
  ignore-words-list = "froms"
181
- skip = "./.venv,./site,./uv.lock,./.git,./debugserver/node_modules,./debugserver/bun.lock,./sqlakit/debugserver.html"
176
+ skip = "./.venv,./site,./uv.lock,./.git"
182
177
 
183
178
  [dependency-groups]
184
179
  dev = [
185
180
  "pytest>=9.1.1",
186
- "ty>=0.0.80",
181
+ "ty>=0.0.84",
187
182
  "ruff>=0.16.6",
188
183
  "poethepoet>=0.48.0",
189
184
  "anyio>=4.14.2",
@@ -194,7 +189,6 @@ dev = [
194
189
  "httpx>=0.28.1",
195
190
  "httpx2>=2.10.0",
196
191
  "sqlmodel>=0.0.39",
197
- "jinja2sql>=0.12.0",
198
192
  "pydantic>=2.13.4",
199
193
  "sqlparse>=0.6.0",
200
194
  "rich>=15.0.0",
@@ -206,6 +200,7 @@ dev = [
206
200
  "flask>=3.1.3",
207
201
  "dirty-equals>=0.11",
208
202
  "codespell>=2.4.3",
203
+ "sqruff>=0.40.0",
209
204
  ]
210
205
  docs = [
211
206
  "mkdocs>=1.6.1,<2",
@@ -213,3 +208,7 @@ docs = [
213
208
  "mkdocstrings[python]>=1.0.6",
214
209
  "ruff>=0.16.6",
215
210
  ]
211
+
212
+ [build-system]
213
+ requires = ["uv_build>=0.11,<0.13"]
214
+ build-backend = "uv_build"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlakit"
3
- version = "0.19.0"
3
+ version = "0.21.0"
4
4
  description = "A toolkit for SQLAlchemy applications."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -33,24 +33,17 @@ sqlakit = "sqlakit._cli:main"
33
33
  sqlakit = "sqlakit.pytest_plugin"
34
34
 
35
35
  [project.urls]
36
- Repository = "https://github.com/antonrh/sqlakit"
37
- Documentation = "https://github.com/antonrh/sqlakit"
36
+ Repository = "https://github.com/sqlakit/sqlakit"
37
+ Documentation = "https://sqlakit.readthedocs.io/en/stable/"
38
38
 
39
39
  [project.optional-dependencies]
40
40
  asyncio = [
41
41
  "sqlalchemy[asyncio]>=2.0.22",
42
42
  ]
43
- sql = [
44
- "jinja2sql>=0.12.0",
45
- ]
46
43
  debug = [
47
44
  "sqlparse>=0.6.0",
48
45
  ]
49
46
 
50
- [build-system]
51
- requires = ["uv_build>=0.11,<0.13"]
52
- build-backend = "uv_build"
53
-
54
47
  [tool.uv.build-backend]
55
48
  module-name = "sqlakit"
56
49
  module-root = ""
@@ -61,7 +54,7 @@ default-groups = ["dev", "docs"]
61
54
  [dependency-groups]
62
55
  dev = [
63
56
  "pytest>=9.1.1",
64
- "ty>=0.0.80",
57
+ "ty>=0.0.84",
65
58
  "ruff>=0.16.6",
66
59
  "poethepoet>=0.48.0",
67
60
  "anyio>=4.14.2",
@@ -72,7 +65,6 @@ dev = [
72
65
  "httpx>=0.28.1",
73
66
  "httpx2>=2.10.0",
74
67
  "sqlmodel>=0.0.39",
75
- "jinja2sql>=0.12.0",
76
68
  "pydantic>=2.13.4",
77
69
  "sqlparse>=0.6.0",
78
70
  "rich>=15.0.0",
@@ -84,6 +76,7 @@ dev = [
84
76
  "flask>=3.1.3",
85
77
  "dirty-equals>=0.11",
86
78
  "codespell>=2.4.3",
79
+ "sqruff>=0.40.0",
87
80
  ]
88
81
  docs = [
89
82
  "mkdocs>=1.6.1,<2",
@@ -101,8 +94,8 @@ filterwarnings = [
101
94
  addopts = "-p pytester"
102
95
  # The example is a project of its own, run by tests/examples, not collected here.
103
96
  norecursedirs = [
104
- "examples/testing_plugin", # a project of its own, run by tests/examples
105
- "tests/projects", # the projects the plugin tests copy
97
+ "examples/testing_plugin", # a project of its own, run by tests/examples
98
+ "tests/projects", # the projects the plugin tests copy
106
99
  ".*",
107
100
  "build",
108
101
  "dist",
@@ -137,39 +130,39 @@ ignore = [
137
130
 
138
131
  [tool.ruff.lint.per-file-ignores]
139
132
  "tests/**/*.py" = [
140
- "S101", # tests assert
141
- "PLR2004", # and compare against the values they mean
142
- "SLF001", # they read the internals they are testing
143
- "PT012", # a `raises` block is allowed to set the scene
144
- "B018", # touching an attribute is how a test triggers it
145
- "SIM117", # nested `with` reads better than a long line of them
146
- "PLC0415", # a fixture imports what it is about, where it is about it
147
- "DTZ001", # naive datetimes are fine as fixtures
148
- "ARG001", # a fixture may be requested for its side effect
149
- "PT011", # `raises` without `match` is enough for our own errors
150
- "ANN401", # test helpers take whatever the example hands them
151
- "D401", # test docstrings read as descriptions
152
- "N818", # a test's own exception needs no Error suffix
153
- "FBT001", # parametrised booleans arrive positionally
154
- "PLW0108", # a lambda in a parameter table is deliberate
155
- "DTZ005", # naive datetimes are fine as fixtures
156
- "S106", # the passwords here are fake, and the point of the test
157
- "S310", # the URLs here are the tests' own server
133
+ "S101", # tests assert
134
+ "PLR2004", # and compare against the values they mean
135
+ "SLF001", # they read the internals they are testing
136
+ "PT012", # a `raises` block is allowed to set the scene
137
+ "B018", # touching an attribute is how a test triggers it
138
+ "SIM117", # nested `with` reads better than a long line of them
139
+ "PLC0415", # a fixture imports what it is about, where it is about it
140
+ "DTZ001", # naive datetimes are fine as fixtures
141
+ "ARG001", # a fixture may be requested for its side effect
142
+ "PT011", # `raises` without `match` is enough for our own errors
143
+ "ANN401", # test helpers take whatever the example hands them
144
+ "D401", # test docstrings read as descriptions
145
+ "N818", # a test's own exception needs no Error suffix
146
+ "FBT001", # parametrised booleans arrive positionally
147
+ "PLW0108", # a lambda in a parameter table is deliberate
148
+ "DTZ005", # naive datetimes are fine as fixtures
149
+ "S106", # the passwords here are fake, and the point of the test
150
+ "S310", # the URLs here are the tests' own server
158
151
  ]
159
152
  "tools/**/*.py" = [
160
- "INP001", # a script, not a package
161
- "T201", # a script that says what it found
153
+ "INP001", # a script, not a package
154
+ "T201", # a script that says what it found
162
155
  ]
163
156
  "examples/**/*.py" = [
164
- "INP001", # examples are read one file at a time, not imported as a package
165
- "ARG001", # FastAPI hands handlers a request they may not use
166
- "TC003", # examples keep their imports where a reader expects them
157
+ "INP001", # examples are read one file at a time, not imported as a package
158
+ "ARG001", # FastAPI hands handlers a request they may not use
159
+ "TC003", # examples keep their imports where a reader expects them
167
160
  ]
168
161
  "examples/testing_plugin/*.py" = [
169
- "S101", # the example is a test suite, and tests assert
162
+ "S101", # the example is a test suite, and tests assert
170
163
  ]
171
164
  "tests/projects/**/*.py" = [
172
- "INP001", # a project the tests copy, not a package of this one
165
+ "INP001", # a project the tests copy, not a package of this one
173
166
  ]
174
167
 
175
168
  [tool.ty.src]
@@ -211,4 +204,8 @@ default_item_type = "cmd"
211
204
  [tool.codespell]
212
205
  # `froms` is SQLAlchemy's own word for what a SELECT selects from.
213
206
  ignore-words-list = "froms"
214
- skip = "./.venv,./site,./uv.lock,./.git,./debugserver/node_modules,./debugserver/bun.lock,./sqlakit/debugserver.html"
207
+ skip = "./.venv,./site,./uv.lock,./.git"
208
+
209
+ [build-system]
210
+ requires = ["uv_build>=0.11,<0.13"]
211
+ build-backend = "uv_build"
@@ -1,6 +1,5 @@
1
1
  from ._base import DEFAULT_ENGINE_ARGS, DEFAULT_SESSION_ARGS
2
2
  from ._db import Database, RetryingTransaction, Transaction
3
- from ._debugserver import DebugServer
4
3
  from ._discovery import import_models, import_string
5
4
  from ._query import (
6
5
  CASE_INSENSITIVE_COLLATIONS,
@@ -16,7 +15,6 @@ from ._routing import Router
16
15
  from .exceptions import (
17
16
  DEFAULT_ALIAS,
18
17
  AliasInUseError,
19
- AsyncFilterError,
20
18
  BulkQueryError,
21
19
  ConflictingDatabaseUrlError,
22
20
  ConflictingJoinError,
@@ -25,12 +23,17 @@ from .exceptions import (
25
23
  DefaultAliasError,
26
24
  DetachedInstanceError,
27
25
  DuplicateKeyError,
26
+ InlineValueError,
28
27
  InstanceNotFoundError,
29
28
  InvalidCursorError,
30
29
  InvalidDatabaseConfigError,
31
30
  InvalidNullsError,
32
31
  InvalidOrderFieldError,
32
+ InvalidSortStringError,
33
33
  KeyLookupError,
34
+ MacroArgumentError,
35
+ MacroDefinitionError,
36
+ MacroSyntaxError,
34
37
  MissingConnectionError,
35
38
  MissingDatabaseUrlError,
36
39
  MissingDefaultDatabaseError,
@@ -40,6 +43,8 @@ from .exceptions import (
40
43
  MultipleInstancesFoundError,
41
44
  NullCursorValueError,
42
45
  PageItemsMismatchError,
46
+ ParameterPathError,
47
+ ProjectConfigError,
43
48
  RawStatementError,
44
49
  RetryNotSupportedError,
45
50
  SQLAKitError,
@@ -50,7 +55,9 @@ from .exceptions import (
50
55
  UncomparableOrderingError,
51
56
  UnknownDatabaseError,
52
57
  UnknownFieldError,
58
+ UnknownIdentifierError,
53
59
  UnknownImportPathError,
60
+ UnknownMacroError,
54
61
  UnknownOrderFieldError,
55
62
  UnorderedPageError,
56
63
  UnregisteredDatabaseError,
@@ -71,7 +78,6 @@ __all__ = [
71
78
  "DEFAULT_ENGINE_ARGS",
72
79
  "DEFAULT_SESSION_ARGS",
73
80
  "AliasInUseError",
74
- "AsyncFilterError",
75
81
  "BulkQueryError",
76
82
  "ConflictingDatabaseUrlError",
77
83
  "ConflictingJoinError",
@@ -81,17 +87,21 @@ __all__ = [
81
87
  "DatabaseConfig",
82
88
  "DatabaseNotConfiguredError",
83
89
  "Databases",
84
- "DebugServer",
85
90
  "DefaultAliasError",
86
91
  "DetachedInstanceError",
87
92
  "DuplicateKeyError",
88
93
  "EngineArgs",
94
+ "InlineValueError",
89
95
  "InstanceNotFoundError",
90
96
  "InvalidCursorError",
91
97
  "InvalidDatabaseConfigError",
92
98
  "InvalidNullsError",
93
99
  "InvalidOrderFieldError",
100
+ "InvalidSortStringError",
94
101
  "KeyLookupError",
102
+ "MacroArgumentError",
103
+ "MacroDefinitionError",
104
+ "MacroSyntaxError",
95
105
  "MissingConnectionError",
96
106
  "MissingDatabaseUrlError",
97
107
  "MissingDefaultDatabaseError",
@@ -103,6 +113,8 @@ __all__ = [
103
113
  "OrderBy",
104
114
  "Page",
105
115
  "PageItemsMismatchError",
116
+ "ParameterPathError",
117
+ "ProjectConfigError",
106
118
  "QueryStats",
107
119
  "RawStatementError",
108
120
  "Recording",
@@ -122,7 +134,9 @@ __all__ = [
122
134
  "UncountedPage",
123
135
  "UnknownDatabaseError",
124
136
  "UnknownFieldError",
137
+ "UnknownIdentifierError",
125
138
  "UnknownImportPathError",
139
+ "UnknownMacroError",
126
140
  "UnknownOrderFieldError",
127
141
  "UnorderedPageError",
128
142
  "UnregisteredDatabaseError",
@@ -24,7 +24,6 @@ import sqlalchemy as sa
24
24
  import sqlalchemy.event
25
25
  from typing_extensions import Unpack
26
26
 
27
- from ._debugserver import DebugServer, send_recording
28
27
  from ._discovery import import_string
29
28
  from ._recording import (
30
29
  KEEP,
@@ -358,7 +357,7 @@ class BaseDatabase(Generic[ConnectionT, SessionT]):
358
357
  stacks: bool = False,
359
358
  skip_queries_from: Sequence[str | PathLike[str]] = (),
360
359
  into: Recording | None = None,
361
- debugserver: DebugServer | tuple[str, int] | None = None,
360
+ send_to: Callable[[Recording], object] | None = None,
362
361
  ) -> Iterator[Recording]:
363
362
  """Record the statements of this block, and what they add up to.
364
363
 
@@ -373,8 +372,8 @@ class BaseDatabase(Generic[ConnectionT, SessionT]):
373
372
 
374
373
  ``logger`` writes a summary when the block ends, at a level the numbers
375
374
  choose. ``echo`` prints the statements instead, coloured where `rich` is
376
- installed. ``debugserver`` sends the recording to a `sqlakit debugserver`
377
- listening there, and says nothing when none is. ``stacks`` has every
375
+ installed. ``send_to`` is called with the recording, to hand it on:
376
+ `sqlakit_debugserver.DebugServer` is one. ``stacks`` has every
378
377
  statement remember the frames that led to it, at the cost of a stack walk
379
378
  each time. ``skip_queries_from`` names the files whose statements are none of
380
379
  your business: what those run is not recorded at all, which leaves a test's
@@ -403,8 +402,8 @@ class BaseDatabase(Generic[ConnectionT, SessionT]):
403
402
  recording.log(logger)
404
403
  if echo:
405
404
  recording.echo()
406
- if debugserver is not None:
407
- send_recording(recording, debugserver)
405
+ if send_to is not None:
406
+ send_to(recording)
408
407
 
409
408
  @contextmanager
410
409
  def assert_queries(
@@ -886,7 +885,7 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
886
885
  stacks: bool = False,
887
886
  skip_queries_from: Sequence[str | PathLike[str]] = (),
888
887
  into: Recording | None = None,
889
- debugserver: DebugServer | tuple[str, int] | None = None,
888
+ send_to: Callable[[Recording], object] | None = None,
890
889
  using: str | DatabaseT | Sequence[str | DatabaseT] | None = None,
891
890
  ) -> Iterator[Recording]:
892
891
  """Record every database this registry has, not the default one alone.
@@ -936,8 +935,8 @@ class _DatabaseRegistryMixin(BaseDatabase[Any, Any], Generic[DatabaseT]):
936
935
  together.log(logger)
937
936
  if echo:
938
937
  together.echo()
939
- if debugserver is not None:
940
- send_recording(together, debugserver)
938
+ if send_to is not None:
939
+ send_to(together)
941
940
 
942
941
  @contextmanager
943
942
  def unbound(self) -> Iterator[None]: