diff --git a/boxes/scripts/__init__.py b/boxes/scripts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/boxes/scripts/boxes_main.py b/boxes/scripts/boxes_main.py new file mode 100755 index 00000000..4d0f3f0a --- /dev/null +++ b/boxes/scripts/boxes_main.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +"""boxes.py + +Generate stencils for wooden boxes. + +Usage: + boxes [...] + boxes --list + boxes --examples + boxes (-h | --help) + +Options: + --list List available generators. + --examples Generates an SVG for every generator into the "examples" folder. + -h --help Show this screen. +""" +from __future__ import annotations + +import gettext +import os +import sys +from pathlib import Path + +try: + import boxes +except ImportError: + sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), '../..')) + import boxes + +import boxes.generators + + +def print_grouped_generators() -> None: + class ConsoleColors: + BOLD = '\033[1m' + CLEAR = '\033[0m' + ITALIC = '\033[3m' + UNDERLINE = '\033[4m' + + print('Available generators:') + for group in generator_groups(): + print('\n' + ConsoleColors.UNDERLINE + group.title + ConsoleColors.CLEAR) + if group.description: + print('\n' + group.description) + print() + for box in group.generators: + description = box.__doc__ or "" + description = description.replace("\n", "").replace("\r", "").strip() + print(f' * {box.__name__:<15} - {ConsoleColors.ITALIC}{description}{ConsoleColors.CLEAR}') + + +def create_example_every_generator() -> None: + print("Generating SVG examples for every possible generator.") + for group in generator_groups(): + for boxExample in group.generators: + boxName = boxExample.__name__ + notTestGenerator = ('GridfinityTrayLayout', 'TrayLayout', 'TrayLayoutFile', 'TypeTray', 'Edges',) + brokenGenerator = () + avoidGenerator = notTestGenerator + brokenGenerator + if boxName in avoidGenerator: + print(f"SKIP: {boxName}") + continue + print(f"Generate example for: {boxName}") + + box = boxExample() + box.translations = get_translation() + box.parseArgs("") + box.metadata["reproducible"] = True + box.open() + box.render() + boxData = box.close() + + file = Path('examples') / (boxName + '.svg') + file.write_bytes(boxData.getvalue()) + + +def get_translation(): + try: + return gettext.translation('boxes.py', localedir='locale') + except OSError: + return gettext.translation('boxes.py', fallback=True) + + +def run_generator(name: str, args) -> None: + generators = generators_by_name() + lower_name = name.lower() + + if lower_name in generators.keys(): + box = generators[lower_name]() + box.translations = get_translation() + box.parseArgs(args) + box.open() + box.render() + data = box.close() + with os.fdopen(sys.stdout.fileno(), "wb", closefd=False) if box.output == "-" else open(box.output, 'wb') as f: + f.write(data.getvalue()) + else: + msg = f"Unknown generator '{name}'. Use boxes --list to get a list of available commands.\n" + sys.stderr.write(msg) + + +def generator_groups(): + generators = generators_by_name() + return group_generators(generators) + + +def group_generators(generators): + groups = boxes.generators.ui_groups + groups_by_name = boxes.generators.ui_groups_by_name + + for name, generator in generators.items(): + group_for_generator = groups_by_name.get(generator.ui_group, groups_by_name['Misc']) + group_for_generator.add(generator) + + return groups + + +def generators_by_name() -> dict[str, type[boxes.Boxes]]: + all_generators = boxes.generators.getAllBoxGenerators() + + return { + name.split('.')[-1].lower(): generator + for name, generator in all_generators.items() + } + + +def print_usage() -> None: + print(__doc__) + + +def print_version() -> None: + print("boxes does not use versioning.") + + +def main() -> None: + if len(sys.argv) > 1 and sys.argv[1].startswith("--id="): + del sys.argv[1] + if len(sys.argv) == 1 or sys.argv[1] == '--help' or sys.argv[1] == '-h': + print_usage() + elif sys.argv[1] == '--version': + print_version() + elif sys.argv[1] == '--list': + print_grouped_generators() + elif sys.argv[1] == '--examples': + create_example_every_generator() + else: + name = sys.argv[1].lower() + if name.startswith("--generator="): + name = name[12:] + run_generator(name, sys.argv[2:]) + + +if __name__ == '__main__': + main() diff --git a/boxes/scripts/boxes_proxy.py b/boxes/scripts/boxes_proxy.py new file mode 100755 index 00000000..0800ab3b --- /dev/null +++ b/boxes/scripts/boxes_proxy.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 + +""" +Extension for InkScape 1.0+ + +boxes.py wrapper script to make it work on Windows and Linux systems without duplicating .inx files + +Author: Mario Voigt / FabLab Chemnitz +Mail: mario.voigt@stadtfabrikanten.org +Date: 27.04.2021 +Last patch: 27.04.2021 +License: GNU GPL v3 + +""" +pass +import subprocess +import sys + +pass +from shlex import quote + +from inkex.extensions import GenerateExtension +from lxml import etree + +import inkex + + +class boxesPyWrapper(GenerateExtension): + def add_arguments(self, pars): + args = sys.argv[1:] + for arg in args: + key = arg.split("=")[0] + if key == "--id": + continue + if len(arg.split("=")) == 2: + value = arg.split("=")[1] + pars.add_argument(key, default=key) + + def generate(self): + cmd = "boxes" # boxes.exe in this local dir (or if present in %PATH%), or boxes from $PATH in linux + for arg in vars(self.options): + if arg in ( + "output", "id", "ids", "selected_nodes", + "input_file", "tab"): + continue + # fix behaviour of "original" arg which does not correctly gets + # interpreted if set to false + if arg == "original" and str(getattr(self.options, arg)) == "false": + continue + cmd += f" --{arg} {quote(str(getattr(self.options, arg)))}" + cmd += f" --output -" + cmd = cmd.replace("boxes --generator", "boxes") + + # run boxes with the parameters provided + result = subprocess.run(cmd.split(), capture_output=True) + + if result.returncode: + inkex.utils.debug("Generating box svg failed. Cannot continue. Command was:") + inkex.utils.debug(str(cmd)) + inkex.utils.debug(str(result.stderr)) + exit(1) + + # write the generated SVG into Inkscape's canvas + p = etree.XMLParser(huge_tree=True) + doc = etree.fromstring(result.stdout, parser=etree.XMLParser(huge_tree=True)) + group = inkex.Group(id="boxes.py") + for element in doc: + group.append(element) + return group + + +def main() -> None: + boxesPyWrapper().run() + + +if __name__ == '__main__': + main() diff --git a/boxes/scripts/boxesserver.py b/boxes/scripts/boxesserver.py new file mode 100755 index 00000000..9f9bf907 --- /dev/null +++ b/boxes/scripts/boxesserver.py @@ -0,0 +1,711 @@ +#!/usr/bin/env python3 +# Copyright (C) 2016-2017 Florian Festi +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +from __future__ import annotations + +import argparse +import gettext +import glob +import html +import io +import mimetypes +import os.path +import re +import sys +import threading +import time +import traceback +from typing import Any, NoReturn +from urllib.parse import quote, unquote_plus +from wsgiref.simple_server import make_server + +import markdown +import qrcode + +try: + import boxes.generators +except ImportError: + sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), "../..")) + import boxes.generators + + +class FileChecker(threading.Thread): + def __init__(self, files=[], checkmodules: bool = True) -> None: + super().__init__() + self.checkmodules = checkmodules + self.timestamps = {} + self._stopped = False + for path in files: + self.timestamps[path] = os.stat(path).st_mtime + if checkmodules: + self._addModules() + + def _addModules(self) -> None: + for name, module in sys.modules.items(): + path = getattr(module, "__file__", None) + if not path: + continue + if path not in self.timestamps: + self.timestamps[path] = os.stat(path).st_mtime + + def filesOK(self) -> bool: + if self.checkmodules: + self._addModules() + for path, timestamp in self.timestamps.items(): + try: + if os.stat(path).st_mtime != timestamp: + return False + except FileNotFoundError: + return False + return True + + def run(self) -> None: + while not self._stopped: + if not self.filesOK(): + os.execv(__file__, sys.argv) + time.sleep(1) + + def stop(self) -> None: + self._stopped = True + + +def filter_url(url, non_default_args): + if len(url) == 0: + return '' + try: + base, args = url.split('?') + except ValueError: + return '' + args = args.split('&') + new_args = [] + args_to_ignore = ["qr_code", "format"] + for arg in args: + a, b = arg.split('=') + if a.strip() in args_to_ignore: + continue + if a in non_default_args: + new_args.append(arg) + if len(new_args): + return f"{base}?{'&'.join(new_args)}" + else: + return f"{base}" + + +class ArgumentParserError(Exception): pass + + +class ThrowingArgumentParser(argparse.ArgumentParser): + def error(self, message) -> NoReturn: + raise ArgumentParserError(message) + + +# Evil hack +boxes.ArgumentParser = ThrowingArgumentParser # type: ignore + + +class BServer: + lang_re = re.compile(r"([a-z]{2,3}(-[-a-zA-Z0-9]*)?)\s*(;\s*q=(\d\.?\d*))?") + + def __init__(self, url_prefix="", static_url="static") -> None: + self.boxes = {b.__name__: b for b in boxes.generators.getAllBoxGenerators().values() if b.webinterface} + self.groups = boxes.generators.ui_groups + self.groups_by_name = boxes.generators.ui_groups_by_name + + for name, box in self.boxes.items(): + box.UI = "web" + self.groups_by_name.get(box.ui_group, + self.groups_by_name["Misc"]).add(box) + + self.staticdir = os.path.join(os.path.dirname(__file__), '../static/') + self._languages = None + self._cache: dict[Any, Any] = {} + self.url_prefix = url_prefix + self.static_url = static_url + + def getLanguages(self, domain=None, localedir=None): + if self._languages is not None: + return self._languages + self._languages = [] + domain = "boxes.py" + for localedir in ["locale", gettext._default_localedir]: + files = glob.glob(os.path.join(localedir, '*', 'LC_MESSAGES', '%s.mo' % domain)) + self._languages.extend([file.split(os.path.sep)[-3] for file in files]) + self._languages.sort() + return self._languages + + def getLanguage(self, args, accept_language): + lang = None + langs = [] + + for i, arg in enumerate(args): + if arg.startswith("language="): + lang = arg[len("language="):] + del args[i] + break + if lang: + try: + return gettext.translation('boxes.py', localedir='locale', languages=[lang]) + except OSError: + pass + try: + return gettext.translation('boxes.py', languages=[lang]) + except OSError: + pass + + # selected language not found try browser default + languages = accept_language.split(",") + for l in languages: + m = self.lang_re.match(l.strip()) + if m: + langs.append((float(m.group(4) or 1.0), m.group(1))) + + langs.sort(reverse=True) + langs = [l[1].replace("-", "_") for l in langs] + + try: + return gettext.translation('boxes.py', localedir='locale', languages=langs) + except OSError: + return gettext.translation('boxes.py', languages=langs, fallback=True) + + def arg2html(self, a, prefix, defaults={}, _=lambda s: s): + name = a.option_strings[0].replace("-", "") + if isinstance(a, argparse._HelpAction): + return "" + viewname = name + if prefix and name.startswith(prefix + '_'): + viewname = name[len(prefix) + 1:] + + default = defaults.get(name, None) + row = """%%s%s\n""" % \ + (name + "_id", name, _(viewname), name + "_description", "" if not a.help else markdown.markdown(_(a.help))) + if (isinstance(a, argparse._StoreAction) and + hasattr(a.type, "html")): + input = a.type.html(name, default or a.default, _) + elif a.type == str and "\n" in a.default: + val = (default or a.default).split("\n") + input = """""" % \ + (name, name, name + "_id", name + "_description", max(len(l) for l in val) + 10, len(val) + 1, default or a.default) + elif a.choices: + options = "\n".join( + """""" % + (e, ' selected="selected"' if (e == (default or a.default)) or (str(e) == str(default or a.default)) else "", + _(e)) for e in a.choices) + input = """\n""".format(name, name, name + "_id", name + "_description", options) + else: + input = """""" % \ + (name, name, name + "_id", name + "_description", default or a.default) + + return row % input + + def args2html_cached(self, name, box, lang, action="", defaults={}): + if defaults == {}: + key = (name, lang.info().get('language', None), action) + if key not in self._cache: + self._cache[key] = list(self.args2html(name, box, lang, action, defaults)) + return self._cache[key] + + return self.args2html(name, box, lang, action, defaults) + + def args2html(self, name, box, lang, action="", defaults={}): + _ = lang.gettext + lang_name = lang.info().get('language', None) + + langparam = "" + if lang_name: + langparam = "?language=" + lang_name + + result = [f"""{self.genHTMLStart(lang)} + + {_("%s - Boxes") % _(name)} + {self.genHTMLMeta()} + {self.genHTMLMetaLanguageLink()} + {self.genHTMLCSS()} + {self.genHTMLJS()} + + + +
+
+

