How to log user activity in a streamlit app? - streamlit

I have a streamlit app that is public (ie. no user log-in). I would like to have log files of the form:
|2023-02-10 16:30:16 : user at ip=___ clicked button key=___
|2023-02-10 16:30:19 : user at ip=___ clicked button key=___
|2023-02-10 16:31:10 : user at ip=___ clicked button key=___
|2023-02-10 16:31:27 : user at ip=___ clicked button key=___
|...
Is there any way to achieve this? It's because I want to do some analytics on how the app is being used.

You can access the remote ip address via get_script_run_ctx and .remote_ip:
from streamlit import runtime
from streamlit.runtime.scriptrunner import get_script_run_ctx
def get_remote_ip() -> str:
"""Get remote ip."""
try:
ctx = get_script_run_ctx()
if ctx is None:
return None
session_info = runtime.get_instance().get_client(ctx.session_id)
if session_info is None:
return None
except Exception as e:
return None
return session_info.request.remote_ip
import streamlit as st
st.title("Title")
st.markdown(f"The remote ip is {get_remote_ip()}")
For the logging part, I suggest you use a ContextFilter:
import logging
class ContextFilter(logging.Filter):
def filter(self, record):
record.user_ip = get_remote_ip()
return super().filter(record)
This custom filter will modify the LogRecord and add it the custom attribute user_ip that you can then use inside the Formatter.
All together, it gives:
import logging
import streamlit as st
from streamlit import runtime
from streamlit.runtime.scriptrunner import get_script_run_ctx
def get_remote_ip() -> str:
"""Get remote ip."""
try:
ctx = get_script_run_ctx()
if ctx is None:
return None
session_info = runtime.get_instance().get_client(ctx.session_id)
if session_info is None:
return None
except Exception as e:
return None
return session_info.request.remote_ip
class ContextFilter(logging.Filter):
def filter(self, record):
record.user_ip = get_remote_ip()
return super().filter(record)
def init_logging():
# Make sure to instanciate the logger only once
# otherwise, it will create a StreamHandler at every run
# and duplicate the messages
# create a custom logger
logger = logging.getLogger("foobar")
if logger.handlers: # logger is already setup, don't setup again
return
logger.propagate = False
logger.setLevel(logging.INFO)
# in the formatter, use the variable "user_ip"
formatter = logging.Formatter("%(name)s %(asctime)s %(levelname)s [user_ip=%(user_ip)s] - %(message)s")
handler = logging.StreamHandler()
handler.setLevel(logging.INFO)
handler.addFilter(ContextFilter())
handler.setFormatter(formatter)
logger.addHandler(handler)
def main():
logger.info("Inside main")
st.title("Title")
text = st.sidebar.text_input("Text:")
logger.info(f"This is the text: {text}")
if __name__ == "__main__":
init_logging()
logger = logging.getLogger("foobar")
main()
foobar 2023-02-13 15:43:57,252 INFO [user_ip=::1] - Inside main
foobar 2023-02-13 15:43:57,253 INFO [user_ip=::1] - This is the text: Hello, world!
Note: Here the user_ip is "::1" because everything was done locally.

Related

Python script runs but does not work and no errors are thrown

