diff --git a/src/part4/cart.py b/src/part4/cart.py index aed9bb72..4851330d 100644 --- a/src/part4/cart.py +++ b/src/part4/cart.py @@ -9,7 +9,7 @@ an explanatory message to stdout and returns ``None``. """ -from .storage import Product +from .storage import QUANTITY_KEY, Product # TODO: при необходимости добавьте свои вспомогательные функции @@ -47,7 +47,22 @@ def add_to_cart( request. """ # TODO: реализуйте функцию - return 0 + if product_id not in storage: + print(f"no product with id {product_id}") + return None + quantity_old = storage[product_id][QUANTITY_KEY] + if quantity_old < quantity: + print( + f"not enough stock for product {product_id}: {quantity_old} available, {quantity} requested" + ) + return None + storage[product_id][QUANTITY_KEY] = quantity_old - quantity + if product_id in cart: + cart[product_id] += quantity + else: + cart[product_id] = quantity + + return cart[product_id] def remove_from_cart( @@ -84,4 +99,21 @@ def remove_from_cart( the cart does not hold the product or holds too few units. """ # TODO: реализуйте функцию - return 0 + if product_id not in cart: + print(f"product {product_id} is not in the cart") + return None + if cart[product_id] < quantity: + print( + f"cart holds only {cart[product_id]} unit(s) of product {product_id}, " + f"cannot remove {quantity}" + ) + return None + if product_id not in storage: + print(f"no product with id {product_id}") + return None + cart[product_id] -= quantity + storage[product_id][QUANTITY_KEY] += quantity + if cart[product_id] == 0: + del cart[product_id] + return 0 + return cart[product_id] diff --git a/src/part4/crud.py b/src/part4/crud.py index 471eff00..71f9ab9c 100644 --- a/src/part4/crud.py +++ b/src/part4/crud.py @@ -23,16 +23,16 @@ than a scan over ``storage.values()``. """ -import uuid # noqa: F401 +import uuid from typing import Any -from .storage import ( # noqa: F401 +from .storage import ( NAME_KEY, PRICE_KEY, QUANTITY_KEY, Product, ) -from .utils import normalize_price, normalize_product_name # noqa: F401 +from .utils import normalize_price, normalize_product_name def generate_product_id() -> str: @@ -42,7 +42,7 @@ def generate_product_id() -> str: A freshly generated UUID, stringified. """ # TODO: реализуйте функцию - return "" + return str(uuid.uuid4()) def create_product( @@ -74,7 +74,22 @@ def create_product( message is printed. """ # TODO: реализуйте функцию - return "" + name, price, quantity = fields[NAME_KEY], fields[PRICE_KEY], fields[QUANTITY_KEY] + normalized_name = normalize_product_name(name) + if not normalized_name: + print("product name must not be blank") + return None + if normalized_name in product_names: + print(f"product name '{normalized_name}' is already taken") + return None + newuuid = generate_product_id() + storage[newuuid] = { + NAME_KEY: normalized_name, + PRICE_KEY: normalize_price(price), + QUANTITY_KEY: quantity, + } + product_names.add(normalized_name) + return newuuid def read_product(storage: dict[str, Product], product_id: str) -> Product | None: @@ -89,7 +104,10 @@ def read_product(storage: dict[str, Product], product_id: str) -> Product | None that identifier (a message is printed in that case). """ # TODO: реализуйте функцию - return {} + if product_id not in storage: + print(f"no product with id {product_id}") + return None + return storage[product_id] def update_product( @@ -129,8 +147,30 @@ def update_product( and an explanatory message is printed. Renaming a product to its own current name is allowed. """ - # TODO: реализуйте функцию - return {} + if product_id not in storage: + print(f"no product with id {product_id}") + return None + name_old = storage[product_id][NAME_KEY] + if NAME_KEY in fields: + normalized_name = normalize_product_name(fields[NAME_KEY]) + if not normalized_name: + print("product name must not be blank") + return None + if normalized_name != name_old and normalized_name in product_names: + print(f"product name '{normalized_name}' is already taken") + return None + storage[product_id][NAME_KEY] = normalized_name + fields[NAME_KEY] = normalized_name + if name_old != normalized_name: + product_names.remove(name_old) + product_names.add(normalized_name) + if PRICE_KEY in fields: + normalized_price = normalize_price(fields[PRICE_KEY]) + fields[PRICE_KEY] = normalized_price + storage[product_id][PRICE_KEY] = normalized_price + if QUANTITY_KEY in fields: + storage[product_id][QUANTITY_KEY] = fields[QUANTITY_KEY] + return storage[product_id] def delete_product( @@ -154,4 +194,10 @@ def delete_product( are left unchanged and a message is printed). """ # TODO: реализуйте функцию - return "" + if product_id not in storage: + print(f"no product with id {product_id}") + return None + name = storage[product_id][NAME_KEY] + del storage[product_id] + product_names.remove(name) + return product_id diff --git a/src/part4/run_admin.py b/src/part4/run_admin.py index 2247b29b..cdc16529 100644 --- a/src/part4/run_admin.py +++ b/src/part4/run_admin.py @@ -28,22 +28,36 @@ is left untouched and the loop keeps running; only ``exit`` stops it. """ -from decimal import Decimal, InvalidOperation # noqa: F401 +from decimal import Decimal, InvalidOperation from typing import Final -from .crud import ( # noqa: F401 +from .crud import ( create_product, delete_product, read_product, update_product, ) -from .storage import NAME_KEY, PRICE_KEY, QUANTITY_KEY, Product # noqa: F401 -from .utils import get_storage_str_representation # noqa: F401 +from .storage import NAME_KEY, PRICE_KEY, QUANTITY_KEY, Product +from .utils import get_storage_str_representation # TODO: задайте приглашение и текст справки (используйте своё решение части 3) -PROMPT: Final[str] = "" -HELP_TEXT: Final[str] = "" + + +PROMPT: Final[str] = "admin> " + +HELP_TEXT: Final[str] = """Available commands: + help show this message + exit leave the console + show print the whole store as a table + create add a product, print its new id + read print the product with that id + update overwrite that product's fields + delete remove the product with that id + +For create and update the price and quantity are the last two words of the +line; everything before them is the product name, so it may contain spaces +(for example "Gibson SG Junior") and needs no quoting.""" def show_help() -> None: @@ -53,7 +67,7 @@ def show_help() -> None: is: a heading, one line per command with a short description, and a note on how multi-word names are parsed. """ - # TODO: реализуйте функцию (используйте своё решение части 3) + print(HELP_TEXT) def print_result(result: object) -> None: @@ -64,7 +78,8 @@ def print_result(result: object) -> None: operation already reported its own failure, so nothing is printed in that case. """ - # TODO: реализуйте функцию (используйте своё решение части 3) + if result is not None: + print(result) def run_command( @@ -92,7 +107,40 @@ def run_command( ``update`` does not parse as a decimal number. """ # TODO: реализуйте функцию - return False + match line.split(): + case ["read", product_id]: + print_result(read_product(storage=storage, product_id=product_id)) + case ["delete", product_id]: + print_result( + delete_product( + storage=storage, product_names=product_names, product_id=product_id + ) + ) + case ["create", *name, price, quantity] if name: + product_name = " ".join(name) + fields = { + NAME_KEY: product_name, + PRICE_KEY: Decimal(price), + QUANTITY_KEY: int(quantity), + } + print_result(create_product(storage, product_names, fields)) + case ["update", product_id, *name, price, quantity] if name: + product_name = " ".join(name) + fields = { + NAME_KEY: product_name, + PRICE_KEY: Decimal(price), + QUANTITY_KEY: int(quantity), + } + print_result(update_product(storage, product_names, product_id, fields)) + case ["show"]: + print(get_storage_str_representation(storage)) + case ["exit"]: + return False + case ["help"]: + show_help() + case _: + print(f"'{line}' is not a command") + return True def main() -> None: diff --git a/src/part4/storage.py b/src/part4/storage.py index 9acedf07..a25f6253 100644 --- a/src/part4/storage.py +++ b/src/part4/storage.py @@ -40,6 +40,6 @@ # Name of each field inside a Product record. # TODO: задайте имена полей записи -NAME_KEY: Final = "" -PRICE_KEY: Final = "" -QUANTITY_KEY: Final = "" +NAME_KEY: Final = "name" +PRICE_KEY: Final = "price" +QUANTITY_KEY: Final = "quantity" diff --git a/src/part4/utils.py b/src/part4/utils.py index d2a940ea..4c0f4a35 100644 --- a/src/part4/utils.py +++ b/src/part4/utils.py @@ -18,10 +18,10 @@ the longest value it holds in that particular call. """ -from decimal import ROUND_HALF_UP, Decimal # noqa: F401 +from decimal import ROUND_HALF_UP, Decimal from typing import Final -from .storage import ( # noqa: F401 +from .storage import ( NAME_KEY, PRICE_KEY, QUANTITY_KEY, @@ -33,14 +33,14 @@ # Quantisation step derived from PRICE_PRECISION, e.g. Decimal("0.01"). # TODO: задайте число знаков после запятой и шаг квантования (используйте своё # решение части 3) -PRICE_PRECISION: Final[int] = 0 -PRICE_STEP: Final = Decimal(0) +PRICE_PRECISION: Final[int] = 2 +PRICE_STEP: Final[Decimal] = Decimal(1).scaleb(-PRICE_PRECISION) # Column headers of the table produced by get_storage_str_representation, # left to right. The width of each column is not fixed here -- it is # measured per call from the data (see the function). # TODO: задайте заголовки столбцов таблицы (используйте своё решение части 3) -TABLE_HEADERS: Final[tuple[str, ...]] = () +TABLE_HEADERS: Final[tuple[str, ...]] = ("ID", "name", "price", "quantity") def normalize_price(price: Decimal) -> Decimal: @@ -53,8 +53,7 @@ def normalize_price(price: Decimal) -> Decimal: ``price`` quantised to :data:`PRICE_PRECISION` fractional digits, with halves rounded up. """ - # TODO: реализуйте функцию (используйте своё решение части 3) - return Decimal(0) + return price.quantize(PRICE_STEP, rounding=ROUND_HALF_UP) def normalize_product_name(name: str) -> str: @@ -71,8 +70,10 @@ def normalize_product_name(name: str) -> str: The result is an empty string when ``name`` holds nothing but whitespace. """ - # TODO: реализуйте функцию (используйте своё решение части 3) - return "" + list_of_words = name.split() + name = " ".join(list_of_words) + name = name.lower() + return name # TODO: при необходимости добавьте свои вспомогательные функции @@ -97,4 +98,23 @@ def get_storage_str_representation(storage: dict[str, Product]) -> str: returned, sized to the header labels. """ # TODO: реализуйте функцию (используйте своё решение части 3) - return "" + mxlen1 = len(TABLE_HEADERS[0]) + mxlen2 = len(TABLE_HEADERS[1]) + mxlen3 = len(TABLE_HEADERS[2]) + mxlen4 = len(TABLE_HEADERS[3]) + for item_id, product in storage.items(): + name = str(product[NAME_KEY]) + price = str(product[PRICE_KEY]) + quantity = str(product[QUANTITY_KEY]) + mxlen1 = max(mxlen1, len(item_id)) + mxlen2 = max(mxlen2, len(name)) + mxlen3 = max(mxlen3, len(price)) + mxlen4 = max(mxlen4, len(quantity)) + s1 = f"| {TABLE_HEADERS[0].ljust(mxlen1)} | {TABLE_HEADERS[1].ljust(mxlen2)} | {TABLE_HEADERS[2].ljust(mxlen3)} | {TABLE_HEADERS[3].ljust(mxlen4)} |" + s2 = f"|{'-' * (mxlen1 + 2)}|{'-' * (mxlen2 + 2)}|{'-' * (mxlen3 + 2)}|{'-' * (mxlen4 + 2)}|" + snext = [ + f"| {str(item_id).ljust(mxlen1)} | {str(product[NAME_KEY]).ljust(mxlen2)} | {str(product[PRICE_KEY]).ljust(mxlen3)} | {str(product[QUANTITY_KEY]).ljust(mxlen4)} |" + for item_id, product in storage.items() + ] + rows = [s1, s2, *snext] + return "\n".join(rows)