diff --git a/fasthtml/_modidx.py b/fasthtml/_modidx.py index 187bf9fa..d49e8a28 100644 --- a/fasthtml/_modidx.py +++ b/fasthtml/_modidx.py @@ -176,7 +176,6 @@ 'fasthtml.jupyter.JupyUvi._live_sse': ('api/jupyter.html#jupyuvi._live_sse', 'fasthtml/jupyter.py'), 'fasthtml.jupyter.JupyUvi._setup_live': ('api/jupyter.html#jupyuvi._setup_live', 'fasthtml/jupyter.py'), 'fasthtml.jupyter.JupyUvi.start': ('api/jupyter.html#jupyuvi.start', 'fasthtml/jupyter.py'), - 'fasthtml.jupyter.JupyUvi.start_async': ('api/jupyter.html#jupyuvi.start_async', 'fasthtml/jupyter.py'), 'fasthtml.jupyter.JupyUvi.stop': ('api/jupyter.html#jupyuvi.stop', 'fasthtml/jupyter.py'), 'fasthtml.jupyter.JupyUviAsync': ('api/jupyter.html#jupyuviasync', 'fasthtml/jupyter.py'), 'fasthtml.jupyter.JupyUviAsync.__init__': ( 'api/jupyter.html#jupyuviasync.__init__', @@ -190,6 +189,7 @@ 'fasthtml.jupyter.render_ft': ('api/jupyter.html#render_ft', 'fasthtml/jupyter.py'), 'fasthtml.jupyter.show': ('api/jupyter.html#show', 'fasthtml/jupyter.py'), 'fasthtml.jupyter.wait_port_free': ('api/jupyter.html#wait_port_free', 'fasthtml/jupyter.py'), + 'fasthtml.jupyter.wait_port_free_async': ('api/jupyter.html#wait_port_free_async', 'fasthtml/jupyter.py'), 'fasthtml.jupyter.ws_client': ('api/jupyter.html#ws_client', 'fasthtml/jupyter.py')}, 'fasthtml.live_reload': {}, 'fasthtml.oauth': { 'fasthtml.oauth.AppleAppClient': ('api/oauth.html#appleappclient', 'fasthtml/oauth.py'), diff --git a/fasthtml/jupyter.py b/fasthtml/jupyter.py index 409f20ea..74b39fb3 100644 --- a/fasthtml/jupyter.py +++ b/fasthtml/jupyter.py @@ -3,8 +3,8 @@ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/06_jupyter.ipynb. # %% auto #0 -__all__ = ['nb_serve', 'nb_serve_async', 'is_port_free', 'wait_port_free', 'show', 'render_ft', 'htmx_config_port', 'JupyUvi', - 'JupyUviAsync', 'HTMX', 'ws_client'] +__all__ = ['nb_serve', 'nb_serve_async', 'is_port_free', 'wait_port_free', 'wait_port_free_async', 'show', 'render_ft', + 'htmx_config_port', 'JupyUvi', 'JupyUviAsync', 'HTMX', 'ws_client'] # %% ../nbs/api/06_jupyter.ipynb #2c69d9d0 import asyncio, socket, time, uvicorn @@ -36,23 +36,31 @@ async def nb_serve_async(app, log_level="error", port=8000, host='0.0.0.0', **kw # %% ../nbs/api/06_jupyter.ipynb #508917bc def is_port_free(port, host='localhost'): - "Check if `port` is free on `host`" sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind((host, port)) + sock.listen(1) return True except OSError: return False finally: sock.close() # %% ../nbs/api/06_jupyter.ipynb #1779cb76 -def wait_port_free(port, host='localhost', max_wait=3): +def wait_port_free(port, host='localhost', max_wait=20): "Wait for `port` to be free on `host`" start_time = time.time() - while not is_port_free(port): - if time.time() - start_time>max_wait: return print(f"Timeout") + while not is_port_free(port, host): + if time.time() - start_time > max_wait: raise TimeoutError(f"Port {host}:{port} not free after {max_wait}s") time.sleep(0.1) +async def wait_port_free_async(port, host='localhost', max_wait=20): + "Async wait for `port` to be free on `host`" + start_time = time.time() + while not is_port_free(port, host): + if time.time() - start_time > max_wait: raise TimeoutError(f"Port {host}:{port} not free after {max_wait}s") + await asyncio.sleep(0.1) + + # %% ../nbs/api/06_jupyter.ipynb #654b36bb @delegates(_show) def show(*s, **kwargs): @@ -94,12 +102,9 @@ def __init__(self, app, log_level="error", host='0.0.0.0', port=8000, start=True def start(self): self.server = nb_serve(self.app, log_level=self.log_level, host=self.host, port=self.port,daemon=self.daemon, **self.kwargs) - async def start_async(self): - self.server = await nb_serve_async(self.app, log_level=self.log_level, host=self.host, port=self.port, **self.kwargs) - def stop(self): self.server.should_exit = True - wait_port_free(self.port) + wait_port_free(self.port, self.host) def _setup_live(self, app): rt = self.live_rt or '/_lr' @@ -117,7 +122,7 @@ async def _live_sse(self): ver = self._live_ver yield 'data: reload\n\n' -# %% ../nbs/api/06_jupyter.ipynb #9134035e +# %% ../nbs/api/06_jupyter.ipynb #f6316c73 class JupyUviAsync(JupyUvi): "Start and stop an async Jupyter compatible uvicorn server with ASGI `app` on `port` with `log_level`" def __init__(self, app, log_level="error", host='0.0.0.0', port=8000, **kwargs): @@ -126,9 +131,10 @@ def __init__(self, app, log_level="error", host='0.0.0.0', port=8000, **kwargs): async def start(self): self.server = await nb_serve_async(self.app, log_level=self.log_level, host=self.host, port=self.port, **self.kwargs) - def stop(self): + async def stop(self): self.server.should_exit = True - wait_port_free(self.port) + await wait_port_free_async(self.port, self.host) + # %% ../nbs/api/06_jupyter.ipynb #a448e420 from starlette.testclient import TestClient diff --git a/nbs/api/06_jupyter.ipynb b/nbs/api/06_jupyter.ipynb index c24696e1..1d1b24e7 100644 --- a/nbs/api/06_jupyter.ipynb +++ b/nbs/api/06_jupyter.ipynb @@ -100,11 +100,11 @@ "source": [ "#| export\n", "def is_port_free(port, host='localhost'):\n", - " \"Check if `port` is free on `host`\"\n", " sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n", + " sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n", " try:\n", - " sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n", " sock.bind((host, port))\n", + " sock.listen(1)\n", " return True\n", " except OSError: return False\n", " finally: sock.close()" @@ -118,12 +118,19 @@ "outputs": [], "source": [ "#| export\n", - "def wait_port_free(port, host='localhost', max_wait=3):\n", + "def wait_port_free(port, host='localhost', max_wait=20):\n", " \"Wait for `port` to be free on `host`\"\n", " start_time = time.time()\n", - " while not is_port_free(port):\n", - " if time.time() - start_time>max_wait: return print(f\"Timeout\")\n", - " time.sleep(0.1)" + " while not is_port_free(port, host):\n", + " if time.time() - start_time > max_wait: raise TimeoutError(f\"Port {host}:{port} not free after {max_wait}s\")\n", + " time.sleep(0.1)\n", + "\n", + "async def wait_port_free_async(port, host='localhost', max_wait=20):\n", + " \"Async wait for `port` to be free on `host`\"\n", + " start_time = time.time()\n", + " while not is_port_free(port, host):\n", + " if time.time() - start_time > max_wait: raise TimeoutError(f\"Port {host}:{port} not free after {max_wait}s\")\n", + " await asyncio.sleep(0.1)\n" ] }, { @@ -206,12 +213,9 @@ " def start(self):\n", " self.server = nb_serve(self.app, log_level=self.log_level, host=self.host, port=self.port,daemon=self.daemon, **self.kwargs)\n", "\n", - " async def start_async(self):\n", - " self.server = await nb_serve_async(self.app, log_level=self.log_level, host=self.host, port=self.port, **self.kwargs)\n", - "\n", " def stop(self):\n", " self.server.should_exit = True\n", - " wait_port_free(self.port)\n", + " wait_port_free(self.port, self.host)\n", "\n", " def _setup_live(self, app):\n", " rt = self.live_rt or '/_lr'\n", @@ -272,29 +276,9 @@ { "cell_type": "code", "execution_count": null, - "id": "0f4b31e9", + "id": "04cb3e4c", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "app = FastHTML()\n", "rt = app.route\n", @@ -342,80 +326,42 @@ "metadata": {}, "outputs": [], "source": [ - "server.stop()\n", - "await asyncio.sleep(0.2)" + "server.stop()" ] }, { - "cell_type": "code", - "execution_count": null, - "id": "97bfb966", + "cell_type": "markdown", + "id": "d7ca5fe0", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], "source": [ - "app = FastHTML()\n", - "rt = app.route\n", - "\n", - "@app.route\n", - "async def index(): return 'hi'\n", - "\n", - "server = JupyUvi(app, port=port, start=False)\n", - "await server.start_async()" + "A good way to avoid spinning up two servers when running all the notebooks cells is to check whether the `server` variable is already defined, and stop if it is the case." ] }, { "cell_type": "code", "execution_count": null, - "id": "8945d436", + "id": "470d81d0", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "hi\n" - ] - } - ], + "outputs": [], "source": [ - "print((await AsyncClient().get(f'http://localhost:{port}')).text)" + "if 'server' in globals(): server.stop()\n", + "server.start()" ] }, { "cell_type": "code", "execution_count": null, - "id": "c881cfc5", + "id": "7bc1aba4", "metadata": {}, "outputs": [], "source": [ - "server.stop()\n", - "await asyncio.sleep(0.2)" + "server.stop()" ] }, { "cell_type": "code", "execution_count": null, - "id": "9134035e", + "id": "f6316c73", "metadata": {}, "outputs": [], "source": [ @@ -428,9 +374,9 @@ " async def start(self):\n", " self.server = await nb_serve_async(self.app, log_level=self.log_level, host=self.host, port=self.port, **self.kwargs)\n", "\n", - " def stop(self):\n", + " async def stop(self):\n", " self.server.should_exit = True\n", - " wait_port_free(self.port)" + " await wait_port_free_async(self.port, self.host)\n" ] }, { @@ -438,27 +384,7 @@ "execution_count": null, "id": "959eb254", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "server = JupyUviAsync(app, port=port)\n", "await server.start()" @@ -491,8 +417,7 @@ "metadata": {}, "outputs": [], "source": [ - "server.stop()\n", - "await asyncio.sleep(0.2)" + "await server.stop()" ] }, { @@ -540,10 +465,12 @@ "text/html": [ "\n", "\n", - "" + "" ], "text/plain": [ - "" + "HTML(\n", + "\n", + ")" ] }, "metadata": {}, @@ -568,27 +495,7 @@ "execution_count": null, "id": "78d40711", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "server = JupyUvi(app, port=port)" ] @@ -601,13 +508,23 @@ "outputs": [ { "data": { + "text/html": [ + "
\n", + "
\n", + "
\n" + ], "text/markdown": [ - "
\n", - "
\n", - "
\n" + "
\n", + "\n", + "```html\n", + "
\n", + "\n", + "```\n", + "\n", + "
" ], "text/plain": [ - "div(('',),{'id': '_BxxYJqh2RdOaxF3FIK0VBA'})" + "
" ] }, "execution_count": null, @@ -646,13 +563,23 @@ "outputs": [ { "data": { + "text/html": [ + "
\n", + "

not loaded

\n", + "
\n" + ], "text/markdown": [ - "
\n", - "

not loaded

\n", - "
\n" + "
\n", + "\n", + "```html\n", + "

not loaded

\n", + "\n", + "```\n", + "\n", + "
" ], "text/plain": [ - "p(('not loaded',),{'hx-get': ._lf object>, 'hx-trigger': 'load', 'id': '_x4TFzZPvTtWff74RmcHGVQ'})" + "

not loaded

" ] }, "execution_count": null, @@ -680,13 +607,23 @@ "outputs": [ { "data": { + "text/html": [ + "
\n", + "
\n", + "
\n" + ], "text/markdown": [ - "
\n", - "
\n", - "
\n" + "
\n", + "\n", + "```html\n", + "
\n", + "\n", + "```\n", + "\n", + "
" ], "text/plain": [ - "div(('',),{'id': '_AsIIqL_PTs_nPASliaNYGA'})" + "
" ] }, "execution_count": null, @@ -706,13 +643,23 @@ "outputs": [ { "data": { + "text/html": [ + "
\n", + "

hi

\n", + "
\n" + ], "text/markdown": [ - "
\n", - "

hi

\n", - "
\n" + "
\n", + "\n", + "```html\n", + "

hi

\n", + "\n", + "```\n", + "\n", + "
" ], "text/plain": [ - "p(('hi',),{'hx-get': ._lf object>, 'hx-trigger': 'load', 'hx-target': '#_AsIIqL_PTs_nPASliaNYGA', 'id': '_LjH2GVPJS_6wMMIQSQpyJA'})" + "

hi

" ] }, "execution_count": null, @@ -733,8 +680,7 @@ "metadata": {}, "outputs": [], "source": [ - "server.stop()\n", - "await asyncio.sleep(0.2)" + "server.stop()" ] }, { @@ -832,30 +778,11 @@ "execution_count": null, "id": "81669294", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - " " - ], - "text/plain": [ - "" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Run the notebook locally to see the HTMX iframe in action\n", - "HTMX()" + "# Do not commit the outputs because it will break SolveIT\n", + "# HTMX()" ] }, { @@ -908,27 +835,14 @@ "#| hide\n", "import nbdev; nbdev.nbdev_export()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d6eafdc3", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { - "kernelspec": { - "display_name": "python3", - "language": "python", - "name": "python3" - }, "solveit": { "default_code": true, "mode": "learning", "use_fence": false, - "use_thinking": false, + "use_thinking": true, "use_tools": true, "ver": 2 }