Source code for xonsh.webconfig.main
#!/usr/bin/env python3
import contextlib
import json
import os
import socketserver
import string
import sys
import typing as tp
from email.message import EmailMessage
from http import HTTPStatus, server
from pathlib import Path
from pprint import pprint
from urllib import parse
from xonsh.built_ins import XSH
from xonsh.webconfig import tags as t
from xonsh.webconfig.file_writes import insert_into_xonshrc
from xonsh.webconfig.routes import Routes
RENDERERS: list[tp.Callable] = []
[docs]
class XonshConfigHTTPRequestHandler(server.SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
kwargs.setdefault("directory", os.path.dirname(__file__))
super().__init__(*args, **kwargs)
def _write_headers(self, *headers: "tuple[str, str]"):
for name, val in headers:
self.send_header(name, val)
self.end_headers()
def _write_data(self, data: "bytes|dict|str"):
if isinstance(data, bytes):
content_type = "text/html"
elif isinstance(data, dict):
content_type = "application/json"
data = json.dumps(data).encode()
else:
content_type = "text/html"
data = str(data).encode()
self._write_headers(
("Content-type", content_type),
("Content-Length", str(len(data))),
)
self.wfile.write(data)
self.wfile.flush()
def _send(
self,
data: "bytes|dict|str|None" = None,
status: "None|int" = None,
redirect: "str|None" = None,
):
status = status or (HTTPStatus.FOUND if redirect else HTTPStatus.OK)
self.send_response(status)
if data:
self._write_data(data)
elif redirect:
self._write_headers(
("Location", redirect),
)
def _read(self):
content_len = int(self.headers.get("content-length", 0))
return self.rfile.read(content_len)
[docs]
def render_get(self, route):
try:
webconfig = Path(__file__).parent
except Exception:
# in case of thread missing __file__ definition
webconfig = Path.cwd()
path = webconfig / "index.html"
tmpl = string.Template(path.read_text())
navlinks = t.to_str(route.get_nav_links())
msgs = t.to_str(route.get_err_msgs())
body = t.to_str(route.get()) # type: ignore
data = tmpl.substitute(navlinks=navlinks, body=msgs + body)
return self._send(data)
def _get_route(self, method: str):
url = parse.urlparse(self.path)
route_cls = Routes.registry.get(url.path)
if route_cls and hasattr(route_cls, method):
params = parse.parse_qs(url.query)
return route_cls(url=url, params=params, xsh=XSH)
[docs]
def do_GET(self) -> None:
route = self._get_route("get")
if route is not None:
return self.render_get(route)
return super().do_GET()
def _read_form(self):
msg = EmailMessage()
msg["Content-Type"] = self.headers.get("content-type")
if msg.get_content_type() == "application/x-www-form-urlencoded":
data = parse.parse_qs(self._read(), keep_blank_values=True)
for name, values in data.items():
value = None
if isinstance(name, bytes):
name = name.decode(encoding="utf-8")
if isinstance(values, list) and values:
if isinstance(values[0], bytes):
value = values[0].decode(encoding="utf-8")
yield name, value
[docs]
def do_POST(self):
"""Reads post request body"""
route = self._get_route("post")
if route is not None:
# redirect after form submission
data = dict(self._read_form())
new_route = route.post(data) or route
return self._send(redirect=new_route.path)
post_body = self._read()
config = json.loads(post_body)
print("Web Config Values:")
pprint(config)
fname = insert_into_xonshrc(config)
print("Wrote out to " + fname)
self._send(b"received post request:<br>" + post_body)
[docs]
def bind_server_to(
port: int = 8421, handler_cls=XonshConfigHTTPRequestHandler, browser=False
):
cls = socketserver.TCPServer
# cls = socketserver.ThreadingTCPServer # required ctrl+c twice ?
cls.allow_reuse_address = True
while port <= 9310:
try:
cls.allow_reuse_address = True
httpd = cls(("", port), handler_cls)
url = f"http://localhost:{port}"
print(f"Web config started at '{url}'. Hit Ctrl+C to stop.")
if browser:
import webbrowser
webbrowser.open(url)
return httpd
except OSError:
type, value = sys.exc_info()[:2]
if "Address already in use" not in str(value):
raise
except KeyboardInterrupt:
break
port += 1
[docs]
def serve(browser=False):
httpd = bind_server_to(browser=browser)
with contextlib.suppress(KeyboardInterrupt):
with httpd:
httpd.serve_forever()
[docs]
def main(browser=False):
"""standalone entry point for webconfig."""
from xonsh.main import setup
setup()
serve(browser)
if __name__ == "__main__":
# watchexec -r -e py -- python -m xonsh.webconfig --no-browser
main()