zero-server 0.0.1__py3-none-any.whl
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/__init__.py +36 -0
- zero_server/codegen.py +133 -0
- zero_server/integrations/__init__.py +1 -0
- zero_server/integrations/starlette.py +115 -0
- zero_server/mutators.py +87 -0
- zero_server/protocol/__init__.py +1 -0
- zero_server/protocol/application_error.py +36 -0
- zero_server/protocol/ast.py +342 -0
- zero_server/protocol/custom_queries.py +51 -0
- zero_server/protocol/error.py +119 -0
- zero_server/protocol/mutate_server.py +33 -0
- zero_server/protocol/mutation.py +127 -0
- zero_server/protocol/mutation_id.py +12 -0
- zero_server/protocol/push.py +25 -0
- zero_server/protocol/query_server.py +29 -0
- zero_server/protocol/wire.py +88 -0
- zero_server/py.typed +0 -0
- zero_server/query/__init__.py +1 -0
- zero_server/query/builder.py +338 -0
- zero_server/query/expression.py +167 -0
- zero_server/query/registry.py +143 -0
- zero_server/schema.py +142 -0
- zero_server/server/__init__.py +1 -0
- zero_server/server/adapters/__init__.py +1 -0
- zero_server/server/adapters/asyncpg.py +43 -0
- zero_server/server/adapters/psycopg.py +55 -0
- zero_server/server/adapters/sqlalchemy.py +49 -0
- zero_server/server/crud.py +134 -0
- zero_server/server/pg_query_executor.py +70 -0
- zero_server/server/pg_types.py +69 -0
- zero_server/server/process_mutations.py +412 -0
- zero_server/server/process_queries.py +109 -0
- zero_server/server/push_processor.py +49 -0
- zero_server/server/server_schema.py +165 -0
- zero_server/server/transaction.py +70 -0
- zero_server/server/zql_database.py +129 -0
- zero_server/shared/__init__.py +1 -0
- zero_server/shared/asserts.py +19 -0
- zero_server/shared/error.py +97 -0
- zero_server/shared/js.py +121 -0
- zero_server/shared/json.py +27 -0
- zero_server/shared/registry.py +64 -0
- zero_server/z2s/__init__.py +1 -0
- zero_server/z2s/compiler.py +375 -0
- zero_server/z2s/sql.py +130 -0
- zero_server-0.0.1.dist-info/METADATA +134 -0
- zero_server-0.0.1.dist-info/RECORD +50 -0
- zero_server-0.0.1.dist-info/WHEEL +4 -0
- zero_server-0.0.1.dist-info/entry_points.txt +3 -0
- zero_server-0.0.1.dist-info/licenses/LICENSE +202 -0
zero_server/__init__.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
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.query.builder import Query, create_builder
|
|
10
|
+
from zero_server.query.registry import QueryParseError, define_queries, define_query, get_query, must_get_query
|
|
11
|
+
from zero_server.schema import Schema, load_schema
|
|
12
|
+
from zero_server.server.process_mutations import handle_mutate_request
|
|
13
|
+
from zero_server.server.process_queries import handle_query_request
|
|
14
|
+
from zero_server.server.push_processor import PushProcessor
|
|
15
|
+
|
|
16
|
+
__version__ = "0.0.1"
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"ApplicationError",
|
|
20
|
+
"PushProcessor",
|
|
21
|
+
"Query",
|
|
22
|
+
"QueryParseError",
|
|
23
|
+
"Schema",
|
|
24
|
+
"create_builder",
|
|
25
|
+
"define_mutator",
|
|
26
|
+
"define_mutators",
|
|
27
|
+
"define_queries",
|
|
28
|
+
"define_query",
|
|
29
|
+
"get_mutator",
|
|
30
|
+
"get_query",
|
|
31
|
+
"handle_mutate_request",
|
|
32
|
+
"handle_query_request",
|
|
33
|
+
"load_schema",
|
|
34
|
+
"must_get_mutator",
|
|
35
|
+
"must_get_query",
|
|
36
|
+
]
|
zero_server/codegen.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Typed Python for a Zero schema: row types per table, and a builder and `tx.mutate` that use them.
|
|
2
|
+
|
|
3
|
+
zero-server codegen schema.json -o zero_schema.py
|
|
4
|
+
|
|
5
|
+
`schema.json` is `JSON.stringify(schema)` of the TypeScript `schema.ts`. The generated module has, per
|
|
6
|
+
table, a `TypedDict` for a row (`Issue`), for an insert (`IssueInsert`, optional columns may be left
|
|
7
|
+
out), for an update (`IssueUpdate`, the key plus any columns) and for a delete (`IssueKey`), and a
|
|
8
|
+
`Literal` of its column names (`IssueColumn`). Its `create_builder` returns queries typed by those rows
|
|
9
|
+
and columns, and `mutate(tx)` is `tx.mutate` typed the same way. Rows of a `related()` relationship
|
|
10
|
+
come back nested under each row but are not part of the row type.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import keyword
|
|
16
|
+
import re
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from zero_server.schema import ColumnSchema, Schema, TableSchema, load_schema
|
|
21
|
+
|
|
22
|
+
_TYPES = {"string": "str", "number": "float", "boolean": "bool", "null": "None", "json": "Any"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _class_name(table: str) -> str:
|
|
26
|
+
parts = re.split(r"[^0-9A-Za-z]+", table)
|
|
27
|
+
name = "".join(part[:1].upper() + part[1:] for part in parts if part)
|
|
28
|
+
if not name or name[0].isdigit():
|
|
29
|
+
name = f"Table{name}"
|
|
30
|
+
return name
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _is_identifier(name: str) -> bool:
|
|
34
|
+
return name.isidentifier() and not keyword.iskeyword(name)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _value_type(column: ColumnSchema) -> str:
|
|
38
|
+
base = _TYPES[column.type]
|
|
39
|
+
return f"{base} | None" if column.optional and base not in ("Any", "None") else base
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _typed_dict(name: str, fields: list[tuple[str, str, bool]]) -> str:
|
|
43
|
+
"""A TypedDict, in class syntax when every key is an identifier, else in functional syntax.
|
|
44
|
+
|
|
45
|
+
Never empty: every table has a primary key, which all four row types include.
|
|
46
|
+
"""
|
|
47
|
+
if all(_is_identifier(key) for key, _, _ in fields):
|
|
48
|
+
lines = [f" {key}: {f'NotRequired[{t}]' if not required else t}" for key, t, required in fields]
|
|
49
|
+
return f"class {name}(TypedDict):\n" + "\n".join(lines) + "\n"
|
|
50
|
+
items = ", ".join(f"{json.dumps(key)}: {f'NotRequired[{t}]' if not required else t}" for key, t, required in fields)
|
|
51
|
+
return f"{name} = TypedDict({json.dumps(name)}, {{{items}}})\n"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _table(name: str, table: TableSchema, used: set[str]) -> tuple[str, str]:
|
|
55
|
+
"""The table's type definitions, and the class name they were given."""
|
|
56
|
+
cls = _class_name(name)
|
|
57
|
+
while cls in used:
|
|
58
|
+
cls += "_"
|
|
59
|
+
used.add(cls)
|
|
60
|
+
pk = set(table.primary_key)
|
|
61
|
+
columns = list(table.columns.items())
|
|
62
|
+
row = [(c, _value_type(s), True) for c, s in columns]
|
|
63
|
+
insert = [(c, _value_type(s), c in pk or not s.optional) for c, s in columns]
|
|
64
|
+
update = [(c, _value_type(s), c in pk) for c, s in columns]
|
|
65
|
+
key = [(c, _value_type(table.columns[c]), True) for c in table.primary_key]
|
|
66
|
+
column_names = ", ".join(json.dumps(c) for c, _ in columns)
|
|
67
|
+
out = [
|
|
68
|
+
f"type {cls}Column = Literal[{column_names}]\n",
|
|
69
|
+
_typed_dict(cls, row),
|
|
70
|
+
_typed_dict(f"{cls}Insert", insert),
|
|
71
|
+
_typed_dict(f"{cls}Update", update),
|
|
72
|
+
_typed_dict(f"{cls}Key", key),
|
|
73
|
+
]
|
|
74
|
+
return "\n\n".join(out), cls
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def generate(schema: Schema, source: str = "schema.json") -> str:
|
|
78
|
+
used: set[str] = set()
|
|
79
|
+
definitions: list[str] = []
|
|
80
|
+
builder: list[str] = []
|
|
81
|
+
mutate: list[str] = []
|
|
82
|
+
for name, table in schema.tables.items():
|
|
83
|
+
types, cls = _table(name, table, used)
|
|
84
|
+
definitions.append(types)
|
|
85
|
+
if _is_identifier(name):
|
|
86
|
+
builder.append(
|
|
87
|
+
f" @property\n def {name}(self) -> Query[{cls}, {cls}Column]:\n"
|
|
88
|
+
f" return self.table({json.dumps(name)})\n"
|
|
89
|
+
)
|
|
90
|
+
crud = f"TableCRUD[{cls}Insert, {cls}Update, {cls}Key]"
|
|
91
|
+
mutate.append(f" @property\n def {name}(self) -> {crud}: ...\n")
|
|
92
|
+
header = f'''"""Types for the Zero schema in {source}. Generated by `zero-server codegen`; do not edit."""
|
|
93
|
+
|
|
94
|
+
from typing import Any, Literal, NotRequired, Protocol, TypedDict, cast
|
|
95
|
+
|
|
96
|
+
from zero_server.query.builder import Builder as _Builder
|
|
97
|
+
from zero_server.query.builder import Query
|
|
98
|
+
from zero_server.schema import Schema
|
|
99
|
+
from zero_server.server.crud import TableCRUD
|
|
100
|
+
from zero_server.server.transaction import ServerTransaction
|
|
101
|
+
'''
|
|
102
|
+
body = "\n\n".join(definitions)
|
|
103
|
+
builder_class = "class Builder(_Builder):\n" + ("\n".join(builder) if builder else " pass\n")
|
|
104
|
+
mutate_class = "class Mutate(Protocol):\n" + ("\n".join(mutate) if mutate else " pass\n")
|
|
105
|
+
footer = '''def create_builder(schema: Schema, *, strict: bool = False) -> Builder:
|
|
106
|
+
return Builder(schema, strict=strict)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def mutate(tx: ServerTransaction[Any]) -> Mutate:
|
|
110
|
+
"""`tx.mutate`, typed by this schema."""
|
|
111
|
+
return cast(Mutate, tx.mutate)
|
|
112
|
+
'''
|
|
113
|
+
# One blank line after the imports, two between definitions: what ruff's formatter and isort keep.
|
|
114
|
+
return header + "\n" + "\n\n".join([body, builder_class, mutate_class, footer]).rstrip() + "\n"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def main(argv: list[str] | None = None) -> int:
|
|
118
|
+
parser = argparse.ArgumentParser(prog="zero-server")
|
|
119
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
120
|
+
codegen = commands.add_parser("codegen", help="generate Python types for a schema")
|
|
121
|
+
codegen.add_argument("schema", type=Path, help="the JSON of the TypeScript schema")
|
|
122
|
+
codegen.add_argument("-o", "--output", type=Path, help="write here instead of to stdout")
|
|
123
|
+
args = parser.parse_args(argv)
|
|
124
|
+
code = generate(load_schema(args.schema), args.schema.name)
|
|
125
|
+
if args.output is None:
|
|
126
|
+
sys.stdout.write(code)
|
|
127
|
+
else:
|
|
128
|
+
args.output.write_text(code)
|
|
129
|
+
return 0
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
if __name__ == "__main__": # pragma: no cover - the `zero-server` script calls `main` directly
|
|
133
|
+
sys.exit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Ready-made endpoints for web frameworks."""
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""`/query` and `/mutate` as Starlette routes. Needs the `zero-server[starlette]` extra.
|
|
2
|
+
|
|
3
|
+
app = Starlette(routes=zero_routes(
|
|
4
|
+
schema=schema, database=db, queries=queries, mutators=mutators, authenticate=user_id_from_jwt,
|
|
5
|
+
))
|
|
6
|
+
|
|
7
|
+
FastAPI is Starlette underneath: `app.router.routes.extend(zero_routes(...))`.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import inspect
|
|
11
|
+
from collections.abc import Awaitable, Callable
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from starlette.requests import Request
|
|
15
|
+
from starlette.responses import JSONResponse, Response
|
|
16
|
+
from starlette.routing import Route
|
|
17
|
+
|
|
18
|
+
from zero_server.mutators import must_get_mutator
|
|
19
|
+
from zero_server.protocol.mutation import CustomMutation, MutationResponse
|
|
20
|
+
from zero_server.query.registry import must_get_query
|
|
21
|
+
from zero_server.schema import Schema
|
|
22
|
+
from zero_server.server.process_mutations import Database, TransactFn, handle_mutate_request
|
|
23
|
+
from zero_server.server.process_queries import handle_query_request
|
|
24
|
+
from zero_server.shared.json import JSONValue
|
|
25
|
+
from zero_server.shared.registry import Registry
|
|
26
|
+
|
|
27
|
+
type Authenticate = Callable[[Request], str | Awaitable[str | None] | None]
|
|
28
|
+
"""The request's user ID, or `None` when logged out. Raise `Unauthorized` to answer 401."""
|
|
29
|
+
type MakeContext = Callable[[Request, str | None], Any]
|
|
30
|
+
"""The `ctx` queries and mutators receive. May be async."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Unauthorized(Exception):
|
|
34
|
+
"""Raised by `authenticate` for a token it rejects. The endpoint answers 401 instead of running."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def _maybe_await(value: Any) -> Any:
|
|
38
|
+
return await value if inspect.isawaitable(value) else value
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _default_context(_request: Request, user_id: str | None) -> dict[str, Any]:
|
|
42
|
+
return {"userID": user_id}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def zero_routes(
|
|
46
|
+
*,
|
|
47
|
+
schema: Schema,
|
|
48
|
+
database: Database[Any] | None = None,
|
|
49
|
+
queries: Registry | None = None,
|
|
50
|
+
mutators: Registry | None = None,
|
|
51
|
+
authenticate: Authenticate,
|
|
52
|
+
context: MakeContext = _default_context,
|
|
53
|
+
query_path: str = "/query",
|
|
54
|
+
mutate_path: str = "/mutate",
|
|
55
|
+
) -> list[Route]:
|
|
56
|
+
"""Routes for whichever of the query and mutate endpoints you pass a registry for.
|
|
57
|
+
|
|
58
|
+
Queries resolve by name through `queries` and mutators through `mutators`, each called with the
|
|
59
|
+
`ctx` that `context` builds (by default `{"userID": user_id}`). Mutations run on `database`.
|
|
60
|
+
"""
|
|
61
|
+
routes: list[Route] = []
|
|
62
|
+
|
|
63
|
+
async def caller(request: Request) -> tuple[str | None, Any] | Response:
|
|
64
|
+
try:
|
|
65
|
+
user_id = await _maybe_await(authenticate(request))
|
|
66
|
+
except Unauthorized as error:
|
|
67
|
+
return JSONResponse({"error": str(error) or "Unauthorized"}, status_code=401)
|
|
68
|
+
return user_id, await _maybe_await(context(request, user_id))
|
|
69
|
+
|
|
70
|
+
if queries is not None:
|
|
71
|
+
|
|
72
|
+
async def query(request: Request) -> Response:
|
|
73
|
+
who = await caller(request)
|
|
74
|
+
if isinstance(who, Response):
|
|
75
|
+
return who
|
|
76
|
+
user_id, ctx = who
|
|
77
|
+
response = await handle_query_request(
|
|
78
|
+
handler=lambda name, args: must_get_query(queries, name).fn(args=args, ctx=ctx),
|
|
79
|
+
schema=schema,
|
|
80
|
+
body=await request.body(),
|
|
81
|
+
user_id=user_id,
|
|
82
|
+
)
|
|
83
|
+
return JSONResponse(response.to_json())
|
|
84
|
+
|
|
85
|
+
routes.append(Route(query_path, query, methods=["POST"]))
|
|
86
|
+
|
|
87
|
+
if mutators is not None:
|
|
88
|
+
if database is None:
|
|
89
|
+
raise ValueError("the mutate endpoint needs a database")
|
|
90
|
+
db = database
|
|
91
|
+
|
|
92
|
+
async def mutate(request: Request) -> Response:
|
|
93
|
+
who = await caller(request)
|
|
94
|
+
if isinstance(who, Response):
|
|
95
|
+
return who
|
|
96
|
+
user_id, ctx = who
|
|
97
|
+
|
|
98
|
+
async def handler(transact: TransactFn[Any], _mutation: CustomMutation) -> MutationResponse:
|
|
99
|
+
async def run(tx: Any, name: str, args: JSONValue | None) -> None:
|
|
100
|
+
await must_get_mutator(mutators, name).fn(tx=tx, ctx=ctx, args=args)
|
|
101
|
+
|
|
102
|
+
return await transact(run)
|
|
103
|
+
|
|
104
|
+
response = await handle_mutate_request(
|
|
105
|
+
db_provider=db,
|
|
106
|
+
handler=handler,
|
|
107
|
+
query=dict(request.query_params),
|
|
108
|
+
body=await request.body(),
|
|
109
|
+
user_id=user_id,
|
|
110
|
+
)
|
|
111
|
+
return JSONResponse(response.to_json())
|
|
112
|
+
|
|
113
|
+
routes.append(Route(mutate_path, mutate, methods=["POST"]))
|
|
114
|
+
|
|
115
|
+
return routes
|
zero_server/mutators.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""`define_mutator` and `define_mutators`: named server mutators the mutate endpoint dispatches to.
|
|
2
|
+
|
|
3
|
+
Ported from rocicorp/mono `packages/zql/src/mutate/mutator.ts` and `mutator-registry.ts` at zero/v1.10.0
|
|
4
|
+
(905d08516ba0caaf314cd28e93a5a47e8acdd482). Apache-2.0, Copyright Rocicorp.
|
|
5
|
+
|
|
6
|
+
A mutator here is the server half of the TypeScript mutator of the same name: the client runs its own
|
|
7
|
+
optimistically, and this one runs authoritatively inside the mutation's transaction.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from collections.abc import Awaitable, Callable, Mapping
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any, overload
|
|
13
|
+
|
|
14
|
+
from zero_server.query.registry import is_schema_validator, validate_input
|
|
15
|
+
from zero_server.shared.json import JSONValue
|
|
16
|
+
from zero_server.shared.registry import Registry, build, merge, value_at_path
|
|
17
|
+
|
|
18
|
+
type MutatorFn = Callable[..., Awaitable[None]]
|
|
19
|
+
"""Called as `await fn(args=..., ctx=..., tx=...)`."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class MutatorDefinition:
|
|
24
|
+
fn: MutatorFn
|
|
25
|
+
validator: Any = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@overload
|
|
29
|
+
def define_mutator(fn_or_validator: MutatorFn, fn: None = None) -> MutatorDefinition: ...
|
|
30
|
+
@overload
|
|
31
|
+
def define_mutator(fn_or_validator: Any, fn: MutatorFn) -> MutatorDefinition: ...
|
|
32
|
+
@overload
|
|
33
|
+
def define_mutator(fn_or_validator: Any, fn: None = None) -> Callable[[MutatorFn], MutatorDefinition]: ...
|
|
34
|
+
def define_mutator(fn_or_validator: Any, fn: MutatorFn | None = None) -> Any:
|
|
35
|
+
"""Define a mutator, called as `await fn(args=..., ctx=..., tx=...)`, in the three forms of
|
|
36
|
+
`define_query`: `define_mutator(fn)` or `@define_mutator`, `define_mutator(validator, fn)`, and
|
|
37
|
+
`@define_mutator(ArgsModel)`."""
|
|
38
|
+
if fn is not None:
|
|
39
|
+
return MutatorDefinition(fn=fn, validator=fn_or_validator)
|
|
40
|
+
if is_schema_validator(fn_or_validator):
|
|
41
|
+
return lambda decorated: MutatorDefinition(fn=decorated, validator=fn_or_validator)
|
|
42
|
+
return MutatorDefinition(fn=fn_or_validator)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class MutateRequest:
|
|
47
|
+
mutator: "Mutator"
|
|
48
|
+
args: JSONValue | None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Mutator:
|
|
52
|
+
def __init__(self, name: str, definition: MutatorDefinition) -> None:
|
|
53
|
+
self.mutator_name = name
|
|
54
|
+
self._definition = definition
|
|
55
|
+
|
|
56
|
+
async def fn(self, *, args: JSONValue | None = None, ctx: Any = None, tx: Any) -> None:
|
|
57
|
+
validated = validate_input(self.mutator_name, args, self._definition.validator, "mutator")
|
|
58
|
+
await self._definition.fn(args=validated, ctx=ctx, tx=tx)
|
|
59
|
+
|
|
60
|
+
def __call__(self, args: JSONValue | None = None) -> MutateRequest:
|
|
61
|
+
return MutateRequest(mutator=self, args=args)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
type MutatorDefinitions = Mapping[str, "MutatorDefinition | MutatorDefinitions"]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def define_mutators(
|
|
68
|
+
definitions: "MutatorDefinitions | Registry", overrides: "MutatorDefinitions | None" = None
|
|
69
|
+
) -> Registry:
|
|
70
|
+
"""Name each mutator by its dot-separated path through `definitions`, as `define_queries` does."""
|
|
71
|
+
is_definition = lambda value: isinstance(value, MutatorDefinition) # noqa: E731
|
|
72
|
+
base = definitions if isinstance(definitions, Registry) else build(definitions, is_definition, Mutator)
|
|
73
|
+
if overrides is None:
|
|
74
|
+
return base
|
|
75
|
+
return merge(base, build(overrides, is_definition, Mutator))
|
|
76
|
+
|
|
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
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def must_get_mutator(mutators: Registry, name: str) -> Mutator:
|
|
84
|
+
mutator = get_mutator(mutators, name)
|
|
85
|
+
if mutator is None:
|
|
86
|
+
raise Exception(f"Mutator not found: {name}")
|
|
87
|
+
return mutator
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Zero's wire types, from rocicorp/mono `packages/zero-protocol`."""
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""The error application code raises to send a message and JSON details back to the client.
|
|
2
|
+
|
|
3
|
+
Ported from rocicorp/mono `packages/zero-protocol/src/application-error.ts` at zero/v1.10.0
|
|
4
|
+
(905d08516ba0caaf314cd28e93a5a47e8acdd482). Apache-2.0, Copyright Rocicorp.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
from zero_server.shared.error import get_error_details, get_error_message
|
|
10
|
+
from zero_server.shared.json import JSONValue
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ApplicationError(Exception):
|
|
14
|
+
def __init__(self, message: str, *, details: JSONValue | None = None, cause: BaseException | None = None) -> None:
|
|
15
|
+
super().__init__(message)
|
|
16
|
+
self.message = message
|
|
17
|
+
self.details = details
|
|
18
|
+
self.__cause__ = cause
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def kind(self) -> Literal["Application"]:
|
|
22
|
+
return "Application"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def is_application_error(error: object) -> bool:
|
|
26
|
+
return isinstance(error, ApplicationError)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def wrap_with_application_error(error: object) -> ApplicationError:
|
|
30
|
+
if isinstance(error, ApplicationError):
|
|
31
|
+
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
|
+
)
|