dryv-compiler 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.
@@ -0,0 +1,21 @@
1
+ from .compatibility import AuthorCompatibilityError, validate_author_compatibility
2
+ from .mapping import compile_app, compile_ir
3
+ from .version import (
4
+ AUTHOR_COMPATIBILITY,
5
+ COMPILER_VERSION,
6
+ SUPPORTED_AUTHORING_PROTOCOLS,
7
+ SUPPORTED_RUNTIME_IR_VERSIONS,
8
+ __version__,
9
+ )
10
+
11
+ __all__ = [
12
+ "AUTHOR_COMPATIBILITY",
13
+ "COMPILER_VERSION",
14
+ "SUPPORTED_AUTHORING_PROTOCOLS",
15
+ "SUPPORTED_RUNTIME_IR_VERSIONS",
16
+ "AuthorCompatibilityError",
17
+ "__version__",
18
+ "compile_app",
19
+ "compile_ir",
20
+ "validate_author_compatibility",
21
+ ]
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
dryv_compiler/cli.py ADDED
@@ -0,0 +1,159 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import signal
6
+ import sys
7
+ from collections.abc import Sequence
8
+ from threading import Thread
9
+
10
+ from .execution import inspect_python_environment
11
+ from .protocol import (
12
+ AuthoringServerMessage,
13
+ CancellationToken,
14
+ CancelRequest,
15
+ CompileRequest,
16
+ ErrorMessage,
17
+ encode_message,
18
+ parse_client_message_text,
19
+ stream_compile,
20
+ )
21
+ from .version import (
22
+ COMPILER_VERSION,
23
+ SUPPORTED_AUTHORING_PROTOCOLS,
24
+ SUPPORTED_RUNTIME_IR_VERSIONS,
25
+ )
26
+
27
+
28
+ def _emit(message: AuthoringServerMessage) -> None:
29
+ sys.stdout.write(encode_message(message))
30
+ sys.stdout.write("\n")
31
+ sys.stdout.flush()
32
+
33
+
34
+ def _install_signal_cancellation(token: CancellationToken) -> None:
35
+ def cancel(_signum: int, _frame: object) -> None:
36
+ token.cancel()
37
+
38
+ signal.signal(signal.SIGINT, cancel)
39
+ signal.signal(signal.SIGTERM, cancel)
40
+
41
+
42
+ def _watch_stdin_cancellation(session_id: str, token: CancellationToken) -> None:
43
+ for raw in sys.stdin:
44
+ if not raw.strip():
45
+ continue
46
+ try:
47
+ message = parse_client_message_text(raw)
48
+ except ValueError:
49
+ continue
50
+ if isinstance(message, CancelRequest) and message.session_id == session_id:
51
+ token.cancel()
52
+ return
53
+
54
+
55
+ def _run_request(request: CompileRequest, *, watch_stdin: bool) -> int:
56
+ token = CancellationToken()
57
+ _install_signal_cancellation(token)
58
+ if watch_stdin:
59
+ Thread(
60
+ target=_watch_stdin_cancellation,
61
+ args=(request.session_id, token),
62
+ daemon=True,
63
+ ).start()
64
+ return 0 if stream_compile(request, _emit, cancellation=token) else 1
65
+
66
+
67
+ def _stdio() -> int:
68
+ first = sys.stdin.readline()
69
+ if not first:
70
+ sys.stderr.write("dryv-py stdio expected a compile.request JSONL message\n")
71
+ return 2
72
+ try:
73
+ message = parse_client_message_text(first)
74
+ except ValueError as exc:
75
+ _emit(ErrorMessage(session_id="unknown", code="PROTOCOL_ERROR", message=str(exc)))
76
+ return 2
77
+ if not isinstance(message, CompileRequest):
78
+ _emit(
79
+ ErrorMessage(
80
+ session_id=message.session_id,
81
+ code="PROTOCOL_ERROR",
82
+ message="first stdio message must be compile.request",
83
+ )
84
+ )
85
+ return 2
86
+ return _run_request(message, watch_stdin=True)
87
+
88
+
89
+ def _compile(
90
+ *,
91
+ entrypoint: str,
92
+ session_id: str,
93
+ batch_size: int,
94
+ protocol: str,
95
+ runtime_ir: str,
96
+ ) -> int:
97
+ request = CompileRequest(
98
+ session_id=session_id,
99
+ entrypoint=entrypoint,
100
+ protocol_versions=(protocol,),
101
+ runtime_ir_versions=(runtime_ir,),
102
+ batch_size=batch_size,
103
+ )
104
+ return _run_request(request, watch_stdin=False)
105
+
106
+
107
+ def _doctor(entrypoint: str) -> int:
108
+ report = inspect_python_environment(entrypoint)
109
+ value: dict[str, object] = {
110
+ "status": report.status.value,
111
+ "pythonExecutable": report.python_executable,
112
+ "pythonVersion": report.python_version,
113
+ "projectRoot": report.project_root,
114
+ "entrypoint": report.entrypoint,
115
+ "virtualEnvironment": report.virtual_environment,
116
+ "authorVersion": report.author_version,
117
+ }
118
+ if report.message is not None:
119
+ value["message"] = report.message
120
+ sys.stdout.write(json.dumps(value, sort_keys=True, separators=(",", ":")))
121
+ sys.stdout.write("\n")
122
+ return 0 if report.status.value == "READY" else 1
123
+
124
+
125
+ def build_parser() -> argparse.ArgumentParser:
126
+ parser = argparse.ArgumentParser(prog="dryv-py")
127
+ parser.add_argument("--version", action="version", version=COMPILER_VERSION)
128
+ commands = parser.add_subparsers(dest="command", required=True)
129
+
130
+ compile_parser = commands.add_parser("compile", help="compile one Python authoring entrypoint")
131
+ compile_parser.add_argument("entrypoint")
132
+ compile_parser.add_argument("--session-id", default="standalone")
133
+ compile_parser.add_argument("--batch-size", type=int, default=64)
134
+ compile_parser.add_argument("--protocol", default=SUPPORTED_AUTHORING_PROTOCOLS[0])
135
+ compile_parser.add_argument("--runtime-ir", default=SUPPORTED_RUNTIME_IR_VERSIONS[0])
136
+
137
+ commands.add_parser("stdio", help="serve one machine-readable JSONL compile session on stdio")
138
+ doctor_parser = commands.add_parser("doctor", help="inspect the Python authoring environment")
139
+ doctor_parser.add_argument("entrypoint")
140
+ return parser
141
+
142
+
143
+ def main(argv: Sequence[str] | None = None) -> int:
144
+ parser = build_parser()
145
+ args = parser.parse_args(argv)
146
+ command = str(args.command)
147
+ if command == "compile":
148
+ return _compile(
149
+ entrypoint=str(args.entrypoint),
150
+ session_id=str(args.session_id),
151
+ batch_size=int(args.batch_size),
152
+ protocol=str(args.protocol),
153
+ runtime_ir=str(args.runtime_ir),
154
+ )
155
+ if command == "stdio":
156
+ return _stdio()
157
+ if command == "doctor":
158
+ return _doctor(str(args.entrypoint))
159
+ parser.error(f"unsupported command: {command}")
@@ -0,0 +1,64 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from dryv_author import AUTHOR_REPRESENTATION_VERSION
6
+ from dryv_author import __version__ as author_version
7
+ from packaging.specifiers import SpecifierSet
8
+ from packaging.version import InvalidVersion, Version
9
+
10
+ from .version import AUTHOR_COMPATIBILITY, AUTHOR_REPRESENTATION
11
+
12
+
13
+ class AuthorCompatibilityError(RuntimeError):
14
+ """Raised when an author SDK cannot be consumed by this compiler."""
15
+
16
+
17
+ @dataclass(frozen=True, slots=True)
18
+ class AuthorCompatibility:
19
+ author_version: str
20
+ supported_versions: str
21
+ representation: str
22
+
23
+
24
+ def current_author_compatibility() -> AuthorCompatibility:
25
+ return AuthorCompatibility(
26
+ author_version=author_version,
27
+ supported_versions=AUTHOR_COMPATIBILITY,
28
+ representation=AUTHOR_REPRESENTATION_VERSION,
29
+ )
30
+
31
+
32
+ def validate_author_details(author_version_value: str, representation: str) -> AuthorCompatibility:
33
+ compatibility = AuthorCompatibility(
34
+ author_version=author_version_value,
35
+ supported_versions=AUTHOR_COMPATIBILITY,
36
+ representation=representation,
37
+ )
38
+ try:
39
+ parsed = Version(compatibility.author_version)
40
+ except InvalidVersion as exc:
41
+ raise AuthorCompatibilityError(
42
+ f"installed dryv-author version {compatibility.author_version!r} is invalid"
43
+ ) from exc
44
+
45
+ if parsed not in SpecifierSet(compatibility.supported_versions):
46
+ raise AuthorCompatibilityError(
47
+ "installed dryv-author version "
48
+ f"{compatibility.author_version!r} is incompatible with compiler requirement "
49
+ f"{compatibility.supported_versions!r}"
50
+ )
51
+ if compatibility.representation != AUTHOR_REPRESENTATION:
52
+ raise AuthorCompatibilityError(
53
+ "installed dryv-author compiler-facing representation "
54
+ f"{compatibility.representation!r} is incompatible with {AUTHOR_REPRESENTATION!r}"
55
+ )
56
+ return compatibility
57
+
58
+
59
+ def validate_author_compatibility() -> AuthorCompatibility:
60
+ compatibility = current_author_compatibility()
61
+ return validate_author_details(
62
+ compatibility.author_version,
63
+ compatibility.representation,
64
+ )
@@ -0,0 +1,23 @@
1
+ from .entrypoint import AuthorEntrypointError, LoadedAuthor, load_author_entrypoint
2
+ from .environment import EnvironmentStatus, PythonEnvironmentReport, inspect_python_environment
3
+ from .project import (
4
+ CapturedAuthor,
5
+ ProjectPythonError,
6
+ capture_author_entrypoint,
7
+ discover_project_python,
8
+ inspect_project_python,
9
+ )
10
+
11
+ __all__ = [
12
+ "AuthorEntrypointError",
13
+ "CapturedAuthor",
14
+ "EnvironmentStatus",
15
+ "LoadedAuthor",
16
+ "ProjectPythonError",
17
+ "PythonEnvironmentReport",
18
+ "capture_author_entrypoint",
19
+ "discover_project_python",
20
+ "inspect_project_python",
21
+ "inspect_python_environment",
22
+ "load_author_entrypoint",
23
+ ]
@@ -0,0 +1,106 @@
1
+ """Project-Python bootstrap used by the reusable Dryv Python compiler.
2
+
3
+ This file intentionally depends only on the Python standard library and the
4
+ project-installed ``dryv_author`` package. It is executed by the project's
5
+ Python interpreter, not imported as compiler runtime code.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import contextlib
11
+ import json
12
+ import runpy
13
+ import sys
14
+ from dataclasses import fields, is_dataclass
15
+ from enum import Enum
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ from dryv_author import AUTHOR_REPRESENTATION_VERSION, AppDeclaration
20
+ from dryv_author import __version__ as author_version
21
+ from dryv_author.state import DeclarationIdentity
22
+
23
+
24
+ def _type_name(value: object) -> str:
25
+ kind = type(value)
26
+ return f"{kind.__module__}:{kind.__qualname__}"
27
+
28
+
29
+ _IDENTITIES: dict[int, int] = {}
30
+
31
+
32
+ def _encode(value: Any) -> Any:
33
+ if isinstance(value, DeclarationIdentity):
34
+ number = _IDENTITIES.setdefault(id(value), len(_IDENTITIES) + 1)
35
+ return {"$identity": number, "$label": value.label}
36
+ if is_dataclass(value) and not isinstance(value, type):
37
+ return {
38
+ "$type": _type_name(value),
39
+ "$fields": {field.name: _encode(getattr(value, field.name)) for field in fields(value)},
40
+ }
41
+ if isinstance(value, Enum):
42
+ return {"$enum": _type_name(value), "$value": _encode(value.value)}
43
+ if isinstance(value, tuple):
44
+ return {"$tuple": [_encode(item) for item in value]}
45
+ if isinstance(value, list):
46
+ return [_encode(item) for item in value]
47
+ if isinstance(value, dict):
48
+ return {
49
+ "$dict": [
50
+ [_encode(key), _encode(value[key])]
51
+ for key in sorted(value, key=lambda item: str(item))
52
+ ]
53
+ }
54
+ if value is None or isinstance(value, (str, int, float, bool)):
55
+ return value
56
+ raise TypeError(
57
+ f"unsupported author snapshot value {type(value).__module__}.{type(value).__qualname__}"
58
+ )
59
+
60
+
61
+ def _load_author(entrypoint: Path) -> AppDeclaration:
62
+ entry_root = str(entrypoint.parent)
63
+ if entry_root not in sys.path:
64
+ sys.path.insert(0, entry_root)
65
+ with contextlib.redirect_stdout(sys.stderr):
66
+ namespace = runpy.run_path(str(entrypoint), run_name="__dryv_authoring__")
67
+ found: dict[int, AppDeclaration] = {}
68
+ for name in sorted(namespace):
69
+ value = namespace[name]
70
+ if isinstance(value, AppDeclaration):
71
+ found[id(value)] = value
72
+ if not found:
73
+ raise RuntimeError(
74
+ "authoring entrypoint did not expose a dryv_author app (dryv.app(...)); "
75
+ "assign the result of dryv(...) to a module-level name"
76
+ )
77
+ if len(found) > 1:
78
+ raise RuntimeError("authoring entrypoint exposed multiple independent Dryv apps")
79
+ return next(iter(found.values()))
80
+
81
+
82
+ def main() -> int:
83
+ if len(sys.argv) != 2:
84
+ sys.stderr.write("usage: bootstrap.py <authoring-entrypoint>\n")
85
+ return 2
86
+ entrypoint = Path(sys.argv[1]).expanduser().resolve()
87
+ if not entrypoint.is_file():
88
+ sys.stderr.write(f"authoring entrypoint does not exist: {entrypoint}\n")
89
+ return 2
90
+ try:
91
+ author = _load_author(entrypoint)
92
+ payload = {
93
+ "authorVersion": author_version,
94
+ "authorRepresentation": AUTHOR_REPRESENTATION_VERSION,
95
+ "state": _encode(author.snapshot()),
96
+ }
97
+ sys.stdout.write(json.dumps(payload, sort_keys=True, separators=(",", ":")))
98
+ sys.stdout.write("\n")
99
+ return 0
100
+ except Exception as exc:
101
+ sys.stderr.write(f"{type(exc).__name__}: {exc}\n")
102
+ return 1
103
+
104
+
105
+ if __name__ == "__main__":
106
+ raise SystemExit(main())
@@ -0,0 +1,63 @@
1
+ from __future__ import annotations
2
+
3
+ import runpy
4
+ import sys
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+ from dryv_author import AppDeclaration
9
+
10
+
11
+ class AuthorEntrypointError(RuntimeError):
12
+ """Raised when a Python authoring entrypoint cannot yield one declaration graph."""
13
+
14
+
15
+ @dataclass(frozen=True, slots=True)
16
+ class LoadedAuthor:
17
+ author: AppDeclaration
18
+ entrypoint: str
19
+ exported_names: tuple[str, ...]
20
+
21
+
22
+ def load_author_entrypoint(entrypoint: str) -> LoadedAuthor:
23
+ path = Path(entrypoint).expanduser().resolve()
24
+ if not path.is_file():
25
+ raise AuthorEntrypointError(f"authoring entrypoint does not exist: {path}")
26
+ if path.suffix.lower() != ".py":
27
+ raise AuthorEntrypointError("Python authoring entrypoint must use the .py extension")
28
+
29
+ entry_root = str(path.parent)
30
+ added_to_path = entry_root not in sys.path
31
+ if added_to_path:
32
+ sys.path.insert(0, entry_root)
33
+ try:
34
+ namespace = runpy.run_path(str(path), run_name="__dryv_authoring__")
35
+ finally:
36
+ if added_to_path:
37
+ sys.path.remove(entry_root)
38
+ found: dict[int, tuple[AppDeclaration, list[str]]] = {}
39
+ for name in sorted(namespace):
40
+ value = namespace[name]
41
+ if not isinstance(value, AppDeclaration):
42
+ continue
43
+ identity = id(value)
44
+ existing = found.get(identity)
45
+ if existing is None:
46
+ found[identity] = (value, [name])
47
+ else:
48
+ existing[1].append(name)
49
+
50
+ if not found:
51
+ raise AuthorEntrypointError(
52
+ "authoring entrypoint did not expose a dryv_author.AppDeclaration instance; "
53
+ "assign the result of dryv(...) to a module-level name"
54
+ )
55
+ if len(found) > 1:
56
+ exports = sorted(name for _, names in found.values() for name in names)
57
+ raise AuthorEntrypointError(
58
+ "authoring entrypoint exposed multiple independent AppDeclaration graphs: "
59
+ + ", ".join(exports)
60
+ )
61
+
62
+ author, names = next(iter(found.values()))
63
+ return LoadedAuthor(author=author, entrypoint=str(path), exported_names=tuple(sorted(names)))
@@ -0,0 +1,123 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from enum import StrEnum
5
+ from pathlib import Path
6
+
7
+ from dryv_compiler.compatibility import AuthorCompatibilityError, validate_author_details
8
+
9
+ from .project import ProjectPythonError, inspect_project_python
10
+
11
+
12
+ class EnvironmentStatus(StrEnum):
13
+ READY = "READY"
14
+ RUNTIME_MISSING = "RUNTIME_MISSING"
15
+ RUNTIME_INCOMPATIBLE = "RUNTIME_INCOMPATIBLE"
16
+ SDK_MISSING = "SDK_MISSING"
17
+ SDK_INCOMPATIBLE = "SDK_INCOMPATIBLE"
18
+ ENTRYPOINT_MISSING = "ENTRYPOINT_MISSING"
19
+
20
+
21
+ @dataclass(frozen=True, slots=True)
22
+ class PythonEnvironmentReport:
23
+ status: EnvironmentStatus
24
+ python_executable: str
25
+ python_version: str
26
+ project_root: str
27
+ entrypoint: str
28
+ virtual_environment: str | None
29
+ author_version: str
30
+ author_representation: str
31
+ message: str | None = None
32
+
33
+
34
+ def _empty_report(
35
+ status: EnvironmentStatus,
36
+ entrypoint: Path,
37
+ *,
38
+ message: str,
39
+ ) -> PythonEnvironmentReport:
40
+ return PythonEnvironmentReport(
41
+ status=status,
42
+ python_executable="",
43
+ python_version="",
44
+ project_root=str(entrypoint.parent),
45
+ entrypoint=str(entrypoint),
46
+ virtual_environment=None,
47
+ author_version="",
48
+ author_representation="",
49
+ message=message,
50
+ )
51
+
52
+
53
+ def inspect_python_environment(entrypoint: str) -> PythonEnvironmentReport:
54
+ resolved = Path(entrypoint).expanduser().resolve()
55
+ if not resolved.is_file():
56
+ return _empty_report(
57
+ EnvironmentStatus.ENTRYPOINT_MISSING,
58
+ resolved,
59
+ message=f"authoring entrypoint does not exist: {resolved}",
60
+ )
61
+
62
+ try:
63
+ facts = inspect_project_python(str(resolved))
64
+ except ProjectPythonError as exc:
65
+ error_message = str(exc)
66
+ status = (
67
+ EnvironmentStatus.SDK_MISSING
68
+ if "dryv_author" in error_message or "dryv-author" in error_message
69
+ else EnvironmentStatus.RUNTIME_MISSING
70
+ )
71
+ return _empty_report(status, resolved, message=error_message)
72
+
73
+ python_version = str(facts["pythonVersion"] or "")
74
+ try:
75
+ major, minor, *_ = (int(part) for part in python_version.split("."))
76
+ except (TypeError, ValueError):
77
+ return _empty_report(
78
+ EnvironmentStatus.RUNTIME_INCOMPATIBLE,
79
+ resolved,
80
+ message=f"project Python reported invalid version {python_version!r}",
81
+ )
82
+ if (major, minor) < (3, 11):
83
+ return PythonEnvironmentReport(
84
+ status=EnvironmentStatus.RUNTIME_INCOMPATIBLE,
85
+ python_executable=str(facts["pythonExecutable"] or ""),
86
+ python_version=python_version,
87
+ project_root=str(facts["projectRoot"] or resolved.parent),
88
+ entrypoint=str(resolved),
89
+ virtual_environment=(
90
+ str(facts["virtualEnvironment"])
91
+ if facts["virtualEnvironment"] is not None
92
+ else None
93
+ ),
94
+ author_version=str(facts["authorVersion"] or ""),
95
+ author_representation=str(facts["authorRepresentation"] or ""),
96
+ message="dryv-py requires Python 3.11 or newer",
97
+ )
98
+
99
+ author_version = str(facts["authorVersion"] or "")
100
+ representation = str(facts["authorRepresentation"] or "")
101
+ compatibility_message: str | None
102
+ try:
103
+ validate_author_details(author_version, representation)
104
+ except AuthorCompatibilityError as exc:
105
+ status = EnvironmentStatus.SDK_INCOMPATIBLE
106
+ compatibility_message = str(exc)
107
+ else:
108
+ status = EnvironmentStatus.READY
109
+ compatibility_message = None
110
+
111
+ return PythonEnvironmentReport(
112
+ status=status,
113
+ python_executable=str(facts["pythonExecutable"] or ""),
114
+ python_version=python_version,
115
+ project_root=str(facts["projectRoot"] or resolved.parent),
116
+ entrypoint=str(resolved),
117
+ virtual_environment=(
118
+ str(facts["virtualEnvironment"]) if facts["virtualEnvironment"] is not None else None
119
+ ),
120
+ author_version=author_version,
121
+ author_representation=representation,
122
+ message=compatibility_message,
123
+ )