diff --git a/.gitignore b/.gitignore index b6e4761..c325b47 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,6 @@ dmypy.json # Pyre type checker .pyre/ + +# Misc +config.yaml \ No newline at end of file diff --git a/client/src/config.yaml.template b/client/src/config.yaml.template new file mode 100644 index 0000000..166e124 --- /dev/null +++ b/client/src/config.yaml.template @@ -0,0 +1,3 @@ +server: + host: "http://localhost" + port: "5000" \ No newline at end of file diff --git a/client/src/connection.py b/client/src/connection.py new file mode 100644 index 0000000..17e6e3e --- /dev/null +++ b/client/src/connection.py @@ -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 diff --git a/client/src/main.py b/client/src/main.py new file mode 100644 index 0000000..f0c3e30 --- /dev/null +++ b/client/src/main.py @@ -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() \ No newline at end of file diff --git a/server/src/config.yaml.template b/server/src/config.yaml.template new file mode 100644 index 0000000..e78695b --- /dev/null +++ b/server/src/config.yaml.template @@ -0,0 +1,6 @@ +database: + host: "hostname or ip" + port: "port" + database: "databasename" + user: "username" + password: "password" \ No newline at end of file diff --git a/server/src/database.py b/server/src/database.py new file mode 100644 index 0000000..16a27ff --- /dev/null +++ b/server/src/database.py @@ -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() diff --git a/server/src/main.py b/server/src/main.py new file mode 100644 index 0000000..762a554 --- /dev/null +++ b/server/src/main.py @@ -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 "

Hello, World!", 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//', methods=['GET']) +def get_monthly_report(year: int, month: int): + return get_monthly_report_from_user(year=year, month=month) + + +@app.route('/scan2kasse/', methods=['GET']) +def get_report_from_user(user: str): + return get_monthly_report_from_user(user=user) + + +@app.route('/scan2kasse///', 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)