diff --git a/poetry.lock b/poetry.lock index 801d271..d036a1c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -135,6 +135,7 @@ files = [ [package.dependencies] mypy_extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} typing_extensions = ">=4.6.0" [package.extras] @@ -265,6 +266,47 @@ postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] pymysql = ["pymysql"] sqlcipher = ["sqlcipher3_binary"] +[[package]] +name = "tomli" +version = "2.2.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, +] + [[package]] name = "typing-extensions" version = "4.12.2" @@ -278,5 +320,5 @@ files = [ [metadata] lock-version = "2.0" -python-versions = "^3.12" -content-hash = "c924c9033c266a86318eb310c7ef433b346d0ad4ff1574e44f7c8c664653106f" +python-versions = "^3.8" +content-hash = "822be0591477b901b0d6f8a34048f570227733ebf8eda1cae6a079ab2f5a1415" diff --git a/pyproject.toml b/pyproject.toml index 69e18b9..ef7684f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ authors = ["Thastertyn "] readme = "README.md" [tool.poetry.dependencies] -python = "^3.6" +python = "^3.8" sqlalchemy = "^2.0.37" python-dotenv = "^1.0.1" diff --git a/src/bank_node/bank_node.py b/src/bank_node/bank_node.py index 5207dce..7e5af57 100644 --- a/src/bank_node/bank_node.py +++ b/src/bank_node/bank_node.py @@ -75,7 +75,7 @@ class BankNode(): client_socket, address = socket_server.accept() self.logger.info("%s connected", address[0]) - process = BankWorker(client_socket, address, self.config.to_dict()) + process = BankWorker(client_socket, address, self.config) process.start() def exit_with_error(self): diff --git a/src/bank_protocol/command_handler.py b/src/bank_protocol/command_handler.py index 2187897..0af99ea 100644 --- a/src/bank_protocol/command_handler.py +++ b/src/bank_protocol/command_handler.py @@ -3,6 +3,7 @@ from typing import Dict, Callable from core import Request, Response from core.exceptions import BankNodeError +from database.exceptions import DatabaseError from bank_protocol.commands import (account_balance, account_create, @@ -36,7 +37,8 @@ class CommandHandler: raise BankNodeError(f"Unknown command {request.command_code}") command = self.registered_commands[request.command_code] - - response = command(request, self.config) - - return f"{request.command_code} {response}" + try: + response = command(request, self.config) + return f"{request.command_code} {response}" + except DatabaseError as e: + return f"ER {e.message}" diff --git a/src/bank_protocol/commands/account_create_command.py b/src/bank_protocol/commands/account_create_command.py index acc007e..0867455 100644 --- a/src/bank_protocol/commands/account_create_command.py +++ b/src/bank_protocol/commands/account_create_command.py @@ -1,7 +1,16 @@ -from core.request import Request +from core import Request, Response, BankNodeConfig +from bank_protocol.exceptions import InvalidRequest +from services.account_service import create_account + + +def account_create(request: Request, config: BankNodeConfig) -> Response: + if request.body is not None: + raise InvalidRequest("Incorrect usage") + + account_number = create_account() + + return f"{account_number}/{config.ip}" -def account_create(request: Request): - pass __all__ = ["account_create"] diff --git a/src/bank_protocol/commands/account_deposit_command.py b/src/bank_protocol/commands/account_deposit_command.py index 88c4681..a6d017e 100644 --- a/src/bank_protocol/commands/account_deposit_command.py +++ b/src/bank_protocol/commands/account_deposit_command.py @@ -1,7 +1,8 @@ -from core import Request +from core import Request, BankNodeConfig from bank_protocol.exceptions import InvalidRequest -def account_deposit(request: Request): + +def account_deposit(request: Request, config: BankNodeConfig): split_body = request.body.split("/") split_ip = split_body[1].split(" ") diff --git a/src/database/database_manager.py b/src/database/database_manager.py index ecee8ae..72160d6 100644 --- a/src/database/database_manager.py +++ b/src/database/database_manager.py @@ -53,7 +53,7 @@ class DatabaseManager(): @classmethod @contextmanager - def get_session(cls) -> Generator[Session]: + def get_session(cls) -> Generator[Session, None, None]: session = cls._instance.Session() try: yield session diff --git a/src/database/exceptions.py b/src/database/exceptions.py index c7469c5..9376d59 100644 --- a/src/database/exceptions.py +++ b/src/database/exceptions.py @@ -37,4 +37,16 @@ class OutOfAccountSpaceError(DatabaseError): self.message = message -__all__ = ["DatabaseError", "DatabaseConnectionError", "DuplicateEntryError"] +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"] diff --git a/src/models/account_model.py b/src/models/account_model.py index 0366cab..35ca5a5 100644 --- a/src/models/account_model.py +++ b/src/models/account_model.py @@ -5,7 +5,7 @@ from .base_model import Base class Account(Base): __tablename__ = 'account' - __table_args__ = (CheckConstraint('account_number > 10000 and account_number <= 99999'),) + __table_args__ = (CheckConstraint('account_number >= 10000 and account_number <= 99999'),) account_number = Column(Integer, nullable=False, primary_key=True) balance = Column(Integer) diff --git a/src/services/account_serice.py b/src/services/account_serice.py deleted file mode 100644 index e0aebc6..0000000 --- a/src/services/account_serice.py +++ /dev/null @@ -1,47 +0,0 @@ -from sqlalchemy import func - -from models import Account -from database import DatabaseManager -from database.exceptions import OutOfAccountSpaceError, NonexistentAccountError -from utils.constants import MIN_ACCOUNT_NUMBER, MAX_ACCOUNT_NUMBER - - -def get_next_id() -> int: - with DatabaseManager.get_session() as session: - new_id = session.query(func.max(Account.account_number)).scalar() - new_id = new_id if new_id is not None else MIN_ACCOUNT_NUMBER - - if new_id > MAX_ACCOUNT_NUMBER: - raise OutOfAccountSpaceError("Too many users already exist, cannot open new account") - - return new_id - - -def create_account() -> int: - new_id = get_next_id() - - with DatabaseManager.get_session() as session: - new_account = Account(account_number=new_id, balance=0) - session.add(new_account) - session.commit() - return new_id - - -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 NotImplemented: - raise NonexistentAccountError(f"Account with number {account_number} doesn't exist") - - return account.balance - -def withdraw_from_account(): - pass - - -def deposit_into_account(): - pass - - -def delete_account(): - pass diff --git a/src/services/account_service.py b/src/services/account_service.py new file mode 100644 index 0000000..10eccc7 --- /dev/null +++ b/src/services/account_service.py @@ -0,0 +1,73 @@ +from sqlalchemy import func + +from models import Account +from database import DatabaseManager +from database.exceptions import OutOfAccountSpaceError, NonexistentAccountError, InsufficientBalance, InvalidOperation +from utils.constants import MIN_ACCOUNT_NUMBER, MAX_ACCOUNT_NUMBER + + +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") + + return current_max_id + + +def create_account() -> int: + new_id = get_next_id() + + with DatabaseManager.get_session() as session: + new_account = Account(account_number=new_id, balance=0) + session.add(new_account) + session.commit() + return new_id + + +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") + + return account.balance + + +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): + 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") + + if subtract: + account.balance += amount + else: + if account.balance - amount < 0: + raise InsufficientBalance("Not enough funds on account to withdraw this much") + account.balance -= amount + + session.commit() + + +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") + + if account.balance > 0: + raise InvalidOperation("Cannot delete an account with leftover funds") + + session.delete(account) + session.commit()