feat: initial functionality
This commit is contained in:
parent
fd3dc79f76
commit
3cabbe22db
3
.gitignore
vendored
3
.gitignore
vendored
@ -127,3 +127,6 @@ dmypy.json
|
|||||||
|
|
||||||
# Pyre type checker
|
# Pyre type checker
|
||||||
.pyre/
|
.pyre/
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
config.yaml
|
||||||
3
client/src/config.yaml.template
Normal file
3
client/src/config.yaml.template
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
server:
|
||||||
|
host: "http://localhost"
|
||||||
|
port: "5000"
|
||||||
44
client/src/connection.py
Normal file
44
client/src/connection.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
from requests import get, put, post, delete
|
||||||
|
from yaml import safe_load
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
LOGGER.setLevel(logging.DEBUG)
|
||||||
|
logFormatter = logging.Formatter(
|
||||||
|
"%(asctime)s [%(threadName)s] [%(levelname)s] %(message)s")
|
||||||
|
|
||||||
|
fileHandler = logging.FileHandler("../logs/connection.log")
|
||||||
|
fileHandler.setFormatter(logFormatter)
|
||||||
|
fileHandler.setLevel(logging.INFO)
|
||||||
|
LOGGER.addHandler(fileHandler)
|
||||||
|
|
||||||
|
consoleHandler = logging.StreamHandler()
|
||||||
|
consoleHandler.setLevel(logging.DEBUG)
|
||||||
|
LOGGER.addHandler(consoleHandler)
|
||||||
|
|
||||||
|
|
||||||
|
with open("config.yaml", 'r') as file:
|
||||||
|
data = safe_load(file)['server']
|
||||||
|
SERVER = data['host']
|
||||||
|
PORT = data['port']
|
||||||
|
del(data)
|
||||||
|
|
||||||
|
|
||||||
|
def check_login(login: str) -> bool:
|
||||||
|
response = get(url=":".join([SERVER, str(PORT)]) + '/scan2kasse/login', json={'login': login})
|
||||||
|
if response.status_code == 200:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def send_scan(login: str, scanned: dict[int: int], date:str = None):
|
||||||
|
infos = {'login': login, 'items': scanned}
|
||||||
|
if date:
|
||||||
|
infos['date'] = date
|
||||||
|
try:
|
||||||
|
response = put(url=":".join([SERVER, str(
|
||||||
|
PORT)]) + '/scan2kasse/insert', json=infos)
|
||||||
|
return True if response.status_code == 201 else False
|
||||||
|
except:
|
||||||
|
return False
|
||||||
77
client/src/main.py
Normal file
77
client/src/main.py
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
from datetime import date
|
||||||
|
from select import select as timedInput
|
||||||
|
from sys import stdin
|
||||||
|
import connection
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
LOGGER.setLevel(logging.DEBUG)
|
||||||
|
logFormatter = logging.Formatter(
|
||||||
|
"%(asctime)s [%(threadName)s] [%(levelname)s] %(message)s")
|
||||||
|
|
||||||
|
fileHandler = logging.FileHandler("../logs/client.log")
|
||||||
|
fileHandler.setFormatter(logFormatter)
|
||||||
|
fileHandler.setLevel(logging.INFO)
|
||||||
|
LOGGER.addHandler(fileHandler)
|
||||||
|
|
||||||
|
consoleHandler = logging.StreamHandler()
|
||||||
|
consoleHandler.setLevel(logging.DEBUG)
|
||||||
|
LOGGER.addHandler(consoleHandler)
|
||||||
|
|
||||||
|
TEMP = []
|
||||||
|
|
||||||
|
TIMEOUT = 60 # Number of seconds for a timeout after being logged in
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
while True:
|
||||||
|
login = input("Enter Login: ")
|
||||||
|
if not connection.check_login(login):
|
||||||
|
continue # Send Error that login wasn't possible
|
||||||
|
scanned = scanning()
|
||||||
|
scanned = group_scanning(scanned)
|
||||||
|
if not connection.send_scan(login, scanned):
|
||||||
|
TEMP.append({'login': login, 'items': scanned, 'date': str(date.today())})
|
||||||
|
if TEMP:
|
||||||
|
for bought in TEMP:
|
||||||
|
if connection.send_scan(bought['login'], bought['items'], bought['date']):
|
||||||
|
TEMP.remove(bought)
|
||||||
|
|
||||||
|
|
||||||
|
def scanning() -> list:
|
||||||
|
scan, scanned = "", []
|
||||||
|
while True:
|
||||||
|
i, _, _ = timedInput([stdin], [], [], TIMEOUT)
|
||||||
|
if not i:
|
||||||
|
break # send a short timeout message before break
|
||||||
|
scan = stdin.readline().strip()
|
||||||
|
match scan:
|
||||||
|
case "logout":
|
||||||
|
break
|
||||||
|
case "delete":
|
||||||
|
while True:
|
||||||
|
i, _, _ = timedInput([stdin], [], [], TIMEOUT)
|
||||||
|
if not i:
|
||||||
|
break # send a short timeout message before break
|
||||||
|
scan = stdin.readline().strip()
|
||||||
|
try:
|
||||||
|
scanned.remove(scan)
|
||||||
|
except ValueError as e:
|
||||||
|
pass
|
||||||
|
case _:
|
||||||
|
scanned.append(scan)
|
||||||
|
return scanned
|
||||||
|
|
||||||
|
|
||||||
|
def group_scanning(scanned: list[int]) -> dict[int: int]:
|
||||||
|
scan_dict = {}
|
||||||
|
for scan in scanned:
|
||||||
|
if scan not in scan_dict:
|
||||||
|
scan_dict[scan] = 1
|
||||||
|
else:
|
||||||
|
scan_dict[scan] += 1
|
||||||
|
return scan_dict
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
6
server/src/config.yaml.template
Normal file
6
server/src/config.yaml.template
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
database:
|
||||||
|
host: "hostname or ip"
|
||||||
|
port: "port"
|
||||||
|
database: "databasename"
|
||||||
|
user: "username"
|
||||||
|
password: "password"
|
||||||
127
server/src/database.py
Normal file
127
server/src/database.py
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
from psycopg2 import connect as psyconn, ProgrammingError
|
||||||
|
from yaml import safe_load
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
LOGGER.setLevel(logging.DEBUG)
|
||||||
|
logFormatter = logging.Formatter(
|
||||||
|
"%(asctime)s [%(threadName)s] [%(levelname)s] %(message)s")
|
||||||
|
|
||||||
|
fileHandler = logging.FileHandler("../logs/database.log")
|
||||||
|
fileHandler.setFormatter(logFormatter)
|
||||||
|
fileHandler.setLevel(logging.INFO)
|
||||||
|
LOGGER.addHandler(fileHandler)
|
||||||
|
|
||||||
|
consoleHandler = logging.StreamHandler()
|
||||||
|
consoleHandler.setLevel(logging.DEBUG)
|
||||||
|
LOGGER.addHandler(consoleHandler)
|
||||||
|
|
||||||
|
|
||||||
|
class Database:
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.connect(**kwargs)
|
||||||
|
|
||||||
|
def connect(self, **kwargs):
|
||||||
|
with open('config.yaml', 'r') as file:
|
||||||
|
data = safe_load(file)['database']
|
||||||
|
LOGGER.debug('Merging passed arguments with default arguments.')
|
||||||
|
for key, value in data.items():
|
||||||
|
if key not in kwargs or not kwargs[key]:
|
||||||
|
kwargs[key] = value
|
||||||
|
LOGGER.info('Connecting to Database.')
|
||||||
|
self.conn = psyconn(host=kwargs["host"], port=kwargs["port"], dbname=kwargs["database"],
|
||||||
|
user=kwargs["user"], password=kwargs["password"])
|
||||||
|
|
||||||
|
def test_connection(self):
|
||||||
|
if not hasattr(self, "conn"):
|
||||||
|
LOGGER.info("Connection was not set, setting...")
|
||||||
|
self.connect()
|
||||||
|
else:
|
||||||
|
with self.conn.cursor() as cursor:
|
||||||
|
try:
|
||||||
|
cursor.execute("SELECT 1;")
|
||||||
|
cursor.fetchall()
|
||||||
|
except:
|
||||||
|
LOGGER.warn(
|
||||||
|
'Connection seem to timed out, reconnecting...')
|
||||||
|
self.connect()
|
||||||
|
|
||||||
|
def connectionpersistence(func):
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
self = args[0]
|
||||||
|
self.test_connection()
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
@connectionpersistence
|
||||||
|
def get_user(self, **kwargs):
|
||||||
|
result = ()
|
||||||
|
if 'login' in kwargs:
|
||||||
|
query = "SELECT login FROM users WHERE login = %(login)s;"
|
||||||
|
with self.conn.cursor() as cursor:
|
||||||
|
cursor.execute(query, kwargs)
|
||||||
|
try:
|
||||||
|
result = cursor.fetchall()
|
||||||
|
except ProgrammingError as e:
|
||||||
|
LOGGER.exception(e)
|
||||||
|
except Exception as e:
|
||||||
|
LOGGER.exception(e)
|
||||||
|
return result
|
||||||
|
|
||||||
|
@connectionpersistence
|
||||||
|
def get_report(self, **kwargs) -> list:
|
||||||
|
query = "SELECT u.name, bp.buy_date, i.name, bp.amount, bp.buy_price FROM bought_with_prices bp INNER JOIN items i ON bp.item = i.id INNER JOIN users u ON bp.user = u.login"
|
||||||
|
if kwargs:
|
||||||
|
query += " WHERE "
|
||||||
|
tempquery = []
|
||||||
|
if "user" in kwargs and kwargs['user']:
|
||||||
|
tempquery.append(f"bp.user = '{kwargs['user']}'")
|
||||||
|
if "year" in kwargs and kwargs['year']:
|
||||||
|
tempstring = ""
|
||||||
|
if "month" in kwargs and kwargs['month']:
|
||||||
|
tempstring += f"bp.buy_date BETWEEN '{kwargs['year']}-{kwargs['month']}-01' AND "
|
||||||
|
tempstring += f"'{kwargs['year']+1}-01-01'" if kwargs['month'] == 12 else f"'{kwargs['year']}-{kwargs['month']+1}-01'"
|
||||||
|
else:
|
||||||
|
tempstring += f"bp.buy_date BETWEEN '{kwargs['year']}-01-01' AND '{kwargs['year']+1}-01-01'"
|
||||||
|
tempstring += "::date - INTERVAL '1' DAY"
|
||||||
|
tempquery.append(tempstring)
|
||||||
|
query += " AND ".join(tempquery)
|
||||||
|
query += " ORDER BY u.name, bp.buy_date, i.name ASC;"
|
||||||
|
LOGGER.debug(f"Executing query: {query}")
|
||||||
|
result = []
|
||||||
|
with self.conn.cursor() as cursor:
|
||||||
|
cursor.execute(query)
|
||||||
|
try:
|
||||||
|
result = cursor.fetchall()
|
||||||
|
except ProgrammingError as e:
|
||||||
|
LOGGER.exception(e)
|
||||||
|
except Exception as e:
|
||||||
|
LOGGER.exception(e)
|
||||||
|
return result
|
||||||
|
|
||||||
|
@connectionpersistence
|
||||||
|
def insert_bought_items(self, user: str, items: dict, date: str = None):
|
||||||
|
temp = ["user, item, amount", "%(user)s, %(item)s, %(amount)s", "bought.user = %(user)s AND bought.item = %(item)s AND bought.date = "]
|
||||||
|
temp[2] += "%(date)s" if date else "NOW()"
|
||||||
|
if date:
|
||||||
|
temp[0] += ", date"
|
||||||
|
temp[1] += ", %(date)s"
|
||||||
|
query = f"INSERT INTO bought({temp[0]}) VALUES({temp[1]}) ON CONFLICT ON CONSTRAINT bought_user_item_buy_date DO UPDATE SET amount = bought.amount + %(amount)s WHERE {temp[2]};"
|
||||||
|
with self.conn.cursor() as cursor:
|
||||||
|
try:
|
||||||
|
if date:
|
||||||
|
cursor.executemany(query, [
|
||||||
|
{'user': user, 'item': key, 'amount': value, 'date': date} for key, value in items.items()])
|
||||||
|
else:
|
||||||
|
cursor.executemany(query, [
|
||||||
|
{'user': user, 'item': key, 'amount': value} for key, value in items.items()])
|
||||||
|
self.conn.commit()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
LOGGER.debug(e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __delete__(self):
|
||||||
|
self.conn.close()
|
||||||
94
server/src/main.py
Normal file
94
server/src/main.py
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
from datetime import date
|
||||||
|
from flask import Flask, abort, request
|
||||||
|
from flask.json import jsonify
|
||||||
|
from database import Database
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
LOGGER.setLevel(logging.DEBUG)
|
||||||
|
logFormatter = logging.Formatter(
|
||||||
|
"%(asctime)s [%(threadName)s] [%(levelname)s] %(message)s")
|
||||||
|
|
||||||
|
fileHandler = logging.FileHandler("../logs/server.log")
|
||||||
|
fileHandler.setFormatter(logFormatter)
|
||||||
|
fileHandler.setLevel(logging.INFO)
|
||||||
|
LOGGER.addHandler(fileHandler)
|
||||||
|
|
||||||
|
consoleHandler = logging.StreamHandler()
|
||||||
|
consoleHandler.setLevel(logging.DEBUG)
|
||||||
|
LOGGER.addHandler(consoleHandler)
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
DATABASE = Database()
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
return "<h1>Hello, World!</h>", 200
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/scan2kasse/login')
|
||||||
|
def login():
|
||||||
|
if not request.json or 'login' not in request.json:
|
||||||
|
abort(400)
|
||||||
|
if not DATABASE.get_user(login = request.json['login']):
|
||||||
|
abort(403)
|
||||||
|
return jsonify({}), 200
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/scan2kasse/insert', methods=['POST'])
|
||||||
|
def insert():
|
||||||
|
match request.json:
|
||||||
|
case {'login': login, 'items': items, 'date': date}:
|
||||||
|
if DATABASE.insert_bought_items(login, items, date):
|
||||||
|
return jsonify({'insert': True}), 201
|
||||||
|
return jsonify({'insert': False}), 400
|
||||||
|
case {'login': login, 'items': items}:
|
||||||
|
if DATABASE.insert_bought_items(login, items):
|
||||||
|
return jsonify({'insert': True}), 201
|
||||||
|
return jsonify({'insert': False}), 400
|
||||||
|
case _:
|
||||||
|
abort(400)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/scan2kasse/<int:year>/<int:month>', methods=['GET'])
|
||||||
|
def get_monthly_report(year: int, month: int):
|
||||||
|
return get_monthly_report_from_user(year=year, month=month)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/scan2kasse/<string:user>', methods=['GET'])
|
||||||
|
def get_report_from_user(user: str):
|
||||||
|
return get_monthly_report_from_user(user=user)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/scan2kasse/<string:user>/<int:year>/<int:month>', methods=['GET'])
|
||||||
|
def get_monthly_report_from_user(user: str = None, year: int = None, month: int = None):
|
||||||
|
if month and (month > 12 or month < 1):
|
||||||
|
abort(400)
|
||||||
|
LOGGER.info("Getting results.")
|
||||||
|
results = DATABASE.get_report(user=user, year=year, month=month)
|
||||||
|
LOGGER.debug("Results get")
|
||||||
|
if results:
|
||||||
|
result_dict = group_results(results)
|
||||||
|
else:
|
||||||
|
result_dict = {}
|
||||||
|
return jsonify(result_dict)
|
||||||
|
|
||||||
|
|
||||||
|
def group_results(results: tuple) -> dict:
|
||||||
|
result_dict = {}
|
||||||
|
LOGGER.debug("Grouping...")
|
||||||
|
for result in results:
|
||||||
|
if result[0] not in result_dict:
|
||||||
|
result_dict[result[0]] = {}
|
||||||
|
if result[1] not in result_dict[result[0]]:
|
||||||
|
result_dict[result[0]][str(result[1])] = {}
|
||||||
|
result_dict[result[0]][str(result[1])][result[2]] = (
|
||||||
|
result[3], result[4])
|
||||||
|
LOGGER.debug("Grouped.")
|
||||||
|
return result_dict
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(debug=True)
|
||||||
Loading…
x
Reference in New Issue
Block a user