Sending Notifications to certain clients using Server Sent Events in FastApi? - python-3.x

I have been working with server sent events to send out certain type of notifications to only certain clients. I am using the module called sse-starlette to try and achieve this.
I am fairly new to FastApi so I am not able to figure out how to send the data to only certain clients instead of broadcasting to everyone.
This is what I have thought so far:
Subscribe request with query param
localhost:8000/subscribe?id=1
from sse_starlette.sse import EventSourceResponse
class EmitEventModel(BaseModel):
event_name: str
event_data: Optional[str] = "No Event Data"
event_id: Optional[int] = None
recipient_id: str
async def connection_established():
yield dict(data="Connection established")
clients = {}
#app.get("/subscribe")
async def loopBackStream(req: Request, id: str = ""):
clients[id] = EventSourceResponse(connection_established())
return clients[id]
#app.post("/emit")
async def emitEvent(event: EmitEventModel):
if clients[event.recipient_id]:
clients[event.recipient_id](publish_event())
Whenever there is an api call to localhost:8000/emit containing the body, Based on the recipient_id the event is going to be routed.
Ofcourse this doesn't work so far. Any pointers as to what should be done to achieve this?
sse_starlette for reference:
https://github.com/sysid/sse-starlette/blob/master/sse_starlette/sse.py

The idea here is that you're going to need to identify the recipient_id on the SSE generator. I've slightly modified your code, to be able to show what I mean:
from __future__ import annotations
import asyncio
import itertools
from collections import defaultdict
from fastapi import Request, FastAPI
from pydantic import BaseModel
from sse_starlette.sse import EventSourceResponse
app = FastAPI()
clients = defaultdict(list)
class EmitEventModel(BaseModel):
event_name: str
event_data: Optional[str] = "No Event Data"
event_id: Optional[int] = None
recipient_id: str
async def retrieve_events(recipient_id: str) -> NoReturn:
yield dict(data="Connection established")
while True:
if recipient_id in clients and len(clients[recipient_id]) > 0:
yield clients[recipient_id].pop()
await asyncio.sleep(1)
print(clients)
#app.get("/subscribe/{recipient_id}")
async def loopBackStream(req: Request, recipient_id: str):
return EventSourceResponse(retrieve_events(recipient_id))
#app.post("/emit")
async def emitEvent(event: EmitEventModel):
clients[event.recipient_id].append(event)

Related

Modern apis with FastAPI - Redis Caching

I'm trying to implement the redis caching in my API for the first time it is a simple fastapi application which is using openweather api to query some weather information and my intention is to cache each json response in the redis server. I'hve made it to work with single key city but with this approach validating query parameters not working with the error handling I put in the place.
caching.py
import sys
from datetime import timedelta
import json
import redis
from services.openweather_service import get_report
def redis_connect() -> redis.client.Redis:
try:
client = redis.Redis(
host="localhost",
port=6379,
db=0,
)
ping = client.ping()
if ping is True:
return client
except redis.ConnectionError:
print("Connection Error!")
sys.exit(1)
client = redis_connect()
def get_routes_from_cache(key: str) -> str:
"""Data from redis."""
val = client.get(key)
return val
def set_routes_to_cache(key: str, value: str) -> bool:
"""Data to redis."""
state = client.setex(
key,
timedelta(hours=24),
value=value,
)
return state
async def route_optima(city: str, state: str, country: str, units=None) -> dict:
location = {"city": city, "state": state, "country": country, "units": units}
# First it looks for the data in redis cache
data = get_routes_from_cache(key=json.dumps(location))
# print(data)
# print(type(data))
# If cache is found then serves the data from cache
if data is not None:
data = data.decode("UTF-8")
data_dict = json.loads(data)
print(data_dict)
print(type(data_dict))
data["cache"] = True
return data
else:
# If cache is not found then sends request to the OpenWeather API
data = await get_report(city, state, country, units)
# This block sets saves the respose to redis and serves it directly
data["cache"] = False
data = json.dumps(data)
state = set_routes_to_cache(key=json.dumps(location), value=json.dumps(data))
if state is True:
return json.dumps(data)
return data
Then I took a different approach by making the query params location = {"city": city, "state": state, "country": country, "units": units} as a key and the json response as a value but when app try to get the response from the cache here it got weird like right after passing the query params dict into the json.dumps it gives me the bytes object and so I decode it with decode("utf-8") but instead of coming back as a dict it gives me <class 'str'> type object. There I am lost... can anyone help me out here?
weather_api.py
from typing import Optional
import fastapi
from fastapi import Depends
from models.location import Location
from infrastructure.caching import route_optima
from models.validation_error import ValidationError
router = fastapi.APIRouter()
#router.get("/api/weather/{city}")
async def weather(loc: Location = Depends(), units: Optional[str] = "metric"):
return await route_optima(loc.city, loc.state, loc.country, units)
And if I am using the wrong approach here then please point to the best approach to take here.
As you are using Fastapi, it is better to use aioredis to leverage the async functionality.
It turns out I was returning the wrong data it should be like this
# If cache is found then serves the data from cache
if data is not None:
data = data.decode("UTF-8")
data_dict = json.loads(data)
print(data_dict)
print(type(data_dict))
data_dict["cache"] = True
return data_dict
and everything works fine.

