Skip to content

Commit df52fd0

Browse files
authored
Merge pull request #47 from apiad/develop
Publish command
2 parents 473803f + 2a7aed9 commit df52fd0

File tree

9 files changed

+273
-46
lines changed

9 files changed

+273
-46
lines changed

auditorium/__main__.py

+25-8
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,35 @@ class Auditorium:
1111
@staticmethod
1212
def run(
1313
path,
14-
host="127.0.0.1",
15-
port=6789,
16-
debug=False,
17-
instance_name="show",
18-
launch=True,
14+
*,
15+
host: str = "127.0.0.1",
16+
port: int = 6789,
17+
debug: bool = False,
18+
instance_name: str = "show",
1919
):
2020
"Runs a custom Python script as a slideshow."
2121

2222
show = Show.load(path, instance_name)
23-
show.run(host=host, port=port, debug=debug, launch=launch)
23+
show.run(host=host, port=port, debug=debug)
2424

2525
@staticmethod
26-
def demo(host="127.0.0.1", port=6789, debug=False, launch=True):
26+
def publish(
27+
path: str,
28+
name: str,
29+
*,
30+
server: str = "wss://auditorium.apiad.net",
31+
instance_name: str = "show"
32+
):
33+
show = Show.load(path, instance_name)
34+
show.publish(server=server, name=name)
35+
36+
@staticmethod
37+
def demo(host: str = "127.0.0.1", port: int = 6789, debug: bool = False):
2738
"Starts the demo slideshow."
2839

2940
from auditorium.demo import show
3041

31-
show.run(host, port, debug=debug, launch=launch)
42+
show.run(host=host, port=port, debug=debug)
3243

3344
@staticmethod
3445
def render(path, theme="white", instance_name="show"):
@@ -37,6 +48,12 @@ def render(path, theme="white", instance_name="show"):
3748
show = Show.load(path, instance_name)
3849
print(show.render(theme))
3950

51+
@staticmethod
52+
def server(host: str = "0.0.0.0", port: int = 9876):
53+
from auditorium.server import run_server
54+
55+
run_server(host=host, port=port)
56+
4057
@staticmethod
4158
def test():
4259
return "It's OK!"

auditorium/server.py

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# coding: utf8
2+
3+
import warnings
4+
import websockets
5+
6+
from typing import Dict, Tuple
7+
from fastapi import FastAPI, HTTPException
8+
from starlette.responses import HTMLResponse
9+
from starlette.websockets import WebSocket
10+
import asyncio
11+
from jinja2 import Template
12+
13+
from .utils import path
14+
from .show import UpdateData
15+
16+
server = FastAPI()
17+
18+
SERVERS: Dict[str, Tuple[asyncio.Queue, asyncio.Queue]] = {}
19+
20+
with open(path('templates/server.html')) as fp:
21+
TEMPLATE = Template(fp.read())
22+
23+
24+
@server.get("/")
25+
async def index():
26+
return HTMLResponse(TEMPLATE.render(servers_list=list(SERVERS)))
27+
28+
29+
@server.get("/{name}/")
30+
async def render(name: str):
31+
try:
32+
queue_in, queue_out = SERVERS[name]
33+
except KeyError:
34+
raise HTTPException(404)
35+
36+
await queue_in.put(dict(type="render"))
37+
38+
response = await queue_out.get()
39+
queue_out.task_done()
40+
41+
return HTMLResponse(response["content"])
42+
43+
44+
@server.post("/{name}/update")
45+
async def update(name: str, data: UpdateData):
46+
try:
47+
queue_in, queue_out = SERVERS[name]
48+
except KeyError:
49+
raise HTTPException(404)
50+
51+
await queue_in.put(data.dict())
52+
53+
response = await queue_out.get()
54+
queue_out.task_done()
55+
56+
return response
57+
58+
59+
@server.websocket("/ws")
60+
async def ws(websocket: WebSocket):
61+
await websocket.accept()
62+
name = await websocket.receive_text()
63+
64+
if name in SERVERS:
65+
await websocket.send_json(dict(type="error", msg="Name is already taken."))
66+
await websocket.close()
67+
return
68+
69+
print("Registering new server: ", name)
70+
71+
queue_in: asyncio.Queue = asyncio.Queue()
72+
queue_out: asyncio.Queue = asyncio.Queue()
73+
74+
SERVERS[name] = (queue_in, queue_out)
75+
76+
try:
77+
while True:
78+
command = await queue_in.get()
79+
await websocket.send_json(command)
80+
response = await websocket.receive_json()
81+
82+
queue_in.task_done()
83+
await queue_out.put(response)
84+
except:
85+
print("(!) Connection to %s closed by client." % name)
86+
87+
for _ in range(queue_in.qsize()):
88+
queue_in.task_done()
89+
90+
for _ in range(queue_out.qsize()):
91+
queue_out.task_done()
92+
93+
print("Unregistering server:", name)
94+
SERVERS.pop(name)
95+
96+
97+
def run_server(*, host="0.0.0.0", port=9876):
98+
try:
99+
import uvicorn
100+
101+
uvicorn.run(server, host=host, port=port)
102+
except ImportError:
103+
warnings.warn("(!) You need `uvicorn` installed in order to call `server`.")
104+
exit(1)

auditorium/show.py

+52-14
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,27 @@
44
This module includes the `Show` class and the main functionalities of `auditorium`.
55
"""
66

7+
import asyncio
78
import base64
89
import io
10+
import json
911
import runpy
1012
import warnings
1113
import webbrowser
1214
from collections import OrderedDict
15+
from typing import Union
1316

17+
import websockets
18+
from fastapi import FastAPI
1419
from jinja2 import Template
1520
from markdown import markdown
21+
from pydantic import BaseModel
1622
from pygments import highlight
1723
from pygments.formatters.html import HtmlFormatter
1824
from pygments.lexers import get_lexer_by_name
1925
from pygments.styles import get_style_by_name
20-
21-
from fastapi import FastAPI
22-
from starlette.staticfiles import StaticFiles
2326
from starlette.responses import HTMLResponse
24-
from pydantic import BaseModel
25-
from typing import Union
27+
from starlette.staticfiles import StaticFiles
2628

2729
from .components import Animation, Block, Column, Fragment, ShowMode
2830
from .utils import fix_indent, path
@@ -64,22 +66,58 @@ def __init__(self, title="", theme="white", code_style="monokai"):
6466

6567
## Show functions
6668

67-
def run(self, host: str, port: int, launch: bool, *args, **kwargs) -> None:
69+
def run(self, *, host: str, port: int, debug: bool = False) -> None:
6870
self._content = self._render_content()
6971

70-
# if launch:
71-
# def launch_server():
72-
# webbrowser.open_new_tab(f"http://{host}:{port}")
73-
74-
# self.app.add_task(launch_server)
75-
7672
try:
7773
import uvicorn
7874

79-
uvicorn.run(self.app, host=host, port=port, *args, **kwargs)
75+
uvicorn.run(self.app, host=host, port=port, debug=debug)
8076
except ImportError:
81-
warnings.warn("In order to call `run` you need `uvicorn` installed.")
77+
warnings.warn("(!) You need `uvicorn` installed in order to call `run`.")
78+
exit(1)
79+
80+
def publish(self, server: str, name: str):
81+
url = "{}/ws".format(server)
82+
asyncio.get_event_loop().run_until_complete(self._ws(url, name))
83+
84+
async def _ws(self, url: str, name: str):
85+
try:
86+
async with websockets.connect(url) as websocket:
87+
print("Connected to server")
88+
await websocket.send(name)
89+
print("Starting command loop.")
90+
91+
while True:
92+
command = await websocket.recv()
93+
command = json.loads(command)
94+
95+
response = self._do_ws_command(command)
96+
response = json.dumps(response)
97+
await websocket.send(response)
98+
except ConnectionRefusedError:
99+
print("(!) Could not connect to %s. Make sure server is up." % url)
82100
exit(1)
101+
except websockets.exceptions.ConnectionClosedError:
102+
print("(!) Connection to %s closed by server." % url)
103+
exit(1)
104+
105+
106+
def _do_ws_command(self, command):
107+
if command["type"] == "render":
108+
print("Rendering content")
109+
return dict(content=self.render())
110+
elif command["type"] == "error":
111+
print("(!) %s" % command['msg'])
112+
raise websockets.exceptions.ConnectionClosedError(1006, command['msg'])
113+
else:
114+
print("Executing slide %s" % command["slide"])
115+
values = {}
116+
values[command["id"]] = command["value"]
117+
update = self.do_code(command["slide"], values)
118+
return update
119+
120+
raise ValueError("Unknown command: %s", command["type"])
83121

84122
@property
85123
def show_title(self) -> str:

auditorium/static/js/auditorium.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ function setupAnimations(parent) {
3131
value: step
3232
});
3333

34-
fetch("/update", {
34+
fetch("update", {
3535
method: "POST",
3636
headers: {
3737
"Accept": "application/json",

auditorium/templates/server.html

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<meta http-equiv="X-UA-Compatible" content="ie=edge">
7+
<title>Auditorium</title>
8+
</head>
9+
<body>
10+
<h1>Welcome to Auditorium</h1>
11+
12+
<h3>The following slideshows have been registered:</h3>
13+
<ul>
14+
{% for name in servers_list %}
15+
<li>
16+
<a href="/{{ name }}/">{{ name }}</a>
17+
</li>
18+
{% endfor %}
19+
</ul>
20+
</body>
21+
</html>

docs/consider.md

+3-9
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,16 @@ That being said, there are some known deficiencies that I might fix, and some ot
2222
## Slides need to be fast
2323

2424
A slide's code is executed completely every time that slide needs to be rendered.
25-
That is, once during loading and then when inputs change or animations tick.
25+
That is, once during the initial rendering and then when inputs change or animations tick.
2626
Hence, you slide logic should be fairly fast.
2727
This is particularly true for animations, so don't expect to be able to train a neural network in real time.
28-
The slide logic is meant to be simple, the kind of one-liners you can run every keystroke, like less than 1 second fast.
28+
The slide logic is meant to be simple, the kind of one-liners you can run every keystroke, in the order of a couple hundred miliseconds at most.
2929
If you need to interactively draw the loss value of a neural network, either is gonna take a while or you will have to fake it, i.e., compute it offline and then simply animate it.
3030

31-
## All slides are executed on load
32-
33-
For now, on the first load all slides are going to be run, which might increase significantly your loading time if you have complex logic in each slide.
34-
At some point, if I run into the problem, I may add a "lazy" loading option so that only the first few slides are executed.
35-
If this is an issue for a lot of people it might become a priority.
36-
3731
## Slides have to be stateless
3832

3933
The code that runs inside a slide should not depend on anything outside of `ctx`, since you have no guarantee when will it be executed.
40-
Right now, slide's code is executed once before any rendering in order to discover vertical slides, then again during the
34+
Right now, slide's code is executed once during the
4135
initial rendering to layout and then everytime an interaction or animation forces the slide to render again.
4236
However, this might be changed at any time, so make no assumptions as to when is that code executed.
4337
The easiest way to do this, is making sure that every slide function is a pure function and all state is handled through

docs/history.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# History
22

3+
### v19.1.4
4+
5+
* Added command `auditorium server` that allows to proxy a slideshow through a public server.
6+
* Added command `auditorium publish` for authors to publish their slides.
7+
* Opened service at [auditorium.apiad.net](http://auditorium.apiad.net) (sorry, no HTTPS yet).
8+
39
### v19.1.3
410

511
* Changed vertical slides API, now slides don't need to be run the first time the slideshow is loaded, since the entire slideshow is flat.

0 commit comments

Comments
 (0)