]> git.mar77i.info Git - hublib/commitdiff
functioning basic chat app
authormar77i <mar77i@protonmail.ch>
Thu, 3 Aug 2023 23:03:14 +0000 (01:03 +0200)
committermar77i <mar77i@protonmail.ch>
Thu, 3 Aug 2023 23:03:14 +0000 (01:03 +0200)
14 files changed:
hub/app.py
hub/cryptresource.py [deleted file]
hub/hub.py [new file with mode: 0644]
hub/staticresource.py [new file with mode: 0644]
hub/utils.py [new file with mode: 0644]
hubapps/first/common.js [new file with mode: 0644]
hubapps/first/index.html [new file with mode: 0644]
hubapps/first/index.html.j2 [deleted file]
hubapps/first/index.js
hubapps/first/master.html.tpl [moved from hubapps/first/master.html.j2 with 70% similarity]
hubapps/first/master.js
requirements.txt [new file with mode: 0644]
run_server.py
setup_venv.sh [new file with mode: 0644]

index 24212d0972581d1e6e7bef054074c80f8b229c6b..8dfb5425b704c111d87513354b6965f1578ca98f 100644 (file)
@@ -1,16 +1,77 @@
 from argparse import ArgumentParser
+from itertools import chain
+from secrets import token_urlsafe
+import socket
+from subprocess import run
+import sys
+from traceback import print_exception
+from typing import List, Optional
+from urllib.parse import urlunsplit
 
 from falcon.asgi import App
 from falcon.constants import MEDIA_HTML
+from uvicorn import Config, Server
 
-from .cryptresource import CryptResource
+from .staticresource import StaticResource
+from .hub import Hub
+
+
+class HubApp(App):
+    def __init__(self, secret, browser, hubapp, *args, **kwargs):
+        kwargs.setdefault("media_type", MEDIA_HTML)
+        super().__init__(*args, **kwargs)
+        self.secret = secret or token_urlsafe(64)
+        self.hub = Hub(self.secret)
+        self.sr = StaticResource(self.secret, hubapp, self.hub.master_ws_uri)
+        self.browser = browser
+        self.hub.add_routes(self)
+        self.sr.add_routes(self)
+        self.add_error_handler(Exception, self.print_exception)
+
+    async def print_exception(self, req, resp, ex, params):
+        print_exception(*sys.exc_info())
 
 
 def get_app():
-    app = App(media_type=MEDIA_HTML)
     ap = ArgumentParser()
     ap.add_argument("--secret")
+    ap.add_argument("--browser", default="firefox")
+    ap.add_argument("--hubapp", default="first")
     args = ap.parse_args()