How to send ros2 messages from a websocket server to connected clients in tornado

I have a ros2 publisher script that sends custom messages from ros2 nodes. What I need to do is to have a subscriber (which is also my websocket server) to listen to the message that the pulisher sends then convert it to a dictionary and send it as a json from the websocket server to a connected websocket client. I have already checked the rosbridge repo but I could not make it work. It doesn't have enough documentation and I am new to ros.
I need something like this:
import rclpy
import sys
from rclpy.node import Node
import tornado.ioloop
import tornado.httpserver
import tornado.web
import threading
from custom.msg import CustomMsg
from .convert import message_to_ordereddict
wss = []
class wsHandler(tornado.websocket.WebSocketHandler):
def open(self):
print 'Online'
if self not in wss:
wss.append(self)
def on_close(self):
print 'Offline'
if self in wss:
wss.remove(self)
def wsSend(message):
for ws in wss:
ws.write_message(message)
class MinimalSubscriber(Node):
def __init__(self):
super().__init__('minimal_subscriber')
self.subscription = self.create_subscription(CustomMsg, 'topic', self.CustomMsg_callback, 10)
self.subscription # prevent unused variable warning
def CustomMsg_callback(self, msg):
ws_message = message_to_ordereddict(msg)
wsSend(ws_message)
if __name__ == "__main__":
http_server = tornado.httpserver.HTTPServer(tornado.web.Application(wsHandler))
http_server.listen(8888)
main_loop = tornado.ioloop.IOLoop.instance()
# Start main loop
main_loop.start()
so the callback function in MinimalSubscriber class, receives the ros message, converts it to dictionary and sends it to websocket client. I am a bit confused how to make these two threads (ros and websocket) to communicate with each other.
So I think I got a bit confused myself going through the threading. So I changed my approach and made it work using the tornado periodic callback and the spin_once function of rclpy as the callback function. I would post my solution as it might help some people who has the same issue.
import queue
import rclpy
from rclpy.node import Node
import tornado.ioloop
import tornado.httpserver
import tornado.web
from custom.msg import CustomMsg
from .convert import message_to_ordereddict
wss = []
class wsHandler(tornado.websocket.WebSocketHandler):
#classmethod
def route_urls(cls):
return [(r'/',cls, {}),]
def open(self):
print 'Online'
if self not in wss:
wss.append(self)
def on_close(self):
print 'Offline'
if self in wss:
wss.remove(self)
def make_app():
myWebHandler = wsHandler.route_urls()
return tornado.web.Application(myWebHandler)
message_queue = queue.Queue
class MinimalSubscriber(Node):
def __init__(self):
super().__init__('minimal_subscriber')
self.subscription = self.create_subscription(CustomMsg, 'topic', self.CustomMsg_callback, 10)
self.subscription # prevent unused variable warning
def CustomMsg_callback(self, msg):
msg_dict = message_to_ordereddict(msg)
msg_queue.put(msg_dict)
if __name__ == "__main__":
rclpy.init(args=args)
minimal_subscriber = MinimalSubscriber()
def send_ros_to_clients():
rclpy.spin_once(minimal_subscriber)
my_msg = msg_queue.get()
for client in ws_clients:
client.write_message(my_msg)
app = make_app()
server = tornado.httpserver.HTTPServer(app)
server.listen(8888)
tornado.ioloop.PeriodicCallback(send_ros_to_clients, 1).start()
tornado.ioloop.IOLoop.current().start()
minimal_subscriber.destroy_node()
rclpy.shutdown()
I also implemented the wsSend function into the send_ros_to_clients function. Some might say that using a global queue is not the best practice but I could not come up with another solution. I would appreciate any suggestions or corrections on my solution.

What is the best way to set a timeout condition for functions decorated with #run_on_executor in Tornado?

What would be the best approach for setting a timeout condition on a task/function that's been submitted to a ThreadPoolExecutor using tornado.concurrent's #run_on_executor decorator? Example Tornado handler below:
import json
import time
import tornado.web
from concurrent.futures import ThreadPoolExecutor
from tornado.concurrent import run_on_executor
class MyHandler(tornado.web.RequestHandler):
def initialize(self) -> None:
self.executor = ThreadPoolExecutor(1)
#run_on_executor
def blocking_function(self) -> None:
""" Run Blocking Function on ThreadPoolExecutor. """
seconds = 10
time.sleep(seconds)
response = json.dumps({"message": f"Slept for {seconds} seconds."})
return response
async def get(self) -> None:
response = await self.blocking_function()
self.write(response)
Does something like tornado.gen.with_timeout found here exist for #run_on_executor?
Thank you for your time.
Since run_on_executor returns a Future object, you can use it with gen.with_timetout:
from datetime import timedelta
async def get(self):
response = await gen.with_timeout(
timedelta(seconds=5),
self.blocking_function()
)
...
Don't forget to handle the timeout exception.