{_("Boxes.py")}

+
+
+self-Logo +
+
+
+
+

{_(name)}

+

{_(box.__doc__) if box.__doc__ else ""}

+
+ """] + groupid = 0 + for group in box.argparser._action_groups[3:] + box.argparser._action_groups[:3]: + if not group._group_actions: + continue + if len(group._group_actions) == 1 and isinstance(group._group_actions[0], argparse._HelpAction): + continue + prefix = getattr(group, "prefix", None) + result.append(f'''

{_(group.title)}

\n\n''') + + for a in group._group_actions: + if a.dest in ("input", "output"): + continue + result.append(self.arg2html(a, prefix, defaults, _)) + result.append("") + groupid += 1 + + result.append(f""" + + +

+ + + + +

+
+
+ +
+
+
+""") + no_img_msg = _('There is no image yet. Please donate an image of your project on GitHub!') + + if box.description: + result.append( + markdown.markdown(_(box.description), extensions=["extra"]) + .replace('src="static/', f'src="{self.static_url}/')) + + result.append(f'''
+Picture of box. +
+
+
+{self.genPagePartFooter(lang)} + + + ''') + return (s.encode("utf-8") for s in result) + + def genPageMenu(self, lang): + _ = lang.gettext + lang_name = lang.info().get('language', None) + + langparam = "" + if lang_name: + langparam = "?language=" + lang_name + + result = [f"""{self.genHTMLStart(lang)} + + {_("Boxes.py")} + {self.genHTMLMeta()} + {self.genHTMLMetaLanguageLink()} + {self.genHTMLCSS()} + {self.genHTMLJS()} + + +
+
+{self.genPagePartHeader(lang)} + +
+{_("Gallery")} +{_("Menu")} +
+
+ + +
+
+
+
+
+{self.genPagePartFooter(lang)} + + +""") + return (s.encode("utf-8") for s in result) + + def genHTMLStart(self, lang) -> str: + lang_attr = lang.info().get("language", "") + + if lang_attr != "": + return f"""""" + + return "" + + def genHTMLMeta(self) -> str: + return f''' + + + + + + ''' + + def genHTMLMetaLanguageLink(self) -> str: + """Generates meta language list for search engines.""" + languages = self.getLanguages() + + s = "" + for language in languages: + s += f'\n' + return s + + def genHTMLCSS(self) -> str: + return f'' + + def genHTMLJS(self) -> str: + return f'' + + def genHTMLLanguageSelection(self, lang) -> str: + """Generates a dropdown selection for the language change.""" + current_language = lang.info().get('language', '') + languages = self.getLanguages() + + if len(languages) < 2: + return "" + + html_option = "" + for language in languages: + html_option += f"\n" + + return """ +
+ +
+ """ + + def genPagePartHeader(self, lang) -> str: + _ = lang.gettext + lang_name = lang.info().get('language', None) + + langparam = "" + if lang_name: + langparam = "?language=" + lang_name + + return f""" +

