codeterrain 0.3.0__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.
@@ -0,0 +1,138 @@
1
+ """The extractor protocol, as the core reads it.
2
+
3
+ An extractor is any program that runs inside the target's runtime and writes one JSON document to
4
+ the path in ``$CODETERRAIN_RESULT``. The core never imports extractor code in-process; it only reads
5
+ that document and refuses any whose ``schema`` it does not know or whose fields have the wrong type.
6
+ See docs/extractor-protocol.md.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+ ORDER_SCHEMA = "codeterrain.order/1"
14
+ ROUTES_SCHEMA = "codeterrain.routes/1"
15
+
16
+ ORDER_KEYS = ("closures", "nodes", "errors")
17
+ ROUTES_KEYS = ("routes", "errors")
18
+ FIXTURE_KEYS = ("name", "scope", "file", "line")
19
+ ROUTE_KEYS = ("app", "method", "path", "handler", "file", "line", "dependencies", "response_model")
20
+ ERROR_KEYS = ("app", "error")
21
+
22
+
23
+ class ProtocolError(ValueError):
24
+ """The extractor's document is not one this core understands."""
25
+
26
+
27
+ def _is_int(value: Any) -> bool:
28
+ return isinstance(value, int) and not isinstance(value, bool)
29
+
30
+
31
+ def _is_str_list(value: Any) -> bool:
32
+ return isinstance(value, list) and all(isinstance(v, str) for v in value)
33
+
34
+
35
+ def _require(schema: str, ok: bool, what: str) -> None:
36
+ if not ok:
37
+ raise ProtocolError(f"{schema}: {what}")
38
+
39
+
40
+ def _check_fixture(schema: str, fx: Any) -> None:
41
+ _require(schema, isinstance(fx, dict), "a closure entry must be an object")
42
+ _require(
43
+ schema,
44
+ all(k in fx for k in FIXTURE_KEYS),
45
+ f"each fixture needs {', '.join(FIXTURE_KEYS)}",
46
+ )
47
+ _require(
48
+ schema,
49
+ all(isinstance(fx[k], str) for k in ("name", "scope", "file")),
50
+ "fixture name, scope and file must be strings",
51
+ )
52
+ _require(schema, _is_int(fx["line"]), "fixture line must be an integer")
53
+
54
+
55
+ def _check_order(doc: dict[str, Any], schema: str) -> None:
56
+ _require(schema, _is_int(doc["errors"]), "field 'errors' must be int")
57
+ closures = doc["closures"]
58
+ for closure in closures:
59
+ _require(schema, isinstance(closure, list), "each closure must be a list of fixtures")
60
+ for fx in closure:
61
+ _check_fixture(schema, fx)
62
+ for node, index in doc["nodes"].items():
63
+ _require(schema, isinstance(node, str), "node ids must be strings")
64
+ _require(schema, _is_int(index), f"node {node!r} must map to an integer closure index")
65
+ _require(
66
+ schema,
67
+ 0 <= index < len(closures),
68
+ f"node {node!r} points at a closure that does not exist",
69
+ )
70
+
71
+
72
+ def _check_routes(doc: dict[str, Any], schema: str) -> None:
73
+ for route in doc["routes"]:
74
+ _require(schema, isinstance(route, dict), "each route must be an object")
75
+ _require(
76
+ schema,
77
+ all(k in route for k in ROUTE_KEYS),
78
+ f"each route needs {', '.join(ROUTE_KEYS)}",
79
+ )
80
+ strings = ("app", "method", "path", "handler", "file")
81
+ _require(
82
+ schema,
83
+ all(isinstance(route[k], str) for k in strings),
84
+ f"route {', '.join(strings)} must be strings",
85
+ )
86
+ _require(schema, _is_int(route["line"]), "route line must be an integer")
87
+ _require(schema, _is_str_list(route["dependencies"]), "route dependencies must be strings")
88
+ _require(
89
+ schema,
90
+ route["response_model"] is None or isinstance(route["response_model"], str),
91
+ "route response_model must be a string or null",
92
+ )
93
+ _require(
94
+ schema,
95
+ isinstance(route.get("also_head", False), bool),
96
+ "route also_head must be a boolean",
97
+ )
98
+ for failure in doc["errors"]:
99
+ _require(schema, isinstance(failure, dict), "each error must be an object")
100
+ _require(
101
+ schema,
102
+ all(k in failure for k in ERROR_KEYS),
103
+ f"each error needs {', '.join(ERROR_KEYS)}",
104
+ )
105
+ _require(
106
+ schema,
107
+ all(isinstance(failure[k], str) for k in ERROR_KEYS),
108
+ "error app and error must be strings",
109
+ )
110
+ _require(
111
+ schema,
112
+ "chain" not in failure or _is_str_list(failure["chain"]),
113
+ "error chain must be a list of strings",
114
+ )
115
+
116
+
117
+ def check(doc: Any, schema: str) -> dict[str, Any]:
118
+ """Return ``doc`` if it is a valid ``schema`` document; raise ProtocolError otherwise."""
119
+ if not isinstance(doc, dict):
120
+ raise ProtocolError(f"extractor output is not a JSON object (expected {schema})")
121
+ found = doc.get("schema")
122
+ if found != schema:
123
+ raise ProtocolError(
124
+ f"unsupported extractor schema {found!r}: this core reads {schema!r}; the extractor "
125
+ "and the core must agree on the protocol version (docs/extractor-protocol.md)"
126
+ )
127
+ if schema == ORDER_SCHEMA:
128
+ kinds: dict[str, type] = {"closures": list, "nodes": dict, "errors": int}
129
+ else:
130
+ kinds = {"routes": list, "errors": list}
131
+ for key, kind in kinds.items():
132
+ if not isinstance(doc.get(key), kind):
133
+ raise ProtocolError(f"{schema}: field {key!r} must be {kind.__name__}")
134
+ if schema == ORDER_SCHEMA:
135
+ _check_order(doc, schema)
136
+ else:
137
+ _check_routes(doc, schema)
138
+ return doc
@@ -0,0 +1,61 @@
1
+ """The ``routes`` view: the configured apps' routes, extracted by importing them in the sandbox.
2
+
3
+ The core runs ``python -m codeterrain._routes_runner`` in the sandbox and reads the
4
+ ``codeterrain.routes/1`` document it leaves behind (docs/extractor-protocol.md). The access class
5
+ is joined from the configured route manifest on every read, so a manifest edit shows at once.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from codeterrain import protocol
14
+ from codeterrain.config import Config
15
+ from codeterrain.fixtures_order import ExtractError
16
+ from codeterrain.plugins import manifest_join
17
+ from codeterrain.sandbox import redact_document, redactor, run_python
18
+
19
+ VIEW = "routes"
20
+
21
+
22
+ def extract(config: Config, cache_dir: Path, python: str) -> dict[str, Any]:
23
+ if not config.apps:
24
+ raise ExtractError("no apps configured; set apps = [\"module:attr\"] in the config")
25
+ args = ["-m", "codeterrain._routes_runner", "--apps", *config.apps]
26
+ if config.plugins:
27
+ args += ["--plugins", *config.plugins]
28
+ if config.moto_mock:
29
+ args.append("--moto-mock")
30
+ result = run_python(python, args, config, cache_dir)
31
+ if result.result is None:
32
+ detail = (result.stderr.strip() or result.stdout.strip())[-1200:]
33
+ raise ExtractError(f"route walk left no document (exit {result.returncode}): {detail}")
34
+ # Every string the child reports, optional and plugin fields included, is its own text and
35
+ # may quote an environment value: redact it before it is cached.
36
+ return protocol.check(redact_document(result.result, redactor(config)), protocol.ROUTES_SCHEMA)
37
+
38
+
39
+ def joined(doc: dict[str, Any], config: Config) -> manifest_join.JoinResult:
40
+ """Routes with their ``access`` (None when no manifest is configured or no row matches)."""
41
+ routes = sorted(doc["routes"], key=lambda r: (r["path"], r["method"], r["app"]))
42
+ if config.route_manifest is None:
43
+ return manifest_join.JoinResult(rows=[{**r, "access": None} for r in routes])
44
+ rows = manifest_join.load_rows(config.route_manifest, config.manifest_keys)
45
+ return manifest_join.join(routes, rows, config.manifest_keys)
46
+
47
+
48
+ def select(
49
+ rows: list[dict[str, Any]],
50
+ substring: str = "",
51
+ app: str | None = None,
52
+ access: str | None = None,
53
+ ) -> list[dict[str, Any]]:
54
+ needle = substring.lower()
55
+ return [
56
+ r
57
+ for r in rows
58
+ if (needle in r["path"].lower() or needle in r["handler"].lower())
59
+ and (app is None or app in r["app"])
60
+ and (access is None or (r["access"] or "").lower() == access.lower())
61
+ ]