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.
Files changed (51) hide show
  1. {zero_server-0.0.1 → zero_server-0.0.2}/PKG-INFO +10 -11
  2. {zero_server-0.0.1 → zero_server-0.0.2}/README.md +2 -2
  3. {zero_server-0.0.1 → zero_server-0.0.2}/pyproject.toml +11 -14
  4. {zero_server-0.0.1 → zero_server-0.0.2}/pyproject.toml.orig +12 -10
  5. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/__init__.py +1 -1
  6. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/server/adapters/psycopg.py +8 -4
  7. zero_server-0.0.2/src/zero_server/server/adapters/sqlalchemy.py +75 -0
  8. zero_server-0.0.1/src/zero_server/server/adapters/sqlalchemy.py +0 -49
  9. {zero_server-0.0.1 → zero_server-0.0.2}/LICENSE +0 -0
  10. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/codegen.py +0 -0
  11. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/integrations/__init__.py +0 -0
  12. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/integrations/starlette.py +0 -0
  13. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/mutators.py +0 -0
  14. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/protocol/__init__.py +0 -0
  15. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/protocol/application_error.py +0 -0
  16. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/protocol/ast.py +0 -0
  17. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/protocol/custom_queries.py +0 -0
  18. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/protocol/error.py +0 -0
  19. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/protocol/mutate_server.py +0 -0
  20. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/protocol/mutation.py +0 -0
  21. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/protocol/mutation_id.py +0 -0
  22. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/protocol/push.py +0 -0
  23. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/protocol/query_server.py +0 -0
  24. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/protocol/wire.py +0 -0
  25. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/py.typed +0 -0
  26. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/query/__init__.py +0 -0
  27. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/query/builder.py +0 -0
  28. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/query/expression.py +0 -0
  29. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/query/registry.py +0 -0
  30. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/schema.py +0 -0
  31. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/server/__init__.py +0 -0
  32. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/server/adapters/__init__.py +0 -0
  33. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/server/adapters/asyncpg.py +0 -0
  34. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/server/crud.py +0 -0
  35. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/server/pg_query_executor.py +0 -0
  36. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/server/pg_types.py +0 -0
  37. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/server/process_mutations.py +0 -0
  38. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/server/process_queries.py +0 -0
  39. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/server/push_processor.py +0 -0
  40. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/server/server_schema.py +0 -0
  41. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/server/transaction.py +0 -0
  42. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/server/zql_database.py +0 -0
  43. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/shared/__init__.py +0 -0
  44. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/shared/asserts.py +0 -0
  45. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/shared/error.py +0 -0
  46. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/shared/js.py +0 -0
  47. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/shared/json.py +0 -0
  48. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/shared/registry.py +0 -0
  49. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/z2s/__init__.py +0 -0
  50. {zero_server-0.0.1 → zero_server-0.0.2}/src/zero_server/z2s/compiler.py +0 -0
  51. {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.1
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 :: 1 - Planning
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
14
+ Requires-Dist: pydantic>=2.12
15
15
  Requires-Dist: typing-extensions>=4.12
16
- Requires-Dist: asyncpg>=0.32 ; extra == 'asyncpg'
17
- Requires-Dist: psycopg>=3.3 ; extra == 'psycopg'
18
- Requires-Dist: psycopg-pool>=3.3 ; extra == 'psycopg'
19
- Requires-Dist: sqlalchemy[asyncio]>=2.1 ; extra == 'sqlalchemy'
20
- Requires-Dist: asyncpg>=0.32 ; extra == 'sqlalchemy'
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.1"
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 :: 1 - Planning",
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.14",
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
- "sqlalchemy[asyncio]>=2.1",
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.3",
47
- "psycopg-pool>=3.3",
43
+ "psycopg>=3.2.10",
44
+ "psycopg-pool>=3.2.6",
48
45
  ]
49
- starlette = ["starlette>=1.8"]
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.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.8",
61
+ "starlette>=1.6",
65
62
  "uvicorn>=0.54",
66
63
  ]
67
64
 
68
65
  [build-system]
69
- requires = ["uv_build>=0.11,<0.12"]
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.1"
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 :: 1 - Planning",
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
- dependencies = ["pydantic>=2.14", "typing-extensions>=4.12"]
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
- sqlalchemy = ["sqlalchemy[asyncio]>=2.1", "asyncpg>=0.32"]
31
- asyncpg = ["asyncpg>=0.32"]
32
- psycopg = ["psycopg>=3.3", "psycopg-pool>=3.3"]
33
- starlette = ["starlette>=1.8"]
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.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.8", "uvicorn>=0.54"]
52
+ e2e = ["starlette>=1.6", "uvicorn>=0.54"]
51
53
 
52
54
  [build-system]
53
- requires = ["uv_build>=0.11,<0.12"]
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.1"
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
- from psycopg_pool import AsyncConnectionPool
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