sqlakit 0.14.0__tar.gz → 0.15.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.14.0 → sqlakit-0.15.0}/PKG-INFO +1 -1
  2. {sqlakit-0.14.0 → sqlakit-0.15.0}/pyproject.toml +1 -1
  3. {sqlakit-0.14.0 → sqlakit-0.15.0}/pyproject.toml.orig +1 -1
  4. {sqlakit-0.14.0 → sqlakit-0.15.0}/sqlakit/_query.py +25 -2
  5. {sqlakit-0.14.0 → sqlakit-0.15.0}/sqlakit/asyncio/orm.py +24 -4
  6. {sqlakit-0.14.0 → sqlakit-0.15.0}/sqlakit/asyncio/sql.py +36 -13
  7. {sqlakit-0.14.0 → sqlakit-0.15.0}/sqlakit/orm.py +22 -4
  8. {sqlakit-0.14.0 → sqlakit-0.15.0}/sqlakit/sql.py +36 -13
  9. {sqlakit-0.14.0 → sqlakit-0.15.0}/LICENSE +0 -0
  10. {sqlakit-0.14.0 → sqlakit-0.15.0}/README.md +0 -0
  11. {sqlakit-0.14.0 → sqlakit-0.15.0}/sqlakit/__init__.py +0 -0
  12. {sqlakit-0.14.0 → sqlakit-0.15.0}/sqlakit/_base.py +0 -0
  13. {sqlakit-0.14.0 → sqlakit-0.15.0}/sqlakit/_cli.py +0 -0
  14. {sqlakit-0.14.0 → sqlakit-0.15.0}/sqlakit/_db.py +0 -0
  15. {sqlakit-0.14.0 → sqlakit-0.15.0}/sqlakit/_debugserver.py +0 -0
  16. {sqlakit-0.14.0 → sqlakit-0.15.0}/sqlakit/_discovery.py +0 -0
  17. {sqlakit-0.14.0 → sqlakit-0.15.0}/sqlakit/_model.py +0 -0
  18. {sqlakit-0.14.0 → sqlakit-0.15.0}/sqlakit/_recording.py +0 -0
  19. {sqlakit-0.14.0 → sqlakit-0.15.0}/sqlakit/_registry.py +0 -0
  20. {sqlakit-0.14.0 → sqlakit-0.15.0}/sqlakit/_routing.py +0 -0
  21. {sqlakit-0.14.0 → sqlakit-0.15.0}/sqlakit/_sql.py +0 -0
  22. {sqlakit-0.14.0 → sqlakit-0.15.0}/sqlakit/asyncio/__init__.py +0 -0
  23. {sqlakit-0.14.0 → sqlakit-0.15.0}/sqlakit/asyncio/_db.py +0 -0
  24. {sqlakit-0.14.0 → sqlakit-0.15.0}/sqlakit/asyncio/_registry.py +0 -0
  25. {sqlakit-0.14.0 → sqlakit-0.15.0}/sqlakit/debugserver.html +0 -0
  26. {sqlakit-0.14.0 → sqlakit-0.15.0}/sqlakit/exceptions.py +0 -0
  27. {sqlakit-0.14.0 → sqlakit-0.15.0}/sqlakit/py.typed +0 -0
  28. {sqlakit-0.14.0 → sqlakit-0.15.0}/sqlakit/pytest_plugin.py +0 -0
  29. {sqlakit-0.14.0 → sqlakit-0.15.0}/sqlakit/testing.py +0 -0
  30. {sqlakit-0.14.0 → sqlakit-0.15.0}/sqlakit/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlakit
3
- Version: 0.14.0
3
+ Version: 0.15.0
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.14.0"
3
+ version = "0.15.0"
4
4
  description = "A toolkit for SQLAlchemy applications."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlakit"
3
- version = "0.14.0"
3
+ version = "0.15.0"
4
4
  description = "A toolkit for SQLAlchemy applications."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -112,6 +112,21 @@ RowT = TypeVar("RowT")
112
112
  RowT_co = TypeVar("RowT_co", covariant=True)
113
113
 
114
114
 
115
+ def merged(
116
+ mapping: Mapping[str, Any] | None, keywords: Mapping[str, Any]
117
+ ) -> Mapping[str, Any]:
118
+ """Return the values a call was given, however they were passed.
119
+
120
+ A caller handed a mapping passes it as it is, and the keywords beside it
121
+ win, so one value can be replaced without copying the mapping.
122
+ """
123
+ if not mapping:
124
+ return keywords
125
+ if not keywords:
126
+ return mapping
127
+ return {**mapping, **keywords}
128
+
129
+
115
130
  class CaseInsensitive(sa.ColumnElement[Any]):