This program is an API based program that has been working for a few months and all of a sudden has went days without pushing anything to Discord. The script looks fine in CMD, but no errors are being thrown. I was wondering if there was a way to eliminate possible issues such as an API instability issue or something obvious. The program is supposed to go to the site www.bitskins.com and pull skins based on parameters set and push them as an embed to a Discord channel every 10 minutes.
There are two files that run this program.
Here is the one that uses Bitskins API (bitskins.py):
import requests, json
from datetime import datetime, timedelta
class Item:
def __init__(self, item):
withdrawable_at= item['withdrawable_at']
price= float(item['price'])
self.available_in= withdrawable_at- datetime.timestamp(datetime.now())
if self.available_in< 0:
self.available= True
else:
self.available= False
self.suggested_price= float(item['suggested_price'])
self.price= price
self.margin= round(self.suggested_price- self.price, 2)
self.reduction= round((1- (self.price/self.suggested_price))*100, 2)
self.image= item['image']
self.name= item['market_hash_name']
self.item_id= item['item_id']
def __str__(self):
if self.available:
return "Name: {}\nPrice: {}\nSuggested Price: {}\nReduction: {}%\nAvailable Now!\nLink: https://bitskins.com/view_item?app_id=730&item_id={}".format(self.name, self.price, self.suggested_price, self.reduction, self.item_id)
else:
return "Name: {}\nPrice: {}\nSuggested Price: {}\nReduction: {}%\nAvailable in: {}\nLink: https://bitskins.com/view_item?app_id=730&item_id={}".format(self.name, self.price, self.suggested_price, self.reduction, str(timedelta(seconds= self.available_in)), self.item_id)
def __lt__(self, other):
return self.reduction < other.reduction
def __gt__(self, other):
return self.reduction > other.reduction
def get_url(API_KEY, code):
PER_PAGE= 30 # the number of items to retrieve. Either 30 or 480.
return "https://bitskins.com/api/v1/get_inventory_on_sale/?api_key="+ API_KEY+"&code=" + code + "&per_page="+ str(PER_PAGE)
def get_data(url):
r= requests.get(url)
data= r.json()
return data
def get_items(code, API_KEY):
url= get_url(API_KEY, code)
try:
data= get_data(url)
if data['status']=="success":
items= []
items_dic= data['data']['items']
for item in items_dic:
tmp= Item(item)
if tmp.reduction>=25 and tmp.price<=200: # Minimum discount and maximum price to look for when grabbing items. Currently set at minimum discount of 25% and maxmimum price of $200.
items.append(tmp)
return items
else:
raise Exception(data["data"]["error_message"])
except:
raise Exception("Couldn't connect to BitSkins.")
# my_token = pyotp.TOTP(my_secret)
# print(my_token.now()) # in python3
And here is the file with Discord's API (solution.py):
#!/bin/env python3.6
import bitskins
import discord
import pyotp, base64, asyncio
from datetime import timedelta, datetime
TOKEN= "Not input for obvious reasons"
API_KEY= "Not input for obvious reasons"
my_secret= 'Not input for obvious reasons'
client = discord.Client()
def get_embed(item):
embed=discord.Embed(title=item.name, url= "https://bitskins.com/view_item?app_id=730&item_id={}".format(item.item_id), color=0xA3FFE8)
embed.set_author(name="Skin Bot", url="http://www.reactor.gg/",icon_url="https://pbs.twimg.com/profile_images/1050077525471158272/4_R8PsrC_400x400.jpg")
embed.set_thumbnail(url=item.image)
embed.add_field(name="Price:", value="${}".format(item.price))
embed.add_field(name="Discount:", value="{}%".format(item.reduction), inline=True)
if item.available:
tmp= "Instantly Withdrawable"
else:
tmp= str(timedelta(seconds= item.available_in))
embed.add_field(name="Availability:", value=tmp, inline=True)
embed.add_field(name="Suggested Price:", value="${}".format(item.suggested_price), inline=True)
embed.add_field(name="Profit:", value="${}".format(item.margin), inline=True)
embed.set_footer(text="Made by Aqyl#0001 | {}".format(datetime.now()), icon_url="https://www.discordapp.com/assets/6debd47ed13483642cf09e832ed0bc1b.png")
return embed
async def status_task(wait_time= 60* 5):
while True:
print("Updated on: {}".format(datetime.now()))
code= pyotp.TOTP(my_secret)
try:
items= bitskins.get_items(code.now(), API_KEY)
for item in items:
await client.send_message(client.get_channel("656913641832185878"), embed=get_embed(item))
except:
pass
await asyncio.sleep(wait_time)
#client.event
async def on_ready():
wait_time= 60 * 10 # 10 mins in this case
print('CSGO BitSkins Bot')
print('Made by Aqyl#0001')
print('Version 1.0.6')
print('')
print('Logged in as:')
print(client.user.name)
print('------------------------------------------')
client.loop.create_task(status_task(wait_time))
try:
client.run(TOKEN)
except:
print("Couldn't connect to the Discord Server.")
You have a general exception, this will lead to catching exceptions that you really don't want to catch.
try:
items= bitskins.get_items(code.now(), API_KEY)
for item in items:
await client.send_message(client.get_channel("656913641832185878"), embed=get_embed(item))
except:
pass
This is the same as catching any exception that appears there (Exceptions that inherit BaseException
To avoid those problems, you should always catch specific exceptions. (i.e. TypeError).
Example:
try:
raise Exception("Example exc")
except Exception as e:
print(f"Exception caught! {e}")

Can't get fresh length of list Telegram bot API

I have flask telegram bot with pyTelegramBotAPI deployed on Heroku. Need to get fresh lenght of list in the start message. List refreshing every 5 minutes in gettinglist.py. Can't find my mistake, please help.
bot.py
import config
import gettinglist
from gettinglist import getting_list
import telebot
from flask import Flask, request
from threading import Thread
def app_run():
app.run(host="0.0.0.0", port=os.environ.get('PORT', 80))
msg_start = """
Lenght of list now: %d
""" % config.LIST_LENGHT
application_thread = Thread(target=app_run)
getting_list_thread = Thread(target=getting_list)
bot = telebot.TeleBot("<MY_BOT_TOKEN>")
#bot.message_handler(commands=['start'])
def start():
cid = m.chat.id
bot.send_message(cid, msg_start, parse_mode='html')
#app.route("/bot", methods=['POST'])
def getMessage():
bot.process_new_updates([telebot.types.Update.de_json(request.stream.read().decode("utf-8"))])
return "ok", 200
#app.route("/")
def webhook():
bot.remove_webhook()
bot.set_webhook(url="<HEROKU_APP_URL>")
return "ok", 200
if __name__ == '__main__':
application_thread.start()
getting_list_thread.start()
gettinglist.py
import config
from time import sleep
LIST_LENGHT = 0
LIST = []
def getting_list():
while True:
global LIST
global LIST_LENGHT
LIST = [num for num in range(0, 100)]
config.LIST_LENGHT = len(LIST)
return LIST
sleep(300)
config.py
LIST_LENGHT = 0

how to load multiple pages one by one in QWebPage

I am trying to crawl news article pages for comments. After some research I found that mostly websites use an iframe for it. I want to get the "src" of the iframe. I am using QtWebKit in Python using PySide. It is actually working but just once. It is not loading other webpages. I am using the following code:
import sys
import pymysql
from PySide.QtGui import *
from PySide.QtCore import *
from PySide.QtWebKit import *
from pprint import pprint
from bs4 import BeautifulSoup
class Render(QWebPage):
def __init__(self, url):
try:
self.app = QApplication(sys.argv)
except RuntimeError:
self.app = QCoreApplication.instance()
QWebPage.__init__(self)
self.loadFinished.connect(self._loadFinished)
self.mainFrame().load(QUrl(url))
self.app.exec_()
def _loadFinished(self, result):
self.frame = self.mainFrame()
self.app.quit()
def visit(url):
r = Render(url)
p = r.frame.toHtml()
f_url = str(r.frame.url().toString())
return p
def is_comment_url(url):
lower_url = url.lower()
n = lower_url.find("comment")
if n>0:
return True
else:
return False
with open("urls.txt") as f:
content = f.read().splitlines()
list_of_urls = []
for url in content:
page = visit(url)
soup = BeautifulSoup(page)
for tag in soup.findAll('iframe', src=True):
link = tag['src']
if is_comment_url(link):
print(link)
list_of_urls += link
pprint(list_of_urls)
But the issue is it works only for single iteration and gets stuck.
Also is there any way to save a web page as it is as displayed by the browser (after executing all the javascript etc.)

Python QtWebKit 2nd link not downloaded

I am trying to download the content of both the links.The first link works properly and the content is downloaded as a html file.But the content of the 2 nd link(a text file) is not downloaded.Please help me out.
Here is my code :
from PySide.QtCore import *
from PySide.QtGui import *
from PySide.QtWebKit import *
import sys
import codecs
class Downloader(QObject):
# To be emitted when every items are downloaded
done = Signal()
def __init__(self, urlList, parent = None):
super(Downloader, self).__init__(parent)
self.urlList = urlList
self.counter = 0
# As you probably don't need to display the page
# you can use QWebPage instead of QWebView
self.page = QWebPage(self)
self.page.loadFinished.connect(self.save)
self.startNext()
def currentUrl(self):
return self.urlList[self.counter][0]
def currentFilename(self):
return self.urlList[self.counter][1]
def startNext(self):
print "Downloading %s..."%self.currentUrl()
self.page.mainFrame().load(self.currentUrl())
def save(self, ok):
if ok:
data = self.page.mainFrame().toHtml()
with codecs.open(self.currentFilename(), encoding="utf-8", mode="w") as f:
f.write(data)
print "Saving %s to %s."%(self.currentUrl(), self.currentFilename())
else:
print "Error while downloading %s\nSkipping."%self.currentUrl()
self.counter += 1
if self.counter < len(self.urlList):
self.startNext()
else:
self.done.emit()
urlList = [("http://www.nsf.gov/awardsearch/simpleSearchResult?queryText=8","nsf.html"),
("http://www.nsf.gov/awardsearch/ExportResultServlet?exportType=txt","a.txt")]
app = QApplication(sys.argv)
downloader = Downloader(urlList)
# Quit when done
downloader.done.connect(app.quit)
# To view the pages
web = QWebView()
# To prevent user action that would interrupt the current page loading
web.setDisabled(True)
web.setPage(downloader.page)
web.show()
sys.exit(app.exec_())

Python logging config that writes to Qt widget

I haven't quite found the answer yet in other threads with similar titles. Let's say I have a logging.conf that looks like this:
[loggers]
keys=root,analyzer
[handlers]
keys=consoleHandler,analyzerFileHandler
[formatters]
keys=consoleFormatter,logFormatter
[logger_root]
level=DEBUG
handlers=consoleHandler,analyzerFileHandler
[logger_analyzer]
level=DEBUG
handlers=consoleHandler,analyzerFileHandler
qualname=analyzer
propagate=0
[handler_consoleHandler]
class=StreamHandler
level=INFO
formatter=consoleFormatter
args=(sys.stdout,)
[handler_analyzerFileHandler]
class=FileHandler
level=DEBUG
formatter=logFormatter
args=('analyzer.log','w')
[formatter_consoleFormatter]
format=%(asctime)s - %(name)s - %(levelname)s | %(message)s
datefmt=%m/%d/%Y %X
[formatter_logFormatter]
format=%(asctime)s - %(levelname)s | %(message)s
datefmt=%m/%d/%Y %X
A logger = logging.getLogger('analyzer') will send text to the log file and the console, how can I make that sys.stdout go towards a QPlainTextEdit() widget instead of the console?
edit
OK looking at this post I made this code. The post's code is good, but for some reason it doesn't address the issue, you can comment out all the instances of logger and still end up with print events going to Qwidget. The logger, as is, has no real interaction with the rest of the program, it is just there. I thought I could rectify the problem by writing a class that took whatever I wanted as text and sent it off to print and logger respectively:
import logging
from logging.config import fileConfig
from os import getcwd
import sys
from PyQt4.QtCore import QObject,\
pyqtSignal
from PyQt4.QtGui import QDialog, \
QVBoxLayout, \
QPushButton, \
QTextBrowser,\
QApplication
class XStream(QObject):
_stdout = None
_stderr = None
messageWritten = pyqtSignal(str)
def flush( self ):
pass
def fileno( self ):
return -1
def write( self, msg ):
if ( not self.signalsBlocked() ):
self.messageWritten.emit(unicode(msg))
#staticmethod
def stdout():
if ( not XStream._stdout ):
XStream._stdout = XStream()
sys.stdout = XStream._stdout
return XStream._stdout
#staticmethod
def stderr():
if ( not XStream._stderr ):
XStream._stderr = XStream()
sys.stderr = XStream._stderr
return XStream._stderr
class XLogger():
def __init__(self, name):
self.logger = logging.getLogger(name)
def debug(self,text):
print text
self.logger.debug(text)
def info(self,text):
print text
self.logger.info(text)
def warning(self,text):
print text
self.logger.warning(text)
def error(self,text):
print text
self.logger.error(text)
class MyDialog(QDialog):
def __init__( self, parent = None ):
super(MyDialog, self).__init__(parent)
# setup the ui
self._console = QTextBrowser(self)
self._button = QPushButton(self)
self._button.setText('Test Me')
# create the layout
layout = QVBoxLayout()
layout.addWidget(self._console)
layout.addWidget(self._button)
self.setLayout(layout)
# create connections
XStream.stdout().messageWritten.connect( self._console.insertPlainText )
XStream.stderr().messageWritten.connect( self._console.insertPlainText )
self.xlogger = XLogger('analyzer')
self._button.clicked.connect(self.test)
def test( self ):
# log some stuff
self.xlogger.debug("Testing debug")
self.xlogger.info('Testing info')
self.xlogger.warning('Testing warning')
self.xlogger.error('Testing error')
if ( __name__ == '__main__' ):
fileConfig(''.join([getcwd(),'/logging.conf']))
app = None
if ( not QApplication.instance() ):
app = QApplication([])
dlg = MyDialog()
dlg.show()
The "cross logger" sends everything to the log and to the Qwidget, however it also sends everything but debug to the aptana console:
02/05/2013 17:38:42 - analyzer - INFO | Testing info
02/05/2013 17:38:42 - analyzer - WARNING | Testing warning
02/05/2013 17:38:42 - analyzer - ERROR | Testing error
While the analyzer.log has:
02/05/2013 17:38:42 - DEBUG | Testing debug
02/05/2013 17:38:42 - INFO | Testing info
02/05/2013 17:38:42 - WARNING | Testing warning
02/05/2013 17:38:42 - ERROR | Testing error
Weird that debug is the only one who doesn't make it to the Aptana console, removing consoleHandler from handlers under [logger_analyzer] in my logging.conf solves the problem of it writing out to the Aptana console, probably has something to do with the args=(sys.stdout,) under [handler_consoleHandler]. I suppose it solves my problem without having to code in a handler for the Qtext object and thus negate the logging.conf file. If someone has a more elegant solution to using a logging.conf file that somehow manages to redirect its console output to a Qwidget of your choice, please feel free to post. Thanks.

Resources