diff --git a/client.py b/client.py new file mode 100644 index 0000000..e7cfd3d --- /dev/null +++ b/client.py @@ -0,0 +1,29 @@ +import requests +import json + + +class Client: + + def __init__(self, host="127.0.0.1", port=8080): + self.host = host + self.port = port + self.session = requests.Session() + + def add_row(self, **kwargs): + addr = f"http://{self.host}:{self.port}/" + resp = self.session.patch(addr, json=kwargs) + resp.raise_for_status() + return ResponseWrapper(resp) + + + +class ResponseWrapper: + + def __init__(self, resp): + self.resp = resp + + def __repr__(self): + return self.resp.text + + + diff --git a/hacks/__init__.py b/hacks/__init__.py new file mode 100644 index 0000000..8c0dc0d --- /dev/null +++ b/hacks/__init__.py @@ -0,0 +1,4 @@ + +from . import patch_st_cp_stop + + diff --git a/hacks/patch_st_cp_stop.py b/hacks/patch_st_cp_stop.py new file mode 100644 index 0000000..6027a18 --- /dev/null +++ b/hacks/patch_st_cp_stop.py @@ -0,0 +1,21 @@ +""" +Monkey patch the streamlit server to stop cherrypy upon its own stop +""" + + +import cherrypy +from streamlit.server.server import Server + + +old_fn = Server._on_stopped + +def new_fn(*args, **kwargs): + print("exit cherrypy") + cherrypy.engine.exit() + print("exit streamlit") + return old_fn(*args, **kwargs) + +Server._on_stopped = new_fn + + + diff --git a/restapi.py b/restapi.py new file mode 100644 index 0000000..60f814a --- /dev/null +++ b/restapi.py @@ -0,0 +1,58 @@ +from threading import Thread +import json + +import cherrypy as cp + +from utils.df_utils import DateFrameHolder +from utils.st_utils import rerun + + +@cp.expose +class TableAPI: + + def __init__(self): + self.dfh = DateFrameHolder() + self.sid = None + self.changed = True + + @property + def data(self): + self.changed = False + return self.dfh.df + + @data.setter + def data(self, df): + self.dfh.df = df + + @cp.tools.json_in() + def PATCH(self, **kwargs): + kwargs = kwargs or cp.request.json + self.dfh.append(kwargs) + self.changed = True + rerun(self.sid) + return str(self.dfh.df) + + + +# creating instances here allows to use TableAPI etc. as singletons + +print(">>> start TableAPI") + +api = TableAPI() + +conf = { + "/": { + "request.dispatch": cp.dispatch.MethodDispatcher(), + "tools.sessions.on": True, + "tools.response_headers.on": True, + "tools.response_headers.headers": [("Content-Type", "text/plain")], + } +} + +args = (api, "/", conf) + +thread = Thread(target=cp.quickstart, args=args, daemon=True) +thread.start() + + + diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..4725405 --- /dev/null +++ b/run.sh @@ -0,0 +1,3 @@ +#!/bin/bash +streamlit run stand.py + diff --git a/stand.py b/stand.py new file mode 100644 index 0000000..86fa566 --- /dev/null +++ b/stand.py @@ -0,0 +1,55 @@ +import streamlit as st +from st_aggrid import AgGrid + +import hacks + +from restapi import api +from utils.st_utils import get_session_id, rerun + + +st.set_page_config(layout="wide") + + +api.sid = get_session_id() # rest api needs current session ID to trigger the next rerun + +changed = api.changed +df = api.data + + + +print(">>> start of streamlit run") + + +response = AgGrid( + df, + + filter=True, + editable=True, + sortable=True, + resizable=True, + +# defaultWidth=5, + fit_columns_on_grid_load=True, + + reload_data=changed, + key="stand" +) + + +new_df = response.get("data")#, df) +if not new_df.equals(df) and not changed: + api.data = new_df +# print("old:") +# print(df) +# print("new:") +# print(new_df) +# print(">>> force rerun") +# rerun() + +#st.dataframe(df.astype(str)) +#st.dataframe(new_df.astype(str)) + +print(">>> end of streamlit run") + + + diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/df_utils.py b/utils/df_utils.py new file mode 100644 index 0000000..bf01c08 --- /dev/null +++ b/utils/df_utils.py @@ -0,0 +1,15 @@ +import pandas as pd + + +class DateFrameHolder: + + def __init__(self, df=None): + self.df = df or pd.DataFrame() +# self.df = pd.DataFrame.from_records([dict(a=1, b=2, c=3)] * 4) #TODO: remove + + def append(self, data): + data = pd.DataFrame.from_records([data]) + self.df = pd.concat([self.df, data], ignore_index=True) + + + diff --git a/utils/st_utils.py b/utils/st_utils.py new file mode 100644 index 0000000..bceb344 --- /dev/null +++ b/utils/st_utils.py @@ -0,0 +1,21 @@ +import streamlit as st + + +def rerun(session_id=None): + if session_id is None: + session_id = get_session_id() + + server = st.server.server.Server.get_current() + session = server._get_session_info(session_id).session + + client_state = None + session.request_rerun(client_state) + + + +def get_session_id(): + ctx = st.scriptrunner.script_run_context.get_script_run_ctx() + return ctx.session_id + + +