116
131
  """A column compared without regard to case, however the dialect does it.
117
132
 
@@ -466,18 +481,26 @@ class BaseQuery(Generic[ModelT]):
466
481
  )
467
482
  return query
468
483
 
469
- def from_sql(self, template: str, /, **context: Any) -> Self: # noqa: ANN401
484
+ def from_sql(
485
+ self,
486
+ template: str,
487
+ /,
488
+ context: Mapping[str, Any] | None = None,
489
+ **values: Any, # noqa: ANN401
490
+ ) -> Self:
470
491
  """Take the rows of a SQL template, mapped onto the model.
471
492
 
472
493
  ```python
473
494
  User.query.from_sql("users/active.sql", team="red").all()
495
+ User.query.from_sql("users/active.sql", context=filters).all()
474
496
  ```
475
497
 
476
498
  Read from the database this query runs on, and rendered for its dialect. As
477
499
  with `from_statement`, nothing can be added afterwards and
478
500
  ``__query_filter__`` is not applied.
479
501
  """
480
- return self.from_statement(self.db.sql.from_file(template, **context).statement)
502
+ rows = self.db.sql.from_file(template, context, **values)
503
+ return self.from_statement(rows.statement)
481
504
 
482
505
  def where(self, *criteria: _ColumnExpressionArgument[bool]) -> Self:
483
506
  """Narrow the rows, as `Select.where` does."""
@@ -28,6 +28,7 @@ from sqlakit._query import (
28
28
  BaseQuery,
29
29
  CursorPage,
30
30
  Page,
31
+ merged,
31
32
  one_row,
32
33
  one_row_or_none,
33
34
  orderable,
@@ -344,19 +345,28 @@ class Query(BaseQuery[ModelT]):
344
345
  scalar=len(columns) == 1,
345
346
  )
346
347
 
347
- async def create(self, **values: Any) -> ModelT: # noqa: ANN401
348
+ async def create(
349
+ self,
350
+ values: Mapping[str, Any] | None = None,
351
+ /,
352
+ **fields: Any, # noqa: ANN401
353
+ ) -> ModelT:
348
354
  """Write a new row, and return it as an instance.
349
355
 
350
356
  ```python
351
357
  user = await User.query.create(name="ada", team="red")
358
+ user = await User.query.create(payload.model_dump())
352
359
  ```
353
360
 
361
+ The fields are keywords, a mapping, or both, where a keyword replaces
362
+ the value of that name.
363
+
354
364
  The row goes through the session, so defaults, relationships and the identity
355
365
  map behave as they do for a model that saves itself. What it adds is a write
356
366
  that needs no model layer: `Query(User, db).create(...)` works on any mapped
357
367
  class.
358
368
  """
359
- instance = self.model(**values)
369
+ instance = self.model(**merged(values, fields))
360
370
  self.db.session.add(instance)
361
371
  await self._persist()
362
372
  return instance
@@ -377,9 +387,17 @@ class Query(BaseQuery[ModelT]):
377
387
  await self._persist()
378
388
  return len(rows)
379
389
 
380
- async def update(self, values: Mapping[str, Any]) -> int:
390
+ async def update(
391
+ self,
392
+ values: Mapping[str, Any] | None = None,
393
+ /,
394
+ **fields: Any, # noqa: ANN401
395
+ ) -> int:
381
396
  """Write these values to every matching row, and return how many.
382
397
 
398
+ A mapping, keywords, or both: `update({"team": "green"})` and
399
+ `update(team="green")` write the same statement.
400
+
383
401
  One statement, so the session's objects are updated from the database
384
402
  rather than in memory. Only the narrowing carries over.
385
403
 
@@ -387,7 +405,9 @@ class Query(BaseQuery[ModelT]):
387
405
  BulkQueryError: if the query carries anything a statement drops.
388
406
 
389
407
  """
390
- result = await self.db.session.execute(self._update_statement(values))
408
+ result = await self.db.session.execute(
409
+ self._update_statement(merged(values, fields))
410
+ )
391
411
  await self._persist()
392
412
  return cast("CursorResult[Any]", result).rowcount
393
413
 
@@ -5,6 +5,7 @@ from typing import TYPE_CHECKING, Any, TypeVar, cast
5
5
  import sqlalchemy as sa
6
6
  from typing_extensions import Unpack
7
7
 
8
+ from sqlakit._query import merged
8
9
  from sqlakit._sql import (
9
10
  BaseSQLQuery,
10
11
  Filter,
@@ -14,7 +15,7 @@ from sqlakit._sql import (
14
15
  )
15
16
 
16
17
  if TYPE_CHECKING:
17
- from collections.abc import AsyncIterator, Sequence
18
+ from collections.abc import AsyncIterator, Mapping, Sequence
18
19
 
19
20
  from sqlalchemy.engine import Result, ScalarResult
20
21
  from sqlalchemy.ext.asyncio import AsyncConnection
@@ -50,27 +51,48 @@ class SQL:
50
51
  def __repr__(self) -> str:
51
52
  return f"{type(self).__name__}({self.db!r})"
52
53
 
53
- def __call__(self, template: str, /, **context: Any) -> SQLQuery: # noqa: ANN401
54
+ def __call__(
55
+ self,
56
+ template: str,
57
+ /,
58
+ context: Mapping[str, Any] | None = None,
59
+ **values: Any, # noqa: ANN401
60
+ ) -> SQLQuery:
54
61
  """Read the rows of a template. Short for `from_file`.
