zero-server 0.0.1__tar.gz → 0.0.2__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.
- {zero_server-0.0.1 → zero_server-0.0.2}/PKG-INFO +10 -11
- {zero_server-0.0.1 → zero_server-0.0.2}/README.md +2 -2
- {zero_server-0.0.1 → zero_server-0.0.2}/pyproject.toml +11 -14
- {zero_server-0.0.1 → zero_server-0.0.2}/pyproject.toml.orig +12 -10
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/__init__.py +1 -1
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/server/adapters/psycopg.py +8 -4
- zero_server-0.0.2/src/zero_server/server/adapters/sqlalchemy.py +75 -0
- zero_server-0.0.1/src/zero_server/server/adapters/sqlalchemy.py +0 -49
- {zero_server-0.0.1 → zero_server-0.0.2}/LICENSE +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/codegen.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/integrations/__init__.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/integrations/starlette.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/mutators.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/protocol/__init__.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/protocol/application_error.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/protocol/ast.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/protocol/custom_queries.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/protocol/error.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/protocol/mutate_server.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/protocol/mutation.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/protocol/mutation_id.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/protocol/push.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/protocol/query_server.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/protocol/wire.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/py.typed +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/query/__init__.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/query/builder.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/query/expression.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/query/registry.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/schema.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/server/__init__.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/server/adapters/__init__.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/server/adapters/asyncpg.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/server/crud.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/server/pg_query_executor.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/server/pg_types.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/server/process_mutations.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/server/process_queries.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/server/push_processor.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/server/server_schema.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/server/transaction.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/server/zql_database.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/shared/__init__.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/shared/asserts.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/shared/error.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/shared/js.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/shared/json.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/shared/registry.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/z2s/__init__.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/z2s/compiler.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/z2s/sql.py +0 -0
|
@@ -1,24 +1,23 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: zero-server
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.2
|
|
4
4
|
Summary: Community Python server SDK for Rocicorp Zero. Not affiliated with Rocicorp.
|
|
5
5
|
Keywords: zero,rocicorp,sync,zql
|
|
6
6
|
Author: Agent Labs
|
|
7
7
|
License-Expression: Apache-2.0
|
|
8
8
|
License-File: LICENSE
|
|
9
|
-
Classifier: Development Status ::
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.12
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.13
|
|
13
13
|
Classifier: Typing :: Typed
|
|
14
|
-
Requires-Dist: pydantic>=2.
|
|
14
|
+
Requires-Dist: pydantic>=2.12
|
|
15
15
|
Requires-Dist: typing-extensions>=4.12
|
|
16
|
-
Requires-Dist: asyncpg>=0.
|
|
17
|
-
Requires-Dist: psycopg>=3.
|
|
18
|
-
Requires-Dist: psycopg-pool>=3.
|
|
19
|
-
Requires-Dist: sqlalchemy[asyncio]>=2.
|
|
20
|
-
Requires-Dist:
|
|
21
|
-
Requires-Dist: starlette>=1.8 ; extra == 'starlette'
|
|
16
|
+
Requires-Dist: asyncpg>=0.30 ; extra == 'asyncpg'
|
|
17
|
+
Requires-Dist: psycopg>=3.2.10 ; extra == 'psycopg'
|
|
18
|
+
Requires-Dist: psycopg-pool>=3.2.6 ; extra == 'psycopg'
|
|
19
|
+
Requires-Dist: sqlalchemy[asyncio]>=2.0.40 ; extra == 'sqlalchemy'
|
|
20
|
+
Requires-Dist: starlette>=1.6 ; extra == 'starlette'
|
|
22
21
|
Requires-Python: >=3.12
|
|
23
22
|
Project-URL: Homepage, https://github.com/agent-labs-dev/zero-python
|
|
24
23
|
Project-URL: Source, https://github.com/agent-labs-dev/zero-python
|
|
@@ -41,7 +40,7 @@ Compatibility is pinned to one Zero release at a time. This version is tested ag
|
|
|
41
40
|
## Usage
|
|
42
41
|
|
|
43
42
|
```sh
|
|
44
|
-
pip install "zero-server[sqlalchemy,starlette]" # or [asyncpg], or [psycopg]
|
|
43
|
+
pip install "zero-server[sqlalchemy,asyncpg,starlette]" # or [asyncpg], or [psycopg]
|
|
45
44
|
```
|
|
46
45
|
|
|
47
46
|
The schema stays in your TypeScript `schema.ts`, because the client needs it. Emit it as JSON, and generate Python types from it:
|
|
@@ -108,7 +107,7 @@ app = Starlette(
|
|
|
108
107
|
|
|
109
108
|
Point `ZERO_QUERY_URL` and `ZERO_MUTATE_URL` at `/query` and `/mutate`. With FastAPI, `app.router.routes.extend(zero_routes(...))`. With another framework, call `handle_query_request` and `handle_mutate_request` yourself; `zero_server/integrations/starlette.py` is short enough to copy.
|
|
110
109
|
|
|
111
|
-
- **Databases.** `SQLAlchemyDatabase` (SQLAlchemy's asyncio engine on asyncpg), `AsyncpgDatabase` (an asyncpg pool) and `PsycopgDatabase` (a psycopg 3 async pool), under `zero_server.server.adapters`. Another driver plugs into `ZQLDatabase` as a `DBConnection` whose `query` takes `$1` placeholders.
|
|
110
|
+
- **Databases.** `SQLAlchemyDatabase` (SQLAlchemy's asyncio engine on asyncpg or psycopg; the `sqlalchemy` extra installs neither driver), `AsyncpgDatabase` (an asyncpg pool) and `PsycopgDatabase` (a psycopg 3 async pool), under `zero_server.server.adapters`. Another driver plugs into `ZQLDatabase` as a `DBConnection` whose `query` takes `$1` placeholders.
|
|
112
111
|
- **Validation.** `@define_query(Model)` and `@define_mutator(Model)` parse args with a Pydantic model or `TypeAdapter` first; a failure reaches the client as upstream's validation error.
|
|
113
112
|
- **Strict building.** `create_builder(schema, strict=True)` refuses an unknown column when the query is built. Upstream only refuses it when the endpoint maps the query, failing the whole batch, so this is for your tests.
|
|
114
113
|
|
|
@@ -9,7 +9,7 @@ Compatibility is pinned to one Zero release at a time. This version is tested ag
|
|
|
9
9
|
## Usage
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
|
-
pip install "zero-server[sqlalchemy,starlette]" # or [asyncpg], or [psycopg]
|
|
12
|
+
pip install "zero-server[sqlalchemy,asyncpg,starlette]" # or [asyncpg], or [psycopg]
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
The schema stays in your TypeScript `schema.ts`, because the client needs it. Emit it as JSON, and generate Python types from it:
|
|
@@ -76,7 +76,7 @@ app = Starlette(
|
|
|
76
76
|
|
|
77
77
|
Point `ZERO_QUERY_URL` and `ZERO_MUTATE_URL` at `/query` and `/mutate`. With FastAPI, `app.router.routes.extend(zero_routes(...))`. With another framework, call `handle_query_request` and `handle_mutate_request` yourself; `zero_server/integrations/starlette.py` is short enough to copy.
|
|
78
78
|
|
|
79
|
-
- **Databases.** `SQLAlchemyDatabase` (SQLAlchemy's asyncio engine on asyncpg), `AsyncpgDatabase` (an asyncpg pool) and `PsycopgDatabase` (a psycopg 3 async pool), under `zero_server.server.adapters`. Another driver plugs into `ZQLDatabase` as a `DBConnection` whose `query` takes `$1` placeholders.
|
|
79
|
+
- **Databases.** `SQLAlchemyDatabase` (SQLAlchemy's asyncio engine on asyncpg or psycopg; the `sqlalchemy` extra installs neither driver), `AsyncpgDatabase` (an asyncpg pool) and `PsycopgDatabase` (a psycopg 3 async pool), under `zero_server.server.adapters`. Another driver plugs into `ZQLDatabase` as a `DBConnection` whose `query` takes `$1` placeholders.
|
|
80
80
|
- **Validation.** `@define_query(Model)` and `@define_mutator(Model)` parse args with a Pydantic model or `TypeAdapter` first; a failure reaches the client as upstream's validation error.
|
|
81
81
|
- **Strict building.** `create_builder(schema, strict=True)` refuses an unknown column when the query is built. Upstream only refuses it when the endpoint maps the query, failing the whole batch, so this is for your tests.
|
|
82
82
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "zero-server"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.2"
|
|
4
4
|
description = "Community Python server SDK for Rocicorp Zero. Not affiliated with Rocicorp."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -13,14 +13,14 @@ keywords = [
|
|
|
13
13
|
"zql",
|
|
14
14
|
]
|
|
15
15
|
classifiers = [
|
|
16
|
-
"Development Status ::
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
17
|
"Programming Language :: Python :: 3",
|
|
18
18
|
"Programming Language :: Python :: 3.12",
|
|
19
19
|
"Programming Language :: Python :: 3.13",
|
|
20
20
|
"Typing :: Typed",
|
|
21
21
|
]
|
|
22
22
|
dependencies = [
|
|
23
|
-
"pydantic>=2.
|
|
23
|
+
"pydantic>=2.12",
|
|
24
24
|
"typing-extensions>=4.12",
|
|
25
25
|
]
|
|
26
26
|
|
|
@@ -37,22 +37,19 @@ Changelog = "https://github.com/agent-labs-dev/zero-python/releases"
|
|
|
37
37
|
zero-server = "zero_server.codegen:main"
|
|
38
38
|
|
|
39
39
|
[project.optional-dependencies]
|
|
40
|
-
sqlalchemy = [
|
|
41
|
-
|
|
42
|
-
"asyncpg>=0.32",
|
|
43
|
-
]
|
|
44
|
-
asyncpg = ["asyncpg>=0.32"]
|
|
40
|
+
sqlalchemy = ["sqlalchemy[asyncio]>=2.0.40"]
|
|
41
|
+
asyncpg = ["asyncpg>=0.30"]
|
|
45
42
|
psycopg = [
|
|
46
|
-
"psycopg>=3.
|
|
47
|
-
"psycopg-pool>=3.
|
|
43
|
+
"psycopg>=3.2.10",
|
|
44
|
+
"psycopg-pool>=3.2.6",
|
|
48
45
|
]
|
|
49
|
-
starlette = ["starlette>=1.
|
|
46
|
+
starlette = ["starlette>=1.6"]
|
|
50
47
|
|
|
51
48
|
[dependency-groups]
|
|
52
49
|
dev = [
|
|
53
50
|
"zero-server[sqlalchemy,asyncpg,psycopg,starlette]",
|
|
54
51
|
"httpx2>=2.13",
|
|
55
|
-
"psycopg[binary]>=3.
|
|
52
|
+
"psycopg[binary]>=3.2.10",
|
|
56
53
|
"hypothesis>=6.169",
|
|
57
54
|
"pytest>=9.1",
|
|
58
55
|
"pytest-asyncio>=1.4",
|
|
@@ -61,12 +58,12 @@ dev = [
|
|
|
61
58
|
"ty>=0.0.87",
|
|
62
59
|
]
|
|
63
60
|
e2e = [
|
|
64
|
-
"starlette>=1.
|
|
61
|
+
"starlette>=1.6",
|
|
65
62
|
"uvicorn>=0.54",
|
|
66
63
|
]
|
|
67
64
|
|
|
68
65
|
[build-system]
|
|
69
|
-
requires = ["uv_build>=0.11,<0.
|
|
66
|
+
requires = ["uv_build>=0.11,<0.14"]
|
|
70
67
|
build-backend = "uv_build"
|
|
71
68
|
|
|
72
69
|
[tool.ruff]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "zero-server"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.2"
|
|
4
4
|
description = "Community Python server SDK for Rocicorp Zero. Not affiliated with Rocicorp."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -9,13 +9,14 @@ requires-python = ">=3.12"
|
|
|
9
9
|
authors = [{ name = "Agent Labs" }]
|
|
10
10
|
keywords = ["zero", "rocicorp", "sync", "zql"]
|
|
11
11
|
classifiers = [
|
|
12
|
-
"Development Status ::
|
|
12
|
+
"Development Status :: 3 - Alpha",
|
|
13
13
|
"Programming Language :: Python :: 3",
|
|
14
14
|
"Programming Language :: Python :: 3.12",
|
|
15
15
|
"Programming Language :: Python :: 3.13",
|
|
16
16
|
"Typing :: Typed",
|
|
17
17
|
]
|
|
18
|
-
|
|
18
|
+
# Floors are the lowest versions the suite passes on, and CI's `floors` job runs it at exactly these.
|
|
19
|
+
dependencies = ["pydantic>=2.12", "typing-extensions>=4.12"]
|
|
19
20
|
|
|
20
21
|
[project.urls]
|
|
21
22
|
Homepage = "https://github.com/agent-labs-dev/zero-python"
|
|
@@ -27,17 +28,18 @@ Changelog = "https://github.com/agent-labs-dev/zero-python/releases"
|
|
|
27
28
|
zero-server = "zero_server.codegen:main"
|
|
28
29
|
|
|
29
30
|
[project.optional-dependencies]
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
# The engine's driver is the caller's: add `asyncpg`, or install psycopg yourself.
|
|
32
|
+
sqlalchemy = ["sqlalchemy[asyncio]>=2.0.40"]
|
|
33
|
+
asyncpg = ["asyncpg>=0.30"]
|
|
34
|
+
psycopg = ["psycopg>=3.2.10", "psycopg-pool>=3.2.6"]
|
|
35
|
+
starlette = ["starlette>=1.6"]
|
|
34
36
|
|
|
35
37
|
[dependency-groups]
|
|
36
38
|
dev = [
|
|
37
39
|
"zero-server[sqlalchemy,asyncpg,psycopg,starlette]",
|
|
38
40
|
"httpx2>=2.13",
|
|
39
41
|
# psycopg's prebuilt libpq, so the tests need no system libpq.
|
|
40
|
-
"psycopg[binary]>=3.
|
|
42
|
+
"psycopg[binary]>=3.2.10",
|
|
41
43
|
"hypothesis>=6.169",
|
|
42
44
|
"pytest>=9.1",
|
|
43
45
|
"pytest-asyncio>=1.4",
|
|
@@ -47,10 +49,10 @@ dev = [
|
|
|
47
49
|
]
|
|
48
50
|
|
|
49
51
|
# The Python app server of the end-to-end tier.
|
|
50
|
-
e2e = ["starlette>=1.
|
|
52
|
+
e2e = ["starlette>=1.6", "uvicorn>=0.54"]
|
|
51
53
|
|
|
52
54
|
[build-system]
|
|
53
|
-
requires = ["uv_build>=0.11,<0.
|
|
55
|
+
requires = ["uv_build>=0.11,<0.14"]
|
|
54
56
|
build-backend = "uv_build"
|
|
55
57
|
|
|
56
58
|
[tool.ruff]
|
|
@@ -13,7 +13,7 @@ from zero_server.server.process_mutations import handle_mutate_request
|
|
|
13
13
|
from zero_server.server.process_queries import handle_query_request
|
|
14
14
|
from zero_server.server.push_processor import PushProcessor
|
|
15
15
|
|
|
16
|
-
__version__ = "0.0.
|
|
16
|
+
__version__ = "0.0.2"
|
|
17
17
|
|
|
18
18
|
__all__ = [
|
|
19
19
|
"ApplicationError",
|
|
@@ -5,11 +5,15 @@ same text the other adapters send.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from collections.abc import Awaitable, Callable, Sequence
|
|
8
|
-
from typing import Any
|
|
8
|
+
from typing import TYPE_CHECKING, Any
|
|
9
9
|
|
|
10
10
|
import psycopg
|
|
11
11
|
from psycopg.rows import dict_row
|
|
12
|
-
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
# Only `PsycopgDatabase` takes a pool. The SQLAlchemy adapter reuses `PsycopgTransaction` on
|
|
15
|
+
# SQLAlchemy's own connection, and must not need psycopg-pool installed for it.
|
|
16
|
+
from psycopg_pool import AsyncConnectionPool
|
|
13
17
|
|
|
14
18
|
from zero_server.schema import Schema
|
|
15
19
|
from zero_server.server.transaction import DBTransaction, Row
|
|
@@ -40,7 +44,7 @@ class PsycopgTransaction:
|
|
|
40
44
|
|
|
41
45
|
|
|
42
46
|
class PsycopgConnection:
|
|
43
|
-
def __init__(self, pool: AsyncConnectionPool) -> None:
|
|
47
|
+
def __init__(self, pool: "AsyncConnectionPool") -> None:
|
|
44
48
|
self.pool = pool
|
|
45
49
|
|
|
46
50
|
async def transaction[R](self, fn: Callable[[DBTransaction[psycopg.AsyncConnection[Any]]], Awaitable[R]]) -> R:
|
|
@@ -51,5 +55,5 @@ class PsycopgConnection:
|
|
|
51
55
|
class PsycopgDatabase(ZQLDatabase[psycopg.AsyncConnection[Any]]):
|
|
52
56
|
"""A `ZQLDatabase` on a psycopg pool (`AsyncConnectionPool(conninfo, open=False)`, then `await pool.open()`)."""
|
|
53
57
|
|
|
54
|
-
def __init__(self, pool: AsyncConnectionPool, schema: Schema | None = None) -> None:
|
|
58
|
+
def __init__(self, pool: "AsyncConnectionPool", schema: Schema | None = None) -> None:
|
|
55
59
|
super().__init__(PsycopgConnection(pool), schema)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""SQLAlchemy's asyncio engine, with the asyncpg or the psycopg driver.
|
|
2
|
+
|
|
3
|
+
Needs the `zero-server[sqlalchemy]` extra, and the driver the engine's URL names.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from collections.abc import Awaitable, Callable, Sequence
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from sqlalchemy.exc import DBAPIError
|
|
10
|
+
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine
|
|
11
|
+
|
|
12
|
+
from zero_server.schema import Schema
|
|
13
|
+
from zero_server.server.transaction import DBTransaction, Row
|
|
14
|
+
from zero_server.server.zql_database import PostgresError, ZQLDatabase
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SQLAlchemyTransaction:
|
|
18
|
+
"""One transaction of an `AsyncConnection`.
|
|
19
|
+
|
|
20
|
+
On asyncpg a statement goes through `exec_driver_sql`, which hands the `$1` placeholders to asyncpg
|
|
21
|
+
as they are. psycopg's paramstyle is `%s`, so on psycopg it goes through psycopg's raw cursor on the
|
|
22
|
+
same connection, inside the transaction SQLAlchemy opened.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, connection: AsyncConnection, driver_transaction: DBTransaction[Any] | None = None) -> None:
|
|
26
|
+
self._connection = connection
|
|
27
|
+
self._driver_transaction = driver_transaction
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def wrapped_transaction(self) -> AsyncConnection:
|
|
31
|
+
return self._connection
|
|
32
|
+
|
|
33
|
+
async def query(self, sql: str, params: Sequence[Any] = ()) -> list[Row]:
|
|
34
|
+
if self._driver_transaction is not None:
|
|
35
|
+
return await self._driver_transaction.query(sql, params)
|
|
36
|
+
try:
|
|
37
|
+
result = await self._connection.exec_driver_sql(sql, tuple(params))
|
|
38
|
+
except DBAPIError as error:
|
|
39
|
+
cause = error.orig.__cause__ if error.orig is not None and error.orig.__cause__ is not None else error.orig
|
|
40
|
+
# asyncpg's `message` is the primary message alone, which is all postgres.js reports.
|
|
41
|
+
message = getattr(cause, "message", None)
|
|
42
|
+
raise PostgresError(message if isinstance(message, str) else str(cause)) from error
|
|
43
|
+
return [dict(row) for row in result.mappings()] if result.returns_rows else []
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def sqlalchemy_transaction(connection: AsyncConnection) -> SQLAlchemyTransaction:
|
|
47
|
+
"""Wrap a connection that is already in a transaction, such as one an `AsyncSession` holds."""
|
|
48
|
+
driver = connection.dialect.driver
|
|
49
|
+
if driver == "asyncpg":
|
|
50
|
+
return SQLAlchemyTransaction(connection)
|
|
51
|
+
if driver == "psycopg":
|
|
52
|
+
from zero_server.server.adapters.psycopg import PsycopgTransaction
|
|
53
|
+
|
|
54
|
+
driver_connection = (await connection.get_raw_connection()).driver_connection
|
|
55
|
+
# `None` only once SQLAlchemy has invalidated the connection, which it never does mid-transaction.
|
|
56
|
+
assert driver_connection is not None
|
|
57
|
+
return SQLAlchemyTransaction(connection, PsycopgTransaction(driver_connection))
|
|
58
|
+
raise ValueError(f"SQLAlchemy driver {driver!r} is not supported; use asyncpg or psycopg")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class SQLAlchemyConnection:
|
|
62
|
+
def __init__(self, engine: AsyncEngine) -> None:
|
|
63
|
+
self.engine = engine
|
|
64
|
+
|
|
65
|
+
async def transaction[R](self, fn: Callable[[DBTransaction[AsyncConnection]], Awaitable[R]]) -> R:
|
|
66
|
+
async with self.engine.begin() as connection:
|
|
67
|
+
return await fn(await sqlalchemy_transaction(connection))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class SQLAlchemyDatabase(ZQLDatabase[AsyncConnection]):
|
|
71
|
+
"""A `ZQLDatabase` on `engine`, whose URL names the asyncpg or the psycopg driver
|
|
72
|
+
(`postgresql+asyncpg://` or `postgresql+psycopg://`)."""
|
|
73
|
+
|
|
74
|
+
def __init__(self, engine: AsyncEngine, schema: Schema | None = None) -> None:
|
|
75
|
+
super().__init__(SQLAlchemyConnection(engine), schema)
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
"""SQLAlchemy's asyncio engine, with the asyncpg driver.
|
|
2
|
-
|
|
3
|
-
Needs the `zero-server[sqlalchemy]` extra.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
from collections.abc import Awaitable, Callable, Sequence
|
|
7
|
-
from typing import Any
|
|
8
|
-
|
|
9
|
-
from sqlalchemy.exc import DBAPIError
|
|
10
|
-
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine
|
|
11
|
-
|
|
12
|
-
from zero_server.schema import Schema
|
|
13
|
-
from zero_server.server.transaction import DBTransaction, Row
|
|
14
|
-
from zero_server.server.zql_database import PostgresError, ZQLDatabase
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class SQLAlchemyTransaction:
|
|
18
|
-
def __init__(self, connection: AsyncConnection) -> None:
|
|
19
|
-
self._connection = connection
|
|
20
|
-
|
|
21
|
-
@property
|
|
22
|
-
def wrapped_transaction(self) -> AsyncConnection:
|
|
23
|
-
return self._connection
|
|
24
|
-
|
|
25
|
-
async def query(self, sql: str, params: Sequence[Any] = ()) -> list[Row]:
|
|
26
|
-
try:
|
|
27
|
-
result = await self._connection.exec_driver_sql(sql, tuple(params))
|
|
28
|
-
except DBAPIError as error:
|
|
29
|
-
cause = error.orig.__cause__ if error.orig is not None and error.orig.__cause__ is not None else error.orig
|
|
30
|
-
# asyncpg's `message` is the primary message alone, which is all postgres.js reports.
|
|
31
|
-
message = getattr(cause, "message", None)
|
|
32
|
-
raise PostgresError(message if isinstance(message, str) else str(cause)) from error
|
|
33
|
-
return [dict(row) for row in result.mappings()] if result.returns_rows else []
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class SQLAlchemyConnection:
|
|
37
|
-
def __init__(self, engine: AsyncEngine) -> None:
|
|
38
|
-
self.engine = engine
|
|
39
|
-
|
|
40
|
-
async def transaction[R](self, fn: Callable[[DBTransaction[AsyncConnection]], Awaitable[R]]) -> R:
|
|
41
|
-
async with self.engine.begin() as connection:
|
|
42
|
-
return await fn(SQLAlchemyTransaction(connection))
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
class SQLAlchemyDatabase(ZQLDatabase[AsyncConnection]):
|
|
46
|
-
"""A `ZQLDatabase` on `engine`, which must use the asyncpg driver (`postgresql+asyncpg://`)."""
|
|
47
|
-
|
|
48
|
-
def __init__(self, engine: AsyncEngine, schema: Schema | None = None) -> None:
|
|
49
|
-
super().__init__(SQLAlchemyConnection(engine), schema)
|
|
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
|
|
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
|
|
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
|