{_("Boxes.py")}

+

{_("Create boxes and more with a laser cutter!")}

+

+{_(''' + Boxes.py is an Open Source box generator written in Python. It features both finished parametrized generators as well as a Python API for writing your own. It features finger and (flat) dovetail joints, flex cuts, holes and slots for screws, hinges, gears, pulleys and much more.''')} +

+ + +
+self-Logo +
+ +
+ +
+ +""" + + def genPagePartFooter(self, lang) -> str: + _ = lang.gettext + + return """ + +""" + + def genPageError(self, name, e, lang) -> list[bytes]: + """Generates a error page.""" + _ = lang.gettext + + h = f"""{self.genHTMLStart(lang)} + + {_("Error generating %s") % _(name)} + {self.genHTMLMeta()} + + + +

{_("An error occurred!")}

+""" + for s in str(e).split("\n"): + h += f"

{html.escape(s)}

\n" + h += "" + return [h.encode("utf-8")] + + def serveStatic(self, environ, start_response): + filename = environ["PATH_INFO"][len("/static/"):] + path = os.path.join(self.staticdir, filename) + if (not re.match(r"[a-zA-Z0-9_/-]+\.[a-zA-Z0-9]+", filename) or + not os.path.exists(path)): + if re.match(r"samples/.*-thumb.jpg", filename): + path = os.path.join(self.staticdir, "nothing.png") + else: + start_response("404 Not Found", [('Content-type', 'text/plain')]) + return [b"Not found"] + + type_, encoding = mimetypes.guess_type(filename) + if encoding is None: + encoding = "utf-8" + + # Images do not have charset. Just bytes. Except text based svg. + # Todo: fallback if type_ is None? + if type_ is not None and "image" in type_ and type_ != "image/svg+xml": + start_response("200 OK", [('Content-type', "%s" % type_)]) + else: + start_response("200 OK", [('Content-type', f"{type_}; charset={encoding}")]) + + f = open(path, 'rb') + return environ['wsgi.file_wrapper'](f, 512 * 1024) + + def getURL(self, environ) -> str: + url = environ['wsgi.url_scheme'] + '://' + + if environ.get('HTTP_HOST'): + url += environ['HTTP_HOST'] + else: + url += environ['SERVER_NAME'] + + if environ['wsgi.url_scheme'] == 'https': + if environ['SERVER_PORT'] != '443': + url += ':' + environ['SERVER_PORT'] + else: + if environ['SERVER_PORT'] != '80': + url += ':' + environ['SERVER_PORT'] + url += quote(self.url_prefix) + url += quote(environ.get('SCRIPT_NAME', '')) + url += quote(environ.get('PATH_INFO', '')) + if environ.get('QUERY_STRING'): + url += '?' + environ['QUERY_STRING'] + + return url + + def serveGallery(self, environ, start_response, lang): + _ = lang.gettext + lang_name = lang.info().get('language', None) + + start_response("200 OK", [('Content-type', "text/html; charset=utf-8")]) + + if ("Gallery", lang_name) in self._cache: + return self._cache[("Gallery", lang_name)] + + langparam = "" + if lang_name: + langparam = "?language=" + lang_name + + result = [f""" +{self.genHTMLStart(lang)} + + {_("Gallery")} - {_("Boxes.py")} + {self.genHTMLMeta()} + {self.genHTMLMetaLanguageLink()} + {self.genHTMLCSS()} + {self.genHTMLJS()} + + +
+
+{self.genPagePartHeader(lang)} +
+{_("Gallery")} +{_("Menu")} +
+"""] + for nr, group in enumerate(self.groups): + result.append(f"

{_(group.title)}

\n") + for box in group.generators: + name = box.__name__ + fn = f"samples/{name}-thumb.jpg" + thumbnail = f"{self.static_url}/{fn}" + static_filename = os.path.join(self.staticdir, fn) + alt = f"{_(name)}" + href = f"{name}{langparam}" + if not os.path.exists(static_filename): + result.append(f""" {_(box.__doc__)}

