sqlakit 0.20.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 (33) hide show
  1. {sqlakit-0.20.0 → sqlakit-0.21.0}/PKG-INFO +14 -12
  2. {sqlakit-0.20.0 → sqlakit-0.21.0}/README.md +11 -7
  3. {sqlakit-0.20.0 → sqlakit-0.21.0}/pyproject.toml +12 -10
  4. {sqlakit-0.20.0 → sqlakit-0.21.0}/pyproject.toml.orig +39 -39
  5. {sqlakit-0.20.0 → sqlakit-0.21.0}/sqlakit/__init__.py +18 -2
  6. sqlakit-0.21.0/sqlakit/_cli.py +288 -0
  7. {sqlakit-0.20.0 → sqlakit-0.21.0}/sqlakit/_db.py +1 -3
  8. sqlakit-0.21.0/sqlakit/_project.py +422 -0
  9. sqlakit-0.21.0/sqlakit/_pycharm.py +156 -0
  10. {sqlakit-0.20.0 → sqlakit-0.21.0}/sqlakit/_query.py +14 -2
  11. sqlakit-0.21.0/sqlakit/_sql.py +3018 -0
  12. sqlakit-0.21.0/sqlakit/_sqruff.py +106 -0
  13. sqlakit-0.21.0/sqlakit/_static.py +544 -0
  14. {sqlakit-0.20.0 → sqlakit-0.21.0}/sqlakit/asyncio/_db.py +1 -3
  15. {sqlakit-0.20.0 → sqlakit-0.21.0}/sqlakit/asyncio/sql.py +22 -9
  16. {sqlakit-0.20.0 → sqlakit-0.21.0}/sqlakit/exceptions.py +171 -19
  17. {sqlakit-0.20.0 → sqlakit-0.21.0}/sqlakit/pytest_plugin.py +22 -3
  18. {sqlakit-0.20.0 → sqlakit-0.21.0}/sqlakit/sql.py +22 -7
  19. {sqlakit-0.20.0 → sqlakit-0.21.0}/sqlakit/types.py +0 -2
  20. sqlakit-0.20.0/sqlakit/_sql.py +0 -461
  21. {sqlakit-0.20.0 → sqlakit-0.21.0}/LICENSE +0 -0
  22. {sqlakit-0.20.0 → sqlakit-0.21.0}/sqlakit/_base.py +0 -0
  23. {sqlakit-0.20.0 → sqlakit-0.21.0}/sqlakit/_discovery.py +0 -0
  24. {sqlakit-0.20.0 → sqlakit-0.21.0}/sqlakit/_model.py +0 -0
  25. {sqlakit-0.20.0 → sqlakit-0.21.0}/sqlakit/_recording.py +0 -0
  26. {sqlakit-0.20.0 → sqlakit-0.21.0}/sqlakit/_registry.py +0 -0
  27. {sqlakit-0.20.0 → sqlakit-0.21.0}/sqlakit/_routing.py +0 -0
  28. {sqlakit-0.20.0 → sqlakit-0.21.0}/sqlakit/asyncio/__init__.py +0 -0
  29. {sqlakit-0.20.0 → sqlakit-0.21.0}/sqlakit/asyncio/_registry.py +0 -0
  30. {sqlakit-0.20.0 → sqlakit-0.21.0}/sqlakit/asyncio/orm.py +0 -0
  31. {sqlakit-0.20.0 → sqlakit-0.21.0}/sqlakit/orm.py +0 -0
  32. {sqlakit-0.20.0 → sqlakit-0.21.0}/sqlakit/py.typed +0 -0
  33. {sqlakit-0.20.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.20.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
 
@@ -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
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlakit"
3
- version = "0.20.0"
3
+ version = "0.21.0"
4
4
  description = "A toolkit for SQLAlchemy applications."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -35,22 +35,20 @@ dependencies = [
35
35
  name = "Anton Ruhlov"
36
36
  email = "antonruhlov@gmail.com"
37
37
 
38
+ [project.scripts]
39
+ sqlakit = "sqlakit._cli:main"
40
+
38
41
  [project.entry-points.pytest11]
39
42
  sqlakit = "sqlakit.pytest_plugin"
40
43
 
41
44
  [project.urls]
42
- Repository = "https://github.com/antonrh/sqlakit"
43
- Documentation = "https://github.com/antonrh/sqlakit"
45
+ Repository = "https://github.com/sqlakit/sqlakit"
46
+ Documentation = "https://sqlakit.readthedocs.io/en/stable/"
44
47
 
45
48
  [project.optional-dependencies]
46
49
  asyncio = ["sqlalchemy[asyncio]>=2.0.22"]
47
- sql = ["jinja2sql>=0.12.0"]
48
50
  debug = ["sqlparse>=0.6.0"]
49
51
 
50
- [build-system]
51
- requires = ["uv_build>=0.11,<0.13"]
52
- build-backend = "uv_build"
53
-
54
52
  [tool.uv]
55
53
  default-groups = [
56
54
  "dev",
@@ -180,7 +178,7 @@ skip = "./.venv,./site,./uv.lock,./.git"
180
178
  [dependency-groups]
181
179
  dev = [
182
180
  "pytest>=9.1.1",
183
- "ty>=0.0.80",
181
+ "ty>=0.0.84",
184
182
  "ruff>=0.16.6",
185
183
  "poethepoet>=0.48.0",
186
184
  "anyio>=4.14.2",
@@ -191,7 +189,6 @@ dev = [
191
189
  "httpx>=0.28.1",
192
190
  "httpx2>=2.10.0",
193
191
  "sqlmodel>=0.0.39",
194
- "jinja2sql>=0.12.0",
195
192
  "pydantic>=2.13.4",
196
193
  "sqlparse>=0.6.0",
197
194
  "rich>=15.0.0",
@@ -203,6 +200,7 @@ dev = [
203
200
  "flask>=3.1.3",
204
201
  "dirty-equals>=0.11",
205
202
  "codespell>=2.4.3",
203
+ "sqruff>=0.40.0",
206
204
  ]
207
205
  docs = [
208
206
  "mkdocs>=1.6.1,<2",
@@ -210,3 +208,7 @@ docs = [
210
208
  "mkdocstrings[python]>=1.0.6",
211
209
  "ruff>=0.16.6",
212
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.20.0"
3
+ version = "0.21.0"
4
4
  description = "A toolkit for SQLAlchemy applications."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -26,28 +26,24 @@ dependencies = [
26
26
  "typing-extensions>=4.13",
27
27
  ]
28
28
 
29
+ [project.scripts]
30
+ sqlakit = "sqlakit._cli:main"
31
+
29
32
  [project.entry-points.pytest11]
30
33
  sqlakit = "sqlakit.pytest_plugin"
31
34
 
32
35
  [project.urls]
33
- Repository = "https://github.com/antonrh/sqlakit"
34
- Documentation = "https://github.com/antonrh/sqlakit"
36
+ Repository = "https://github.com/sqlakit/sqlakit"
37
+ Documentation = "https://sqlakit.readthedocs.io/en/stable/"
35
38
 
36
39
  [project.optional-dependencies]
37
40
  asyncio = [
38
41
  "sqlalchemy[asyncio]>=2.0.22",
39
42
  ]
40
- sql = [
41
- "jinja2sql>=0.12.0",
42
- ]
43
43
  debug = [
44
44
  "sqlparse>=0.6.0",
45
45
  ]
46
46
 
47
- [build-system]
48
- requires = ["uv_build>=0.11,<0.13"]
49
- build-backend = "uv_build"
50
-
51
47
  [tool.uv.build-backend]
52
48
  module-name = "sqlakit"
53
49
  module-root = ""
@@ -58,7 +54,7 @@ default-groups = ["dev", "docs"]
58
54
  [dependency-groups]
59
55
  dev = [
60
56
  "pytest>=9.1.1",
61
- "ty>=0.0.80",
57
+ "ty>=0.0.84",
62
58
  "ruff>=0.16.6",
63
59
  "poethepoet>=0.48.0",
64
60
  "anyio>=4.14.2",
@@ -69,7 +65,6 @@ dev = [
69
65
  "httpx>=0.28.1",
70
66
  "httpx2>=2.10.0",
71
67
  "sqlmodel>=0.0.39",
72
- "jinja2sql>=0.12.0",
73
68
  "pydantic>=2.13.4",
74
69
  "sqlparse>=0.6.0",
75
70
  "rich>=15.0.0",
@@ -81,6 +76,7 @@ dev = [
81
76
  "flask>=3.1.3",
82
77
  "dirty-equals>=0.11",
83
78
  "codespell>=2.4.3",
79
+ "sqruff>=0.40.0",
84
80
  ]
85
81
  docs = [
86
82
  "mkdocs>=1.6.1,<2",
@@ -98,8 +94,8 @@ filterwarnings = [
98
94
  addopts = "-p pytester"
99
95
  # The example is a project of its own, run by tests/examples, not collected here.
100
96
  norecursedirs = [
101
- "examples/testing_plugin", # a project of its own, run by tests/examples
102
- "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
103
99
  ".*",
104
100
  "build",
105
101
  "dist",
@@ -134,39 +130,39 @@ ignore = [
134
130
 
135
131
  [tool.ruff.lint.per-file-ignores]
136
132
  "tests/**/*.py" = [
137
- "S101", # tests assert
138
- "PLR2004", # and compare against the values they mean
139
- "SLF001", # they read the internals they are testing
140
- "PT012", # a `raises` block is allowed to set the scene
141
- "B018", # touching an attribute is how a test triggers it
142
- "SIM117", # nested `with` reads better than a long line of them
143
- "PLC0415", # a fixture imports what it is about, where it is about it
144
- "DTZ001", # naive datetimes are fine as fixtures
145
- "ARG001", # a fixture may be requested for its side effect
146
- "PT011", # `raises` without `match` is enough for our own errors
147
- "ANN401", # test helpers take whatever the example hands them
148
- "D401", # test docstrings read as descriptions
149
- "N818", # a test's own exception needs no Error suffix
150
- "FBT001", # parametrised booleans arrive positionally
151
- "PLW0108", # a lambda in a parameter table is deliberate
152
- "DTZ005", # naive datetimes are fine as fixtures
153
- "S106", # the passwords here are fake, and the point of the test
154
- "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
155
151
  ]
156
152
  "tools/**/*.py" = [
157
- "INP001", # a script, not a package
158
- "T201", # a script that says what it found
153
+ "INP001", # a script, not a package
154
+ "T201", # a script that says what it found
159
155
  ]
160
156
  "examples/**/*.py" = [
161
- "INP001", # examples are read one file at a time, not imported as a package
162
- "ARG001", # FastAPI hands handlers a request they may not use
163
- "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
164
160
  ]
165
161
  "examples/testing_plugin/*.py" = [
166
- "S101", # the example is a test suite, and tests assert
162
+ "S101", # the example is a test suite, and tests assert
167
163
  ]
168
164
  "tests/projects/**/*.py" = [
169
- "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
170
166
  ]
171
167
 
172
168
  [tool.ty.src]
@@ -209,3 +205,7 @@ default_item_type = "cmd"
209
205
  # `froms` is SQLAlchemy's own word for what a SELECT selects from.
210
206
  ignore-words-list = "froms"
211
207
  skip = "./.venv,./site,./uv.lock,./.git"
208
+
209
+ [build-system]
210
+ requires = ["uv_build>=0.11,<0.13"]
211
+ build-backend = "uv_build"
@@ -15,7 +15,6 @@ from ._routing import Router
15
15
  from .exceptions import (
16
16
  DEFAULT_ALIAS,
17
17
  AliasInUseError,
18
- AsyncFilterError,
19
18
  BulkQueryError,
20
19
  ConflictingDatabaseUrlError,
21
20
  ConflictingJoinError,
@@ -24,12 +23,17 @@ from .exceptions import (
24
23
  DefaultAliasError,
25
24
  DetachedInstanceError,
26
25
  DuplicateKeyError,
26
+ InlineValueError,
27
27
  InstanceNotFoundError,
28
28
  InvalidCursorError,
29
29
  InvalidDatabaseConfigError,
30
30
  InvalidNullsError,
31
31
  InvalidOrderFieldError,
32
+ InvalidSortStringError,
32
33
  KeyLookupError,
34
+ MacroArgumentError,
35
+ MacroDefinitionError,
36
+ MacroSyntaxError,
33
37
  MissingConnectionError,
34
38
  MissingDatabaseUrlError,
35
39
  MissingDefaultDatabaseError,
@@ -39,6 +43,8 @@ from .exceptions import (
39
43
  MultipleInstancesFoundError,
40
44
  NullCursorValueError,
41
45
  PageItemsMismatchError,
46
+ ParameterPathError,
47
+ ProjectConfigError,
42
48
  RawStatementError,
43
49
  RetryNotSupportedError,
44
50
  SQLAKitError,
@@ -49,7 +55,9 @@ from .exceptions import (
49
55
  UncomparableOrderingError,
50
56
  UnknownDatabaseError,
51
57
  UnknownFieldError,
58
+ UnknownIdentifierError,
52
59
  UnknownImportPathError,
60
+ UnknownMacroError,
53
61
  UnknownOrderFieldError,
54
62
  UnorderedPageError,
55
63
  UnregisteredDatabaseError,
@@ -70,7 +78,6 @@ __all__ = [
70
78
  "DEFAULT_ENGINE_ARGS",
71
79
  "DEFAULT_SESSION_ARGS",
72
80
  "AliasInUseError",
73
- "AsyncFilterError",
74
81
  "BulkQueryError",
75
82
  "ConflictingDatabaseUrlError",
76
83
  "ConflictingJoinError",
@@ -84,12 +91,17 @@ __all__ = [
84
91
  "DetachedInstanceError",
85
92
  "DuplicateKeyError",
86
93
  "EngineArgs",
94
+ "InlineValueError",
87
95
  "InstanceNotFoundError",
88
96
  "InvalidCursorError",
89
97
  "InvalidDatabaseConfigError",
90
98
  "InvalidNullsError",
91
99
  "InvalidOrderFieldError",
100
+ "InvalidSortStringError",
92
101
  "KeyLookupError",
102
+ "MacroArgumentError",
103
+ "MacroDefinitionError",
104
+ "MacroSyntaxError",
93
105
  "MissingConnectionError",
94
106
  "MissingDatabaseUrlError",
95
107
  "MissingDefaultDatabaseError",
@@ -101,6 +113,8 @@ __all__ = [
101
113
  "OrderBy",
102
114
  "Page",
103
115
  "PageItemsMismatchError",
116
+ "ParameterPathError",
117
+ "ProjectConfigError",
104
118
  "QueryStats",
105
119
  "RawStatementError",
106
120
  "Recording",
@@ -120,7 +134,9 @@ __all__ = [
120
134
  "UncountedPage",
121
135
  "UnknownDatabaseError",
122
136
  "UnknownFieldError",
137
+ "UnknownIdentifierError",
123
138
  "UnknownImportPathError",
139
+ "UnknownMacroError",
124
140
  "UnknownOrderFieldError",
125
141
  "UnorderedPageError",
126
142
  "UnregisteredDatabaseError",