commit
9fa9572987
4 changed files with 401 additions and 0 deletions
@ -0,0 +1,5 @@ |
||||
* |
||||
!app.py |
||||
!requirements.txt |
||||
!create_credentials.py |
||||
!.gitignore |
||||
@ -0,0 +1,263 @@ |
||||
import streamlit as st |
||||
import pandas as pd |
||||
from sqlalchemy import create_engine |
||||
import streamlit_authenticator as stauth |
||||
import yaml |
||||
from yaml.loader import SafeLoader |
||||
|
||||
class Params(): |
||||
def __init__(self, d): |
||||
|
||||
for key in d: |
||||
setattr(self, key, d[key]) |
||||
|
||||
for attr in ['q', 'category']: |
||||
if attr not in d: |
||||
setattr(self, attr, '') |
||||
|
||||
self.set_params() |
||||
|
||||
def set_params(self): |
||||
st.experimental_set_query_params(q=self.q, category=self.category) |
||||
|
||||
def update(self, param, value): |
||||
setattr(self, param, value) |
||||
self.set_params() |
||||
|
||||
def reset_q(): |
||||
st.experimental_set_query_params(q='') |
||||
|
||||
def download(df): |
||||
|
||||
st.download_button( |
||||
"CSV", |
||||
df.to_csv(index=False, sep=';').encode('utf-8'), |
||||
"file.csv", |
||||
"text/csv", |
||||
key='download-csv') |
||||
|
||||
|
||||
def define_search_terms(user_input): |
||||
""" Takes user input and make them into search terms for SQL. |
||||
|
||||
Args: |
||||
user_input (str): The string resulting from user input (input()). |
||||
|
||||
Returns: |
||||
list: List of search terms. |
||||
""" |
||||
# Search for quated phrases. |
||||
search_terms = [] |
||||
while '"' in user_input: |
||||
q1 = user_input.find('"') |
||||
q2 = user_input.find('"', q1 + 1) |
||||
quoted_term = user_input[q1 + 1 : q2] |
||||
search_terms.append(quoted_term.lower()) |
||||
user_input = user_input.replace(f'"{quoted_term}"', "") |
||||
while " " in user_input: |
||||
user_input = user_input.replace( |
||||
" ", " " |
||||
).strip() # Remove double and trailing blanks. |
||||
|
||||
# Add non-quoted terms. |
||||
if len(user_input) > 0: |
||||
search_terms += [i.lower() for i in user_input.strip().split(" ")] |
||||
return search_terms |
||||
|
||||
def create_sql_query(search_terms, table): |
||||
"""Returns a valid sql query.""" |
||||
word_list = [] |
||||
select_columns = 'body, "to" as m2, "from" as m1, senddate_str' |
||||
return_limit = 1000 |
||||
for word in search_terms: |
||||
|
||||
if "*" not in word: #Searching for the exact word. |
||||
word_list.append(f" {word} ") |
||||
|
||||
else: |
||||
if word[0] == "*" and word[-1] == "*": |
||||
word_list.append(word.replace("*", "")) |
||||
elif word[0] == "*": |
||||
word_list.append(f"{word.replace('*', '')} ") |
||||
elif word[-1] == "*": |
||||
word_list.append(f" {word.replace('*', '')}") |
||||
|
||||
# Format for SQL. |
||||
search_list = [f"'%%{i}%%'" for i in word_list] |
||||
|
||||
n = 0 |
||||
for i in search_list: |
||||
if " or " in i: |
||||
search_list[n] = "OR" |
||||
n += 1 |
||||
|
||||
# Handle searches with OR. |
||||
or_terms = [] |
||||
while "OR" in search_list: |
||||
n_or = search_list.count("OR") |
||||
or_terms.append(search_list.pop(search_list.index("OR") - 1)) |
||||
if n_or == 1: |
||||
or_terms.append(search_list.pop(search_list.index("OR") + 1)) |
||||
search_list.remove("OR") |
||||
or_sql = f"( body_lower LIKE {' OR body_lower LIKE '.join(or_terms)})" |
||||
|
||||
# Handle searches with -. |
||||
not_terms = [] |
||||
for term in search_list: |
||||
if "-" in term: # TODO Make this not include words with hyphen. |
||||
not_terms.append(search_list.pop(search_list.index(term)).replace("-", "")) |
||||
|
||||
# Create SQL query. |
||||
search_sql = '' |
||||
if search_list != []: |
||||
search_sql = f'(body_lower LIKE {" AND body_lower LIKE ".join(search_list)}) ' |
||||
|
||||
if or_terms != []: |
||||
if search_sql == '': |
||||
search_sql = or_sql |
||||
else: |
||||
search_sql = search_sql + " AND " + or_sql |
||||
|
||||
if len(not_terms) > 0: |
||||
search_sql += ( |
||||
f' AND (body_lower NOT LIKE {" AND body_lower NOT LIKE ".join(not_terms)})' |
||||
) |
||||
|
||||
sql = f"SELECT {select_columns} FROM {table} WHERE {search_sql} LIMIT {return_limit}" |
||||
|
||||
return sql |
||||
|
||||
def search_messages(search_for, engine, user=False): |
||||
|
||||
if user: # Search for all messages from/to a single user. |
||||
select_columns = 'body, "to" as m2, "from" as m1, senddate_str' |
||||
sql = f'select {select_columns} from messages where "to" == "{search_for}" or "from" == "{search_for}"' |
||||
|
||||
else: # Search for keywords. |
||||
sql = create_sql_query(define_search_terms(search_for), 'messages') |
||||
|
||||
# Get data from db. |
||||
df = pd.read_sql(sql, engine) |
||||
download(df) |
||||
|
||||
st.write(f'Träffar: {len(df)}') |
||||
|
||||
talkers = list(set(df['m1'].tolist() + df['m2'].tolist())) |
||||
if 'admin' in talkers: # Remove admin from conversations. |
||||
talkers.remove('admin') |
||||
conversations = {} |
||||
|
||||
for talker in talkers: |
||||
df_ = df.query(f'm1=="{talker}" | m2=="{talker}" ').copy() |
||||
others = list(set(df_['m1'].tolist() + df_['m2'].tolist())) |
||||
others.remove(talker) |
||||
|
||||
for other in others: |
||||
parts_list = [str(talker), str(other)] |
||||
parts_list.sort() |
||||
parts = '_'.join(parts_list) |
||||
|
||||
if parts not in conversations: |
||||
df_ = df_.query(f'm1=="{other}" | m2=="{other}" ').copy() |
||||
df_.sort_values('senddate_str', inplace=True) |
||||
conversations[parts] = {'df': df_[['m1', 'm2', 'body', 'senddate_str']], 'parts': parts_list} |
||||
|
||||
for _, data in conversations.items(): |
||||
st.write(' - '.join(data['parts'])) |
||||
df = data['df'] |
||||
df.rename({'m1': 'från', 'm2':'till', 'body':'meddelande', 'senddate_str':'datum'}, inplace=True, axis=1) |
||||
st.dataframe(df, hide_index=True) |
||||
|
||||
def search_user(search_for, engine): |
||||
|
||||
search_for = search_for.lower() |
||||
|
||||
select = 'member_id, email, username' |
||||
if '@' in search_for: |
||||
search_columns = 'email_lower' |
||||
else: |
||||
search_columns = 'username_lower' |
||||
|
||||
df = pd.read_sql(f'select {select} from members_expanded where {search_columns} == "{search_for}"', engine) |
||||
|
||||
st.dataframe(df, use_container_width=True) |
||||
|
||||
params = st.experimental_get_query_params() |
||||
# if 'user' in params: |
||||
if df.shape[0] == 1: |
||||
search_messages(df.username[0], engine, user=True) |
||||
|
||||
|
||||
def main(): |
||||
engine = create_engine('sqlite:///db.sqlite') |
||||
|
||||
#global params |
||||
params = Params(st.experimental_get_query_params()) |
||||
|
||||
categories = ['Meddelanden', 'Användare'] |
||||
if params.category != '': |
||||
index_category = categories.index(params.category[0]) |
||||
else: |
||||
index_category = 0 |
||||
search_category = st.selectbox('Vad vill du söka efter?', categories, index_category, on_change=reset_q) |
||||
params.update('category', search_category) |
||||
|
||||
if params.q != '': |
||||
placeholder=params.q[0] |
||||
else: |
||||
placeholder = 'Skriv här' |
||||
|
||||
search_for = st.text_input('Vad vill du söka efter?', placeholder=placeholder) |
||||
|
||||
if search_for == '' and params.q != '': |
||||
search_for = params.q[0] |
||||
|
||||
if search_for != '': |
||||
params.update('q', search_for) |
||||
search_for.replace('å', '?').replace('ä', '?').replace('ö', '?') |
||||
|
||||
#* Sök meddelanden |
||||
if search_category == 'Meddelanden': |
||||
search_messages(search_for, engine) |
||||
|
||||
# m1, m2, body, senddate_str = st.columns([1, 1, 5, 1]) |
||||
# for row in data['df'].iterrows(): |
||||
# row = row[1] |
||||
# with m1: |
||||
# row['m1'] |
||||
# with m2: |
||||
# row['m2'] |
||||
# with body: |
||||
# row['body'] |
||||
# with senddate_str: |
||||
# row['senddate_str'] |
||||
|
||||
|
||||
#* Sök användare |
||||
elif search_category == 'Användare': |
||||
search_user(search_for, engine) |
||||
|
||||
#main() |
||||
with open('credentials.yaml') as file: |
||||
config = yaml.load(file, Loader=SafeLoader) |
||||
|
||||
authenticator = stauth.Authenticate( |
||||
config['credentials'], |
||||
config['cookie']['name'], |
||||
config['cookie']['key'], |
||||
config['cookie']['expiry_days'], |
||||
config['preauthorized'] |
||||
) |
||||
|
||||
name, authentication_status, username = authenticator.login('Login', 'main') |
||||
|
||||
if authentication_status: |
||||
main() |
||||
|
||||
elif authentication_status is False: |
||||
st.error('Username/password is incorrect') |
||||
elif authentication_status is None: |
||||
st.warning('Please enter your username and password') |
||||
|
||||
|
||||
|
||||
@ -0,0 +1,76 @@ |
||||
import streamlit_authenticator as stauth |
||||
from sys import argv |
||||
import yaml |
||||
|
||||
def load_credentials(): |
||||
with open('credentials.yaml','r') as f: |
||||
return yaml.safe_load(f) |
||||
|
||||
def credentials(username, name, pwd): |
||||
pwd = stauth.Hasher([pwd]).generate()[0] |
||||
if username in list(load_credentials()['credentials']['usernames'].keys()): |
||||
print('Användarnamnet finns redan.') |
||||
return 'Användarnamnet finns redan.', False |
||||
credentials = { |
||||
'text': f''' |
||||
{username}: |
||||
email: '', |
||||
name: {name} |
||||
password: '***' |
||||
''', |
||||
'data': {username: |
||||
{'email': '', |
||||
'name': name, |
||||
'password': pwd} |
||||
} |
||||
} |
||||
for k, v in credentials.items(): |
||||
print(k, v) |
||||
return credentials, True |
||||
|
||||
def update(c, print_out=True, update=False): |
||||
if print_out: |
||||
print(c['text']) |
||||
if not update: |
||||
if input('Update credentials file? (y/n)') in ['y', 'yes']: |
||||
update = True |
||||
if update: |
||||
try: |
||||
cur_yaml = load_credentials() |
||||
cur_yaml['credentials']['usernames'].update(c['data']) |
||||
|
||||
if cur_yaml: |
||||
with open('credentials.yaml','w') as f: |
||||
yaml.safe_dump(cur_yaml, f) |
||||
|
||||
except FileNotFoundError: |
||||
print('Found no yaml file') |
||||
|
||||
return c['text'], False |
||||
|
||||
if __name__ == '__main__': |
||||
if len(argv) == 2: |
||||
if argv[1] == 'help': |
||||
print('username, name, pwd') |
||||
exit() |
||||
pwd = argv[1] |
||||
print(stauth.Hasher([pwd]).generate()[0]) |
||||
|
||||
elif len(argv) == 4: |
||||
username = argv[1] |
||||
name = argv[2] |
||||
pwd = argv[3] |
||||
c = credentials(username, name, pwd) |
||||
update(c) |
||||
|
||||
else: |
||||
pwd = input('Password: ').strip() |
||||
username = input('Username: ') |
||||
if username != '': |
||||
name = input('Name: ') |
||||
c = credentials(username, name, pwd) |
||||
update(c[0]) |
||||
else: |
||||
print(stauth.Hasher([pwd]).generate()[0]) |
||||
|
||||
|
||||
@ -0,0 +1,57 @@ |
||||
altair==5.0.1 |
||||
attrs==23.1.0 |
||||
backports.zoneinfo==0.2.1 |
||||
bcrypt==4.0.1 |
||||
blinker==1.6.2 |
||||
cachetools==5.3.1 |
||||
certifi==2023.5.7 |
||||
charset-normalizer==3.2.0 |
||||
click==8.1.5 |
||||
decorator==5.1.1 |
||||
extra-streamlit-components==0.1.56 |
||||
gitdb==4.0.10 |
||||
GitPython==3.1.32 |
||||
greenlet==2.0.2 |
||||
idna==3.4 |
||||
importlib-metadata==6.8.0 |
||||
importlib-resources==6.0.0 |
||||
Jinja2==3.1.2 |
||||
jsonschema==4.18.4 |
||||
jsonschema-specifications==2023.6.1 |
||||
markdown-it-py==3.0.0 |
||||
MarkupSafe==2.1.3 |
||||
mdurl==0.1.2 |
||||
numpy==1.24.4 |
||||
packaging==23.1 |
||||
pandas==2.0.3 |
||||
Pillow==9.5.0 |
||||
pkgutil_resolve_name==1.3.10 |
||||
protobuf==4.23.4 |
||||
pyarrow==12.0.1 |
||||
pydeck==0.8.1b0 |
||||
Pygments==2.15.1 |
||||
PyJWT==2.7.0 |
||||
Pympler==1.0.1 |
||||
python-dateutil==2.8.2 |
||||
pytz==2023.3 |
||||
pytz-deprecation-shim==0.1.0.post0 |
||||
PyYAML==6.0.1 |
||||
referencing==0.29.3 |
||||
requests==2.31.0 |
||||
rich==13.4.2 |
||||
rpds-py==0.9.2 |
||||
six==1.16.0 |
||||
smmap==5.0.0 |
||||
SQLAlchemy==2.0.19 |
||||
streamlit==1.24.1 |
||||
streamlit-authenticator==0.2.2 |
||||
tenacity==8.2.2 |
||||
toml==0.10.2 |
||||
toolz==0.12.0 |
||||
tornado==6.3.2 |
||||
typing_extensions==4.7.1 |
||||
tzdata==2023.3 |
||||
tzlocal==4.3.1 |
||||
urllib3==2.0.3 |
||||
validators==0.20.0 |
||||
zipp==3.16.2 |
||||
Loading…
Reference in new issue