Compare commits

..

No commits in common. "main" and "v0.11.1" have entirely different histories.

261 changed files with 798 additions and 20850 deletions

View File

@ -1,10 +0,0 @@
# Ignore pycaches
logs
__pycache__
Pipfile*
.env*
.flaskenv*
!.env.project
!.env.vault

View File

@ -1,12 +0,0 @@
#################################################################################
# #
# This file uniquely identifies your project in dotenv-vault. #
# You SHOULD commit this file to source control. #
# #
# Generated with 'npx dotenv-vault new' #
# #
# Learn more at https://dotenv.org/env-vault #
# #
#################################################################################
DOTENV_VAULT=vlt_54536d38674329461d10b74ec7e56be43df6021bb5267bedec1f774758371784

View File

@ -4,47 +4,19 @@ on:
tags: tags:
- '*' - '*'
jobs: jobs:
release: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: https://gitea.com/actions/checkout@master - uses: actions/checkout@master
- name: Zip Artifacts - name: Archive Server
uses: https://github.com/thedoctor0/zip-release@master uses: thedoctor0/zip-release@master
with: with:
type: 'zip' type: 'zip'
filename: 'server.zip' filename: 'server.zip'
exclusions: '*.git*' exclusions: '*.git*'
- name: Release Archive - name: Release Archive
uses: https://gitea.com/actions/gitea-release-action@v1 uses: ncipollo/release-action@v1
with: with:
server_url: https://gitea.wpgcommunity.net allowUpdates: true
files: 'server.zip' artifacts: "server.zip"
docker: token: ${{ secrets.GITHUB_TOKEN }}
runs-on: ubuntu-latest
steps:
- uses: https://gitea.com/actions/checkout@master
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ vars.DOCKER_REPO }}/costhive
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
config-inline: |
[registry."${{ vars.DOCKER_REPO }}"]
http = true
insecure = true
- name: Login to Gitea Container Registry
uses: docker/login-action@v3
with:
registry: ${{ vars.DOCKER_REPO }}
username: ${{ vars.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASS }}
- name: Build and push
uses: https://github.com/docker/build-push-action@v6
with:
push: true
context: .
tags: ${{ steps.meta.outputs.tags }}

237
.gitignore vendored
View File