{_(name)}
\n""") + else: + result.append(f""" {alt}
{_(name)}
\n""") + + result.append(f""" +
+

+{self.genPagePartFooter(lang)} + + +""" + ) + self._cache[("Gallery", lang_name)] = [s.encode("utf-8") for s in result] + return self._cache[("Gallery", lang_name)] + + def serve(self, environ, start_response): + # serve favicon from static for generated SVGs + if environ["PATH_INFO"] == "favicon.ico": + environ["PATH_INFO"] = "/static/favicon.ico" + if environ["PATH_INFO"].startswith("/static/"): + return self.serveStatic(environ, start_response) + + status = '200 OK' + headers = [('Content-type', 'text/html; charset=utf-8'), ('X-XSS-Protection', '1; mode=block'), ('X-Content-Type-Options', 'nosniff'), ('x-frame-options', 'SAMEORIGIN'), ('Referrer-Policy', 'no-referrer')] + + name = environ["PATH_INFO"][1:] + args = [unquote_plus(arg) for arg in environ.get('QUERY_STRING', '').split("&")] + render = "0" + for arg in args: + if arg.startswith("render="): + render = arg[len("render="):] + + lang = self.getLanguage(args, environ.get("HTTP_ACCEPT_LANGUAGE", "")) + _ = lang.gettext + + if not name or name == "Gallery": + return self.serveGallery(environ, start_response, lang) + + box_cls = self.boxes.get(name, None) + if not box_cls: + start_response(status, headers) + + lang_name = lang.info().get('language', None) + if lang_name not in self._cache: + self._cache[lang_name] = list(self.genPageMenu(lang)) + return self._cache[lang_name] + + box = box_cls() + + box.translations = lang + + if render == "0": + defaults = {} + for a in args: + kv = a.split('=') + if len(kv) == 2: + k, v = kv + defaults[k] = html.escape(v, True) + start_response(status, headers) + return self.args2html_cached(name, box, lang, "./" + name, defaults=defaults) + + args = ["--" + arg for arg in args if not arg.startswith("render=")] + try: + box.parseArgs(args) + except ArgumentParserError as e: + start_response(status, headers) + return self.genPageError(name, e, lang) + + try: + box.metadata["url"] = self.getURL(environ) + box.metadata["url_short"] = filter_url(box.metadata["url"], + box.non_default_args) + box.open() + box.render() + data = box.close() + except Exception as e: + if not isinstance(e, ValueError): + print("Exception during rendering:") + traceback.print_exc() + start_response("500 Internal Server Error", headers) + return self.genPageError(name, e, lang) + + http_headers = box.formats.http_headers.get(box.format, [('Content-type', 'application/unknown; charset=utf-8')])[:] + # Prevent crawlers. + http_headers.append(('X-Robots-Tag', 'noindex,nofollow')) + + if render == "3": + http_headers = [('Content-type', 'image/png')] + http_headers.append(('X-Robots-Tag', 'noindex,nofollow')) + qr_format = "png" + fn = box.__class__.__name__ + start_response(status, http_headers) + qrcode = get_qrcode(box.metadata["url_short"], qr_format) + return (qrcode,) + + if box.format != "svg" or render == "2": + extension = box.format + if extension == "svg_Ponoko": + extension = "svg" + http_headers.append(('Content-Disposition', f'attachment; filename="{box.__class__.__name__}.{extension}"')) + start_response(status, http_headers) + return environ['wsgi.file_wrapper'](data, 512 * 1024) + + +def get_qrcode(url, format): + if url is None: + url = "no url" + img = qrcode.make(url) + image_bytes = io.BytesIO() + img.save(image_bytes, format=format) + return image_bytes.getvalue() + + +def main() -> None: + parser = argparse.ArgumentParser() + + parser.add_argument("--host", default="") + parser.add_argument("--port", type=int, default=8000) + parser.add_argument("--url_prefix", default="", + help="URL path to Boxes.py instance") + parser.add_argument("--static_url", default="static", + help="URL of static content") + args = parser.parse_args() + + boxserver = BServer(url_prefix=args.url_prefix, static_url=args.static_url) + + fc = FileChecker() + fc.start() + + httpd = make_server(args.host, args.port, boxserver.serve) + print(f"BoxesServer serving on {args.host}:{args.port}...") + try: + httpd.serve_forever() + except KeyboardInterrupt: + fc.stop() + httpd.server_close() + print("BoxesServer stops.") + + +if __name__ == "__main__": + main() +else: + boxserver = BServer(static_url="https://florianfesti.github.io/boxes/static") + application = boxserver.serve diff --git a/pyproject.toml b/pyproject.toml index cfa5b372..c16b0523 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,8 +28,9 @@ dependencies = {file=["requirements.txt"]} optional-dependencies = {dev = { file = ["requirements_dev.txt"] }} [project.scripts] -boxes = 'scripts.boxes:main' -boxesserver = 'scripts.boxesserver:main' +boxes = 'boxes.scripts.boxes_main:main' +boxesserver = 'boxes.scripts.boxesserver:main' +boxes_proxy = 'boxes.scripts.boxes_proxy:main' [project.urls] Homepage = "https://hackaday.io/project/10649-boxespy" diff --git a/scripts/boxes b/scripts/boxes deleted file mode 100755 index 5ea9e903..00000000 --- a/scripts/boxes +++ /dev/null @@ -1,154 +0,0 @@ -#!/usr/bin/env python3 -"""boxes.py - -Generate stencils for wooden boxes. - -Usage: - boxes [...] - boxes --list - boxes --examples - boxes (-h | --help) - -Options: - --list List available generators. - --examples Generates an SVG for every generator into the "examples" folder. - -h --help Show this screen. -""" -from __future__ import annotations - -import gettext -import os -import sys -from pathlib import Path - -try: - import boxes -except ImportError: - sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..')) - import boxes - -import boxes.generators - - -def print_grouped_generators() -> None: - class ConsoleColors: - BOLD = '\033[1m' - CLEAR = '\033[0m' - ITALIC = '\033[3m' - UNDERLINE = '\033[4m' - - print('Available generators:') - for group in generator_groups(): - print('\n' + ConsoleColors.UNDERLINE + group.title + ConsoleColors.CLEAR) - if group.description: - print('\n' + group.description) - print() - for box in group.generators: - description = box.__doc__ or "" - description = description.replace("\n", "").replace("\r", "").strip() - print(f' * {box.__name__:<15} - {ConsoleColors.ITALIC}{description}{ConsoleColors.CLEAR}') - - -def create_example_every_generator() -> None: - print("Generating SVG examples for every possible generator.") - for group in generator_groups(): - for boxExample in group.generators: - boxName = boxExample.__name__ - notTestGenerator = ('GridfinityTrayLayout', 'TrayLayout', 'TrayLayoutFile', 'TypeTray', 'Edges',) - brokenGenerator = () - avoidGenerator = notTestGenerator + brokenGenerator - if boxName in avoidGenerator: - print(f"SKIP: {boxName}") - continue - print(f"Generate example for: {boxName}") - - box = boxExample() - box.translations = get_translation() - box.parseArgs("") - box.metadata["reproducible"] = True - box.open() - box.render() - boxData = box.close() - - file = Path('examples') / (boxName + '.svg') - file.write_bytes(boxData.getvalue()) - - -def get_translation(): - try: - return gettext.translation('boxes.py', localedir='locale') - except OSError: - return gettext.translation('boxes.py', fallback=True) - - -def run_generator(name: str, args) -> None: - generators = generators_by_name() - lower_name = name.lower() - - if lower_name in generators.keys(): - box = generators[lower_name]() - box.translations = get_translation() - box.parseArgs(args) - box.open() - box.render() - data = box.close() - with os.fdopen(sys.stdout.fileno(), "wb", closefd=False) if box.output == "-" else open(box.output, 'wb') as f: - f.write(data.getvalue()) - else: - msg = f"Unknown generator '{name}'. Use boxes --list to get a list of available commands.\n" - sys.stderr.write(msg) - - -def generator_groups(): - generators = generators_by_name() - return group_generators(generators) - - -def group_generators(generators): - groups = boxes.generators.ui_groups - groups_by_name = boxes.generators.ui_groups_by_name - - for name, generator in generators.items(): - group_for_generator = groups_by_name.get(generator.ui_group, groups_by_name['Misc']) - group_for_generator.add(generator) - - return groups - - -def generators_by_name() -> dict[str, type[boxes.Boxes]]: - all_generators = boxes.generators.getAllBoxGenerators() - - return { - name.split('.')[-1].lower(): generator - for name, generator in all_generators.items() - } - - -def print_usage() -> None: - print(__doc__) - - -def print_version() -> None: - print("boxes does not use versioning.") - - -def main() -> None: - if len(sys.argv) > 1 and sys.argv[1].startswith("--id="): - del sys.argv[1] - if len(sys.argv) == 1 or sys.argv[1] == '--help' or sys.argv[1] == '-h': - print_usage() - elif sys.argv[1] == '--version': - print_version() - elif sys.argv[1] == '--list': - print_grouped_generators() - elif sys.argv[1] == '--examples': - create_example_every_generator() - else: - name = sys.argv[1].lower() - if name.startswith("--generator="): - name = name[12:] - run_generator(name, sys.argv[2:]) - - -if __name__ == '__main__': - main() diff --git a/scripts/boxes b/scripts/boxes new file mode 120000 index 00000000..f405ea2a --- /dev/null +++ b/scripts/boxes @@ -0,0 +1 @@ +../boxes/scripts/boxes_main.py \ No newline at end of file diff --git a/scripts/boxes_proxy.py b/scripts/boxes_proxy.py deleted file mode 100755 index 0800ab3b..00000000 --- a/scripts/boxes_proxy.py +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env python3 - -""" -Extension for InkScape 1.0+ - -boxes.py wrapper script to make it work on Windows and Linux systems without duplicating .inx files - -Author: Mario Voigt / FabLab Chemnitz -Mail: mario.voigt@stadtfabrikanten.org -Date: 27.04.2021 -Last patch: 27.04.2021 -License: GNU GPL v3 - -""" -pass -import subprocess -import sys - -pass -from shlex import quote - -from inkex.extensions import GenerateExtension -from lxml import etree - -import inkex - - -class boxesPyWrapper(GenerateExtension): - def add_arguments(self, pars): - args = sys.argv[1:] - for arg in args: - key = arg.split("=")[0] - if key == "--id": - continue - if len(arg.split("=")) == 2: - value = arg.split("=")[1] - pars.add_argument(key, default=key) - - def generate(self): - cmd = "boxes" # boxes.exe in this local dir (or if present in %PATH%), or boxes from $PATH in linux - for arg in vars(self.options): - if arg in ( - "output", "id", "ids", "selected_nodes", - "input_file", "tab"): - continue - # fix behaviour of "original" arg which does not correctly gets - # interpreted if set to false - if arg == "original" and str(getattr(self.options, arg)) == "false": - continue - cmd += f" --{arg} {quote(str(getattr(self.options, arg)))}" - cmd += f" --output -" - cmd = cmd.replace("boxes --generator", "boxes") - - # run boxes with the parameters provided - result = subprocess.run(cmd.split(), capture_output=True) - - if result.returncode: - inkex.utils.debug("Generating box svg failed. Cannot continue. Command was:") - inkex.utils.debug(str(cmd)) - inkex.utils.debug(str(result.stderr)) - exit(1) - - # write the generated SVG into Inkscape's canvas - p = etree.XMLParser(huge_tree=True) - doc = etree.fromstring(result.stdout, parser=etree.XMLParser(huge_tree=True)) - group = inkex.Group(id="boxes.py") - for element in doc: - group.append(element) - return group - - -def main() -> None: - boxesPyWrapper().run() - - -if __name__ == '__main__': - main() diff --git a/scripts/boxes_proxy.py b/scripts/boxes_proxy.py new file mode 120000 index 00000000..db1c73ab --- /dev/null +++ b/scripts/boxes_proxy.py @@ -0,0 +1 @@ +../boxes/scripts/boxes_proxy.py \ No newline at end of file diff --git a/scripts/boxesserver b/scripts/boxesserver deleted file mode 100755 index 8ae4de4d..00000000 --- a/scripts/boxesserver +++ /dev/null @@ -1,711 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (C) 2016-2017 Florian Festi -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -from __future__ import annotations - -import argparse -import gettext -import glob -import html -import io -import mimetypes -import os.path -import re -import sys -import threading -import time -import traceback -from typing import Any, NoReturn -from urllib.parse import quote, unquote_plus -from wsgiref.simple_server import make_server - -import markdown -import qrcode - -try: - import boxes.generators -except ImportError: - sys.path.append(os.path.join(os.path.dirname(__file__), "..")) - import boxes.generators - - -class FileChecker(threading.Thread): - def __init__(self, files=[], checkmodules: bool = True) -> None: - super().__init__() - self.checkmodules = checkmodules - self.timestamps = {} - self._stopped = False - for path in files: - self.timestamps[path] = os.stat(path).st_mtime - if checkmodules: - self._addModules() - - def _addModules(self) -> None: - for name, module in sys.modules.items(): - path = getattr(module, "__file__", None) - if not path: - continue - if path not in self.timestamps: - self.timestamps[path] = os.stat(path).st_mtime - - def filesOK(self) -> bool: - if self.checkmodules: - self._addModules() - for path, timestamp in self.timestamps.items(): - try: - if os.stat(path).st_mtime != timestamp: - return False - except FileNotFoundError: - return False - return True - - def run(self) -> None: - while not self._stopped: - if not self.filesOK(): - os.execv(__file__, sys.argv) - time.sleep(1) - - def stop(self) -> None: - self._stopped = True - - -def filter_url(url, non_default_args): - if len(url) == 0: - return '' - try: - base, args = url.split('?') - except ValueError: - return '' - args = args.split('&') - new_args = [] - args_to_ignore = ["qr_code", "format"] - for arg in args: - a, b = arg.split('=') - if a.strip() in args_to_ignore: - continue - if a in non_default_args: - new_args.append(arg) - if len(new_args): - return f"{base}?{'&'.join(new_args)}" - else: - return f"{base}" - - -class ArgumentParserError(Exception): pass - - -class ThrowingArgumentParser(argparse.ArgumentParser): - def error(self, message) -> NoReturn: - raise ArgumentParserError(message) - - -# Evil hack -boxes.ArgumentParser = ThrowingArgumentParser # type: ignore - - -class BServer: - lang_re = re.compile(r"([a-z]{2,3}(-[-a-zA-Z0-9]*)?)\s*(;\s*q=(\d\.?\d*))?") - - def __init__(self, url_prefix="", static_url="static") -> None: - self.boxes = {b.__name__: b for b in boxes.generators.getAllBoxGenerators().values() if b.webinterface} - self.groups = boxes.generators.ui_groups - self.groups_by_name = boxes.generators.ui_groups_by_name - - for name, box in self.boxes.items(): - box.UI = "web" - self.groups_by_name.get(box.ui_group, - self.groups_by_name["Misc"]).add(box) - - self.staticdir = os.path.join(os.path.dirname(__file__), '../static/') - self._languages = None - self._cache: dict[Any, Any] = {} - self.url_prefix = url_prefix - self.static_url = static_url - - def getLanguages(self, domain=None, localedir=None): - if self._languages is not None: - return self._languages - self._languages = [] - domain = "boxes.py" - for localedir in ["locale", gettext._default_localedir]: - files = glob.glob(os.path.join(localedir, '*', 'LC_MESSAGES', '%s.mo' % domain)) - self._languages.extend([file.split(os.path.sep)[-3] for file in files]) - self._languages.sort() - return self._languages - - def getLanguage(self, args, accept_language): - lang = None - langs = [] - - for i, arg in enumerate(args): - if arg.startswith("language="): - lang = arg[len("language="):] - del args[i] - break - if lang: - try: - return gettext.translation('boxes.py', localedir='locale', languages=[lang]) - except OSError: - pass - try: - return gettext.translation('boxes.py', languages=[lang]) - except OSError: - pass - - # selected language not found try browser default - languages = accept_language.split(",") - for l in languages: - m = self.lang_re.match(l.strip()) - if m: - langs.append((float(m.group(4) or 1.0), m.group(1))) - - langs.sort(reverse=True) - langs = [l[1].replace("-", "_") for l in langs] - - try: - return gettext.translation('boxes.py', localedir='locale', languages=langs) - except OSError: - return gettext.translation('boxes.py', languages=langs, fallback=True) - - def arg2html(self, a, prefix, defaults={}, _=lambda s: s): - name = a.option_strings[0].replace("-", "") - if isinstance(a, argparse._HelpAction): - return "" - viewname = name - if prefix and name.startswith(prefix + '_'): - viewname = name[len(prefix) + 1:] - - default = defaults.get(name, None) - row = """%%s%s\n""" % \ - (name + "_id", name, _(viewname), name + "_description", "" if not a.help else markdown.markdown(_(a.help))) - if (isinstance(a, argparse._StoreAction) and - hasattr(a.type, "html")): - input = a.type.html(name, default or a.default, _) - elif a.type == str and "\n" in a.default: - val = (default or a.default).split("\n") - input = """""" % \ - (name, name, name + "_id", name + "_description", max(len(l) for l in val) + 10, len(val) + 1, default or a.default) - elif a.choices: - options = "\n".join( - """""" % - (e, ' selected="selected"' if (e == (default or a.default)) or (str(e) == str(default or a.default)) else "", - _(e)) for e in a.choices) - input = """\n""".format(name, name, name + "_id", name + "_description", options) - else: - input = """""" % \ - (name, name, name + "_id", name + "_description", default or a.default) - - return row % input - - def args2html_cached(self, name, box, lang, action="", defaults={}): - if defaults == {}: - key = (name, lang.info().get('language', None), action) - if key not in self._cache: - self._cache[key] = list(self.args2html(name, box, lang, action, defaults)) - return self._cache[key] - - return self.args2html(name, box, lang, action, defaults) - - def args2html(self, name, box, lang, action="", defaults={}): - _ = lang.gettext - lang_name = lang.info().get('language', None) - - langparam = "" - if lang_name: - langparam = "?language=" + lang_name - - result = [f"""{self.genHTMLStart(lang)} - - {_("%s - Boxes") % _(name)} - {self.genHTMLMeta()} - {self.genHTMLMetaLanguageLink()} - {self.genHTMLCSS()} - {self.genHTMLJS()} - - - -
- -
-self-Logo -
-
-
-
-

