diaspore 0.2.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.
Files changed (44) hide show
  1. diaspore/__init__.py +69 -0
  2. diaspore/cli.py +326 -0
  3. diaspore/compiler.py +1289 -0
  4. diaspore/dspb.py +704 -0
  5. diaspore/encoding.py +143 -0
  6. diaspore/ir.py +464 -0
  7. diaspore/py.typed +0 -0
  8. diaspore/stdlib/_pyparse.py +1749 -0
  9. diaspore/stdlib/_pyunparse.py +253 -0
  10. diaspore/stdlib/ast.py +1338 -0
  11. diaspore/stdlib/asyncio.py +127 -0
  12. diaspore/stdlib/base64.py +128 -0
  13. diaspore/stdlib/binascii.py +44 -0
  14. diaspore/stdlib/collections/__init__.py +182 -0
  15. diaspore/stdlib/collections/abc.py +110 -0
  16. diaspore/stdlib/contextlib.py +116 -0
  17. diaspore/stdlib/copy.py +72 -0
  18. diaspore/stdlib/dataclasses.py +312 -0
  19. diaspore/stdlib/fractions.py +101 -0
  20. diaspore/stdlib/functools.py +78 -0
  21. diaspore/stdlib/gc.py +11 -0
  22. diaspore/stdlib/gzip.py +105 -0
  23. diaspore/stdlib/hashlib.py +187 -0
  24. diaspore/stdlib/io.py +55 -0
  25. diaspore/stdlib/itertools.py +174 -0
  26. diaspore/stdlib/operator.py +188 -0
  27. diaspore/stdlib/os/__init__.py +82 -0
  28. diaspore/stdlib/os/path.py +122 -0
  29. diaspore/stdlib/pathlib.py +361 -0
  30. diaspore/stdlib/queue.py +107 -0
  31. diaspore/stdlib/socket.py +241 -0
  32. diaspore/stdlib/string.py +54 -0
  33. diaspore/stdlib/threading.py +268 -0
  34. diaspore/stdlib/types.py +86 -0
  35. diaspore/stdlib/unittest.py +295 -0
  36. diaspore/stdlib/weakref.py +76 -0
  37. diaspore/stdlib/zlib.py +380 -0
  38. diaspore/validator.py +315 -0
  39. diaspore/vm.py +3905 -0
  40. diaspore-0.2.0.dist-info/METADATA +150 -0
  41. diaspore-0.2.0.dist-info/RECORD +44 -0
  42. diaspore-0.2.0.dist-info/WHEEL +4 -0
  43. diaspore-0.2.0.dist-info/entry_points.txt +2 -0
  44. diaspore-0.2.0.dist-info/licenses/LICENSE +21 -0
diaspore/__init__.py ADDED
@@ -0,0 +1,69 @@
1
+ """Diaspore: compile a Python subset to portable JSON; execute it in sandboxed VMs.
2
+
3
+ The runtime has no third-party dependencies. For embedding:
4
+
5
+ from diaspore import compile_bundle, validate_bundle, Machine
6
+
7
+ bundle = compile_bundle("plugin.py")
8
+ m = Machine(bundle, bindings={"host": {"greet": lambda n: f"hi {n}"}},
9
+ max_steps=1_000_000)
10
+ m.run()
11
+
12
+ Use Machine.run_durable() and Machine.resume() with durable=True for durable
13
+ execution. Schema generation loads Pydantic lazily from diaspore.ir; install
14
+ it with `uv sync --group dev`.
15
+ """
16
+
17
+ from .compiler import DiasporeCompileError, compile_bundle, dumps
18
+ from .encoding import load_bundle_bytes, load_bundle_file, write_bundle_file
19
+ from .validator import BundleValidationError, validate_bundle
20
+ from .vm import (
21
+ Completed,
22
+ DiasporeError,
23
+ DiasporeException,
24
+ Machine,
25
+ Suspended,
26
+ bundle_digest,
27
+ )
28
+
29
+ __version__ = "0.3.0"
30
+
31
+ __all__ = [
32
+ "BundleValidationError",
33
+ "Completed",
34
+ "DiasporeCompileError",
35
+ "DiasporeError",
36
+ "DiasporeException",
37
+ "Machine",
38
+ "Suspended",
39
+ "bundle_digest",
40
+ "bundle_schema",
41
+ "compile_bundle",
42
+ "dumps",
43
+ "load_bundle_bytes",
44
+ "load_bundle_file",
45
+ "validate_bundle",
46
+ "write_bundle_file",
47
+ "__version__",
48
+ ]
49
+
50
+
51
+ def __getattr__(name: str):
52
+ """Load optional schema exports on demand.
53
+
54
+ Args:
55
+ name: Requested module attribute.
56
+
57
+ Returns:
58
+ The schema function or bundle model.
59
+
60
+ Raises:
61
+ AttributeError: The requested attribute is not an optional export.
62
+ """
63
+ if name == "bundle_schema":
64
+ from .ir import bundle_schema
65
+ return bundle_schema
66
+ if name == "Bundle":
67
+ from .ir import Bundle
68
+ return Bundle
69
+ raise AttributeError(f"module 'diaspore' has no attribute {name!r}")
diaspore/cli.py ADDED
@@ -0,0 +1,326 @@
1
+ """Diaspore CLI: build Python into JSON bundles, validate them, run them sandboxed."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ from .compiler import DiasporeCompileError, compile_bundle
11
+ from .encoding import load_bundle_file, write_bundle_file
12
+ from .validator import BundleValidationError, validate_bundle
13
+ from .vm import Machine, Suspended
14
+
15
+
16
+ def _caps_and_policy(args: argparse.Namespace) -> tuple[set, dict | None]:
17
+ caps: set[str] = set()
18
+ policy: dict = {}
19
+ if args.allow_net:
20
+ caps.add("net")
21
+ if not all(s == "all" for s in args.allow_net):
22
+ pol: dict[str, list[str]] = {"listen": [], "connect": []}
23
+ for spec in args.allow_net:
24
+ if spec == "all":
25
+ continue
26
+ kind, _, addr = spec.partition(":")
27
+ if kind not in pol or not addr:
28
+ raise SystemExit(
29
+ f"diaspore: bad --allow-net spec {spec!r} "
30
+ f"(use listen:HOST:PORT or connect:HOST:PORT; if you meant "
31
+ f"a bare grant, put --allow-net after the target or use "
32
+ f"--allow-net=all)")
33
+ pol[kind].append(addr)
34
+ policy["net"] = pol
35
+ if args.allow_fs:
36
+ caps.add("fs")
37
+ if not all(s == "all" for s in args.allow_fs):
38
+ pol = {"read": [], "write": []}
39
+ for spec in args.allow_fs:
40
+ if spec == "all":
41
+ continue
42
+ kind, _, path = spec.partition(":")
43
+ if kind not in pol or not path:
44
+ raise SystemExit(
45
+ f"diaspore: bad --allow-fs spec {spec!r} "
46
+ f"(use read:PATH or write:PATH)")
47
+ pol[kind].append(path)
48
+ policy["fs"] = pol
49
+ if args.allow_spawn:
50
+ caps.add("spawn")
51
+ return caps, (policy or None)
52
+
53
+
54
+ def _finish_durable(outcome, snapshot_path: str) -> int:
55
+ if isinstance(outcome, Suspended):
56
+ Path(snapshot_path).write_text(json.dumps(outcome.snapshot), encoding="utf-8")
57
+ print(f"diaspore: suspended (payload: {json.dumps(outcome.payload)}); "
58
+ f"snapshot -> {snapshot_path}", file=sys.stderr)
59
+ return 7
60
+ return outcome.code
61
+
62
+
63
+ def _load_bundle(target: str, validate: bool, bindings: list[str] | None = None,
64
+ search_paths: list[str] | None = None) -> dict:
65
+ path = Path(target)
66
+ if path.suffix == ".py":
67
+ # Embed source so in-memory compilation retains readable tracebacks.
68
+ bundle = compile_bundle(str(path), extra_modules=set(bindings or ()), debug=True,
69
+ search_paths=search_paths)
70
+ else:
71
+ bundle = load_bundle_file(path)
72
+ if validate:
73
+ validate_bundle(bundle)
74
+ return bundle
75
+
76
+
77
+ def cmd_build(args: argparse.Namespace) -> int:
78
+ """Compile and write a validated bundle.
79
+
80
+ Args:
81
+ args: Parsed build command options.
82
+
83
+ Returns:
84
+ Zero on success.
85
+ """
86
+ bundle = compile_bundle(args.entry, extra_modules=set(args.binding),
87
+ debug=args.debug, search_paths=args.path)
88
+ validate_bundle(bundle)
89
+ out = args.output or str(Path(args.entry).with_suffix(".dsp.json"))
90
+ size = write_bundle_file(bundle, out, pretty=args.pretty)
91
+ n_modules = len(bundle["modules"])
92
+ print(f"diaspore: built {out} ({n_modules} module{'s' if n_modules != 1 else ''}, "
93
+ f"{size} bytes)", file=sys.stderr)
94
+ return 0
95
+
96
+
97
+ def cmd_check(args: argparse.Namespace) -> int:
98
+ """Compile and validate an entry point without writing a bundle.
99
+
100
+ Args:
101
+ args: Parsed check command options.
102
+
103
+ Returns:
104
+ Zero on success.
105
+ """
106
+ unresolved: dict = {}
107
+ bundle = compile_bundle(args.entry, extra_modules=set(args.binding),
108
+ unresolved_out=unresolved, search_paths=args.path)
109
+ validate_bundle(bundle)
110
+ for dep, src in unresolved.items():
111
+ print(f"diaspore: warning: import {dep!r} (from {src}) will raise "
112
+ f"ImportError if executed", file=sys.stderr)
113
+ print(f"diaspore: ok ({', '.join(sorted(bundle['modules']))})", file=sys.stderr)
114
+ return 0
115
+
116
+
117
+ def cmd_run(args: argparse.Namespace) -> int:
118
+ """Run a bundle or Python source file with the requested capabilities.
119
+
120
+ Args:
121
+ args: Parsed run command options and program arguments.
122
+
123
+ Returns:
124
+ The program exit code, or seven when execution suspends.
125
+ """
126
+ bundle = _load_bundle(args.target, validate=not args.no_validate,
127
+ bindings=args.binding, search_paths=args.path)
128
+ caps, policy = _caps_and_policy(args)
129
+ m = Machine(
130
+ bundle,
131
+ argv=[args.target, *args.args],
132
+ caps=caps,
133
+ policy=policy,
134
+ max_steps=args.max_steps,
135
+ max_depth=args.max_depth,
136
+ durable=args.durable,
137
+ deterministic=args.deterministic,
138
+ )
139
+ if args.durable:
140
+ snap = args.snapshot or f"{args.target}.flow.json"
141
+ return _finish_durable(m.run_durable(), snap)
142
+ return m.run()
143
+
144
+
145
+ def cmd_resume(args: argparse.Namespace) -> int:
146
+ """Resume a durable execution from a snapshot.
147
+
148
+ Args:
149
+ args: Parsed resume command options.
150
+
151
+ Returns:
152
+ The program exit code, or seven when execution suspends.
153
+ """
154
+ bundle = _load_bundle(args.bundle, validate=not args.no_validate)
155
+ try:
156
+ with open(args.snapshot_in, encoding="utf-8") as f:
157
+ snapshot = json.load(f)
158
+ except json.JSONDecodeError as e:
159
+ raise SystemExit(f"diaspore: bad snapshot {args.snapshot_in}: {e.msg}") from None
160
+ try:
161
+ send = json.loads(args.send) if args.send is not None else None
162
+ except json.JSONDecodeError:
163
+ raise SystemExit(f"diaspore: --send must be JSON (got {args.send!r})") from None
164
+ try:
165
+ m = Machine(bundle, argv=snapshot.get("argv") or None, durable=True,
166
+ max_steps=args.max_steps, max_depth=args.max_depth)
167
+ outcome = m.resume(snapshot, send)
168
+ except ValueError as e:
169
+ raise SystemExit(f"diaspore: {e}") from None
170
+ out = args.snapshot or args.snapshot_in
171
+ return _finish_durable(outcome, out)
172
+
173
+
174
+ def cmd_schema(args: argparse.Namespace) -> int:
175
+ """Export the bundle JSON Schema.
176
+
177
+ Args:
178
+ args: Parsed schema command options.
179
+
180
+ Returns:
181
+ Zero on success, or two if schema dependencies are unavailable.
182
+ """
183
+ try:
184
+ from .ir import bundle_schema # Keep Pydantic optional for other commands.
185
+ except ImportError:
186
+ print("diaspore: schema export needs the dev tools: uv sync --group dev "
187
+ "(runtime validation itself is dependency-free)", file=sys.stderr)
188
+ return 2
189
+ text = json.dumps(bundle_schema(), indent=2)
190
+ if args.output:
191
+ Path(args.output).write_text(text + "\n", encoding="utf-8")
192
+ print(f"diaspore: wrote {args.output}", file=sys.stderr)
193
+ else:
194
+ print(text)
195
+ return 0
196
+
197
+
198
+ def cmd_dis(args: argparse.Namespace) -> int:
199
+ """Print a validated bundle as indented JSON.
200
+
201
+ Args:
202
+ args: Parsed disassembly command options.
203
+
204
+ Returns:
205
+ Zero on success.
206
+ """
207
+ bundle = _load_bundle(args.target, validate=True)
208
+ print(json.dumps(bundle, indent=2, ensure_ascii=False))
209
+ return 0
210
+
211
+
212
+ def build_parser() -> argparse.ArgumentParser:
213
+ """Build the command-line parser.
214
+
215
+ Returns:
216
+ A parser for all Diaspore commands.
217
+ """
218
+ p = argparse.ArgumentParser(
219
+ prog="diaspore",
220
+ description="Compile a Python subset to portable JSON; run it in a sandboxed VM.",
221
+ )
222
+ from . import __version__
223
+ p.add_argument("--version", action="version", version=f"diaspore {__version__}")
224
+ sub = p.add_subparsers(dest="cmd", required=True)
225
+
226
+ b = sub.add_parser("build", help="compile entry.py (and its imports) to a .dsp.json bundle")
227
+ b.add_argument("entry")
228
+ b.add_argument("-o", "--output")
229
+ b.add_argument("--pretty", action="store_true", help="indented JSON output")
230
+ b.add_argument("--binding", action="append", default=[], metavar="NAME",
231
+ help="declare a host-binding module (repeatable)")
232
+ b.add_argument("--path", action="append", default=[], metavar="DIR",
233
+ help="add a module search directory (repeatable)")
234
+ b.add_argument("--debug", action="store_true",
235
+ help="embed source lines for source-mapped tracebacks")
236
+ b.set_defaults(fn=cmd_build)
237
+
238
+ c = sub.add_parser("check", help="compile + validate without writing output")
239
+ c.add_argument("entry")
240
+ c.add_argument("--binding", action="append", default=[], metavar="NAME")
241
+ c.add_argument("--path", action="append", default=[], metavar="DIR",
242
+ help="add a module search directory (repeatable)")
243
+ c.set_defaults(fn=cmd_check)
244
+
245
+ r = sub.add_parser("run", help="run a .dsp.json bundle (or a .py, compiled in-memory)")
246
+ r.add_argument("target")
247
+ r.add_argument("args", nargs="*", help="argv for the program (after --)")
248
+ r.add_argument("--allow-net", nargs="?", const="all", action="append", default=[],
249
+ metavar="SPEC",
250
+ help="grant net; optionally scoped (listen:HOST:PORT / "
251
+ "connect:HOST:PORT, '*' wildcards; repeatable)")
252
+ r.add_argument("--allow-fs", nargs="?", const="all", action="append", default=[],
253
+ metavar="SPEC",
254
+ help="grant fs; optionally scoped (read:PATH / write:PATH; repeatable)")
255
+ r.add_argument("--allow-spawn", action="store_true",
256
+ help="grant the 'spawn' capability (tasks module)")
257
+ r.add_argument("--max-steps", type=int, default=None, help="step budget (trap on exhaustion)")
258
+ r.add_argument("--max-depth", type=int, default=1000, help="max diaspore recursion depth")
259
+ r.add_argument("--no-validate", action="store_true", help="skip IR schema validation")
260
+ r.add_argument("--binding", action="append", default=[], metavar="NAME",
261
+ help="declare a host-binding module (repeatable; .py targets only)")
262
+ r.add_argument("--path", action="append", default=[], metavar="DIR",
263
+ help="add a module search directory (repeatable; .py targets only)")
264
+ r.add_argument("--durable", action="store_true",
265
+ help="durable run: flow.suspend() pauses to a portable snapshot")
266
+ r.add_argument("--snapshot", metavar="PATH",
267
+ help="where to write the snapshot on suspend (default: TARGET.flow.json)")
268
+ r.add_argument("--deterministic", action="store_true",
269
+ help="logical clock + seeded random (replayable runs)")
270
+ r.set_defaults(fn=cmd_run)
271
+
272
+ z = sub.add_parser("resume", help="resume a suspended durable run from its snapshot")
273
+ z.add_argument("bundle", help="the same bundle the snapshot was taken from")
274
+ z.add_argument("snapshot_in", metavar="snapshot", help="snapshot file from a suspended run")
275
+ z.add_argument("--send", metavar="JSON",
276
+ help="value returned by the pending flow.suspend() (default null)")
277
+ z.add_argument("--snapshot", metavar="PATH",
278
+ help="snapshot output on re-suspend (default: overwrite input)")
279
+ z.add_argument("--max-steps", type=int, default=None)
280
+ z.add_argument("--max-depth", type=int, default=1000)
281
+ z.add_argument("--no-validate", action="store_true")
282
+ z.set_defaults(fn=cmd_resume)
283
+
284
+ s = sub.add_parser("schema", help="print the IR JSON Schema")
285
+ s.add_argument("-o", "--output")
286
+ s.set_defaults(fn=cmd_schema)
287
+
288
+ d = sub.add_parser("dis", help="pretty-print a bundle's JSON")
289
+ d.add_argument("target")
290
+ d.set_defaults(fn=cmd_dis)
291
+
292
+ return p
293
+
294
+
295
+ def main(argv: list[str] | None = None) -> int:
296
+ """Dispatch a command and report compilation, validation, or file errors.
297
+
298
+ Args:
299
+ argv: Command-line arguments, or None to use sys.argv.
300
+
301
+ Returns:
302
+ The command exit code, or two for a reported error.
303
+ """
304
+ argv = list(sys.argv[1:] if argv is None else argv)
305
+ prog_args: list[str] = []
306
+ if "--" in argv:
307
+ cut = argv.index("--")
308
+ argv, prog_args = argv[:cut], argv[cut + 1:]
309
+ args = build_parser().parse_args(argv)
310
+ if hasattr(args, "args"):
311
+ args.args = list(args.args) + prog_args
312
+ try:
313
+ return args.fn(args)
314
+ except DiasporeCompileError as e:
315
+ print(f"diaspore: compile error: {e}", file=sys.stderr)
316
+ return 2
317
+ except BundleValidationError as e:
318
+ print(f"diaspore: invalid bundle: {e}", file=sys.stderr)
319
+ return 2
320
+ except FileNotFoundError as e:
321
+ print(f"diaspore: {e}", file=sys.stderr)
322
+ return 2
323
+
324
+
325
+ if __name__ == "__main__":
326
+ sys.exit(main())