├── .gitignore
├── streamlit
├── config.py
├── .streamlit
│ ├── .credentials.IGNORE.toml
│ └── config.IGNORE.toml
├── src
│ ├── style
│ │ ├── stringformats.py
│ │ └── charts.py
│ ├── pages
│ │ ├── misc.py
│ │ ├── goals.py
│ │ ├── new_tracker.py
│ │ └── entries.py
│ └── crud.py
├── Pipfile
├── Dockerfile
├── user
│ └── user.py
├── app.py
├── tracker
│ └── tracker.py
├── sessionstate.py
└── Pipfile.lock
├── api
├── alembic
│ ├── README
│ ├── script.py.mako
│ ├── versions
│ │ ├── a5ee42f3cea7_created_user_model.py
│ │ ├── aaa8141c13b6_import_user_model_metadata_into_alembic.py
│ │ ├── e9ac74c2fb4f_passing_user_model_thru_user_.py
│ │ ├── 09d5d3f71cf4_created_user_model_but_imported_into_.py
│ │ ├── 8d31408b2f13_fixed_metadata_bug_in_alembic_env_py_.py
│ │ ├── e74329689b23_first_commit.py
│ │ └── 2171019a778e_created_entries_db_model.py
│ └── env.py
├── .env.template
├── Dockerfile
├── user
│ ├── schema.py
│ ├── model.py
│ └── router.py
├── Pipfile
├── schema.py
├── tracker
│ ├── schema.py
│ ├── model.py
│ └── router.py
├── models.py
├── alembic.ini
├── main.py
└── Pipfile.lock
├── svelte
├── .gitignore
├── public
│ ├── favicon.png
│ ├── Dockerfile
│ ├── index.html
│ └── global.css
├── src
│ ├── main.js
│ └── App.svelte
├── Dockerfile
├── package.json
├── rollup.config.js
├── README.md
└── scripts
│ └── setupTypeScript.js
├── pgadmin4
└── .env.template
├── postgres
└── .env.template
├── nginx
├── index.html
├── Dockerfile
├── auth
│ └── README.md
└── conf
│ └── project.conf
├── dhparam
└── dhparam-2048.pem
├── docker-compose.yml
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | .htpasswd
--------------------------------------------------------------------------------
/streamlit/config.py:
--------------------------------------------------------------------------------
1 | API_URL = 'http://api:8000'
--------------------------------------------------------------------------------
/api/alembic/README:
--------------------------------------------------------------------------------
1 | Generic single-database configuration.
--------------------------------------------------------------------------------
/streamlit/.streamlit/.credentials.IGNORE.toml:
--------------------------------------------------------------------------------
1 | [general]
2 |
3 | email=""
--------------------------------------------------------------------------------
/svelte/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules/
2 | /public/build/
3 |
4 | .DS_Store
5 |
--------------------------------------------------------------------------------
/svelte/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sapped/flip/HEAD/svelte/public/favicon.png
--------------------------------------------------------------------------------
/pgadmin4/.env.template:
--------------------------------------------------------------------------------
1 | PGADMIN_DEFAULT_EMAIL=pgadmin4@pgadmin.org
2 | PGADMIN_DEFAULT_PASSWORD=admin
--------------------------------------------------------------------------------
/postgres/.env.template:
--------------------------------------------------------------------------------
1 | POSTGRES_USER=postgres
2 | POSTGRES_PASSWORD=postgres
3 | POSTGRES_DB=postgres
--------------------------------------------------------------------------------
/nginx/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | My First Heading
6 | My first paragraph.
7 |
8 |
9 |
--------------------------------------------------------------------------------
/streamlit/src/style/stringformats.py:
--------------------------------------------------------------------------------
1 | import datetime as dt
2 |
3 | def es_date_format(x):
4 | return dt.datetime.fromtimestamp(x).strftime('%b %e, %Y %r')
--------------------------------------------------------------------------------
/api/.env.template:
--------------------------------------------------------------------------------
1 | POSTGRES_USER=postgres
2 | POSTGRES_PASSWORD=postgres
3 | POSTGRES_DB=postgres
4 |
5 | DATABASE_URL = postgresql+psycopg2://{user}:{password}@{host}:{port}
--------------------------------------------------------------------------------
/nginx/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM nginx:mainline-alpine
2 |
3 | WORKDIR /nginx
4 | COPY . /nginx
5 |
6 | COPY /conf/project.conf /etc/nginx/conf.d/
7 | RUN rm /etc/nginx/conf.d/default.conf
--------------------------------------------------------------------------------
/svelte/public/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:latest
2 | WORKDIR /svelte
3 |
4 | COPY rollup.config.js ./
5 | COPY package*.json ./
6 | RUN npm i -g npm
7 | RUN npm install
8 |
9 | COPY . .
--------------------------------------------------------------------------------
/svelte/src/main.js:
--------------------------------------------------------------------------------
1 | import App from './App.svelte';
2 |
3 | const app = new App({
4 | target: document.body,
5 | props: {
6 | name: 'world'
7 | }
8 | });
9 |
10 | export default app;
--------------------------------------------------------------------------------
/svelte/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:latest
2 | WORKDIR /svelte
3 |
4 |
5 | # COPY . /svelte
6 |
7 | COPY rollup.config.js ./
8 | COPY package*.json ./
9 | RUN npm i -g npm
10 | RUN npm install
11 |
12 | COPY . .
--------------------------------------------------------------------------------
/api/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.9
2 |
3 | ENV PYTHONDONTWRITEBYTECODE 1
4 | ENV PYTHONUNBUFFERED 1
5 |
6 | WORKDIR /api/
7 |
8 | RUN pip install pipenv
9 | COPY Pipfile Pipfile.lock /api/
10 | RUN pipenv install --system --dev
11 |
12 | COPY . /api/
--------------------------------------------------------------------------------
/api/user/schema.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 | from pydantic import BaseModel
3 |
4 | class User(BaseModel):
5 | id: Optional[int]
6 | username: str
7 | created_at: Optional[float]
8 |
9 | class Config:
10 | orm_mode=True
--------------------------------------------------------------------------------
/streamlit/Pipfile:
--------------------------------------------------------------------------------
1 | [[source]]
2 | url = "https://pypi.org/simple"
3 | verify_ssl = true
4 | name = "pypi"
5 |
6 | [packages]
7 | streamlit = "*"
8 | pandas = "*"
9 | awesome-streamlit = "*"
10 | plotly = "*"
11 | plotly-express = "*"
12 |
13 | [dev-packages]
14 |
15 | [requires]
16 | python_version = "3.8"
17 |
--------------------------------------------------------------------------------
/api/Pipfile:
--------------------------------------------------------------------------------
1 | [[source]]
2 | url = "https://pypi.python.org/simple"
3 | verify_ssl = true
4 | name = "pypi"
5 |
6 | [packages]
7 | uvicorn = "*"
8 | fastapi = "*"
9 | fastapi-sqlalchemy = "*"
10 | pydantic = "*"
11 | alembic = "*"
12 | python-dotenv = "*"
13 | psycopg2-binary = "*"
14 |
15 | [dev-packages]
16 |
17 | [requires]
18 | python_version = "3.8"
19 |
--------------------------------------------------------------------------------
/streamlit/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.9
2 |
3 | WORKDIR /streamlit
4 | ENV PYTHONDONTWRITEBYTECODE 1
5 | ENV PYTHONBUFFERED 1
6 |
7 | COPY . /streamlit
8 |
9 | # Intentionally do not expose port 8501 or else people can circumvent login
10 |
11 | RUN pip install pipenv
12 | COPY Pipfile Pipfile.lock /streamlit/
13 | RUN pipenv install --system --dev
14 |
15 | CMD streamlit run app.py
--------------------------------------------------------------------------------
/api/schema.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 | from pydantic import BaseModel
3 |
4 | class Goal(BaseModel):
5 | goal: str
6 | has_amount: bool
7 | date_created: float
8 |
9 | class Config:
10 | orm_mode = True
11 |
12 | class Entry(BaseModel):
13 | goal_id: int
14 | tracked: bool
15 | amount: Optional[float]
16 | date: Optional[float]
17 |
18 | class Config:
19 | orm_mode = True
--------------------------------------------------------------------------------
/api/user/model.py:
--------------------------------------------------------------------------------
1 | from sqlalchemy.ext.declarative import declarative_base
2 | from sqlalchemy import Column, Integer, ForeignKey, String, Float, Boolean
3 | from sqlalchemy.orm import relationship
4 |
5 | Base = declarative_base()
6 |
7 | class User(Base):
8 | __tablename__ = 'user'
9 | id = Column(Integer, primary_key=True, index=True)
10 | username = Column(String, nullable=False)
11 | created_at = Column(Float, nullable=False)
--------------------------------------------------------------------------------
/dhparam/dhparam-2048.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN DH PARAMETERS-----
2 | MIIBCAKCAQEA4gKiHvyveN1v90rUvreuRRYjjxrzqZVQuAsvKo/y3BdsmFLWCKww
3 | 0X5KjCrzYWgOf23H5eqeH8fb9XFkAxOYnR9ITZGPxJg5ylJTfAG1DxdEZOlGcZU2
4 | ewgMjQAzAZmWFTA7T4c6qet+DcxZC3Vw0UpYyfyu7a2cvsBeuEWjai/Ho9yzs26J
5 | VFyAy/QRRcimvEaNaQv1FYNhXwNqamdGzwtHnvD295Cyk+06RPS0FxR71HJMTKeC
6 | WTdcPo+OH5qBWeTe6CxZ0lOAoDZJjV/Cm7d/GkyLias+Dz60GLlAo2WMycjKRshD
7 | GWLTJCHfn/TYQyLQYNKJ8EXqmyaQbIsBgwIBAg==
8 | -----END DH PARAMETERS-----
9 |
--------------------------------------------------------------------------------
/streamlit/src/pages/misc.py:
--------------------------------------------------------------------------------
1 | # stdlib imports
2 | import time
3 |
4 | # library imports
5 | import streamlit as st
6 |
7 | # local imports
8 | from config import API_URL
9 |
10 | PAGE_TITLE = 'Miscellaneous Gags'
11 |
12 | def write():
13 | st.markdown(f'# {PAGE_TITLE}')
14 |
15 | st.markdown('### Seconds since January 1, 1970')
16 | st.write('Press r to refresh the page')
17 | st.write(time.time())
18 |
19 | if __name__=='__main__':
20 | write()
--------------------------------------------------------------------------------
/svelte/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Svelte app
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/nginx/auth/README.md:
--------------------------------------------------------------------------------
1 | put your .htpasswd file here & run:
2 |
3 | htpasswd -c .htpasswd
4 |
5 | to add another to an existing file (-c overwrites existing), I run:
6 |
7 | htpasswd -n
8 |
9 | then copy-paste the command-line result into the existing .htpasswd file
10 |
11 | [more here](https://httpd.apache.org/docs/2.4/programs/htpasswd.html)
12 |
13 |
14 | FYI, you may need to install apache2-utils if you don't have it already
15 | $ sudo apt update
16 | $ sudo apt install apache2-utils\
--------------------------------------------------------------------------------
/api/tracker/schema.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 | from pydantic import BaseModel
3 |
4 | class Entry_Type(BaseModel):
5 | entry_type: str
6 | has_description: bool
7 | has_amount: bool
8 | created_at: Optional[float]
9 | user_id: int
10 |
11 | class Config:
12 | orm_mode=True
13 |
14 | class Entry(BaseModel):
15 | id: Optional[int]
16 | entry_type: str
17 | description: Optional[str]
18 | amount: Optional[float]
19 | created_at: Optional[float]
20 | user_id: int
21 |
22 | class Config:
23 | orm_mode=True
--------------------------------------------------------------------------------
/svelte/src/App.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 | Hello {name}!
7 | Visit the Svelte tutorial to learn how to build Svelte apps.
8 |
9 |
10 |
--------------------------------------------------------------------------------
/api/alembic/script.py.mako:
--------------------------------------------------------------------------------
1 | """${message}
2 |
3 | Revision ID: ${up_revision}
4 | Revises: ${down_revision | comma,n}
5 | Create Date: ${create_date}
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 | ${imports if imports else ""}
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = ${repr(up_revision)}
14 | down_revision = ${repr(down_revision)}
15 | branch_labels = ${repr(branch_labels)}
16 | depends_on = ${repr(depends_on)}
17 |
18 |
19 | def upgrade():
20 | ${upgrades if upgrades else "pass"}
21 |
22 |
23 | def downgrade():
24 | ${downgrades if downgrades else "pass"}
25 |
--------------------------------------------------------------------------------
/api/alembic/versions/a5ee42f3cea7_created_user_model.py:
--------------------------------------------------------------------------------
1 | """created user model
2 |
3 | Revision ID: a5ee42f3cea7
4 | Revises: e74329689b23
5 | Create Date: 2021-01-09 21:27:26.887222
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = 'a5ee42f3cea7'
14 | down_revision = 'e74329689b23'
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | pass
22 | # ### end Alembic commands ###
23 |
24 |
25 | def downgrade():
26 | # ### commands auto generated by Alembic - please adjust! ###
27 | pass
28 | # ### end Alembic commands ###
29 |
--------------------------------------------------------------------------------
/api/alembic/versions/aaa8141c13b6_import_user_model_metadata_into_alembic.py:
--------------------------------------------------------------------------------
1 | """import user model metadata into alembic
2 |
3 | Revision ID: aaa8141c13b6
4 | Revises: 09d5d3f71cf4
5 | Create Date: 2021-01-09 21:31:53.065966
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = 'aaa8141c13b6'
14 | down_revision = '09d5d3f71cf4'
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | pass
22 | # ### end Alembic commands ###
23 |
24 |
25 | def downgrade():
26 | # ### commands auto generated by Alembic - please adjust! ###
27 | pass
28 | # ### end Alembic commands ###
29 |
--------------------------------------------------------------------------------
/api/alembic/versions/e9ac74c2fb4f_passing_user_model_thru_user_.py:
--------------------------------------------------------------------------------
1 | """passing User model thru user relationship)
2 |
3 |
4 | Revision ID: e9ac74c2fb4f
5 | Revises: 2171019a778e
6 | Create Date: 2021-01-09 22:57:55.812261
7 |
8 | """
9 | from alembic import op
10 | import sqlalchemy as sa
11 |
12 |
13 | # revision identifiers, used by Alembic.
14 | revision = 'e9ac74c2fb4f'
15 | down_revision = '2171019a778e'
16 | branch_labels = None
17 | depends_on = None
18 |
19 |
20 | def upgrade():
21 | # ### commands auto generated by Alembic - please adjust! ###
22 | pass
23 | # ### end Alembic commands ###
24 |
25 |
26 | def downgrade():
27 | # ### commands auto generated by Alembic - please adjust! ###
28 | pass
29 | # ### end Alembic commands ###
30 |
--------------------------------------------------------------------------------
/api/alembic/versions/09d5d3f71cf4_created_user_model_but_imported_into_.py:
--------------------------------------------------------------------------------
1 | """created user model but imported into main models.py this time
2 |
3 | Revision ID: 09d5d3f71cf4
4 | Revises: a5ee42f3cea7
5 | Create Date: 2021-01-09 21:29:04.659030
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = '09d5d3f71cf4'
14 | down_revision = 'a5ee42f3cea7'
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | pass
22 | # ### end Alembic commands ###
23 |
24 |
25 | def downgrade():
26 | # ### commands auto generated by Alembic - please adjust! ###
27 | pass
28 | # ### end Alembic commands ###
29 |
--------------------------------------------------------------------------------
/api/models.py:
--------------------------------------------------------------------------------
1 | from sqlalchemy.ext.declarative import declarative_base
2 | from sqlalchemy import Column, Integer, ForeignKey, String, Float, Boolean
3 |
4 | Base = declarative_base()
5 |
6 | class Goal(Base):
7 | __tablename__ = 'goals'
8 | id = Column(Integer, primary_key=True, index=True)
9 | goal = Column(String, nullable=False, unique=True)
10 | has_amount = Column(Boolean, nullable=False)
11 | date_created = Column(Float, nullable=False)
12 |
13 | class Entry(Base):
14 | __tablename__ = 'entries'
15 |
16 | id = Column(Integer, primary_key=True, index=True)
17 | goal_id = Column(Integer, ForeignKey('goals.id'), nullable=False)
18 | date = Column(Float, nullable=False)
19 | tracked = Column(Boolean, nullable=False)
20 | amount = Column(Float, nullable=True)
--------------------------------------------------------------------------------
/svelte/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "svelte-app",
3 | "version": "1.0.0",
4 | "private": true,
5 | "scripts": {
6 | "build": "rollup -c",
7 | "dev": "rollup -c -w",
8 | "start": "sirv public --host -s"
9 | },
10 | "devDependencies": {
11 | "@rollup/plugin-commonjs": "^17.0.0",
12 | "@rollup/plugin-node-resolve": "^11.0.0",
13 | "rollup": "^2.3.4",
14 | "rollup-plugin-css-only": "^3.1.0",
15 | "rollup-plugin-livereload": "^2.0.0",
16 | "rollup-plugin-svelte": "^7.0.0",
17 | "rollup-plugin-terser": "^7.0.0",
18 | "svelte": "^3.0.0"
19 | },
20 | "dependencies": {
21 | "@rollup/plugin-replace": "^2.4.1",
22 | "@urql/svelte": "^1.2.0",
23 | "graphql": "^15.5.0",
24 | "sirv-cli": "^1.0.0",
25 | "svelte-routing": "^1.5.0",
26 | "svelte-typewriter": "^2.4.5"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/streamlit/user/user.py:
--------------------------------------------------------------------------------
1 | import requests
2 |
3 | import streamlit as st
4 |
5 | from sessionstate import _get_full_session
6 | from config import API_URL
7 |
8 | class User():
9 | def __init__(self):
10 | self.resource = f'{API_URL}/user'
11 | self.current_username = self.get_active_user()
12 | self.db_user = self.get_db_user()
13 |
14 |
15 | def get_db_user(self):
16 | res = requests.get(f'{self.resource}/{self.current_username}')
17 | return res.json()
18 | f
19 | def get_active_user(self):
20 | session = _get_full_session()
21 | try:
22 | user = session.ws.request.headers['X-Forwarded-User']
23 | except KeyError:
24 | user = 'ed'
25 | # user_list = requests.get(f'{self.resource}')
26 | # st.write(user_list)
27 | # user = st.selectbox(options=user_list['name'])
28 |
29 | return user
--------------------------------------------------------------------------------
/api/user/router.py:
--------------------------------------------------------------------------------
1 | import time
2 | from typing import List
3 |
4 | from fastapi import APIRouter, HTTPException
5 | from fastapi_sqlalchemy import db
6 |
7 | from . import model
8 | from . import schema
9 |
10 | router = APIRouter(
11 | prefix = '/user',
12 | tags = ['User']
13 | )
14 |
15 | @router.get('/', response_model=List[schema.User])
16 | def get_users():
17 | db_users = db.session.query(model.User).all()
18 | return db_users
19 |
20 | @router.get('/{username}', response_model=schema.User)
21 | def get_or_create_user(username: str):
22 | db_user = db.session.query(model.User).filter(model.User.username==username).one_or_none()
23 | # user doesn't exist, create (users managed by htpasswd)
24 | if db_user is None:
25 | db_user = model.User(
26 | username = user.username,
27 | created_at = time.time()
28 | )
29 | db.session.add(db_user)
30 | db.session.commit()
31 |
32 | return db_user
--------------------------------------------------------------------------------
/streamlit/src/style/charts.py:
--------------------------------------------------------------------------------
1 | # library imports
2 | import plotly.express as px
3 | import pandas as pd
4 | import streamlit as st
5 |
6 | def table_cols(df_in):
7 | df = df_in.reset_index()
8 | st.table(df)
9 | return 0
10 |
11 | # TBU - row-wise containers necessary to implement this
12 | rows, cols = df.shape[0], df.shape[1]
13 | col_headers = st.beta_columns(cols)
14 | col_placeholders = st.beta_columns(cols)
15 |
16 | for colnum, col in enumerate(df.columns):
17 | with col_headers[colnum]:
18 | st.write(col)
19 |
20 | st.markdown('***')
21 |
22 | for colnum, col in enumerate(df.columns):
23 | with col_placeholders[colnum]:
24 | for row in df[col]:
25 | st.write(row)
26 |
27 |
28 |
29 |
30 |
31 |
32 | return 0
33 |
34 | def line_chart(df):
35 | fig = px.line(df, x='date', y='weight', title='Weight')
36 | fig.show()
37 |
38 | def heatmap(df):
39 | df.style.apply(lambda x: ['background: green' if x > 1 else ''])
40 | return df
41 |
--------------------------------------------------------------------------------
/api/tracker/model.py:
--------------------------------------------------------------------------------
1 | from sqlalchemy.ext.declarative import declarative_base
2 | from sqlalchemy import Column, Integer, ForeignKey, String, Float, Boolean
3 | from sqlalchemy.orm import relationship
4 |
5 | from user.model import User
6 |
7 | Base = declarative_base()
8 |
9 | class Entry_Type(Base):
10 | __tablename__='entry_type'
11 | entry_type = Column(String, primary_key=True, index=True)
12 | has_description = Column(Boolean, nullable=False)
13 | has_amount = Column(Boolean, nullable=False)
14 | created_at = Column(Float, nullable=False)
15 | user_id = Column(Integer, ForeignKey(User.id))
16 |
17 | # backref: https://docs.sqlalchemy.org/en/13/orm/basic_relationships.html
18 | class Entry(Base):
19 | __tablename__='entry'
20 | id = Column(Integer, primary_key=True, index=True)
21 | description = Column(String)
22 | amount = Column(Float)
23 | created_at = Column(Float, nullable=False)
24 | entry_type = Column(String, ForeignKey('entry_type.entry_type'))
25 | user_id = Column(Integer, ForeignKey(User.id))
26 | user = relationship(User,backref='entries')
--------------------------------------------------------------------------------
/streamlit/app.py:
--------------------------------------------------------------------------------
1 | # stdlib imports
2 |
3 | # library imports
4 | import streamlit as st
5 | import pandas as pd
6 | import awesome_streamlit as ast # https://github.com/MarcSkovMadsen/awesome-streamlit
7 |
8 | # local imports
9 | import src.pages.new_tracker
10 | import src.pages.goals
11 | import src.pages.entries
12 | import src.pages.misc
13 | from user.user import User
14 |
15 | st.set_page_config(
16 | initial_sidebar_state="collapsed",
17 | page_title='Flip that rip')
18 |
19 | PAGES = {
20 | 'Goal Tracker v2': src.pages.new_tracker,
21 | 'Track Performance': src.pages.entries,
22 | 'Manage Goals': src.pages.goals,
23 | 'Miscellaneous Gags': src.pages.misc,
24 | }
25 |
26 | def main():
27 | user = User()
28 | st.write(user)
29 | st.sidebar.title("Navigation")
30 | selection = st.sidebar.radio("Go to", list(PAGES.keys()))
31 | st.sidebar.markdown(f'Logged in as **{user.current_username}**.')
32 |
33 | page = PAGES[selection]
34 |
35 | with st.spinner(f"Loading {selection} ..."):
36 | ast.shared.components.write_page(page)
37 |
38 | if __name__=='__main__':
39 | main()
--------------------------------------------------------------------------------
/api/alembic/versions/8d31408b2f13_fixed_metadata_bug_in_alembic_env_py_.py:
--------------------------------------------------------------------------------
1 | """fixed metadata bug in alembic env.py target_metadata imports
2 |
3 | Revision ID: 8d31408b2f13
4 | Revises: aaa8141c13b6
5 | Create Date: 2021-01-09 21:43:22.586666
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = '8d31408b2f13'
14 | down_revision = 'aaa8141c13b6'
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.create_table('user',
22 | sa.Column('id', sa.Integer(), nullable=False),
23 | sa.Column('username', sa.String(), nullable=False),
24 | sa.Column('created_at', sa.Float(), nullable=False),
25 | sa.PrimaryKeyConstraint('id')
26 | )
27 | op.create_index(op.f('ix_user_id'), 'user', ['id'], unique=False)
28 | # ### end Alembic commands ###
29 |
30 |
31 | def downgrade():
32 | # ### commands auto generated by Alembic - please adjust! ###
33 | op.drop_index(op.f('ix_user_id'), table_name='user')
34 | op.drop_table('user')
35 | # ### end Alembic commands ###
36 |
--------------------------------------------------------------------------------
/svelte/public/global.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | position: relative;
3 | width: 100%;
4 | height: 100%;
5 | }
6 |
7 | body {
8 | color: #333;
9 | margin: 0;
10 | padding: 8px;
11 | box-sizing: border-box;
12 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
13 | }
14 |
15 | a {
16 | color: rgb(0,100,200);
17 | text-decoration: none;
18 | }
19 |
20 | a:hover {
21 | text-decoration: underline;
22 | }
23 |
24 | a:visited {
25 | color: rgb(0,80,160);
26 | }
27 |
28 | label {
29 | display: block;
30 | }
31 |
32 | input, button, select, textarea {
33 | font-family: inherit;
34 | font-size: inherit;
35 | -webkit-padding: 0.4em 0;
36 | padding: 0.4em;
37 | margin: 0 0 0.5em 0;
38 | box-sizing: border-box;
39 | border: 1px solid #ccc;
40 | border-radius: 2px;
41 | }
42 |
43 | input:disabled {
44 | color: #ccc;
45 | }
46 |
47 | button {
48 | color: #333;
49 | background-color: #f4f4f4;
50 | outline: none;
51 | }
52 |
53 | button:disabled {
54 | color: #999;
55 | }
56 |
57 | button:not(:disabled):active {
58 | background-color: #ddd;
59 | }
60 |
61 | button:focus {
62 | border-color: #666;
63 | }
64 |
--------------------------------------------------------------------------------
/api/tracker/router.py:
--------------------------------------------------------------------------------
1 | import time
2 | from typing import List
3 |
4 | from fastapi import APIRouter, HTTPException
5 | from fastapi_sqlalchemy import db
6 |
7 | from . import model
8 | from . import schema
9 |
10 | router = APIRouter(
11 | prefix='/tracker',
12 | tags=['Tracker']
13 | )
14 |
15 | @router.get('/entry_type', response_model=List[schema.Entry_Type])
16 | def get_entry_types():
17 | db_entry_types = db.session.query(model.Entry_Type).all()
18 | return db_entry_types
19 |
20 | @router.get('/entry', response_model=List[schema.Entry])
21 | def get_entry():
22 | db_entry = db.session.query(model.Entry).all()
23 | return db_entry
24 |
25 | @router.post('/entry_type', response_model=schema.Entry_Type)
26 | def create_entry_type(entry_type: schema.Entry_Type):
27 | db_entry_type = model.Entry_Type(
28 | entry_type = entry_type.entry_type,
29 | has_description = entry_type.has_description,
30 | has_amount = entry_type.has_amount,
31 | created_at = time.time(),
32 | user_id = entry_type.user_id,
33 | )
34 | db.session.add(db_entry_type)
35 | db.session.commit()
36 |
37 | return db_entry_type
38 |
39 | @router.post('/entry', response_model=schema.Entry)
40 | def create_entry(entry: schema.Entry):
41 | db_entry = model.Entry(
42 | description = entry.description,
43 | amount = entry.amount,
44 | created_at = time.time(),
45 | entry_type = entry.entry_type,
46 | user_id = entry.user_id,
47 | )
48 | db.session.add(db_entry)
49 | db.session.commit()
50 |
51 | return db_entry
--------------------------------------------------------------------------------
/api/alembic/versions/e74329689b23_first_commit.py:
--------------------------------------------------------------------------------
1 | """first commit
2 |
3 | Revision ID: e74329689b23
4 | Revises:
5 | Create Date: 2021-01-03 16:12:23.163633
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = 'e74329689b23'
14 | down_revision = None
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.create_table('goals',
22 | sa.Column('id', sa.Integer(), nullable=False),
23 | sa.Column('goal', sa.String(), nullable=False),
24 | sa.Column('has_amount', sa.Boolean(), nullable=False),
25 | sa.Column('date_created', sa.Float(), nullable=False),
26 | sa.PrimaryKeyConstraint('id'),
27 | sa.UniqueConstraint('goal')
28 | )
29 | op.create_index(op.f('ix_goals_id'), 'goals', ['id'], unique=False)
30 | op.create_table('entries',
31 | sa.Column('id', sa.Integer(), nullable=False),
32 | sa.Column('goal_id', sa.Integer(), nullable=False),
33 | sa.Column('date', sa.Float(), nullable=False),
34 | sa.Column('tracked', sa.Boolean(), nullable=False),
35 | sa.Column('amount', sa.Float(), nullable=True),
36 | sa.ForeignKeyConstraint(['goal_id'], ['goals.id'], ),
37 | sa.PrimaryKeyConstraint('id')
38 | )
39 | op.create_index(op.f('ix_entries_id'), 'entries', ['id'], unique=False)
40 | # ### end Alembic commands ###
41 |
42 |
43 | def downgrade():
44 | # ### commands auto generated by Alembic - please adjust! ###
45 | op.drop_index(op.f('ix_entries_id'), table_name='entries')
46 | op.drop_table('entries')
47 | op.drop_index(op.f('ix_goals_id'), table_name='goals')
48 | op.drop_table('goals')
49 | # ### end Alembic commands ###
50 |
--------------------------------------------------------------------------------
/streamlit/tracker/tracker.py:
--------------------------------------------------------------------------------
1 | import time
2 | import requests
3 | from datetime import datetime
4 |
5 | import pandas as pd
6 |
7 | from sessionstate import _get_full_session
8 | from config import API_URL
9 |
10 | class Tracker():
11 | def __init__(self, hours_back=24):
12 | self.resource = f'{API_URL}/tracker'
13 | self.existing_types = self.existing_types()
14 | self.existing_entries = self.existing_entries(hours_back)
15 |
16 | def existing_entries(self,hours_back):
17 | res = requests.get(f'{self.resource}/entry')
18 | cutoff_time = time.time()
19 | cutoff_time -= 24*60*60
20 | LD = res.json()
21 | if LD == []:
22 | return pd.DataFrame()
23 | else:
24 | v = {k: [dic[k] for dic in LD] for k in LD[0]}
25 | df = pd.DataFrame(v)
26 | df = df[df['created_at']>=cutoff_time]
27 | df['created_at'] = df['created_at'].apply(datetime.fromtimestamp)
28 | return df.set_index('id')
29 |
30 | def existing_types(self):
31 | res = requests.get(f'{self.resource}/entry_type')
32 | LD = res.json()
33 | if LD == []:
34 | return pd.DataFrame()
35 | else:
36 | v = {k: [dic[k] for dic in LD] for k in LD[0]}
37 | df = pd.DataFrame(v)
38 | return df.set_index('entry_type')
39 |
40 | def create_entry(self, json):
41 | res = requests.post(f'{self.resource}/entry', json=json)
42 | LD = res.json()
43 | return LD
44 |
45 | def create_type(self, entry_type, has_description, has_amount, user_id):
46 | json = {
47 | 'entry_type': entry_type,
48 | 'has_description': has_description,
49 | 'has_amount': has_amount,
50 | 'user_id': user_id,
51 | }
52 | res = requests.post(f'{self.resource}/entry_type', json=json)
53 | return res.json()
54 |
55 | def _get_active_user():
56 | session = _get_full_session()
57 | user = session.ws.request.headers['X-Forwarded-User']
58 | return user
--------------------------------------------------------------------------------
/api/alembic/versions/2171019a778e_created_entries_db_model.py:
--------------------------------------------------------------------------------
1 | """created entries db model
2 |
3 | Revision ID: 2171019a778e
4 | Revises: 8d31408b2f13
5 | Create Date: 2021-01-09 22:36:26.851407
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = '2171019a778e'
14 | down_revision = '8d31408b2f13'
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.create_table('entry_type',
22 | sa.Column('entry_type', sa.String(), nullable=False),
23 | sa.Column('has_description', sa.Boolean(), nullable=False),
24 | sa.Column('has_amount', sa.Boolean(), nullable=False),
25 | sa.Column('created_at', sa.Float(), nullable=False),
26 | sa.Column('user_id', sa.Integer(), nullable=True),
27 | sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
28 | sa.PrimaryKeyConstraint('entry_type')
29 | )
30 | op.create_index(op.f('ix_entry_type_entry_type'), 'entry_type', ['entry_type'], unique=False)
31 | op.create_table('entry',
32 | sa.Column('id', sa.Integer(), nullable=False),
33 | sa.Column('description', sa.String(), nullable=True),
34 | sa.Column('amount', sa.Float(), nullable=True),
35 | sa.Column('created_at', sa.Float(), nullable=False),
36 | sa.Column('entry_type', sa.String(), nullable=True),
37 | sa.Column('user_id', sa.Integer(), nullable=True),
38 | sa.ForeignKeyConstraint(['entry_type'], ['entry_type.entry_type'], ),
39 | sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
40 | sa.PrimaryKeyConstraint('id')
41 | )
42 | op.create_index(op.f('ix_entry_id'), 'entry', ['id'], unique=False)
43 | # ### end Alembic commands ###
44 |
45 |
46 | def downgrade():
47 | # ### commands auto generated by Alembic - please adjust! ###
48 | op.drop_index(op.f('ix_entry_id'), table_name='entry')
49 | op.drop_table('entry')
50 | op.drop_index(op.f('ix_entry_type_entry_type'), table_name='entry_type')
51 | op.drop_table('entry_type')
52 | # ### end Alembic commands ###
53 |
--------------------------------------------------------------------------------
/svelte/rollup.config.js:
--------------------------------------------------------------------------------
1 | import svelte from 'rollup-plugin-svelte';
2 | import commonjs from '@rollup/plugin-commonjs';
3 | import resolve from '@rollup/plugin-node-resolve';
4 | import livereload from 'rollup-plugin-livereload';
5 | import { terser } from 'rollup-plugin-terser';
6 | import css from 'rollup-plugin-css-only';
7 |
8 | const production = !process.env.ROLLUP_WATCH;
9 |
10 | function serve() {
11 | let server;
12 |
13 | function toExit() {
14 | if (server) server.kill(0);
15 | }
16 |
17 | return {
18 | writeBundle() {
19 | if (server) return;
20 | server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
21 | stdio: ['ignore', 'inherit', 'inherit'],
22 | shell: true
23 | });
24 |
25 | process.on('SIGTERM', toExit);
26 | process.on('exit', toExit);
27 | }
28 | };
29 | }
30 |
31 | export default {
32 | input: 'src/main.js',
33 | output: {
34 | sourcemap: true,
35 | format: 'iife',
36 | name: 'app',
37 | file: 'public/build/bundle.js'
38 | },
39 | plugins: [
40 | svelte({
41 | compilerOptions: {
42 | // enable run-time checks when not in production
43 | dev: !production
44 | }
45 | }),
46 | // we'll extract any component CSS out into
47 | // a separate file - better for performance
48 | css({ output: 'bundle.css' }),
49 |
50 | // If you have external dependencies installed from
51 | // npm, you'll most likely need these plugins. In
52 | // some cases you'll need additional configuration -
53 | // consult the documentation for details:
54 | // https://github.com/rollup/plugins/tree/master/packages/commonjs
55 | resolve({
56 | browser: true,
57 | dedupe: ['svelte']
58 | }),
59 | commonjs(),
60 |
61 | // In dev mode, call `npm run start` once
62 | // the bundle has been generated
63 | !production && serve(),
64 |
65 | // Watch the `public` directory and refresh the
66 | // browser on changes when not in production
67 | !production && livereload('public'),
68 |
69 | // If we're building for production (npm run build
70 | // instead of npm run dev), minify
71 | production && terser()
72 | ],
73 | watch: {
74 | clearScreen: false
75 | }
76 | };
77 |
--------------------------------------------------------------------------------
/api/alembic.ini:
--------------------------------------------------------------------------------
1 | # A generic, single database configuration.
2 |
3 | [alembic]
4 | # path to migration scripts
5 | script_location = alembic
6 |
7 | # template used to generate migration files
8 | # file_template = %%(rev)s_%%(slug)s
9 |
10 | # timezone to use when rendering the date
11 | # within the migration file as well as the filename.
12 | # string value is passed to dateutil.tz.gettz()
13 | # leave blank for localtime
14 | # timezone =
15 |
16 | # max length of characters to apply to the
17 | # "slug" field
18 | # truncate_slug_length = 40
19 |
20 | # set to 'true' to run the environment during
21 | # the 'revision' command, regardless of autogenerate
22 | # revision_environment = false
23 |
24 | # set to 'true' to allow .pyc and .pyo files without
25 | # a source .py file to be detected as revisions in the
26 | # versions/ directory
27 | # sourceless = false
28 |
29 | # version location specification; this defaults
30 | # to alembic/versions. When using multiple version
31 | # directories, initial revisions must be specified with --version-path
32 | # version_locations = %(here)s/bar %(here)s/bat alembic/versions
33 |
34 | # the output encoding used when revision files
35 | # are written from script.py.mako
36 | # output_encoding = utf-8
37 |
38 | sqlalchemy.url =
39 |
40 |
41 | [post_write_hooks]
42 | # post_write_hooks defines scripts or Python functions that are run
43 | # on newly generated revision scripts. See the documentation for further
44 | # detail and examples
45 |
46 | # format using "black" - use the console_scripts runner, against the "black" entrypoint
47 | # hooks=black
48 | # black.type=console_scripts
49 | # black.entrypoint=black
50 | # black.options=-l 79
51 |
52 | # Logging configuration
53 | [loggers]
54 | keys = root,sqlalchemy,alembic
55 |
56 | [handlers]
57 | keys = console
58 |
59 | [formatters]
60 | keys = generic
61 |
62 | [logger_root]
63 | level = WARN
64 | handlers = console
65 | qualname =
66 |
67 | [logger_sqlalchemy]
68 | level = WARN
69 | handlers =
70 | qualname = sqlalchemy.engine
71 |
72 | [logger_alembic]
73 | level = INFO
74 | handlers =
75 | qualname = alembic
76 |
77 | [handler_console]
78 | class = StreamHandler
79 | args = (sys.stderr,)
80 | level = NOTSET
81 | formatter = generic
82 |
83 | [formatter_generic]
84 | format = %(levelname)-5.5s [%(name)s] %(message)s
85 | datefmt = %H:%M:%S
86 |
--------------------------------------------------------------------------------
/streamlit/src/pages/goals.py:
--------------------------------------------------------------------------------
1 | # stdlib
2 | import time
3 |
4 | # libraries
5 | import streamlit as st
6 | import pandas as pd
7 |
8 | # local
9 | from src.crud import Goal
10 | from src.style.charts import line_chart, heatmap
11 | from config import API_URL
12 | from src.style.stringformats import es_date_format
13 |
14 |
15 | PAGE_TITLE = 'Manage Goals'
16 |
17 | def write():
18 | st.markdown(f'# {PAGE_TITLE}')
19 |
20 | # initiate & read goals
21 | goals = Goal()
22 | st.markdown('### List Existing Goals')
23 |
24 | if goals.existing_goals.empty == True:
25 | st.markdown('### Create your first goal!')
26 | st.write('This page will change after refresh (press r)')
27 | create_goal(goals)
28 | footer()
29 | return
30 |
31 | out_existing_goals = goals.existing_goals
32 | out_existing_goals['date_created'] = out_existing_goals['date_created'].apply(es_date_format)
33 | st.write(out_existing_goals)
34 |
35 | # create goal
36 | create_goal(goals)
37 |
38 | # delete goal
39 | delete_goal(goals)
40 |
41 | footer()
42 |
43 | def footer():
44 | st.write('You can track your entries by \
45 | clicking on the little arrow in the \
46 | top left to open navigation, then \
47 | select the appropriate page.')
48 |
49 | def create_goal(goals):
50 | st.markdown('### Create New Goal')
51 | new_goal = {
52 | 'goal': st.text_input(label='Goal Name'),
53 | 'has_amount': st.checkbox(label='Goal tracks an amount? (weight, calories, etc.)'),
54 | 'date_created': time.time(),
55 | }
56 |
57 | create = st.button(label='Create goal')
58 |
59 | if create:
60 | goal_response = goals.create_goal(new_goal)
61 | if goal_response is not None:
62 | create = False
63 | st.write(goal_response)
64 |
65 | def delete_goal(goals):
66 | st.markdown('### Delete Existing Goal')
67 | delete_id = st.selectbox(
68 | label='Choose goal to delete',
69 | options=goals.existing_goals.index,
70 | format_func=lambda x: goals.existing_goals.loc[x,'goal'])
71 |
72 | delete = st.button(label='Delete goal')
73 |
74 | if delete:
75 | goal_response = goals.delete_goal(delete_id)
76 | if goal_response is not None:
77 | delete = False
78 | st.write(goal_response)
79 |
80 | if __name__=='__main__':
81 | write()
--------------------------------------------------------------------------------
/streamlit/src/crud.py:
--------------------------------------------------------------------------------
1 | # stdlib imports
2 | import requests
3 | import json
4 |
5 | # library imports
6 | import streamlit as st
7 | import pandas as pd
8 |
9 | # local imports
10 | from config import API_URL
11 |
12 | class Goal():
13 |
14 | def __init__(self):
15 | self.resource = f'{API_URL}/goals/'
16 | self.existing_goals = self.read_goals()
17 |
18 | def read_goals(self):
19 | res = requests.get(self.resource)
20 | df = pd.read_json(res.text, convert_dates=False)
21 | try:
22 | df.set_index('id', inplace=True)
23 | return df
24 | except KeyError:
25 | return df
26 |
27 | def create_goal(self, goal):
28 | res = requests.post(self.resource, json=goal)
29 | return json.loads(res.text)
30 |
31 | def delete_goal(self, id):
32 | url = f'{self.resource}delete/{id}'
33 | res = requests.post(url)
34 | return json.loads(res.text)
35 |
36 | class Entry():
37 |
38 | def __init__(self):
39 | # list of dates, least to most recent
40 | self.resource = f'{API_URL}/entries/'
41 | self.existing_entries = self.read_entries()
42 |
43 | def read_entries(self):
44 | res = requests.get(self.resource)
45 | df = pd.read_json(res.text, convert_dates=False)
46 | try:
47 | df.set_index('id', inplace=True)
48 | return df
49 | except KeyError:
50 | return df
51 |
52 | def create_entry(self, entry):
53 | res = requests.post(self.resource, json=entry)
54 | return json.loads(res.text)
55 |
56 | def delete_entry(self, entries):
57 | res = requests.post(f'{self.resource}delete/', json=entries)
58 | return json.loads(res.text)
59 |
60 |
61 | class Tracker():
62 |
63 | def read_tracker():
64 | url = f'{API_URL}/trackers/'
65 | res = requests.get(url)
66 | df = pd.read_json(res.text)
67 | return df.set_index('id')
68 |
69 | def submit_tracker(tracker):
70 | url = f'{API_URL}/tracker/'
71 | res = requests.post(url, json=tracker)
72 | return json.loads(res.text)
73 |
74 | def delete_tracker(id):
75 | url = f'{API_URL}/tracker/delete/{id}'
76 | res = requests.post(url)
77 | return json.loads(res.text)
78 |
79 | def update_tracker(tracker, id):
80 | url = f'{API_URL}/tracker/update/{id}'
81 | res = requests.post(url, json=tracker)
82 | return json.loads(res.text)
--------------------------------------------------------------------------------
/streamlit/src/pages/new_tracker.py:
--------------------------------------------------------------------------------
1 | # stdlib imports
2 |
3 | # library imports
4 | import streamlit as st
5 | import pandas as pd
6 |
7 | # local imports
8 | from user.user import User
9 | from tracker.tracker import Tracker
10 |
11 | PAGE_TITLE = 'Goal Tracking v2'
12 |
13 | def write():
14 | st.markdown(f'# {PAGE_TITLE}')
15 |
16 | # initialize models
17 | user = User()
18 | user_id = user.db_user['id']
19 | tracker = Tracker()
20 |
21 | # TBU - make hours_back based on goal type (calories 24, workout 7*24)
22 | st.markdown('## 1. Choose Goal & Submit Entry')
23 | choose = submit_entry(tracker, user_id)
24 |
25 | st.markdown('## 2. Review Entries')
26 | df = tracker.existing_entries
27 | df = df[df['entry_type']==choose]
28 | sum = df.sum()
29 | sum['entry_type'] = 'Total'
30 | sum['description'] = '24hr Total'
31 | df = df.append(sum, ignore_index=True)
32 | df.drop(columns=['user_id','created_at'],inplace=True)
33 | st.write(df)
34 | st.markdown('## 3. Create New Entry Type')
35 | create_type(tracker, user_id)
36 |
37 | footer()
38 |
39 | def submit_entry(tracker, user_id):
40 | choose = st.selectbox(label='Select Entry Type', options=tracker.existing_types.index, index=0)
41 | row = tracker.existing_types.loc[choose]
42 | submission = {}
43 | if row['has_description']:
44 | submission['description'] = st.text_input(label='Description')
45 | if row['has_amount']:
46 | submission['amount'] = st.number_input(label='Amount')
47 | submission['entry_type'] = choose
48 | submission['user_id'] = user_id
49 | submit = st.button(label='submit')
50 |
51 | if submit:
52 | # need to get variables before dict, then give to dict
53 | res = tracker.create_entry(submission)
54 | st.write(res)
55 | create = False
56 |
57 | return choose
58 |
59 |
60 |
61 | def footer():
62 | st.write('Feel free to delete or create new goals! \
63 | Click on the little arrow in the top left \
64 | to open navigation, then \
65 | select the appropriate page.')
66 |
67 | def create_type(tracker, user_id):
68 |
69 | type_input = {
70 | 'entry_type': st.text_input(label='Entry Type'),
71 | 'has_description': st.checkbox(label='Has Description?'),
72 | 'has_amount': st.checkbox(label='Has Amount?'),
73 | }
74 |
75 | create = st.button(label='create')
76 |
77 | if create:
78 |
79 | # need to get variables before dict, then give to dict
80 | res = tracker.create_type(
81 | entry_type=type_input['entry_type'],
82 | has_description=type_input['has_description'],
83 | has_amount=type_input['has_amount'],
84 | user_id=user_id,
85 | )
86 | st.write(res)
87 | create = False
88 |
89 | if __name__=='__main__':
90 | write()
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | # streamlit:
5 | # volumes:
6 | # - ./streamlit/:/streamlit/
7 | # build:
8 | # context: ./streamlit
9 | # dockerfile: Dockerfile
10 | # container_name: streamlit
11 | # restart: always
12 | # # do not publicly expose port 8501
13 | # # ports:
14 | # # - "8501:8501"
15 | # depends_on:
16 | # - api
17 | # networks:
18 | # - app-network
19 |
20 | certbot:
21 | image: certbot/certbot
22 | container_name: certbot
23 | volumes:
24 | - certbot-etc:/etc/letsencrypt
25 | - certbot-var:/var/lib/letsencrypt
26 | - web-root:/var/www/html
27 | depends_on:
28 | - reverse
29 | command: certonly --webroot --webroot-path=/var/www/html --email edwardsapp@gmail.com --agree-tos --no-eff-email -d flip.rip -d www.flip.rip
30 |
31 | reverse:
32 | container_name: reverse
33 | hostname: reverse
34 | restart: always
35 | build: ./nginx
36 | ports:
37 | - "80:80"
38 | - "443:443"
39 | depends_on:
40 | - pgadmin4
41 | - svelte
42 | # - streamlit
43 | - postgres
44 | - api
45 | volumes:
46 | - certbot-etc:/etc/letsencrypt
47 | - certbot-var:/var/lib/letsencrypt
48 | - web-root:/var/www/html
49 | - dhparam:/etc/ssl/certs
50 | networks:
51 | - app-network
52 |
53 | postgres:
54 | image: postgres:12
55 | volumes:
56 | - postgres_data:/var/lib/postgresql/data/
57 | # ports:
58 | # - "5432:5432"
59 | env_file:
60 | - ./postgres/.env
61 | networks:
62 | - app-network
63 |
64 | api:
65 | build:
66 | context: ./api
67 | dockerfile: Dockerfile
68 | volumes:
69 | - ./api/:/api/
70 | command: bash -c "alembic upgrade head && uvicorn main:app --host 0.0.0.0 --port 8000 --reload"
71 | # ports:
72 | # - "8000:8000"
73 | depends_on:
74 | - postgres
75 | networks:
76 | - app-network
77 |
78 | pgadmin4:
79 | container_name: pgadmin4
80 | image: dpage/pgadmin4
81 | env_file:
82 | - ./pgadmin4/.env
83 | # ports:
84 | # - "5050:80"
85 | depends_on:
86 | - postgres
87 | logging:
88 | driver: none
89 | networks:
90 | - app-network
91 |
92 | svelte:
93 | build: ./svelte
94 | command: npm run dev
95 | volumes:
96 | - ./svelte/:/svelte/
97 | networks:
98 | - app-network
99 |
100 | volumes:
101 | postgres_data:
102 | certbot-etc:
103 | certbot-var:
104 | web-root:
105 | driver: local
106 | driver_opts:
107 | type: none
108 | device: /home/ed/flip/svelte/public
109 | o: bind
110 | dhparam:
111 | driver: local
112 | driver_opts:
113 | type: none
114 | device: /home/ed/flip/dhparam/
115 | o: bind
116 |
117 | networks:
118 | app-network:
119 | driver: bridge
--------------------------------------------------------------------------------
/api/main.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 | import uvicorn
4 | from fastapi import FastAPI
5 | from fastapi.openapi.utils import get_openapi
6 |
7 | import os
8 | import datetime as dt
9 | from fastapi_sqlalchemy import DBSessionMiddleware
10 | from fastapi_sqlalchemy import db
11 | from dotenv import load_dotenv
12 |
13 | import models
14 | import schema
15 |
16 | from user import router as user_router
17 | from tracker import router as tracker_router
18 |
19 | BASE_DIR = os.path.dirname(os.path.abspath(__file__))
20 | load_dotenv(os.path.join(BASE_DIR,'.env'))
21 |
22 | app = FastAPI(
23 | openapi_url="/api/v1/openapi.json",
24 | root_path='/api/v1'
25 | )
26 |
27 | app.add_middleware(
28 | DBSessionMiddleware,
29 | db_url=os.environ['DATABASE_URL'])
30 |
31 | app.include_router(user_router.router)
32 | app.include_router(tracker_router.router)
33 |
34 |
35 | # ---------- MANAGE GOALS ---------- #
36 |
37 | # CREATE new goal
38 | @app.post('/goals/', response_model=schema.Goal)
39 | def create_goal(goal: schema.Goal):
40 |
41 | db_goal = models.Goal(
42 | goal = goal.goal,
43 | has_amount = goal.has_amount,
44 | date_created = goal.date_created,
45 | )
46 | db.session.add(db_goal)
47 | db.session.commit()
48 | return db_goal
49 |
50 | # READ all goals
51 | @app.get('/goals/')
52 | def read_goals():
53 | db_goals = db.session.query(models.Goal).all()
54 | return db_goals
55 |
56 | # UPDATE na
57 |
58 | # DELETE a goal
59 | @app.post("/goals/delete/{id}", response_model=schema.Goal)
60 | def delete_goal(id: int):
61 | db_goal = db.session.query(models.Goal).filter(models.Goal.id == id).first()
62 | db.session.delete(db_goal)
63 | db.session.commit()
64 | return db_goal
65 |
66 | # --------- MANAGE ENTRIES --------- #
67 |
68 | # CREATE an entry
69 | @app.post("/entries/", response_model=List[schema.Entry])
70 | def create_entry(entries: List[schema.Entry]):
71 |
72 | db_entries = []
73 | for entry in entries:
74 | db_entry = models.Entry(
75 | goal_id=entry.goal_id,
76 | date=entry.date,
77 | tracked=entry.tracked,
78 | amount=entry.amount,)
79 | db.session.add(db_entry)
80 | db.session.commit()
81 | db_entries.append(db_entry)
82 | return db_entries
83 |
84 | # READ entries
85 | @app.get('/entries/')
86 | def get_entries(count: int = 365):
87 | db_entries = db.session.query(models.Entry).limit(count).all()
88 | return db_entries
89 |
90 | # UPDATE na
91 |
92 | # DELETE an existing entry
93 | @app.post("/entries/delete/", response_model=List[schema.Entry])
94 | def delete_entry(entries: List[int]):
95 | print('-------------------------------')
96 | print(entries)
97 | print('-------------------------------')
98 |
99 | db_entries = []
100 | for id in entries:
101 | db_entry = db.session.query(models.Entry).filter(models.Entry.id == id).first()
102 | db.session.delete(db_entry)
103 | db.session.commit()
104 | db_entries.append(db_entry)
105 | return db_entries
106 |
107 | if __name__ == "__main__":
108 | uvicorn.run(app, host="127.0.0.1", port=8000)
--------------------------------------------------------------------------------
/svelte/README.md:
--------------------------------------------------------------------------------
1 | *Looking for a shareable component template? Go here --> [sveltejs/component-template](https://github.com/sveltejs/component-template)*
2 |
3 | ---
4 |
5 | # svelte app
6 |
7 | This is a project template for [Svelte](https://svelte.dev) apps. It lives at https://github.com/sveltejs/template.
8 |
9 | To create a new project based on this template using [degit](https://github.com/Rich-Harris/degit):
10 |
11 | ```bash
12 | npx degit sveltejs/template svelte-app
13 | cd svelte-app
14 | ```
15 |
16 | *Note that you will need to have [Node.js](https://nodejs.org) installed.*
17 |
18 |
19 | ## Get started
20 |
21 | Install the dependencies...
22 |
23 | ```bash
24 | cd svelte-app
25 | npm install
26 | ```
27 |
28 | ...then start [Rollup](https://rollupjs.org):
29 |
30 | ```bash
31 | npm run dev
32 | ```
33 |
34 | Navigate to [localhost:5000](http://localhost:5000). You should see your app running. Edit a component file in `src`, save it, and reload the page to see your changes.
35 |
36 | By default, the server will only respond to requests from localhost. To allow connections from other computers, edit the `sirv` commands in package.json to include the option `--host 0.0.0.0`.
37 |
38 | If you're using [Visual Studio Code](https://code.visualstudio.com/) we recommend installing the official extension [Svelte for VS Code](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). If you are using other editors you may need to install a plugin in order to get syntax highlighting and intellisense.
39 |
40 | ## Building and running in production mode
41 |
42 | To create an optimised version of the app:
43 |
44 | ```bash
45 | npm run build
46 | ```
47 |
48 | You can run the newly built app with `npm run start`. This uses [sirv](https://github.com/lukeed/sirv), which is included in your package.json's `dependencies` so that the app will work when you deploy to platforms like [Heroku](https://heroku.com).
49 |
50 |
51 | ## Single-page app mode
52 |
53 | By default, sirv will only respond to requests that match files in `public`. This is to maximise compatibility with static fileservers, allowing you to deploy your app anywhere.
54 |
55 | If you're building a single-page app (SPA) with multiple routes, sirv needs to be able to respond to requests for *any* path. You can make it so by editing the `"start"` command in package.json:
56 |
57 | ```js
58 | "start": "sirv public --single"
59 | ```
60 |
61 | ## Using TypeScript
62 |
63 | This template comes with a script to set up a TypeScript development environment, you can run it immediately after cloning the template with:
64 |
65 | ```bash
66 | node scripts/setupTypeScript.js
67 | ```
68 |
69 | Or remove the script via:
70 |
71 | ```bash
72 | rm scripts/setupTypeScript.js
73 | ```
74 |
75 | ## Deploying to the web
76 |
77 | ### With [Vercel](https://vercel.com)
78 |
79 | Install `vercel` if you haven't already:
80 |
81 | ```bash
82 | npm install -g vercel
83 | ```
84 |
85 | Then, from within your project folder:
86 |
87 | ```bash
88 | cd public
89 | vercel deploy --name my-project
90 | ```
91 |
92 | ### With [surge](https://surge.sh/)
93 |
94 | Install `surge` if you haven't already:
95 |
96 | ```bash
97 | npm install -g surge
98 | ```
99 |
100 | Then, from within your project folder:
101 |
102 | ```bash
103 | npm run build
104 | surge public my-project.surge.sh
105 | ```
106 |
--------------------------------------------------------------------------------
/api/alembic/env.py:
--------------------------------------------------------------------------------
1 | from logging.config import fileConfig
2 |
3 | from sqlalchemy import engine_from_config
4 | from sqlalchemy import pool
5 |
6 | from alembic import context
7 |
8 | # ---------------- added code here -------------------------#
9 | import os, sys
10 | from dotenv import load_dotenv
11 |
12 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
13 | load_dotenv(os.path.join(BASE_DIR, ".env"))
14 | sys.path.append(BASE_DIR)
15 |
16 | #------------------------------------------------------------#
17 |
18 | # this is the Alembic Config object, which provides
19 | # access to the values within the .ini file in use.
20 | config = context.config
21 |
22 | # ---------------- added code here -------------------------#
23 | # this will overwrite the ini-file sqlalchemy.url path
24 | # with the path given in the config of the main code
25 | config.set_main_option('sqlalchemy.url', os.environ['DATABASE_URL'])
26 |
27 | #------------------------------------------------------------#
28 |
29 | # Interpret the config file for Python logging.
30 | # This line sets up loggers basically.
31 | fileConfig(config.config_file_name)
32 |
33 | # add your model's MetaData object here
34 | # for 'autogenerate' support
35 | # from myapp import mymodel
36 | # target_metadata = mymodel.Base.metadata
37 |
38 | # ---------------- added code here -------------------------#
39 | from models import Base as ParentBase
40 | from user.model import Base as UserBase
41 | from tracker.model import Base as TrackerBase
42 | #------------------------------------------------------------#
43 |
44 |
45 | # ---------------- added code here -------------------------#
46 | # was target_metadata = None
47 | target_metadata = [ParentBase.metadata, UserBase.metadata, TrackerBase.metadata]
48 | #------------------------------------------------------------#
49 |
50 | # other values from the config, defined by the needs of env.py,
51 | # can be acquired:
52 | # my_important_option = config.get_main_option("my_important_option")
53 | # ... etc.
54 |
55 |
56 | def run_migrations_offline():
57 | """Run migrations in 'offline' mode.
58 |
59 | This configures the context with just a URL
60 | and not an Engine, though an Engine is acceptable
61 | here as well. By skipping the Engine creation
62 | we don't even need a DBAPI to be available.
63 |
64 | Calls to context.execute() here emit the given string to the
65 | script output.
66 |
67 | """
68 | url = config.get_main_option("sqlalchemy.url")
69 | context.configure(
70 | url=url,
71 | target_metadata=target_metadata,
72 | literal_binds=True,
73 | dialect_opts={"paramstyle": "named"},
74 | )
75 |
76 | with context.begin_transaction():
77 | context.run_migrations()
78 |
79 |
80 | def run_migrations_online():
81 | """Run migrations in 'online' mode.
82 |
83 | In this scenario we need to create an Engine
84 | and associate a connection with the context.
85 |
86 | """
87 | connectable = engine_from_config(
88 | config.get_section(config.config_ini_section),
89 | prefix="sqlalchemy.",
90 | poolclass=pool.NullPool,
91 | )
92 |
93 | with connectable.connect() as connection:
94 | context.configure(
95 | connection=connection, target_metadata=target_metadata
96 | )
97 |
98 | with context.begin_transaction():
99 | context.run_migrations()
100 |
101 |
102 | if context.is_offline_mode():
103 | run_migrations_offline()
104 | else:
105 | run_migrations_online()
106 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Backend Code for personal site, flip.rip
2 |
3 | Full-stack Streamlit implementation, [see this repo](https://github.com/sapped/Authenticated-Full-Stack-Streamlit) for a template you can more quickly rip. This is for my personal website. I predominately use it to track my own goals, but the idea is to put it all here. It will continue to change, but figure I'd share what I make here.
4 |
5 | ## Goals Tracker
6 | Create goals, which have optional amounts. Then track them. Store it in a Postgres DB, manage db with Pgadmin4, and GUI via Streamlit.
7 |
8 | ## Users
9 | Functionality for barebones users. Lacks basics like creation routine, logout, etc. But if you just need ***something***, I hope this does the trick for you.
10 |
11 | Relies upon:
12 | - Docker (internal, non-exposed networking & enable reverse-proxy with NGINX)
13 | - NGINX (auth_basic, $remote_user)
14 | - Implementation of Streamlit Sessionstate (important to avoid cross-talk between sessions, don't want three users logged in on different computers but they're all being served as if they were the same user)
15 |
16 | See sidebar for confirmation 'you are logged in as {user}'. Worth testing on two devices simultaneously with two different users to confirm they are both different, and not the same user.
17 |
18 | ### The magic behind my implementation
19 |
20 | #### Configure reverse proxy, the auth_basic manges the user auth
21 | Probably fancier ways to do this, but auth_basic proves the concept
22 |
23 | 1. edit file ~/main_dir/nginx/conf/project.conf
24 | 2. At every exposed endpoint, add the code I have, anything that starts with 'auth_basic' (as of writing, this is 'auth_basic' and 'auth_basic_user_file')
25 | 3. follow the README.md in ~/main_dir/nginx/auth/README.md to create users. Requires some apache2 tools installed so you can run htpasswd
26 | 4. This is it for configuration of nginx reverse proxy for auth. The magic now is: within our local docker container network, we can speak in HTTP without worrying about HTTPS stifling us ([see forum](https://discuss.streamlit.io/t/user-authentication/612/5?u=eddie))
27 |
28 | #### Forward User via HTTP
29 | 1. edit file ~/main_dir/nginx/conf/project.conf
30 | 2. go to 'location /stream {}'. Not fully sure what stream does, but I know it passes the HTTP headers to my app
31 | 3. add code "proxy_set_header $remote_user (this shares username)"
32 |
33 | #### Access headers in streamlit
34 | This one was a doozy to figure out! Thanks to all in links referenced below for valuable guidance.
35 | - see code in ~/main_dir/streamlit/user/user.py, which relies on session_state
36 | - also see how the user is grabbed in app.py
37 | - app.py grabs user and displays on sidebar
38 | - you can use from users.users import get_user on whichever page you need users. Or maybe just implement it as part of global context somehow.
39 | - in this build, look at sidebar (if collapsed, click the tiny triangle in the top left of the webpage). Should say at bottom "logged in as **{user}**".
40 |
41 | #### My Opinion on Security
42 | From what I've read in the links below, this is secure. All user communication is handled inside the docker network (internal, inaccessible by outside world). One thing this is missing for a real-life implementation is an SSL certificate. I'll let you handle that on your own - it's a straightforward nginx configuration that you can Google easily.
43 |
44 | But PLEASE let me know if you disagree! Security is essential. I want to ensure this implementation is totally safe and up to modern standards of security.
45 |
46 | ### Links referenced when designing users
47 | - https://discuss.streamlit.io/t/access-request-header-in-streamlit/1882
48 | - https://github.com/streamlit/streamlit/issues/1083
49 | - https://gist.github.com/okld/0aba4869ba6fdc8d49132e6974e2e662
50 |
51 | ## Other helpful things
52 | - dbdiagram.io lets me visualize my ERD (entity relationship diagrams). Aka, those DB tables with connector things. Right now, I think DBML is a cool language for designing databases).
53 | - play around with live dummy at [flip.rip](http://www.flip.rip), creds are "public/public"
54 |
55 | # TBU
56 | - currently no auth_basic on /stream/ endpoint
--------------------------------------------------------------------------------
/streamlit/.streamlit/config.IGNORE.toml:
--------------------------------------------------------------------------------
1 | # Below are all the sections and options you can have in ~/.streamlit/config.toml.
2 |
3 | [global]
4 |
5 | # By default, Streamlit checks if the Python watchdog module is available and, if not, prints a warning asking for you to install it. The watchdog module is not required, but highly recommended. It improves Streamlit's ability to detect changes to files in your filesystem.
6 | # If you'd like to turn off this warning, set this to True.
7 | # Default: false
8 | disableWatchdogWarning = false
9 |
10 | # Configure the ability to share apps to the cloud.
11 | # Should be set to one of these values: - "off" : turn off sharing. - "s3" : share to S3, based on the settings under the [s3] section of this config file.
12 | # Default: "off"
13 | sharingMode = "off"
14 |
15 | # If True, will show a warning when you run a Streamlit-enabled script via "python my_script.py".
16 | # Default: true
17 | showWarningOnDirectExecution = true
18 |
19 |
20 | [client]
21 |
22 | # Whether to enable st.cache.
23 | # Default: true
24 | caching = true
25 |
26 | # If false, makes your Streamlit script not draw to a Streamlit app.
27 | # Default: true
28 | displayEnabled = true
29 |
30 |
31 | [runner]
32 |
33 | # Allows you to type a variable or string by itself in a single line of Python code to write it to the app.
34 | # Default: true
35 | magicEnabled = true
36 |
37 | # Install a Python tracer to allow you to stop or pause your script at any point and introspect it. As a side-effect, this slows down your script's execution.
38 | # Default: false
39 | installTracer = false
40 |
41 | # Sets the MPLBACKEND environment variable to Agg inside Streamlit to prevent Python crashing.
42 | # Default: true
43 | fixMatplotlib = true
44 |
45 |
46 | [server]
47 |
48 | # List of folders that should not be watched for changes. Relative paths will be taken as relative to the current working directory.
49 | # Example: ['/home/user1/env', 'relative/path/to/folder']
50 | # Default: []
51 | folderWatchBlacklist = ['']
52 |
53 | enableWebsocketCompression = false
54 |
55 |
56 |
57 | # If false, will attempt to open a browser window on start.
58 | # Default: false unless (1) we are on a Linux box where DISPLAY is unset, or (2) server.liveSave is set.
59 | headless = true
60 |
61 | # Immediately share the app in such a way that enables live monitoring, and post-run analysis.
62 | # Default: false
63 | liveSave = false
64 |
65 | # Automatically rerun script when the file is modified on disk.
66 | # Default: false
67 | runOnSave = false
68 |
69 | # The port where the server will listen for client and browser connections.
70 | # Default: 8501
71 | port = 8501
72 |
73 | # Enables support for Cross-Origin Request Sharing, for added security.
74 | # Default: true
75 | enableCORS = false
76 |
77 |
78 | [browser]
79 |
80 | # Internet address of the server server that the browser should connect to. Can be IP address or DNS name.
81 | # Default: 'localhost'
82 | serverAddress = 'streamlit'
83 |
84 | # Whether to send usage statistics to Streamlit.
85 | # Default: true
86 | gatherUsageStats = true
87 |
88 | # Port that the browser should use to connect to the server when in liveSave mode.
89 | # Default: whatever value is set in server.port.
90 | serverPort = 80
91 |
92 | [s3]
93 |
94 | # Name of the AWS S3 bucket to save apps.
95 | # Default: (unset)
96 | #bucket =
97 |
98 | # URL root for external view of Streamlit apps.
99 | # Default: (unset)
100 | #url =
101 |
102 | # Access key to write to the S3 bucket.
103 | # Leave unset if you want to use an AWS profile.
104 | # Default: (unset)
105 | #accessKeyId =
106 |
107 | # Secret access key to write to the S3 bucket.
108 | # Leave unset if you want to use an AWS profile.
109 | # Default: (unset)
110 | #secretAccessKey =
111 |
112 | # The "subdirectory" within the S3 bucket where to save apps.
113 | # S3 calls paths "keys" which is why the keyPrefix is like a subdirectory. Use "" to mean the root directory.
114 | # Default: ""
115 | keyPrefix = ""
116 |
117 | # AWS region where the bucket is located, e.g. "us-west-2".
118 | # Default: (unset)
119 | #region =
120 |
121 | # AWS credentials profile to use.
122 | # Leave unset to use your default profile.
123 | # Default: (unset)
124 | #profile =
--------------------------------------------------------------------------------
/svelte/scripts/setupTypeScript.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | /** This script modifies the project to support TS code in .svelte files like:
4 |
5 |
8 |
9 | As well as validating the code for CI.
10 | */
11 |
12 | /** To work on this script:
13 | rm -rf test-template template && git clone sveltejs/template test-template && node scripts/setupTypeScript.js test-template
14 | */
15 |
16 | const fs = require("fs")
17 | const path = require("path")
18 | const { argv } = require("process")
19 |
20 | const projectRoot = argv[2] || path.join(__dirname, "..")
21 |
22 | // Add deps to pkg.json
23 | const packageJSON = JSON.parse(fs.readFileSync(path.join(projectRoot, "package.json"), "utf8"))
24 | packageJSON.devDependencies = Object.assign(packageJSON.devDependencies, {
25 | "svelte-check": "^1.0.0",
26 | "svelte-preprocess": "^4.0.0",
27 | "@rollup/plugin-typescript": "^8.0.0",
28 | "typescript": "^4.0.0",
29 | "tslib": "^2.0.0",
30 | "@tsconfig/svelte": "^1.0.0"
31 | })
32 |
33 | // Add script for checking
34 | packageJSON.scripts = Object.assign(packageJSON.scripts, {
35 | "validate": "svelte-check"
36 | })
37 |
38 | // Write the package JSON
39 | fs.writeFileSync(path.join(projectRoot, "package.json"), JSON.stringify(packageJSON, null, " "))
40 |
41 | // mv src/main.js to main.ts - note, we need to edit rollup.config.js for this too
42 | const beforeMainJSPath = path.join(projectRoot, "src", "main.js")
43 | const afterMainTSPath = path.join(projectRoot, "src", "main.ts")
44 | fs.renameSync(beforeMainJSPath, afterMainTSPath)
45 |
46 | // Switch the app.svelte file to use TS
47 | const appSveltePath = path.join(projectRoot, "src", "App.svelte")
48 | let appFile = fs.readFileSync(appSveltePath, "utf8")
49 | appFile = appFile.replace("