zero-server 0.0.2__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 (63) hide show
  1. {zero_server-0.0.2 → zero_server-0.0.3}/PKG-INFO +11 -5
  2. {zero_server-0.0.2 → zero_server-0.0.3}/README.md +10 -4
  3. {zero_server-0.0.2 → zero_server-0.0.3}/pyproject.toml +3 -2
  4. {zero_server-0.0.2 → zero_server-0.0.3}/pyproject.toml.orig +3 -2
  5. zero_server-0.0.3/src/zero_server/__init__.py +73 -0
  6. {zero_server-0.0.2 → zero_server-0.0.3}/src/zero_server/integrations/starlette.py +15 -7
  7. {zero_server-0.0.2 → zero_server-0.0.3}/src/zero_server/mutators.py +5 -5
  8. {zero_server-0.0.2 → 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.2 → zero_server-0.0.3}/src/zero_server/protocol/custom_queries.py +18 -10
  12. {zero_server-0.0.2 → zero_server-0.0.3}/src/zero_server/protocol/error.py +22 -8
  13. {zero_server-0.0.2 → zero_server-0.0.3}/src/zero_server/protocol/mutate_server.py +12 -1
  14. {zero_server-0.0.2 → zero_server-0.0.3}/src/zero_server/protocol/mutation.py +14 -9
  15. {zero_server-0.0.2 → 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.2 → zero_server-0.0.3}/src/zero_server/query/builder.py +130 -72
  20. {zero_server-0.0.2 → zero_server-0.0.3}/src/zero_server/query/expression.py +41 -35
  21. {zero_server-0.0.2 → zero_server-0.0.3}/src/zero_server/query/registry.py +25 -15
  22. {zero_server-0.0.2 → 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.2 → zero_server-0.0.3}/src/zero_server/server/adapters/asyncpg.py +20 -6
  25. {zero_server-0.0.2 → zero_server-0.0.3}/src/zero_server/server/adapters/psycopg.py +36 -8
  26. zero_server-0.0.3/src/zero_server/server/adapters/sqlalchemy.py +120 -0
  27. {zero_server-0.0.2 → zero_server-0.0.3}/src/zero_server/server/crud.py +74 -17
  28. {zero_server-0.0.2 → zero_server-0.0.3}/src/zero_server/server/pg_query_executor.py +13 -7
  29. {zero_server-0.0.2 → zero_server-0.0.3}/src/zero_server/server/pg_types.py +2 -0
  30. {zero_server-0.0.2 → 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.2 → zero_server-0.0.3}/src/zero_server/server/push_processor.py +3 -2
  33. {zero_server-0.0.2 → zero_server-0.0.3}/src/zero_server/server/server_schema.py +3 -3
  34. {zero_server-0.0.2 → zero_server-0.0.3}/src/zero_server/server/transaction.py +3 -2
  35. {zero_server-0.0.2 → 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.2 → zero_server-0.0.3}/src/zero_server/shared/js.py +79 -13
  38. {zero_server-0.0.2 → 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.2 → 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.2 → 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.2/src/zero_server/__init__.py +0 -36
  45. zero_server-0.0.2/src/zero_server/protocol/ast.py +0 -342
  46. zero_server-0.0.2/src/zero_server/protocol/query_server.py +0 -29
  47. zero_server-0.0.2/src/zero_server/protocol/wire.py +0 -88
  48. zero_server-0.0.2/src/zero_server/server/adapters/sqlalchemy.py +0 -75
  49. zero_server-0.0.2/src/zero_server/server/process_queries.py +0 -109
  50. zero_server-0.0.2/src/zero_server/shared/error.py +0 -97
  51. zero_server-0.0.2/src/zero_server/z2s/sql.py +0 -130
  52. {zero_server-0.0.2 → zero_server-0.0.3}/LICENSE +0 -0
  53. {zero_server-0.0.2 → zero_server-0.0.3}/src/zero_server/codegen.py +0 -0
  54. {zero_server-0.0.2 → zero_server-0.0.3}/src/zero_server/integrations/__init__.py +0 -0
  55. {zero_server-0.0.2 → zero_server-0.0.3}/src/zero_server/protocol/__init__.py +0 -0
  56. {zero_server-0.0.2 → zero_server-0.0.3}/src/zero_server/protocol/mutation_id.py +0 -0
  57. {zero_server-0.0.2 → zero_server-0.0.3}/src/zero_server/py.typed +0 -0
  58. {zero_server-0.0.2 → zero_server-0.0.3}/src/zero_server/query/__init__.py +0 -0
  59. {zero_server-0.0.2 → zero_server-0.0.3}/src/zero_server/server/__init__.py +0 -0
  60. {zero_server-0.0.2 → zero_server-0.0.3}/src/zero_server/server/adapters/__init__.py +0 -0
  61. {zero_server-0.0.2 → zero_server-0.0.3}/src/zero_server/shared/__init__.py +0 -0
  62. {zero_server-0.0.2 → zero_server-0.0.3}/src/zero_server/shared/asserts.py +0 -0
  63. {zero_server-0.0.2 → zero_server-0.0.3}/src/zero_server/z2s/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zero-server
3
- Version: 0.0.2
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
@@ -105,9 +105,9 @@ app = Starlette(
105
105
  )
106
106
  ```
107
107
 
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.
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.
109
109
 
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.
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.
111
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.
112
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.
113
113
 
@@ -116,6 +116,7 @@ Point `ZERO_QUERY_URL` and `ZERO_MUTATE_URL` at `/query` and `/mutate`. With Fas
116
116
  The published npm package is the definition of correct. Every test diffs Python against it:
117
117
 
118
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.
119
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.
120
121
 
121
122
  The wire and SQL tiers run against every adapter.
@@ -123,10 +124,15 @@ The wire and SQL tiers run against every adapter.
123
124
 
124
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`.
125
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
+
126
133
  ## Known differences
127
134
 
128
- - **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.
129
- - **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.
130
136
 
131
137
  ## License
132
138
 
@@ -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 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.
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.2"
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"
@@ -56,6 +56,7 @@ dev = [
56
56
  "pytest-cov>=7.1",
57
57
  "ruff>=0.17",
58
58
  "ty>=0.0.87",
59
+ "pytest-codspeed>=5.0.3",
59
60
  ]
60
61
  e2e = [
61
62
  "starlette>=1.6",
@@ -89,7 +90,7 @@ testpaths = ["tests"]
89
90
  asyncio_mode = "auto"
90
91
  asyncio_default_fixture_loop_scope = "session"
91
92
  asyncio_default_test_loop_scope = "session"
92
- addopts = "--cov=zero_server --cov-branch --cov-report=term-missing"
93
+ addopts = "--cov=zero_server --cov-branch --cov-context=test --cov-report=term-missing"
93
94
 
94
95
  [tool.coverage.run]
95
96
  concurrency = [
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "zero-server"
3
- version = "0.0.2"
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"
@@ -46,6 +46,7 @@ dev = [
46
46
  "pytest-cov>=7.1",
47
47
  "ruff>=0.17",
48
48
  "ty>=0.0.87",
49
+ "pytest-codspeed>=5.0.3",
49
50
  ]
50
51
 
51
52
  # The Python app server of the end-to-end tier.
@@ -67,7 +68,7 @@ testpaths = ["tests"]
67
68
  asyncio_mode = "auto"
68
69
  asyncio_default_fixture_loop_scope = "session"
69
70
  asyncio_default_test_loop_scope = "session"
70
- addopts = "--cov=zero_server --cov-branch --cov-report=term-missing"
71
+ addopts = "--cov=zero_server --cov-branch --cov-context=test --cov-report=term-missing"
71
72
 
72
73
  [tool.coverage.run]
73
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))