diff --git a/.github/actions/setup-env/action.yml b/.github/actions/setup-env/action.yml index 795d52bd..ba93cc41 100644 --- a/.github/actions/setup-env/action.yml +++ b/.github/actions/setup-env/action.yml @@ -59,10 +59,7 @@ runs: - name: Install dev dependencies shell: bash -el {0} - run: | - pip install \ - git+https://github.com/bokeh/bokeh-fastapi.git@main \ - git+https://github.com/holoviz/panel@7377c9e99bef0d32cbc65e94e908e365211f4421 + run: pip install --pre panel[fastapi] - name: Install ragna shell: bash -el {0} diff --git a/pyproject.toml b/pyproject.toml index f44c823f..e4b2b3a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,9 +15,9 @@ authors = [ readme = "README.md" classifiers = [ "Development Status :: 4 - Beta", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] requires-python = ">=3.10" dependencies = [ @@ -27,9 +27,7 @@ dependencies = [ "fastapi", "httpx", "packaging", - # FIXME: pin them to released versions - "bokeh-fastapi", - "panel", + "panel[fastapi]==1.5.0", "pydantic>=2", "pydantic-core", "pydantic-settings>=2", diff --git a/ragna/core/_components.py b/ragna/core/_components.py index ecec015b..a4d3e519 100644 --- a/ragna/core/_components.py +++ b/ragna/core/_components.py @@ -1,11 +1,11 @@ from __future__ import annotations import abc -import datetime import enum import functools import inspect import uuid +from datetime import datetime, timezone from typing import ( AsyncIterable, AsyncIterator, @@ -185,7 +185,7 @@ def __init__( role: MessageRole = MessageRole.SYSTEM, sources: Optional[list[Source]] = None, id: Optional[uuid.UUID] = None, - timestamp: Optional[datetime.datetime] = None, + timestamp: Optional[datetime] = None, ) -> None: if isinstance(content, str): self._content: str = content @@ -200,7 +200,7 @@ def __init__( self.id = id if timestamp is None: - timestamp = datetime.datetime.utcnow() + timestamp = datetime.now(timezone.utc) self.timestamp = timestamp async def __aiter__(self) -> AsyncIterator[str]: diff --git a/ragna/deploy/_core.py b/ragna/deploy/_core.py index 44c672a8..65cca3cd 100644 --- a/ragna/deploy/_core.py +++ b/ragna/deploy/_core.py @@ -2,12 +2,15 @@ import threading import time import webbrowser +from pathlib import Path from typing import AsyncContextManager, AsyncIterator, Callable, Optional, cast import httpx +import panel.io.fastapi from fastapi import FastAPI, Request, status from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse, Response +from fastapi.staticfiles import StaticFiles import ragna from ragna.core import RagnaException @@ -79,8 +82,14 @@ def server_available() -> bool: app.include_router(make_api_router(engine), prefix="/api") if ui: - panel_app = make_ui_app(engine) - panel_app.serve_with_fastapi(app, endpoint="/ui") + ui_app = make_ui_app(engine) + panel.io.fastapi.add_applications({"/ui": ui_app.index_page}, app=app) + for dir in ["css", "imgs"]: + app.mount( + f"/{dir}", + StaticFiles(directory=str(Path(__file__).parent / "_ui" / dir)), + name=dir, + ) @app.get("/", include_in_schema=False) async def base_redirect() -> Response: diff --git a/ragna/deploy/_ui/app.py b/ragna/deploy/_ui/app.py index 052ff36d..4163c378 100644 --- a/ragna/deploy/_ui/app.py +++ b/ragna/deploy/_ui/app.py @@ -1,10 +1,5 @@ -from pathlib import Path -from typing import cast - import panel as pn import param -from fastapi import FastAPI -from fastapi.staticfiles import StaticFiles from ragna.deploy._engine import Engine @@ -83,61 +78,6 @@ def index_page(self): def health_page(self): return pn.pane.HTML("

Ok

") - def add_panel_app(self, server, panel_app_fn, endpoint): - # FIXME: this code will ultimately be distributed as part of panel - from functools import partial - - import panel as pn - from bokeh.application import Application - from bokeh.application.handlers.function import FunctionHandler - from bokeh_fastapi import BokehFastAPI - from bokeh_fastapi.handler import WSHandler - from fastapi.responses import FileResponse - from panel.io.document import extra_socket_handlers - from panel.io.resources import COMPONENT_PATH - from panel.io.server import ComponentResourceHandler - from panel.io.state import set_curdoc - - def dispatch_fastapi(conn, events=None, msg=None): - if msg is None: - msg = conn.protocol.create("PATCH-DOC", events) - return [conn._socket.send_message(msg)] - - extra_socket_handlers[WSHandler] = dispatch_fastapi - - def panel_app(doc): - doc.on_event("document_ready", partial(pn.state._schedule_on_load, doc)) - - with set_curdoc(doc): - panel_app = panel_app_fn() - panel_app.server_doc(doc) - - handler = FunctionHandler(panel_app) - application = Application(handler) - - BokehFastAPI({endpoint: application}, server=server) - - @server.get( - f"/{COMPONENT_PATH.rstrip('/')}" + "/{path:path}", include_in_schema=False - ) - def get_component_resource(path: str): - # ComponentResourceHandler.parse_url_path only ever accesses - # self._resource_attrs, which fortunately is a class attribute. Thus, we can - # get away with using the method without actually instantiating the class - self_ = cast(ComponentResourceHandler, ComponentResourceHandler) - resolved_path = ComponentResourceHandler.parse_url_path(self_, path) - return FileResponse(resolved_path) - - def serve_with_fastapi(self, app: FastAPI, endpoint: str): - self.add_panel_app(app, self.index_page, endpoint) - - for dir in ["css", "imgs"]: - app.mount( - f"/{dir}", - StaticFiles(directory=str(Path(__file__).parent / dir)), - name=dir, - ) - def app(engine: Engine) -> App: return App(engine)