sqlakit 0.19.0__tar.gz → 0.20.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 (30) hide show
  1. {sqlakit-0.19.0 → sqlakit-0.20.0}/PKG-INFO +8 -4
  2. {sqlakit-0.19.0 → sqlakit-0.20.0}/README.md +7 -3
  3. {sqlakit-0.19.0 → sqlakit-0.20.0}/pyproject.toml +2 -5
  4. {sqlakit-0.19.0 → sqlakit-0.20.0}/pyproject.toml.orig +2 -5
  5. {sqlakit-0.19.0 → sqlakit-0.20.0}/sqlakit/__init__.py +0 -2
  6. {sqlakit-0.19.0 → sqlakit-0.20.0}/sqlakit/_base.py +8 -9
  7. {sqlakit-0.19.0 → sqlakit-0.20.0}/sqlakit/pytest_plugin.py +24 -10
  8. sqlakit-0.19.0/sqlakit/_cli.py +0 -101
  9. sqlakit-0.19.0/sqlakit/_debugserver.py +0 -396
  10. sqlakit-0.19.0/sqlakit/debugserver.html +0 -103
  11. {sqlakit-0.19.0 → sqlakit-0.20.0}/LICENSE +0 -0
  12. {sqlakit-0.19.0 → sqlakit-0.20.0}/sqlakit/_db.py +0 -0
  13. {sqlakit-0.19.0 → sqlakit-0.20.0}/sqlakit/_discovery.py +0 -0
  14. {sqlakit-0.19.0 → sqlakit-0.20.0}/sqlakit/_model.py +0 -0
  15. {sqlakit-0.19.0 → sqlakit-0.20.0}/sqlakit/_query.py +0 -0
  16. {sqlakit-0.19.0 → sqlakit-0.20.0}/sqlakit/_recording.py +0 -0
  17. {sqlakit-0.19.0 → sqlakit-0.20.0}/sqlakit/_registry.py +0 -0
  18. {sqlakit-0.19.0 → sqlakit-0.20.0}/sqlakit/_routing.py +0 -0
  19. {sqlakit-0.19.0 → sqlakit-0.20.0}/sqlakit/_sql.py +0 -0
  20. {sqlakit-0.19.0 → sqlakit-0.20.0}/sqlakit/asyncio/__init__.py +0 -0
  21. {sqlakit-0.19.0 → sqlakit-0.20.0}/sqlakit/asyncio/_db.py +0 -0
  22. {sqlakit-0.19.0 → sqlakit-0.20.0}/sqlakit/asyncio/_registry.py +0 -0
  23. {sqlakit-0.19.0 → sqlakit-0.20.0}/sqlakit/asyncio/orm.py +0 -0
  24. {sqlakit-0.19.0 → sqlakit-0.20.0}/sqlakit/asyncio/sql.py +0 -0
  25. {sqlakit-0.19.0 → sqlakit-0.20.0}/sqlakit/exceptions.py +0 -0
  26. {sqlakit-0.19.0 → sqlakit-0.20.0}/sqlakit/orm.py +0 -0
  27. {sqlakit-0.19.0 → sqlakit-0.20.0}/sqlakit/py.typed +0 -0
  28. {sqlakit-0.19.0 → sqlakit-0.20.0}/sqlakit/sql.py +0 -0
  29. {sqlakit-0.19.0 → sqlakit-0.20.0}/sqlakit/testing.py +0 -0
  30. {sqlakit-0.19.0 → sqlakit-0.20.0}/sqlakit/types.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.20.0
4
4
  Summary: A toolkit for SQLAlchemy applications.
5
5
  Keywords: sqlalchemy,database,orm,sql,asyncio
6
6
  Author: Anton Ruhlov
@@ -249,16 +249,20 @@ project has `rich`, the output is colored too.
249
249
 
250
250
  ### The debug server
251
251
 