55
62
 
56
63
  ```python
57
64
  await db.sql("users/active.sql", team="red").all()
58
65
  ```
59
66
  """
60
- return self.from_file(template, **context)
61
-
62
- def from_file(self, template: str, /, **context: Any) -> SQLQuery: # noqa: ANN401
67
+ return self.from_file(template, context, **values)
68
+
69
+ def from_file(
70
+ self,
71
+ template: str,
72
+ /,
73
+ context: Mapping[str, Any] | None = None,
74
+ **values: Any, # noqa: ANN401
75
+ ) -> SQLQuery:
63
76
  """Read the rows of a template kept under the database's ``templates=``.
64
77
 
65
78
  ```python
66
79
  await db.sql.from_file("users/active.sql", team="red").all()
80
+ await db.sql.from_file("users/active.sql", context=filters).all()
67
81
  ```
68
82
 
69
- The keyword arguments are the template's context.
83
+ The keyword arguments are the template's context, and ``context`` takes
84
+ the same values as a mapping, for values a caller was handed rather than
85
+ wrote. A value named `context` lives in that mapping.
70
86
  """
71
- return SQLQuery(self.db, template, context)
72
-
73
- def from_string(self, source: str, /, **context: Any) -> SQLQuery: # noqa: ANN401
87
+ return SQLQuery(self.db, template, merged(context, values))
88
+
89
+ def from_string(
90
+ self,
91
+ source: str,
92
+ /,
93
+ context: Mapping[str, Any] | None = None,
94
+ **values: Any, # noqa: ANN401
95
+ ) -> SQLQuery:
74
96
  """Read the rows of SQL written out here rather than kept in a file.
75
97
 
76
98
  ```python
@@ -79,11 +101,12 @@ class SQL:
79
101
  )
80
102
  ```
81
103
 
82
- Values are named in `{{ }}` and passed by keyword, as in a template. A
83
- `:name` or a `?` binds nothing here, and rendering says so rather than
84
- reaching the driver. It needs no ``templates=``.
104
+ Values are named in `{{ }}` and passed by keyword, or as the ``context``
105
+ mapping, as in a template. A `:name` or a `?` binds nothing here, and
106
+ rendering says so rather than reaching the driver. It needs no
107
+ ``templates=``.
85
108
  """
86
- return SQLQuery(self.db, source, context, inline=True)
109
+ return SQLQuery(self.db, source, merged(context, values), inline=True)
87
110
 
88
111
  def from_statement(self, statement: Executable) -> SQLQuery:
89
112
  """Read the rows of a statement built with SQLAlchemy.
@@ -29,6 +29,7 @@ from ._query import (
29
29
  BaseQuery,
30
30
  CursorPage,
31
31
  Page,
32
+ merged,
32
33
  one_row,
33
34
  one_row_or_none,
34
35
  orderable,
@@ -334,19 +335,28 @@ class Query(BaseQuery[ModelT]):
334
335
  scalar=len(columns) == 1,
335
336
  )
336
337
 
337
- def create(self, **values: Any) -> ModelT: # noqa: ANN401
338
+ def create(
339
+ self,
340
+ values: Mapping[str, Any] | None = None,
341
+ /,
342
+ **fields: Any, # noqa: ANN401
343
+ ) -> ModelT:
338
344
  """Write a new row, and return it as an instance.
339
345
 
340
346
  ```python
341
347
  user = User.query.create(name="ada", team="red")
348
+ user = User.query.create(payload.model_dump())
342
349
  ```
343
350
 
351
+ The fields are keywords, a mapping, or both, where a keyword replaces
352
+ the value of that name.
353
+
344
354
  The row goes through the session, so defaults, relationships and the identity
345
355
  map behave as they do for a model that saves itself. What it adds is a write
346
356
  that needs no model layer: `Query(User, db).create(...)` works on any mapped
347
357
  class.
348
358
  """
349
- instance = self.model(**values)
359
+ instance = self.model(**merged(values, fields))
350
360
  self.db.session.add(instance)
351
361
  self._persist()
352
362
  return instance
@@ -367,9 +377,17 @@ class Query(BaseQuery[ModelT]):
367
377
  self._persist()
368
378
  return len(rows)
369
379
 
