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.
- dryv_compiler/__init__.py +21 -0
- dryv_compiler/__main__.py +3 -0
- dryv_compiler/cli.py +159 -0
- dryv_compiler/compatibility.py +64 -0
- dryv_compiler/execution/__init__.py +23 -0
- dryv_compiler/execution/bootstrap.py +106 -0
- dryv_compiler/execution/entrypoint.py +63 -0
- dryv_compiler/execution/environment.py +123 -0
- dryv_compiler/execution/project.py +245 -0
- dryv_compiler/mapping/__init__.py +4 -0
- dryv_compiler/mapping/compile.py +90 -0
- dryv_compiler/mapping/encode.py +277 -0
- dryv_compiler/mapping/index.py +174 -0
- dryv_compiler/protocol/__init__.py +42 -0
- dryv_compiler/protocol/contracts.py +180 -0
- dryv_compiler/protocol/encoding.py +127 -0
- dryv_compiler/protocol/parsing.py +81 -0
- dryv_compiler/protocol/records.py +102 -0
- dryv_compiler/protocol/session.py +237 -0
- dryv_compiler/version.py +18 -0
- dryv_compiler-0.0.1.dist-info/METADATA +120 -0
- dryv_compiler-0.0.1.dist-info/RECORD +24 -0
- dryv_compiler-0.0.1.dist-info/WHEEL +4 -0
- dryv_compiler-0.0.1.dist-info/entry_points.txt +2 -0
|
@@ -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
|
+
]
|
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
|
+
)
|