{_(name)}

-

{_(box.__doc__) if box.__doc__ else ""}

-
- """] - groupid = 0 - for group in box.argparser._action_groups[3:] + box.argparser._action_groups[:3]: - if not group._group_actions: - continue - if len(group._group_actions) == 1 and isinstance(group._group_actions[0], argparse._HelpAction): - continue - prefix = getattr(group, "prefix", None) - result.append(f'''

{_(group.title)}

\n\n''') - - for a in group._group_actions: - if a.dest in ("input", "output"): - continue - result.append(self.arg2html(a, prefix, defaults, _)) - result.append("") - groupid += 1 - - result.append(f""" - - -

- - - - -

-
-
- -
-
-
-""") - no_img_msg = _('There is no image yet. Please donate an image of your project on GitHub!') - - if box.description: - result.append( - markdown.markdown(_(box.description), extensions=["extra"]) - .replace('src="static/', f'src="{self.static_url}/')) - - result.append(f'''
-Picture of box. -
-
-
-{self.genPagePartFooter(lang)} - - - ''') - return (s.encode("utf-8") for s in result) - - def genPageMenu(self, lang): - _ = lang.gettext - lang_name = lang.info().get('language', None) - - langparam = "" - if lang_name: - langparam = "?language=" + lang_name - - result = [f"""{self.genHTMLStart(lang)} - - {_("Boxes.py")} - {self.genHTMLMeta()} - {self.genHTMLMetaLanguageLink()} - {self.genHTMLCSS()} - {self.genHTMLJS()} - - -
-
-{self.genPagePartHeader(lang)} - -
-{_("Gallery")} -{_("Menu")} -
-
- - -
-
-
-
-
-{self.genPagePartFooter(lang)} - - -""") - return (s.encode("utf-8") for s in result) - - def genHTMLStart(self, lang) -> str: - lang_attr = lang.info().get("language", "") - - if lang_attr != "": - return f"""""" - - return "" - - def genHTMLMeta(self) -> str: - return f''' - - - - - - ''' - - def genHTMLMetaLanguageLink(self) -> str: - """Generates meta language list for search engines.""" - languages = self.getLanguages() - - s = "" - for language in languages: - s += f'\n' - return s - - def genHTMLCSS(self) -> str: - return f'' - - def genHTMLJS(self) -> str: - return f'' - - def genHTMLLanguageSelection(self, lang) -> str: - """Generates a dropdown selection for the language change.""" - current_language = lang.info().get('language', '') - languages = self.getLanguages() - - if len(languages) < 2: - return "" - - html_option = "" - for language in languages: - html_option += f"\n" - - return """ -
- -
- """ - - def genPagePartHeader(self, lang) -> str: - _ = lang.gettext - lang_name = lang.info().get('language', None) - - langparam = "" - if lang_name: - langparam = "?language=" + lang_name - - return f""" -