370
- def update(self, values: Mapping[str, Any]) -> int:
380
+ def update(
381
+ self,
382
+ values: Mapping[str, Any] | None = None,
383
+ /,
384
+ **fields: Any, # noqa: ANN401
385
+ ) -> int:
371
386
  """Write these values to every matching row, and return how many.
372
387
 
388
+ A mapping, keywords, or both: `update({"team": "green"})` and
389
+ `update(team="green")` write the same statement.
390
+
373
391
  One statement, so the session's objects are updated from the database
374
392
  rather than in memory. Only the narrowing carries over.
375
393
 
@@ -377,7 +395,7 @@ class Query(BaseQuery[ModelT]):
377
395
  BulkQueryError: if the query carries anything a statement drops.
378
396
 
379
397
  """
380
- result = self.db.session.execute(self._update_statement(values))
398
+ result = self.db.session.execute(self._update_statement(merged(values, fields)))
381
399
  self._persist()
382
400
  return cast("CursorResult[Any]", result).rowcount
383
401
 
@@ -5,6 +5,7 @@ from typing import TYPE_CHECKING, Any, TypeVar, cast
5
5
  import sqlalchemy as sa
6
6
  from typing_extensions import Unpack
7
7
 
8
+ from ._query import merged
8
9
  from ._sql import (
9
10
  BaseSQLQuery,
10
11
  Filter,
@@ -14,7 +15,7 @@ from ._sql import (
14
15
  )
15
16
 
16
17
  if TYPE_CHECKING:
17
- from collections.abc import Iterator, Sequence
18
+ from collections.abc import Iterator, Mapping, Sequence
18
19
 
19
20
  from sqlalchemy.engine import Result, ScalarResult
20
21
  from sqlalchemy.sql import Executable
@@ -48,38 +49,60 @@ class SQL:
48
49
  def __repr__(self) -> str:
49
50
  return f"{type(self).__name__}({self.db!r})"
50
51
 
51
- def __call__(self, template: str, /, **context: Any) -> SQLQuery: # noqa: ANN401
52
+ def __call__(
53
+ self,
54
+ template: str,
55
+ /,
56
+ context: Mapping[str, Any] | None = None,
57
+ **values: Any, # noqa: ANN401
58
+ ) -> SQLQuery:
52
59
  """Read the rows of a template. Short for `from_file`.
53
60
 
54
61
  ```python
55
62
  db.sql("users/active.sql", team="red").all()
56
63
  ```
57
64
  """
58
- return self.from_file(template, **context)
59
-
60
- def from_file(self, template: str, /, **context: Any) -> SQLQuery: # noqa: ANN401
65
+ return self.from_file(template, context, **values)
66
+
67
+ def from_file(
68
+ self,
69
+ template: str,
70
+ /,
71
+ context: Mapping[str, Any] | None = None,
72
+ **values: Any, # noqa: ANN401
73
+ ) -> SQLQuery:
61
74
  """Read the rows of a template kept under the database's ``templates=``.
62
75
 
63
76
  ```python
64
77
  db.sql.from_file("users/active.sql", team="red").all()
78
+ db.sql.from_file("users/active.sql", context=filters).all()
65
79
  ```
66
80
 
67
- The keyword arguments are the template's context.
81
+ The keyword arguments are the template's context, and ``context`` takes
82
+ the same values as a mapping, for values a caller was handed rather than
83
+ wrote. A value named `context` lives in that mapping.
68
84
  """
69
- return SQLQuery(self.db, template, context)
70
-
71
- def from_string(self, source: str, /, **context: Any) -> SQLQuery: # noqa: ANN401
85
+ return SQLQuery(self.db, template, merged(context, values))
86
+
87
+ def from_string(
88
+ self,
89
+ source: str,
90
+ /,
91
+ context: Mapping[str, Any] | None = None,
92
+ **values: Any, # noqa: ANN401
93
+ ) -> SQLQuery:
72
94
  """Read the rows of SQL written out here rather than kept in a file.
73
95
 
74
96
  ```python
75
97
  db.sql.from_string("SELECT id FROM users WHERE team = {{ team }}", team="red")
76
98
  ```
77
99
 
78
- Values are named in `{{ }}` and passed by keyword, as in a template. A
79
- `:name` or a `?` binds nothing here, and rendering says so rather than
80
- reaching the driver. It needs no ``templates=``.
100
+ Values are named in `{{ }}` and passed by keyword, or as the ``context``
101
+ mapping, as in a template. A `:name` or a `?` binds nothing here, and
102
+ rendering says so rather than reaching the driver. It needs no
103
+ ``templates=``.
81
104
  """
82
- return SQLQuery(self.db, source, context, inline=True)
105
+ return SQLQuery(self.db, source, merged(context, values), inline=True)
83
106
 
84
107
  def from_statement(self, statement: Executable) -> SQLQuery:
85
108
  """Read the rows of a statement built with SQLAlchemy.
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes