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.
- diaspore/__init__.py +69 -0
- diaspore/cli.py +326 -0
- diaspore/compiler.py +1289 -0
- diaspore/dspb.py +704 -0
- diaspore/encoding.py +143 -0
- diaspore/ir.py +464 -0
- diaspore/py.typed +0 -0
- diaspore/stdlib/_pyparse.py +1749 -0
- diaspore/stdlib/_pyunparse.py +253 -0
- diaspore/stdlib/ast.py +1338 -0
- diaspore/stdlib/asyncio.py +127 -0
- diaspore/stdlib/base64.py +128 -0
- diaspore/stdlib/binascii.py +44 -0
- diaspore/stdlib/collections/__init__.py +182 -0
- diaspore/stdlib/collections/abc.py +110 -0
- diaspore/stdlib/contextlib.py +116 -0
- diaspore/stdlib/copy.py +72 -0
- diaspore/stdlib/dataclasses.py +312 -0
- diaspore/stdlib/fractions.py +101 -0
- diaspore/stdlib/functools.py +78 -0
- diaspore/stdlib/gc.py +11 -0
- diaspore/stdlib/gzip.py +105 -0
- diaspore/stdlib/hashlib.py +187 -0
- diaspore/stdlib/io.py +55 -0
- diaspore/stdlib/itertools.py +174 -0
- diaspore/stdlib/operator.py +188 -0
- diaspore/stdlib/os/__init__.py +82 -0
- diaspore/stdlib/os/path.py +122 -0
- diaspore/stdlib/pathlib.py +361 -0
- diaspore/stdlib/queue.py +107 -0
- diaspore/stdlib/socket.py +241 -0
- diaspore/stdlib/string.py +54 -0
- diaspore/stdlib/threading.py +268 -0
- diaspore/stdlib/types.py +86 -0
- diaspore/stdlib/unittest.py +295 -0
- diaspore/stdlib/weakref.py +76 -0
- diaspore/stdlib/zlib.py +380 -0
- diaspore/validator.py +315 -0
- diaspore/vm.py +3905 -0
- diaspore-0.2.0.dist-info/METADATA +150 -0
- diaspore-0.2.0.dist-info/RECORD +44 -0
- diaspore-0.2.0.dist-info/WHEEL +4 -0
- diaspore-0.2.0.dist-info/entry_points.txt +2 -0
- 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())
|