@giamme/forge 0.0.0-stage → 0.8.0
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.
- package/CHANGELOG.md +181 -0
- package/LICENSE +21 -0
- package/README.md +1854 -2
- package/SKILL.md +122 -0
- package/VERSION +1 -0
- package/forge +74 -0
- package/package.json +26 -4
- package/references/dataset.md +329 -0
- package/references/decompose.md +667 -0
- package/references/fractal-dashboard.md +21 -0
- package/references/fractal.md +307 -0
- package/references/harnesses.md +353 -0
- package/references/jev.md +865 -0
- package/references/memory.md +228 -0
- package/references/planning.md +42 -0
- package/references/ripwire.md +47 -0
- package/references/runtime.md +64 -0
- package/references/solo.md +113 -0
- package/references/ui.md +112 -0
- package/registry.tsv +81 -0
- package/scripts/forge-artifact.sh +78 -0
- package/scripts/forge-contract.py +430 -0
- package/scripts/forge-dataset.py +6 -0
- package/scripts/forge-dispatch.sh +665 -0
- package/scripts/forge-fail-first.py +195 -0
- package/scripts/forge-fractal-dispatch.sh +4 -0
- package/scripts/forge-fractal-options.sh +54 -0
- package/scripts/forge-fractal.py +6 -0
- package/scripts/forge-guard.py +266 -0
- package/scripts/forge-install-fractal.sh +4 -0
- package/scripts/forge-install-ripwire.sh +52 -0
- package/scripts/forge-install.sh +135 -0
- package/scripts/forge-jev-options.sh +259 -0
- package/scripts/forge-jev.py +6 -0
- package/scripts/forge-lib-plans.sh +785 -0
- package/scripts/forge-lib-review.sh +494 -0
- package/scripts/forge-lib-task.sh +302 -0
- package/scripts/forge-lib-verify.sh +30 -0
- package/scripts/forge-memory.sh +575 -0
- package/scripts/forge-metrics.sh +39 -0
- package/scripts/forge-parallel-options.sh +76 -0
- package/scripts/forge-parallel.sh +1717 -0
- package/scripts/forge-plan-check.py +118 -0
- package/scripts/forge-prompt.py +45 -0
- package/scripts/forge-ripwire.py +136 -0
- package/scripts/forge-route.py +344 -0
- package/scripts/forge-runtime.py +533 -0
- package/scripts/forge-schedule.py +312 -0
- package/scripts/forge-shadow.py +305 -0
- package/scripts/forge-solo.sh +347 -0
- package/scripts/forge-ui-options.sh +42 -0
- package/scripts/forge-ui.py +435 -0
- package/scripts/forge-update.sh +71 -0
- package/scripts/forge-verify-coverage.py +956 -0
- package/scripts/forge-version.py +198 -0
- package/scripts/forge_dataset/__init__.py +439 -0
- package/scripts/forge_dataset/capture.py +330 -0
- package/scripts/forge_dataset/checks.py +109 -0
- package/scripts/forge_dataset/cli.py +233 -0
- package/scripts/forge_dataset/export.py +1009 -0
- package/scripts/forge_dataset/harvest.py +716 -0
- package/scripts/forge_dataset/importer.py +412 -0
- package/scripts/forge_dataset/outcomes.py +275 -0
- package/scripts/forge_dataset/routing.py +278 -0
- package/scripts/forge_fractal/__init__.py +83 -0
- package/scripts/forge_fractal/bridge.py +46 -0
- package/scripts/forge_fractal/cli.py +202 -0
- package/scripts/forge_fractal/dashboard.css +138 -0
- package/scripts/forge_fractal/dashboard.html +41 -0
- package/scripts/forge_fractal/dashboard.js +687 -0
- package/scripts/forge_fractal/decomposition.py +262 -0
- package/scripts/forge_fractal/execution.py +659 -0
- package/scripts/forge_fractal/inspection.py +261 -0
- package/scripts/forge_fractal/install.py +158 -0
- package/scripts/forge_fractal/invocation.py +94 -0
- package/scripts/forge_fractal/pipeline.py +124 -0
- package/scripts/forge_fractal/selection.py +179 -0
- package/scripts/forge_jev/__init__.py +376 -0
- package/scripts/forge_jev/backtest.py +322 -0
- package/scripts/forge_jev/cli.py +980 -0
- package/scripts/forge_jev/client.py +418 -0
- package/scripts/forge_jev/corpus.py +156 -0
- package/scripts/forge_jev/coupling.py +244 -0
- package/scripts/forge_jev/features.py +24 -0
- package/scripts/forge_jev/gates.py +100 -0
- package/scripts/forge_jev/memory.py +163 -0
- package/scripts/forge_jev/outcome.py +49 -0
- package/scripts/forge_jev/predict.py +204 -0
- package/scripts/forge_jev/questions.py +661 -0
- package/scripts/forge_jev/review.py +73 -0
- package/scripts/forge_jev/routing.py +271 -0
- package/scripts/forge_jev/subset.py +123 -0
- package/scripts/forge_jev/verify.py +453 -0
- package/scripts/forge_registry.py +140 -0
- package/ui/README.md +287 -0
- package/ui/THIRD_PARTY_NOTICES.md +69 -0
- package/ui/docs/api.md +888 -0
- package/ui/forge_ui/__init__.py +20 -0
- package/ui/forge_ui/__main__.py +135 -0
- package/ui/forge_ui/diffs.py +454 -0
- package/ui/forge_ui/executions.py +436 -0
- package/ui/forge_ui/export.py +204 -0
- package/ui/forge_ui/findings.py +132 -0
- package/ui/forge_ui/fractal.py +583 -0
- package/ui/forge_ui/models.py +216 -0
- package/ui/forge_ui/reader.py +1336 -0
- package/ui/forge_ui/server.py +303 -0
- package/ui/forge_ui/static/app.js +924 -0
- package/ui/forge_ui/static/cloud3d.js +646 -0
- package/ui/forge_ui/static/cloudmodel.js +375 -0
- package/ui/forge_ui/static/cloudui.js +394 -0
- package/ui/forge_ui/static/diffview.js +216 -0
- package/ui/forge_ui/static/favicon.svg +1 -0
- package/ui/forge_ui/static/files.js +776 -0
- package/ui/forge_ui/static/gantt.js +97 -0
- package/ui/forge_ui/static/graph.js +266 -0
- package/ui/forge_ui/static/index.html +55 -0
- package/ui/forge_ui/static/iso.js +765 -0
- package/ui/forge_ui/static/leaderboard.js +315 -0
- package/ui/forge_ui/static/map3d.js +45 -0
- package/ui/forge_ui/static/modelstats.js +249 -0
- package/ui/forge_ui/static/notify.js +179 -0
- package/ui/forge_ui/static/panel.js +526 -0
- package/ui/forge_ui/static/runmodel.js +651 -0
- package/ui/forge_ui/static/scene3d.js +996 -0
- package/ui/forge_ui/static/search.js +119 -0
- package/ui/forge_ui/static/style.css +618 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"""Local HTTP server: the JSON API of docs/api.md and the static page. Read-only."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import base64
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
import hashlib
|
|
7
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import re
|
|
12
|
+
import sys
|
|
13
|
+
import threading
|
|
14
|
+
import time
|
|
15
|
+
import urllib.parse
|
|
16
|
+
|
|
17
|
+
from forge_ui import __version__, build_id
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
STATIC_DIR = Path(__file__).resolve().parent / 'static'
|
|
21
|
+
# The build of the code this process loaded. Files edited later must not change it: the launcher
|
|
22
|
+
# compares /api/health with its own build_id() to find a server still running old code.
|
|
23
|
+
BUILD = build_id()
|
|
24
|
+
|
|
25
|
+
_RUN_PATH = re.compile(r'/api/runs/([0-9a-f]{12})')
|
|
26
|
+
_TASK_PATH = re.compile(r'/api/runs/([0-9a-f]{12})/tasks/([^/]+)')
|
|
27
|
+
_EXEC_PATH = re.compile(r'/api/runs/([0-9a-f]{12})/executions/((?:task|dwarf|qa|solo|verification|parallel)-[0-9a-f]{32})')
|
|
28
|
+
_REVIEW_PATH = re.compile(r'/api/runs/([0-9a-f]{12})/final-reviews/((?:final-review|integrate)-[0-9]+-[0-9]+)')
|
|
29
|
+
_TASK_DIFF_PATH = re.compile(_TASK_PATH.pattern + r'/diff(?:/(current|prev|committed|existing))?')
|
|
30
|
+
_EXEC_DIFF_PATH = re.compile(_EXEC_PATH.pattern + '/diff')
|
|
31
|
+
_REVIEW_DIFF_PATH = re.compile(_REVIEW_PATH.pattern + '/diff')
|
|
32
|
+
_FILES_PATH = re.compile(r'/api/runs/([0-9a-f]{12})/files')
|
|
33
|
+
_EXPORT_PATH = re.compile(r'/api/runs/([0-9a-f]{12})/export')
|
|
34
|
+
_FRACTAL_PATH = re.compile(_TASK_PATH.pattern + r'/fractal(?:/nodes/([A-Za-z0-9][A-Za-z0-9_-]{0,100})(/diff)?)?')
|
|
35
|
+
_STATIC_PATH = re.compile(r'/([A-Za-z0-9_.-]+\.(?:html|js|css|svg))')
|
|
36
|
+
_TYPES = {
|
|
37
|
+
'html': 'text/html; charset=utf-8',
|
|
38
|
+
'js': 'text/javascript; charset=utf-8',
|
|
39
|
+
'css': 'text/css; charset=utf-8',
|
|
40
|
+
'svg': 'image/svg+xml',
|
|
41
|
+
}
|
|
42
|
+
_JSON = 'application/json; charset=utf-8'
|
|
43
|
+
_CSP = ("default-src 'self'; style-src 'self'; script-src 'self'; img-src 'self' data:; "
|
|
44
|
+
"connect-src 'self'; frame-ancestors 'none'")
|
|
45
|
+
# The page may load three.js, pinned, from the CDN (only when the 3D view is opened); its inline
|
|
46
|
+
# import map is allowed by hash, computed from the file actually served.
|
|
47
|
+
_PAGE_CSP = ("default-src 'self'; style-src 'self'; script-src 'self' https://cdn.jsdelivr.net/npm/three@0.160.0/{}; "
|
|
48
|
+
"img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; object-src 'none'")
|
|
49
|
+
_IMPORTMAP = re.compile(r'<script type="importmap">(.*?)</script>', re.DOTALL | re.IGNORECASE)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _log(message):
|
|
53
|
+
print(f'forge-ui: {message}', file=sys.stderr)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def page_csp(body):
|
|
57
|
+
"""The CSP for an HTML page: _PAGE_CSP plus the sha256 of each inline import map in it."""
|
|
58
|
+
# Browsers hash the script text after normalising newlines, so the hash is taken the same way.
|
|
59
|
+
text = body.decode('utf-8', errors='replace').replace('\r\n', '\n').replace('\r', '\n')
|
|
60
|
+
hashes = ''.join(" 'sha256-%s'" % base64.b64encode(hashlib.sha256(script.encode('utf-8')).digest()).decode('ascii')
|
|
61
|
+
for script in _IMPORTMAP.findall(text))
|
|
62
|
+
return _PAGE_CSP.format(hashes)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _task_id(segment):
|
|
66
|
+
"""A task id from its URL segment, or None when it cannot name a task dir."""
|
|
67
|
+
try:
|
|
68
|
+
value = urllib.parse.unquote(segment, errors='strict')
|
|
69
|
+
except UnicodeDecodeError:
|
|
70
|
+
return None
|
|
71
|
+
if not value or value in ('.', '..') or '/' in value or '\x00' in value or '\ufffd' in value:
|
|
72
|
+
return None
|
|
73
|
+
return value
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
NO_ROUTE = object()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def api_get(catalog, path):
|
|
80
|
+
"""What a GET of the JSON API path `path` (no query) answers: the object, None for a 404, or NO_ROUTE
|
|
81
|
+
when `path` is not one of these routes. /api/health and /api/runs/<key>/export are the handler's."""
|
|
82
|
+
if path == '/api/runs':
|
|
83
|
+
return {'runs': catalog.runs()}
|
|
84
|
+
if path == '/api/models':
|
|
85
|
+
return catalog.models()
|
|
86
|
+
match = _RUN_PATH.fullmatch(path)
|
|
87
|
+
if match:
|
|
88
|
+
return catalog.get(match.group(1))
|
|
89
|
+
match = _TASK_PATH.fullmatch(path)
|
|
90
|
+
if match:
|
|
91
|
+
task_id = _task_id(match.group(2))
|
|
92
|
+
return None if task_id is None else catalog.task(match.group(1), task_id)
|
|
93
|
+
match = _EXEC_PATH.fullmatch(path)
|
|
94
|
+
if match:
|
|
95
|
+
return catalog.execution(*match.groups())
|
|
96
|
+
match = _REVIEW_PATH.fullmatch(path)
|
|
97
|
+
if match:
|
|
98
|
+
return catalog.final_review(*match.groups())
|
|
99
|
+
match = _TASK_DIFF_PATH.fullmatch(path)
|
|
100
|
+
if match:
|
|
101
|
+
task_id = _task_id(match.group(2))
|
|
102
|
+
return None if task_id is None else catalog.diff(match.group(1), task_id, match.group(3) or 'current')
|
|
103
|
+
match = _EXEC_DIFF_PATH.fullmatch(path)
|
|
104
|
+
if match:
|
|
105
|
+
return catalog.execution_diff(*match.groups())
|
|
106
|
+
match = _REVIEW_DIFF_PATH.fullmatch(path)
|
|
107
|
+
if match:
|
|
108
|
+
return catalog.final_review_diff(*match.groups())
|
|
109
|
+
match = _FILES_PATH.fullmatch(path)
|
|
110
|
+
if match:
|
|
111
|
+
return catalog.files(match.group(1))
|
|
112
|
+
match = _FRACTAL_PATH.fullmatch(path)
|
|
113
|
+
if match:
|
|
114
|
+
key, task_id, node, diff = match.group(1), _task_id(match.group(2)), match.group(3), match.group(4)
|
|
115
|
+
if task_id is None:
|
|
116
|
+
return None
|
|
117
|
+
if node is None:
|
|
118
|
+
return catalog.fractal(key, task_id)
|
|
119
|
+
read = catalog.fractal_node_diff if diff else catalog.fractal_node
|
|
120
|
+
return read(key, task_id, node)
|
|
121
|
+
return NO_ROUTE
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class Handler(BaseHTTPRequestHandler):
|
|
125
|
+
# A client that connects and then stalls must not hold its thread forever.
|
|
126
|
+
timeout = 30
|
|
127
|
+
|
|
128
|
+
def do_GET(self):
|
|
129
|
+
self._serve(head=False)
|
|
130
|
+
|
|
131
|
+
def do_HEAD(self):
|
|
132
|
+
self._serve(head=True)
|
|
133
|
+
|
|
134
|
+
def __getattr__(self, name):
|
|
135
|
+
# Every other method (POST, PUT, a made-up verb, ...) gets a 405, not the 501 of the base class.
|
|
136
|
+
if name.startswith('do_'):
|
|
137
|
+
return self._method_not_allowed
|
|
138
|
+
raise AttributeError(name)
|
|
139
|
+
|
|
140
|
+
def handle(self):
|
|
141
|
+
# The client may reset the connection while the request line is still being read. That error
|
|
142
|
+
# comes from rfile.readline(), before any response exists, so the base class would print a traceback.
|
|
143
|
+
try:
|
|
144
|
+
super().handle()
|
|
145
|
+
except ConnectionError:
|
|
146
|
+
pass # the client hung up (ConnectionResetError, BrokenPipeError, ...): nothing to tell it
|
|
147
|
+
|
|
148
|
+
def send_error(self, code, message=None, explain=None):
|
|
149
|
+
# The base class answers malformed requests with an HTML page; keep the JSON contract.
|
|
150
|
+
with self._active():
|
|
151
|
+
self._send(*self._json(code, {'error': self.responses[code][0].lower()}), head=self.command == 'HEAD')
|
|
152
|
+
|
|
153
|
+
def log_request(self, code='-', size='-'):
|
|
154
|
+
if not self._quiet():
|
|
155
|
+
path = (getattr(self, 'path', None) or '').partition('?')[0]
|
|
156
|
+
_log(f'{self.command or "-"} {path} {code}')
|
|
157
|
+
|
|
158
|
+
def log_message(self, format, *args):
|
|
159
|
+
# Also reached by the base class for stalled connections (log_error): --quiet silences those too.
|
|
160
|
+
if not self._quiet():
|
|
161
|
+
_log(f'{self.address_string()} {format % args}')
|
|
162
|
+
|
|
163
|
+
def _quiet(self):
|
|
164
|
+
return getattr(self.server, 'quiet', False)
|
|
165
|
+
|
|
166
|
+
def _host_allowed(self):
|
|
167
|
+
# DNS rebinding: a hostile page can make its own domain resolve to 127.0.0.1 and read us same-origin.
|
|
168
|
+
# Only the local names are accepted, with any port: a port forward (ssh -L, VS Code) changes the port.
|
|
169
|
+
host = (self.headers.get('Host') or '').lower()
|
|
170
|
+
if not host:
|
|
171
|
+
return True # HTTP/1.0 clients may omit Host; a rebinding page always sends its own name
|
|
172
|
+
name = host.rsplit(':', 1)[0] if ':' in host else host
|
|
173
|
+
return name in ('127.0.0.1', 'localhost')
|
|
174
|
+
|
|
175
|
+
def _method_not_allowed(self):
|
|
176
|
+
with self._active():
|
|
177
|
+
self._refuse_method()
|
|
178
|
+
|
|
179
|
+
def _refuse_method(self):
|
|
180
|
+
if not self._host_allowed():
|
|
181
|
+
self._send(*self._json(403, {'error': 'forbidden'}))
|
|
182
|
+
return
|
|
183
|
+
self._send(*self._json(405, {'error': 'method not allowed'}, {'Allow': 'GET, HEAD'}))
|
|
184
|
+
|
|
185
|
+
@contextmanager
|
|
186
|
+
def _active(self):
|
|
187
|
+
# Idle exit measures from the last activity, and a request in flight is activity: stamp when it
|
|
188
|
+
# arrives and when it ends, and count it so a slow one is not cut off at the idle deadline.
|
|
189
|
+
server = self.server
|
|
190
|
+
# The stamp and the count change together under busy_lock: the idle watcher reads both under it.
|
|
191
|
+
with server.busy_lock:
|
|
192
|
+
server.last_request = time.monotonic()
|
|
193
|
+
server.busy += 1
|
|
194
|
+
try:
|
|
195
|
+
yield
|
|
196
|
+
finally:
|
|
197
|
+
with server.busy_lock:
|
|
198
|
+
server.busy -= 1
|
|
199
|
+
server.last_request = time.monotonic()
|
|
200
|
+
|
|
201
|
+
def _serve(self, head):
|
|
202
|
+
with self._active():
|
|
203
|
+
self._respond(head)
|
|
204
|
+
|
|
205
|
+
def _respond(self, head):
|
|
206
|
+
path, _, self.query = self.path.partition('?')
|
|
207
|
+
if not self._host_allowed():
|
|
208
|
+
response = self._json(403, {'error': 'forbidden'})
|
|
209
|
+
else:
|
|
210
|
+
try:
|
|
211
|
+
response = self._route(path)
|
|
212
|
+
except Exception as exc: # one bad request must not kill the server
|
|
213
|
+
detail = ' '.join(str(exc).split())
|
|
214
|
+
_log(f'internal error on {self.command} {path}: {type(exc).__name__}: {detail}')
|
|
215
|
+
response = self._json(500, {'error': 'internal error'})
|
|
216
|
+
self._send(*response, head=head)
|
|
217
|
+
|
|
218
|
+
def _route(self, path):
|
|
219
|
+
if path == '/api/health':
|
|
220
|
+
return self._json(200, {
|
|
221
|
+
'app': 'forge-ui',
|
|
222
|
+
'version': __version__,
|
|
223
|
+
'build': BUILD,
|
|
224
|
+
'pid': os.getpid(),
|
|
225
|
+
})
|
|
226
|
+
catalog = self.server.catalog
|
|
227
|
+
match = _EXPORT_PATH.fullmatch(path)
|
|
228
|
+
if match:
|
|
229
|
+
return self._export(catalog, match.group(1))
|
|
230
|
+
value = api_get(catalog, path)
|
|
231
|
+
if value is not NO_ROUTE:
|
|
232
|
+
return self._found(value)
|
|
233
|
+
if path == '/':
|
|
234
|
+
return self._static('index.html')
|
|
235
|
+
match = _STATIC_PATH.fullmatch(path)
|
|
236
|
+
if match:
|
|
237
|
+
return self._static(match.group(1))
|
|
238
|
+
return self._json(404, {'error': 'not found'})
|
|
239
|
+
|
|
240
|
+
def _static(self, name):
|
|
241
|
+
root = Path(self.server.static_dir).resolve()
|
|
242
|
+
body = None
|
|
243
|
+
if '..' not in name:
|
|
244
|
+
try:
|
|
245
|
+
target = (root / name).resolve()
|
|
246
|
+
# Directly inside the static dir, after symlinks are resolved: no subdirs, no escapes.
|
|
247
|
+
if target.parent == root and target.is_file():
|
|
248
|
+
body = target.read_bytes()
|
|
249
|
+
except (OSError, RuntimeError, ValueError):
|
|
250
|
+
body = None
|
|
251
|
+
if body is None:
|
|
252
|
+
return self._json(404, {'error': 'not found'})
|
|
253
|
+
kind = name.rsplit('.', 1)[1]
|
|
254
|
+
return 200, _TYPES[kind], body, {'Content-Security-Policy': page_csp(body)} if kind == 'html' else {}
|
|
255
|
+
|
|
256
|
+
def _export(self, catalog, key):
|
|
257
|
+
# One run as one HTML file, to download: never rendered from this origin (sandbox, no sources).
|
|
258
|
+
from forge_ui import export
|
|
259
|
+
params = urllib.parse.parse_qs(getattr(self, 'query', ''), max_num_fields=8)
|
|
260
|
+
flag = lambda name: (params.get(name) or ['0'])[0] == '1'
|
|
261
|
+
built = export.build(catalog, key, texts=flag('texts'), paths=flag('paths'))
|
|
262
|
+
if built is None:
|
|
263
|
+
return self._json(404, {'error': 'not found'})
|
|
264
|
+
name, body = built
|
|
265
|
+
return 200, _TYPES['html'], body, {'Content-Disposition': f'attachment; filename="{name}"',
|
|
266
|
+
'Content-Security-Policy': "default-src 'none'; sandbox"}
|
|
267
|
+
|
|
268
|
+
def _found(self, value):
|
|
269
|
+
return self._json(404, {'error': 'not found'}) if value is None else self._json(200, value)
|
|
270
|
+
|
|
271
|
+
@staticmethod
|
|
272
|
+
def _json(status, obj, extra=None):
|
|
273
|
+
body = json.dumps(obj, ensure_ascii=False).encode('utf-8')
|
|
274
|
+
return status, _JSON, body, extra or {}
|
|
275
|
+
|
|
276
|
+
def _send(self, status, ctype, body, extra, head=False):
|
|
277
|
+
extra = dict(extra)
|
|
278
|
+
try:
|
|
279
|
+
self.send_response(int(status))
|
|
280
|
+
self.send_header('Content-Type', ctype)
|
|
281
|
+
self.send_header('Content-Length', str(len(body)))
|
|
282
|
+
self.send_header('Cache-Control', 'no-store')
|
|
283
|
+
self.send_header('X-Content-Type-Options', 'nosniff')
|
|
284
|
+
self.send_header('Content-Security-Policy', extra.pop('Content-Security-Policy', _CSP))
|
|
285
|
+
self.send_header('Referrer-Policy', 'no-referrer')
|
|
286
|
+
for name, value in extra.items():
|
|
287
|
+
self.send_header(name, value)
|
|
288
|
+
self.end_headers()
|
|
289
|
+
if not head:
|
|
290
|
+
self.wfile.write(body)
|
|
291
|
+
except OSError:
|
|
292
|
+
pass # the client hung up (BrokenPipeError, ConnectionResetError, ...): nothing to tell it
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def make_server(catalog, host='127.0.0.1', port=0, static_dir=None, quiet=False):
|
|
296
|
+
server = ThreadingHTTPServer((host, port), Handler)
|
|
297
|
+
server.catalog = catalog
|
|
298
|
+
server.static_dir = STATIC_DIR if static_dir is None else static_dir
|
|
299
|
+
server.quiet = quiet
|
|
300
|
+
server.last_request = time.monotonic() # stamped when a request arrives and when it ends
|
|
301
|
+
server.busy = 0 # requests in flight; idle exit waits for them
|
|
302
|
+
server.busy_lock = threading.Lock()
|
|
303
|
+
return server
|