{_("Boxes.py")}

-

{_("Create boxes and more with a laser cutter!")}

-

-{_(''' - Boxes.py is an Open Source box generator written in Python. It features both finished parametrized generators as well as a Python API for writing your own. It features finger and (flat) dovetail joints, flex cuts, holes and slots for screws, hinges, gears, pulleys and much more.''')} -

-
- -
-self-Logo -
- -
- -
- -""" - - def genPagePartFooter(self, lang) -> str: - _ = lang.gettext - - return """ - -""" - - def genPageError(self, name, e, lang) -> list[bytes]: - """Generates a error page.""" - _ = lang.gettext - - h = f"""{self.genHTMLStart(lang)} - - {_("Error generating %s") % _(name)} - {self.genHTMLMeta()} - - - -

{_("An error occurred!")}

-""" - for s in str(e).split("\n"): - h += f"

{html.escape(s)}

\n" - h += "" - return [h.encode("utf-8")] - - def serveStatic(self, environ, start_response): - filename = environ["PATH_INFO"][len("/static/"):] - path = os.path.join(self.staticdir, filename) - if (not re.match(r"[a-zA-Z0-9_/-]+\.[a-zA-Z0-9]+", filename) or - not os.path.exists(path)): - if re.match(r"samples/.*-thumb.jpg", filename): - path = os.path.join(self.staticdir, "nothing.png") - else: - start_response("404 Not Found", [('Content-type', 'text/plain')]) - return [b"Not found"] - - type_, encoding = mimetypes.guess_type(filename) - if encoding is None: - encoding = "utf-8" - - # Images do not have charset. Just bytes. Except text based svg. - # Todo: fallback if type_ is None? - if type_ is not None and "image" in type_ and type_ != "image/svg+xml": - start_response("200 OK", [('Content-type', "%s" % type_)]) - else: - start_response("200 OK", [('Content-type', f"{type_}; charset={encoding}")]) - - f = open(path, 'rb') - return environ['wsgi.file_wrapper'](f, 512 * 1024) - - def getURL(self, environ) -> str: - url = environ['wsgi.url_scheme'] + '://' - - if environ.get('HTTP_HOST'): - url += environ['HTTP_HOST'] - else: - url += environ['SERVER_NAME'] - - if environ['wsgi.url_scheme'] == 'https': - if environ['SERVER_PORT'] != '443': - url += ':' + environ['SERVER_PORT'] - else: - if environ['SERVER_PORT'] != '80': - url += ':' + environ['SERVER_PORT'] - url += quote(self.url_prefix) - url += quote(environ.get('SCRIPT_NAME', '')) - url += quote(environ.get('PATH_INFO', '')) - if environ.get('QUERY_STRING'): - url += '?' + environ['QUERY_STRING'] - - return url - - def serveGallery(self, environ, start_response, lang): - _ = lang.gettext - lang_name = lang.info().get('language', None) - - start_response("200 OK", [('Content-type', "text/html; charset=utf-8")]) - - if ("Gallery", lang_name) in self._cache: - return self._cache[("Gallery", lang_name)] - - langparam = "" - if lang_name: - langparam = "?language=" + lang_name - - result = [f""" -{self.genHTMLStart(lang)} - - {_("Gallery")} - {_("Boxes.py")} - {self.genHTMLMeta()} - {self.genHTMLMetaLanguageLink()} - {self.genHTMLCSS()} - {self.genHTMLJS()} - - -
-
-{self.genPagePartHeader(lang)} -
-{_("Gallery")} -{_("Menu")} -
-"""] - for nr, group in enumerate(self.groups): - result.append(f"

{_(group.title)}

\n") - for box in group.generators: - name = box.__name__ - fn = f"samples/{name}-thumb.jpg" - thumbnail = f"{self.static_url}/{fn}" - static_filename = os.path.join(self.staticdir, fn) - alt = f"{_(name)}" - href = f"{name}{langparam}" - if not os.path.exists(static_filename): - result.append(f""" {_(box.__doc__)}

