Added an error catching decorator and finished account deposit
This commit is contained in:
parent
e28e1027d0
commit
7a7417ac1c
10
README.md
10
README.md
@ -1,14 +1,20 @@
|
|||||||
# ksi
|
# nu (ν)
|
||||||
|
|
||||||
## Sources
|
## Sources
|
||||||
|
|
||||||
### Signal catching
|
### Signal catching
|
||||||
|
|
||||||
- [Catch SIGTERM](https://dnmtechs.com/graceful-sigterm-signal-handling-in-python-3-best-practices-and-implementation/)
|
- [Catch SIGTERM](https://dnmtechs.com/graceful-sigterm-signal-handling-in-python-3-best-practices-and-implementation/)
|
||||||
- [Get ENUM name from value](https://stackoverflow.com/a/38716384)
|
|
||||||
- [Windows termination signals](https://stackoverflow.com/a/35792192)
|
- [Windows termination signals](https://stackoverflow.com/a/35792192)
|
||||||
|
- ~~[Get ENUM name from value](https://stackoverflow.com/a/38716384)~~
|
||||||
|
Unused because of required compatibility with lower version python (3.9)
|
||||||
|
|
||||||
### Networking
|
### Networking
|
||||||
|
|
||||||
- [Dynamically finding host IP address](https://stackoverflow.com/a/28950776)
|
- [Dynamically finding host IP address](https://stackoverflow.com/a/28950776)
|
||||||
|
- [IP Regex](https://ihateregex.io/expr/ip/)
|
||||||
|
|
||||||
### Database
|
### Database
|
||||||
|
|
||||||
- [SqlAlchemy session generator](https://stackoverflow.com/a/71053353)
|
- [SqlAlchemy session generator](https://stackoverflow.com/a/71053353)
|
||||||
|
- [Error handling decorator](https://chatgpt.com/share/67a46109-d38c-8005-ac36-677e6511ddcd)
|
||||||
|
@ -51,6 +51,7 @@ class BankNode():
|
|||||||
signal.signal(signal.SIGBREAK, self.gracefully_exit)
|
signal.signal(signal.SIGBREAK, self.gracefully_exit)
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
|
try:
|
||||||
for port in range(self.config.scan_port_start, self.config.scan_port_end + 1):
|
for port in range(self.config.scan_port_start, self.config.scan_port_end + 1):
|
||||||
self.logger.debug("Trying port %d", port)
|
self.logger.debug("Trying port %d", port)
|
||||||
try:
|
try:
|
||||||
@ -60,9 +61,18 @@ class BankNode():
|
|||||||
except socket.error as e:
|
except socket.error as e:
|
||||||
if e.errno == 98: # Address is in use
|
if e.errno == 98: # Address is in use
|
||||||
self.logger.info("Port %d in use, trying next port", port)
|
self.logger.info("Port %d in use, trying next port", port)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
self.logger.error("All ports are in use")
|
self.logger.error("All ports are in use")
|
||||||
self.exit_with_error()
|
self.exit_with_error()
|
||||||
|
except socket.error as e:
|
||||||
|
if e.errno == 99: # Cannot assign to requested address
|
||||||
|
self.logger.critical("Cannot use the IP address %s", self.config.ip)
|
||||||
|
else:
|
||||||
|
self.logger.critical("Unknown error: %s", e)
|
||||||
|
self.exit_with_error()
|
||||||
|
|
||||||
def __start_server(self, port: int):
|
def __start_server(self, port: int):
|
||||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as socket_server:
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as socket_server:
|
||||||
|
@ -39,6 +39,9 @@ class CommandHandler:
|
|||||||
command = self.registered_commands[request.command_code]
|
command = self.registered_commands[request.command_code]
|
||||||
try:
|
try:
|
||||||
response = command(request, self.config)
|
response = command(request, self.config)
|
||||||
|
if response is not None:
|
||||||
return f"{request.command_code} {response}"
|
return f"{request.command_code} {response}"
|
||||||
|
else:
|
||||||
|
return request.command_code
|
||||||
except DatabaseError as e:
|
except DatabaseError as e:
|
||||||
return f"ER {e.message}"
|
return f"ER {e.message}"
|
||||||
|
@ -1,14 +1,19 @@
|
|||||||
from core import Request, BankNodeConfig
|
from core import Request, BankNodeConfig
|
||||||
from bank_protocol.exceptions import InvalidRequest
|
from bank_protocol.exceptions import InvalidRequest
|
||||||
|
from services.account_service import deposit_into_account
|
||||||
|
from utils.constants import MONEY_AMOUNT_MAXIMUM
|
||||||
|
|
||||||
|
|
||||||
def account_deposit(request: Request, config: BankNodeConfig):
|
def account_deposit(request: Request, config: BankNodeConfig):
|
||||||
|
try:
|
||||||
split_body = request.body.split("/")
|
split_body = request.body.split("/")
|
||||||
split_ip = split_body[1].split(" ")
|
split_ip = split_body[1].split(" ")
|
||||||
|
|
||||||
account = split_body[0]
|
account = split_body[0]
|
||||||
ip = split_ip[0]
|
ip = split_ip[0]
|
||||||
amount = split_ip[1]
|
amount = split_ip[1]
|
||||||
|
except IndexError as e:
|
||||||
|
raise InvalidRequest("Invalid request format") from e
|
||||||
|
|
||||||
if not account.isdigit():
|
if not account.isdigit():
|
||||||
raise InvalidRequest("Account must be a number")
|
raise InvalidRequest("Account must be a number")
|
||||||
@ -18,5 +23,15 @@ def account_deposit(request: Request, config: BankNodeConfig):
|
|||||||
if account_parsed < 10_000 or account_parsed > 99_999:
|
if account_parsed < 10_000 or account_parsed > 99_999:
|
||||||
raise InvalidRequest("Account number out of range")
|
raise InvalidRequest("Account number out of range")
|
||||||
|
|
||||||
|
if not amount.isdigit():
|
||||||
|
raise InvalidRequest("Deposit amount must be a number")
|
||||||
|
|
||||||
|
amount_parsed = int(amount)
|
||||||
|
|
||||||
|
if amount_parsed > MONEY_AMOUNT_MAXIMUM:
|
||||||
|
raise InvalidRequest("Cannot deposit this much")
|
||||||
|
|
||||||
|
deposit_into_account(account_parsed, amount_parsed)
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["account_deposit"]
|
__all__ = ["account_deposit"]
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
from typing import Dict
|
from core import Request, Response, BankNodeConfig
|
||||||
|
|
||||||
from core import Request, Response
|
|
||||||
from bank_protocol.exceptions import InvalidRequest
|
from bank_protocol.exceptions import InvalidRequest
|
||||||
|
|
||||||
def bank_code(request: Request, config: Dict) -> Response:
|
def bank_code(request: Request, config: BankNodeConfig) -> Response:
|
||||||
if request.body is not None:
|
if request.body is not None:
|
||||||
raise InvalidRequest("Incorrect usage")
|
raise InvalidRequest("Incorrect usage")
|
||||||
|
|
||||||
return config["ip"]
|
return config.ip
|
||||||
|
|
||||||
__all__ = ["bank_code"]
|
__all__ = ["bank_code"]
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
from core.request import Request
|
from core import Request, Response
|
||||||
|
from bank_protocol.exceptions import InvalidRequest
|
||||||
|
from services.account_service import get_account_count
|
||||||
|
|
||||||
def bank_number_of_clients(request: Request):
|
def bank_number_of_clients(request: Request, _) -> Response:
|
||||||
pass
|
if request.body is not None:
|
||||||
|
raise InvalidRequest("Incorrect usage")
|
||||||
|
|
||||||
|
number_of_clients = get_account_count()
|
||||||
|
return number_of_clients
|
||||||
|
|
||||||
__all__ = ["bank_number_of_clients"]
|
__all__ = ["bank_number_of_clients"]
|
||||||
|
@ -1,7 +1,13 @@
|
|||||||
from core.request import Request
|
from core import Request, Response
|
||||||
|
from bank_protocol.exceptions import InvalidRequest
|
||||||
|
from services.account_service import get_total_balance
|
||||||
|
|
||||||
def bank_total_amount(request: Request):
|
def bank_total_amount(request: Request, _):
|
||||||
pass
|
if request.body is not None:
|
||||||
|
raise InvalidRequest("Incorrect usage")
|
||||||
|
|
||||||
|
total_balace = get_total_balance()
|
||||||
|
return total_balace
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["bank_total_amount"]
|
__all__ = ["bank_total_amount"]
|
||||||
|
@ -48,8 +48,7 @@ class BankNodeConfig:
|
|||||||
|
|
||||||
# IP validation
|
# IP validation
|
||||||
if not re.match(IP_REGEX, ip):
|
if not re.match(IP_REGEX, ip):
|
||||||
self.logger.error("Invalid IP in configuration")
|
raise ConfigError(f"Invalid IP {ip} in configuration")
|
||||||
raise ConfigError("Invalid IP in configuration")
|
|
||||||
|
|
||||||
self.used_port: int
|
self.used_port: int
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
|
@ -4,9 +4,9 @@ from contextlib import contextmanager
|
|||||||
|
|
||||||
from sqlalchemy.orm import sessionmaker, Session
|
from sqlalchemy.orm import sessionmaker, Session
|
||||||
from sqlalchemy import create_engine, text
|
from sqlalchemy import create_engine, text
|
||||||
from sqlalchemy.exc import DatabaseError
|
from sqlalchemy.exc import DatabaseError as SqlAlchemyDatabaseError
|
||||||
|
|
||||||
from database.exceptions import DatabaseConnectionError
|
from database.exceptions import DatabaseError
|
||||||
from models.base_model import Base
|
from models.base_model import Base
|
||||||
|
|
||||||
|
|
||||||
@ -45,9 +45,9 @@ class DatabaseManager():
|
|||||||
connection.execute(text("select 1"))
|
connection.execute(text("select 1"))
|
||||||
self.logger.debug("Database connection successful")
|
self.logger.debug("Database connection successful")
|
||||||
return True
|
return True
|
||||||
except DatabaseError as e:
|
except SqlAlchemyDatabaseError as e:
|
||||||
self.logger.critical("Database connection failed: %s", e)
|
self.logger.critical("Database connection failed: %s", e)
|
||||||
raise DatabaseConnectionError("Database connection failed") from e
|
raise DatabaseError("Database connection failed", DatabaseError.CONNECTION_ERROR) from e
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
13
src/database/exception_catcher_decorator.py
Normal file
13
src/database/exception_catcher_decorator.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from functools import wraps
|
||||||
|
from sqlalchemy.exc import DatabaseError as SqlAlchemyDatabaseError
|
||||||
|
from database.exceptions import DatabaseError
|
||||||
|
|
||||||
|
|
||||||
|
def handle_database_errors(func):
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
except SqlAlchemyDatabaseError as e:
|
||||||
|
raise DatabaseError(str(e), -1) from e
|
||||||
|
return wrapper
|
@ -1,52 +1,24 @@
|
|||||||
class DatabaseError(Exception):
|
class DatabaseError(Exception):
|
||||||
def __init__(self, message: str):
|
# Inspired by OSError which also uses errno's
|
||||||
|
# It's a better approach than using a class for each error
|
||||||
|
UNKNOWN_ERROR = -1
|
||||||
|
|
||||||
|
CONNECTION_ERROR = 1
|
||||||
|
EMPTY_CONFIG = 2
|
||||||
|
DUPLICATE_ENTRY = 3
|
||||||
|
NONEXISTENT_ACCOUNT = 4
|
||||||
|
OUT_OF_ACCOUNT_SPACE = 5
|
||||||
|
INSUFFICIENT_BALANCE = 6
|
||||||
|
INVALID_OPERATION = 7
|
||||||
|
|
||||||
|
def __init__(self, message: str, errno: int, **kwargs):
|
||||||
|
|
||||||
super().__init__(message)
|
super().__init__(message)
|
||||||
self.message = message
|
self.message = message
|
||||||
|
self.errno = errno
|
||||||
|
|
||||||
|
for key, value in kwargs.items():
|
||||||
|
setattr(self, key, value)
|
||||||
|
|
||||||
|
|
||||||
class DatabaseConnectionError(DatabaseError):
|
__all__ = ["DatabaseError"]
|
||||||
def __init__(self, message: str):
|
|
||||||
super().__init__(message)
|
|
||||||
self.message = message
|
|
||||||
|
|
||||||
|
|
||||||
class EmptyDatabaseConfigError(Exception):
|
|
||||||
def __init__(self, message: str, config_name: str):
|
|
||||||
super().__init__(message)
|
|
||||||
|
|
||||||
self.message = message
|
|
||||||
self.config_name = config_name
|
|
||||||
|
|
||||||
|
|
||||||
class DuplicateEntryError(DatabaseError):
|
|
||||||
def __init__(self, duplicate_entry_name: str, message: str):
|
|
||||||
super().__init__(message)
|
|
||||||
self.duplicate_entry_name = duplicate_entry_name
|
|
||||||
self.message = message
|
|
||||||
|
|
||||||
|
|
||||||
class NonexistentAccountError(DatabaseError):
|
|
||||||
def __init__(self, message: str):
|
|
||||||
super().__init__(message)
|
|
||||||
self.message = message
|
|
||||||
|
|
||||||
|
|
||||||
class OutOfAccountSpaceError(DatabaseError):
|
|
||||||
def __init__(self, message: str):
|
|
||||||
super().__init__(message)
|
|
||||||
self.message = message
|
|
||||||
|
|
||||||
|
|
||||||
class InsufficientBalance(DatabaseError):
|
|
||||||
def __init__(self, message: str):
|
|
||||||
super().__init__(message)
|
|
||||||
self.message = message
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidOperation(DatabaseError):
|
|
||||||
def __init__(self, message: str):
|
|
||||||
super().__init__(message)
|
|
||||||
self.message = message
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["DatabaseError", "DatabaseConnectionError", "DuplicateEntryError", "InsufficientBalance", "OutOfAccountSpaceError", "NonexistentAccountError", "EmptyDatabaseConfigError", "InvalidOperation"]
|
|
||||||
|
@ -2,21 +2,24 @@ from sqlalchemy import func
|
|||||||
|
|
||||||
from models import Account
|
from models import Account
|
||||||
from database import DatabaseManager
|
from database import DatabaseManager
|
||||||
from database.exceptions import OutOfAccountSpaceError, NonexistentAccountError, InsufficientBalance, InvalidOperation
|
from database.exceptions import DatabaseError
|
||||||
|
from database.exception_catcher_decorator import handle_database_errors
|
||||||
from utils.constants import MIN_ACCOUNT_NUMBER, MAX_ACCOUNT_NUMBER
|
from utils.constants import MIN_ACCOUNT_NUMBER, MAX_ACCOUNT_NUMBER
|
||||||
|
|
||||||
|
|
||||||
|
@handle_database_errors
|
||||||
def get_next_id() -> int:
|
def get_next_id() -> int:
|
||||||
with DatabaseManager.get_session() as session:
|
with DatabaseManager.get_session() as session:
|
||||||
current_max_id = session.query(func.max(Account.account_number)).scalar()
|
current_max_id = session.query(func.max(Account.account_number)).scalar()
|
||||||
current_max_id = current_max_id + 1 if current_max_id is not None else MIN_ACCOUNT_NUMBER
|
current_max_id = current_max_id + 1 if current_max_id is not None else MIN_ACCOUNT_NUMBER
|
||||||
|
|
||||||
if current_max_id > MAX_ACCOUNT_NUMBER:
|
if current_max_id > MAX_ACCOUNT_NUMBER:
|
||||||
raise OutOfAccountSpaceError("Too many users already exist, cannot open new account")
|
raise DatabaseError("Too many users already exist, cannot open new account", DatabaseError.OUT_OF_ACCOUNT_SPACE)
|
||||||
|
|
||||||
return current_max_id
|
return current_max_id
|
||||||
|
|
||||||
|
|
||||||
|
@handle_database_errors
|
||||||
def create_account() -> int:
|
def create_account() -> int:
|
||||||
new_id = get_next_id()
|
new_id = get_next_id()
|
||||||
|
|
||||||
@ -27,47 +30,68 @@ def create_account() -> int:
|
|||||||
return new_id
|
return new_id
|
||||||
|
|
||||||
|
|
||||||
|
@handle_database_errors
|
||||||
def get_account_balance(account_number: int) -> int:
|
def get_account_balance(account_number: int) -> int:
|
||||||
with DatabaseManager.get_session() as session:
|
with DatabaseManager.get_session() as session:
|
||||||
account: Account = session.query(Account).where(Account.account_number == account_number).one_or_none()
|
account: Account = session.query(Account).where(Account.account_number == account_number).one_or_none()
|
||||||
if account is None:
|
if account is None:
|
||||||
raise NonexistentAccountError(f"Account with number {account_number} doesn't exist")
|
raise DatabaseError(f"Account with number {account_number} doesn't exist", DatabaseError.NONEXISTENT_ACCOUNT)
|
||||||
|
|
||||||
return account.balance
|
return account.balance
|
||||||
|
|
||||||
|
|
||||||
|
@handle_database_errors
|
||||||
def withdraw_from_account(account_number: int, amount: int):
|
def withdraw_from_account(account_number: int, amount: int):
|
||||||
modify_balance(account_number, amount, True)
|
|
||||||
|
|
||||||
|
|
||||||
def deposit_into_account(account_number: int, amount: int):
|
|
||||||
modify_balance(account_number, amount, False)
|
modify_balance(account_number, amount, False)
|
||||||
|
|
||||||
|
|
||||||
def modify_balance(account_number: int, amount: int, subtract: bool):
|
@handle_database_errors
|
||||||
|
def deposit_into_account(account_number: int, amount: int):
|
||||||
|
modify_balance(account_number, amount, True)
|
||||||
|
|
||||||
|
|
||||||
|
@handle_database_errors
|
||||||
|
def modify_balance(account_number: int, amount: int, add: bool):
|
||||||
with DatabaseManager.get_session() as session:
|
with DatabaseManager.get_session() as session:
|
||||||
account: Account = session.query(Account).where(Account.account_number == account_number).one_or_none()
|
account: Account = session.query(Account).where(Account.account_number == account_number).one_or_none()
|
||||||
if account is None:
|
if account is None:
|
||||||
raise NonexistentAccountError(f"Account with number {account_number} doesn't exist")
|
raise DatabaseError(f"Account with number {account_number} doesn't exist", DatabaseError.NONEXISTENT_ACCOUNT)
|
||||||
|
|
||||||
if subtract:
|
if add:
|
||||||
account.balance += amount
|
account.balance += amount
|
||||||
else:
|
else:
|
||||||
if account.balance - amount < 0:
|
if account.balance - amount < 0:
|
||||||
raise InsufficientBalance("Not enough funds on account to withdraw this much")
|
raise DatabaseError("Not enough funds on account to withdraw this much", DatabaseError.INSUFFICIENT_BALANCE)
|
||||||
account.balance -= amount
|
account.balance -= amount
|
||||||
|
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@handle_database_errors
|
||||||
def delete_account(account_number: int):
|
def delete_account(account_number: int):
|
||||||
with DatabaseManager.get_session() as session:
|
with DatabaseManager.get_session() as session:
|
||||||
account: Account = session.query(Account).where(Account.account_number == account_number).one_or_none()
|
account: Account = session.query(Account).where(Account.account_number == account_number).one_or_none()
|
||||||
if account is None:
|
if account is None:
|
||||||
raise NonexistentAccountError(f"Account with number {account_number} doesn't exist")
|
raise DatabaseError(f"Account with number {account_number} doesn't exist", DatabaseError.NONEXISTENT_ACCOUNT)
|
||||||
|
|
||||||
if account.balance > 0:
|
if account.balance > 0:
|
||||||
raise InvalidOperation("Cannot delete an account with leftover funds")
|
raise DatabaseError("Cannot delete an account with leftover funds", DatabaseError.INVALID_OPERATION)
|
||||||
|
|
||||||
session.delete(account)
|
session.delete(account)
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@handle_database_errors
|
||||||
|
def get_total_balance() -> int:
|
||||||
|
with DatabaseManager.get_session() as session:
|
||||||
|
total_sum = session.query(func.sum(Account.balance)).scalar()
|
||||||
|
|
||||||
|
return total_sum if total_sum is not None else 0
|
||||||
|
|
||||||
|
|
||||||
|
@handle_database_errors
|
||||||
|
def get_account_count() -> int:
|
||||||
|
with DatabaseManager.get_session() as session:
|
||||||
|
total_sum = session.query(func.count(Account.account_number)).scalar()
|
||||||
|
|
||||||
|
return total_sum if total_sum is not None else 0
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
IP_REGEX = r"^[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}$"
|
IP_REGEX = r"^(\b25[0-5]|\b2[0-4][0-9]|\b[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$"
|
||||||
ACCOUNT_NUMBER_REGEX = r"[0-9]{9}"
|
ACCOUNT_NUMBER_REGEX = r"^(1[0-9]{8})|([2-9][0-9]{8})$"
|
||||||
MONEY_AMOUNT_MAXIMUM = (2 ^ 63) - 1
|
MONEY_AMOUNT_MAXIMUM = 2**63 - 1
|
||||||
|
|
||||||
MIN_ACCOUNT_NUMBER = 10_000
|
MIN_ACCOUNT_NUMBER = 10_000
|
||||||
MAX_ACCOUNT_NUMBER = 99_999
|
MAX_ACCOUNT_NUMBER = 99_999
|
||||||
|
Loading…
x
Reference in New Issue
Block a user