maprt-emulator 0.1.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.
- maprt_emulator/__init__.py +11 -0
- maprt_emulator/__main__.py +6 -0
- maprt_emulator/app.py +182 -0
- maprt_emulator/auth.py +105 -0
- maprt_emulator/check.py +262 -0
- maprt_emulator/cli.py +378 -0
- maprt_emulator/data/maps/demo_hfs_lowres.txt +1334 -0
- maprt_emulator/data/maps/index.json +17 -0
- maprt_emulator/data/patients.json +22 -0
- maprt_emulator/data/rooms.json +26 -0
- maprt_emulator/data/surfaces/17c0b9d4-3e8a-4f21-b6c5-9d20e8713f4a.obj +28808 -0
- maprt_emulator/data/surfaces/a6f3fbee-6714-4f92-80c1-f5aadda4cbeb.obj +28808 -0
- maprt_emulator/data/surfaces/dad7394f-6893-4ce3-96ed-8bc3091cf4c3.obj +28808 -0
- maprt_emulator/envelope.py +123 -0
- maprt_emulator/inventory.py +322 -0
- maprt_emulator/library.py +430 -0
- maprt_emulator/mapgen.py +333 -0
- maprt_emulator/maps.py +176 -0
- maprt_emulator/obj.py +103 -0
- maprt_emulator/packs.py +438 -0
- maprt_emulator/replies.py +98 -0
- maprt_emulator/routes/__init__.py +1 -0
- maprt_emulator/routes/beams.py +166 -0
- maprt_emulator/routes/getmap.py +270 -0
- maprt_emulator/routes/inventory.py +23 -0
- maprt_emulator/routes/ping.py +26 -0
- maprt_emulator/routes/rooms.py +61 -0
- maprt_emulator/routes/surfaces.py +114 -0
- maprt_emulator/scenarios.py +215 -0
- maprt_emulator/schemas.py +153 -0
- maprt_emulator/settings.py +239 -0
- maprt_emulator/store.py +210 -0
- maprt_emulator-0.1.0.dist-info/METADATA +179 -0
- maprt_emulator-0.1.0.dist-info/RECORD +38 -0
- maprt_emulator-0.1.0.dist-info/WHEEL +4 -0
- maprt_emulator-0.1.0.dist-info/entry_points.txt +2 -0
- maprt_emulator-0.1.0.dist-info/licenses/LICENSE +21 -0
- maprt_emulator-0.1.0.dist-info/licenses/LICENSE-packs +396 -0
maprt_emulator/cli.py
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
"""``maprt-emulator`` — starting the emulator, and the token banner.
|
|
2
|
+
|
|
3
|
+
The emulator mints a **fresh token every time it starts** and makes it available two ways:
|
|
4
|
+
|
|
5
|
+
* printed in a banner, for someone watching a terminal;
|
|
6
|
+
* written to ``.maprt_emulator/token``, for everyone else.
|
|
7
|
+
|
|
8
|
+
The file is not a convenience, it is the only thing that works on Binder and Colab, where the
|
|
9
|
+
server runs as a background process and nobody ever sees its output.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import uvicorn
|
|
18
|
+
|
|
19
|
+
from maprt_emulator.app import create_app
|
|
20
|
+
from maprt_emulator.library import load_library
|
|
21
|
+
from maprt_emulator.maps import MapLibrary, MapLibraryError
|
|
22
|
+
from maprt_emulator.scenarios import SCENARIOS, catalogue
|
|
23
|
+
from maprt_emulator.settings import LICENSABLE, EmulatorSettings
|
|
24
|
+
from maprt_emulator.store import Catalog, CatalogError
|
|
25
|
+
|
|
26
|
+
BANNER_WIDTH = 74
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _stdout_can_print_the_banner():
|
|
30
|
+
"""Switch stdout to UTF-8 if its encoding can't hold the banner's box and dash characters.
|
|
31
|
+
|
|
32
|
+
On Windows a redirected or piped stdout (a launcher capturing the output, a log file) is
|
|
33
|
+
cp1252, and printing the box there raised ``UnicodeEncodeError`` before the server started.
|
|
34
|
+
"""
|
|
35
|
+
try:
|
|
36
|
+
"╭─╮│╰╯—".encode(sys.stdout.encoding or "ascii")
|
|
37
|
+
except (UnicodeEncodeError, LookupError):
|
|
38
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
39
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _box(lines):
|
|
43
|
+
inner = BANNER_WIDTH - 2
|
|
44
|
+
out = ["╭" + "─" * inner + "╮"]
|
|
45
|
+
for line in lines:
|
|
46
|
+
out.append("│" + line.ljust(inner)[:inner] + "│")
|
|
47
|
+
out.append("╰" + "─" * inner + "╯")
|
|
48
|
+
return "\n".join(out)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _elide(path, width):
|
|
52
|
+
"""Shorten a path to ``width``, dropping leading components rather than trailing ones.
|
|
53
|
+
|
|
54
|
+
Truncating from the right hides the part that matters — *which* data directory this is — and
|
|
55
|
+
cuts mid-word, which reads like a bug in the banner rather than a deliberate shortening.
|
|
56
|
+
"""
|
|
57
|
+
text = str(path)
|
|
58
|
+
if len(text) <= width:
|
|
59
|
+
return text
|
|
60
|
+
parts = text.split("/")
|
|
61
|
+
for start in range(1, len(parts)):
|
|
62
|
+
candidate = ".../" + "/".join(parts[start:])
|
|
63
|
+
if len(candidate) <= width:
|
|
64
|
+
return candidate
|
|
65
|
+
# A single component longer than the whole field: nothing to drop, so cut the front.
|
|
66
|
+
return "..." + text[-(width - 3):]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def banner(settings, catalog, token_path, maps=None, library=None):
|
|
70
|
+
"""The startup box, plus the copy-paste line for a notebook, and what the packs need said."""
|
|
71
|
+
serving_packs = library is not None and bool(library.packs)
|
|
72
|
+
warnings = []
|
|
73
|
+
if settings.scenario != "ok":
|
|
74
|
+
warnings.append(f" Scenario {settings.scenario}: {SCENARIOS[settings.scenario].description}")
|
|
75
|
+
if settings.map_source != "auto":
|
|
76
|
+
warnings.append(f" Maps --map-source {settings.map_source}")
|
|
77
|
+
if settings.shuffle_map:
|
|
78
|
+
warnings.append(" Maps triples served in random order")
|
|
79
|
+
if settings.unlicensed:
|
|
80
|
+
warnings.append(f" Unlicensed {', '.join(settings.unlicensed)}")
|
|
81
|
+
if settings.auth_401:
|
|
82
|
+
warnings.append(" Auth a token problem returns HTTP 401 (an appliance sends 200)")
|
|
83
|
+
if not settings.strict_user_agent:
|
|
84
|
+
warnings.append(" User-Agent not enforced (an appliance answers VersionMismatch)")
|
|
85
|
+
if settings.open_ping:
|
|
86
|
+
warnings.append(" Ping served WITHOUT a token")
|
|
87
|
+
if settings.source_headers:
|
|
88
|
+
warnings.append(" Headers X-Emulator-* source headers sent (an appliance sends none)")
|
|
89
|
+
|
|
90
|
+
lines = [
|
|
91
|
+
" MapRT Emulator — NOT a medical device. Teaching only.",
|
|
92
|
+
"",
|
|
93
|
+
f" Base URL {settings.base_url}",
|
|
94
|
+
f" Docs {settings.base_url}/docs",
|
|
95
|
+
*([" (Try it out needs --lenient-user-agent)"] if settings.strict_user_agent else []),
|
|
96
|
+
*_data_lines(settings, catalog, maps, library),
|
|
97
|
+
f" Token file {token_path}",
|
|
98
|
+
]
|
|
99
|
+
if serving_packs and library.warnings:
|
|
100
|
+
warnings.append(" Appliance packs from several versions; see below")
|
|
101
|
+
if warnings:
|
|
102
|
+
lines.extend(["", *warnings])
|
|
103
|
+
|
|
104
|
+
return "\n".join([
|
|
105
|
+
"",
|
|
106
|
+
_box(lines),
|
|
107
|
+
*_pack_report(library),
|
|
108
|
+
"",
|
|
109
|
+
"Copy this line into your notebook (or let it read the token file):",
|
|
110
|
+
"",
|
|
111
|
+
f" MAPRT_EMULATOR_TOKEN={settings.resolve_token()}",
|
|
112
|
+
"",
|
|
113
|
+
"Press Ctrl+C to stop the server.",
|
|
114
|
+
"",
|
|
115
|
+
])
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _data_lines(settings, catalog, maps, library):
|
|
119
|
+
"""The banner's lines about what is served: the packs, or the catalog."""
|
|
120
|
+
width = BANNER_WIDTH - 16
|
|
121
|
+
if library is not None and library.packs:
|
|
122
|
+
appliance = library.appliance
|
|
123
|
+
first, *rest = _wrap(library.summary(), BANNER_WIDTH - 16)
|
|
124
|
+
return [f" Packs {first}", *(f" {part}" for part in rest),
|
|
125
|
+
f" {_elide(settings.packs_dir, width)}",
|
|
126
|
+
f" Appliance {appliance['server']} (API {appliance['apiVersion']})"]
|
|
127
|
+
lines = [f" Data {catalog.summary()}" + (f" · {maps.summary()}" if maps else ""),
|
|
128
|
+
f" {_elide(settings.data_dir, width)}"]
|
|
129
|
+
if settings.no_packs:
|
|
130
|
+
lines.append(" Packs off (--no-packs)")
|
|
131
|
+
elif library is not None:
|
|
132
|
+
lines.append(f" Packs none in {_elide(settings.packs_dir, width - 8)}")
|
|
133
|
+
return lines
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _wrap(text, width):
|
|
137
|
+
"""``text`` as lines of at most ``width``, broken after its ``;`` and ``,`` separators."""
|
|
138
|
+
lines = []
|
|
139
|
+
for clause in text.split("; "):
|
|
140
|
+
line = ""
|
|
141
|
+
for part in clause.split(", "):
|
|
142
|
+
candidate = f"{line}, {part}" if line else part
|
|
143
|
+
if line and len(candidate) + 1 > width:
|
|
144
|
+
lines.append(line + ",")
|
|
145
|
+
line = part
|
|
146
|
+
else:
|
|
147
|
+
line = candidate
|
|
148
|
+
lines.append(line)
|
|
149
|
+
return lines
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _pack_report(library):
|
|
153
|
+
"""Below the box: each pack warning and notice once, and every skipped pack with its findings."""
|
|
154
|
+
if library is None or not (library.warnings or library.notices or library.skipped):
|
|
155
|
+
return []
|
|
156
|
+
lines = ["", "Packs:"]
|
|
157
|
+
lines += [f" ! {warning}" for warning in library.warnings]
|
|
158
|
+
for entry in library.skipped:
|
|
159
|
+
lines.append(f" ! Skipped {entry.name or entry.path} ({entry.path}), which failed its checks:")
|
|
160
|
+
lines += [f" - {finding}" for finding in entry.findings]
|
|
161
|
+
lines += [f" · {notice}" for notice in library.notices]
|
|
162
|
+
return lines
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _parser(prog="maprt-emulator"):
|
|
166
|
+
parser = argparse.ArgumentParser(
|
|
167
|
+
prog=prog,
|
|
168
|
+
description="Run a local emulator of the Vision RT MapRT API 2.0. Teaching use only.",
|
|
169
|
+
epilog="maprt-emulator inventory [the same options] prints, as Markdown, what those options "
|
|
170
|
+
"would serve, without starting a server (also at /inventory and /inventory.md).",
|
|
171
|
+
)
|
|
172
|
+
parser.add_argument("--host", default=None, help="interface to bind (default 127.0.0.1)")
|
|
173
|
+
parser.add_argument("--port", type=int, default=None, help="port to bind (default 8000)")
|
|
174
|
+
parser.add_argument("--token", default=None,
|
|
175
|
+
help="use this API token instead of generating a fresh one")
|
|
176
|
+
parser.add_argument("--data-dir", default=None,
|
|
177
|
+
help="the catalog to serve (default: the nearest data/MapRT/ directory)")
|
|
178
|
+
parser.add_argument("--packs-dir", default=None,
|
|
179
|
+
help="serve the session packs in this folder (every .zip, and every folder "
|
|
180
|
+
"with a manifest.json); they replace the catalog")
|
|
181
|
+
parser.add_argument("--pack", action="append", default=None, metavar="NAME",
|
|
182
|
+
help="serve only this pack from --packs-dir, by its manifest name "
|
|
183
|
+
"(repeatable); a named pack that fails its checks refuses startup")
|
|
184
|
+
parser.add_argument("--no-packs", action="store_true",
|
|
185
|
+
help="read no pack, even with MAPRT_EMULATOR_PACKS_DIR set: serve the catalog")
|
|
186
|
+
parser.add_argument("--state-dir", default=None,
|
|
187
|
+
help="where to write the token file (default .maprt_emulator)")
|
|
188
|
+
parser.add_argument("--unlicensed", default=None,
|
|
189
|
+
help=f"comma-separated features to report as unlicensed: "
|
|
190
|
+
f"{', '.join(LICENSABLE)}")
|
|
191
|
+
parser.add_argument("--scenario", default=None, choices=sorted(SCENARIOS),
|
|
192
|
+
help="arm a fault for the whole run (default ok: none); see --list-scenarios")
|
|
193
|
+
parser.add_argument("--list-scenarios", action="store_true",
|
|
194
|
+
help="describe every scenario and exit")
|
|
195
|
+
parser.add_argument("--latency-ms", type=int, default=None,
|
|
196
|
+
help="artificial delay per request, so calls are visibly not free")
|
|
197
|
+
parser.add_argument("--map-source", default=None, choices=("auto", "recorded", "synthetic"),
|
|
198
|
+
help="auto prefers a recorded map; recorded refuses to invent one; "
|
|
199
|
+
"synthetic ignores the recordings (default auto)")
|
|
200
|
+
parser.add_argument("--seed", type=int, default=None,
|
|
201
|
+
help="varies the clearance model slightly; same seed, same maps")
|
|
202
|
+
parser.add_argument("--map-seconds", type=float, default=None,
|
|
203
|
+
help="roughly how long GetMap takes to stream (default 3; a real "
|
|
204
|
+
"appliance takes minutes)")
|
|
205
|
+
parser.add_argument("--shuffle-map", action="store_true",
|
|
206
|
+
help="randomise the order of the map's triples; the spec says the list "
|
|
207
|
+
"is unordered, so a client must not depend on it")
|
|
208
|
+
parser.add_argument("--auth-401", action="store_true",
|
|
209
|
+
help="answer a token problem with HTTP 401 instead of the appliance's 200")
|
|
210
|
+
parser.add_argument("--lenient-user-agent", action="store_true",
|
|
211
|
+
help="accept any User-Agent (only log it); an appliance answers one that "
|
|
212
|
+
"isn't VisionRT.Integration.Saturn/<2.0 or older> with VersionMismatch")
|
|
213
|
+
parser.add_argument("--open-ping", action="store_true",
|
|
214
|
+
help="serve /integration/ping without a token (debugging aid)")
|
|
215
|
+
parser.add_argument("--source-headers", action="store_true",
|
|
216
|
+
help="name the answering source (recorded or synthetic) in X-Emulator-* "
|
|
217
|
+
"response headers; an appliance sends none, so they are off by default")
|
|
218
|
+
parser.add_argument("--check-data", action="store_true",
|
|
219
|
+
help="validate the data directory (and the packs, if any) and exit, "
|
|
220
|
+
"without starting a server")
|
|
221
|
+
parser.add_argument("--reload", action="store_true", help="reload on code changes (developing)")
|
|
222
|
+
parser.add_argument("--log-level", default="info", help="uvicorn log level")
|
|
223
|
+
return parser
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def settings_from_args(args):
|
|
227
|
+
"""Turn parsed arguments into :class:`EmulatorSettings`, leaving unset flags to the environment."""
|
|
228
|
+
overrides = {}
|
|
229
|
+
if args.host is not None:
|
|
230
|
+
overrides["host"] = args.host
|
|
231
|
+
if args.port is not None:
|
|
232
|
+
overrides["port"] = args.port
|
|
233
|
+
if args.token is not None:
|
|
234
|
+
overrides["token"] = args.token
|
|
235
|
+
if args.data_dir is not None:
|
|
236
|
+
overrides["data_dir"] = Path(args.data_dir)
|
|
237
|
+
if args.state_dir is not None:
|
|
238
|
+
overrides["state_dir"] = Path(args.state_dir)
|
|
239
|
+
if args.no_packs and (args.packs_dir is not None or args.pack):
|
|
240
|
+
raise SystemExit("maprt-emulator: --no-packs reads no pack, so it can't be given with "
|
|
241
|
+
"--packs-dir or --pack.")
|
|
242
|
+
if args.packs_dir is not None:
|
|
243
|
+
overrides["packs_dir"] = Path(args.packs_dir)
|
|
244
|
+
if args.pack:
|
|
245
|
+
overrides["packs"] = tuple(args.pack)
|
|
246
|
+
if args.latency_ms is not None:
|
|
247
|
+
overrides["latency_ms"] = args.latency_ms
|
|
248
|
+
if args.map_source is not None:
|
|
249
|
+
overrides["map_source"] = args.map_source
|
|
250
|
+
if args.seed is not None:
|
|
251
|
+
overrides["seed"] = args.seed
|
|
252
|
+
if args.scenario is not None:
|
|
253
|
+
overrides["scenario"] = args.scenario
|
|
254
|
+
if args.map_seconds is not None:
|
|
255
|
+
overrides["map_seconds"] = args.map_seconds
|
|
256
|
+
if args.unlicensed is not None:
|
|
257
|
+
features = tuple(f.strip() for f in args.unlicensed.split(",") if f.strip())
|
|
258
|
+
unknown = [f for f in features if f not in LICENSABLE]
|
|
259
|
+
if unknown:
|
|
260
|
+
raise SystemExit(f"maprt-emulator: unknown feature(s) {', '.join(unknown)}. "
|
|
261
|
+
f"Choose from: {', '.join(LICENSABLE)}")
|
|
262
|
+
overrides["unlicensed"] = features
|
|
263
|
+
# Store-true flags: only pass them when set, so the environment can still turn them on.
|
|
264
|
+
for flag in ("auth_401", "open_ping", "shuffle_map", "source_headers", "no_packs"):
|
|
265
|
+
if getattr(args, flag):
|
|
266
|
+
overrides[flag] = True
|
|
267
|
+
if args.lenient_user_agent:
|
|
268
|
+
overrides["strict_user_agent"] = False
|
|
269
|
+
return EmulatorSettings(**overrides)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def list_scenarios():
|
|
273
|
+
"""Every scenario: what it does, and what it is there to teach."""
|
|
274
|
+
lines = []
|
|
275
|
+
for scenario in catalogue():
|
|
276
|
+
lines += [scenario["name"], f" {scenario['description']}"]
|
|
277
|
+
if scenario["teaches"]:
|
|
278
|
+
lines.append(f" Teaches: {scenario['teaches']}")
|
|
279
|
+
lines.append("")
|
|
280
|
+
return "\n".join(lines)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def inventory(argv):
|
|
284
|
+
"""``maprt-emulator inventory [--packs-dir …] [--pack …] [--no-packs] [--data-dir …]``: print
|
|
285
|
+
the inventory page's Markdown for what these settings would serve, without starting a server."""
|
|
286
|
+
settings = settings_from_args(_parser("maprt-emulator inventory").parse_args(argv))
|
|
287
|
+
from maprt_emulator import inventory as page
|
|
288
|
+
|
|
289
|
+
try:
|
|
290
|
+
app = create_app(settings)
|
|
291
|
+
except CatalogError as exc:
|
|
292
|
+
raise SystemExit(f"maprt-emulator: {exc}") from exc
|
|
293
|
+
_stdout_can_print_the_banner()
|
|
294
|
+
print(page.markdown(page.build(app)), end="")
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def main(argv=None):
|
|
298
|
+
argv = sys.argv[1:] if argv is None else list(argv)
|
|
299
|
+
if argv[:1] == ["inventory"]:
|
|
300
|
+
inventory(argv[1:])
|
|
301
|
+
return
|
|
302
|
+
args = _parser().parse_args(argv)
|
|
303
|
+
if args.list_scenarios:
|
|
304
|
+
print(list_scenarios())
|
|
305
|
+
return
|
|
306
|
+
settings = settings_from_args(args)
|
|
307
|
+
|
|
308
|
+
if args.check_data:
|
|
309
|
+
# Before the catalog load below, because that one raises on the first problem it meets.
|
|
310
|
+
# The checker reports everything at once, which is what you want while editing data.
|
|
311
|
+
from maprt_emulator.check import main as check_main
|
|
312
|
+
raise SystemExit(check_main(settings.data_dir, settings))
|
|
313
|
+
|
|
314
|
+
# Load the catalog here, before uvicorn starts, so a data problem is a clear message at the
|
|
315
|
+
# prompt rather than a traceback inside a worker during someone's first notebook cell.
|
|
316
|
+
try:
|
|
317
|
+
catalog = Catalog.load(settings.data_dir)
|
|
318
|
+
except CatalogError as exc:
|
|
319
|
+
raise SystemExit(f"maprt-emulator: {exc}") from exc
|
|
320
|
+
|
|
321
|
+
try:
|
|
322
|
+
maps = MapLibrary.load(settings.data_dir)
|
|
323
|
+
except MapLibraryError as exc:
|
|
324
|
+
raise SystemExit(f"maprt-emulator: {exc}") from exc
|
|
325
|
+
|
|
326
|
+
# The packs too, once, here: a refusal is a message at the prompt, the banner reports what was
|
|
327
|
+
# loaded, and the app is handed the same library rather than loading it again.
|
|
328
|
+
try:
|
|
329
|
+
library = load_library(settings)
|
|
330
|
+
except CatalogError as exc:
|
|
331
|
+
raise SystemExit(f"maprt-emulator: {exc}") from exc
|
|
332
|
+
|
|
333
|
+
packs_report = {"off": True} if settings.no_packs else (library.report() if library is not None else None)
|
|
334
|
+
token_path = settings.write_token(packs=packs_report)
|
|
335
|
+
_stdout_can_print_the_banner()
|
|
336
|
+
print(banner(settings, catalog, token_path, maps, library), flush=True)
|
|
337
|
+
|
|
338
|
+
# Build the app HERE, from the settings we just parsed and just described in the banner, and
|
|
339
|
+
# hand uvicorn the object. The alternative — giving uvicorn an import string and letting it
|
|
340
|
+
# call create_app() itself — re-resolves EmulatorSettings from the environment in the server
|
|
341
|
+
# process, so every command-line flag is silently dropped while the banner still claims it
|
|
342
|
+
# is in effect. That is worse than not having the flags.
|
|
343
|
+
#
|
|
344
|
+
# --reload is the one case that cannot work this way: reloading means re-importing in a child
|
|
345
|
+
# process, which needs an import string. There, the settings go through the environment
|
|
346
|
+
# instead, which is why they all have to be environment-expressible.
|
|
347
|
+
if args.reload:
|
|
348
|
+
os.environ.update(settings.as_env())
|
|
349
|
+
target, factory = "maprt_emulator.app:create_app", True
|
|
350
|
+
else:
|
|
351
|
+
target, factory = create_app(settings, library), False
|
|
352
|
+
|
|
353
|
+
uvicorn.run(
|
|
354
|
+
target,
|
|
355
|
+
factory=factory,
|
|
356
|
+
host=settings.host,
|
|
357
|
+
port=settings.port,
|
|
358
|
+
reload=args.reload,
|
|
359
|
+
log_level=args.log_level,
|
|
360
|
+
# uvicorn would otherwise advertise itself in the Server header; the middleware replaces
|
|
361
|
+
# it with Saturn/... to match a real appliance.
|
|
362
|
+
server_header=False,
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def address(argv, prog="maprt-emulator"):
|
|
367
|
+
"""The ``(host, port)`` the emulator would listen on, given these arguments.
|
|
368
|
+
|
|
369
|
+
What a launcher (the SGRT Scripting Workshop's ``maprt-workshop``) calls before starting
|
|
370
|
+
the emulator, so it knows what to wait for. Parsing here also means ``maprt-workshop --help``
|
|
371
|
+
and a mistyped flag are reported at the prompt, before anything starts.
|
|
372
|
+
"""
|
|
373
|
+
settings = settings_from_args(_parser(prog).parse_args(argv))
|
|
374
|
+
return settings.host, settings.port
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
if __name__ == "__main__":
|
|
378
|
+
main()
|