File: serve.py

package info (click to toggle)
python-trame 3.12.0-1
  • links: PTS, VCS
  • area: main
  • in suites:
  • size: 101,620 kB
  • sloc: python: 13,515; sh: 183; javascript: 93; makefile: 7
file content (154 lines) | stat: -rw-r--r-- 4,590 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
import argparse
import asyncio
import importlib
import uuid

import aiohttp
import aiohttp.web as aiohttp_web


class MultiClientServer:
    def __init__(self, trame_app, max_msg_size=10000000, heartbeat=30):
        self._completion = None
        self.trame_app = trame_app
        self.ws_options = dict(max_msg_size=max_msg_size, heartbeat=heartbeat)
        self.web_server = aiohttp_web.Application()
        self.web_server.router.add_route("GET", "/", self._index_handler)

        routes = [aiohttp_web.get("/ws", self._ws_handler)]
        app_www = self.trame_app("__www__").server
        for route in sorted(app_www.serve.keys(), reverse=True):
            routes.append(
                aiohttp_web.static(
                    f"/{route}",
                    app_www.serve[route],
                    append_version=True,
                )
            )
        routes.append(aiohttp_web.static("/", app_www._www, append_version=True))
        self.web_server.add_routes(routes)

    async def _index_handler(self, request):
        if request.query_string:
            return aiohttp.web.HTTPFound(f"index.html?{request.query_string}")
        return aiohttp.web.HTTPFound("index.html")

    async def _ws_handler(self, request):
        name = str(uuid.uuid4()).replace("-", "")

        # App setup
        print(f" + {name}")
        app = self.trame_app(name)
        task = app.server.start(backend="generic", exec_mode="task")
        await app.server.ready

        ws_network = aiohttp_web.WebSocketResponse(**self.ws_options)

        async def on_msg_from_server(binary, content):
            if binary:
                await ws_network.send_bytes(content)
            else:
                await ws_network.send_str(content)

        try:
            await ws_network.prepare(request)
            ws_app = app.server._server.ws
            connection = await ws_app.connect()
            connection.on_message(on_msg_from_server)
            async for msg in ws_network:
                await connection.send(
                    msg.type == aiohttp.WSMsgType.BINARY,
                    msg,
                )
        finally:
            await connection.close()
            await app.server.stop()
            await task

        print(f" - {name}")
        return ws_network

    async def run(self, host="localhost", port=8080):
        self._completion = asyncio.get_event_loop().create_future()
        runner = aiohttp_web.AppRunner(
            self.web_server,
            handle_signals=True,
        )
        await runner.setup()

        site = aiohttp_web.TCPSite(runner, host, port)

        await site.start()
        await self._completion

    def stop(self):
        if self._completion is not None:
            self._completion.set_result(True)


def main():
    parser = argparse.ArgumentParser(description="Serve trame application")

    parser.add_argument(
        "--exec",
        default="trame.app.demo:Cone",
        help="Trame app to serve (default: trame.app.demo:Cone)",
    )
    parser.add_argument(
        "--host",
        default="localhost",
        help="IP or hostname to serve on (default: localhost)",
    )
    parser.add_argument(
        "--port",
        default=8080,
        type=int,
        help="Port to serve on (default: 8080)",
    )

    parser.add_argument(
        "--ws-heart-beat",
        default=30,
        type=int,
        help="WebSocket heart beat (default: 30)",
    )

    parser.add_argument(
        "--ws-max-size",
        default=10000000,
        type=int,
        help="WebSocket maximum message size (default: 10000000)",
    )

    args, _ = parser.parse_known_args()
    if args.exec is None or ":" not in args.exec:
        parser.print_help()
        return

    module_path, app_name = args.exec.split(":")
    try:
        module = importlib.import_module(module_path)
    except ModuleNotFoundError:
        print(f"Invalid trame app to serve: {args.app}")
        parser.print_help()
        return

    print("Server configuration")
    print(f"  - host: {args.host}")
    print(f"  - port: {args.port}")
    print(f"  - app: {app_name} from {module_path}")
    print("Websocket configuration")
    print(f"  - heartbeat: {args.ws_heart_beat}")
    print(f"  - Max message size: {args.ws_max_size}")

    trame_app = getattr(module, app_name)
    web_server = MultiClientServer(
        trame_app,
        heartbeat=args.ws_heart_beat,
        max_msg_size=args.ws_max_size,
    )
    asyncio.run(web_server.run(args.host, args.port))


if __name__ == "__main__":
    main()