Support all time resolutions on all counter views
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
[server]
|
[server]
|
||||||
port = 8501
|
port = 8501
|
||||||
address = "0.0.0.0"
|
address = "0.0.0.0"
|
||||||
enableStaticServing = true
|
enableStaticServing = false
|
||||||
|
|
||||||
[browser]
|
[browser]
|
||||||
gatherUsageStats = false
|
gatherUsageStats = false
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ VOLUME /app/data
|
|||||||
|
|
||||||
RUN touch .streamlit/secrets.toml \
|
RUN touch .streamlit/secrets.toml \
|
||||||
&& toml add_section --toml-path='.streamlit/secrets.toml' 'connections.sqlite' \
|
&& toml add_section --toml-path='.streamlit/secrets.toml' 'connections.sqlite' \
|
||||||
&& toml set --toml-path='.streamlit/secrets.toml' 'connections.sqlite.type' 'sql' \
|
&& toml set --toml-path='.streamlit/secrets.toml' 'connections.sqlite.type' 'queries' \
|
||||||
&& toml set --toml-path='.streamlit/secrets.toml' 'connections.sqlite.url' 'sqlite:///data/daily-counter.db'
|
&& toml set --toml-path='.streamlit/secrets.toml' 'connections.sqlite.url' 'sqlite:///data/daily-counter.db'
|
||||||
|
|
||||||
HEALTHCHECK --interval=60s --retries=5 CMD wget -qO- http://127.0.0.1:8501/_stcore/health || exit 1
|
HEALTHCHECK --interval=60s --retries=5 CMD wget -qO- http://127.0.0.1:8501/_stcore/health || exit 1
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import streamlit as st
|
import streamlit as st
|
||||||
|
|
||||||
import sql
|
from queries import crud, daily_stats, weekly_stats, monthly_stats, yearly_stats
|
||||||
from enums import CounterType
|
from enums import CounterType
|
||||||
|
|
||||||
@st.dialog("Add New Counter", icon=":material/add_box:")
|
@st.dialog("Add New Counter", icon=":material/add_box:")
|
||||||
def _add_counter():
|
def _add_counter():
|
||||||
colors = sql.get_colors(1)
|
colors = crud.get_colors(1)
|
||||||
with st.form(key="add_counter", border=False, clear_on_submit=True):
|
with st.form(key="add_counter", border=False, clear_on_submit=True):
|
||||||
title = st.text_input("Title:")
|
title = st.text_input("Title:")
|
||||||
counter_type_name = st.selectbox("Type", options=[e.name for e in CounterType])
|
counter_type_name = st.selectbox("Type", options=[e.name for e in CounterType])
|
||||||
@@ -16,7 +16,7 @@ def _add_counter():
|
|||||||
format_func=lambda c: f"#{c}")
|
format_func=lambda c: f"#{c}")
|
||||||
with st.container(horizontal=True, width="stretch", horizontal_alignment="center"):
|
with st.container(horizontal=True, width="stretch", horizontal_alignment="center"):
|
||||||
if st.form_submit_button(label="Create", icon=":material/save:"):
|
if st.form_submit_button(label="Create", icon=":material/save:"):
|
||||||
sql.create_counter(title, CounterType[counter_type_name], color)
|
crud.create_counter(title, CounterType[counter_type_name], color)
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
|
|
||||||
@@ -26,10 +26,10 @@ def _remove_counter(counter_id:int):
|
|||||||
st.subheader("Are you sure?")
|
st.subheader("Are you sure?")
|
||||||
with st.container(horizontal=True, width="stretch", horizontal_alignment="center"):
|
with st.container(horizontal=True, width="stretch", horizontal_alignment="center"):
|
||||||
if st.form_submit_button("Confirm", icon=":material/delete:"):
|
if st.form_submit_button("Confirm", icon=":material/delete:"):
|
||||||
sql.remove_counter(counter_id)
|
crud.remove_counter(counter_id)
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
df = sql.get_counters()
|
df = crud.get_counters()
|
||||||
|
|
||||||
with st.container(key="counter-table"):
|
with st.container(key="counter-table"):
|
||||||
for counter_id, name, counter_type_str, color in zip(df['id'], df['name'], df['type'], df['color']):
|
for counter_id, name, counter_type_str, color in zip(df['id'], df['name'], df['type'], df['color']):
|
||||||
@@ -38,7 +38,7 @@ with st.container(key="counter-table"):
|
|||||||
st.header(f":material/calendar_clock: {name}", width="stretch")
|
st.header(f":material/calendar_clock: {name}", width="stretch")
|
||||||
|
|
||||||
if st.button("", icon=":material/exposure_plus_1:", key=f"increment_counter_{counter_id}"):
|
if st.button("", icon=":material/exposure_plus_1:", key=f"increment_counter_{counter_id}"):
|
||||||
sql.increment_counter(counter_id)
|
crud.increment_counter(counter_id)
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
if st.button("", icon=":material/delete_forever:", key=f"remove_counter_{counter_id}"):
|
if st.button("", icon=":material/delete_forever:", key=f"remove_counter_{counter_id}"):
|
||||||
@@ -51,22 +51,22 @@ with st.container(key="counter-table"):
|
|||||||
stats_prev_unit = counter_type.previous_unit_text()
|
stats_prev_unit = counter_type.previous_unit_text()
|
||||||
match counter_type:
|
match counter_type:
|
||||||
case CounterType.DAILY.value | CounterType.SIMPLE.value:
|
case CounterType.DAILY.value | CounterType.SIMPLE.value:
|
||||||
stats = sql.get_daily_analytics(counter_id)
|
stats = daily_stats.get_daily_analytics(counter_id)
|
||||||
stats_current = stats.iloc[0]["count"]
|
stats_current = stats.iloc[0]["count"]
|
||||||
stats_prev = stats.iloc[1]["count"]
|
stats_prev = stats.iloc[1]["count"]
|
||||||
|
|
||||||
case CounterType.WEEKLY.value:
|
case CounterType.WEEKLY.value:
|
||||||
stats = sql.get_weekly_analytics(counter_id)
|
stats = weekly_stats.get_weekly_analytics(counter_id)
|
||||||
stats_current = stats.iloc[0]["count"]
|
stats_current = stats.iloc[0]["count"]
|
||||||
stats_prev = stats.iloc[1]["count"]
|
stats_prev = stats.iloc[1]["count"]
|
||||||
|
|
||||||
case CounterType.MONTHLY.value:
|
case CounterType.MONTHLY.value:
|
||||||
stats = sql.get_monthly_analytics(counter_id)
|
stats = monthly_stats.get_monthly_analytics(counter_id)
|
||||||
stats_current = stats.iloc[-1]["count"]
|
stats_current = stats.iloc[-1]["count"]
|
||||||
stats_prev = stats.iloc[-2]["count"]
|
stats_prev = stats.iloc[-2]["count"]
|
||||||
|
|
||||||
case CounterType.YEARLY.value:
|
case CounterType.YEARLY.value:
|
||||||
stats = sql.get_yearly_analytics(counter_id)
|
stats = yearly_stats.get_yearly_analytics(counter_id)
|
||||||
stats_current = stats.iloc[-1]["count"]
|
stats_current = stats.iloc[-1]["count"]
|
||||||
stats_prev = stats.iloc[-2]["count"]
|
stats_prev = stats.iloc[-2]["count"]
|
||||||
|
|
||||||
|
|||||||
@@ -1,44 +1,89 @@
|
|||||||
|
import enum
|
||||||
import logging
|
import logging
|
||||||
import streamlit as st
|
import streamlit as st
|
||||||
import json
|
import json
|
||||||
import sql
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from enums import CounterType
|
from enums import CounterType
|
||||||
|
from enum import StrEnum
|
||||||
|
from queries import crud, daily_stats, weekly_stats, monthly_stats, yearly_stats
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
counter_type_names = ([e.name for e in CounterType])
|
||||||
|
options = counter_type_names
|
||||||
|
options.remove(CounterType.SIMPLE.name)
|
||||||
|
|
||||||
if "counter_id" in st.query_params.keys():
|
if "counter_id" in st.query_params.keys():
|
||||||
|
|
||||||
|
'''
|
||||||
|
Show specific Counter analytics where the counter id is passed as query parameter "counter_id".
|
||||||
|
'''
|
||||||
|
|
||||||
counter_id = int(st.query_params["counter_id"])
|
counter_id = int(st.query_params["counter_id"])
|
||||||
df = sql.get_counter(counter_id)
|
df = crud.get_counter(counter_id)
|
||||||
|
|
||||||
st.header('Counter: ' + df['name'])
|
counter_type_id = df['type'] - 1
|
||||||
|
counter_type = [e for e in CounterType][counter_type_id]
|
||||||
|
counter_color ='#' + df['color']
|
||||||
|
|
||||||
|
with st.container(horizontal_alignment="right", vertical_alignment="bottom", horizontal=True):
|
||||||
|
st.header('Counter: ' + df['name'])
|
||||||
|
selection = st.segmented_control("Time Range", options, selection_mode="single", required=True, default=counter_type.name, label_visibility="hidden")
|
||||||
|
|
||||||
|
match getattr(CounterType, selection):
|
||||||
|
case CounterType.DAILY:
|
||||||
|
st.bar_chart(daily_stats.get_daily_analytics(counter_id), x="date", y="count", color=counter_color)
|
||||||
|
case CounterType.WEEKLY:
|
||||||
|
st.bar_chart(weekly_stats.get_weekly_analytics(counter_id), x="week", y="count", color=counter_color)
|
||||||
|
case CounterType.MONTHLY:
|
||||||
|
st.bar_chart(monthly_stats.get_monthly_analytics(counter_id), x="month", y="count", color=counter_color)
|
||||||
|
case CounterType.YEARLY:
|
||||||
|
st.bar_chart(yearly_stats.get_yearly_analytics(counter_id), x="year", y="count", color=counter_color)
|
||||||
|
case _:
|
||||||
|
logger.error(f"Unknown selection: {selection}")
|
||||||
|
|
||||||
color ='#' + df['color']
|
|
||||||
match df['type']:
|
|
||||||
case CounterType.DAILY.value | CounterType.SIMPLE.value:
|
|
||||||
st.bar_chart(sql.get_daily_analytics(int(df['id'])), x="date", y="count", color=color)
|
|
||||||
case CounterType.WEEKLY.value:
|
|
||||||
st.bar_chart(sql.get_weekly_analytics(int(df['id'])), x="week", y="count", color=color)
|
|
||||||
case CounterType.MONTHLY.value:
|
|
||||||
st.bar_chart(sql.get_monthly_analytics(int(df['id'])), x="month", y="count", color=color)
|
|
||||||
case CounterType.YEARLY.value:
|
|
||||||
st.bar_chart(sql.get_yearly_analytics(int(df['id'])), x="year", y="count", color=color)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
st.header("Statistics")
|
'''
|
||||||
|
By default, if no counter id is passed then show all counters in a a stacked graph
|
||||||
|
'''
|
||||||
|
with st.container(horizontal_alignment="right", vertical_alignment="bottom", horizontal=True):
|
||||||
|
st.header("Statistics")
|
||||||
|
selection = st.segmented_control("Time range", options, selection_mode="single", default=f"{CounterType.DAILY.name}", required=True, label_visibility="hidden")
|
||||||
|
|
||||||
|
selectedRange = getattr(CounterType, selection)
|
||||||
|
match getattr(CounterType, selection):
|
||||||
|
case CounterType.DAILY:
|
||||||
|
unit = 'date'
|
||||||
|
unit_label = 'Date'
|
||||||
|
entries = daily_stats.get_all_daily_analytics()
|
||||||
|
case CounterType.WEEKLY:
|
||||||
|
unit = 'week'
|
||||||
|
unit_label ='Week'
|
||||||
|
entries = weekly_stats.get_all_weekly_analytics()
|
||||||
|
case CounterType.MONTHLY:
|
||||||
|
unit = 'month'
|
||||||
|
unit_label = 'Month'
|
||||||
|
entries = monthly_stats.get_all_monthly_analytics()
|
||||||
|
case CounterType.YEARLY:
|
||||||
|
unit = 'year'
|
||||||
|
unit_label = 'Year'
|
||||||
|
entries = yearly_stats.get_all_yearly_analytics()
|
||||||
|
case _:
|
||||||
|
logger.error(f"Unknown selection: {selection}")
|
||||||
|
|
||||||
entries = sql.get_analytics()
|
|
||||||
entries_norm = pd.json_normalize(entries.counters.apply(json.loads)).fillna(0)
|
entries_norm = pd.json_normalize(entries.counters.apply(json.loads)).fillna(0)
|
||||||
entries_full = pd.concat([entries, entries_norm], axis=1).drop(['counters'], axis=1)
|
entries_full = pd.concat([entries, entries_norm], axis=1).drop(['counters'], axis=1)
|
||||||
|
|
||||||
selected_counters = [c for c in entries_full.columns if c != "date"]
|
selected_counters = [c for c in entries_full.columns if c != unit]
|
||||||
all_counters = sql.get_counters()
|
all_counters = crud.get_counters()
|
||||||
|
|
||||||
colors = all_counters.loc[all_counters['name'].isin(selected_counters), ["name", "color"]]
|
colors = all_counters.loc[all_counters['name'].isin(selected_counters), ["name", "color"]]
|
||||||
colors.name = colors.name.astype("category")
|
colors.name = colors.name.astype("category")
|
||||||
colors.name = colors.name.cat.set_categories(selected_counters)
|
colors.name = colors.name.cat.set_categories(selected_counters)
|
||||||
colors = colors.sort_values(["name"])
|
colors = colors.sort_values(["name"])
|
||||||
colors = colors.color.apply(lambda c: "#" + c).tolist()
|
colors = colors.color.apply(lambda c: "#" + c).tolist()
|
||||||
|
|
||||||
st.bar_chart(entries_full, x="date", x_label="Date", y_label="Count", color=colors)
|
st.bar_chart(entries_full, x=unit, x_label=unit_label, y_label="Count", color=colors)
|
||||||
|
|||||||
9
app/queries/connection.py
Normal file
9
app/queries/connection.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
|
||||||
|
import streamlit as st
|
||||||
|
from sqlalchemy.sql import text
|
||||||
|
from streamlit.connections import BaseConnection
|
||||||
|
|
||||||
|
connection: BaseConnection = st.connection("sqlite")
|
||||||
|
|
||||||
|
with connection.session as configure_session:
|
||||||
|
configure_session.execute(text('PRAGMA foreign_keys=ON'))
|
||||||
64
app/queries/crud.py
Normal file
64
app/queries/crud.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import logging
|
||||||
|
import streamlit as st
|
||||||
|
from sqlalchemy.sql import text
|
||||||
|
from queries.connection import connection
|
||||||
|
from enums import CounterType
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def create_counter(title:str, counter_type:CounterType, counter_color) -> None:
|
||||||
|
logger.info("Adding counter %s", counter_type)
|
||||||
|
with connection.session as session:
|
||||||
|
try:
|
||||||
|
query = text('INSERT INTO counters (name, type, color) VALUES (:title, :type, :color)')
|
||||||
|
session.execute(query, {'title': title, 'type': counter_type, 'color': counter_color})
|
||||||
|
session.commit()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
session.rollback()
|
||||||
|
|
||||||
|
def get_counters():
|
||||||
|
try:
|
||||||
|
return connection.query('SELECT id, name, type, color FROM counters', ttl=0)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
return st.dataframe()
|
||||||
|
|
||||||
|
def increment_counter(counter_id:int) -> None:
|
||||||
|
logger.info("Incrementing counter %s", counter_id)
|
||||||
|
with connection.session as session:
|
||||||
|
try:
|
||||||
|
query = text('INSERT INTO entries (counter_id) VALUES (:id)')
|
||||||
|
session.execute(query, {'id': counter_id})
|
||||||
|
session.commit()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
session.rollback()
|
||||||
|
|
||||||
|
|
||||||
|
def remove_counter(counter_id:int) -> None:
|
||||||
|
logger.info("Removing counter %s", counter_id)
|
||||||
|
with connection.session as session:
|
||||||
|
try:
|
||||||
|
query = text('DELETE FROM counters WHERE id = :id')
|
||||||
|
session.execute(query, {'id': counter_id})
|
||||||
|
session.commit()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
session.rollback()
|
||||||
|
|
||||||
|
|
||||||
|
def get_counter(counter_id:int):
|
||||||
|
try:
|
||||||
|
return connection.query('SELECT * FROM counters WHERE id = :id', params={'id': counter_id}, ttl=0).iloc[0]
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_colors(palette_id:int):
|
||||||
|
try:
|
||||||
|
return connection.query('''SELECT color1,color2,color3,color4,color5 FROM color_palettes WHERE id = :id''', params={'id': palette_id})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
return None
|
||||||
66
app/queries/daily_stats.py
Normal file
66
app/queries/daily_stats.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import logging
|
||||||
|
from queries.connection import connection
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def get_all_daily_analytics(end_date:str = 'now'):
|
||||||
|
try:
|
||||||
|
return connection.query('''
|
||||||
|
WITH RECURSIVE timeseries(d) AS (
|
||||||
|
VALUES(date(:end_date))
|
||||||
|
UNION ALL
|
||||||
|
SELECT date(d, '-1 day') as d
|
||||||
|
FROM timeseries
|
||||||
|
WHERE d > date(:end_date, '-30 days')
|
||||||
|
),
|
||||||
|
stats AS (
|
||||||
|
SELECT
|
||||||
|
date(timestamp) as d,
|
||||||
|
counter_id,
|
||||||
|
sum(increment) as count
|
||||||
|
FROM entries
|
||||||
|
group by counter_id, date(timestamp)
|
||||||
|
)
|
||||||
|
select
|
||||||
|
s.d as date,
|
||||||
|
case
|
||||||
|
when counter_id is null then json_object()
|
||||||
|
else json_group_object(name, count)
|
||||||
|
end as counters
|
||||||
|
FROM timeseries s
|
||||||
|
left outer join stats t on s.d = t.d
|
||||||
|
left join counters c on t.counter_id = c.id
|
||||||
|
GROUP by s.d
|
||||||
|
''', params={"end_date": end_date}, ttl=0)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_daily_analytics(counter_id:int, end_date:str = 'now'):
|
||||||
|
try:
|
||||||
|
return connection.query('''
|
||||||
|
WITH RECURSIVE timeseries(d) AS (
|
||||||
|
VALUES(date(:end_date))
|
||||||
|
UNION ALL
|
||||||
|
SELECT date(d, '-1 day') as d
|
||||||
|
FROM timeseries
|
||||||
|
WHERE d > date(:end_date, '-7 days')
|
||||||
|
),
|
||||||
|
stats AS (
|
||||||
|
SELECT
|
||||||
|
date(timestamp) as d,
|
||||||
|
sum(increment) as count
|
||||||
|
FROM entries
|
||||||
|
where counter_id = :id
|
||||||
|
group by date(timestamp)
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
t.d as "date",
|
||||||
|
coalesce(s.count, 0) as count
|
||||||
|
FROM timeseries as t
|
||||||
|
LEFT JOIN stats as s on s.d = t.d
|
||||||
|
''', params={'id': counter_id, "end_date": end_date}, ttl=0)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
return None
|
||||||
79
app/queries/monthly_stats.py
Normal file
79
app/queries/monthly_stats.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import logging
|
||||||
|
from queries.connection import connection
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def get_all_monthly_analytics(end_date:str = 'now'):
|
||||||
|
try:
|
||||||
|
return connection.query('''
|
||||||
|
WITH RECURSIVE timeseries(d) AS (
|
||||||
|
VALUES(date(:end_date,'start of year'))
|
||||||
|
UNION ALL
|
||||||
|
SELECT date(d, '+1 month') as d
|
||||||
|
FROM timeseries
|
||||||
|
WHERE d < date(:end_date, '-1 month')
|
||||||
|
),
|
||||||
|
months AS (
|
||||||
|
SELECT
|
||||||
|
strftime('%m',d) as m,
|
||||||
|
strftime('%Y',d) as y
|
||||||
|
FROM timeseries
|
||||||
|
),
|
||||||
|
stats AS (
|
||||||
|
SELECT
|
||||||
|
strftime('%m', timestamp) as m,
|
||||||
|
strftime('%Y', timestamp) as y,
|
||||||
|
counter_id,
|
||||||
|
sum(increment) as count
|
||||||
|
FROM entries
|
||||||
|
group by counter_id, strftime('%m', timestamp), strftime('%Y', timestamp)
|
||||||
|
)
|
||||||
|
select
|
||||||
|
concat(m.m,', ',m.y) as "month",
|
||||||
|
case
|
||||||
|
when counter_id is null then json_object()
|
||||||
|
else json_group_object(name, count)
|
||||||
|
end as counters
|
||||||
|
FROM months m
|
||||||
|
left outer join stats t on m.m = t.m and m.y = t.y
|
||||||
|
left join counters c on t.counter_id = c.id
|
||||||
|
GROUP by m.m, m.y
|
||||||
|
''', params={"end_date": end_date}, ttl=0)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_monthly_analytics(counter_id:int, end_date:str = 'now'):
|
||||||
|
try:
|
||||||
|
return connection.query('''
|
||||||
|
WITH RECURSIVE timeseries(d) AS (
|
||||||
|
VALUES( date(:end_date, 'start of year'))
|
||||||
|
UNION ALL
|
||||||
|
SELECT date(d, '+1 month')
|
||||||
|
FROM timeseries
|
||||||
|
WHERE d < date(:end_date, '-1 month')
|
||||||
|
),
|
||||||
|
months AS (
|
||||||
|
SELECT
|
||||||
|
strftime('%m',d) as m,
|
||||||
|
strftime('%Y',d) as y
|
||||||
|
FROM timeseries
|
||||||
|
),
|
||||||
|
stats AS (
|
||||||
|
SELECT
|
||||||
|
strftime('%m', timestamp) as m,
|
||||||
|
strftime('%Y', timestamp) as y,
|
||||||
|
sum(increment) as count
|
||||||
|
FROM entries
|
||||||
|
where counter_id = :id
|
||||||
|
group by strftime('%m', timestamp), strftime('%Y', timestamp)
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
concat(m.m,', ',m.y) as "month",
|
||||||
|
coalesce(s.count, 0) as count
|
||||||
|
FROM months as m
|
||||||
|
LEFT JOIN stats as s on s.m = m.m and s.y = m.y
|
||||||
|
''', params={'id': counter_id, "end_date": end_date}, ttl=0)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
return None
|
||||||
73
app/queries/weekly_stats.py
Normal file
73
app/queries/weekly_stats.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import logging
|
||||||
|
from queries.connection import connection
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def get_all_weekly_analytics(end_date:str = 'now'):
|
||||||
|
try:
|
||||||
|
return connection.query('''
|
||||||
|
WITH RECURSIVE timeseries(d) AS (
|
||||||
|
VALUES(date(:end_date, 'weekday 0'))
|
||||||
|
UNION ALL
|
||||||
|
SELECT date(d, '-7 day') as d
|
||||||
|
FROM timeseries
|
||||||
|
WHERE d > date(:end_date, '-30 days')
|
||||||
|
),
|
||||||
|
weeks AS (
|
||||||
|
SELECT strftime('%W',d) as w
|
||||||
|
FROM timeseries
|
||||||
|
),
|
||||||
|
stats AS (
|
||||||
|
SELECT
|
||||||
|
strftime('%W', timestamp) as w,
|
||||||
|
counter_id,
|
||||||
|
sum(increment) as count
|
||||||
|
FROM entries
|
||||||
|
group by counter_id, strftime('%W', timestamp)
|
||||||
|
)
|
||||||
|
select
|
||||||
|
s.w as week,
|
||||||
|
case
|
||||||
|
when counter_id is null then json_object()
|
||||||
|
else json_group_object(name, count)
|
||||||
|
end as counters
|
||||||
|
FROM weeks s
|
||||||
|
left outer join stats t on s.w = t.w
|
||||||
|
left join counters c on t.counter_id = c.id
|
||||||
|
GROUP by s.w
|
||||||
|
''', params={"end_date": end_date}, ttl=0)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_weekly_analytics(counter_id:int, end_date:str = 'now'):
|
||||||
|
try:
|
||||||
|
return connection.query('''
|
||||||
|
WITH RECURSIVE timeseries(d) AS (
|
||||||
|
VALUES(date(:end_date, 'weekday 0'))
|
||||||
|
UNION ALL
|
||||||
|
SELECT date(d, '-7 day')
|
||||||
|
FROM timeseries
|
||||||
|
WHERE d > date(:end_date, '-30 days')
|
||||||
|
),
|
||||||
|
weeks AS (
|
||||||
|
SELECT strftime('%W',d) as w
|
||||||
|
FROM timeseries
|
||||||
|
),
|
||||||
|
stats AS (
|
||||||
|
SELECT
|
||||||
|
strftime('%W', timestamp) as w,
|
||||||
|
sum(increment) as count
|
||||||
|
FROM entries
|
||||||
|
where counter_id = :id
|
||||||
|
group by strftime('%W', timestamp)
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
w.w as "week",
|
||||||
|
coalesce(s.count, 0) as count
|
||||||
|
FROM weeks as w
|
||||||
|
LEFT JOIN stats as s on s.w = w.w
|
||||||
|
''', params={'id': counter_id, "end_date": end_date}, ttl=0)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
return None
|
||||||
73
app/queries/yearly_stats.py
Normal file
73
app/queries/yearly_stats.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import logging
|
||||||
|
from queries.connection import connection
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def get_all_yearly_analytics(end_date:str = 'now'):
|
||||||
|
try:
|
||||||
|
return connection.query('''
|
||||||
|
WITH RECURSIVE timeseries(d) AS (
|
||||||
|
VALUES(date(:end_date,'start of year', '-4 years'))
|
||||||
|
UNION ALL
|
||||||
|
SELECT date(d, '+1 year') as d
|
||||||
|
FROM timeseries
|
||||||
|
WHERE d < date(:end_date, '-1 year')
|
||||||
|
),
|
||||||
|
years AS (
|
||||||
|
SELECT strftime('%Y',d) as y
|
||||||
|
FROM timeseries
|
||||||
|
),
|
||||||
|
stats AS (
|
||||||
|
SELECT
|
||||||
|
strftime('%Y', timestamp) as y,
|
||||||
|
counter_id,
|
||||||
|
sum(increment) as count
|
||||||
|
FROM entries
|
||||||
|
group by counter_id, strftime('%Y', timestamp)
|
||||||
|
)
|
||||||
|
select
|
||||||
|
y.y as "year",
|
||||||
|
case
|
||||||
|
when counter_id is null then json_object()
|
||||||
|
else json_group_object(name, count)
|
||||||
|
end as counters
|
||||||
|
FROM years y
|
||||||
|
left outer join stats t on y.y = t.y
|
||||||
|
left join counters c on t.counter_id = c.id
|
||||||
|
GROUP by y.y
|
||||||
|
''', params={"end_date": end_date}, ttl=0)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_yearly_analytics(counter_id:int, end_date:str = 'now'):
|
||||||
|
try:
|
||||||
|
return connection.query('''
|
||||||
|
WITH RECURSIVE timeseries(d) AS (
|
||||||
|
VALUES( date(:end_date, 'start of year', '-4 years'))
|
||||||
|
UNION ALL
|
||||||
|
SELECT date(d, '+1 year')
|
||||||
|
FROM timeseries
|
||||||
|
WHERE d < date(:end_date, '-1 year')
|
||||||
|
),
|
||||||
|
years AS (
|
||||||
|
SELECT strftime('%Y',d) as y
|
||||||
|
FROM timeseries
|
||||||
|
),
|
||||||
|
stats AS (
|
||||||
|
SELECT
|
||||||
|
strftime('%Y', timestamp) as y,
|
||||||
|
sum(increment) as count
|
||||||
|
FROM entries
|
||||||
|
where counter_id = :id
|
||||||
|
group by strftime('%Y', timestamp)
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
m.y as "year",
|
||||||
|
coalesce(s.count, 0) as count
|
||||||
|
FROM years as m
|
||||||
|
LEFT JOIN stats as s on s.y = m.y
|
||||||
|
''', params={'id': counter_id, "end_date": end_date}, ttl=0)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
return None
|
||||||
230
app/sql.py
230
app/sql.py
@@ -1,230 +0,0 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
import streamlit as st
|
|
||||||
|
|
||||||
from sqlalchemy.sql import text
|
|
||||||
from enums import CounterType
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
connection = st.connection("sqlite")
|
|
||||||
with connection.session as configure_session:
|
|
||||||
configure_session.execute(text('PRAGMA foreign_keys=ON'))
|
|
||||||
|
|
||||||
def create_counter(title:str, counter_type:CounterType, counter_color) -> None:
|
|
||||||
logger.info("Adding counter %s", counter_type)
|
|
||||||
with connection.session as session:
|
|
||||||
try:
|
|
||||||
query = text('INSERT INTO counters (name, type, color) VALUES (:title, :type, :color)')
|
|
||||||
session.execute(query, {'title': title, 'type': counter_type, 'color': counter_color})
|
|
||||||
session.commit()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(e)
|
|
||||||
session.rollback()
|
|
||||||
|
|
||||||
def get_counters():
|
|
||||||
try:
|
|
||||||
return connection.query('SELECT id, name, type, color FROM counters', ttl=0)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(e)
|
|
||||||
return st.dataframe()
|
|
||||||
|
|
||||||
def increment_counter(counter_id:int) -> None:
|
|
||||||
logger.info("Incrementing counter %s", counter_id)
|
|
||||||
with connection.session as session:
|
|
||||||
try:
|
|
||||||
query = text('INSERT INTO entries (counter_id) VALUES (:id)')
|
|
||||||
session.execute(query, {'id': counter_id})
|
|
||||||
session.commit()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(e)
|
|
||||||
session.rollback()
|
|
||||||
|
|
||||||
|
|
||||||
def remove_counter(counter_id:int) -> None:
|
|
||||||
logger.info("Removing counter %s", counter_id)
|
|
||||||
with connection.session as session:
|
|
||||||
try:
|
|
||||||
query = text('DELETE FROM counters WHERE id = :id')
|
|
||||||
session.execute(query, {'id': counter_id})
|
|
||||||
session.commit()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(e)
|
|
||||||
session.rollback()
|
|
||||||
|
|
||||||
|
|
||||||
def get_counter(counter_id:int):
|
|
||||||
try:
|
|
||||||
return connection.query('SELECT * FROM counters WHERE id = :id', params={'id': counter_id}, ttl=0).iloc[0]
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(e)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_analytics(end_date:str = 'now'):
|
|
||||||
try:
|
|
||||||
return connection.query('''
|
|
||||||
WITH RECURSIVE timeseries(d) AS (
|
|
||||||
VALUES(date(:end_date))
|
|
||||||
UNION ALL
|
|
||||||
SELECT date(d, '-1 day') as d
|
|
||||||
FROM timeseries
|
|
||||||
WHERE d > date(:end_date, '-30 days')
|
|
||||||
),
|
|
||||||
stats AS (
|
|
||||||
SELECT
|
|
||||||
date(timestamp) as d,
|
|
||||||
counter_id,
|
|
||||||
sum(increment) as count
|
|
||||||
FROM entries
|
|
||||||
group by counter_id, date(timestamp)
|
|
||||||
)
|
|
||||||
select
|
|
||||||
s.d as date,
|
|
||||||
case
|
|
||||||
when counter_id is null then json_object()
|
|
||||||
else json_group_object(name, count)
|
|
||||||
end as counters
|
|
||||||
FROM timeseries s
|
|
||||||
left outer join stats t on s.d = t.d
|
|
||||||
left join counters c on t.counter_id = c.id
|
|
||||||
GROUP by s.d
|
|
||||||
''', params={"end_date": end_date}, ttl=0)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(e)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_daily_analytics(counter_id:int, end_date:str = 'now'):
|
|
||||||
try:
|
|
||||||
return connection.query('''
|
|
||||||
WITH RECURSIVE timeseries(d) AS (
|
|
||||||
VALUES(date(:end_date))
|
|
||||||
UNION ALL
|
|
||||||
SELECT date(d, '-1 day') as d
|
|
||||||
FROM timeseries
|
|
||||||
WHERE d > date(:end_date, '-7 days')
|
|
||||||
),
|
|
||||||
stats AS (
|
|
||||||
SELECT
|
|
||||||
date(timestamp) as d,
|
|
||||||
sum(increment) as count
|
|
||||||
FROM entries
|
|
||||||
where counter_id = :id
|
|
||||||
group by date(timestamp)
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
t.d as "date",
|
|
||||||
coalesce(s.count, 0) as count
|
|
||||||
FROM timeseries as t
|
|
||||||
LEFT JOIN stats as s on s.d = t.d
|
|
||||||
''', params={'id': counter_id, "end_date": end_date}, ttl=0)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(e)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_weekly_analytics(counter_id:int, end_date:str = 'now'):
|
|
||||||
try:
|
|
||||||
return connection.query('''
|
|
||||||
WITH RECURSIVE timeseries(d) AS (
|
|
||||||
VALUES(date(:end_date, 'weekday 0'))
|
|
||||||
UNION ALL
|
|
||||||
SELECT date(d, '-7 day')
|
|
||||||
FROM timeseries
|
|
||||||
WHERE d > date(:end_date, '-30 days')
|
|
||||||
),
|
|
||||||
weeks AS (
|
|
||||||
SELECT strftime('%W',d) as w
|
|
||||||
FROM timeseries
|
|
||||||
),
|
|
||||||
stats AS (
|
|
||||||
SELECT
|
|
||||||
strftime('%W', timestamp) as w,
|
|
||||||
sum(increment) as count
|
|
||||||
FROM entries
|
|
||||||
where counter_id = :id
|
|
||||||
group by strftime('%W', timestamp)
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
w.w as "week",
|
|
||||||
coalesce(s.count, 0) as count
|
|
||||||
FROM weeks as w
|
|
||||||
LEFT JOIN stats as s on s.w = w.w
|
|
||||||
''', params={'id': counter_id, "end_date": end_date}, ttl=0)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(e)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_monthly_analytics(counter_id:int, end_date:str = 'now'):
|
|
||||||
try:
|
|
||||||
return connection.query('''
|
|
||||||
WITH RECURSIVE timeseries(d) AS (
|
|
||||||
VALUES( date(:end_date, 'start of year'))
|
|
||||||
UNION ALL
|
|
||||||
SELECT date(d, '+1 month')
|
|
||||||
FROM timeseries
|
|
||||||
WHERE d < date(:end_date, '-1 month')
|
|
||||||
),
|
|
||||||
months AS (
|
|
||||||
SELECT
|
|
||||||
strftime('%m',d) as m,
|
|
||||||
strftime('%Y',d) as y
|
|
||||||
FROM timeseries
|
|
||||||
),
|
|
||||||
stats AS (
|
|
||||||
SELECT
|
|
||||||
strftime('%m', timestamp) as m,
|
|
||||||
strftime('%Y', timestamp) as y,
|
|
||||||
sum(increment) as count
|
|
||||||
FROM entries
|
|
||||||
where counter_id = :id
|
|
||||||
group by strftime('%m', timestamp), strftime('%Y', timestamp)
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
concat(m.m,', ',m.y) as "month",
|
|
||||||
coalesce(s.count, 0) as count
|
|
||||||
FROM months as m
|
|
||||||
LEFT JOIN stats as s on s.m = m.m and s.y = m.y
|
|
||||||
''', params={'id': counter_id, "end_date": end_date}, ttl=0)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(e)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_yearly_analytics(counter_id:int, end_date:str = 'now'):
|
|
||||||
try:
|
|
||||||
return connection.query('''
|
|
||||||
WITH RECURSIVE timeseries(d) AS (
|
|
||||||
VALUES( date(:end_date, 'start of year', '-4 years'))
|
|
||||||
UNION ALL
|
|
||||||
SELECT date(d, '+1 year')
|
|
||||||
FROM timeseries
|
|
||||||
WHERE d < date(:end_date, '-1 year')
|
|
||||||
),
|
|
||||||
years AS (
|
|
||||||
SELECT strftime('%Y',d) as y
|
|
||||||
FROM timeseries
|
|
||||||
),
|
|
||||||
stats AS (
|
|
||||||
SELECT
|
|
||||||
strftime('%Y', timestamp) as y,
|
|
||||||
sum(increment) as count
|
|
||||||
FROM entries
|
|
||||||
where counter_id = :id
|
|
||||||
group by strftime('%Y', timestamp)
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
m.y as "year",
|
|
||||||
coalesce(s.count, 0) as count
|
|
||||||
FROM years as m
|
|
||||||
LEFT JOIN stats as s on s.y = m.y
|
|
||||||
''', params={'id': counter_id, "end_date": end_date}, ttl=0)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(e)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_colors(palette_id:int):
|
|
||||||
try:
|
|
||||||
return connection.query('''SELECT color1,color2,color3,color4,color5 FROM color_palettes WHERE id = :id''', params={'id': palette_id})
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(e)
|
|
||||||
return None
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import streamlit as st
|
import streamlit as st
|
||||||
import sql
|
import queries
|
||||||
|
from queries import crud
|
||||||
|
|
||||||
def _load_css(filepath):
|
def _load_css(filepath):
|
||||||
with open(filepath) as file:
|
with open(filepath) as file:
|
||||||
@@ -8,7 +8,7 @@ def _load_css(filepath):
|
|||||||
|
|
||||||
|
|
||||||
def _load_color_selector_styles():
|
def _load_color_selector_styles():
|
||||||
colors = sql.get_colors(1) #FIXME Change to use user profile color palette
|
colors = crud.get_colors(1) #FIXME Change to use user profile color palette
|
||||||
for idx, c in enumerate(colors.keys()):
|
for idx, c in enumerate(colors.keys()):
|
||||||
css_color = '#' + colors[c][0]
|
css_color = '#' + colors[c][0]
|
||||||
st.html(f"""
|
st.html(f"""
|
||||||
|
|||||||
Reference in New Issue
Block a user