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.
Files changed (64) hide show
  1. {zero_server-0.0.1 → zero_server-0.0.3}/PKG-INFO +19 -14
  2. {zero_server-0.0.1 → zero_server-0.0.3}/README.md +11 -5
  3. {zero_server-0.0.1 → zero_server-0.0.3}/pyproject.toml +13 -15
  4. {zero_server-0.0.1 → zero_server-0.0.3}/pyproject.toml.orig +14 -11
  5. zero_server-0.0.3/src/zero_server/__init__.py +73 -0
  6. {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/integrations/starlette.py +15 -7
  7. {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/mutators.py +5 -5
  8. {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/protocol/application_error.py +5 -6
  9. zero_server-0.0.3/src/zero_server/protocol/ast.py +533 -0
  10. zero_server-0.0.3/src/zero_server/protocol/ast_wire.py +149 -0
  11. {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/protocol/custom_queries.py +18 -10
  12. {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/protocol/error.py +22 -8
  13. {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/protocol/mutate_server.py +12 -1
  14. {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/protocol/mutation.py +14 -9
  15. {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/protocol/push.py +4 -4
  16. zero_server-0.0.3/src/zero_server/protocol/query_server.py +43 -0
  17. zero_server-0.0.3/src/zero_server/protocol/schemas.py +100 -0
  18. zero_server-0.0.3/src/zero_server/protocol/wire.py +286 -0
  19. {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/query/builder.py +130 -72
  20. {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/query/expression.py +41 -35
  21. {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/query/registry.py +25 -15
  22. {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/schema.py +40 -51
  23. zero_server-0.0.3/src/zero_server/server/adapters/_session.py +68 -0
  24. {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/server/adapters/asyncpg.py +20 -6
  25. zero_server-0.0.3/src/zero_server/server/adapters/psycopg.py +87 -0
  26. zero_server-0.0.3/src/zero_server/server/adapters/sqlalchemy.py +120 -0
  27. {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/server/crud.py +74 -17
  28. {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/server/pg_query_executor.py +13 -7
  29. {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/server/pg_types.py +2 -0
  30. {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/server/process_mutations.py +121 -84
  31. zero_server-0.0.3/src/zero_server/server/process_queries.py +161 -0
  32. {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/server/push_processor.py +3 -2
  33. {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/server/server_schema.py +3 -3
  34. {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/server/transaction.py +3 -2
  35. {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/server/zql_database.py +89 -8
  36. zero_server-0.0.3/src/zero_server/shared/error.py +78 -0
  37. {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/shared/js.py +79 -13
  38. {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/shared/json.py +3 -5
  39. zero_server-0.0.3/src/zero_server/shared/json_parse.py +320 -0
  40. {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/shared/registry.py +10 -2
  41. zero_server-0.0.3/src/zero_server/shared/valita.py +412 -0
  42. {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/z2s/compiler.py +211 -95
  43. zero_server-0.0.3/src/zero_server/z2s/sql.py +226 -0
  44. zero_server-0.0.1/src/zero_server/__init__.py +0 -36
  45. zero_server-0.0.1/src/zero_server/protocol/ast.py +0 -342
  46. zero_server-0.0.1/src/zero_server/protocol/query_server.py +0 -29
  47. zero_server-0.0.1/src/zero_server/protocol/wire.py +0 -88
  48. zero_server-0.0.1/src/zero_server/server/adapters/psycopg.py +0 -55
  49. zero_server-0.0.1/src/zero_server/server/adapters/sqlalchemy.py +0 -49
  50. zero_server-0.0.1/src/zero_server/server/process_queries.py +0 -109
  51. zero_server-0.0.1/src/zero_server/shared/error.py +0 -97
  52. zero_server-0.0.1/src/zero_server/z2s/sql.py +0 -130
  53. {zero_server-0.0.1 → zero_server-0.0.3}/LICENSE +0 -0
  54. {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/codegen.py +0 -0
  55. {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/integrations/__init__.py +0 -0
  56. {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/protocol/__init__.py +0 -0
  57. {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/protocol/mutation_id.py +0 -0
  58. {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/py.typed +0 -0
  59. {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/query/__init__.py +0 -0
  60. {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/server/__init__.py +0 -0
  61. {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/server/adapters/__init__.py +0 -0
  62. {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/shared/__init__.py +0 -0
  63. {zero_server-0.0.1 → zero_server-0.0.3}/src/zero_server/shared/asserts.py +0 -0
  64. {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.1
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 :: 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:
@@ -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
- - **Parse errors.** When a request is malformed, the message after `Failed to parse push body:` describes the bad field in Pydantic's words, not valita's. The rest of the response matches, and zero-cache only logs it.
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
- - **Parse errors.** When a request is malformed, the message after `Failed to parse push body:` describes the bad field in Pydantic's words, not valita's. The rest of the response matches, and zero-cache only logs it.
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.1"
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 :: 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,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
- "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",
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.8",
62
+ "starlette>=1.6",
65
63
  "uvicorn>=0.54",
66
64
  ]
67
65
 
68
66
  [build-system]
69
- requires = ["uv_build>=0.11,<0.12"]
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.1"
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 :: 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,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
- 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",
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.8", "uvicorn>=0.54"]
53
+ e2e = ["starlette>=1.6", "uvicorn>=0.54"]
51
54
 
52
55
  [build-system]
53
- requires = ["uv_build>=0.11,<0.12"]
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 JSONResponse({"error": str(error) or "Unauthorized"}, status_code=401)
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 JSONResponse(response.to_json())
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 JSONResponse(response.to_json())
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
- found = value_at_path(mutators, name, ".")
80
- return found if isinstance(found, Mutator) else None
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: object) -> ApplicationError:
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))