zero-server 0.0.1__tar.gz → 0.0.3__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.3}/PKG-INFO +19 -14
- {zero_server-0.0.1 → zero_server-0.0.3}/README.md +11 -5
- {zero_server-0.0.1 → zero_server-0.0.3}/pyproject.toml +13 -15
- {zero_server-0.0.1 → zero_server-0.0.3}/pyproject.toml.orig +14 -11
- zero_server-0.0.3/src/zero_server/__init__.py +73 -0
- {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/integrations/starlette.py +15 -7
- {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/mutators.py +5 -5
- {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/protocol/application_error.py +5 -6
- zero_server-0.0.3/src/zero_server/protocol/ast.py +533 -0
- zero_server-0.0.3/src/zero_server/protocol/ast_wire.py +149 -0
- {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/protocol/custom_queries.py +18 -10
- {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/protocol/error.py +22 -8
- {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/protocol/mutate_server.py +12 -1
- {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/protocol/mutation.py +14 -9
- {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/protocol/push.py +4 -4
- zero_server-0.0.3/src/zero_server/protocol/query_server.py +43 -0
- zero_server-0.0.3/src/zero_server/protocol/schemas.py +100 -0
- zero_server-0.0.3/src/zero_server/protocol/wire.py +286 -0
- {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/query/builder.py +130 -72
- {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/query/expression.py +41 -35
- {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/query/registry.py +25 -15
- {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/schema.py +40 -51
- zero_server-0.0.3/src/zero_server/server/adapters/_session.py +68 -0
- {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/server/adapters/asyncpg.py +20 -6
- zero_server-0.0.3/src/zero_server/server/adapters/psycopg.py +87 -0
- zero_server-0.0.3/src/zero_server/server/adapters/sqlalchemy.py +120 -0
- {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/server/crud.py +74 -17
- {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/server/pg_query_executor.py +13 -7
- {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/server/pg_types.py +2 -0
- {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/server/process_mutations.py +121 -84
- zero_server-0.0.3/src/zero_server/server/process_queries.py +161 -0
- {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/server/push_processor.py +3 -2
- {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/server/server_schema.py +3 -3
- {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/server/transaction.py +3 -2
- {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/server/zql_database.py +89 -8
- zero_server-0.0.3/src/zero_server/shared/error.py +78 -0
- {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/shared/js.py +79 -13
- {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/shared/json.py +3 -5
- zero_server-0.0.3/src/zero_server/shared/json_parse.py +320 -0
- {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/shared/registry.py +10 -2
- zero_server-0.0.3/src/zero_server/shared/valita.py +412 -0
- {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/z2s/compiler.py +211 -95
- zero_server-0.0.3/src/zero_server/z2s/sql.py +226 -0
- zero_server-0.0.1/src/zero_server/__init__.py +0 -36
- zero_server-0.0.1/src/zero_server/protocol/ast.py +0 -342
- zero_server-0.0.1/src/zero_server/protocol/query_server.py +0 -29
- zero_server-0.0.1/src/zero_server/protocol/wire.py +0 -88
- zero_server-0.0.1/src/zero_server/server/adapters/psycopg.py +0 -55
- zero_server-0.0.1/src/zero_server/server/adapters/sqlalchemy.py +0 -49
- zero_server-0.0.1/src/zero_server/server/process_queries.py +0 -109
- zero_server-0.0.1/src/zero_server/shared/error.py +0 -97
- zero_server-0.0.1/src/zero_server/z2s/sql.py +0 -130
- {zero_server-0.0.1 → zero_server-0.0.3}/LICENSE +0 -0
- {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/codegen.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/integrations/__init__.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/protocol/__init__.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/protocol/mutation_id.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/py.typed +0 -0
- {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/query/__init__.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/server/__init__.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/server/adapters/__init__.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/shared/__init__.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/shared/asserts.py +0 -0
- {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/z2s/__init__.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.3
|
|
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:
|
|
@@ -106,9 +105,9 @@ app = Starlette(
|
|
|
106
105
|
)
|
|
107
106
|
```
|
|
108
107
|
|
|
109
|
-
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.
|
|
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. The types to annotate those handlers (`QueryRequestHandler`, `TransactFn`, `MutateResponse`, `MutationResponse`, `ServerPushFailed` and the rest) import from `zero_server`, and `server_ast(query, schema)` returns the AST zero-cache receives for a query.
|
|
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`. A push's mutations run on one connection from the pool, taken at its first transaction and returned when the push ends. So mutators in one push share session state (a `SET` without `LOCAL`, session advisory locks, temp tables), and a handler that takes a second connection from the same pool while its push holds one can deadlock once the pool has fewer connections than there are concurrent pushes. 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
|
|
|
@@ -117,6 +116,7 @@ Point `ZERO_QUERY_URL` and `ZERO_MUTATE_URL` at `/query` and `/mutate`. With Fas
|
|
|
117
116
|
The published npm package is the definition of correct. Every test diffs Python against it:
|
|
118
117
|
|
|
119
118
|
- **Wire.** `harness/` runs the real `handleMutateRequest`, `handleQueryRequest`, `PushProcessor` and the query builder under Node. Hypothesis generates pushes (with duplicates, gaps, refusals and cleanup mutations) and builder programs, sends each through both sides against identical Postgres databases, and diffs the responses and every row written.
|
|
119
|
+
- **HTTP.** The raw bytes of a request go to upstream's handlers as a Fetch `Request` and to the Starlette routes, so query-string parsing, body decoding and `JSON.parse`'s error messages are compared too, along with the response as `JSON.stringify` writes it.
|
|
120
120
|
- **SQL.** The same generated queries run through upstream's `ZQLDatabase.run` and through `tx.run` against one seeded database, and the result rows are compared, never the SQL text.
|
|
121
121
|
|
|
122
122
|
The wire and SQL tiers run against every adapter.
|
|
@@ -124,10 +124,15 @@ The wire and SQL tiers run against every adapter.
|
|
|
124
124
|
|
|
125
125
|
Run them with `uv run pytest` (needs Node 24 and Docker, or `ZERO_SERVER_PG` pointing at a Postgres) and `uv run --group e2e python e2e/run.py`.
|
|
126
126
|
|
|
127
|
+
A diff that passes only covers the inputs tried, so `uv run python -m tests.compat` then checks the inputs were enough. Every line and branch of the upstream modules the port cites must have run inside the npm package during the tests, and every line and branch of `zero_server` must have run during a differential test. [`docs/compat.md`](docs/compat.md) explains the check, lists what it leaves out and why, and records the known differences.
|
|
128
|
+
|
|
129
|
+
## Performance
|
|
130
|
+
|
|
131
|
+
`bench/` runs the same workloads through zero-server and the published package. The CPU-only cases (the query endpoint, request parsing, the mutate endpoint over an in-memory database, z2s) are [CodSpeed](https://codspeed.io) benchmarks on both sides, and CodSpeed flags a pull request that makes any of them slower. `uv run python -m bench` times every case on the wall clock, including those that wait on Postgres (`tx.run`, mutate, `PushProcessor`, per adapter), and prints each case's Python/TypeScript ratio. CI shows that table without gating on it. `bench/__init__.py` has the commands.
|
|
132
|
+
|
|
127
133
|
## Known differences
|
|
128
134
|
|
|
129
|
-
-
|
|
130
|
-
- **Non-string values against text columns.** Upstream passes such a value to the JavaScript driver as is, so the driver decides how it binds. zero-server binds `String(value)`, which is what postgres.js does except for an array whose first element is a boolean, which it binds as `false`.
|
|
135
|
+
[`docs/compat.md`](docs/compat.md#known-differences) has the full list. The two most likely to matter: the deprecated upstream exports (`handleTransformRequest`, `getMutation`, the positional handler overloads, `tx.query` and the rest) are not ported, and where upstream's result depends on its JavaScript driver, zero-server does what the postgres.js adapter does.
|
|
131
136
|
|
|
132
137
|
## License
|
|
133
138
|
|
|
@@ -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:
|
|
@@ -74,9 +74,9 @@ app = Starlette(
|
|
|
74
74
|
)
|
|
75
75
|
```
|
|
76
76
|
|
|
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.
|
|
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. The types to annotate those handlers (`QueryRequestHandler`, `TransactFn`, `MutateResponse`, `MutationResponse`, `ServerPushFailed` and the rest) import from `zero_server`, and `server_ast(query, schema)` returns the AST zero-cache receives for a query.
|
|
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`. A push's mutations run on one connection from the pool, taken at its first transaction and returned when the push ends. So mutators in one push share session state (a `SET` without `LOCAL`, session advisory locks, temp tables), and a handler that takes a second connection from the same pool while its push holds one can deadlock once the pool has fewer connections than there are concurrent pushes. 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
|
|
|
@@ -85,6 +85,7 @@ Point `ZERO_QUERY_URL` and `ZERO_MUTATE_URL` at `/query` and `/mutate`. With Fas
|
|
|
85
85
|
The published npm package is the definition of correct. Every test diffs Python against it:
|
|
86
86
|
|
|
87
87
|
- **Wire.** `harness/` runs the real `handleMutateRequest`, `handleQueryRequest`, `PushProcessor` and the query builder under Node. Hypothesis generates pushes (with duplicates, gaps, refusals and cleanup mutations) and builder programs, sends each through both sides against identical Postgres databases, and diffs the responses and every row written.
|
|
88
|
+
- **HTTP.** The raw bytes of a request go to upstream's handlers as a Fetch `Request` and to the Starlette routes, so query-string parsing, body decoding and `JSON.parse`'s error messages are compared too, along with the response as `JSON.stringify` writes it.
|
|
88
89
|
- **SQL.** The same generated queries run through upstream's `ZQLDatabase.run` and through `tx.run` against one seeded database, and the result rows are compared, never the SQL text.
|
|
89
90
|
|
|
90
91
|
The wire and SQL tiers run against every adapter.
|
|
@@ -92,10 +93,15 @@ The wire and SQL tiers run against every adapter.
|
|
|
92
93
|
|
|
93
94
|
Run them with `uv run pytest` (needs Node 24 and Docker, or `ZERO_SERVER_PG` pointing at a Postgres) and `uv run --group e2e python e2e/run.py`.
|
|
94
95
|
|
|
96
|
+
A diff that passes only covers the inputs tried, so `uv run python -m tests.compat` then checks the inputs were enough. Every line and branch of the upstream modules the port cites must have run inside the npm package during the tests, and every line and branch of `zero_server` must have run during a differential test. [`docs/compat.md`](docs/compat.md) explains the check, lists what it leaves out and why, and records the known differences.
|
|
97
|
+
|
|
98
|
+
## Performance
|
|
99
|
+
|
|
100
|
+
`bench/` runs the same workloads through zero-server and the published package. The CPU-only cases (the query endpoint, request parsing, the mutate endpoint over an in-memory database, z2s) are [CodSpeed](https://codspeed.io) benchmarks on both sides, and CodSpeed flags a pull request that makes any of them slower. `uv run python -m bench` times every case on the wall clock, including those that wait on Postgres (`tx.run`, mutate, `PushProcessor`, per adapter), and prints each case's Python/TypeScript ratio. CI shows that table without gating on it. `bench/__init__.py` has the commands.
|
|
101
|
+
|
|
95
102
|
## Known differences
|
|
96
103
|
|
|
97
|
-
-
|
|
98
|
-
- **Non-string values against text columns.** Upstream passes such a value to the JavaScript driver as is, so the driver decides how it binds. zero-server binds `String(value)`, which is what postgres.js does except for an array whose first element is a boolean, which it binds as `false`.
|
|
104
|
+
[`docs/compat.md`](docs/compat.md#known-differences) has the full list. The two most likely to matter: the deprecated upstream exports (`handleTransformRequest`, `getMutation`, the positional handler overloads, `tx.query` and the rest) are not ported, and where upstream's result depends on its JavaScript driver, zero-server does what the postgres.js adapter does.
|
|
99
105
|
|
|
100
106
|
## License
|
|
101
107
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "zero-server"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.3"
|
|
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,36 +37,34 @@ 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",
|
|
59
56
|
"pytest-cov>=7.1",
|
|
60
57
|
"ruff>=0.17",
|
|
61
58
|
"ty>=0.0.87",
|
|
59
|
+
"pytest-codspeed>=5.0.3",
|
|
62
60
|
]
|
|
63
61
|
e2e = [
|
|
64
|
-
"starlette>=1.
|
|
62
|
+
"starlette>=1.6",
|
|
65
63
|
"uvicorn>=0.54",
|
|
66
64
|
]
|
|
67
65
|
|
|
68
66
|
[build-system]
|
|
69
|
-
requires = ["uv_build>=0.11,<0.
|
|
67
|
+
requires = ["uv_build>=0.11,<0.14"]
|
|
70
68
|
build-backend = "uv_build"
|
|
71
69
|
|
|
72
70
|
[tool.ruff]
|
|
@@ -92,7 +90,7 @@ testpaths = ["tests"]
|
|
|
92
90
|
asyncio_mode = "auto"
|
|
93
91
|
asyncio_default_fixture_loop_scope = "session"
|
|
94
92
|
asyncio_default_test_loop_scope = "session"
|
|
95
|
-
addopts = "--cov=zero_server --cov-branch --cov-report=term-missing"
|
|
93
|
+
addopts = "--cov=zero_server --cov-branch --cov-context=test --cov-report=term-missing"
|
|
96
94
|
|
|
97
95
|
[tool.coverage.run]
|
|
98
96
|
concurrency = [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "zero-server"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.3"
|
|
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,30 +28,32 @@ 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",
|
|
44
46
|
"pytest-cov>=7.1",
|
|
45
47
|
"ruff>=0.17",
|
|
46
48
|
"ty>=0.0.87",
|
|
49
|
+
"pytest-codspeed>=5.0.3",
|
|
47
50
|
]
|
|
48
51
|
|
|
49
52
|
# The Python app server of the end-to-end tier.
|
|
50
|
-
e2e = ["starlette>=1.
|
|
53
|
+
e2e = ["starlette>=1.6", "uvicorn>=0.54"]
|
|
51
54
|
|
|
52
55
|
[build-system]
|
|
53
|
-
requires = ["uv_build>=0.11,<0.
|
|
56
|
+
requires = ["uv_build>=0.11,<0.14"]
|
|
54
57
|
build-backend = "uv_build"
|
|
55
58
|
|
|
56
59
|
[tool.ruff]
|
|
@@ -65,7 +68,7 @@ testpaths = ["tests"]
|
|
|
65
68
|
asyncio_mode = "auto"
|
|
66
69
|
asyncio_default_fixture_loop_scope = "session"
|
|
67
70
|
asyncio_default_test_loop_scope = "session"
|
|
68
|
-
addopts = "--cov=zero_server --cov-branch --cov-report=term-missing"
|
|
71
|
+
addopts = "--cov=zero_server --cov-branch --cov-context=test --cov-report=term-missing"
|
|
69
72
|
|
|
70
73
|
[tool.coverage.run]
|
|
71
74
|
# SQLAlchemy's asyncio layer resumes coroutines inside greenlets.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Community Python server SDK for Rocicorp Zero. Not affiliated with Rocicorp.
|
|
2
|
+
|
|
3
|
+
The SQLAlchemy transaction provider lives in `zero_server.server.adapters.sqlalchemy` and needs the
|
|
4
|
+
`zero-server[sqlalchemy]` extra.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from zero_server.mutators import define_mutator, define_mutators, get_mutator, must_get_mutator
|
|
8
|
+
from zero_server.protocol.application_error import ApplicationError
|
|
9
|
+
from zero_server.protocol.ast import AST
|
|
10
|
+
from zero_server.protocol.error import ErrorReason, ServerPushFailed
|
|
11
|
+
from zero_server.protocol.mutate_server import MutateSuccess
|
|
12
|
+
from zero_server.protocol.mutation import (
|
|
13
|
+
CustomMutation,
|
|
14
|
+
MutationAppError,
|
|
15
|
+
MutationOk,
|
|
16
|
+
MutationResponse,
|
|
17
|
+
MutationZeroError,
|
|
18
|
+
)
|
|
19
|
+
from zero_server.protocol.push import PushBody
|
|
20
|
+
from zero_server.query.builder import Builder, OneQuery, Query, create_builder
|
|
21
|
+
from zero_server.query.expression import ExpressionBuilder
|
|
22
|
+
from zero_server.query.registry import QueryParseError, define_queries, define_query, get_query, must_get_query
|
|
23
|
+
from zero_server.schema import Schema, load_schema
|
|
24
|
+
from zero_server.server.process_mutations import (
|
|
25
|
+
MutateResponse,
|
|
26
|
+
OutOfOrderMutation,
|
|
27
|
+
TransactFn,
|
|
28
|
+
handle_mutate_request,
|
|
29
|
+
)
|
|
30
|
+
from zero_server.server.process_queries import QueryRequestHandler, handle_query_request, server_ast
|
|
31
|
+
from zero_server.server.push_processor import PushProcessor
|
|
32
|
+
from zero_server.shared.json import JSONValue
|
|
33
|
+
|
|
34
|
+
__version__ = "0.0.3"
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"AST",
|
|
38
|
+
"ApplicationError",
|
|
39
|
+
"Builder",
|
|
40
|
+
"CustomMutation",
|
|
41
|
+
"ErrorReason",
|
|
42
|
+
"ExpressionBuilder",
|
|
43
|
+
"JSONValue",
|
|
44
|
+
"MutateResponse",
|
|
45
|
+
"MutateSuccess",
|
|
46
|
+
"MutationAppError",
|
|
47
|
+
"MutationOk",
|
|
48
|
+
"MutationResponse",
|
|
49
|
+
"MutationZeroError",
|
|
50
|
+
"OneQuery",
|
|
51
|
+
"OutOfOrderMutation",
|
|
52
|
+
"PushBody",
|
|
53
|
+
"PushProcessor",
|
|
54
|
+
"Query",
|
|
55
|
+
"QueryParseError",
|
|
56
|
+
"QueryRequestHandler",
|
|
57
|
+
"Schema",
|
|
58
|
+
"ServerPushFailed",
|
|
59
|
+
"TransactFn",
|
|
60
|
+
"create_builder",
|
|
61
|
+
"define_mutator",
|
|
62
|
+
"define_mutators",
|
|
63
|
+
"define_queries",
|
|
64
|
+
"define_query",
|
|
65
|
+
"get_mutator",
|
|
66
|
+
"get_query",
|
|
67
|
+
"handle_mutate_request",
|
|
68
|
+
"handle_query_request",
|
|
69
|
+
"load_schema",
|
|
70
|
+
"must_get_mutator",
|
|
71
|
+
"must_get_query",
|
|
72
|
+
"server_ast",
|
|
73
|
+
]
|
|
@@ -21,6 +21,7 @@ from zero_server.query.registry import must_get_query
|
|
|
21
21
|
from zero_server.schema import Schema
|
|
22
22
|
from zero_server.server.process_mutations import Database, TransactFn, handle_mutate_request
|
|
23
23
|
from zero_server.server.process_queries import handle_query_request
|
|
24
|
+
from zero_server.shared.js import js_stringify
|
|
24
25
|
from zero_server.shared.json import JSONValue
|
|
25
26
|
from zero_server.shared.registry import Registry
|
|
26
27
|
|
|
@@ -30,6 +31,13 @@ type MakeContext = Callable[[Request, str | None], Any]
|
|
|
30
31
|
"""The `ctx` queries and mutators receive. May be async."""
|
|
31
32
|
|
|
32
33
|
|
|
34
|
+
class _JSONResponse(JSONResponse):
|
|
35
|
+
"""The bytes a TypeScript server sends: `JSON.stringify`'s text, which escapes any lone surrogate."""
|
|
36
|
+
|
|
37
|
+
def render(self, content: Any) -> bytes:
|
|
38
|
+
return js_stringify(content).encode()
|
|
39
|
+
|
|
40
|
+
|
|
33
41
|
class Unauthorized(Exception):
|
|
34
42
|
"""Raised by `authenticate` for a token it rejects. The endpoint answers 401 instead of running."""
|
|
35
43
|
|
|
@@ -63,15 +71,15 @@ def zero_routes(
|
|
|
63
71
|
async def caller(request: Request) -> tuple[str | None, Any] | Response:
|
|
64
72
|
try:
|
|
65
73
|
user_id = await _maybe_await(authenticate(request))
|
|
66
|
-
except Unauthorized as error:
|
|
67
|
-
return
|
|
74
|
+
except Unauthorized as error: # python-only: upstream leaves authentication to the app's route
|
|
75
|
+
return _JSONResponse({"error": str(error) or "Unauthorized"}, status_code=401)
|
|
68
76
|
return user_id, await _maybe_await(context(request, user_id))
|
|
69
77
|
|
|
70
78
|
if queries is not None:
|
|
71
79
|
|
|
72
80
|
async def query(request: Request) -> Response:
|
|
73
81
|
who = await caller(request)
|
|
74
|
-
if isinstance(who, Response):
|
|
82
|
+
if isinstance(who, Response): # python-only: the 401 from `authenticate`
|
|
75
83
|
return who
|
|
76
84
|
user_id, ctx = who
|
|
77
85
|
response = await handle_query_request(
|
|
@@ -80,18 +88,18 @@ def zero_routes(
|
|
|
80
88
|
body=await request.body(),
|
|
81
89
|
user_id=user_id,
|
|
82
90
|
)
|
|
83
|
-
return
|
|
91
|
+
return _JSONResponse(response.to_json())
|
|
84
92
|
|
|
85
93
|
routes.append(Route(query_path, query, methods=["POST"]))
|
|
86
94
|
|
|
87
95
|
if mutators is not None:
|
|
88
|
-
if database is None:
|
|
96
|
+
if database is None: # python-only: a configuration error in these routes' own arguments
|
|
89
97
|
raise ValueError("the mutate endpoint needs a database")
|
|
90
98
|
db = database
|
|
91
99
|
|
|
92
100
|
async def mutate(request: Request) -> Response:
|
|
93
101
|
who = await caller(request)
|
|
94
|
-
if isinstance(who, Response):
|
|
102
|
+
if isinstance(who, Response): # python-only: the 401 from `authenticate`
|
|
95
103
|
return who
|
|
96
104
|
user_id, ctx = who
|
|
97
105
|
|
|
@@ -108,7 +116,7 @@ def zero_routes(
|
|
|
108
116
|
body=await request.body(),
|
|
109
117
|
user_id=user_id,
|
|
110
118
|
)
|
|
111
|
-
return
|
|
119
|
+
return _JSONResponse(response.to_json())
|
|
112
120
|
|
|
113
121
|
routes.append(Route(mutate_path, mutate, methods=["POST"]))
|
|
114
122
|
|
|
@@ -37,7 +37,7 @@ def define_mutator(fn_or_validator: Any, fn: MutatorFn | None = None) -> Any:
|
|
|
37
37
|
`@define_mutator(ArgsModel)`."""
|
|
38
38
|
if fn is not None:
|
|
39
39
|
return MutatorDefinition(fn=fn, validator=fn_or_validator)
|
|
40
|
-
if is_schema_validator(fn_or_validator):
|
|
40
|
+
if is_schema_validator(fn_or_validator): # python-only: the decorator form, `@define_mutator(Model)`
|
|
41
41
|
return lambda decorated: MutatorDefinition(fn=decorated, validator=fn_or_validator)
|
|
42
42
|
return MutatorDefinition(fn=fn_or_validator)
|
|
43
43
|
|
|
@@ -75,12 +75,12 @@ def define_mutators(
|
|
|
75
75
|
return merge(base, build(overrides, is_definition, Mutator))
|
|
76
76
|
|
|
77
77
|
|
|
78
|
-
def get_mutator(mutators: Registry, name: str) -> Mutator | None:
|
|
79
|
-
|
|
80
|
-
return
|
|
78
|
+
def get_mutator(mutators: Registry, name: str) -> Mutator | Registry | None:
|
|
79
|
+
"""The mutator at a dot-separated `name`, or, as upstream has it, the namespace there."""
|
|
80
|
+
return value_at_path(mutators, name, ".")
|
|
81
81
|
|
|
82
82
|
|
|
83
|
-
def must_get_mutator(mutators: Registry, name: str) -> Mutator:
|
|
83
|
+
def must_get_mutator(mutators: Registry, name: str) -> Mutator | Registry:
|
|
84
84
|
mutator = get_mutator(mutators, name)
|
|
85
85
|
if mutator is None:
|
|
86
86
|
raise Exception(f"Mutator not found: {name}")
|
|
@@ -11,6 +11,9 @@ from zero_server.shared.json import JSONValue
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class ApplicationError(Exception):
|
|
14
|
+
# Upstream's constructor sets the name, so a subclass reports it too.
|
|
15
|
+
error_name = "ApplicationError"
|
|
16
|
+
|
|
14
17
|
def __init__(self, message: str, *, details: JSONValue | None = None, cause: BaseException | None = None) -> None:
|
|
15
18
|
super().__init__(message)
|
|
16
19
|
self.message = message
|
|
@@ -26,11 +29,7 @@ def is_application_error(error: object) -> bool:
|
|
|
26
29
|
return isinstance(error, ApplicationError)
|
|
27
30
|
|
|
28
31
|
|
|
29
|
-
def wrap_with_application_error(error:
|
|
32
|
+
def wrap_with_application_error(error: BaseException) -> ApplicationError:
|
|
30
33
|
if isinstance(error, ApplicationError):
|
|
31
34
|
return error
|
|
32
|
-
return ApplicationError(
|
|
33
|
-
get_error_message(error),
|
|
34
|
-
cause=error if isinstance(error, BaseException) else None,
|
|
35
|
-
details=get_error_details(error),
|
|
36
|
-
)
|
|
35
|
+
return ApplicationError(get_error_message(error), cause=error, details=get_error_details(error))
|