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
|
||||
|
||||
### Signal catching
|
||||
|
||||
- [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)
|
||||
- ~~[Get ENUM name from value](https://stackoverflow.com/a/38716384)~~
|
||||
Unused because of required compatibility with lower version python (3.9)
|
||||
|
||||
### Networking
|
||||
|
||||
- [Dynamically finding host IP address](https://stackoverflow.com/a/28950776)
|
||||
- [IP Regex](https://ihateregex.io/expr/ip/)
|
||||
|
||||
### Database
|
||||
|
||||
- [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)
|
||||
|
||||
def start(self):
|
||||
try:
|
||||
for port in range(self.config.scan_port_start, self.config.scan_port_end + 1):
|
||||
self.logger.debug("Trying port %d", port)
|
||||
try:
|
||||
@ -60,9 +61,18 @@ class BankNode():
|
||||
except socket.error as e:
|
||||
if e.errno == 98: # Address is in use
|
||||
self.logger.info("Port %d in use, trying next port", port)
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
self.logger.error("All ports are in use")
|
||||
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):
|
||||
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]
|
||||
try:
|
||||
response = command(request, self.config)
|
||||
if response is not None:
|
||||
return f"{request.command_code} {response}"
|
||||
else:
|
||||
return request.command_code
|
||||
except DatabaseError as e:
|
||||
return f"ER {e.message}"
|
||||
|
@ -1,14 +1,19 @@
|
||||
from core import Request, BankNodeConfig
|
||||
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):
|
||||
try:
|
||||
split_body = request.body.split("/")
|
||||
split_ip = split_body[1].split(" ")
|
||||
|
||||
account = split_body[0]
|
||||
ip = split_ip[0]
|
||||
amount = split_ip[1]
|
||||
except IndexError as e:
|
||||
raise InvalidRequest("Invalid request format") from e
|
||||
|
||||
if not account.isdigit():
|
||||
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:
|
||||
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"]
|
||||
|
@ -1,12 +1,10 @@
|
||||
from typing import Dict
|
||||
|
||||
from core import Request, Response
|
||||
from core import Request, Response, BankNodeConfig
|
||||
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:
|
||||
raise InvalidRequest("Incorrect usage")
|
||||
|
||||
return config["ip"]
|
||||
return config.ip
|
||||
|
||||
__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):
|
||||
pass
|
||||
def bank_number_of_clients(request: Request, _) -> Response:
|
||||
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"]
|
||||
|
@ -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):
|
||||
pass
|
||||
def bank_total_amount(request: Request, _):
|
||||
if request.body is not None:
|
||||
raise InvalidRequest("Incorrect usage")
|
||||
|
||||
total_balace = get_total_balance()
|
||||
return total_balace
|
||||
|
||||
|
||||
__all__ = ["bank_total_amount"]
|
||||
|
@ -48,8 +48,7 @@ class BankNodeConfig:
|
||||
|
||||
# IP validation
|
||||
if not re.match(IP_REGEX, ip):
|
||||
self.logger.error("Invalid IP in configuration")
|
||||
raise ConfigError("Invalid IP in configuration")
|
||||
raise ConfigError(f"Invalid IP {ip} in configuration")
|
||||
|
||||
self.used_port: int
|
||||
self.ip = ip
|
||||
|
@ -4,9 +4,9 @@ from contextlib import contextmanager
|
||||
|
||||
from sqlalchemy.orm import sessionmaker, Session
|
||||
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
|
||||
|
||||
|
||||
@ -45,9 +45,9 @@ class DatabaseManager():
|
||||
connection.execute(text("select 1"))
|
||||
self.logger.debug("Database connection successful")
|
||||
return True
|
||||
except DatabaseError as e:
|
||||
except SqlAlchemyDatabaseError as 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
|
||||
|
||||
|
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):
|
||||
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)
|
||||
self.message = message
|
||||
self.errno = errno
|
||||
|
||||
for key, value in kwargs.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
|
||||
class DatabaseConnectionError(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"]
|
||||
__all__ = ["DatabaseError"]
|
||||
|
@ -2,21 +2,24 @@ from sqlalchemy import func
|
||||
|
||||
from models import Account
|
||||
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
|
||||
|
||||
|
||||
@handle_database_errors
|
||||
def get_next_id() -> int:
|
||||
with DatabaseManager.get_session() as session:
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
|
||||
@handle_database_errors
|
||||
def create_account() -> int:
|
||||
new_id = get_next_id()
|
||||
|
||||
@ -27,47 +30,68 @@ def create_account() -> int:
|
||||
return new_id
|
||||
|
||||
|
||||
@handle_database_errors
|
||||
def get_account_balance(account_number: int) -> int:
|
||||
with DatabaseManager.get_session() as session:
|
||||
account: Account = session.query(Account).where(Account.account_number == account_number).one_or_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
|
||||
|
||||
|
||||
@handle_database_errors
|
||||
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)
|
||||
|
||||
|
||||
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:
|
||||
account: Account = session.query(Account).where(Account.account_number == account_number).one_or_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
|
||||
else:
|
||||
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
|
||||
|
||||
session.commit()
|
||||
|
||||
|
||||
@handle_database_errors
|
||||
def delete_account(account_number: int):
|
||||
with DatabaseManager.get_session() as session:
|
||||
account: Account = session.query(Account).where(Account.account_number == account_number).one_or_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:
|
||||
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.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 sys
|
||||
|
||||
IP_REGEX = r"^[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}$"
|
||||
ACCOUNT_NUMBER_REGEX = r"[0-9]{9}"
|
||||
MONEY_AMOUNT_MAXIMUM = (2 ^ 63) - 1
|
||||
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"^(1[0-9]{8})|([2-9][0-9]{8})$"
|
||||
MONEY_AMOUNT_MAXIMUM = 2**63 - 1
|
||||
|
||||
MIN_ACCOUNT_NUMBER = 10_000
|
||||
MAX_ACCOUNT_NUMBER = 99_999
|
||||
|
Loading…
x
Reference in New Issue
Block a user