{_(name)}
\n""") - else: - result.append(f""" {alt}
{_(name)}
\n""") - - result.append(f""" -
-

-{self.genPagePartFooter(lang)} - - -""" - ) - self._cache[("Gallery", lang_name)] = [s.encode("utf-8") for s in result] - return self._cache[("Gallery", lang_name)] - - def serve(self, environ, start_response): - # serve favicon from static for generated SVGs - if environ["PATH_INFO"] == "favicon.ico": - environ["PATH_INFO"] = "/static/favicon.ico" - if environ["PATH_INFO"].startswith("/static/"): - return self.serveStatic(environ, start_response) - - status = '200 OK' - headers = [('Content-type', 'text/html; charset=utf-8'), ('X-XSS-Protection', '1; mode=block'), ('X-Content-Type-Options', 'nosniff'), ('x-frame-options', 'SAMEORIGIN'), ('Referrer-Policy', 'no-referrer')] - - name = environ["PATH_INFO"][1:] - args = [unquote_plus(arg) for arg in environ.get('QUERY_STRING', '').split("&")] - render = "0" - for arg in args: - if arg.startswith("render="): - render = arg[len("render="):] - - lang = self.getLanguage(args, environ.get("HTTP_ACCEPT_LANGUAGE", "")) - _ = lang.gettext - - if not name or name == "Gallery": - return self.serveGallery(environ, start_response, lang) - - box_cls = self.boxes.get(name, None) - if not box_cls: - start_response(status, headers) - - lang_name = lang.info().get('language', None) - if lang_name not in self._cache: - self._cache[lang_name] = list(self.genPageMenu(lang)) - return self._cache[lang_name] - - box = box_cls() - - box.translations = lang - - if render == "0": - defaults = {} - for a in args: - kv = a.split('=') - if len(kv) == 2: - k, v = kv - defaults[k] = html.escape(v, True) - start_response(status, headers) - return self.args2html_cached(name, box, lang, "./" + name, defaults=defaults) - - args = ["--" + arg for arg in args if not arg.startswith("render=")] - try: - box.parseArgs(args) - except ArgumentParserError as e: - start_response(status, headers) - return self.genPageError(name, e, lang) - - try: - box.metadata["url"] = self.getURL(environ) - box.metadata["url_short"] = filter_url(box.metadata["url"], - box.non_default_args) - box.open() - box.render() - data = box.close() - except Exception as e: - if not isinstance(e, ValueError): - print("Exception during rendering:") - traceback.print_exc() - start_response("500 Internal Server Error", headers) - return self.genPageError(name, e, lang) - - http_headers = box.formats.http_headers.get(box.format, [('Content-type', 'application/unknown; charset=utf-8')])[:] - # Prevent crawlers. - http_headers.append(('X-Robots-Tag', 'noindex,nofollow')) - - if render == "3": - http_headers = [('Content-type', 'image/png')] - http_headers.append(('X-Robots-Tag', 'noindex,nofollow')) - qr_format = "png" - fn = box.__class__.__name__ - start_response(status, http_headers) - qrcode = get_qrcode(box.metadata["url_short"], qr_format) - return (qrcode,) - - if box.format != "svg" or render == "2": - extension = box.format - if extension == "svg_Ponoko": - extension = "svg" - http_headers.append(('Content-Disposition', f'attachment; filename="{box.__class__.__name__}.{extension}"')) - start_response(status, http_headers) - return environ['wsgi.file_wrapper'](data, 512 * 1024) - - -def get_qrcode(url, format): - if url is None: - url = "no url" - img = qrcode.make(url) - image_bytes = io.BytesIO() - img.save(image_bytes, format=format) - return image_bytes.getvalue() - - -def main() -> None: - parser = argparse.ArgumentParser() - - parser.add_argument("--host", default="") - parser.add_argument("--port", type=int, default=8000) - parser.add_argument("--url_prefix", default="", - help="URL path to Boxes.py instance") - parser.add_argument("--static_url", default="static", - help="URL of static content") - args = parser.parse_args() - - boxserver = BServer(url_prefix=args.url_prefix, static_url=args.static_url) - - fc = FileChecker() - fc.start() - - httpd = make_server(args.host, args.port, boxserver.serve) - print(f"BoxesServer serving on {args.host}:{args.port}...") - try: - httpd.serve_forever() - except KeyboardInterrupt: - fc.stop() - httpd.server_close() - print("BoxesServer stops.") - - -if __name__ == "__main__": - main() -else: - boxserver = BServer(static_url="https://florianfesti.github.io/boxes/static") - application = boxserver.serve diff --git a/scripts/boxesserver b/scripts/boxesserver new file mode 120000 index 00000000..354b2960 --- /dev/null +++ b/scripts/boxesserver @@ -0,0 +1 @@ +../boxes/scripts/boxesserver.py \ No newline at end of file