@ -1,45 +1,3 @@
# Created by https://www.toptal.com/developers/gitignore/api/flask,python,git,visualstudiocode,angular,venv
# Edit at https://www.toptal.com/developers/gitignore?templates=flask,python,git,visualstudiocode,angular,venv
### Angular ###
## Angular ##
# compiled output
dist/
tmp/
app/**/*.js
app/**/*.js.map
# dependencies
node_modules/
bower_components/
# IDEs and editors
.idea/
# misc
.sass-cache/
connect.lock/
coverage/
libpeerconnection.log/
npm-debug.log
testem.log
typings/
.angular/
# e2e
e2e/*.js
e2e/*.map
# System Files
.DS_Store/
### Flask ###
instance/*
!instance/.gitignore
.webassets-cache
.env
### Flask.Python Stack ###
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
@ -52,6 +10,7 @@ __pycache__/
.Python .Python
build/ build/
develop-eggs/ develop-eggs/
dist/
downloads/ downloads/
eggs/ eggs/
.eggs/ .eggs/
@ -61,6 +20,7 @@ parts/
sdist/ sdist/
var/ var/
wheels/ wheels/
pip-wheel-metadata/
share/python-wheels/ share/python-wheels/
*.egg-info/ *.egg-info/
.installed.cfg .installed.cfg
@ -90,7 +50,6 @@ coverage.xml
*.py,cover *.py,cover
.hypothesis/ .hypothesis/
.pytest_cache/ .pytest_cache/
cover/
# Translations # Translations
*.mo *.mo
@ -104,6 +63,7 @@ db.sqlite3-journal
# Flask stuff: # Flask stuff:
instance/ instance/
.webassets-cache
# Scrapy stuff: # Scrapy stuff:
.scrapy .scrapy
@ -112,7 +72,6 @@ instance/
docs/_build/ docs/_build/
# PyBuilder # PyBuilder
.pybuilder/
target/ target/
# Jupyter Notebook # Jupyter Notebook
@ -123,9 +82,7 @@ profile_default/
ipython_config.py ipython_config.py
# pyenv # pyenv
# For a library or package, you might want to ignore these files since the code is .python-version
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv # pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
@ -134,22 +91,7 @@ ipython_config.py
# install all needed dependencies. # install all needed dependencies.
#Pipfile.lock #Pipfile.lock
# poetry # PEP 582; used by e.g. github.com/David-OConnor/pyflow
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/ __pypackages__/
# Celery stuff # Celery stuff
@ -160,7 +102,9 @@ celerybeat.pid
*.sage.py *.sage.py
# Environments # Environments
.env
.venv .venv
.flaskenv
env/ env/
venv/ venv/
ENV/ ENV/
@ -185,174 +129,9 @@ dmypy.json
# Pyre type checker # Pyre type checker
.pyre/ .pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
### Git ###
# Created by git for backups. To disable backups in Git:
# $ git config --global mergetool.keepBackup false
*.orig
# Created by git when using merge tools for conflicts
*.BACKUP.*
*.BASE.*
*.LOCAL.*
*.REMOTE.*
*_BACKUP_*.txt
*_BASE_*.txt
*_LOCAL_*.txt
*_REMOTE_*.txt
### Python ###
# Byte-compiled / optimized / DLL files
# C extensions
# Distribution / packaging
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
# Installer logs
# Unit test / coverage reports
# Translations
# Django stuff:
# Flask stuff:
# Scrapy stuff:
# Sphinx documentation
# PyBuilder
# Jupyter Notebook
# IPython
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
# Celery stuff
# SageMath parsed files
# Environments
# Spyder project settings
# Rope project settings
# mkdocs documentation
# mypy
# Pyre type checker
# pytype static type analyzer
# Cython debug symbols
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
### Python Patch ###
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
poetry.toml
# ruff
.ruff_cache/
### venv ###
# Virtualenv
# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/
[Bb]in
[Ii]nclude
[Ll]ib
[Ll]ib64
[Ll]ocal
[Ss]cripts
pyvenv.cfg
pip-selfcheck.json
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
# End of https://www.toptal.com/developers/gitignore/api/flask,python,git,visualstudiocode,angular,venv
# Misc # Misc
config.yaml config.yaml
scans.json scans.json
test_*.*
test.* test.*
tests*
*.db *.db
.vscode .vscode
*.tar
*.code-workspace
.env*
.flaskenv*
!.env.project
!.env.vault
*.ps1
*.pdf
*.backup
Dockerfile.*
docker-compose.*
*debug.py

View File

@ -1,27 +1,27 @@
FROM python@sha256:c66cf219ac0083a9af2ff90e16530f16cd503c59eb7909feb3b8f3524dc1a87e FROM python@sha256:38000b248a186dcae150fe2f64d23bd44a0730347d1e5e4d1faedd449a9a4913
# python:3.12.2-slim-bullseye (amd64)
RUN useradd costhive
WORKDIR /home/costhive RUN useradd scan2kasse
RUN apt update && apt -y upgrade; \ WORKDIR /home/scan2kasse
apt install -y libpq-dev gcc g++ swig make cmake m4; \
rm -rf /var/lib/apt/lists
COPY boot.sh backend/requirements.txt ./ RUN apt update && apt upgrade
RUN python -m venv venv; \ RUN apt install -y libpq-dev gcc g++
venv/bin/pip install --upgrade pip; \
venv/bin/pip install wheel gunicorn; \
venv/bin/pip install -r requirements.txt
COPY backend backend COPY requirements.txt requirements.txt
RUN python -m venv venv
RUN venv/bin/pip install -r requirements.txt
RUN venv/bin/pip install gunicorn
ENV FLASK_APP=run.py COPY app app
COPY migrations migrations
COPY configs configs
COPY run.py boot.sh ./
RUN chmod +x boot.sh
RUN chmod +x boot.sh; \ ENV FLASK_APP run.py
chown -R costhive:costhive .
USER costhive RUN chown -R scan2kasse:scan2kasse ./
USER scan2kasse
EXPOSE 5000 EXPOSE 5000
ENTRYPOINT ["./boot.sh"] ENTRYPOINT ["./boot.sh"]

View File

@ -1 +1 @@
# CostHive # WG_Scan2Kasse

51
app/__init__.py Normal file
View File

@ -0,0 +1,51 @@
from configs.config import Config
from flask import Flask
from flask_bootstrap import Bootstrap
from flask_login import LoginManager
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from logging import getLogger
from logging.config import fileConfig
from os import makedirs
from os.path import dirname, exists
try:
dir_name = dirname(__file__)
if dir_name:
DIR = dir_name + "/"
else:
DIR = "./"
except NameError:
DIR = "./"
if not exists(DIR + "../logs"):
makedirs(DIR + "../logs")
fileConfig(DIR + "../configs/log.conf")
LOGGER = getLogger("root")
bootstrap = Bootstrap()
db = SQLAlchemy()
login = LoginManager()
login.login_view = 'web_login'
migrate = Migrate()
def create_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(config_class)
bootstrap.init_app(app)
db.init_app(app)
login.init_app(app)
migrate.init_app(app, db, render_as_batch=True)
from app.auth import bp as auth_bp
app.register_blueprint(auth_bp, url_prefix='/auth')
from app.errors import bp as errors_bp
app.register_blueprint(errors_bp)
from app.main import bp as main_bp
app.register_blueprint(main_bp)
return app
from app import models

5
app/auth/__init__.py Normal file
View File

@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint('auth', __name__)
from app.auth import forms, routes

View File

@ -1,6 +1,6 @@
from models.user import User from app.models import User
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import BooleanField, PasswordField, StringField, SubmitField from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Email, EqualTo, ValidationError from wtforms.validators import DataRequired, Email, EqualTo, ValidationError
class LoginForm(FlaskForm): class LoginForm(FlaskForm):
@ -24,14 +24,4 @@ class RegistrationForm(FlaskForm):
def validate_email(self, email): def validate_email(self, email):
user = User.query.filter_by(email=email.data).first() user = User.query.filter_by(email=email.data).first()
if user is not None: if user is not None:
raise ValidationError('Please use a different email address.') raise ValidationError('Please use a different email address.')
class ResetPasswordRequestForm(FlaskForm):
email = StringField('Email', validators=[DataRequired(), Email()])
submit = SubmitField('Request Password Reset', render_kw={"class": "btn btn-primary mt-3"})
class ResetPasswordForm(FlaskForm):
password = PasswordField('Password', validators=[DataRequired()])
password2 = PasswordField(
'Repeat Password', validators=[DataRequired(), EqualTo('password')])
submit = SubmitField('Request Password Reset', render_kw={"class": "btn btn-primary mt-3"})

44
app/auth/routes.py Normal file
View File

@ -0,0 +1,44 @@
from app import db
from app.auth import bp
from app.auth.forms import LoginForm, RegistrationForm
from app.models import User
from app.utils.routes_utils import render_custom_template as render_template
from flask import flash, redirect, request, url_for
from flask_login import current_user, login_user, logout_user
from werkzeug.urls import url_parse
@bp.route(f'/register', methods=['GET', 'POST'])
def web_register():
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = RegistrationForm()
if form.validate_on_submit():
user = User(email=form.email.data)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
flash('Congratulations, you are now a registered user!')
return redirect(url_for('auth.web_login'))
return render_template('auth/register.html', title='Register', form=form)
@bp.route(f'/login', methods=['GET', 'POST'])
def web_login():
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user is None or not user.check_password(form.password.data):
flash('Invalid email or password')
return redirect(url_for('auth.web_login'))
login_user(user, remember=form.remember_me.data)
next_page = request.args.get('next')
if not next_page or url_parse(next_page).netloc != '':
next_page = url_for('main.index')
return redirect(next_page)
return render_template('auth/login.html', title='Sign In', form=form)
@bp.route(f'/logout')
def web_logout():
logout_user()
return redirect(url_for('main.index'))

5
app/errors/__init__.py Normal file
View File

@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint('errors', __name__)
from app.errors import handlers

View File

@ -1,11 +1,7 @@
from src import db from app import db
from src.errors import bp from app.errors import bp
from flask import render_template from flask import render_template
@bp.app_errorhandler(403)
def not_allowed_error(error):
return render_template('errors/403.html'), 403
@bp.app_errorhandler(404) @bp.app_errorhandler(404)
def not_found_error(error): def not_found_error(error):
return render_template('errors/404.html'), 404 return render_template('errors/404.html'), 404

View File

@ -2,4 +2,4 @@ from flask import Blueprint
bp = Blueprint('main', __name__) bp = Blueprint('main', __name__)
from . import forms, routes from app.main import forms, routes

View File

@ -1,22 +1,37 @@
from models import Brand, Category from app.models import Brand, Category
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import DateField, FloatField, IntegerField, SelectField, SelectMultipleField, StringField, SubmitField from wtforms import DateField, FloatField, IntegerField, SelectField, SelectMultipleField, StringField, SubmitField
from wtforms.validators import DataRequired, Optional from wtforms.validators import DataRequired, Optional
class NewItemForm(FlaskForm): class NewItemForm(FlaskForm):
id = IntegerField("Product EAN", validators=[DataRequired()], render_kw={"class": "form-control"}) id = IntegerField("Product EAN", validators=[DataRequired()])
name = StringField("Name", validators=[DataRequired()], render_kw={"class": "form-control"}) name = StringField("Name", validators=[DataRequired()])
description = StringField("Description", validators=[DataRequired()], render_kw={"class": "form-control"}) description = StringField("Description", validators=[DataRequired()])
date = DateField("Insert Date", validators=[DataRequired()], render_kw={"class": "form-control"}) date = DateField("Insert Date", validators=[DataRequired()])
price_change = FloatField("Price", validators=[DataRequired()], render_kw={"class": "form-control"}) price_change = FloatField("Price", validators=[DataRequired()])
amount_change = IntegerField("Amount", validators=[Optional()], render_kw={"class": "form-control"}) amount_change = IntegerField("Amount", validators=[Optional()])
category = SelectMultipleField("Categories", choices=[], validators=[Optional()], render_kw={"class": "form-control"}) category = SelectMultipleField("Categories", choices=[], validators=[Optional()])
brand = SelectField("Brand", choices=[], validators=[DataRequired()], render_kw={"class": "form-control"}) brand = SelectField("Brand", choices=[], validators=[DataRequired()])
submit = SubmitField("Submit", render_kw={"class": "btn btn-primary mt-3"}) submit = SubmitField("Submit")
@classmethod @classmethod
def new(cls): def new(cls):
form = cls() form = cls()
form.category.choices = [(c.id, c.name) for c in Category.query.order_by("name").all()] form.category.choices = [(c.id, c.name) for c in Category.query.order_by("name").all()]
form.brand.choices = [(b.id, b.name) for b in Brand.query.order_by("name").all()] form.brand.choices = [(b.id, b.name) for b in Brand.query.order_by("name").all()]
return form return form
class NewCategoryForm(FlaskForm):
name = StringField("Name", validators=[DataRequired()])
submit = SubmitField("Submit", render_kw={"class": "btn btn-primary mt-3"})
class NewBrandForm(FlaskForm):
name = StringField("Name", validators=[DataRequired()])
submit = SubmitField("Submit", render_kw={"class": "btn btn-primary mt-3"})
class NewEstablishmentForm(FlaskForm):
establishment_name = StringField("Name", validators=[DataRequired()])
submit = SubmitField("Submit", render_kw={"class": "btn btn-primary mt-3"})
class JoinEstablishmentForm(FlaskForm):
submit = SubmitField("Submit", render_kw={"class": "btn btn-primary mt-3"})

116
app/main/routes.py Normal file
View File

@ -0,0 +1,116 @@
from app import db, LOGGER
from app.main.forms import NewItemForm
from app.main import bp
from app.models import AmountChange, Brand, Establishment, LoginToken, Item, PriceChange
from app.utils import view_utils, database_utils
from app.utils.routes_utils import render_custom_template as render_template
from datetime import date
from flask import abort, redirect, request, url_for
from flask.json import jsonify
from flask_login import current_user, login_required
@bp.route('/')
@bp.route('/index')
def index():
return render_template("base.html")
# @bp.route('/')
# def test():
# return "Hello World"
@bp.route('/overview', methods=['GET'])
@login_required
def get_report_from_user():
if current_user.is_anonymous:
abort(403)
if 'month' in request.args:
try:
month = int(request.args['month'])
except Exception as e:
LOGGER.exception("")
abort(400)
else:
if (month > 12 or month < 1):
abort(400)
LOGGER.info("Getting results.")
results = database_utils.get_report(**request.args)
LOGGER.debug(f"Results received.")
# LOGGER.debug(str(results))
if results:
result_list = view_utils.group_results(results)
else:
result_list = []
if request.content_type == "application/json":
return jsonify(result_list)
else:
if "establishment" in request.args:
return render_template("main/overview.html", results=result_list, establishment = Establishment.query.get(int(request.args['establishment'])))
else:
return render_template("main/overview.html", results=result_list)
@bp.route('/token_authorization')
def token_authorization():
LOGGER.debug("Token Login")
if not request.json or 'login' not in request.json:
abort(400)
if not LoginToken.query.filter_by(token=request.json['login']).first():
abort(403)
return jsonify({}), 200
@bp.route('/token_insert', methods=['POST'])
def insert():
match request.json:
case {'user': user, 'items': items, 'date': date}:
failed = database_utils.insert_bought_items(user, items, date)
case {'user': user, 'items': items}:
failed = database_utils.insert_bought_items(user, items)
case _:
abort(400)
if failed:
return jsonify(failed), 400
return jsonify({'inserted': True}), 201
@bp.route('/new_item', methods=['GET', 'POST'])
@login_required
def new_item():
if current_user.is_anonymous:
abort(403)
form=NewItemForm.new()
if form.is_submitted():
LOGGER.debug("submitted")
if form.validate():
LOGGER.debug("valid")
else:
LOGGER.debug(form.errors)
if form.validate_on_submit():
LOGGER.debug("valid form")
brand = Brand.query.get(form.brand.data)
new_item = Item(id = form.id.data, name = form.name.data, brand = brand.id, description = form.description.data)
# if form.category.data:
# category = Category.query.get(id = form.category.data)
# new_item.Category = category
new_item.PriceChange = [PriceChange(Item = new_item, date = date(2021, 12, 1), price = form.price_change.data)]
if form.amount_change.data:
new_item.AmountChange = [AmountChange(Item = new_item, date = date(2021, 12, 1), amount = form.amount_change.data)]
db.session.add(new_item)
db.session.commit()
return redirect(url_for('index'))
return render_template('main/new_item.html', form=form)
@bp.route('/overview/register_boughts', methods=['GET'])
@login_required
def check_unregistered_items():
if current_user.is_anonymous or not request.args or 'establishment' not in request.args:
abort(403)
establishment = Establishment.query.get(int(request.args['establishment']))
if current_user.id != establishment.owner:
abort(403)
results = database_utils.get_unregistered_and_register(establishment.id)
if results:
result_list = view_utils.group_results(results)
else:
result_list = []
if request.content_type == "application/json":
return jsonify(result_list)
else:
return render_template("main/overview.html", results=result_list)

126
app/models.py Normal file
View File

@ -0,0 +1,126 @@
from app import db, login
from datetime import date
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
item_category = db.Table("item_category",
db.Column("item", db.ForeignKey("item.id"), primary_key=True, server_onupdate=db.FetchedValue()),
db.Column("category", db.ForeignKey("category.id"), primary_key=True, server_onupdate=db.FetchedValue())
)
class User(UserMixin, db.Model):
id = db.Column(db.BigInteger, primary_key=True)
email = db.Column(db.String(255), nullable=False, unique=True)
password_hash = db.Column(db.String(128), nullable=False)
LoginToken = db.relationship("LoginToken", backref='User', lazy='dynamic')
Bought = db.relationship("Bought", secondary="login_token",
lazy='dynamic', overlaps="User,LoginToken")
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
def __repr__(self) -> str:
return f"<User {self.id} ({self.email})>"
class Establishment(db.Model):
id = db.Column(db.BigInteger, primary_key=True)
name = db.Column(db.String(64), nullable=False)
owner = db.Column(db.ForeignKey('user.id'), nullable=False)
LoginToken = db.relationship("LoginToken", backref='Establishment', lazy='dynamic')
Bought = db.relationship("Bought", secondary="login_token",
lazy='dynamic', overlaps="Establishment,LoginToken,Bought")
def __repr__(self) -> str:
return f"<Establishment {self.id} ({self.name})>"
class LoginToken(db.Model):
user = db.Column(db.ForeignKey('user.id'), primary_key=True, server_onupdate=db.FetchedValue())
establishment = db.Column(db.ForeignKey('establishment.id'), primary_key=True, server_onupdate=db.FetchedValue())
token = db.Column(db.String(15), nullable=True, unique=True)
paid = db.Column(db.BigInteger, nullable=False, server_default=str(0))
def __repr__(self) -> str:
return f"LoginToken {self.token}"
class Brand(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(32), nullable=False)
def __repr__(self) -> str:
return f"<Brand {self.id} ({self.name})>"
class Category(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(32), nullable=False, unique=True)
Item = db.relationship("Item", secondary=item_category, lazy="dynamic", back_populates="Category")
def __repr__(self) -> str:
return f"<Category {self.id} ({self.name})>"
class Item(db.Model):
id = db.Column(db.BigInteger, primary_key=True, autoincrement=False)
name = db.Column(db.String(64), nullable=False)
brand = db.Column(db.ForeignKey('brand.id'), nullable=False, server_onupdate=db.FetchedValue())
description = db.Column(db.Text, nullable=False)
Category = db.relationship("Category", secondary=item_category, lazy="dynamic", back_populates="Item")
Bought = db.relationship("Bought", backref='Item', lazy='dynamic')
PriceChange = db.relationship("PriceChange", backref='Item', lazy='dynamic')
AmountChange = db.relationship("AmountChange", backref='Item', lazy='dynamic')
def __repr__(self) -> str:
return f"<Item {self.id} ({self.name})>"
class Bought(db.Model):
token = db.Column(db.ForeignKey('login_token.token'), primary_key=True, server_onupdate=db.FetchedValue())
item = db.Column(db.ForeignKey('item.id'), primary_key=True, server_onupdate=db.FetchedValue())
date = db.Column(db.Date, primary_key=True)
amount = db.Column(db.SmallInteger, nullable=False)
registered = db.Column(db.Boolean, nullable=False, server_default=str(False))
def __repr__(self) -> str:
return f"<Bought Object>"
class PriceChange(db.Model):
item = db.Column(db.ForeignKey('item.id'), primary_key=True, server_onupdate=db.FetchedValue())
date = db.Column(db.Date, primary_key=True, server_default=str(date(2021, 12, 1)))
price = db.Column(db.SmallInteger, nullable=False)
def __repr__(self) -> str:
return f"<Price_Change {self.item} ({self.date})>"
class AmountChange(db.Model):
item = db.Column(db.ForeignKey('item.id'), primary_key=True, server_onupdate=db.FetchedValue())
date = db.Column(db.Date, primary_key=True, server_default=str(date(2021, 12, 1)))
amount = db.Column(db.SmallInteger, nullable=False, server_default=str(1))
def __repr__(self) -> str:
return f"<Amount_Change {self.item} ({self.date})>"
class Receipt(db.Model):
id = db.Column(db.Numeric(precision=24, scale=0), primary_key=True, autoincrement=False)
date = db.Column(db.Date, nullable=False)
from_user = db.Column(db.ForeignKey("login_token.token"), server_onupdate=db.FetchedValue())
registered = db.Column(db.Boolean, nullable=False, server_default=str(False))
def __repr__(self) -> str:
return f"<Receipt {self.id}>"
class ItemReceipt(db.Model):
receipt = db.Column(db.ForeignKey("receipt.id"), primary_key=True, server_onupdate=db.FetchedValue())
item = db.Column(db.ForeignKey("item.id"), primary_key=True, server_onupdate=db.FetchedValue())
amount = db.Column(db.SmallInteger, nullable=False)
def __repr__(self) -> str:
return f"<ItemReceipt {self.receipt}: {self.item}>"
@login.user_loader
def load_user(id):
return User.query.get(int(id))

View File

@ -0,0 +1,19 @@
{% extends "base.html" %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
<h1>Sign In</h1>
<form action="" method="post" novalidate>
{{ form.hidden_tag() }}
<p>
{{ wtf.form_field(form.email) }}
</p>
<p>
{{ wtf.form_field(form.password) }}
</p>
<p>
{{ wtf.form_field(form.remember_me) }}
</p>
{{ wtf.form_field(form.submit, class=form.submit.render_kw["class"]) }}
</form>
{% endblock %}

View File

@ -0,0 +1,19 @@
{% extends "base.html" %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
<h1>Register</h1>
<form action="" method="post">
{{ form.hidden_tag() }}
<p>
{{ wtf.form_field(form.email) }}
</p>
<p>
{{ wtf.form_field(form.password) }}
</p>
<p>
{{ wtf.form_field(form.password2) }}
</p>
{{ wtf.form_field(form.submit, class=form.submit.render_kw["class"]) }}
</form>
{% endblock %}

116
app/templates/base.html Normal file
View File

@ -0,0 +1,116 @@
{% extends "bootstrap/base.html" %}
{% block title %}
{% if title %}
{{ title }}
{% else %}
Scan2Kasse
{% endif %}
{% endblock %}
{% block metas %}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
{% endblock %}
{% block styles %}
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<link rel="stylesheet" href={{ url_for('static', filename="sidebars.css")}}>
{% endblock %}
{% block navbar %}
<main>
<div class="flex-shrink-0 p-3 bg-black bg-opacity-10 scrollbar-primary h-100 position-fixed" style="width: 280px;">
<a href="/" class="d-flex align-items-center pb-3 mb-3 link-dark text-decoration-none border-bottom border-dark">
<svg class="bi me-2" width="30" height="24"><use xlink:href="#bootstrap"></use></svg>
<span class="fs-5 fw-semibold">Scan2Kasse</span>
</a>
<ul class="list-unstyled ps-0">
<li class="mb-1">
<button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse" data-bs-target="#home-collapse" aria-expanded="true">
Home (WIP)
</button>
<div class="collapse show" id="home-collapse">
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
<li><a href="#" class="link-dark rounded">Übersicht</a></li>
<li><a href="#" class="link-dark rounded">Updates</a></li>
<li><a href="#" class="link-dark rounded">Reports</a></li>
</ul>
</div>
</li>
<li class="mb-1">
<button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse" data-bs-target="#dashboard-collapse" aria-expanded="false">
Übersicht
</button>
<div class="collapse" id="dashboard-collapse">
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
{% if establishments %}
<li><a href="#" class="link-dark rounded">Allgemein</a></li>
{% for establishment in establishments %}
<li><a href="{{ url_for('main.get_report_from_user', establishment=establishment.id) }}" class="link-dark rounded">{{ establishment.name }}</a></li>
{% endfor %}
{% endif %}
</ul>
</div>
</li>
<!-- <li class="mb-1">
<button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse" data-bs-target="#orders-collapse" aria-expanded="false">
Orders
</button>
<div class="collapse" id="orders-collapse">
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
<li><a href="#" class="link-dark rounded">New</a></li>
<li><a href="#" class="link-dark rounded">Processed</a></li>
<li><a href="#" class="link-dark rounded">Shipped</a></li>
<li><a href="#" class="link-dark rounded">Returned</a></li>
</ul>
</div>
</li> -->
<li class="border-top border-dark my-3"></li>
<li class="mb-1">
<button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse" data-bs-target="#account-collapse" aria-expanded="false">
Account
</button>
<div class="collapse" id="account-collapse" style="">
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
<!-- <li><a href="#" class="link-dark rounded">New...</a></li>
<li><a href="#" class="link-dark rounded">Settings</a></li> -->
{% if current_user.is_authenticated %}
<!-- <li><a href="#" class="link-dark rounded">Profile</a></li> -->
<li><a href={{ url_for('auth.web_logout') }} class="link-dark rounded">Sign out</a></li>
{% else %}
<li><a href={{ url_for('auth.web_register') }} class="link-dark rounded">Register</a></li>
<li><a href={{ url_for('auth.web_login') }} class="link-dark rounded">Sign in</a></li>
{% endif %}
</ul>
</div>
</li>
</ul>
</div>
{% endblock %}
{% block content %}
{% with messages = get_flashed_messages() %}
{% if messages %}
<ul>
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
<div style="width: 280px;"></div>
<div class="container">
<div class="row my-3"></div>
<div class="row-md-3">
{% block app_content %}{% endblock %}
</div>
<div class="row my-3"></div>
</div>
</main>
{% endblock %}
{% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
<!-- <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> -->
{% endblock %}

View File

@ -0,0 +1,24 @@
{% extends "base.html" %}
{% block app_content %}
<form action="" method="post">
{{ form.hidden_tag() }}
<div class="row">{{form.id.label}}</div>
<div class="row">{{form.id()}}</div>
<div class="row">{{form.name.label}}</div>
<div class="row">{{form.name()}}</div>
<div class="row">{{form.description.label}}</div>
<div class="row">{{form.description()}}</div>
<div class="row">{{form.date.label}}</div>
<div class="row">{{form.date()}}</div>
<div class="row">{{form.price_change.label}}</div>
<div class="row">{{form.price_change()}}</div>
<div class="row">{{form.amount_change.label}}</div>
<div class="row">{{form.amount_change()}}</div>
<div class="row">{{form.category.label}}</div>
<div class="row">{{form.category()}}</div>
<div class="row">{{form.brand.label}}</div>
<div class="row">{{form.brand()}}</div>
<div class="row">{{form.submit()}}</div>
</form>
{% endblock %}

View File

@ -0,0 +1,38 @@
{% extends "base.html" %}
{% block app_content %}
{% if establishment %}
{% if current_user.id == establishment.owner %}
<button type="button" class="btn btn-outline-dark px-2" data-bs-toggle="button" autocomplete="off" onclick="window.location.href='{{ url_for('main.check_unregistered_items', establishment=establishment.id) }}'">
Abrechnung
</button>
{% endif %}
{% endif %}
{% for user in results %}
<div class="card">
<button class="btn btn-primary" data-bs-toggle="collapse" data-bs-target="#b{{ user.id }}" aria-expanded="true">
<div class="card-header">
<h3>{{ user.email }}: {{ user.sum/100 }} €</h3>
</div>
</button>
<div class="collapse" id="b{{ user.id }}">
{% for item_infos in user.item_infos %}
<div class="card-body">
<div class="col-sm-1"></div>
<div class="col">
<h4>{{ item_infos.date }}</h4>
{% for item in item_infos.item_list %}
<div class="row">
<div class="col-sm-1"></div>
<div class="col">
{{ item.amount }}x {{ item.name }} je {{ item.price/100 }} €
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
{% endblock %}

1
app/utils/constants.py Normal file
View File

@ -0,0 +1 @@
APPNAME = "scan2kasse"

View File

@ -0,0 +1,83 @@
from app import db, LOGGER
from app.models import Bought, Establishment, Item, LoginToken, User
from app.utils.view_utils import bought_with_prices as bwp
from copy import deepcopy
from datetime import date as dtdate, timedelta
from flask_login import current_user
from psycopg2 import errors
from random import choice as rndchoice
from sqlalchemy import and_, text
from sqlalchemy.dialects.postgresql import insert
from string import ascii_letters, digits
def insert_bought_items(token: str, items: dict, date: str = None):
if not date:
date = dtdate.today()
for item, amount in deepcopy(items).items():
query_insert = insert(Bought).values(token=token, item=int(item), date=date, amount=int(amount))
query_insert = query_insert.on_conflict_do_update("bought_pkey", set_=dict(amount=text(f'bought.amount + {amount}')))
try:
db.session.execute(query_insert)
db.session.commit()
except errors.ForeignKeyViolation as e:
db.session.rollback()
except Exception as e:
db.session.rollback()
LOGGER.exception("")
else:
del(items[item])
return {'user':token, 'date': date, 'items': items} if items else {}
def get_report(**kwargs):
query_select = db.session.query(bwp.c.token, User.email, bwp.c.date, bwp.c.item, Item.name, bwp.c.amount, bwp.c.price)
query_select = query_select.select_from(bwp).join(LoginToken, LoginToken.token==bwp.c.token).join(User, LoginToken.user==User.id).join(Item, Item.id==bwp.c.item)
match kwargs:
case {"token": token}:
LOGGER.debug("Token present")
query_select = query_select.filter_by(token == token)
case {"establishment": establishment}:
LOGGER.debug("Establishment present")
if current_user.id == Establishment.query.get(int(establishment)).owner:
_filter = db.session.query(LoginToken.token).filter_by(establishment = int(establishment))
else:
_filter = db.session.query(LoginToken.token).filter_by(establishment = int(establishment), user=current_user.id)
query_select = query_select.filter(bwp.c.token.in_(_filter))
# LOGGER.debug(str(query_select))
match kwargs:
case {"month": month}:
LOGGER.debug("Month present")
year = kwargs["year"] if "year" in kwargs else dtdate.today().year
query_select = query_select.filter(bwp.c.date.between(dtdate(int(year), int(month), 1), dtdate(int(year), int(month)+1, 1)-timedelta(days=1)))
case {"year": year}:
LOGGER.debug("Year present")
query_select = query_select.filter(bwp.c.date.between(dtdate(int(year), 1, 1), dtdate(int(year), 12, 31)))
query_select = query_select.order_by(bwp.c.token, bwp.c.date, bwp.c.item)
# LOGGER.debug(str(query_select))
results = query_select.all()
return tuple(results)
def get_unregistered_and_register(intEstablishment: int):
LOGGER.debug("Getting unregistered")
establishment = Establishment.query.get(intEstablishment)
if current_user.id != establishment.owner:
LOGGER.debug("!!!Wrong User!!!")
return False
query_select = db.session.query(bwp.c.token, User.email, bwp.c.date, bwp.c.item, Item.name, bwp.c.amount, bwp.c.price)
query_select = query_select.select_from(bwp).join(LoginToken, LoginToken.token==bwp.c.token).join(User, LoginToken.user==User.id)
query_select = query_select.join(Item, Item.id==bwp.c.item).join(Bought, and_(Bought.token==bwp.c.token, Bought.item==bwp.c.item, Bought.date==bwp.c.date))
query_select = query_select.filter(bwp.c.token.in_(db.session.query(LoginToken.token).filter_by(establishment = intEstablishment)))
query_select = query_select.filter(Bought.registered == False)
query_select = query_select.order_by(bwp.c.token, bwp.c.date, bwp.c.item)
results = query_select.all()
unregistered_boughts = establishment.Bought.filter_by(registered = False).all()
for x in unregistered_boughts:
x.registered = True
db.session.commit()
return results
def generate_token(length = 15, allowed_chars = ascii_letters + digits):
new_token = "".join((rndchoice(allowed_chars) for i in range(length)))
if not LoginToken.query.filter_by(token=new_token).first():
return new_token
return generate_token()

View File

@ -1,7 +1,6 @@
from datetime import date from datetime import date
from flask import render_template from flask import render_template
from flask_login import current_user from flask_login import current_user
from src import LOGGER
def get_base_infos(): def get_base_infos():
@ -12,9 +11,8 @@ def get_base_infos():
tokens = current_user.LoginToken.all() tokens = current_user.LoginToken.all()
establishments = [logintoken.Establishment for logintoken in tokens] establishments = [logintoken.Establishment for logintoken in tokens]
if establishments: if establishments:
infos['current_user_establishments'] = establishments infos['establishments'] = establishments
return infos return infos
def render_custom_template(*args, **kwargs): def render_custom_template(*args, **kwargs):
LOGGER.debug("Rendering template")
return render_template(*args, **kwargs, **get_base_infos()) return render_template(*args, **kwargs, **get_base_infos())

52
app/utils/view_utils.py Normal file
View File

@ -0,0 +1,52 @@
from app import db, LOGGER
from app.models import AmountChange, Bought, PriceChange
from sqlalchemy_utils import create_view
def group_results(results: tuple) -> list:
result_list = []
LOGGER.debug("Grouping...")
for result in results:
try:
result_user_index = [result[0] == result_item['id'] for result_item in result_list].index(True)
except ValueError as e:
result_list.append({"id": result[0], "email": result[1], "sum": 0, "item_infos": []})
result_user_index = -1
result_user = result_list[result_user_index]
try:
result_date_index = [result[2] == result_list_date['date'] for result_list_date in result_user["item_infos"]].index(True)
except ValueError as e:
result_user["item_infos"].append({'date': result[2], 'item_list': []})
result_date_index = -1
result_date = result_user['item_infos'][result_date_index]
result_date['item_list'].append({'id': result[3], 'name': result[4], 'amount': result[5], 'price': result[6]})
for result_user in result_list:
for result_date in result_user['item_infos']:
for result_item in result_date['item_list']:
result_user['sum'] += result_item['amount'] * result_item['price']
LOGGER.debug("Grouped.")
return result_list
def selectable_price_per_amount_view():
p = db.aliased(PriceChange, name="p")
a = db.aliased(AmountChange, name="a")
date = db.func.greatest(p.date, a.date).label("date")
price = db.func.ceil(p.price.cast(db.Float)/db.func.coalesce(a.amount, 1)).label("price")
select = db.select(p.item.label("item"), date, price)
select = select.distinct(p.item, date)
select = select.join(a, p.item==a.item, isouter=True)
select = select.order_by(p.item, db.desc(db.func.greatest(p.date, a.date)))
return select
price_per_amount = create_view("price_per_amount", selectable_price_per_amount_view(), db.metadata)
def selectable_bought_with_prices_view():
b = db.aliased(Bought, name="b")
ppa = price_per_amount.alias("ppa")
select = db.select(b.token.label("token"), b.date.label("date"), b.item.label("item"), b.amount.label("amount"), ppa.c.price.label("price"))
select = select.distinct(b.token, b.date, b.item)
select = select.join(ppa, b.item==ppa.c.item)
select = select.where(ppa.c.date<=b.date)
select = select.order_by(db.desc(b.token), db.desc(b.date), db.desc(b.item), db.desc(ppa.c.date))
return select
bought_with_prices = create_view("bought_with_prices", selectable_bought_with_prices_view(), db.metadata)

View File

@ -1,31 +0,0 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
email-validator = "*"
flask = "*"
flask-bootstrap = "*"
flask-cors = "*"
flask-login = "*"
flask-mail = "*"
flask-marshmallow = "*"
flask-migrate = "*"
flask-sqlalchemy = "*"
flask-wtf = "*"
marshmallow-sqlalchemy = "*"
psycopg2-binary = "*"
pyjwt = "*"
python-dotenv = "*"
pymupdf = "*"
requests = "*"
sqlalchemy-utils = "*"
wtforms-sqlalchemy = "*"
pytest = "*"
[dev-packages]
[requires]
python_version = "3.11"
python_full_version = "3.11.4"

721
backend/Pipfile.lock generated
View File

@ -1,721 +0,0 @@
{
"_meta": {
"hash": {
"sha256": "6f12189a28bcfca261128bb19798f69649619d796466a01c69fe3a8df6457fbf"
},
"pipfile-spec": 6,
"requires": {
"python_full_version": "3.11.4",
"python_version": "3.11"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"alembic": {
"hashes": [
"sha256:47d52e3dfb03666ed945becb723d6482e52190917fdb47071440cfdba05d92cb",
"sha256:bca5877e9678b454706347bc10b97cb7d67f300320fa5c3a94423e8266e2823f"
],
"markers": "python_version >= '3.7'",
"version": "==1.12.1"
},
"blinker": {
"hashes": [
"sha256:c3f865d4d54db7abc53758a01601cf343fe55b84c1de4e3fa910e420b438d5b9",
"sha256:e6820ff6fa4e4d1d8e2747c2283749c3f547e4fee112b98555cdcdae32996182"
],
"markers": "python_version >= '3.8'",
"version": "==1.7.0"
},
"certifi": {
"hashes": [
"sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082",
"sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"
],
"markers": "python_version >= '3.6'",
"version": "==2023.7.22"
},
"charset-normalizer": {
"hashes": [
"sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027",
"sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087",
"sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786",
"sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8",
"sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09",
"sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185",
"sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574",
"sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e",
"sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519",
"sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898",
"sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269",
"sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3",
"sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f",
"sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6",
"sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8",
"sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a",
"sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73",
"sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc",
"sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714",
"sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2",
"sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc",
"sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce",
"sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d",
"sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e",
"sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6",
"sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269",
"sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96",
"sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d",
"sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a",
"sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4",
"sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77",
"sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d",
"sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0",
"sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed",
"sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068",
"sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac",
"sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25",
"sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8",
"sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab",
"sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26",
"sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2",
"sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db",
"sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f",
"sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5",
"sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99",
"sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c",
"sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d",
"sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811",
"sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa",
"sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a",
"sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03",
"sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b",
"sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04",
"sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c",
"sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001",
"sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458",
"sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389",
"sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99",
"sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985",
"sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537",
"sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238",
"sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f",
"sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d",
"sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796",
"sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a",
"sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143",
"sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8",
"sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c",
"sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5",
"sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5",
"sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711",
"sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4",
"sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6",
"sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c",
"sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7",
"sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4",
"sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b",
"sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae",
"sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12",
"sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c",
"sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae",
"sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8",
"sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887",
"sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b",
"sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4",
"sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f",
"sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5",
"sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33",
"sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519",
"sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"
],
"markers": "python_full_version >= '3.7.0'",
"version": "==3.3.2"
},
"click": {
"hashes": [
"sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28",
"sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"
],
"markers": "python_version >= '3.7'",
"version": "==8.1.7"
},
"colorama": {
"hashes": [
"sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44",
"sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"
],
"markers": "sys_platform == 'win32'",
"version": "==0.4.6"
},
"dnspython": {
"hashes": [
"sha256:57c6fbaaeaaf39c891292012060beb141791735dbb4004798328fc2c467402d8",
"sha256:8dcfae8c7460a2f84b4072e26f1c9f4101ca20c071649cb7c34e8b6a93d58984"
],
"markers": "python_version >= '3.8' and python_version < '4.0'",
"version": "==2.4.2"
},
"dominate": {
"hashes": [
"sha256:1a916479c45b95fedba0d077b081d77c2d2e0f0f484ac827105087af23661d73",
"sha256:4c90c3befaf88e612b71f4b39af7bcbef8977acfa855cec957225a8fbf504007"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.8.0"
},
"email-validator": {
"hashes": [
"sha256:a4b0bd1cf55f073b924258d19321b1f3aa74b4b5a71a42c305575dba920e1a44",
"sha256:c973053efbeddfef924dc0bd93f6e77a1ea7ee0fce935aea7103c7a3d6d2d637"
],
"index": "pypi",
"version": "==2.1.0.post1"
},
"flask": {
"hashes": [
"sha256:21128f47e4e3b9d597a3e8521a329bf56909b690fcc3fa3e477725aa81367638",
"sha256:cfadcdb638b609361d29ec22360d6070a77d7463dcb3ab08d2c2f2f168845f58"
],
"index": "pypi",
"version": "==3.0.0"
},
"flask-bootstrap": {
"hashes": [
"sha256:cb08ed940183f6343a64e465e83b3a3f13c53e1baabb8d72b5da4545ef123ac8"
],
"index": "pypi",
"version": "==3.3.7.1"
},
"flask-cors": {
"hashes": [
"sha256:bc3492bfd6368d27cfe79c7821df5a8a319e1a6d5eab277a3794be19bdc51783",
"sha256:f268522fcb2f73e2ecdde1ef45e2fd5c71cc48fe03cffb4b441c6d1b40684eb0"
],
"index": "pypi",
"version": "==4.0.0"
},
"flask-login": {
"hashes": [
"sha256:5e23d14a607ef12806c699590b89d0f0e0d67baeec599d75947bf9c147330333",
"sha256:849b25b82a436bf830a054e74214074af59097171562ab10bfa999e6b78aae5d"
],
"index": "pypi",
"version": "==0.6.3"
},
"flask-mail": {
"hashes": [
"sha256:22e5eb9a940bf407bcf30410ecc3708f3c56cc44b29c34e1726fe85006935f41"
],
"index": "pypi",
"version": "==0.9.1"
},
"flask-marshmallow": {
"hashes": [
"sha256:2083ae55bebb5142fff98c6bbd483a2f5dbc531a8bc1be2180ed5f75e7f3fccc",
"sha256:ce08a153f74da6ebfffd8065d1687508b0179df37fff7fc0c8f28ccfb64f1b56"
],
"index": "pypi",
"version": "==0.15.0"
},
"flask-migrate": {
"hashes": [
"sha256:613a2df703998e78716cace68cd83972960834424457f5b67f56e74fff950aef",
"sha256:d3f437a8b5f3849d1bb1b60e1b818efc564c66e3fefe90b62e5db08db295e1b1"
],
"index": "pypi",
"version": "==4.0.5"
},
"flask-sqlalchemy": {
"hashes": [
"sha256:4ba4be7f419dc72f4efd8802d69974803c37259dd42f3913b0dcf75c9447e0a0",
"sha256:e4b68bb881802dda1a7d878b2fc84c06d1ee57fb40b874d3dc97dabfa36b8312"
],
"index": "pypi",
"version": "==3.1.1"
},
"flask-wtf": {
"hashes": [
"sha256:8bb269eb9bb46b87e7c8233d7e7debdf1f8b74bf90cc1789988c29b37a97b695",
"sha256:fa6793f2fb7e812e0fe9743b282118e581fb1b6c45d414b8af05e659bd653287"
],
"index": "pypi",
"version": "==1.2.1"
},
"greenlet": {
"hashes": [
"sha256:0a02d259510b3630f330c86557331a3b0e0c79dac3d166e449a39363beaae174",
"sha256:0b6f9f8ca7093fd4433472fd99b5650f8a26dcd8ba410e14094c1e44cd3ceddd",
"sha256:100f78a29707ca1525ea47388cec8a049405147719f47ebf3895e7509c6446aa",
"sha256:1757936efea16e3f03db20efd0cd50a1c86b06734f9f7338a90c4ba85ec2ad5a",
"sha256:19075157a10055759066854a973b3d1325d964d498a805bb68a1f9af4aaef8ec",
"sha256:19bbdf1cce0346ef7341705d71e2ecf6f41a35c311137f29b8a2dc2341374565",
"sha256:20107edf7c2c3644c67c12205dc60b1bb11d26b2610b276f97d666110d1b511d",
"sha256:22f79120a24aeeae2b4471c711dcf4f8c736a2bb2fabad2a67ac9a55ea72523c",
"sha256:2847e5d7beedb8d614186962c3d774d40d3374d580d2cbdab7f184580a39d234",
"sha256:28e89e232c7593d33cac35425b58950789962011cc274aa43ef8865f2e11f46d",
"sha256:329c5a2e5a0ee942f2992c5e3ff40be03e75f745f48847f118a3cfece7a28546",
"sha256:337322096d92808f76ad26061a8f5fccb22b0809bea39212cd6c406f6a7060d2",
"sha256:3fcc780ae8edbb1d050d920ab44790201f027d59fdbd21362340a85c79066a74",
"sha256:41bdeeb552d814bcd7fb52172b304898a35818107cc8778b5101423c9017b3de",
"sha256:4eddd98afc726f8aee1948858aed9e6feeb1758889dfd869072d4465973f6bfd",
"sha256:52e93b28db27ae7d208748f45d2db8a7b6a380e0d703f099c949d0f0d80b70e9",
"sha256:55d62807f1c5a1682075c62436702aaba941daa316e9161e4b6ccebbbf38bda3",
"sha256:5805e71e5b570d490938d55552f5a9e10f477c19400c38bf1d5190d760691846",
"sha256:599daf06ea59bfedbec564b1692b0166a0045f32b6f0933b0dd4df59a854caf2",
"sha256:60d5772e8195f4e9ebf74046a9121bbb90090f6550f81d8956a05387ba139353",
"sha256:696d8e7d82398e810f2b3622b24e87906763b6ebfd90e361e88eb85b0e554dc8",
"sha256:6e6061bf1e9565c29002e3c601cf68569c450be7fc3f7336671af7ddb4657166",
"sha256:80ac992f25d10aaebe1ee15df45ca0d7571d0f70b645c08ec68733fb7a020206",
"sha256:816bd9488a94cba78d93e1abb58000e8266fa9cc2aa9ccdd6eb0696acb24005b",
"sha256:85d2b77e7c9382f004b41d9c72c85537fac834fb141b0296942d52bf03fe4a3d",
"sha256:87c8ceb0cf8a5a51b8008b643844b7f4a8264a2c13fcbcd8a8316161725383fe",
"sha256:89ee2e967bd7ff85d84a2de09df10e021c9b38c7d91dead95b406ed6350c6997",
"sha256:8bef097455dea90ffe855286926ae02d8faa335ed8e4067326257cb571fc1445",
"sha256:8d11ebbd679e927593978aa44c10fc2092bc454b7d13fdc958d3e9d508aba7d0",
"sha256:91e6c7db42638dc45cf2e13c73be16bf83179f7859b07cfc139518941320be96",
"sha256:97e7ac860d64e2dcba5c5944cfc8fa9ea185cd84061c623536154d5a89237884",
"sha256:990066bff27c4fcf3b69382b86f4c99b3652bab2a7e685d968cd4d0cfc6f67c6",
"sha256:9fbc5b8f3dfe24784cee8ce0be3da2d8a79e46a276593db6868382d9c50d97b1",
"sha256:ac4a39d1abae48184d420aa8e5e63efd1b75c8444dd95daa3e03f6c6310e9619",
"sha256:b2c02d2ad98116e914d4f3155ffc905fd0c025d901ead3f6ed07385e19122c94",
"sha256:b2d3337dcfaa99698aa2377c81c9ca72fcd89c07e7eb62ece3f23a3fe89b2ce4",
"sha256:b489c36d1327868d207002391f662a1d163bdc8daf10ab2e5f6e41b9b96de3b1",
"sha256:b641161c302efbb860ae6b081f406839a8b7d5573f20a455539823802c655f63",
"sha256:b8ba29306c5de7717b5761b9ea74f9c72b9e2b834e24aa984da99cbfc70157fd",
"sha256:b9934adbd0f6e476f0ecff3c94626529f344f57b38c9a541f87098710b18af0a",
"sha256:ce85c43ae54845272f6f9cd8320d034d7a946e9773c693b27d620edec825e376",
"sha256:cf868e08690cb89360eebc73ba4be7fb461cfbc6168dd88e2fbbe6f31812cd57",
"sha256:d2905ce1df400360463c772b55d8e2518d0e488a87cdea13dd2c71dcb2a1fa16",
"sha256:d57e20ba591727da0c230ab2c3f200ac9d6d333860d85348816e1dca4cc4792e",
"sha256:d6a8c9d4f8692917a3dc7eb25a6fb337bff86909febe2f793ec1928cd97bedfc",
"sha256:d923ff276f1c1f9680d32832f8d6c040fe9306cbfb5d161b0911e9634be9ef0a",
"sha256:daa7197b43c707462f06d2c693ffdbb5991cbb8b80b5b984007de431493a319c",
"sha256:dbd4c177afb8a8d9ba348d925b0b67246147af806f0b104af4d24f144d461cd5",
"sha256:dc4d815b794fd8868c4d67602692c21bf5293a75e4b607bb92a11e821e2b859a",
"sha256:e9d21aaa84557d64209af04ff48e0ad5e28c5cca67ce43444e939579d085da72",
"sha256:ea6b8aa9e08eea388c5f7a276fabb1d4b6b9d6e4ceb12cc477c3d352001768a9",
"sha256:eabe7090db68c981fca689299c2d116400b553f4b713266b130cfc9e2aa9c5a9",
"sha256:f2f6d303f3dee132b322a14cd8765287b8f86cdc10d2cb6a6fae234ea488888e",
"sha256:f33f3258aae89da191c6ebaa3bc517c6c4cbc9b9f689e5d8452f7aedbb913fa8",
"sha256:f7bfb769f7efa0eefcd039dd19d843a4fbfbac52f1878b1da2ed5793ec9b1a65",
"sha256:f89e21afe925fcfa655965ca8ea10f24773a1791400989ff32f467badfe4a064",
"sha256:fa24255ae3c0ab67e613556375a4341af04a084bd58764731972bcbc8baeba36"
],
"markers": "platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))",
"version": "==3.0.1"
},
"idna": {
"hashes": [
"sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4",
"sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"
],
"markers": "python_version >= '3.5'",
"version": "==3.4"
},
"iniconfig": {
"hashes": [
"sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3",
"sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"
],
"markers": "python_version >= '3.7'",
"version": "==2.0.0"
},
"itsdangerous": {
"hashes": [
"sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44",
"sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"
],
"markers": "python_version >= '3.7'",
"version": "==2.1.2"
},
"jinja2": {
"hashes": [
"sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852",
"sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"
],
"markers": "python_version >= '3.7'",
"version": "==3.1.2"
},
"mako": {
"hashes": [
"sha256:57d4e997349f1a92035aa25c17ace371a4213f2ca42f99bee9a602500cfd54d9",
"sha256:e3a9d388fd00e87043edbe8792f45880ac0114e9c4adc69f6e9bfb2c55e3b11b"
],
"markers": "python_version >= '3.8'",
"version": "==1.3.0"
},
"markupsafe": {
"hashes": [
"sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e",
"sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e",
"sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431",
"sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686",
"sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c",
"sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559",
"sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc",
"sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb",
"sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939",
"sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c",
"sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0",
"sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4",
"sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9",
"sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575",
"sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba",
"sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d",
"sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd",
"sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3",
"sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00",
"sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155",
"sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac",
"sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52",
"sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f",
"sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8",
"sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b",
"sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007",
"sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24",
"sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea",
"sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198",
"sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0",
"sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee",
"sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be",
"sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2",
"sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1",
"sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707",
"sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6",
"sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c",
"sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58",
"sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823",
"sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779",
"sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636",
"sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c",
"sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad",
"sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee",
"sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc",
"sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2",
"sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48",
"sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7",
"sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e",
"sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b",
"sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa",
"sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5",
"sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e",
"sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb",
"sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9",
"sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57",
"sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc",
"sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc",
"sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2",
"sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"
],
"markers": "python_version >= '3.7'",
"version": "==2.1.3"
},
"marshmallow": {
"hashes": [
"sha256:5d2371bbe42000f2b3fb5eaa065224df7d8f8597bc19a1bbfa5bfe7fba8da889",
"sha256:684939db93e80ad3561392f47be0230743131560a41c5110684c16e21ade0a5c"
],
"markers": "python_version >= '3.8'",
"version": "==3.20.1"
},
"marshmallow-sqlalchemy": {
"hashes": [
"sha256:3523a774390ef0c1c0f7c708a7519809c5396cf608720f14f55c36f74ff5bbec",
"sha256:3cee0bf61ed10687c0a41448e1916649b28222334a02f7b937c39d1c69c18bee"
],
"index": "pypi",
"version": "==0.29.0"
},
"packaging": {
"hashes": [
"sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5",
"sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"
],
"markers": "python_version >= '3.7'",
"version": "==23.2"
},
"pluggy": {
"hashes": [
"sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12",
"sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"
],
"markers": "python_version >= '3.8'",
"version": "==1.3.0"
},
"psycopg2-binary": {
"hashes": [
"sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9",
"sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77",
"sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e",
"sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84",
"sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3",
"sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2",
"sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67",
"sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876",
"sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152",
"sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f",
"sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a",
"sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6",
"sha256:275ff571376626195ab95a746e6a04c7df8ea34638b99fc11160de91f2fef503",
"sha256:281309265596e388ef483250db3640e5f414168c5a67e9c665cafce9492eda2f",
"sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493",
"sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996",
"sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f",
"sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e",
"sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59",
"sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94",
"sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7",
"sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682",
"sha256:4154ad09dac630a0f13f37b583eae260c6aa885d67dfbccb5b02c33f31a6d420",
"sha256:420f9bbf47a02616e8554e825208cb947969451978dceb77f95ad09c37791dae",
"sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291",
"sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe",
"sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980",
"sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93",
"sha256:68fc1f1ba168724771e38bee37d940d2865cb0f562380a1fb1ffb428b75cb692",
"sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119",
"sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716",
"sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472",
"sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b",
"sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2",
"sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc",
"sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c",
"sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5",
"sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab",
"sha256:8359bf4791968c5a78c56103702000105501adb557f3cf772b2c207284273984",
"sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9",
"sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf",
"sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0",
"sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f",
"sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212",
"sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb",
"sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be",
"sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90",
"sha256:a148c5d507bb9b4f2030a2025c545fccb0e1ef317393eaba42e7eabd28eb6041",
"sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7",
"sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860",
"sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d",
"sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245",
"sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27",
"sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417",
"sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359",
"sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202",
"sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0",
"sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7",
"sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba",
"sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1",
"sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd",
"sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07",
"sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98",
"sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55",
"sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d",
"sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972",
"sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f",
"sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e",
"sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26",
"sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957",
"sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53",
"sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52"
],
"index": "pypi",
"version": "==2.9.9"
},
"pyjwt": {
"hashes": [
"sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de",
"sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"
],
"index": "pypi",
"version": "==2.8.0"
},
"pymupdf": {
"hashes": [
"sha256:0f852d125defc26716878b1796f4d68870e9065041d00cf46bde317fd8d30e68",
"sha256:1103eea4ab727e32b9cb93347b35f71562033018c333a7f3a17d115e980fea4a",
"sha256:19d1711d5908c4527ad2deef5af2d066649f3f9a12950faf30be5f7251d18abc",
"sha256:1cbcf05c06f314fdf3042ceee674e9a0ac7fae598347d5442e2138c6046d4e82",
"sha256:224c341fe254adda97c8f06a4c5838cdbcf609fa89e70b1fb179752533378f2f",
"sha256:271bdf6059bb8347f9c9c6b721329bd353a933681b1fc62f43241b410e7ab7ae",
"sha256:2885a26220a32fb45ea443443b72194bb7107d6862d8d546b59e4ad0c8a1f2c9",
"sha256:2c141f33e2733e48de8524dfd2de56d889feef0c7773b20a8cd216c03ab24793",
"sha256:2e27857a15c8a810d0b66455b8c8a79013640b6267a9b4ea808a5fe1f47711f2",
"sha256:361cab1be45481bd3dc4e00ec82628ebc189b4f4b6fd9bd78a00cfeed54e0034",
"sha256:3ce2d3678dbf822cff213b1902f2e59756313e543efd516a2b4f15bb0353bd6c",
"sha256:3f0f9b76bc4f039e7587003cbd40684d93a98441549dd033cab38ca07d61988d",
"sha256:4ac9673a6d6ee7e80cb242dacb43f9ca097b502d9c5e44687dbdffc2bce7961a",
"sha256:4d06751d5cd213e96f84f2faaa71a51cf4d641851e07579247ca1190121f173b",
"sha256:526b26a5207e923aab65877ad305644402851823a352cb92d362053426899354",
"sha256:57725e15872f7ab67a9fb3e06e5384d1047b2121e85755c93a6d4266d3ca8983",
"sha256:57e22bea69690450197b34dcde16bd9fe0265ac4425b4033535ccc5c044246fb",
"sha256:5bdf7020b90987412381acc42427dd1b7a03d771ee9ec273de003e570164ec1a",
"sha256:5cd05700c8f18c9dafef63ac2ed3b1099ca06017ca0c32deea13093cea1b8671",
"sha256:618b8e884190ac1cca9df1c637f87669d2d532d421d4ee7e4763c848dc4f3a1e",
"sha256:6e319c1f49476e07b9a12017c2d031687617713f8a46b7adcec03c636ed04607",
"sha256:761501a4965264e81acdd8f2224f993020bf24474e9b34fcdb5805a6826eda1c",
"sha256:8fd9c4ee1dd4744a515b9190d8ba9133348b0d94c362293ed77726aa1c13b0a6",
"sha256:951d280c1daafac2fd6a664b031f7f98b27eb2def55d39c92a19087bd8041c5d",
"sha256:991a37e1cba43775ce094da87cf0bf72172a5532a09644003276bc8bfdfe9f1a",
"sha256:c4eb71b88a22c1008f764b3121b36a9d25340f9920b870508356050a365d9ca1",
"sha256:c8ea81964c1433ea163ad4b53c56053a87a9ef6e1bd7a879d4d368a3988b60d1",
"sha256:e047571d799b30459ad7ee0bc6e68900a7f6b928876f956c976f279808814e72",
"sha256:e2d64799c6d9a3735be9e162a5d11061c0b7fbcb1e5fc7446e0993d0f815a93a",
"sha256:e33f8ec5ba7265fe78b30332840b8f454184addfa79f9c27f160f19789aa5ffd",
"sha256:fd8388e82b6045807d19addf310d8119d32908e89f76cc8bbf8cf1ec36fce947"
],
"index": "pypi",
"version": "==1.23.6"
},
"pymupdfb": {
"hashes": [
"sha256:009e2cff166059e13bf71f93919e688f46b8fc11d122433574cfb0cc9134690e",
"sha256:7132b30e6ad6ff2013344e3a481b2287fe0be3710d80694807dd6e0d8635f085",
"sha256:7bef75988e6979b10ca804cf9487f817aae43b0fff1c6e315b3b9ee0cf1cc32f",
"sha256:9925816cbe3e05e920f9be925e5752c2eef42b793885b62075bb0f6a69178598",
"sha256:9d24ddadc204e895bee5000ddc7507c801643548e59f5a56aad6d32981d17eeb",
"sha256:e5af77580aad3d1103aeec57009d156bfca429cecda14a17c573fcbe97bafb30"
],
"markers": "python_version >= '3.8'",
"version": "==1.23.6"
},
"pytest": {
"hashes": [
"sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac",
"sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"
],
"index": "pypi",
"version": "==7.4.3"
},
"python-dotenv": {
"hashes": [
"sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba",
"sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"
],
"index": "pypi",
"version": "==1.0.0"
},
"requests": {
"hashes": [
"sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f",
"sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"
],
"index": "pypi",
"version": "==2.31.0"
},
"sqlalchemy": {
"hashes": [
"sha256:0666031df46b9badba9bed00092a1ffa3aa063a5e68fa244acd9f08070e936d3",
"sha256:0a8c6aa506893e25a04233bc721c6b6cf844bafd7250535abb56cb6cc1368884",
"sha256:0e680527245895aba86afbd5bef6c316831c02aa988d1aad83c47ffe92655e74",
"sha256:14aebfe28b99f24f8a4c1346c48bc3d63705b1f919a24c27471136d2f219f02d",
"sha256:1e018aba8363adb0599e745af245306cb8c46b9ad0a6fc0a86745b6ff7d940fc",
"sha256:227135ef1e48165f37590b8bfc44ed7ff4c074bf04dc8d6f8e7f1c14a94aa6ca",
"sha256:31952bbc527d633b9479f5f81e8b9dfada00b91d6baba021a869095f1a97006d",
"sha256:3e983fa42164577d073778d06d2cc5d020322425a509a08119bdcee70ad856bf",
"sha256:42d0b0290a8fb0165ea2c2781ae66e95cca6e27a2fbe1016ff8db3112ac1e846",
"sha256:42ede90148b73fe4ab4a089f3126b2cfae8cfefc955c8174d697bb46210c8306",
"sha256:4895a63e2c271ffc7a81ea424b94060f7b3b03b4ea0cd58ab5bb676ed02f4221",
"sha256:4af79c06825e2836de21439cb2a6ce22b2ca129bad74f359bddd173f39582bf5",
"sha256:5f94aeb99f43729960638e7468d4688f6efccb837a858b34574e01143cf11f89",
"sha256:616fe7bcff0a05098f64b4478b78ec2dfa03225c23734d83d6c169eb41a93e55",
"sha256:62d9e964870ea5ade4bc870ac4004c456efe75fb50404c03c5fd61f8bc669a72",
"sha256:638c2c0b6b4661a4fd264f6fb804eccd392745c5887f9317feb64bb7cb03b3ea",
"sha256:63bfc3acc970776036f6d1d0e65faa7473be9f3135d37a463c5eba5efcdb24c8",
"sha256:6463aa765cf02b9247e38b35853923edbf2f6fd1963df88706bc1d02410a5577",
"sha256:64ac935a90bc479fee77f9463f298943b0e60005fe5de2aa654d9cdef46c54df",
"sha256:683ef58ca8eea4747737a1c35c11372ffeb84578d3aab8f3e10b1d13d66f2bc4",
"sha256:75eefe09e98043cff2fb8af9796e20747ae870c903dc61d41b0c2e55128f958d",
"sha256:787af80107fb691934a01889ca8f82a44adedbf5ef3d6ad7d0f0b9ac557e0c34",
"sha256:7c424983ab447dab126c39d3ce3be5bee95700783204a72549c3dceffe0fc8f4",
"sha256:7e0dc9031baa46ad0dd5a269cb7a92a73284d1309228be1d5935dac8fb3cae24",
"sha256:87a3d6b53c39cd173990de2f5f4b83431d534a74f0e2f88bd16eabb5667e65c6",
"sha256:89a01238fcb9a8af118eaad3ffcc5dedaacbd429dc6fdc43fe430d3a941ff965",
"sha256:9585b646ffb048c0250acc7dad92536591ffe35dba624bb8fd9b471e25212a35",
"sha256:964971b52daab357d2c0875825e36584d58f536e920f2968df8d581054eada4b",
"sha256:967c0b71156f793e6662dd839da54f884631755275ed71f1539c95bbada9aaab",
"sha256:9ca922f305d67605668e93991aaf2c12239c78207bca3b891cd51a4515c72e22",
"sha256:a86cb7063e2c9fb8e774f77fbf8475516d270a3e989da55fa05d08089d77f8c4",
"sha256:aeb397de65a0a62f14c257f36a726945a7f7bb60253462e8602d9b97b5cbe204",
"sha256:b41f5d65b54cdf4934ecede2f41b9c60c9f785620416e8e6c48349ab18643855",
"sha256:bd45a5b6c68357578263d74daab6ff9439517f87da63442d244f9f23df56138d",
"sha256:c14eba45983d2f48f7546bb32b47937ee2cafae353646295f0e99f35b14286ab",
"sha256:c1bda93cbbe4aa2aa0aa8655c5aeda505cd219ff3e8da91d1d329e143e4aff69",
"sha256:c4722f3bc3c1c2fcc3702dbe0016ba31148dd6efcd2a2fd33c1b4897c6a19693",
"sha256:c80c38bd2ea35b97cbf7c21aeb129dcbebbf344ee01a7141016ab7b851464f8e",
"sha256:cabafc7837b6cec61c0e1e5c6d14ef250b675fa9c3060ed8a7e38653bd732ff8",
"sha256:cc1d21576f958c42d9aec68eba5c1a7d715e5fc07825a629015fe8e3b0657fb0",
"sha256:d0f7fb0c7527c41fa6fcae2be537ac137f636a41b4c5a4c58914541e2f436b45",
"sha256:d4041ad05b35f1f4da481f6b811b4af2f29e83af253bf37c3c4582b2c68934ab",
"sha256:d5578e6863eeb998980c212a39106ea139bdc0b3f73291b96e27c929c90cd8e1",
"sha256:e3b5036aa326dc2df50cba3c958e29b291a80f604b1afa4c8ce73e78e1c9f01d",
"sha256:e599a51acf3cc4d31d1a0cf248d8f8d863b6386d2b6782c5074427ebb7803bda",
"sha256:f3420d00d2cb42432c1d0e44540ae83185ccbbc67a6054dcc8ab5387add6620b",
"sha256:f48ed89dd11c3c586f45e9eec1e437b355b3b6f6884ea4a4c3111a3358fd0c18",
"sha256:f508ba8f89e0a5ecdfd3761f82dda2a3d7b678a626967608f4273e0dba8f07ac",
"sha256:fd54601ef9cc455a0c61e5245f690c8a3ad67ddb03d3b91c361d076def0b4c60"
],
"markers": "python_version >= '3.7'",
"version": "==2.0.23"
},
"sqlalchemy-utils": {
"hashes": [
"sha256:6c96b0768ea3f15c0dc56b363d386138c562752b84f647fb8d31a2223aaab801",
"sha256:a2181bff01eeb84479e38571d2c0718eb52042f9afd8c194d0d02877e84b7d74"
],
"index": "pypi",
"version": "==0.41.1"
},
"typing-extensions": {
"hashes": [
"sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0",
"sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"
],
"markers": "python_version >= '3.8'",
"version": "==4.8.0"
},
"urllib3": {
"hashes": [
"sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84",
"sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"
],
"markers": "python_version >= '3.7'",
"version": "==2.0.7"
},
"visitor": {
"hashes": [
"sha256:2c737903b2b6864ebc6167eef7cf3b997126f1aa94bdf590f90f1436d23e480a"
],
"version": "==0.1.3"
},
"werkzeug": {
"hashes": [
"sha256:507e811ecea72b18a404947aded4b3390e1db8f826b494d76550ef45bb3b1dcc",
"sha256:90a285dc0e42ad56b34e696398b8122ee4c681833fb35b8334a095d82c56da10"
],
"markers": "python_version >= '3.8'",
"version": "==3.0.1"
},
"wtforms": {
"hashes": [
"sha256:5e51df8af9a60f6beead75efa10975e97768825a82146a65c7cbf5b915990620",
"sha256:ae7c54b29806c70f7bce8eb9f24afceb10ca5c32af3d9f04f74d2f66ccc5c7e0"
],
"markers": "python_version >= '3.8'",
"version": "==3.1.1"
},
"wtforms-sqlalchemy": {
"hashes": [
"sha256:7ca42824ad7c453a036f502b42d9c17d4ad8398ff9248a62ed538d1ce0bc41c3",
"sha256:90195d7592bf256d82498c42c79d416832e4a4e6fbca4f1e745a018f66d26c47"
],
"index": "pypi",
"version": "==0.3"
}
},
"develop": {}
}

View File

View File

@ -1,23 +0,0 @@
import os
from dotenv import load_dotenv
basedir = os.path.abspath(os.path.dirname(__file__))
load_dotenv(os.path.join(basedir, '.env'))
os.environ["TESSDATA_PREFIX"] = os.path.join(basedir, 'tessdata')
class Config(object):
SECRET_KEY = os.environ.get('SECRET_KEY') or "s0m37h!n6-obfu5c471ng"
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL', '').replace(
'postgres://', 'postgresql://') or \
(f"postgresql://{os.environ.get('DATABASE_USER', 'costhive')}:{os.environ.get('DATABASE_PASS', 'asdf1337')}"
f"@{os.environ.get('DATABASE_HOST', 'localhost')}:{os.environ.get('DATABASE_PORT', '5432')}"
f"/{os.environ.get('DATABASE_DB', '') or os.environ.get('DATABASE_USER', 'costhive')}")
SQLALCHEMY_TRACK_MODIFICATIONS = False
MAIL_SERVER = os.environ.get('MAIL_SERVER')
MAIL_PORT = int(os.environ.get('MAIL_PORT'))
MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS') is not None
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
ADMINS = ['postmaster@wpgcommunity.net']
POSTS_PER_PAGE = 15
RECEIPT_FOLDER = f"{basedir}/../PDFReceipts"

View File

@ -1 +0,0 @@
tessedit_create_alto 1

View File

@ -1,7 +0,0 @@
tessedit_ambigs_training 1
load_freq_dawg 0
load_punc_dawg 0
load_system_dawg 0
load_number_dawg 0
ambigs_debug_level 3
load_fixed_length_dawgs 0

View File

@ -1 +0,0 @@
tessedit_zero_rejection T

View File

@ -1,5 +0,0 @@
load_bigram_dawg True
tessedit_enable_bigram_correction True
tessedit_bigram_debug 3
save_raw_choices True
save_alt_choices True

View File

@ -1,12 +0,0 @@
disable_character_fragments T
file_type .bl
textord_fast_pitch_test T
tessedit_zero_rejection T
tessedit_minimal_rejection F
tessedit_write_rep_codes F
edges_children_fix F
edges_childarea 0.65
edges_boxarea 0.9
tessedit_resegment_from_boxes T
tessedit_train_from_boxes T
textord_no_rejects T

View File

@ -1,13 +0,0 @@
file_type .bl
#tessedit_use_nn F
textord_fast_pitch_test T
tessedit_zero_rejection T
tessedit_minimal_rejection F
tessedit_write_rep_codes F
edges_children_fix F
edges_childarea 0.65
edges_boxarea 0.9
tessedit_resegment_from_boxes T
tessedit_train_from_boxes T
#textord_repeat_extraction F
textord_no_rejects T

View File

@ -1 +0,0 @@
tessedit_char_whitelist 0123456789-.

View File

@ -1 +0,0 @@
tessedit_write_images T

View File

@ -1,2 +0,0 @@
tessedit_create_hocr 1
hocr_font_info 0

View File

@ -1,2 +0,0 @@
interactive_display_mode T
tessedit_display_outwords T

View File

@ -1,4 +0,0 @@
textord_skewsmooth_offset 8
textord_skewsmooth_offset2 8
textord_merge_desc 0.5
textord_no_rejects 1

View File

@ -1,2 +0,0 @@
tessedit_resegment_from_line_boxes 1
tessedit_make_boxes_from_boxes 1

View File

@ -1 +0,0 @@
debug_file tesseract.log

View File

@ -1,11 +0,0 @@
file_type .bl
textord_fast_pitch_test T
tessedit_zero_rejection T
tessedit_minimal_rejection F
tessedit_write_rep_codes F
edges_children_fix F
edges_childarea 0.65
edges_boxarea 0.9
tessedit_train_line_recognizer T
textord_no_rejects T
tessedit_init_config_only T

View File

@ -1 +0,0 @@
tessedit_create_lstmbox 1

View File

@ -1,4 +0,0 @@
stopper_debug_level 1
classify_debug_level 1
segsearch_debug_level 1
language_model_debug_level 3

View File

@ -1 +0,0 @@
tessedit_create_boxfile 1

View File

@ -1 +0,0 @@
tessedit_create_pdf 1

View File

@ -1 +0,0 @@
debug_file /dev/null

View File

@ -1,2 +0,0 @@
tessedit_resegment_from_boxes 1
tessedit_make_boxes_from_boxes 1

View File

@ -1,12 +0,0 @@
textord_show_blobs 0
textord_debug_tabfind 3
textord_tabfind_show_partitions 1
textord_tabfind_show_initial_partitions 1
textord_tabfind_show_columns 1
textord_tabfind_show_blocks 1
textord_tabfind_show_initialtabs 1
textord_tabfind_show_finaltabs 1
textord_tabfind_show_strokewidths 1
textord_tabfind_show_vlines 0
textord_tabfind_show_images 1
tessedit_dump_pageseg_images 0

View File

@ -1 +0,0 @@
tessedit_create_tsv 1

View File

@ -1,3 +0,0 @@
# This config file should be used with other config files which create renderers.
# usage example: tesseract eurotext.tif eurotext txt hocr pdf
tessedit_create_txt 1

View File

@ -1,2 +0,0 @@
tessedit_write_unlv 1
unlv_tilde_crunching T

View File

@ -1 +0,0 @@
tessedit_create_wordstrbox 1

View File

@ -1,2 +0,0 @@
1-\d\d\d-GOOG-411
www.\n\\\*.com

View File

@ -1,5 +0,0 @@
the
quick
brown
fox
jumped

Binary file not shown.

View File

@ -1 +0,0 @@
# No content needed as all defaults are correct.

View File

@ -1,2 +0,0 @@
chop_enable 0
wordrec_enable_assoc 0

View File

@ -1,7 +0,0 @@
#################################################
# Adaptive Matcher Using PreAdapted Templates
#################################################
classify_enable_adaptive_debugger 1
matcher_debug_flags 6
matcher_debug_level 1

View File

@ -1,12 +0,0 @@
#################################################
# Adaptive Matcher Using PreAdapted Templates
#################################################
classify_enable_adaptive_debugger 1
matcher_debug_flags 6
matcher_debug_level 1
wordrec_display_splits 0
wordrec_display_all_blobs 1
wordrec_display_segmentations 2
classify_debug_level 1

View File

@ -1,9 +0,0 @@
#################################################
# Adaptive Matcher Using PreAdapted Templates
#################################################
wordrec_display_splits 0
wordrec_display_all_blobs 1
wordrec_display_segmentations 2
classify_debug_level 1
stopper_debug_level 1

View File

@ -1,34 +0,0 @@
"""Add table for logintoken de-/activation dates
Revision ID: 015f4256bb4c
Revises: 9a8c73f0ab11
Create Date: 2023-11-19 20:52:03.377745
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '015f4256bb4c'
down_revision = '9a8c73f0ab11'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('login_token_dates',
sa.Column('token', sa.String(length=15), nullable=False),
sa.Column('activation_date', sa.Date(), server_default=sa.text('now()'), nullable=False),
sa.Column('deactivation_date', sa.Date(), nullable=True),
sa.ForeignKeyConstraint(['token'], ['login_token.token'], ),
sa.PrimaryKeyConstraint('token', 'activation_date')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('login_token_dates')
# ### end Alembic commands ###

View File

@ -1,54 +0,0 @@
"""receipt id serial
Revision ID: 0fa2ef37e440
Revises: f6f97ed9c053
Create Date: 2023-07-25 21:26:25.353435
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '0fa2ef37e440'
down_revision = 'f6f97ed9c053'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.execute("CREATE SEQUENCE receipt_id_seq;")
with op.batch_alter_table('receipt', schema=None) as batch_op:
batch_op.alter_column('id',
existing_type=sa.BIGINT(),
type_=sa.Integer(),
existing_nullable=False,
server_default=sa.sql.func.next_value(sa.Sequence('receipt_id_seq')))
with op.batch_alter_table('receipt_item', schema=None) as batch_op:
batch_op.alter_column('receipt',
existing_type=sa.BIGINT(),
type_=sa.Integer(),
existing_nullable=False)
op.execute("ALTER SEQUENCE receipt_id_seq OWNED BY receipt.id;")
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('receipt_item', schema=None) as batch_op:
batch_op.alter_column('receipt',
existing_type=sa.Integer(),
type_=sa.BIGINT(),
existing_nullable=False)
with op.batch_alter_table('receipt', schema=None) as batch_op:
batch_op.alter_column('id',
existing_type=sa.Integer(),
type_=sa.BIGINT(),
existing_nullable=False)
# ### end Alembic commands ###

View File

@ -1,35 +0,0 @@
"""new table for bought entries with unknown items 2
Revision ID: 2a64d3b9235a
Revises: 015f4256bb4c
Create Date: 2024-06-02 13:19:59.901053
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '2a64d3b9235a'
down_revision = '015f4256bb4c'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('bought_with_unknown_item',
sa.Column('token', sa.String(length=15), nullable=False),
sa.Column('item', sa.BigInteger(), nullable=False),
sa.Column('date', sa.Date(), nullable=False),
sa.Column('amount', sa.SmallInteger(), nullable=False),
sa.ForeignKeyConstraint(['token'], ['login_token.token'], ),
sa.PrimaryKeyConstraint('token', 'item', 'date')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('bought_with_unknown_item')
# ### end Alembic commands ###

View File

@ -1,32 +0,0 @@
"""Remove accepted from receiptitem
Revision ID: 36f532800705
Revises: cdbe3f5e3b30
Create Date: 2023-11-11 10:45:44.652161
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '36f532800705'
down_revision = 'cdbe3f5e3b30'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('receipt_item', schema=None) as batch_op:
batch_op.drop_column('accepted')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('receipt_item', schema=None) as batch_op:
batch_op.add_column(sa.Column('accepted', sa.BOOLEAN(), autoincrement=False, nullable=False))
# ### end Alembic commands ###

View File

@ -1,38 +0,0 @@
"""Delete unnecessary columns
Revision ID: 4fec22c35530
Revises: 610854a6e2a0
Create Date: 2023-05-28 08:10:27.689070
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '4fec22c35530'
down_revision = '610854a6e2a0'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('bought', schema=None) as batch_op:
batch_op.drop_column('registered')
with op.batch_alter_table('login_token', schema=None) as batch_op:
batch_op.drop_column('paid')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('login_token', schema=None) as batch_op:
batch_op.add_column(sa.Column('paid', sa.BIGINT(), server_default=sa.text("'0'::bigint"), autoincrement=False, nullable=False))
with op.batch_alter_table('bought', schema=None) as batch_op:
batch_op.add_column(sa.Column('registered', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=False))
# ### end Alembic commands ###

View File

@ -1,53 +0,0 @@
"""establishment candidate table
Revision ID: 610854a6e2a0
Revises: 2be4d1ae5493
Create Date: 2023-03-18 13:36:12.953900
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '610854a6e2a0'
down_revision = '2be4d1ae5493'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('establishment_candidate',
sa.Column('user', sa.BigInteger(), nullable=False),
sa.Column('establishment', sa.BigInteger(), nullable=False),
sa.ForeignKeyConstraint(['establishment'], ['establishment.id'], ),
sa.ForeignKeyConstraint(['user'], ['user.id'], ),
sa.PrimaryKeyConstraint('user', 'establishment')
)
op.create_table('payment',
sa.Column('id', sa.BigInteger(), nullable=False, autoincrement=True),
sa.Column('token', sa.String(length=15), nullable=False),
sa.Column('date', sa.Date(), server_default=sa.text('now()'), nullable=False),
sa.Column('amount', sa.BigInteger(), server_default='0', nullable=False),
sa.ForeignKeyConstraint(['token'], ['login_token.token'], ),
sa.PrimaryKeyConstraint('id', 'token')
)
with op.batch_alter_table('login_token', schema=None) as batch_op:
batch_op.alter_column('token',
existing_type=sa.VARCHAR(length=15),
nullable=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('login_token', schema=None) as batch_op:
batch_op.alter_column('token',
existing_type=sa.VARCHAR(length=15),
nullable=True)
op.drop_table('payment')
op.drop_table('establishment_candidate')
# ### end Alembic commands ###

View File

@ -1,38 +0,0 @@
"""raise password char length
Revision ID: 782a2409df41
Revises: 926395732c3e
Create Date: 2025-06-03 21:01:23.169897
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '782a2409df41'
down_revision = '926395732c3e'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.alter_column('password_hash',
existing_type=sa.VARCHAR(length=128),
type_=sa.String(length=255),
existing_nullable=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.alter_column('password_hash',
existing_type=sa.String(length=255),
type_=sa.VARCHAR(length=128),
existing_nullable=False)
# ### end Alembic commands ###

View File

@ -1,38 +0,0 @@
"""raise bonid digits
Revision ID: 926395732c3e
Revises: 2a64d3b9235a
Create Date: 2024-08-24 10:33:39.109944
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '926395732c3e'
down_revision = '2a64d3b9235a'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('receipt', schema=None) as batch_op:
batch_op.alter_column('bonid',
existing_type=sa.NUMERIC(precision=24, scale=0),
type_=sa.Numeric(precision=28, scale=0),
existing_nullable=True)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('receipt', schema=None) as batch_op:
batch_op.alter_column('bonid',
existing_type=sa.Numeric(precision=28, scale=0),
type_=sa.NUMERIC(precision=24, scale=0),
existing_nullable=True)
# ### end Alembic commands ###

View File

@ -1,35 +0,0 @@
"""add receiptitem names
Revision ID: 9a8c73f0ab11
Revises: 36f532800705
Create Date: 2023-11-18 23:38:56.865780
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '9a8c73f0ab11'
down_revision = '36f532800705'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('receipt_item', schema=None) as batch_op:
batch_op.add_column(sa.Column('name', sa.String(), nullable=True))
op.execute("UPDATE receipt_item SET name='Unknown' WHERE name IS NULL;")
with op.batch_alter_table('receipt_item', schema=None) as batch_op:
batch_op.alter_column('name', nullable=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('receipt_item', schema=None) as batch_op:
batch_op.drop_column('name')
# ### end Alembic commands ###

View File

@ -1,64 +0,0 @@
"""Remove item.receiptitem relationship
Revision ID: cdbe3f5e3b30
Revises: 0fa2ef37e440
Create Date: 2023-09-19 19:02:23.003034
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'cdbe3f5e3b30'
down_revision = '0fa2ef37e440'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('receipt', schema=None) as batch_op:
batch_op.alter_column('id',
existing_type=sa.INTEGER(),
type_=sa.BigInteger(),
existing_nullable=False,
autoincrement=True)
with op.batch_alter_table('receipt_item', schema=None) as batch_op:
batch_op.add_column(sa.Column('price', sa.SmallInteger(), nullable=False))
batch_op.alter_column('receipt',
existing_type=sa.INTEGER(),
type_=sa.BigInteger(),
existing_nullable=False)
batch_op.alter_column('item',
existing_type=sa.BIGINT(),
type_=sa.SmallInteger(),
existing_nullable=False)
batch_op.drop_constraint('item_receipt_item_fkey', type_='foreignkey')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('receipt_item', schema=None) as batch_op:
batch_op.create_foreign_key('item_receipt_item_fkey', 'item', ['item'], ['id'])
batch_op.alter_column('item',
existing_type=sa.SmallInteger(),
type_=sa.BIGINT(),
existing_nullable=False)
batch_op.alter_column('receipt',
existing_type=sa.BigInteger(),
type_=sa.INTEGER(),
existing_nullable=False)
batch_op.drop_column('price')
with op.batch_alter_table('receipt', schema=None) as batch_op:
batch_op.alter_column('id',
existing_type=sa.BigInteger(),
type_=sa.INTEGER(),
existing_nullable=False,
autoincrement=True)
# ### end Alembic commands ###

View File

@ -1,64 +0,0 @@
"""receipt-id reorganized
Revision ID: f6f97ed9c053
Revises: 4fec22c35530
Create Date: 2023-07-02 14:16:06.260479
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'f6f97ed9c053'
down_revision = '4fec22c35530'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('receipt_item', schema=None) as batch_op:
batch_op.add_column(sa.Column('accepted', sa.Boolean(), nullable=False))
batch_op.drop_constraint("item_receipt_receipt_fkey")
batch_op.alter_column('receipt',
existing_type=sa.NUMERIC(precision=24, scale=0),
type_=sa.BigInteger(),
existing_nullable=False)
with op.batch_alter_table('receipt', schema=None) as batch_op:
batch_op.add_column(sa.Column('bonid', sa.Numeric(precision=24, scale=0), nullable=True))
with op.batch_alter_table('receipt', schema=None) as batch_op:
batch_op.execute("UPDATE receipt SET bonid=id;")
batch_op.alter_column('id',
existing_type=sa.NUMERIC(precision=24, scale=0),
type_=sa.BigInteger(),
existing_nullable=False,
autoincrement=True)
batch_op.create_unique_constraint(None, ['bonid'])
with op.batch_alter_table('receipt_item', schema=None) as batch_op:
batch_op.create_foreign_key(None, 'receipt', ['receipt'], ['id'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('receipt_item', schema=None) as batch_op:
batch_op.alter_column('receipt',
existing_type=sa.BigInteger(),
type_=sa.NUMERIC(precision=24, scale=0),
existing_nullable=False)
batch_op.drop_column('accepted')
with op.batch_alter_table('receipt', schema=None) as batch_op:
batch_op.drop_constraint(None, type_='unique')
batch_op.alter_column('id',
existing_type=sa.BigInteger(),
type_=sa.NUMERIC(precision=24, scale=0),
existing_nullable=False,
autoincrement=True)
batch_op.drop_column('bonid')
# ### end Alembic commands ###

View File

@ -1,18 +0,0 @@
from .payment import Payment
from .receipt_item import ReceiptItem
from .receipt import Receipt
from .login_token import LoginToken
from .bought import Bought
from .bought_with_unknown_item import BoughtWithUnknownItem
from .establishment import Establishment
from .establishment_candidate import EstablishmentCandidate
from .user import User
from .amount_change import AmountChange
from .price_change import PriceChange
from .category import Category
from .item import Item
from .brand import Brand
from .item_category import item_category
from .login_token_dates import LoginTokenDates
from .schemas import *

View File

@ -1,13 +0,0 @@
from datetime import date
from src import db
class AmountChange(db.Model):
item = db.Column(db.ForeignKey('item.id'), primary_key=True,
server_onupdate=db.FetchedValue())
date = db.Column(db.Date, primary_key=True,
server_default=str(date(2021, 12, 1)))
amount = db.Column(db.SmallInteger, nullable=False, server_default=str(1))
def __repr__(self) -> str:
return f"<Amount_Change {self.item} ({self.date})>"

View File

@ -1,13 +0,0 @@
from src import db
class Bought(db.Model):
token = db.Column(db.ForeignKey('login_token.token'),
primary_key=True, server_onupdate=db.FetchedValue())
item = db.Column(db.ForeignKey('item.id'), primary_key=True,
server_onupdate=db.FetchedValue())
date = db.Column(db.Date, primary_key=True)
amount = db.Column(db.SmallInteger, nullable=False)
def __repr__(self) -> str:
return f"<Bought Object>"

View File

@ -1,14 +0,0 @@
from src import db
from .bought import Bought
class BoughtWithUnknownItem(db.Model):
token = db.Column(db.ForeignKey('login_token.token'),
primary_key=True, server_onupdate=db.FetchedValue())
item = db.Column(db.BigInteger, primary_key=True,
server_onupdate=db.FetchedValue())
date = db.Column(db.Date, primary_key=True)
amount = db.Column(db.SmallInteger, nullable=False)
def __repr__(self) -> str:
return f"<Bought Object>"

View File

@ -1,14 +0,0 @@
from src import db
class Brand(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String(32), nullable=False)
Item = db.relationship("Item", backref='Brand', lazy='dynamic')
def __repr__(self) -> str:
return f"<Brand {self.id} ({self.name})>"
def __str__(self) -> str:
return f"{self.name}"

View File

@ -1,13 +0,0 @@
from src import db
from .item_category import item_category
class Category(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String(32), nullable=False, unique=True)
Item = db.relationship("Item", secondary=item_category,
lazy="dynamic", back_populates="Category")
def __repr__(self) -> str:
return f"<Category {self.id} ({self.name})>"

View File

@ -1,17 +0,0 @@
from src import db
class Establishment(db.Model):
id = db.Column(db.BigInteger, primary_key=True, autoincrement=True)
name = db.Column(db.String(64), nullable=False)
owner = db.Column(db.ForeignKey('user.id'), nullable=False)
LoginToken = db.relationship(
"LoginToken", backref='Establishment', lazy='dynamic')
Bought = db.relationship("Bought", secondary="login_token",
lazy='dynamic', overlaps="Establishment,LoginToken,Bought")
EstablishmentCandidate = db.relationship(
"EstablishmentCandidate", backref='Establishment', lazy='dynamic')
def __repr__(self) -> str:
return f"<Establishment {self.id} ({self.name})>"

View File

@ -1,10 +0,0 @@
from src import db
class EstablishmentCandidate(db.Model):
user = db.Column(db.ForeignKey('user.id'), primary_key=True)
establishment = db.Column(db.ForeignKey(
'establishment.id'), primary_key=True)
def __repr__(self) -> str:
return f"<EstablishmentCandidate {self.user} ({self.establishment})>"

View File

@ -1,24 +0,0 @@
from src import db, ma
from .item_category import item_category
class Item(db.Model):
id = db.Column(db.BigInteger, primary_key=True, autoincrement=False)
name = db.Column(db.String(64), nullable=False)
brand = db.Column(db.ForeignKey('brand.id'), nullable=False,
server_onupdate=db.FetchedValue())
description = db.Column(db.Text, nullable=False)
AmountChange = db.relationship(
"AmountChange", backref='Item', lazy='dynamic')
Bought = db.relationship("Bought", backref='Item', lazy='dynamic')
Category = db.relationship(
"Category", secondary=item_category, lazy="dynamic", back_populates="Item")
PriceChange = db.relationship(
"PriceChange", backref='Item', lazy='dynamic')
def __repr__(self) -> str:
return f"<Item {self.id} ({self.name})>"
def __str__(self) -> str:
return f"({self.id}) {self.description}"

View File

@ -1,6 +0,0 @@
from src import db
item_category = db.Table("item_category",
db.Column("item", db.ForeignKey("item.id"), primary_key=True, server_onupdate=db.FetchedValue()),
db.Column("category", db.ForeignKey("category.id"), primary_key=True, server_onupdate=db.FetchedValue())
)

View File

@ -1,16 +0,0 @@
from src import db
class LoginToken(db.Model):
user = db.Column(db.ForeignKey('user.id'), primary_key=True,
server_onupdate=db.FetchedValue())
establishment = db.Column(db.ForeignKey(
'establishment.id'), primary_key=True, server_onupdate=db.FetchedValue())
token = db.Column(db.String(15), nullable=False, unique=True)
LoginTokenDates = db.relationship("LoginTokenDates", backref='LoginToken', lazy='dynamic')
Payment = db.relationship("Payment", backref='LoginToken', lazy='dynamic')
Receipt = db.relationship("Receipt", backref='LoginToken', lazy='dynamic')
def __repr__(self) -> str:
return f"<LoginToken {self.token}>"

View File

@ -1,13 +0,0 @@
from src import db
class LoginTokenDates(db.Model):
token = db.Column(db.ForeignKey('login_token.token'),
nullable = False, primary_key=True,
server_onupdate=db.FetchedValue())
activation_date = db.Column(db.Date,
nullable=False, primary_key=True,
server_default=db.func.now())
deactivation_date = db.Column(db.Date, nullable=True)
def __repr__(self) -> str:
return f"<LoginTokenDates {self.token, self.activation_date, self.deactivation_date}>"

View File

@ -1,12 +0,0 @@
from src import db
class Payment(db.Model):
id = db.Column(db.BigInteger, primary_key=True, autoincrement=True)
token = db.Column(db.ForeignKey('login_token.token'),
server_onupdate=db.FetchedValue(), nullable=False)
date = db.Column(db.Date, nullable=False, server_default=db.func.now())
amount = db.Column(db.BigInteger, nullable=False, server_default=str(0))
def __repr__(self) -> str:
return f"<Payment {self.id}>"

View File

@ -1,13 +0,0 @@
from datetime import date
from src import db
class PriceChange(db.Model):
item = db.Column(db.ForeignKey('item.id'), primary_key=True,
server_onupdate=db.FetchedValue())
date = db.Column(db.Date, primary_key=True,
server_default=str(date(2021, 12, 1)))
price = db.Column(db.SmallInteger, nullable=False)
def __repr__(self) -> str:
return f"<Price_Change {self.item} ({self.date})>"

View File

@ -1,17 +0,0 @@
from src import db
class Receipt(db.Model):
id = db.Column(db.BigInteger, primary_key=True, autoincrement=True)
date = db.Column(db.Date, nullable=False)
bonid = db.Column(db.Numeric(precision=28, scale=0), unique=True)
from_user = db.Column(db.ForeignKey("login_token.token"),
server_onupdate=db.FetchedValue())
registered = db.Column(db.Boolean, nullable=False,
server_default=str(False))
ReceiptItem = db.relationship(
"ReceiptItem", backref='Receipt', lazy='dynamic')
def __repr__(self) -> str:
return f"<Receipt {self.id}>"

Some files were not shown because too many files have changed in this diff Show More