-    cr = CryptResource(args.secret)
-    cr.register_on(app)
-    return app
+    return HubApp(args.secret, args.browser, args.hubapp)
+
+
+class HubServer(Server):
+    async def startup(self, sockets: Optional[List[socket.socket]] = None) -> None:
+        await super().startup(sockets)
+        config = self.config
+        protocol_name = "https" if config.ssl else "http"
+        host = "0.0.0.0" if config.host is None else config.host
+        if ":" in host:
+            # It's an IPv6 address.
+            host = f"[{host.rstrip(']').lstrip('[')}]"
+
+        port = config.port
+        if port == 0:
+            try:
+                port = next(
+                    chain.from_iterable((server.sockets for server in getattr(self, "servers", ())))
+                ).getsockname()[1]
+            except StopIteration:
+                pass
+        if {"http": 80, "https": 443}[protocol_name] != port:
+            host = f"{host}:{port}"
+        app = config.loaded_app.app
+        print("master_uri", app.sr.master_uri)
+        master_url = urlunsplit((protocol_name, host, app.sr.master_uri, "", ""))
+        print("secret:", app.secret)
+        if not app.browser:
+            print("master url", master_url)
+        else:
+            run([app.browser, master_url])
+
+
+def main():
+    HubServer(
+        Config("hub.app:get_app", factory=True, port=5000, log_level="info")
+    ).run()
diff --git a/hub/cryptresource.py b/hub/cryptresource.py
deleted file mode 100644 (file)
index ce2dc5c..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-from base64 import urlsafe_b64encode
-from hashlib import sha3_512
-from pathlib import Path
-from secrets import token_urlsafe
-
-from falcon.constants import MEDIA_JS
-from jinja2 import Environment, FileSystemLoader
-
-
-class CryptResource:
-    @staticmethod
-    def scramble(secret, value):
-        h = sha3_512()
-        h.update(f"{secret}{value}".encode())
-        return urlsafe_b64encode(h.digest()).rstrip(b'=').decode('ascii')
-
-    def __init__(self, secret=None):
-        self.current_hubapp = "first"
-        self.secret = secret or token_urlsafe(64)
-        self.master_uri = f"/{self.scramble(self.secret, 'index.html')}"
-        self.master_js_uri = f"/{self.scramble(self.secret, 'master.js')}"
-        hubapp_dir = Path(__file__).parents[1] / "hubapps" / self.current_hubapp
-        self.env = Environment(loader=FileSystemLoader(hubapp_dir))
-        with open(hubapp_dir / "index.js", "rb") as fh:
-            self._index_js = fh.read()
-        with open(hubapp_dir / "master.js", "rb") as fh:
-            self._master_js = fh.read()
-
-    async def on_get(self, req, resp):
-        resp.data = self.env.get_template("index.html.j2").render({}).encode()
-
-    async def on_get_index_js(self, req, resp):
-        resp.data = self.env.get_template("index.js").render({}).encode()
-
-    async def on_get_master(self, req, resp):
-        resp.data = self.env.get_template("master.html.j2").render({"master_js": self.master_js_uri}).encode()
-
-    async def on_get_master_js(self, req, resp):
-        resp.data = self._master_js
-        resp.content_type = MEDIA_JS
-
-    def register_on(self, app):
-        app.add_route("/", self)
-        app.add_route("/index.js", self, suffix="index_js")
-        app.add_route(self.master_uri, self, suffix="master")
-        app.add_route(self.master_js_uri, self, suffix="master_js")
diff --git a/hub/hub.py b/hub/hub.py
new file mode 100644 (file)
index 0000000..0eee3f8
--- /dev/null
@@ -0,0 +1,102 @@
+import asyncio
+from asyncio.exceptions import CancelledError
+import json
+import pickle
+import sys
+from traceback import print_exception
+
+from falcon import WebSocketDisconnected
+from redis.asyncio import StrictRedis
+
+from .utils import get_redis_pass, scramble
+
+
+class Hub:
+    def __init__(self, secret):
+        self.master_ws_uri = f"/{scramble(secret, 'ws')}"
+        self.redis = StrictRedis(password=get_redis_pass("/etc/redis/redis.conf"))
+        asyncio.ensure_future(self.redis.set("client_id", 0))
+
+    async def process_websocket(self, client_id, web_socket):
+        try:
+            while True:
+                data = await web_socket.receive_text()
+                try:
+                    parsed_data = json.loads(data)
+                except json.JSONDecodeError:
+                    parsed_data = None
+                if not isinstance(parsed_data, dict):
+                    parsed_data = {"data": data}
+                parsed_data["client_id"] = client_id
+                await self.redis.publish("master", pickle.dumps(parsed_data))
+        except (CancelledError, WebSocketDisconnected):
+            pass
+
+    async def process_pubsub(self, pubsub, web_socket):
+        try:
+            while True:
+                data = await pubsub.get_message(True, .3)
+                if not web_socket.ready or web_socket.closed:
+                    break
+                if data is not None:
+                    await web_socket.send_text(json.dumps(pickle.loads(data["data"])))
+        except (CancelledError, WebSocketDisconnected):
+            pass
+
+    async def on_websocket(self, req, web_socket):
+        client_id = await self.redis.incr("client_id")
+        await web_socket.accept()
+        pubsub = self.redis.pubsub()
+        await pubsub.subscribe(f"client-{client_id}")
+        await self.redis.publish(
+            "master", pickle.dumps({"action": "join", "client_id": client_id}),
+        )
+        try:
+            await asyncio.gather(
+                self.process_websocket(client_id, web_socket),
+                self.process_pubsub(pubsub, web_socket),
+                return_exceptions=True,
+            )
+        except (CancelledError, WebSocketDisconnected):
+            pass
+        except Exception:
+            print_exception(*sys.exc_info())
+        finally:
+            await web_socket.close()
+            await self.redis.publish(
+                "master",
+                pickle.dumps({"action": "leave", "client_id": client_id}),
+            )
+
+    async def process_websocket_master(self, web_socket):
+        try:
+            while True:
+                data = json.loads(await web_socket.receive_text())
+                for client_id in data.pop("client_ids", ()):
+                    await self.redis.publish(
+                        f"client-{client_id}",
+                        pickle.dumps(data),
+                    )
+        except (CancelledError, WebSocketDisconnected) as e:
+            pass
+
+    async def on_websocket_master(self, req, web_socket):
+        await web_socket.accept()
+        pubsub = self.redis.pubsub()
+        await pubsub.subscribe("master")
+        try:
+            await asyncio.gather(
+                self.process_websocket_master(web_socket),
+                self.process_pubsub(pubsub, web_socket),
+                return_exceptions=True,
+            )
+        except (CancelledError, WebSocketDisconnected):
+            pass
+        except Exception:
+            print_exception(*sys.exc_info())
+        finally:
+            await web_socket.close()
+
+    def add_routes(self, app):
+        app.add_route("/ws", self)
+        app.add_route(self.master_ws_uri, self, suffix="master")
diff --git a/hub/staticresource.py b/hub/staticresource.py
new file mode 100644 (file)
index 0000000..9cc949a
--- /dev/null
@@ -0,0 +1,83 @@
+import json
+import mimetypes
+import os
+from pathlib import Path
+
+from .utils import scramble
+
+
+class StaticFile:
+    template_ext = ".tpl"
+
+    def __init__(self, path):
+        self.path = path
+        self.mime_type = mimetypes.guess_type(self.remove_template_ext(str(path)))[0]
+        self.mtime = None
+        self.content = None
+
+    def load_content(self):
+        with open(self.path, "rb") as fh:
+            return fh.read()
+
+    def get(self):
+        if self.path.stat().st_mtime != self.mtime:
+            self.content = self.load_content()
+        return self.content
+
+    def render(self, context_vars):
+        content = self.get()
+        if self.path.suffix == self.template_ext:
+            for k, v in context_vars.items():
+                content = content.replace(b"{{ %s }}" % k.encode(), v.encode())
+        return content
+
+    def __str__(self):
+        return f"<StaticFile at \"{os.sep.join(self.path.parts[-2:])}\" [{self.mime_type}]>"
+
+    @classmethod
+    def remove_template_ext(cls, filename):
+        if filename.endswith(cls.template_ext):
+            filename = filename[:-4]
+        return filename
+
+
+class StaticResource:
+    def __init__(self, secret, current_hubapp, master_ws_uri):
+        mimetypes.init()
+        hub_js_path = Path(__file__).parent / "hub.js"
+        self.static_files = {
+            "/hub.js": StaticFile(hub_js_path),
+        }
+        self.context_vars = {
+            "master_ws_uri": json.dumps(master_ws_uri),
+        }
+        self.master_uri = ""
+        self.setup(secret, current_hubapp)
+
+    def setup(self, secret, current_hubapp):
+        hubapp_dir = Path(__file__).parents[1] / "hubapps" / current_hubapp
+        for file in os.listdir(hubapp_dir):
+            uri_file = StaticFile.remove_template_ext(file)
+            if uri_file.startswith("master."):
+                uri = f"/{scramble(secret, uri_file)}"
+                if uri_file == "master.html":
+                    self.master_uri = uri
+                self.context_vars[uri_file.replace(".", "_")] = uri
+            else:
+                uri = f"/{uri_file}"
+            self.static_files[uri] = StaticFile(hubapp_dir / file)
+            if uri_file == "index.html":
+                self.static_files["/"] = self.static_files[uri]
+
+    async def on_get(self, req, resp):
+        path = req.path
+        index_uri = f"/index.html"
+        if path == "/":
+            path = index_uri
+        res = self.static_files[path]
+        resp.data = res.render(self.context_vars)
+        resp.content_type = res.mime_type
+
+    def add_routes(self, app):
+        for uri_template in self.static_files:
+            app.add_route(uri_template, self)
diff --git a/hub/utils.py b/hub/utils.py
new file mode 100644 (file)
index 0000000..c9d955c
--- /dev/null
@@ -0,0 +1,18 @@
+from base64 import urlsafe_b64encode
+from hashlib import sha3_512
+
+
+def scramble(secret, value):
+    h = sha3_512()
+    h.update(f"{secret}{value}".encode())
+    return urlsafe_b64encode(h.digest()).rstrip(b"=").decode("ascii")
+
+
+def get_redis_pass(redis_conf):
+    prefix = "requirepass "
+    with open(redis_conf, "rt") as fh:
+        for line in fh:
+            if not line.startswith(prefix):
+                continue
+            return line[len(prefix) :].rstrip()
+    return None
diff --git a/hubapps/first/common.js b/hubapps/first/common.js
new file mode 100644 (file)
index 0000000..74449f9
--- /dev/null
@@ -0,0 +1,125 @@
+(function () {
+    window.common = {
+        "HubClient": function (ws_uri, open, close, message) {
+            var ws_protocols = {"http:": "ws:", "https:": "wss:"};
+            this.close = close.bind(this);
+            this.message = message.bind(this);
+            this.open = open.bind(this);
+            this.ws = new WebSocket(
+                ws_protocols[window.location.protocol] +
+                "//" +
+                window.location.host +
+                "/" +
+                ws_uri
+            );
+            this.ws.addEventListener("close", this.close);
+            this.ws.addEventListener("message", this.message);
+            this.ws.addEventListener("open", this.open);
+            this.send = this.ws.send.bind(this.ws);
+        },
+        "get_input": function (on_enter) {
+            var input = document.createElement("input");
+            input.addEventListener("keydown", function (event) {
+                var keycode;
+                if (!event) {
+                    event = window.event;
+                }
+                keycode = event.code || event.key;
+                if (keycode === "Enter" || keycode === "NumpadEnter") {
+                    on_enter();
+                }
+            });
+            return input;
+        },
+        "write": function (output, msg) {
+            if (output.childNodes.length > 0) {
+                output.append(document.createElement("br"));
+            }
+            if (typeof msg !== "string") {
+                msg = String(msg);
+            }
+            output.append(document.createTextNode(msg));
+        },
+        "setup": function (callback) {
+            document.addEventListener("readystatechange", function (event) {
+                if (!event) {
+                    event = window.event;
+                }
+                if (event.target.readyState !== "complete") {
+                    return;
+                }
+                callback();
+            });
+        },
+        "ClientsList": function () {
+            this.ul = document.createElement("ul");
+            document.body.append(this.ul);
+            this.get_label = function (client_id) {
+                return "client-" + client_id.toString();
+            };
+            this.get_name = function (client_id) {
+                var input = this.get_checkbox(client_id);
+                if (input !== null) {
+                    return input.parentNode.childNodes[1].textContent;
+                }
+                return this.get_label(client_id);
+            };
+            this.append = function (client_id) {
+                var li = document.createElement("li");
+                var input = document.createElement("input");
+                var client_label = this.get_label(client_id);
+                input.type = "checkbox";
+                input.id = "checkbox-" + client_label;
+                input.checked = true;
+                li.append(input);
+                li.append(document.createTextNode(client_label));
+                this.ul.append(li);
+            };
+            this.get_checkbox = function (client_id) {
+                return document.getElementById("checkbox-" + this.get_label(client_id));
+            };
+            this.remove = function (client_id) {
+                var input = this.get_checkbox(client_id);
+                if (input === null) {
+                    return;
+                }
+                input.parentNode.remove();
+            };
+            this.set_name = function(client_id, new_name) {
+                var input = this.get_checkbox(client_id);
+                var old_name;
+                if (input === null) {
+                    return;
+                }
+                old_name = input.parentNode.childNodes[1].textContent;
+                while (input.parentNode.childNodes.length > 1)
+                    input.parentNode.childNodes[1].remove();
+                input.parentNode.append(document.createTextNode(new_name));
+                input.parentNode.append(
+                    document.createTextNode(" [" + client_id.toString() + "]")
+                );
+                return old_name;
+            };
+            this.all = function () {
+                var result = [];
+                Array.prototype.forEach.call(this.ul.children, function (child) {
+                    var id_parts = child.getElementsByTagName("input")[0].id.split("-");
+                    result.push(Number(id_parts[id_parts.length - 1]));
+                });
+                return result;
+            };
+            this.selected = function () {
+                var result = [];
+                Array.prototype.forEach.call(this.ul.children, function (child) {
+                    var input = child.getElementsByTagName("input")[0];
+                    var id_parts;
+                    if (input.checked) {
+                        id_parts = input.id.split("-");
+                        result.push(Number(id_parts[id_parts.length - 1]));
+                    }
+                });
+                return result;
+            };
+        },
+    };
+}());
diff --git a/hubapps/first/index.html b/hubapps/first/index.html
new file mode 100644 (file)
index 0000000..7560554
--- /dev/null
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <meta charset="UTF-8">
+        <meta name="viewport" content="width=device-width, initial-scale=1.0">
+        <meta http-equiv="X-UA-Compatible" content="ie=edge">
+        <title>Index</title>
+        <!-- <link rel="stylesheet" href="style.css"> -->
+    </head>
+    <body>
+    <script src="common.js"></script>
+    <script src="hub.js"></script>
+    <script type="text/javascript">
+        var ws_uri = "/ws";
+    </script>
+    <script src="index.js"></script>
+    </body>
+</html>
diff --git a/hubapps/first/index.html.j2 b/hubapps/first/index.html.j2
deleted file mode 100644 (file)
index dfc36eb..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-  <head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <meta http-equiv="X-UA-Compatible" content="ie=edge">
-    <title>Index</title>
-    <!-- <link rel="stylesheet" href="style.css"> -->
-  </head>
-  <body>
-       <script src="index.js"></script>
-    hello, this is index.html, the "controller"
-  </body>
-</html>
index 20d7101f2cf8885b9e7ea7b8d2fb1cd3b27c0d3b..f6fa9c7367a62c371053a2f9d74a03ddc03f3cfd 100644 (file)
@@ -1,13 +1,80 @@
-"use strict";
-
 (function () {
-    document.addEventListener("readystatechange", function (event) {
-        if (!event) {
-            event = window.event;
-        }
-        if (event.target.readyState !== "complete") {
-            return;
+    "use strict";
+    var input_div = null;
+
+    function write(msg) {
+        common.write(input_div.previousSibling, msg);
+    }
+
+    function change_name_button(hub) {
+        var btn = document.createElement("button");
+        btn.append(document.createTextNode("change name"));
+        btn.addEventListener("click", function () {
+            setup_change_name(hub);
+        });
+        return btn;
+    }
+
+    function setup_chat(hub) {
+        input_div = document.createElement("div");
+        input_div.append(common.get_input(function () {
+            var input = input_div.getElementsByTagName("input")[0];
+            hub.send(input.value);
+            input.value = "";
+        }));
+        input_div.append(document.createTextNode(" "));
+        input_div.append(change_name_button(hub));
+
+        document.body.append(document.createElement("div"));
+        document.body.append(input_div);
+    }
+
+    function setup_change_name(hub) {
+        var div = document.createElement("div");
+        div.append(document.createTextNode("Enter name: "));
+        div.append(common.get_input(function () {
+            var client_name = div.children[0].value;
+            if (input_div.childNodes[0] !== input_div.children[0]) {
+                input_div.childNodes[0].remove();
+            }
+            input_div.insertBefore(
+                document.createTextNode(client_name + " "), input_div.children[0]
+            );
+            hub.send(JSON.stringify({"action": "set_name", "name": client_name}));
+            div.remove();
+            input_div.style.display = "";
+            input_div.children[0].focus();
+        }));
+        document.body.append(div);
+        input_div.style.display = "none";
+        div.children[0].focus();
+    }
+
+    function open() {
+        setup_chat(this);
+        write("connected to " + ws_uri);
+        setup_change_name(this);
+    }
+
+    function close() {
+        write("connection lost");
+    }
+
+    function message(msg) {
+        var obj = JSON.parse(msg.data);
+        if (obj.action === "join" || obj.action === "leave") {
+            write("[action]: " + obj.name + " " + obj.action + "s");
+        } else if (obj.action == "set_name") {
+            write(
+                "[action]: " +
+                obj.old_name +
+                " changes name to " +
+                obj.name
+            );
+        } else if (obj.hasOwnProperty("data")) {
+            write(obj.name + ": " + obj.data);
         }
-        document.body.append(document.createTextNode("(this is index.js) "));
-    });
+    }
+
+    common.setup(function () {new common.HubClient(ws_uri, open, close, message)});
 })();
similarity index 70%
rename from hubapps/first/master.html.j2
rename to hubapps/first/master.html.tpl
index 12dfc85d417e476360480c75f34e81d147680d61..7a00c9361b48fc39c523d2b7c49e726c77de6c6f 100644 (file)
@@ -8,7 +8,11 @@
     <!-- <link rel="stylesheet" href="style.css"> -->
   </head>
   <body>
+       <script src="common.js"></script>
+    <script src="hub.js"></script>
+       <script type="text/javascript">
+         var ws_uri = {{ master_ws_uri }};
+       </script>
        <script src="{{ master_js }}"></script>
-    hello, this is master.html, the "playing field"
   </body>
 </html>
index e33e9fecf19a0923feb3d253141c50a74edf3d62..d6074e8588d6e0ca6b55b1bfcf6785eca08814af 100644 (file)
@@ -1,15 +1,86 @@
-"use strict";
-
 (function () {
-    document.addEventListener("readystatechange", function (event) {
-        if (!event) {
-            event = window.event;
-        }
-        if (event.target.readyState !== "complete") {
-            return;
+    "use strict";
+    var input_div = null;
+    var clients_list = null;
+
+    function write(msg) {
+        common.write(input_div.previousSibling, msg);
+    }
+    function master_send(hub, client_ids, client_id, obj) {
+        obj.client_ids = client_ids;
+        obj.client_id = client_id;
+        hub.send(JSON.stringify(obj));
+    }
+
+    function setup_chat(hub) {
+        clients_list = new common.ClientsList();
+        input_div = document.createElement("div");
+        input_div.append(document.createTextNode("MASTER "));
+        input_div.append(common.get_input(function () {
+            var input = input_div.getElementsByTagName("input")[0];
+            master_send(hub, clients_list.selected(), null, {
+                "name": "MASTER",
+                "data": input.value,
+            });
+            write("MASTER [null]: " + input.value);
+            input.value = "";
+        }));
+
+        document.body.append(document.createElement("div")); // output
+        document.body.append(input_div);
+    }
+
+    function open() {
+        setup_chat(this);
+        write("connected to " + ws_uri);
+    }
+
+    function close() {
+        write("connection lost");
+    }
+
+    function join_leave_broadcast(hub, client_id, client_name, action) {
+        write("[action]: " + client_name + " " + obj.action + "s");
+        master_send(hub, clients_list.all(), obj.client_id, {
+            "action": obj.action,
+            "name": client_name,
+        });
+    }
+
+    function message(msg) {
+        var obj = JSON.parse(msg.data);
+        var client_name;
+        if (obj.action === "join") {
+            clients_list.append(obj.client_id);
+            join_leave_broadcast(
+                this, obj.client_id, clients_list.get_name(obj.client_id), obj.action
+            );
+        } else if (obj.action === "leave") {
+            client_name = clients_list.get_name(obj.client_id);
+            clients_list.remove(obj.client_id);
+            join_leave_broadcast(this, obj.client_id, client_name, obj.action);
+        } else if (obj.action == "set_name") {
+            client_name = clients_list.set_name(obj.client_id, obj.name);
+            write(
+                "[action]: " +
+                client_name +
+                " changes name to " +
+                obj.name
+            );
+            master_send(this, clients_list.all(), obj.client_id, {
+                "action": "set_name",
+                "old_name": client_name,
+                "name": obj.name,
+            });
+        } else if (obj.hasOwnProperty("data")) {
+            client_name = clients_list.get_name(obj.client_id);
+            write(client_name + " [" + obj.client_id + "]: " + obj.data);
+            master_send(this, clients_list.all(), obj.client_id, {
+                "name": client_name,
+                "data": obj.data,
+            });
         }
-        document.body.append(document.createTextNode("(this is master.js) "));
-        // hide real url
-        history.pushState({}, "", new URL(window.location.origin + "/"));
-    });
+    }
+
+    common.setup(function () {new common.HubClient(ws_uri, open, close, message)});
 })();
diff --git a/requirements.txt b/requirements.txt
new file mode 100644 (file)
index 0000000..01e36a7
--- /dev/null
@@ -0,0 +1,23 @@
+anyio==3.7.1
+black==23.7.0
+click==8.1.4
+falcon==3.1.1
+h11==0.14.0
+httptools==0.6.0
+idna==3.4
+isort==5.12.0
+Jinja2==3.1.2
+MarkupSafe==2.1.3
+mypy-extensions==1.0.0
+packaging==23.1
+pathspec==0.11.1
+pipdeptree==2.11.0
+platformdirs==3.8.1
+python-dotenv==1.0.0
+PyYAML==6.0.1
+redis==4.6.0
+sniffio==1.3.0
+uvicorn==0.23.1
+uvloop==0.17.0
+watchfiles==0.19.0
+websockets==11.0.3
index 818817d9bc8e2ec08edcffb3a483ad0560fcf45d..6ce118875e57b0d98168a99a1eb71e602cdc3c96 100755 (executable)
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 
-import uvicorn
+from hub.app import main
 
 if __name__ == "__main__":
-    uvicorn.run("hub.app:get_app", factory=True, port=5000, log_level="info")
+    main()
diff --git a/setup_venv.sh b/setup_venv.sh
new file mode 100644 (file)
index 0000000..91fc1e7
--- /dev/null
@@ -0,0 +1,10 @@
+venv_dir="${VIRTUAL_ENV:-venv}"
+if type deactivate &>/dev/null; then
+    deactivate
+fi
+if [[ ! -d "${venv_dir}" ]]; then
+    "${PYTHON:-python}" -m venv "${venv_dir}"
+fi
+source "${venv_dir}/bin/activate"
+pip install -qU pip
+pip install -qU 'uvicorn[standard]' falcon black pip redis isort Jinja2