python3 aiogram. How to send message to user_id

from aiogram import Bot
import config
def send_message(message):
operator = Bot(config.operator_token)
operator.send_message(config.user_id, message)
def main():
send_message('hi')
I want to send message to user with id from my config.py (config.user_id). But it's not workinkg. I tried many ways for it but I always got an error.
For example like this
Unclosed client session
client_session: <aiohttp.client.ClientSession object at 0x7f94a9b62550>
Unclosed connector
connections: ['[(<aiohttp.client_proto.ResponseHandler object at 0x7f94a98f8040>,
23065.38)]']
connector: <aiohttp.connector.TCPConnector object at 0x7f94a9b3c400>'
If you use aiogram you must use async/await to send message.
bot = Bot(token=TOKEN)
dp = Dispatcher(bot_init)
#dp.message_handler(commands=['test'])
async def process_start_command(message: types.Message):
await bot.send_message(message.from_user.id, "test message") #like this
#await message.answer("test message")
#or like this
Maybe this?!
import asyncio
from aiogram import Bot
import config
async def send_message(message):
operator = Bot(config.operator_token)
await operator.send_message(config.user_id, message)
def main():
asyncio.run(send_message('hi'))
from aiogram import Bot, types
from aiogram.dispatcher import Dispatcher
from aiogram.utils import executor
from config import TOKEN
bot = Bot(token=TOKEN)
dp = Dispatcher(bot)
#dp.message_handler(commands=['start'])
async def process_start_command(message: types.Message):
await message.reply("Hello\nWrite me something!")
#dp.message_handler(commands=['help'])
async def process_help_command(message: types.Message):
await message.reply("Implementation EchoBot")
#dp.message_handler()
async def echo_message(msg: types.Message):
await bot.send_message(msg.from_user.id, msg.text)
if __name__ == '__main__':
executor.start_polling(dp)

Response not loading in localhost when using requests.request method in python

import tornado.web
import tornado.ioloop
from apiApplicationModel import userData
from cleanArray import Label_Correction
import json
import requests
colName=['age', 'resting_blood_pressure', 'cholesterol', 'max_heart_rate_achieved', 'st_depression', 'num_major_vessels', 'st_slope_downsloping', 'st_slope_flat', 'st_slope_upsloping', 'sex_male', 'chest_pain_type_atypical angina', 'chest_pain_type_non-anginal pain', 'chest_pain_type_typical angina', 'fasting_blood_sugar_lower than 120mg/ml', 'rest_ecg_left ventricular hypertrophy', 'rest_ecg_normal', 'exercise_induced_angina_yes', 'thalassemia_fixed defect', 'thalassemia_normal',
'thalassemia_reversable defect']
class processRequestHandler(tornado.web.RequestHandler):
def post(self):
data_input_array = []
for name in colName:
x = self.get_body_argument(name, default=0)
data_input_array.append(int(x))
label = Label_Correction(data_input_array)
finalResult = int(userData(label))
output = json.dumps({"Giveput":finalResult})
self.write(output)
class basicRequestHandler(tornado.web.RequestHandler):
def get(self):
self.render('report.html')
class staticRequestHandler(tornado.web.RequestHandler):
def post(self):
data_input_array = []
for name in colName:
x = self.get_body_argument(name, default=0)
data_input_array.append(str(x))
send_data = dict(zip(colName, data_input_array))
print(send_data)
print(type(send_data))
url = "http://localhost:8887/output"
headers={}
response = requests.request('POST',url,headers=headers,data=send_data)
print(response.text.encode('utf8'))
print("DONE")
if __name__=='__main__':
app = tornado.web.Application([(r"/",basicRequestHandler),
(r"/result",staticRequestHandler),
(r"/output",processRequestHandler)])
print("API IS RUNNING.......")
app.listen(8887)
tornado.ioloop.IOLoop.current().start()
Actually I am trying to create an API and the result of the API can be used but
The page keeps on loading, and no response is shown.
Response should be a python dictionary send by post function of class processRequestHandler
After using a debugger the lines after response = requests.request('POST',url,headers=headers,data=send_data) are not executed.
The class processRequestHandler is working fine when checked with POSTMAN.
requests.request is a blocking method. This blocks the event loop and prevents any other handlers from running. In a Tornado handler, you need to use Tornado's AsyncHTTPClient (or another non-blocking HTTP client such as aiohttp) instead.
async def post(self):
...
response = await AsyncHTTPClient().fetch(url, method='POST', headers=headers, body=send_data)
See the Tornado users's guide for more information.

Resources