252
- `sqlakit debugserver` serves a page that fills as the recordings arrive:
252
+ [`sqlakit-debugserver`](https://github.com/sqlakit/sqlakit-debugserver) serves a page that fills as the recordings
253
+ arrive. It is a package of its own:
253
254
 
254
255
  ```console
255
- $ sqlakit debugserver
256
+ $ pip install sqlakit-debugserver
257
+ $ sqlakit-debugserver
256
258
 
257
259
  SQLAKit debug server on http://localhost:5555
258
260
 
259
261
  Send recordings to it:
260
262
 
261
- │ with db.recording("GET /users", debugserver=("localhost", 5555)):
263
+ │ from sqlakit_debugserver import DebugServer
264
+ │
265
+ │ with db.recording("GET /users", send_to=DebugServer("localhost", 5555)):
262
266
  │ list_users()
263
267
  ```
264
268
 
@@ -216,16 +216,20 @@ project has `rich`, the output is colored too.
216
216
 
217
217
  ### The debug server
218
218
 
219
- `sqlakit debugserver` serves a page that fills as the recordings arrive:
219
+ [`sqlakit-debugserver`](https://github.com/sqlakit/sqlakit-debugserver) serves a page that fills as the recordings
220
+ arrive. It is a package of its own:
220
221
 
221
222
  ```console
222
- $ sqlakit debugserver
223
+ $ pip install sqlakit-debugserver
224
+ $ sqlakit-debugserver
223
225
 
224
226
  SQLAKit debug server on http://localhost:5555
225
227
 
226
228
  Send recordings to it:
227
229
 
228
- │ with db.recording("GET /users", debugserver=("localhost", 5555)):
230
+ │ from sqlakit_debugserver import DebugServer
231
+ │
232
+ │ with db.recording("GET /users", send_to=DebugServer("localhost", 5555)):
229
233
  │ list_users()
230
234
  ```
231
235
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlakit"
3
- version = "0.19.0"
3
+ version = "0.20.0"
4
4
  description = "A toolkit for SQLAlchemy applications."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -35,9 +35,6 @@ dependencies = [
35
35
  name = "Anton Ruhlov"
36
36
  email = "antonruhlov@gmail.com"
37
37
 
38
- [project.scripts]
39
- sqlakit = "sqlakit._cli:main"
40
-
41
38
  [project.entry-points.pytest11]
42
39
  sqlakit = "sqlakit.pytest_plugin"
43
40
 
@@ -178,7 +175,7 @@ default_item_type = "cmd"
178
175
 
179
176
  [tool.codespell]
180
177
  ignore-words-list = "froms"
181
- skip = "./.venv,./site,./uv.lock,./.git,./debugserver/node_modules,./debugserver/bun.lock,./sqlakit/debugserver.html"
178
+ skip = "./.venv,./site,./uv.lock,./.git"
182
179
 
183
180
  [dependency-groups]
184
181
  dev = [
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlakit"
3
- version = "0.19.0"
3
+ version = "0.20.0"
4
4
  description = "A toolkit for SQLAlchemy applications."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -26,9 +26,6 @@ dependencies = [
26
26
  "typing-extensions>=4.13",
27
27
  ]
28
28
 
29
- [project.scripts]
30
- sqlakit = "sqlakit._cli:main"
31
-
32
29
  [project.entry-points.pytest11]
33
30
  sqlakit = "sqlakit.pytest_plugin"
34
31
 
@@ -211,4 +208,4 @@ default_item_type = "cmd"
211
208
  [tool.codespell]
212
209
  # `froms` is SQLAlchemy's own word for what a SELECT selects from.
213
210
  ignore-words-list = "froms"
214
- skip = "./.venv,./site,./uv.lock,./.git,./debugserver/node_modules,./debugserver/bun.lock,./sqlakit/debugserver.html"
211
+ skip = "./.venv,./site,./uv.lock,./.git"
@@ -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,
@@ -81,7 +80,6 @@ __all__ = [
81
80
  "DatabaseConfig",
82
81
  "DatabaseNotConfiguredError",
83
82
  "Databases",
84
- "DebugServer",
85
83
  "DefaultAliasError",
86
84
  "DetachedInstanceError",
87
85
  "DuplicateKeyError",
@@ -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]:
@@ -38,6 +38,7 @@ Every other test connects to nothing, and reaching for a session there raises
38
38
  from __future__ import annotations
39
39
 
40
40
  import asyncio
41
+ import importlib.util
41
42
  import inspect
42
43
  import pathlib
43
44
  import time
@@ -48,7 +49,6 @@ from typing import TYPE_CHECKING, Any
48
49
 
49
50
  import pytest
50
51
 
51
- from ._debugserver import as_payload, write_report
52
52
  from ._recording import Recording, check, require_expectation
53
53
  from ._registry import db as importable_db
54
54
  from .exceptions import UnknownDatabaseError
@@ -63,7 +63,7 @@ if TYPE_CHECKING:
63
63
  MARKER = "db"
64
64
  SYNC_FIXTURE = "_sqlakit_transaction"
65
65
  ASYNC_FIXTURE = "_sqlakit_async_transaction"
66
- REPORT = pytest.StashKey[list[dict[str, Any]]]()
66
+ REPORT = pytest.StashKey[list[tuple[Recording, str, list[str]]]]()
67
67
  WHERE = pytest.StashKey["pathlib.Path | None"]()
68
68
  NAMED_BY_THE_CLOCK = ""
69
69
  """What `--sqlakit-report` with no path means."""
@@ -114,9 +114,18 @@ def pytest_terminal_summary(
114
114
  ) -> None:
115
115
  """Write the report at the end, and say where it went."""
116
116
  where = config.stash.get(WHERE, None)
117
- records = config.stash.get(REPORT, [])
118
- if where is None or not records:
117
+ reported = config.stash.get(REPORT, [])
118
+ if where is None or not reported:
119
119
  return
120
+ # Installed apart from `sqlakit`, and only when a report is asked for.
121
+ from sqlakit_debugserver import ( # noqa: PLC0415 # ty: ignore[unresolved-import]
122
+ as_payload,
123
+ write_report,
124
+ )
125
+
126
+ records = [
127
+ as_payload(recording, app=app, tags=tags) for recording, app, tags in reported
128
+ ]
120
129
  tests = len({record["app"] + record["label"] for record in records})
121
130
  queries = sum(record["count"] for record in records)
122
131
  written = write_report(where, records, about=f"{tests} tests · {queries} queries")
@@ -133,9 +142,8 @@ def _report_path(config: pytest.Config) -> pathlib.Path | None:
133
142
  if asked is None:
134
143
  return None
135
144
  if asked == NAMED_BY_THE_CLOCK:
136
- return config.rootpath / time.strftime("sqlakit-%Y%m%d-%H%M%S.html")
137
- where = pathlib.Path(asked)
138
- if where.is_dir():
145
+ where = config.rootpath / time.strftime("sqlakit-%Y%m%d-%H%M%S.html")
146
+ elif (where := pathlib.Path(asked)).is_dir():
139
147
  # `--sqlakit-report tests/unit` reads as the path to write to.
140
148
  message = (
141
149
  f"--sqlakit-report was given `{asked}`, which is a directory. "
@@ -143,6 +151,12 @@ def _report_path(config: pytest.Config) -> pathlib.Path | None:
143
151
  f"for a name with the time in it."
144
152
  )
145
153
  raise pytest.UsageError(message)
154
+ if importlib.util.find_spec("sqlakit_debugserver") is None:
155
+ message = (
156
+ "--sqlakit-report writes the debug server's page, which comes with "
157
+ "it: pip install sqlakit-debugserver"
158
+ )
159
+ raise pytest.UsageError(message)
146
160
  return where
147
161
 
148
162
 
@@ -388,10 +402,10 @@ def _reported(request: pytest.FixtureRequest, db: Any) -> Iterator[None]: # noq
388
402
  )
389
403
  yield
390
404
  request.config.stash[REPORT].append(
391
- as_payload(
405
+ (
392
406
  recording,
393
- app=str(node.path.relative_to(request.config.rootpath)),
394
- tags=[mark.name for mark in node.iter_markers() if mark.name != MARKER],
407
+ str(node.path.relative_to(request.config.rootpath)),
408
+ [mark.name for mark in node.iter_markers() if mark.name != MARKER],
395
409
  )
396
410
  )
397
411
 
@@ -1,101 +0,0 @@
1
- """`sqlakit`, the command line."""
2
-
3
- from __future__ import annotations
4
-
5
- import argparse
6
- import os
7
- import sys
8
-
9
- from ._debugserver import create_server
10
-
11
-
12
- def main(argv: list[str] | None = None) -> int:
13
- """Run a command, and return what the shell should make of it."""
14
- parser = argparse.ArgumentParser(prog="sqlakit", description="SQLAKit.")
15
- commands = parser.add_subparsers(dest="command", required=True)
16
-
17
- debugserver = commands.add_parser(
18
- "debugserver", help="watch the recordings an application sends"
19
- )
20
- debugserver.add_argument("-H", "--host", default="localhost")
21
- debugserver.add_argument("-p", "--port", type=int, default=5555)
22
-
23
- arguments = parser.parse_args(argv)
24
- if arguments.command == "debugserver":
25
- return _debugserver(arguments.host, arguments.port)
26
- return 1
27
-
28
-
29
- def _debugserver(host: str, port: int) -> int:
30
- """Serve the recordings until the terminal says otherwise."""
31
- try:
32
- server = create_server(host, port)
33
- except OSError as error:
34
- _say(f"\n{_paint('cannot listen', BOLD, RED)} on {host}:{port} — {error}\n")
35
- return 1
36
- _greeting(host, port)
37
- try:
38
- server.serve_forever()
39
- except KeyboardInterrupt:
40
- _say(_paint("\nstopped", DIM))
41
- finally:
42
- server.server_close()
43
- return 0
44
-
45
-
46
- BOLD = "1"
47
- DIM = "2"
48
- TEAL = "36"
49
- RED = "31"
50
- GREEN = "32"
51
- VIOLET = "35"
52
-
53
-
54
- def _colours() -> bool:
55
- """Whether to paint: a terminal that wants it, and was not told otherwise."""
56
- if os.environ.get("NO_COLOR"):
57
- return False
58
- if os.environ.get("FORCE_COLOR"):
59
- return True
60
- return sys.stdout.isatty()
61
-
62
-
63
- def _paint(text: str, *codes: str) -> str:
64
- """Return the text in those colours, or as it is where colour is unwanted."""
65
- if not codes or not _colours():
66
- return text
67
- return f"\033[{';'.join(codes)}m{text}\033[0m"
68
-
69
-
70
- def _say(text: str) -> None:
71
- """Print, and flush: the server then blocks, and a pipe would hold this."""
72
- print(text, flush=True) # noqa: T201
73
-
74
-
75
- def _block(*lines: str) -> str:
76
- """Return these lines set off by a rule, the way a page sets off code."""
77
- rule = _paint("│", DIM)
78
- return "".join(f"\n {rule} {line}" for line in lines) + "\n"
79
-
80
-
81
- def _greeting(host: str, port: int) -> None:
82
- """Print where the page is, and the block that fills it."""
83
- where = _paint(f"http://{host}:{port}", TEAL, BOLD)
84
- label = _paint('"GET /users"', GREEN)
85
- named = _paint(f'"{host}"', GREEN)
86
- number = _paint(str(port), VIOLET)
87
- _say(
88
- f"\nSQLAKit debug server on {where}\n\n"
89
- + _paint(
90
- "Send recordings to it:\n",
91
- DIM,
92
- )
93
- + _block(
94
- f"with db.recording({label}, debugserver=({named}, {number})):",
95
- " list_users()",
96
- )
97
- )
98
-
99
-
100
- if __name__ == "__main__":
101
- sys.exit(main())