dirigent-block-jq 0.17.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dirigent_block_jq-0.17.2/LICENSE +18 -0
- dirigent_block_jq-0.17.2/PKG-INFO +24 -0
- dirigent_block_jq-0.17.2/README.md +10 -0
- dirigent_block_jq-0.17.2/pyproject.toml +30 -0
- dirigent_block_jq-0.17.2/pyproject.toml.orig +28 -0
- dirigent_block_jq-0.17.2/src/dirigent_block_jq/__init__.py +24 -0
- dirigent_block_jq-0.17.2/src/dirigent_block_jq/jq_runner.py +65 -0
- dirigent_block_jq-0.17.2/src/dirigent_block_jq/messages.py +36 -0
- dirigent_block_jq-0.17.2/src/dirigent_block_jq/py.typed +0 -0
- dirigent_block_jq-0.17.2/src/dirigent_block_jq/transform_jq.py +155 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Copyright (c) 2026 Morten Olav Hansen <morten@winterop.com>. All rights reserved.
|
|
2
|
+
|
|
3
|
+
This source code and accompanying documentation are the property of
|
|
4
|
+
Morten Olav Hansen. No license, express or implied, is granted to use, copy,
|
|
5
|
+
modify, merge, publish, distribute, sublicense, or sell copies of this
|
|
6
|
+
software or its derivatives.
|
|
7
|
+
|
|
8
|
+
The source is published for reference only. Any use beyond reading
|
|
9
|
+
requires written permission from the copyright holder.
|
|
10
|
+
|
|
11
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
12
|
+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
13
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
|
|
14
|
+
IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES,
|
|
15
|
+
OR OTHER LIABILITY ARISING FROM THE USE OF THE SOFTWARE.
|
|
16
|
+
|
|
17
|
+
Third-party components redistributed with this software, and the licences they
|
|
18
|
+
carry, are listed in THIRD_PARTY_NOTICES.md.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dirigent-block-jq
|
|
3
|
+
Version: 0.17.2
|
|
4
|
+
Summary: The jq engine for dirigent's transform verbs: transform.jq, map.jq and filter.jq.
|
|
5
|
+
License-Expression: LicenseRef-Proprietary
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
9
|
+
Requires-Dist: dirigent-common==0.17.2
|
|
10
|
+
Requires-Dist: dirigent-plugin==0.17.2
|
|
11
|
+
Requires-Dist: jq>=1.8
|
|
12
|
+
Requires-Python: >=3.13
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# dirigent-block-jq
|
|
16
|
+
|
|
17
|
+
The jq engine for the transform verbs: `transform.jq` reshapes a whole value with a jq
|
|
18
|
+
program, `map.jq` replaces every element of a list with what a program makes of it, and
|
|
19
|
+
`filter.jq` keeps the elements a program answers true for.
|
|
20
|
+
|
|
21
|
+
A jq program is evaluated in a child process rather than in the worker, because jq holds the
|
|
22
|
+
GIL for as long as a program runs and there is no way to interrupt one. The runner that child
|
|
23
|
+
starts is a module of this package, run by path so it costs jq and the standard library to
|
|
24
|
+
start.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# dirigent-block-jq
|
|
2
|
+
|
|
3
|
+
The jq engine for the transform verbs: `transform.jq` reshapes a whole value with a jq
|
|
4
|
+
program, `map.jq` replaces every element of a list with what a program makes of it, and
|
|
5
|
+
`filter.jq` keeps the elements a program answers true for.
|
|
6
|
+
|
|
7
|
+
A jq program is evaluated in a child process rather than in the worker, because jq holds the
|
|
8
|
+
GIL for as long as a program runs and there is no way to interrupt one. The runner that child
|
|
9
|
+
starts is a module of this package, run by path so it costs jq and the standard library to
|
|
10
|
+
start.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "dirigent-block-jq"
|
|
3
|
+
version = "0.17.2"
|
|
4
|
+
description = "The jq engine for dirigent's transform verbs: transform.jq, map.jq and filter.jq."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
license = "LicenseRef-Proprietary"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Programming Language :: Python :: 3",
|
|
11
|
+
"Programming Language :: Python :: 3.13",
|
|
12
|
+
]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"dirigent-common==0.17.2",
|
|
15
|
+
"dirigent-plugin==0.17.2",
|
|
16
|
+
"jq>=1.8",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.entry-points."dirigent.plugins.v1"]
|
|
20
|
+
block-jq = "dirigent_block_jq:plugin"
|
|
21
|
+
|
|
22
|
+
[build-system]
|
|
23
|
+
requires = ["uv_build>=0.12.0,<0.13.0"]
|
|
24
|
+
build-backend = "uv_build"
|
|
25
|
+
|
|
26
|
+
[tool.uv.sources.dirigent-common]
|
|
27
|
+
workspace = true
|
|
28
|
+
|
|
29
|
+
[tool.uv.sources.dirigent-plugin]
|
|
30
|
+
workspace = true
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "dirigent-block-jq"
|
|
3
|
+
version = "0.17.2"
|
|
4
|
+
description = "The jq engine for dirigent's transform verbs: transform.jq, map.jq and filter.jq."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
license = "LicenseRef-Proprietary"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Programming Language :: Python :: 3",
|
|
11
|
+
"Programming Language :: Python :: 3.13",
|
|
12
|
+
]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"dirigent-common==0.17.2",
|
|
15
|
+
"dirigent-plugin==0.17.2",
|
|
16
|
+
"jq>=1.8",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.entry-points."dirigent.plugins.v1"]
|
|
20
|
+
block-jq = "dirigent_block_jq:plugin"
|
|
21
|
+
|
|
22
|
+
[build-system]
|
|
23
|
+
requires = ["uv_build>=0.12.0,<0.13.0"]
|
|
24
|
+
build-backend = "uv_build"
|
|
25
|
+
|
|
26
|
+
[tool.uv.sources]
|
|
27
|
+
dirigent-common = { workspace = true }
|
|
28
|
+
dirigent-plugin = { workspace = true }
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""The jq engine for the transform verbs: reshaping a value, and the element-wise pair."""
|
|
2
|
+
|
|
3
|
+
from dirigent_block_jq.transform_jq import JqFilterer, JqMapper, JqTransformer
|
|
4
|
+
from dirigent_plugin import Contribution, extension
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class JqBlocks:
|
|
8
|
+
"""The plugin object the host discovers under the dirigent.plugins.v1 entry-point group."""
|
|
9
|
+
|
|
10
|
+
@extension
|
|
11
|
+
def contribute(self) -> Contribution:
|
|
12
|
+
"""Contribute the jq engines behind the transform, map and filter verbs."""
|
|
13
|
+
return Contribution(operators=[JqTransformer(), JqMapper(), JqFilterer()])
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
plugin = JqBlocks()
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"JqBlocks",
|
|
20
|
+
"JqFilterer",
|
|
21
|
+
"JqMapper",
|
|
22
|
+
"JqTransformer",
|
|
23
|
+
"plugin",
|
|
24
|
+
]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""The jq runner: the process a jq program is compiled and evaluated in, spoken to over its pipes.
|
|
2
|
+
|
|
3
|
+
jq is a C extension that holds the GIL for as long as a program runs, so a program evaluated
|
|
4
|
+
in the worker's own process holds its event loop too: the step's timeout never fires, the
|
|
5
|
+
lease heartbeat never runs, and the sweeper eventually hands the attempt to another worker
|
|
6
|
+
while this one is still computing. There is no way to interrupt a jq program and no way to
|
|
7
|
+
cancel a thread, so a program runs here, where killing the process ends it.
|
|
8
|
+
|
|
9
|
+
The runner protocol is what the pipes carry, one JSON object per line each way: ``compile``
|
|
10
|
+
holds a program under the id it is named by, ``run`` evaluates the program held under an id
|
|
11
|
+
over one value, and ``forget`` releases one. The pipe closing is how the parent says it is
|
|
12
|
+
done, and compiling a program and reading its outputs are the only jq in here.
|
|
13
|
+
|
|
14
|
+
This module is run by path, never imported, so it costs the standard library and jq and
|
|
15
|
+
nothing else to start. Importing it as part of its package would load the
|
|
16
|
+
whole plugin in order to evaluate ``.name``.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import sys
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
import jq
|
|
24
|
+
|
|
25
|
+
#: Typed ``Any`` because the jq binding is a C extension that ships no type information.
|
|
26
|
+
libjq: Any = jq
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def main() -> None:
|
|
30
|
+
"""Answer one request at a time until the pipe closes, holding the programs compiled so far."""
|
|
31
|
+
out = sys.stdout.buffer
|
|
32
|
+
programs: dict[str, Any] = {}
|
|
33
|
+
for line in sys.stdin.buffer:
|
|
34
|
+
reply = answer(programs, json.loads(line))
|
|
35
|
+
out.write(json.dumps(reply).encode() + b"\n")
|
|
36
|
+
out.flush()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def answer(programs: dict[str, Any], request: dict[str, Any]) -> dict[str, Any]:
|
|
40
|
+
"""Compile a program under its id, run the one an id names, or release it."""
|
|
41
|
+
kind = request["kind"]
|
|
42
|
+
if kind == "compile":
|
|
43
|
+
try:
|
|
44
|
+
compiled = libjq.compile(request["program"])
|
|
45
|
+
except ValueError as error:
|
|
46
|
+
return {"error": str(error).strip()}
|
|
47
|
+
# Held only once it has compiled, so a refused program is not one an id names.
|
|
48
|
+
programs[request["id"]] = compiled
|
|
49
|
+
return {"ok": True}
|
|
50
|
+
if kind == "forget":
|
|
51
|
+
programs.pop(request["id"], None)
|
|
52
|
+
return {"ok": True}
|
|
53
|
+
if kind == "run":
|
|
54
|
+
compiled = programs.get(request["id"])
|
|
55
|
+
if compiled is None:
|
|
56
|
+
return {"error": f"no program is compiled under {request['id']}"}
|
|
57
|
+
try:
|
|
58
|
+
return {"outputs": compiled.input_value(request["value"]).all()}
|
|
59
|
+
except ValueError as error:
|
|
60
|
+
return {"error": str(error).strip()}
|
|
61
|
+
return {"error": f"{kind} is not a request kind: compile, run, or forget"}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if __name__ == "__main__":
|
|
65
|
+
main()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Every refusal the jq engine makes, catalogued under the ``transform.jq`` prefix.
|
|
2
|
+
|
|
3
|
+
A jq refusal reaches an attempt as ``plugin.transform_failed``: the verb frame owns the
|
|
4
|
+
failure, and the engine's sentence rides on it as the ``detail`` param.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dirigent_common import Catalogue
|
|
8
|
+
|
|
9
|
+
JQ = Catalogue("transform.jq")
|
|
10
|
+
|
|
11
|
+
PROGRAM_REFUSED = JQ.define("program_refused", "{detail}")
|
|
12
|
+
|
|
13
|
+
NO_OUTPUT = JQ.define(
|
|
14
|
+
"no_output",
|
|
15
|
+
"the program produced no output, and a transform step has to produce one; "
|
|
16
|
+
"a program that means 'possibly nothing' emits [] or null explicitly",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
MAP_OUTPUT_COUNT = JQ.define(
|
|
20
|
+
"map_output_count",
|
|
21
|
+
"the program produced {produced} outputs, and a map replaces an element with "
|
|
22
|
+
"exactly one; a program that drops elements is a filter.jq step, and one that "
|
|
23
|
+
"changes how many there are is a transform.jq step",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
FILTER_OUTPUT_COUNT = JQ.define(
|
|
27
|
+
"filter_output_count",
|
|
28
|
+
"the program produced {produced} outputs, and a filter answers one true or false "
|
|
29
|
+
"per element; a program that reshapes an element is a map.jq or transform.jq step",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
FILTER_ANSWER = JQ.define(
|
|
33
|
+
"filter_answer",
|
|
34
|
+
"the program answered {answer}, and a filter answers true or false; jq's "
|
|
35
|
+
"truthiness is not applied, so a program meaning 'has readings' writes '.count > 0'",
|
|
36
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""The ``jq`` engines: a jq program as the transform, map, and filter verbs' program.
|
|
2
|
+
|
|
3
|
+
A program is compiled and evaluated in a jq process of its own, over the runner protocol the
|
|
4
|
+
plugin contract defines. jq is a C extension that holds the GIL for as long as a program
|
|
5
|
+
runs, so evaluating one on the worker would hold the event loop with it and starve the step
|
|
6
|
+
timeout and the lease heartbeat that share it. A program cannot be interrupted and a thread
|
|
7
|
+
cannot be cancelled, so the step's timeout ends a program the only way there is: the process
|
|
8
|
+
running it is killed, and the step fails with it.
|
|
9
|
+
|
|
10
|
+
A bad program is refused with jq's own message: at apply on the worker, where there is no
|
|
11
|
+
runner, and again by the runner when the step compiles it.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
from collections.abc import Sequence
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Annotated, Any, ClassVar
|
|
19
|
+
|
|
20
|
+
import jq
|
|
21
|
+
from pydantic import BaseModel, Field, JsonValue
|
|
22
|
+
|
|
23
|
+
from dirigent_block_jq.messages import (
|
|
24
|
+
FILTER_ANSWER,
|
|
25
|
+
FILTER_OUTPUT_COUNT,
|
|
26
|
+
MAP_OUTPUT_COUNT,
|
|
27
|
+
PROGRAM_REFUSED,
|
|
28
|
+
)
|
|
29
|
+
from dirigent_block_jq.messages import NO_OUTPUT as NO_OUTPUT_MESSAGE
|
|
30
|
+
from dirigent_common import JQ_MEDIA_TYPE
|
|
31
|
+
from dirigent_plugin import Filterer, Mapper, ProgramConfig, RunnerEngine, Transformer, TransformError
|
|
32
|
+
|
|
33
|
+
#: What a program producing nothing is told to write instead.
|
|
34
|
+
NO_OUTPUT = NO_OUTPUT_MESSAGE.render()
|
|
35
|
+
|
|
36
|
+
#: Wraps the author's program so jq's ``env`` and ``$ENV`` read an empty object instead of
|
|
37
|
+
#: the worker's environment, which is where dirigent's own secrets live. The program keeps
|
|
38
|
+
#: its own line numbers because the message a bad one is refused with comes from compiling
|
|
39
|
+
#: the author's text, unwrapped.
|
|
40
|
+
SANDBOX = "{} as $ENV | def env: {}; (\n"
|
|
41
|
+
|
|
42
|
+
#: The script a jq process runs, started by path so that it imports jq and the standard
|
|
43
|
+
#: library and nothing else.
|
|
44
|
+
RUNNER = Path(__file__).with_name("jq_runner.py")
|
|
45
|
+
|
|
46
|
+
#: Typed ``Any`` because the jq binding is a C extension that ships no type information.
|
|
47
|
+
libjq: Any = jq
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class JqEngine(RunnerEngine):
|
|
51
|
+
"""Where the three jq engines' programs run: in a jq process, never on the worker's loop."""
|
|
52
|
+
|
|
53
|
+
command: ClassVar[Sequence[str]] = (sys.executable, str(RUNNER))
|
|
54
|
+
|
|
55
|
+
def source(self, program: str) -> str:
|
|
56
|
+
"""Hand the runner the author's program with env and $ENV shadowed."""
|
|
57
|
+
return f"{SANDBOX}{program}\n)"
|
|
58
|
+
|
|
59
|
+
def check_program(self, program: str) -> None:
|
|
60
|
+
"""Compile the program here at apply, refusing a bad one with jq's own message about it."""
|
|
61
|
+
try:
|
|
62
|
+
# The author's text is compiled first, so a refusal carries jq's message about
|
|
63
|
+
# their program and not about the wrapper.
|
|
64
|
+
libjq.compile(program)
|
|
65
|
+
libjq.compile(self.source(program))
|
|
66
|
+
except ValueError as error:
|
|
67
|
+
raise TransformError(PROGRAM_REFUSED.render(detail=str(error).strip())) from error
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class JqProgramConfig(ProgramConfig):
|
|
71
|
+
"""The program-shaped config of a jq engine, whichever verb is running it.
|
|
72
|
+
|
|
73
|
+
All three verbs take the same program in the same language, so all three publish the
|
|
74
|
+
same config: the frame's fields, with ``program`` narrowed to jq source.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
program: Annotated[str, Field(min_length=1, json_schema_extra={"contentMediaType": JQ_MEDIA_TYPE})]
|
|
78
|
+
"""The jq program this step runs."""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class JqTransformer(JqEngine, Transformer):
|
|
82
|
+
"""Reshapes one whole value by running a jq program over it.
|
|
83
|
+
|
|
84
|
+
A jq program opens no file, no socket, and starts nothing: it is handed a value and
|
|
85
|
+
returns values, which is why this engine executes no code on the worker and needs no
|
|
86
|
+
allowlist entry. Its two ways of reading an environment, ``env`` and ``$ENV``, are
|
|
87
|
+
shadowed, so a program sees an empty object rather than the process it runs in -- which
|
|
88
|
+
is a jq process of dirigent's own, so that a step's timeout can end it.
|
|
89
|
+
|
|
90
|
+
A jq program is a stream: it can emit zero, one, or many outputs. One output is the
|
|
91
|
+
step's value, many outputs are that list of values, and none is a refusal, because a
|
|
92
|
+
step that produced nothing has no output for the next step to read.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
kind = "jq"
|
|
96
|
+
summary = "Reshape a value with a jq program."
|
|
97
|
+
config_model: ClassVar[type[BaseModel]] = JqProgramConfig
|
|
98
|
+
|
|
99
|
+
def apply(self, compiled: object, value: JsonValue) -> JsonValue:
|
|
100
|
+
"""Run the program over one value, and read its stream of outputs as one result."""
|
|
101
|
+
produced = self.outputs(compiled, value)
|
|
102
|
+
if not produced:
|
|
103
|
+
raise TransformError(NO_OUTPUT)
|
|
104
|
+
return produced[0] if len(produced) == 1 else produced
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class JqMapper(JqEngine, Mapper):
|
|
108
|
+
"""Replaces every element of a list with what a jq program makes of that element.
|
|
109
|
+
|
|
110
|
+
The program is run once per element, with the element as its input, and is sandboxed
|
|
111
|
+
exactly as ``transform.jq`` is: ``env`` and ``$ENV`` read an empty object.
|
|
112
|
+
|
|
113
|
+
A map replaces each element with one thing, so the stream a program emits for an element
|
|
114
|
+
has to be exactly one output. Zero of them and several of them are both refusals rather
|
|
115
|
+
than a shorter or longer list, because dropping elements is what ``filter.jq`` is for and
|
|
116
|
+
changing their number is what ``transform.jq`` is for.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
kind = "jq"
|
|
120
|
+
summary = "Replace every element of a list with what a jq program makes of it."
|
|
121
|
+
config_model: ClassVar[type[BaseModel]] = JqProgramConfig
|
|
122
|
+
|
|
123
|
+
def apply(self, compiled: object, value: JsonValue) -> JsonValue:
|
|
124
|
+
"""Run the program over one element, and take its single output as the replacement."""
|
|
125
|
+
produced = self.outputs(compiled, value)
|
|
126
|
+
if len(produced) != 1:
|
|
127
|
+
raise TransformError(MAP_OUTPUT_COUNT.render(produced=len(produced)))
|
|
128
|
+
return produced[0]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class JqFilterer(JqEngine, Filterer):
|
|
132
|
+
"""Keeps the elements of a list a jq program answers true for.
|
|
133
|
+
|
|
134
|
+
The program is run once per element, with the element as its input, and is sandboxed
|
|
135
|
+
exactly as ``transform.jq`` is: ``env`` and ``$ENV`` read an empty object.
|
|
136
|
+
|
|
137
|
+
The answer has to be exactly one output, and that output has to be ``true`` or ``false``.
|
|
138
|
+
jq's own truthiness is not applied: a program emitting ``0`` or ``""`` is a mistake
|
|
139
|
+
surfaced rather than an element quietly dropped, and an author who means "has readings"
|
|
140
|
+
writes ``.count > 0``.
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
kind = "jq"
|
|
144
|
+
summary = "Keep the elements of a list a jq program answers true for."
|
|
145
|
+
config_model: ClassVar[type[BaseModel]] = JqProgramConfig
|
|
146
|
+
|
|
147
|
+
def keep(self, compiled: object, value: JsonValue) -> bool:
|
|
148
|
+
"""Run the program over one element, and read its single boolean output as the verdict."""
|
|
149
|
+
produced = self.outputs(compiled, value)
|
|
150
|
+
if len(produced) != 1:
|
|
151
|
+
raise TransformError(FILTER_OUTPUT_COUNT.render(produced=len(produced)))
|
|
152
|
+
answer = produced[0]
|
|
153
|
+
if not isinstance(answer, bool):
|
|
154
|
+
raise TransformError(FILTER_ANSWER.render(answer=json.dumps(answer)))
|
|
155
|
+
return answer
|