From f5bf983732c2c1ac767a1bd241a6eada2e1098ae Mon Sep 17 00:00:00 2001 From: Mihail Date: Fri, 2 Oct 2026 23:42:12 +0300 Subject: [PATCH 01/18] Solve task 2 --- src/part2/cart.py | 119 +++++++++++++++++++++++++------------------ src/part2/crud.py | 106 ++++++++++++-------------------------- src/part2/storage.py | 8 +-- src/part2/utils.py | 18 ++----- 4 files changed, 109 insertions(+), 142 deletions(-) diff --git a/src/part2/cart.py b/src/part2/cart.py index 063f9aea..3851d83a 100644 --- a/src/part2/cart.py +++ b/src/part2/cart.py @@ -11,8 +11,8 @@ from typing import Final -from .crud import read_product, update_product # noqa: F401 -from .storage import ( # noqa: F401 +from .crud import read_product, update_product +from .storage import ( NAME_INDEX, PRICE_INDEX, QUANTITY_INDEX, @@ -22,9 +22,16 @@ type CartLine = tuple[int, int] -# TODO: задайте позиции полей внутри кортежа CartLine + LINE_PRODUCT_ID_INDEX: Final = 0 -LINE_QUANTITY_INDEX: Final = 0 +LINE_QUANTITY_INDEX: Final = 1 + + +def find_cart_line(cart: list[CartLine], product_id: int) -> CartLine | None: + for line in cart: + if line[LINE_PRODUCT_ID_INDEX] == product_id: + return line + return None def add_to_cart( @@ -33,28 +40,33 @@ def add_to_cart( product_id: int, quantity: int, ) -> CartLine | None: - """Move ``quantity`` units of ``product_id`` from the store into ``cart``. - - The product is looked up in ``storage``. If it is missing, a message is - printed (by :func:`~src.part2.crud.read_product`) and nothing - changes. If the store holds fewer units than requested, an explanatory - message is printed and nothing changes. Otherwise the store record is - decremented by ``quantity`` and the cart line for ``product_id`` gains - ``quantity`` units -- a new line is created when the cart had none. - - Args: - storage: The product store to draw stock from; modified in place on - success. - cart: The cart to add the units to; modified in place on success. - product_id: The identifier of the product to add. - quantity: Number of units to move into the cart. - - Returns: - The cart line for ``product_id`` after the addition, or ``None`` - when the product is unknown or the store cannot cover the request. - """ - # TODO: реализуйте функцию - return (0, 0) + product = read_product(storage, product_id) + if product is None: + return None + + in_stock = product[QUANTITY_INDEX] + if in_stock < quantity: + print( + f"not enough stock for product {product_id}: " + f"{in_stock} available, {quantity} requested" + ) + return None + + update_product( + storage, + product_id, + (product[NAME_INDEX], product[PRICE_INDEX], in_stock - quantity), + ) + + line = find_cart_line(cart, product_id) + if line is None: + new_line = (product_id, quantity) + cart.append(new_line) + return new_line + + new_line = (product_id, line[LINE_QUANTITY_INDEX] + quantity) + cart[cart.index(line)] = new_line + return new_line def remove_from_cart( @@ -63,27 +75,34 @@ def remove_from_cart( product_id: int, quantity: int, ) -> CartLine | None: - """Move ``quantity`` units of ``product_id`` from ``cart`` back to the store. - - The cart line for ``product_id`` is looked up. If there is none, a - message is printed and nothing changes. If the line holds fewer units - than requested, an explanatory message is printed and nothing changes. - Otherwise the cart line loses ``quantity`` units -- the line is dropped - when it reaches zero -- and the store record gains ``quantity`` units - back. - - Args: - storage: The product store to return stock to; modified in place on - success. - cart: The cart to take the units from; modified in place on - success. - product_id: The identifier of the product to remove. - quantity: Number of units to move back into the store. - - Returns: - The cart line for ``product_id`` after the removal (a quantity of - zero means the line was dropped), or ``None`` when the cart has no - line for the product or holds too few units. - """ - # TODO: реализуйте функцию - return (0, 0) + line = find_cart_line(cart, product_id) + if line is None: + print(f"product {product_id} is not in the cart") + return None + + in_cart = line[LINE_QUANTITY_INDEX] + if in_cart < quantity: + print( + f"cart holds only {in_cart} unit(s) of product {product_id}, " + f"cannot remove {quantity}" + ) + return None + + product = read_product(storage, product_id) + if product is None: + return None + + update_product( + storage, + product_id, + (product[NAME_INDEX], product[PRICE_INDEX], product[QUANTITY_INDEX] + quantity), + ) + + remaining = in_cart - quantity + if remaining == 0: + cart.remove(line) + return (product_id, 0) + + new_line = (product_id, remaining) + cart[cart.index(line)] = new_line + return new_line diff --git a/src/part2/crud.py b/src/part2/crud.py index d026cbcb..6c0ad070 100644 --- a/src/part2/crud.py +++ b/src/part2/crud.py @@ -14,66 +14,42 @@ from decimal import Decimal -from .storage import ( # noqa: F401 +from .storage import ( NAME_INDEX, PRODUCT_ID_INDEX, PRODUCT_ID_MIN, Product, ) -from .utils import normalize_price # noqa: F401 +from .utils import normalize_price def generate_product_id(storage: list[Product]) -> int: - """Choose the identifier for the next product added to ``storage``. - - Args: - storage: The product store to inspect. - - Returns: - One past the greatest identifier currently held in ``storage``, or - :data:`~src.part2.storage.PRODUCT_ID_MIN` when ``storage`` is - empty. - """ - # TODO: реализуйте функцию - return 0 + if not storage: + return PRODUCT_ID_MIN + return max(p[PRODUCT_ID_INDEX] for p in storage) + 1 def create_product( storage: list[Product], fields: tuple[str, Decimal, int] ) -> int | None: - """Append a new product to ``storage`` and return its new identifier. + name, price, quantity = fields - Args: - storage: The product store to append to; modified in place on - success. - fields: A ``(name, price, quantity)`` tuple describing the product. - ``price`` is a :class:`~decimal.Decimal` amount and is rounded - to the stored money precision before it is saved. + for product in storage: + if product[NAME_INDEX] == name: + print(f"product name '{name}' is already taken") + return None - Returns: - The identifier generated for the new product, or ``None`` when a - product with the same name already exists. In the ``None`` case - ``storage`` is left unchanged and a message naming the clashing - name is printed. - """ - # TODO: реализуйте функцию - return 0 + product_id = generate_product_id(storage) + storage.append((product_id, name, normalize_price(price), quantity)) + return product_id def read_product(storage: list[Product], product_id: int) -> Product | None: - """Return the product stored under ``product_id``. - - Args: - storage: The product store to search. - product_id: The identifier to look up. - - Returns: - The matching ``(product_id, name, price, quantity)`` record, or - ``None`` when no product carries that identifier (a message is - printed in that case). - """ - # TODO: реализуйте функцию - return (0, "", Decimal(0), 0) + for product in storage: + if product[PRODUCT_ID_INDEX] == product_id: + return product + print(f"no product with id {product_id}") + return None def update_product( @@ -81,40 +57,22 @@ def update_product( product_id: int, fields: tuple[str, Decimal, int], ) -> Product | None: - """Overwrite the fields of the product stored under ``product_id``. - - The identifier itself is preserved; only ``name``, ``price`` and - ``quantity`` are replaced. + name, price, quantity = fields - Args: - storage: The product store to modify; the matching record is - replaced in place on success. - product_id: The identifier of the product to change. - fields: A ``(name, price, quantity)`` tuple with the new values. - ``price`` is a :class:`~decimal.Decimal` amount and is rounded - to the stored money precision before it is saved. + for i, product in enumerate(storage): + if product[PRODUCT_ID_INDEX] == product_id: + updated = (product_id, name, normalize_price(price), quantity) + storage[i] = updated + return updated - Returns: - The updated ``(product_id, name, price, quantity)`` record, or - ``None`` when no product carries that identifier (``storage`` is - left unchanged and a message is printed). - """ - # TODO: реализуйте функцию - return (0, "", Decimal(0), 0) + print(f"no product with id {product_id}") + return None def delete_product(storage: list[Product], product_id: int) -> int | None: - """Remove the product stored under ``product_id`` from ``storage``. - - Args: - storage: The product store to remove from; modified in place on - success. - product_id: The identifier of the product to remove. - - Returns: - ``product_id`` when a product was removed, or ``None`` when no - product carried that identifier (``storage`` is left unchanged and - a message is printed). - """ - # TODO: реализуйте функцию - return 0 + for i, product in enumerate(storage): + if product[PRODUCT_ID_INDEX] == product_id: + del storage[i] + return product_id + print(f"no product with id {product_id}") + return None diff --git a/src/part2/storage.py b/src/part2/storage.py index b63d5fc7..35821c5f 100644 --- a/src/part2/storage.py +++ b/src/part2/storage.py @@ -26,9 +26,9 @@ # TODO: задайте позиции полей внутри кортежа Product PRODUCT_ID_INDEX: Final = 0 -NAME_INDEX: Final = 0 -PRICE_INDEX: Final = 0 -QUANTITY_INDEX: Final = 0 +NAME_INDEX: Final = 1 +PRICE_INDEX: Final = 2 +QUANTITY_INDEX: Final = 3 # TODO: задайте идентификатор первого товара в пустом хранилище -PRODUCT_ID_MIN: Final[int] = 0 +PRODUCT_ID_MIN: Final[int] = 1 diff --git a/src/part2/utils.py b/src/part2/utils.py index ec0348d3..2af03b11 100644 --- a/src/part2/utils.py +++ b/src/part2/utils.py @@ -6,24 +6,14 @@ currency values free of binary floating-point error. """ -from decimal import ROUND_HALF_UP, Decimal # noqa: F401 +from decimal import ROUND_HALF_UP, Decimal from typing import Final # TODO: задайте число знаков после запятой и шаг квантования -PRICE_PRECISION: Final[int] = 0 -PRICE_STEP: Final = Decimal(0) +PRICE_PRECISION: Final[int] = 2 +PRICE_STEP: Final = Decimal(1).scaleb(-PRICE_PRECISION) def normalize_price(price: Decimal) -> Decimal: - """Round a price to the precision every stored record uses. - - Args: - price: The raw price amount. - - Returns: - ``price`` quantised to :data:`PRICE_PRECISION` fractional digits, - with halves rounded up. - """ - # TODO: реализуйте функцию - return Decimal(0) + return price.quantize(PRICE_STEP, rounding=ROUND_HALF_UP) From 4baf4f601c878446bc5ef45f6a3f90a6fd14f01b Mon Sep 17 00:00:00 2001 From: Mihail Date: Mon, 12 Oct 2026 12:22:17 +0300 Subject: [PATCH 02/18] feat: add index in storage --- src/part3/storage.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/part3/storage.py b/src/part3/storage.py index c5e34be5..36e9af7f 100644 --- a/src/part3/storage.py +++ b/src/part3/storage.py @@ -27,9 +27,9 @@ # TODO: задайте позиции полей внутри кортежа Product (используйте своё решение # части 2) PRODUCT_ID_INDEX: Final = 0 -NAME_INDEX: Final = 0 -PRICE_INDEX: Final = 0 -QUANTITY_INDEX: Final = 0 +NAME_INDEX: Final = 1 +PRICE_INDEX: Final = 2 +QUANTITY_INDEX: Final = 3 # TODO: задайте идентификатор первого товара в пустом хранилище -PRODUCT_ID_MIN: Final[int] = 0 +PRODUCT_ID_MIN: Final[int] = 1 From c17ff77d64e3f7c79cf9202777922f51c0f34fe9 Mon Sep 17 00:00:00 2001 From: Mihail Date: Mon, 12 Oct 2026 12:42:01 +0300 Subject: [PATCH 03/18] feart: normalization names and constans in utils --- src/part3/utils.py | 38 ++++++++------------------------------ 1 file changed, 8 insertions(+), 30 deletions(-) diff --git a/src/part3/utils.py b/src/part3/utils.py index 318ded0b..bacab0b8 100644 --- a/src/part3/utils.py +++ b/src/part3/utils.py @@ -17,7 +17,7 @@ text table meant to be printed to a terminal; each column is sized to the longest value it holds in that particular call. """ - +import re from decimal import ROUND_HALF_UP, Decimal # noqa: F401 from typing import Final @@ -28,46 +28,24 @@ # Quantisation step derived from PRICE_PRECISION, e.g. Decimal("0.01"). # TODO: задайте число знаков после запятой и шаг квантования (используйте своё # решение части 2) -PRICE_PRECISION: Final[int] = 0 -PRICE_STEP: Final = Decimal(0) +PRICE_PRECISION: Final[int] = 2 +PRICE_STEP: Final = Decimal(1).scaleb(-PRICE_PRECISION) +_WHITESPACE_RUN: Final = re.complite(r"\s+") +_CELL_PADDING: Final = 2 # 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: задайте заголовки столбцов таблицы -TABLE_HEADERS: Final[tuple[str, ...]] = () +TABLE_HEADERS: Final[tuple[str, ...]] = ("id", "name", "price", "quantity") def normalize_price(price: Decimal) -> Decimal: - """Round a price to the precision every stored record uses. - - Args: - price: The raw price amount. - - Returns: - ``price`` quantised to :data:`PRICE_PRECISION` fractional digits, - with halves rounded up. - """ - # TODO: реализуйте функцию (используйте своё решение части 2) - return Decimal(0) + return price.quantize(PRICE_STEP, rounding=ROUND_HALF_UP) def normalize_product_name(name: str) -> str: - """Reduce a product name to the canonical form the store keeps. - - Args: - name: The raw product name. - - Returns: - ``name`` with every leading and trailing whitespace character - removed, every internal run of whitespace (spaces, tabs and the - like) collapsed to a single space, and the rest lower-cased. For - example ``"abc def\tghi\tjkl"`` becomes ``"abc def ghi jkl"``. - The result is an empty string when ``name`` holds nothing but - whitespace. - """ - # TODO: реализуйте функцию - return "" + return _WHITESPACE_RUN.sub(" ", name.strip()).lower() # TODO: при необходимости добавьте свои вспомогательные функции From fa0b5f87c4e0ff3d571c63a7db29121a16e9c67b Mon Sep 17 00:00:00 2001 From: Mihail Date: Mon, 12 Oct 2026 12:57:32 +0300 Subject: [PATCH 04/18] feat: add get_storage_str_representtion --- src/part3/utils.py | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/src/part3/utils.py b/src/part3/utils.py index bacab0b8..b5734cd0 100644 --- a/src/part3/utils.py +++ b/src/part3/utils.py @@ -48,26 +48,27 @@ def normalize_product_name(name: str) -> str: return _WHITESPACE_RUN.sub(" ", name.strip()).lower() -# TODO: при необходимости добавьте свои вспомогательные функции +def _measure_widths(rows: list[tuple[str, ...]]) -> list[int]: + return[ + max(len(header), *(len(row[i]) for row in rows)) + for i, header in enumerate(TABLE_HEADERS) + ] + + +def _render_row(cells: tuple[str, ...], widths: list[int]) -> str: + padded = (cell.ljust(widths) for cell, width in zip(cells, widths)) + return "| " + " | ".join(padded) + " |" + + +def _render_separator(widths: list[int]) -> str: + runs = ("-" * (widths + _CELL_PADDING) for width in widths) + return "|" + "|".join(runs) + "|" def get_storage_str_representation(storage: list[Product]) -> str: - """Render the product store as a text table with data-sized columns. - - The table has the columns named by :data:`TABLE_HEADERS` -- id, name, - price and quantity. Each column is made exactly as wide as the longest - value it carries in this call (its header counted), so the columns - line up when the string is printed to a terminal and no value is ever - truncated. - - Args: - storage: The product store to render. - - Returns: - A multi-line string: a header row, a dashed separator row, then - one row per product in ``storage`` in list order. When ``storage`` - is empty only the header and separator rows are returned, sized to - the header labels. - """ - # TODO: реализуйте функцию - return "" + rows = [tuple(str(value) for value in product) for product in storage] + widths = _measure_widths(rows) + + lines = [_render_row(TABLE_HEADERS, widths), _render_separator(widths)] + lines.extend(_render_row(row, widths) for row in rows) + return "\n".join(lines) From fb1dbc063e7d2824ac94145d6c91d1aa122881d0 Mon Sep 17 00:00:00 2001 From: Mihail Date: Mon, 12 Oct 2026 13:02:27 +0300 Subject: [PATCH 05/18] feat: add generate id and read product from part 2 --- src/part3/crud.py | 37 +++++++++++-------------------------- 1 file changed, 11 insertions(+), 26 deletions(-) diff --git a/src/part3/crud.py b/src/part3/crud.py index 68811d7e..99627b52 100644 --- a/src/part3/crud.py +++ b/src/part3/crud.py @@ -30,18 +30,10 @@ def generate_product_id(storage: list[Product]) -> int: - """Choose the identifier for the next product added to ``storage``. - - Args: - storage: The product store to inspect. - - Returns: - One past the greatest identifier currently held in ``storage``, or - :data:`~src.part3.storage.PRODUCT_ID_MIN` when ``storage`` is - empty. - """ - # TODO: реализуйте функцию (используйте своё решение части 2) - return 0 + if not storage: + return PRODUCT_ID_MIN + return max(p[PRODUCT_ID_INDEX] for p in storage) + 1 + def create_product( @@ -69,20 +61,13 @@ def create_product( def read_product(storage: list[Product], product_id: int) -> Product | None: - """Return the product stored under ``product_id``. - - Args: - storage: The product store to search. - product_id: The identifier to look up. - - Returns: - The matching ``(product_id, name, price, quantity)`` record, or - ``None`` when no product carries that identifier (a message is - printed in that case). - """ - # TODO: реализуйте функцию (используйте своё решение части 2) - return (0, "", Decimal(0), 0) - + def read_product(storage: list[Product], product_id: int) -> Product | None: + for product in storage: + if product[PRODUCT_ID_INDEX] == product_id: + return product + print(f"no product with id {product_id}") + return None + def update_product( storage: list[Product], From 1259085229bc0373b77f84262aa9a7925249833e Mon Sep 17 00:00:00 2001 From: Mihail Date: Mon, 12 Oct 2026 13:13:38 +0300 Subject: [PATCH 06/18] feat: add find index and name index --- src/part3/crud.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/part3/crud.py b/src/part3/crud.py index 99627b52..506541e0 100644 --- a/src/part3/crud.py +++ b/src/part3/crud.py @@ -67,6 +67,25 @@ def read_product(storage: list[Product], product_id: int) -> Product | None: return product print(f"no product with id {product_id}") return None + + +def _find_index(storage: list[Product], product_id: int) -> int | None: + return next( + i + for i, product in enumerate(storage) + if product[PRODUCT_ID_INDEX] == product_id + ), + None + + +def _name_taken( + storage: list[Product], name: str, except_id: int | None = None +) -> bool: + return any( + product[NAME_INDEX] == name + and product[PRODUCT_ID_INDEX] != except_id + for product in storage + ) def update_product( From 55cabe5a092103f1caca2a5b850f4fb2558c65b2 Mon Sep 17 00:00:00 2001 From: Mihail Date: Mon, 12 Oct 2026 18:48:21 +0300 Subject: [PATCH 07/18] feat: add create_productt --- src/part3/crud.py | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/src/part3/crud.py b/src/part3/crud.py index 506541e0..588a0c5c 100644 --- a/src/part3/crud.py +++ b/src/part3/crud.py @@ -39,25 +39,16 @@ def generate_product_id(storage: list[Product]) -> int: def create_product( storage: list[Product], fields: tuple[str, Decimal, int] ) -> int | None: - """Append a new product to ``storage`` and return its new identifier. - - Args: - storage: The product store to append to; modified in place on - success. - fields: A ``(name, price, quantity)`` tuple describing the product. - ``name`` is normalised (surrounding whitespace stripped, - lower-cased) before it is stored or compared. ``price`` is a - :class:`~decimal.Decimal` amount and is rounded to the stored - money precision before it is saved. - - Returns: - The identifier generated for the new product, or ``None`` when - ``name`` is blank once normalised or a product with the same - normalised name already exists. In the ``None`` case ``storage`` - is left unchanged and an explanatory message is printed. - """ - # TODO: реализуйте функцию - return 0 + name = normalize_product_name(fields[0]) + if not name: + print("product name must not be blank") + return None + if _name_taken(storage, name): + print(f"product name '{name}' is already taken") + return None + + product_id = generate_product_id(storage) + storage.append((product_id, name, normalize_price(fields[1]), fields[2])) def read_product(storage: list[Product], product_id: int) -> Product | None: From 7ea4b3ad4522d4812663ee15331dfdfa2eb19f9e Mon Sep 17 00:00:00 2001 From: Mihail Date: Mon, 12 Oct 2026 18:55:08 +0300 Subject: [PATCH 08/18] feat: add update_product --- src/part3/crud.py | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/src/part3/crud.py b/src/part3/crud.py index 588a0c5c..8fbf7095 100644 --- a/src/part3/crud.py +++ b/src/part3/crud.py @@ -84,29 +84,23 @@ def update_product( product_id: int, fields: tuple[str, Decimal, int], ) -> Product | None: - """Overwrite the fields of the product stored under ``product_id``. + name = normalize_product_name(fields[0]) + if not name: + print("product name must not be blank") + return None - The identifier itself is preserved; only ``name``, ``price`` and - ``quantity`` are replaced. + index = _find_index(storage, product_id) + if index is None: + print(f"no product with id {product_id}") + return None - Args: - storage: The product store to modify; the matching record is - replaced in place on success. - product_id: The identifier of the product to change. - fields: A ``(name, price, quantity)`` tuple with the new values. - ``price`` is a :class:`~decimal.Decimal` amount and is rounded - to the stored money precision before it is saved. + if _name_taken(storage, name, except_id=product_id): + print(f"product name '{name}' is already taken") + return None - Returns: - The updated ``(product_id, name, price, quantity)`` record, or - ``None`` when ``name`` is blank once normalised, no product - carries that identifier, or the normalised name is already taken - by another product. In every ``None`` case ``storage`` is left - unchanged and an explanatory message is printed. Renaming a - product to its own current name is allowed. - """ - # TODO: реализуйте функцию - return (0, "", Decimal(0), 0) + updated = (product_id, name, normalize_price(fields[1]), fields[2]) + storage[index] = updated + return updated def delete_product(storage: list[Product], product_id: int) -> int | None: From a04f2f2121f6e0256d47410ec653204fb6ec8749 Mon Sep 17 00:00:00 2001 From: Mihail Date: Mon, 12 Oct 2026 19:04:39 +0300 Subject: [PATCH 09/18] feat: add delete_product --- src/part3/crud.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/part3/crud.py b/src/part3/crud.py index 8fbf7095..3260610a 100644 --- a/src/part3/crud.py +++ b/src/part3/crud.py @@ -104,17 +104,9 @@ def update_product( def delete_product(storage: list[Product], product_id: int) -> int | None: - """Remove the product stored under ``product_id`` from ``storage``. - - Args: - storage: The product store to remove from; modified in place on - success. - product_id: The identifier of the product to remove. - - Returns: - ``product_id`` when a product was removed, or ``None`` when no - product carried that identifier (``storage`` is left unchanged and - a message is printed). - """ - # TODO: реализуйте функцию (используйте своё решение части 2) - return 0 + index = _find_index(storage, product_id) + if index is None: + print(f"no product with id {product_id}") + return None + del storage[index] + return product_id \ No newline at end of file From 53e32d4ee12062285055b2914249564bf340f50b Mon Sep 17 00:00:00 2001 From: Mihail Date: Mon, 12 Oct 2026 19:13:19 +0300 Subject: [PATCH 10/18] feat: add promp, help_text --- src/part3/run_admin.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/part3/run_admin.py b/src/part3/run_admin.py index c5feeb0b..718c1a64 100644 --- a/src/part3/run_admin.py +++ b/src/part3/run_admin.py @@ -42,9 +42,21 @@ from .utils import get_storage_str_representation # noqa: F401 -# TODO: задайте приглашение и текст справки -PROMPT: Final[str] = "" -HELP_TEXT: Final[str] = "" +PROMPT: Final[str] = "admin>" +HELP_TEXT: Final[str] =""""Available commands: +* help -- print the list of commands and what each one does; +* exit -- leave the loop and end the program; +* show -- print the whole store as a text table; +* create -- add a product; +* read -- print one product; +* update -- overwrite a product; +* delete -- remove a product. + +For create and update the price and quantity are always the last +two words of the line; everything between the verb (and, for update, +the id) and those two words is joined with single spaces into the +product name, so a multi-word name such as Gibson SG Junior needs no +quoting.""" def show_help() -> None: From 11593511e583ae5e9c95069a9c1b939adcd66b2c Mon Sep 17 00:00:00 2001 From: Mihail Date: Mon, 12 Oct 2026 19:14:48 +0300 Subject: [PATCH 11/18] feat: add show_help and print_result --- src/part3/run_admin.py | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/src/part3/run_admin.py b/src/part3/run_admin.py index 718c1a64..7d82b5a7 100644 --- a/src/part3/run_admin.py +++ b/src/part3/run_admin.py @@ -60,24 +60,11 @@ def show_help() -> None: - """Print the command reference to stdout. - - The text is the module-level :data:`HELP_TEXT` constant, printed as - is: a heading, one line per command with a short description, and a - note on how multi-word names are parsed. - """ - # TODO: реализуйте функцию - + print(HELP_TEXT) def print_result(result: object) -> None: - """Print a CRUD result to stdout unless it is ``None``. - - Args: - result: The value returned by a CRUD operation. ``None`` means the - operation already reported its own failure, so nothing is - printed in that case. - """ - # TODO: реализуйте функцию + if result is not None: + print(result) def run_command(storage: list[Product], line: str) -> bool: From 5f431bee12455f09e0bc6eac106a43615d4f1946 Mon Sep 17 00:00:00 2001 From: Mihail Date: Mon, 12 Oct 2026 19:21:04 +0300 Subject: [PATCH 12/18] feat: add _parse_fields --- src/part3/run_admin.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/part3/run_admin.py b/src/part3/run_admin.py index 7d82b5a7..92ede15d 100644 --- a/src/part3/run_admin.py +++ b/src/part3/run_admin.py @@ -67,6 +67,13 @@ def print_result(result: object) -> None: print(result) +def _parse_fields(args: list[str]) -> tuple[str, Decimal, int]: + name = " ".join(args[:-2]) + price = Decimal(args[-2]) + quantity = int(args[-1]) + return name, price, quantity + + def run_command(storage: list[Product], line: str) -> bool: """Parse one console line and carry out the command it names. From cd6782331362937a516fb4bba502be8cde1786fb Mon Sep 17 00:00:00 2001 From: Mihail Date: Mon, 12 Oct 2026 19:48:14 +0300 Subject: [PATCH 13/18] feat: add command handlers and table _commands --- src/part3/run_admin.py | 47 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/part3/run_admin.py b/src/part3/run_admin.py index 92ede15d..67e8eb61 100644 --- a/src/part3/run_admin.py +++ b/src/part3/run_admin.py @@ -58,6 +58,9 @@ product name, so a multi-word name such as Gibson SG Junior needs no quoting.""" +_NAME_AND_TWO_NUMBERS: Final = 3 +_ID_AND_NAME_AND_TWO_NUNBERS: Final = 4 + def show_help() -> None: print(HELP_TEXT) @@ -74,6 +77,50 @@ def _parse_fields(args: list[str]) -> tuple[str, Decimal, int]: return name, price, quantity +def _run_read(storage: list[Product], args: list[str]) -> None: + print_result(read_product(storage, int(args[0]))) + + +def _run_delete(storage: list[Product], args: list[str]) -> None: + print_result(delete_product(storage, int(args[0]))) + + +def _run_create(storage: list[Product], args: list[str]) -> None: + print_result(create_product(storage, _parse_fields(args))) + + +def _run_update(storage: list[Product], args: list[str]) -> None: + product_id = int(args[0]) + fields = _parse_fields(args[1:]) + print_result(update_product(storage, product_id, fields)) + + +def _run_show(storage: list[Product], args: list[str]) -> None: + print(get_storage_str_representation(storage)) + + +def _run_help(storage: list[Product], args: list[str]) -> None: + show_help() + + +def _run_exit(storage: list[Product], args: list[str]) -> None: + return False + + +type _Handler = callable[[list[Product], list[str]], bool | None] + + +_COMMANDS: Final[dict[str, tuple[_Handler, int | None]]] = { + "help": (_run_help, 0), + "exit": (_run_exit, 0), + "show": (_run_show, 0), + "read": (_run_read, 1), + "delete": (_run_delete, 1), + "create": (_run_create, _NAME_AND_TWO_NUMBERS), + "update": (_run_update, _ID_AND_NAME_AND_TWO_NUNBERS), +} + + def run_command(storage: list[Product], line: str) -> bool: """Parse one console line and carry out the command it names. From 62921098d95b0ebd70ab85b0cb27a69765e5db3f Mon Sep 17 00:00:00 2001 From: Mihail Date: Mon, 12 Oct 2026 20:10:53 +0300 Subject: [PATCH 14/18] feat: add run_command --- src/part3/run_admin.py | 35 ++++++++++++++--------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/src/part3/run_admin.py b/src/part3/run_admin.py index 67e8eb61..504641d4 100644 --- a/src/part3/run_admin.py +++ b/src/part3/run_admin.py @@ -121,28 +121,21 @@ def _run_exit(storage: list[Product], args: list[str]) -> None: } +def _accepts(verb: str, args: list[str]) -> bool: + _, arity = _COMMANDS[verb] + return arity is None or len(args) == arity + + def run_command(storage: list[Product], line: str) -> bool: - """Parse one console line and carry out the command it names. - - Args: - storage: The product store shared across the session; mutated in - place by the ``create``, ``update`` and ``delete`` commands. - line: One line of console input, already stripped of surrounding - whitespace. - - Returns: - ``True`` to keep the read-eval-print loop running, or ``False`` - once the ``exit`` command has been seen. A line that is not a - known command prints a notice and still returns ``True``. - - Raises: - ValueError: If an id or quantity argument of a CRUD command does - not parse as a base-10 integer. - decimal.InvalidOperation: If the price argument of ``create`` or - ``update`` does not parse as a decimal number. - """ - # TODO: реализуйте функцию - return False + words = line.split() + verb = words[0] if words else "" + + if verb not in _COMMANDS or not _accepts(verb, words[1:]): + print(f"'{line}' is not a command") + return True + + handler, _ = _COMMANDS[verb] + return handler(storage, words[1:]) is not False def main() -> None: From 95a941067feac3813c4b5e6ac72ee067bbc45106 Mon Sep 17 00:00:00 2001 From: Mihail Date: Mon, 12 Oct 2026 20:21:42 +0300 Subject: [PATCH 15/18] fix: del #noqa --- src/part3/crud.py | 4 ++-- src/part3/run_admin.py | 6 +++--- src/part3/utils.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/part3/crud.py b/src/part3/crud.py index 3260610a..e74f5211 100644 --- a/src/part3/crud.py +++ b/src/part3/crud.py @@ -20,13 +20,13 @@ from decimal import Decimal -from .storage import ( # noqa: F401 +from .storage import ( NAME_INDEX, PRODUCT_ID_INDEX, PRODUCT_ID_MIN, Product, ) -from .utils import normalize_price, normalize_product_name # noqa: F401 +from .utils import normalize_price, normalize_product_name def generate_product_id(storage: list[Product]) -> int: diff --git a/src/part3/run_admin.py b/src/part3/run_admin.py index 504641d4..b0e2b8cf 100644 --- a/src/part3/run_admin.py +++ b/src/part3/run_admin.py @@ -29,17 +29,17 @@ 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 Product -from .utils import get_storage_str_representation # noqa: F401 +from .utils import get_storage_str_representation PROMPT: Final[str] = "admin>" diff --git a/src/part3/utils.py b/src/part3/utils.py index b5734cd0..3bf29971 100644 --- a/src/part3/utils.py +++ b/src/part3/utils.py @@ -18,7 +18,7 @@ the longest value it holds in that particular call. """ import re -from decimal import ROUND_HALF_UP, Decimal # noqa: F401 +from decimal import ROUND_HALF_UP, Decimal from typing import Final from .storage import Product From 2ce02a014218cb7a4fae54521afb4b3cf0bc9049 Mon Sep 17 00:00:00 2001 From: Mihail Date: Mon, 12 Oct 2026 20:24:29 +0300 Subject: [PATCH 16/18] fix: _find_index --- src/part3/crud.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/part3/crud.py b/src/part3/crud.py index e74f5211..ef4a41bf 100644 --- a/src/part3/crud.py +++ b/src/part3/crud.py @@ -62,11 +62,13 @@ def read_product(storage: list[Product], product_id: int) -> Product | None: def _find_index(storage: list[Product], product_id: int) -> int | None: return next( - i - for i, product in enumerate(storage) - if product[PRODUCT_ID_INDEX] == product_id - ), - None + ( + i + for i, product in enumerate(storage) + if product[PRODUCT_ID_INDEX] == product_id + ), + None, + ) def _name_taken( From 5aab043c5e55f82576c67681bfc727fe1d526967 Mon Sep 17 00:00:00 2001 From: Mihail Date: Mon, 12 Oct 2026 20:27:24 +0300 Subject: [PATCH 17/18] style: ruff format --- src/part3/crud.py | 68 ++++++++++++++++++++---------------------- src/part3/run_admin.py | 5 ++-- src/part3/utils.py | 13 ++++---- 3 files changed, 43 insertions(+), 43 deletions(-) diff --git a/src/part3/crud.py b/src/part3/crud.py index ef4a41bf..ef97d63b 100644 --- a/src/part3/crud.py +++ b/src/part3/crud.py @@ -31,33 +31,32 @@ def generate_product_id(storage: list[Product]) -> int: if not storage: - return PRODUCT_ID_MIN + return PRODUCT_ID_MIN return max(p[PRODUCT_ID_INDEX] for p in storage) + 1 - def create_product( storage: list[Product], fields: tuple[str, Decimal, int] ) -> int | None: - name = normalize_product_name(fields[0]) - if not name: - print("product name must not be blank") - return None - if _name_taken(storage, name): - print(f"product name '{name}' is already taken") - return None + name = normalize_product_name(fields[0]) + if not name: + print("product name must not be blank") + return None + if _name_taken(storage, name): + print(f"product name '{name}' is already taken") + return None - product_id = generate_product_id(storage) - storage.append((product_id, name, normalize_price(fields[1]), fields[2])) + product_id = generate_product_id(storage) + storage.append((product_id, name, normalize_price(fields[1]), fields[2])) def read_product(storage: list[Product], product_id: int) -> Product | None: - def read_product(storage: list[Product], product_id: int) -> Product | None: - for product in storage: - if product[PRODUCT_ID_INDEX] == product_id: - return product - print(f"no product with id {product_id}") - return None + def read_product(storage: list[Product], product_id: int) -> Product | None: + for product in storage: + if product[PRODUCT_ID_INDEX] == product_id: + return product + print(f"no product with id {product_id}") + return None def _find_index(storage: list[Product], product_id: int) -> int | None: @@ -75,34 +74,33 @@ def _name_taken( storage: list[Product], name: str, except_id: int | None = None ) -> bool: return any( - product[NAME_INDEX] == name - and product[PRODUCT_ID_INDEX] != except_id + product[NAME_INDEX] == name and product[PRODUCT_ID_INDEX] != except_id for product in storage ) - + def update_product( storage: list[Product], product_id: int, fields: tuple[str, Decimal, int], ) -> Product | None: - name = normalize_product_name(fields[0]) - if not name: - print("product name must not be blank") - return None + name = normalize_product_name(fields[0]) + if not name: + print("product name must not be blank") + return None - index = _find_index(storage, product_id) - if index is None: - print(f"no product with id {product_id}") - return None + index = _find_index(storage, product_id) + if index is None: + print(f"no product with id {product_id}") + return None - if _name_taken(storage, name, except_id=product_id): - print(f"product name '{name}' is already taken") - return None + if _name_taken(storage, name, except_id=product_id): + print(f"product name '{name}' is already taken") + return None - updated = (product_id, name, normalize_price(fields[1]), fields[2]) - storage[index] = updated - return updated + updated = (product_id, name, normalize_price(fields[1]), fields[2]) + storage[index] = updated + return updated def delete_product(storage: list[Product], product_id: int) -> int | None: @@ -111,4 +109,4 @@ def delete_product(storage: list[Product], product_id: int) -> int | None: print(f"no product with id {product_id}") return None del storage[index] - return product_id \ No newline at end of file + return product_id diff --git a/src/part3/run_admin.py b/src/part3/run_admin.py index b0e2b8cf..c5c4c452 100644 --- a/src/part3/run_admin.py +++ b/src/part3/run_admin.py @@ -43,7 +43,7 @@ PROMPT: Final[str] = "admin>" -HELP_TEXT: Final[str] =""""Available commands: +HELP_TEXT: Final[str] = """"Available commands: * help -- print the list of commands and what each one does; * exit -- leave the loop and end the program; * show -- print the whole store as a text table; @@ -63,7 +63,8 @@ def show_help() -> None: - print(HELP_TEXT) + print(HELP_TEXT) + def print_result(result: object) -> None: if result is not None: diff --git a/src/part3/utils.py b/src/part3/utils.py index 3bf29971..4e47bd77 100644 --- a/src/part3/utils.py +++ b/src/part3/utils.py @@ -17,6 +17,7 @@ text table meant to be printed to a terminal; each column is sized to the longest value it holds in that particular call. """ + import re from decimal import ROUND_HALF_UP, Decimal from typing import Final @@ -49,7 +50,7 @@ def normalize_product_name(name: str) -> str: def _measure_widths(rows: list[tuple[str, ...]]) -> list[int]: - return[ + return [ max(len(header), *(len(row[i]) for row in rows)) for i, header in enumerate(TABLE_HEADERS) ] @@ -66,9 +67,9 @@ def _render_separator(widths: list[int]) -> str: def get_storage_str_representation(storage: list[Product]) -> str: - rows = [tuple(str(value) for value in product) for product in storage] - widths = _measure_widths(rows) + rows = [tuple(str(value) for value in product) for product in storage] + widths = _measure_widths(rows) - lines = [_render_row(TABLE_HEADERS, widths), _render_separator(widths)] - lines.extend(_render_row(row, widths) for row in rows) - return "\n".join(lines) + lines = [_render_row(TABLE_HEADERS, widths), _render_separator(widths)] + lines.extend(_render_row(row, widths) for row in rows) + return "\n".join(lines) From 84881b028a1b6f0e652696e635aa24d664de38fc Mon Sep 17 00:00:00 2001 From: Mihail Date: Mon, 12 Oct 2026 21:07:25 +0300 Subject: [PATCH 18/18] Solve task 3 --- src/part3/crud.py | 12 ++++++------ src/part3/run_admin.py | 42 +++++++++++++++++++++++------------------- src/part3/utils.py | 8 ++++---- 3 files changed, 33 insertions(+), 29 deletions(-) diff --git a/src/part3/crud.py b/src/part3/crud.py index ef97d63b..a045aa33 100644 --- a/src/part3/crud.py +++ b/src/part3/crud.py @@ -48,15 +48,15 @@ def create_product( product_id = generate_product_id(storage) storage.append((product_id, name, normalize_price(fields[1]), fields[2])) + return product_id def read_product(storage: list[Product], product_id: int) -> Product | None: - def read_product(storage: list[Product], product_id: int) -> Product | None: - for product in storage: - if product[PRODUCT_ID_INDEX] == product_id: - return product - print(f"no product with id {product_id}") - return None + for product in storage: + if product[PRODUCT_ID_INDEX] == product_id: + return product + print(f"no product with id {product_id}") + return None def _find_index(storage: list[Product], product_id: int) -> int | None: diff --git a/src/part3/run_admin.py b/src/part3/run_admin.py index c5c4c452..a97085e8 100644 --- a/src/part3/run_admin.py +++ b/src/part3/run_admin.py @@ -29,6 +29,7 @@ it. """ +from collections.abc import Callable from decimal import Decimal, InvalidOperation from typing import Final @@ -42,24 +43,23 @@ from .utils import get_storage_str_representation -PROMPT: Final[str] = "admin>" -HELP_TEXT: Final[str] = """"Available commands: -* help -- print the list of commands and what each one does; -* exit -- leave the loop and end the program; -* show -- print the whole store as a text table; -* create -- add a product; -* read -- print one product; -* update -- overwrite a product; -* delete -- remove a product. +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 always the last -two words of the line; everything between the verb (and, for update, -the id) and those two words is joined with single spaces into the -product name, so a multi-word name such as Gibson SG Junior needs no -quoting.""" +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.""" _NAME_AND_TWO_NUMBERS: Final = 3 -_ID_AND_NAME_AND_TWO_NUNBERS: Final = 4 +_ID_AND_NAME_AND_TWO_NUMBERS: Final = 4 +_VARIADIC: Final = frozenset({"create", "update"}) def show_help() -> None: @@ -104,11 +104,11 @@ def _run_help(storage: list[Product], args: list[str]) -> None: show_help() -def _run_exit(storage: list[Product], args: list[str]) -> None: +def _run_exit(storage: list[Product], args: list[str]) -> bool | None: return False -type _Handler = callable[[list[Product], list[str]], bool | None] +type _Handler = Callable[[list[Product], list[str]], bool | None] _COMMANDS: Final[dict[str, tuple[_Handler, int | None]]] = { @@ -118,13 +118,17 @@ def _run_exit(storage: list[Product], args: list[str]) -> None: "read": (_run_read, 1), "delete": (_run_delete, 1), "create": (_run_create, _NAME_AND_TWO_NUMBERS), - "update": (_run_update, _ID_AND_NAME_AND_TWO_NUNBERS), + "update": (_run_update, _ID_AND_NAME_AND_TWO_NUMBERS), } def _accepts(verb: str, args: list[str]) -> bool: _, arity = _COMMANDS[verb] - return arity is None or len(args) == arity + if arity is None: + return True + if verb in _VARIADIC: + return len(args) >= arity + return len(args) == arity def run_command(storage: list[Product], line: str) -> bool: diff --git a/src/part3/utils.py b/src/part3/utils.py index 4e47bd77..7d16258f 100644 --- a/src/part3/utils.py +++ b/src/part3/utils.py @@ -31,14 +31,14 @@ # решение части 2) PRICE_PRECISION: Final[int] = 2 PRICE_STEP: Final = Decimal(1).scaleb(-PRICE_PRECISION) -_WHITESPACE_RUN: Final = re.complite(r"\s+") +_WHITESPACE_RUN: Final = re.compile(r"\s+") _CELL_PADDING: Final = 2 # 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: задайте заголовки столбцов таблицы -TABLE_HEADERS: Final[tuple[str, ...]] = ("id", "name", "price", "quantity") +TABLE_HEADERS: Final[tuple[str, ...]] = ("ID", "name", "price", "quantity") def normalize_price(price: Decimal) -> Decimal: @@ -57,12 +57,12 @@ def _measure_widths(rows: list[tuple[str, ...]]) -> list[int]: def _render_row(cells: tuple[str, ...], widths: list[int]) -> str: - padded = (cell.ljust(widths) for cell, width in zip(cells, widths)) + padded = (cell.ljust(width) for cell, width in zip(cells, widths)) return "| " + " | ".join(padded) + " |" def _render_separator(widths: list[int]) -> str: - runs = ("-" * (widths + _CELL_PADDING) for width in widths) + runs = ("-" * (width + _CELL_PADDING) for width in widths) return "|" + "|".join(runs) + "|"