-
Notifications
You must be signed in to change notification settings - Fork 146
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Webhooks - example #841
Comments
In addition, would be nice if we can add additional (custom) endpoints to the solara server, and implement it into the app. |
Ok, i am pretty sure I am doing something very unholy here, but let me share an example. from copy import deepcopy
import solara
import solara.lab
from starlette.responses import JSONResponse
import asyncio
added = False
webhooks_result = solara.reactive('initial value')
async def update_webhook_value(message):
old_value = webhooks_result.value
message = deepcopy(message)
print(f'Updating webhook value from "{old_value}" to "{message}"')
webhooks_result.set(message)
print(f'Updated webhook value to: "{webhooks_result.value}"')
async def webhook_handler(request):
try:
payload = await request.json()
print('Request received!')
print(payload.get('message', 'no message'))
payload = deepcopy(payload)
asyncio.create_task(update_webhook_value(payload.get('message', 'no message')))
return JSONResponse({
"status": "success",
"message": "Webhook received",
"data": payload
})
except Exception as e:
return JSONResponse({
"status": "error",
"message": str(e)
}, status_code=400)
def add_webhook_handler():
global added
if not added:
import solara.server.starlette
solara.server.starlette.app.router.add_route("/webhook", webhook_handler, methods=["POST"])
added = True
@solara.component
def Page():
add_webhook_handler()
value = solara.use_reactive('')
def fetch_webhook_value():
print('Fetching webhook value')
print(f'The webhook value is: {webhooks_result.value}')
value.set(webhooks_result.value)
webhooks_result.set('what is this?')
with solara.Column(align="center"):
with solara.Card(style="width: 400px; margin: auto;"):
solara.Text("Hello")
solara.Markdown("This is a markdown block")
solara.Markdown(f"Webhook added: {added}")
solara.Markdown("### Latest Webhook Data")
solara.Markdown(f'{webhooks_result.value}')
solara.Button(label='Fetch webhook value', on_click=fetch_webhook_value)
solara.Text(f'Value: {value.value}') Then one can hit the endpoint with
So i am almost happy with this! I can query the endpoint, and the endpoint can trigger functions inside the codebase. In the example above, the global reactive variable However the UI does not know of any of these changes! First I thought that a re-render is not triggered upon updating the global reactive variable, but looks like something beyond that. If by some other (solara proper process), i update the webhooks_result variable (say but pressing a button what sets a fixed value), I see that the previous value has nothing to do with the values obtained via the webhook, but the initial value was there. Upon setting, if you hit the webhook endpoint, the reactive variable has no knowledge of the interactions with the solara UI. So in short.. webhooks_result seems to live in two contexts: the solara/reactor render context, and something to do with starlette context. Same variable (at least same name) same starting point, but that'sit.. How do I connect these two? Is it even possible? Thank you and many thanks! |
You got pretty far! The issue is that a reactive var is a container for multiple virtual kernels, so it should be set for each kernel (i.e. all pages connect). from copy import deepcopy
import solara
import solara.lab
from starlette.responses import JSONResponse
import asyncio
import solara.server.kernel_context
added = False
webhooks_result = solara.reactive('initial value')
async def update_webhook_value(message):
for kernel_id, context in solara.server.kernel_context.contexts.items():
with context:
old_value = webhooks_result.value
message = deepcopy(message)
print(f'Updating webhook value from "{old_value}" to "{message}". Kernel: {kernel_id}')
webhooks_result.value = message
print(f'Updated webhook value to: "{webhooks_result.value}"')
async def webhook_handler(request):
try:
payload = await request.json()
print('Request received!')
print(payload.get('message', 'no message'))
payload = deepcopy(payload)
await update_webhook_value(payload.get('message', 'no message'))
return JSONResponse({
"status": "success",
"message": "Webhook received",
"data": payload
})
except Exception as e:
return JSONResponse({
"status": "error",
"message": str(e)
}, status_code=400)
def add_webhook_handler():
global added
if not added:
import solara.server.starlette
solara.server.starlette.app.router.add_route("/webhook", webhook_handler, methods=["POST"])
added = True
@solara.component
def Page():
counter = solara.use_reactive(0)
add_webhook_handler()
print("Render", solara.get_kernel_id(), webhooks_result.value, counter.value)
solara.Text(f'Value: {webhooks_result.value}')
solara.Button("Increment", on_click=lambda: counter.set(counter.value + 1)) Something like this should work, but it for some reason does not render it to the screen. At least it might satisfy your curiosity! |
Thanks for the response! I get the idea.. but still does not work. When you click the counter value, there is the new value printed on the terminal and the page component updates. This is a curious case indeed. Would be nice if it is solved at some point in the future, with a more elegant api. Webhooks are quite common (I am learning..) so it is very helpful when interacting with other services for more serious use-case... Thanks! I will report back if I figure this out in the meantime.. |
I think I got it to work (sort of). See the example below. When I git the No idea how this example is different from what you showed above.. Anyway, this is good enough for now I think. I would be fantastic if down the line webhooks are officially supported! from copy import deepcopy
import random
import string
import solara
import solara.lab
import solara.server.kernel_context
from starlette.responses import JSONResponse
added = False
webhooks_result = solara.reactive('initial value')
async def update_webhook_value(message):
for kernel_id, context in solara.server.kernel_context.contexts.items():
with context:
old_value = webhooks_result.value
message = deepcopy(message)
print(f'Updating webhook value from "{old_value}" to "{message}". Kernel: {kernel_id}')
webhooks_result.value = message
print(f'Updated webhook value to: "{webhooks_result.value}"')
async def webhook_handler(request):
try:
payload = await request.json()
print('Request received!')
print(payload.get('message', 'no message'))
payload = deepcopy(payload)
await update_webhook_value(payload.get('message', 'no message'))
return JSONResponse({
"status": "success",
"message": "Webhook received",
"data": payload
})
except Exception as e:
return JSONResponse({
"status": "error",
"message": str(e)
}, status_code=400)
def add_webhook_handler():
global added
if not added:
import solara.server.starlette
solara.server.starlette.app.router.add_route("/webhook", webhook_handler, methods=["POST"])
added = True
@solara.component
def Page():
add_webhook_handler()
value = solara.use_reactive('')
increment = solara.use_reactive(0)
print("Render", solara.get_kernel_id(), webhooks_result.value, value.value)
def fetch_webhook_value():
print('Fetching webhook value')
print(f'The webhook value is: {webhooks_result.value}')
value.set(webhooks_result.value)
random_string = ''.join(random.choices(string.ascii_letters + string.digits, k=5))
what = f"what is this? {random_string}"
value.set(what)
with solara.Column(align="center"):
with solara.Card(style="width: 400px; margin: auto;"):
solara.Text("Hello")
solara.Markdown("This is a markdown block")
solara.Markdown(f"Webhook added: {added}")
with solara.Column():
solara.Markdown("### Latest Webhook Data")
solara.Markdown(f'{webhooks_result.value}')
solara.Button(label='Fetch webhook value', on_click=fetch_webhook_value)
solara.Text(f'Value: {value.value}')
solara.Button(label='Increment', on_click=lambda: increment.set(increment.value + 1))
solara.Text(f'Increment: {increment.value}') |
Instead of assuming all exceptions are due to a closed connection, we only ignored the exception when it is due to a closed connection. This caused issues in #841 where we called from the same thread as the websocket's portal, which caused the connection to be ignored without any error message.
Instead of assuming all exceptions are due to a closed connection, we only ignored the exception when it is due to a closed connection. This caused issues in #841 where we called from the same thread as the websocket's portal, which caused the connection to be ignored without any error message.
Seeing @Ben-Epstein also wanted to have this working, I've take another look at this. It appeared the problem was due to #919 which should be released soon. Together with this code: from copy import deepcopy
import random
import string
import solara
import solara.lab
import solara.server.kernel_context
from starlette.responses import JSONResponse
from starlette.requests import Request
from anyio import to_thread
added = False
webhooks_result = solara.reactive(None)
def update_webhook_value(message):
# we update the reactive value for all kernels (i.e. connected browser pages)
for kernel_id, context in solara.server.kernel_context.contexts.items():
with context:
old_value = webhooks_result.value
message = deepcopy(message)
print(f'Updating webhook value from "{old_value}" to "{message}". Kernel: {kernel_id}')
webhooks_result.value = message
async def webhook_handler(request: Request):
try:
payload = await request.json()
print('Request received!')
print(payload.get('message', 'no message'))
payload = deepcopy(payload)
# if we call this directly, we run in the same thread as the uvicorn/starlette server (or anyio thread)
# that the websocket is related to, which causes issues (it will raise an exception in solara >= 1.43)
# using anyio, we run it in a separate thread
await to_thread.run_sync(update_webhook_value, payload.get('message', 'no message'))
return JSONResponse({
"status": "success",
"message": "Webhook received",
"data": payload
})
except Exception as e:
return JSONResponse({
"status": "error",
"message": str(e)
}, status_code=400)
def add_webhook_handler():
# workaround since we cannot import solara.server.starlette directly
global added
if not added:
import solara.server.starlette
solara.server.starlette.app.router.add_route("/webhook", webhook_handler, methods=["POST"])
added = True
@solara.component
def Page():
add_webhook_handler()
with solara.Column(align="center", style={"height": "100%", "justify-content": "center"}):
with solara.Card(style="width: 400px; margin: auto;"):
with solara.Column():
if webhooks_result.value is None:
solara.Markdown("No data yet")
else:
solara.Markdown(f'{webhooks_result.value}') And this curl command:
Should be a good working example. Once released, I'll close the issue, if there are more issues, feel free to let me know! |
Hi,
Would it be possible to add the simplest possible example of how one would create or "listen to" webhooks with solara?
Thank you!
The text was updated successfully, but these errors were encountered: