From 92a9f0387fd25ed1ad645e4ad4eb7cd0cb011e37 Mon Sep 17 00:00:00 2001 From: Roman_Lebedev Date: Tue, 22 Sep 2026 19:39:12 +0300 Subject: [PATCH 1/8] Solve task 1 --- src/part1/constants.py | 26 ++++++++++++++++++-------- src/part1/snowflake.py | 40 ++++++++++++++++++++++++++++++++-------- 2 files changed, 50 insertions(+), 16 deletions(-) diff --git a/src/part1/constants.py b/src/part1/constants.py index 9abfbba2..2547296b 100644 --- a/src/part1/constants.py +++ b/src/part1/constants.py @@ -19,20 +19,30 @@ # Все константы, зависящие от ёмкостей *_BITS, должны вычисляться из них. # Twitter's original Snowflake epoch: 2010-11-04 01:42:54.657 UTC. -EPOCH_MS_DEFAULT: Final[int] = 0 +EPOCH_MS_DEFAULT: Final[int] = 1288834974657 # Node identifier used when the caller does not supply one. -NODE_ID_DEFAULT: Final[int] = 0 +NODE_ID_DEFAULT: Final[int] = 1 # Width of each field, in bits. -TIMESTAMP_BITS: Final[int] = 0 -NODE_ID_BITS: Final[int] = 0 -SEQUENCE_ID_BITS: Final[int] = 0 +TIMESTAMP_BITS: Final[int] = 41 +NODE_ID_BITS: Final[int] = 10 +SEQUENCE_ID_BITS: Final[int] = 12 # Largest value each field can hold. -TIMESTAMP_MS_MAX: Final[int] = 0 -NODE_ID_MAX: Final[int] = 0 -SEQUENCE_ID_MAX: Final[int] = 0 +TIMESTAMP_MS_MAX: Final[int] = 2**TIMESTAMP_BITS - 1 +NODE_ID_MAX: Final[int] = 2**NODE_ID_BITS - 1 +SEQUENCE_ID_MAX: Final[int] = 2**SEQUENCE_ID_BITS - 1 # Здесь можно добавить собственные вспомогательные константы # (например, сдвиги полей при сборке идентификатора). +TIMESTAMP_MASK: Final[int] = int( + ("0" + "1" * TIMESTAMP_BITS + "0" * NODE_ID_BITS + "0" * SEQUENCE_ID_BITS), 2 +) + +NODE_ID_MASK: Final[int] = int( + ("0" + "0" * TIMESTAMP_BITS + "1" * NODE_ID_BITS + "0" * SEQUENCE_ID_BITS), 2 +) +SEQUENCE_ID_MASK: Final[int] = int( + ("0" + "0" * TIMESTAMP_BITS + "0" * NODE_ID_BITS + "1" * SEQUENCE_ID_BITS), 2 +) diff --git a/src/part1/snowflake.py b/src/part1/snowflake.py index d3abf050..f1881217 100644 --- a/src/part1/snowflake.py +++ b/src/part1/snowflake.py @@ -13,13 +13,18 @@ :func:`decode_node_id` and :func:`decode_sequence_id`. """ -import time # noqa: F401 +import time -from .constants import ( # noqa: F401 +from .constants import ( EPOCH_MS_DEFAULT, + NODE_ID_BITS, NODE_ID_DEFAULT, + NODE_ID_MASK, NODE_ID_MAX, + SEQUENCE_ID_BITS, + SEQUENCE_ID_MASK, SEQUENCE_ID_MAX, + TIMESTAMP_MASK, TIMESTAMP_MS_MAX, ) @@ -34,7 +39,7 @@ def read_current_millis(epoch_ms: int) -> int: The count of whole milliseconds between ``epoch_ms`` and now. May be negative if ``epoch_ms`` lies in the future. """ - # TODO: реализуйте функцию + return int(time.time() * 1000) - epoch_ms return 0 @@ -50,7 +55,7 @@ def decode_timestamp_ms(snowflake_id: int, epoch_ms: int = EPOCH_MS_DEFAULT) -> The absolute Unix time in milliseconds at which the identifier was generated. """ - # TODO: реализуйте функцию + return (snowflake_id & TIMESTAMP_MASK) + epoch_ms return 0 @@ -64,7 +69,7 @@ def decode_node_id(snowflake_id: int) -> int: The node identifier packed into ``snowflake_id``, in the range ``[0, NODE_ID_MAX]``. """ - # TODO: реализуйте функцию + return snowflake_id & NODE_ID_MASK return 0 @@ -78,7 +83,7 @@ def decode_sequence_id(snowflake_id: int) -> int: The per-millisecond sequence counter packed into ``snowflake_id``, in the range ``[0, SEQUENCE_ID_MAX]``. """ - # TODO: реализуйте функцию + return snowflake_id & SEQUENCE_ID_MASK return 0 @@ -110,5 +115,24 @@ def generate_snowflake_id( timestamp field (roughly 69 years after ``epoch_ms``). In each of those cases an explanatory message is printed to stdout first. """ - # TODO: реализуйте функцию - return 0 + if node_id < 0 or node_id > NODE_ID_MAX: + print(f"node_id must be in [0, {NODE_ID_MAX}], given value: {node_id}") + return None + + elif sequence_id < 0 or sequence_id > SEQUENCE_ID_MAX: + print( + f"sequence_id must be in [0, {SEQUENCE_ID_MAX}], given value: {sequence_id}" + ) + return None + + if read_current_millis(epoch_ms) > TIMESTAMP_MS_MAX: + print("overflows") + return None + + generated_id = ( + (read_current_millis(epoch_ms) << (SEQUENCE_ID_BITS + NODE_ID_BITS)) + | (node_id << (SEQUENCE_ID_BITS)) + | sequence_id + ) + + return generated_id From 457a19fe48aa4a812952221dfb6815117b97d754 Mon Sep 17 00:00:00 2001 From: Roman_Lebedev Date: Mon, 28 Sep 2026 22:13:57 +0300 Subject: [PATCH 2/8] fix: replace mask logic with shift --- src/part1/constants.py | 12 ++---------- src/part1/snowflake.py | 33 ++++++++++++--------------------- 2 files changed, 14 insertions(+), 31 deletions(-) diff --git a/src/part1/constants.py b/src/part1/constants.py index 2547296b..4c96b2ee 100644 --- a/src/part1/constants.py +++ b/src/part1/constants.py @@ -36,13 +36,5 @@ # Здесь можно добавить собственные вспомогательные константы # (например, сдвиги полей при сборке идентификатора). -TIMESTAMP_MASK: Final[int] = int( - ("0" + "1" * TIMESTAMP_BITS + "0" * NODE_ID_BITS + "0" * SEQUENCE_ID_BITS), 2 -) - -NODE_ID_MASK: Final[int] = int( - ("0" + "0" * TIMESTAMP_BITS + "1" * NODE_ID_BITS + "0" * SEQUENCE_ID_BITS), 2 -) -SEQUENCE_ID_MASK: Final[int] = int( - ("0" + "0" * TIMESTAMP_BITS + "0" * NODE_ID_BITS + "1" * SEQUENCE_ID_BITS), 2 -) +NODE_ID_SHIFT: Final[int] = SEQUENCE_ID_BITS +TIMESTAMP_SHIFT: Final[int] = SEQUENCE_ID_BITS + NODE_ID_BITS diff --git a/src/part1/snowflake.py b/src/part1/snowflake.py index f1881217..478dd93e 100644 --- a/src/part1/snowflake.py +++ b/src/part1/snowflake.py @@ -17,15 +17,12 @@ from .constants import ( EPOCH_MS_DEFAULT, - NODE_ID_BITS, NODE_ID_DEFAULT, - NODE_ID_MASK, NODE_ID_MAX, - SEQUENCE_ID_BITS, - SEQUENCE_ID_MASK, + NODE_ID_SHIFT, SEQUENCE_ID_MAX, - TIMESTAMP_MASK, TIMESTAMP_MS_MAX, + TIMESTAMP_SHIFT ) @@ -40,7 +37,6 @@ def read_current_millis(epoch_ms: int) -> int: negative if ``epoch_ms`` lies in the future. """ return int(time.time() * 1000) - epoch_ms - return 0 def decode_timestamp_ms(snowflake_id: int, epoch_ms: int = EPOCH_MS_DEFAULT) -> int: @@ -55,8 +51,7 @@ def decode_timestamp_ms(snowflake_id: int, epoch_ms: int = EPOCH_MS_DEFAULT) -> The absolute Unix time in milliseconds at which the identifier was generated. """ - return (snowflake_id & TIMESTAMP_MASK) + epoch_ms - return 0 + return ((snowflake_id >> TIMESTAMP_SHIFT) & TIMESTAMP_MS_MAX) + epoch_ms def decode_node_id(snowflake_id: int) -> int: @@ -69,8 +64,7 @@ def decode_node_id(snowflake_id: int) -> int: The node identifier packed into ``snowflake_id``, in the range ``[0, NODE_ID_MAX]``. """ - return snowflake_id & NODE_ID_MASK - return 0 + return (snowflake_id >> NODE_ID_SHIFT) & NODE_ID_MAX def decode_sequence_id(snowflake_id: int) -> int: @@ -83,8 +77,7 @@ def decode_sequence_id(snowflake_id: int) -> int: The per-millisecond sequence counter packed into ``snowflake_id``, in the range ``[0, SEQUENCE_ID_MAX]``. """ - return snowflake_id & SEQUENCE_ID_MASK - return 0 + return snowflake_id & SEQUENCE_ID_MAX def generate_snowflake_id( @@ -115,24 +108,22 @@ def generate_snowflake_id( timestamp field (roughly 69 years after ``epoch_ms``). In each of those cases an explanatory message is printed to stdout first. """ - if node_id < 0 or node_id > NODE_ID_MAX: + if not 0 <= node_id <= NODE_ID_MAX: print(f"node_id must be in [0, {NODE_ID_MAX}], given value: {node_id}") return None - elif sequence_id < 0 or sequence_id > SEQUENCE_ID_MAX: + elif not 0 <= sequence_id <= SEQUENCE_ID_MAX: print( f"sequence_id must be in [0, {SEQUENCE_ID_MAX}], given value: {sequence_id}" ) return None - - if read_current_millis(epoch_ms) > TIMESTAMP_MS_MAX: + elapsed_ms = read_current_millis(epoch_ms) + if elapsed_ms > TIMESTAMP_MS_MAX: print("overflows") return None - generated_id = ( - (read_current_millis(epoch_ms) << (SEQUENCE_ID_BITS + NODE_ID_BITS)) - | (node_id << (SEQUENCE_ID_BITS)) + return ( + (elapsed_ms << TIMESTAMP_SHIFT) + | (node_id << NODE_ID_SHIFT) | sequence_id ) - - return generated_id From 2b9db7497384052effbfd75f1cd914bb95d605be Mon Sep 17 00:00:00 2001 From: Roman_Lebedev Date: Mon, 28 Sep 2026 22:20:54 +0300 Subject: [PATCH 3/8] fix: now passes all tests --- src/part1/constants.py | 3 --- src/part1/snowflake.py | 12 +++++------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/part1/constants.py b/src/part1/constants.py index 4c96b2ee..fb2c280c 100644 --- a/src/part1/constants.py +++ b/src/part1/constants.py @@ -15,9 +15,6 @@ from typing import Final -# TODO: замените заглушки (0) на корректные значения, см. TASK.md. -# Все константы, зависящие от ёмкостей *_BITS, должны вычисляться из них. - # Twitter's original Snowflake epoch: 2010-11-04 01:42:54.657 UTC. EPOCH_MS_DEFAULT: Final[int] = 1288834974657 diff --git a/src/part1/snowflake.py b/src/part1/snowflake.py index 478dd93e..9da8d8f6 100644 --- a/src/part1/snowflake.py +++ b/src/part1/snowflake.py @@ -22,7 +22,7 @@ NODE_ID_SHIFT, SEQUENCE_ID_MAX, TIMESTAMP_MS_MAX, - TIMESTAMP_SHIFT + TIMESTAMP_SHIFT, ) @@ -112,18 +112,16 @@ def generate_snowflake_id( print(f"node_id must be in [0, {NODE_ID_MAX}], given value: {node_id}") return None - elif not 0 <= sequence_id <= SEQUENCE_ID_MAX: + if not 0 <= sequence_id <= SEQUENCE_ID_MAX: print( f"sequence_id must be in [0, {SEQUENCE_ID_MAX}], given value: {sequence_id}" ) return None + elapsed_ms = read_current_millis(epoch_ms) + if elapsed_ms > TIMESTAMP_MS_MAX: print("overflows") return None - return ( - (elapsed_ms << TIMESTAMP_SHIFT) - | (node_id << NODE_ID_SHIFT) - | sequence_id - ) + return (elapsed_ms << TIMESTAMP_SHIFT) | (node_id << NODE_ID_SHIFT) | sequence_id From 1fd4bb901ff6336392ca5bef7f8e9a2167add66a Mon Sep 17 00:00:00 2001 From: Roman_Lebedev Date: Wed, 30 Sep 2026 18:26:30 +0300 Subject: [PATCH 4/8] feat: add task part2 code --- src/part2/cart.py | 70 +++++++++++++++++++++++++++++++++++++++----- src/part2/crud.py | 48 ++++++++++++++++++++++-------- src/part2/storage.py | 10 +++---- src/part2/utils.py | 10 +++---- 4 files changed, 106 insertions(+), 32 deletions(-) diff --git a/src/part2/cart.py b/src/part2/cart.py index 063f9aea..b4e7a166 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,8 @@ type CartLine = tuple[int, int] -# TODO: задайте позиции полей внутри кортежа CartLine LINE_PRODUCT_ID_INDEX: Final = 0 -LINE_QUANTITY_INDEX: Final = 0 +LINE_QUANTITY_INDEX: Final = 1 def add_to_cart( @@ -53,8 +52,33 @@ def add_to_cart( 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), + ) + + for index, line in enumerate(cart): + if line[LINE_PRODUCT_ID_INDEX] == product_id: + new_line = (product_id, line[LINE_QUANTITY_INDEX] + quantity) + cart[index] = new_line + return new_line + + new_line = (product_id, quantity) + cart.append(new_line) + return new_line def remove_from_cart( @@ -85,5 +109,35 @@ def remove_from_cart( 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) + + for line_index, line in enumerate(cart): + if line[LINE_PRODUCT_ID_INDEX] == product_id: + break + else: + print(f"product {product_id} is not in the cart") + return None + + in_cart = cart[line_index][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), + ) + + new_line = (product_id, in_cart - quantity) + if new_line[LINE_QUANTITY_INDEX]: + cart[line_index] = new_line + else: + del cart[line_index] + return new_line diff --git a/src/part2/crud.py b/src/part2/crud.py index d026cbcb..fbc3cee4 100644 --- a/src/part2/crud.py +++ b/src/part2/crud.py @@ -14,13 +14,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 # noqa: F401 +from .utils import normalize_price def generate_product_id(storage: list[Product]) -> int: @@ -34,8 +34,9 @@ def generate_product_id(storage: list[Product]) -> int: :data:`~src.part2.storage.PRODUCT_ID_MIN` when ``storage`` is empty. """ - # TODO: реализуйте функцию - return 0 + if not storage: + return PRODUCT_ID_MIN + return max(product[PRODUCT_ID_INDEX] for product in storage) + 1 def create_product( @@ -56,8 +57,15 @@ def create_product( ``storage`` is left unchanged and a message naming the clashing name is printed. """ - # TODO: реализуйте функцию - return 0 + name, price, quantity = fields + + if any(product[NAME_INDEX] == name for product in storage): + print(f"product name '{name}' is already taken") + return None + + 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: @@ -72,8 +80,11 @@ def read_product(storage: list[Product], product_id: int) -> Product | None: ``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( @@ -99,8 +110,17 @@ def update_product( ``None`` when no product carries that identifier (``storage`` is left unchanged and a message is printed). """ - # TODO: реализуйте функцию - return (0, "", Decimal(0), 0) + product = read_product(storage, product_id) + + if product is None: + return None + + name, price, quantity = fields + + updated_product = (product_id, name, normalize_price(price), quantity) + storage[storage.index(product)] = updated_product + + return updated_product def delete_product(storage: list[Product], product_id: int) -> int | None: @@ -116,5 +136,9 @@ def delete_product(storage: list[Product], product_id: int) -> int | None: product carried that identifier (``storage`` is left unchanged and a message is printed). """ - # TODO: реализуйте функцию - return 0 + product = read_product(storage, product_id) + if product is None: + return None + + storage.remove(product) + return product_id diff --git a/src/part2/storage.py b/src/part2/storage.py index b63d5fc7..e4bceff1 100644 --- a/src/part2/storage.py +++ b/src/part2/storage.py @@ -24,11 +24,9 @@ type Product = tuple[int, str, Decimal, int] -# 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..d848125d 100644 --- a/src/part2/utils.py +++ b/src/part2/utils.py @@ -6,13 +6,12 @@ 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: @@ -25,5 +24,4 @@ def normalize_price(price: Decimal) -> Decimal: ``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 4bd0f84475ec71d786ab0e9d40027c4afeb8834b Mon Sep 17 00:00:00 2001 From: Lebedev Roman Date: Wed, 7 Oct 2026 23:48:59 +0300 Subject: [PATCH 5/8] Solve task 3 --- src/part3/crud.py | 79 ++++++++++++++++++++++++++++++------------ src/part3/run_admin.py | 49 +++++++++++++++++++++----- src/part3/storage.py | 15 ++++---- src/part3/utils.py | 79 +++++++++++++++++++++--------------------- 4 files changed, 142 insertions(+), 80 deletions(-) diff --git a/src/part3/crud.py b/src/part3/crud.py index 68811d7e..f11d0c1a 100644 --- a/src/part3/crud.py +++ b/src/part3/crud.py @@ -1,32 +1,26 @@ """Create/read/update/delete operations over the in-memory product store. Every operation takes the store -- a list of -:data:`~src.part3.storage.Product` tuples -- as its first argument and +:data:`~src.part2.storage.Product` tuples -- as its first argument and works on it in place. The failure path never raises: the operation prints an explanatory message to stdout and returns ``None``. The identifier of a new product is derived from the store itself (:func:`generate_product_id`): one past the greatest identifier in use, or -:data:`~src.part3.storage.PRODUCT_ID_MIN` when the store is empty. - -Every product name that reaches the store is passed through -:func:`~src.part3.utils.normalize_product_name` first. A name that -normalises to an empty string is rejected -- the operation prints a -message and changes nothing. Names are also kept unique -- -:func:`create_product` refuses a normalised name that is already taken, -and :func:`update_product` refuses one that is already taken by another -product. +:data:`~src.part2.storage.PRODUCT_ID_MIN` when the store is empty. +Product names are kept unique -- :func:`create_product` refuses a name that +is already taken. """ 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: @@ -37,11 +31,12 @@ def generate_product_id(storage: list[Product]) -> int: Returns: One past the greatest identifier currently held in ``storage``, or - :data:`~src.part3.storage.PRODUCT_ID_MIN` when ``storage`` is + :data:`~src.part2.storage.PRODUCT_ID_MIN` when ``storage`` is empty. """ - # TODO: реализуйте функцию (используйте своё решение части 2) - return 0 + if not storage: + return PRODUCT_ID_MIN + return max(product[PRODUCT_ID_INDEX] for product in storage) + 1 def create_product( @@ -64,8 +59,20 @@ def create_product( normalised name already exists. In the ``None`` case ``storage`` is left unchanged and an explanatory message is printed. """ - # TODO: реализуйте функцию - return 0 + raw_name, price, quantity = fields + name = normalize_product_name(raw_name) + + if not name: + print("product name must not be blank") + return None + + if any(product[NAME_INDEX] == name for product in storage): + print(f"product name '{name}' is already taken") + return None + + 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: @@ -80,8 +87,11 @@ def read_product(storage: list[Product], product_id: int) -> Product | None: ``None`` when no product carries that identifier (a message is printed in that case). """ - # TODO: реализуйте функцию (используйте своё решение части 2) - 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( @@ -110,8 +120,27 @@ def update_product( unchanged and an explanatory message is printed. Renaming a product to its own current name is allowed. """ - # TODO: реализуйте функцию - return (0, "", Decimal(0), 0) + raw_name, price, quantity = fields + name = normalize_product_name(raw_name) + + if not name: + print("product name must not be blank") + return None + + product = read_product(storage, product_id) + if product is None: + return None + + if any( + other[NAME_INDEX] == name and other[PRODUCT_ID_INDEX] != product_id + for other in storage + ): + print(f"product name '{name}' is already taken") + return None + + updated_product = (product_id, name, normalize_price(price), quantity) + storage[storage.index(product)] = updated_product + return updated_product def delete_product(storage: list[Product], product_id: int) -> int | None: @@ -127,5 +156,9 @@ def delete_product(storage: list[Product], product_id: int) -> int | None: product carried that identifier (``storage`` is left unchanged and a message is printed). """ - # TODO: реализуйте функцию (используйте своё решение части 2) - return 0 + product = read_product(storage, product_id) + if product is None: + return None + + storage.remove(product) + return product_id diff --git a/src/part3/run_admin.py b/src/part3/run_admin.py index c5feeb0b..3f737d3a 100644 --- a/src/part3/run_admin.py +++ b/src/part3/run_admin.py @@ -29,22 +29,34 @@ 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 # TODO: задайте приглашение и текст справки -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: @@ -54,7 +66,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: реализуйте функцию + print(HELP_TEXT) def print_result(result: object) -> None: @@ -65,7 +77,8 @@ def print_result(result: object) -> None: 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: @@ -88,8 +101,26 @@ def run_command(storage: list[Product], line: str) -> bool: decimal.InvalidOperation: If the price argument of ``create`` or ``update`` does not parse as a decimal number. """ - # TODO: реализуйте функцию - return False + match line.split(): + case ["help"]: + show_help() + case ["exit"]: + return False + case ["show"]: + print(get_storage_str_representation(storage)) + case ["create", *name_words, price, quantity] if name_words: + fields = (" ".join(name_words), Decimal(price), int(quantity)) + print_result(create_product(storage, fields)) + case ["read", product_id]: + print_result(read_product(storage, int(product_id))) + case ["update", product_id, *name_words, price, quantity] if name_words: + fields = (" ".join(name_words), Decimal(price), int(quantity)) + print_result(update_product(storage, int(product_id), fields)) + case ["delete", product_id]: + print_result(delete_product(storage, int(product_id))) + case _: + print(f"'{line}' is not a command") + return True def main() -> None: diff --git a/src/part3/storage.py b/src/part3/storage.py index c5e34be5..e4bceff1 100644 --- a/src/part3/storage.py +++ b/src/part3/storage.py @@ -6,11 +6,11 @@ where: * ``product_id`` -- unique positive integer key, assigned by the create - operation in :mod:`src.part3.crud`; + operation in :mod:`src.part2.crud`; * ``name`` -- human-readable product name, unique across the store; * ``price`` -- price of a single unit, held as a :class:`~decimal.Decimal` amount rounded to a fixed number of decimal places (see - :func:`src.part3.utils.normalize_price`) so that money is never + :func:`src.part2.utils.normalize_price`) so that money is never subject to binary floating-point error; * ``quantity`` -- number of units currently in stock. @@ -24,12 +24,9 @@ type Product = tuple[int, str, Decimal, int] -# 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 diff --git a/src/part3/utils.py b/src/part3/utils.py index 318ded0b..16c57c75 100644 --- a/src/part3/utils.py +++ b/src/part3/utils.py @@ -1,41 +1,20 @@ -"""Standalone helpers shared by the part3 storage and CRUD modules. +"""Standalone helpers shared by the part2 storage and CRUD modules. -Normalisation: - -* :func:`normalize_price` rounds a raw :class:`~decimal.Decimal` amount to - the fixed number of fractional digits (:data:`PRICE_PRECISION`) that - every stored price uses, keeping currency values free of binary - floating-point error. -* :func:`normalize_product_name` strips surrounding whitespace from a - product name, collapses every internal run of whitespace to a single - space and lower-cases it, giving every stored name a single canonical - form. - -Presentation: - -* :func:`get_storage_str_representation` renders the whole store as a - text table meant to be printed to a terminal; each column is sized to - the longest value it holds in that particular call. +For now this is limited to money handling: :func:`normalize_price` rounds a +raw :class:`~decimal.Decimal` amount to the fixed number of fractional +digits (:data:`PRICE_PRECISION`) that every stored price uses, keeping +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 from .storage import Product -# Number of fractional digits every stored price is rounded to. -# Quantisation step derived from PRICE_PRECISION, e.g. Decimal("0.01"). -# TODO: задайте число знаков после запятой и шаг квантования (используйте своё -# решение части 2) -PRICE_PRECISION: Final[int] = 0 -PRICE_STEP: Final = Decimal(0) - -# 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, ...]] = () +PRICE_PRECISION: Final[int] = 2 +PRICE_STEP: Final = Decimal(1).scaleb(-PRICE_PRECISION) +TABLE_HEADERS: Final[tuple[str, ...]] = ("ID", "name", "price", "quantity") def normalize_price(price: Decimal) -> Decimal: @@ -48,8 +27,7 @@ def normalize_price(price: Decimal) -> Decimal: ``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: @@ -66,11 +44,7 @@ def normalize_product_name(name: str) -> str: The result is an empty string when ``name`` holds nothing but whitespace. """ - # TODO: реализуйте функцию - return "" - - -# TODO: при необходимости добавьте свои вспомогательные функции + return " ".join(name.split()).lower() def get_storage_str_representation(storage: list[Product]) -> str: @@ -91,5 +65,32 @@ def get_storage_str_representation(storage: list[Product]) -> str: is empty only the header and separator rows are returned, sized to the header labels. """ - # TODO: реализуйте функцию - return "" + + widths = [] + for column, header in enumerate(TABLE_HEADERS): + lengths = [len(str(product[column])) for product in storage] + widths.append(max([len(header), *lengths])) + + rows: list[tuple[str, ...]] = [TABLE_HEADERS] + for product in storage: + rows.append(tuple(str(field) for field in product)) + + result = [] + + line = "|" + for i in range(len(rows[0])): + line += " " + rows[0][i].ljust(widths[i]) + " |" + result.append(line) + + line = "|" + for width in widths: + line += "-" * (width + 2) + "|" + result.append(line) + + for row in rows[1:]: + line = "|" + for i in range(len(row)): + line += " " + row[i].ljust(widths[i]) + " |" + result.append(line) + + return "\n".join(result) From 36ffb983608219177f5cb33fa14b67e7364c3c12 Mon Sep 17 00:00:00 2001 From: Lebedev Roman Date: Wed, 28 Oct 2026 18:45:06 +0300 Subject: [PATCH 6/8] Solve task 4 --- src/part4/cart.py | 102 ++++++++++++++++++++++++++++------------- src/part4/crud.py | 76 ++++++++++++++++++++++-------- src/part4/run_admin.py | 79 ++++++++++++++++++++++--------- src/part4/storage.py | 7 ++- src/part4/utils.py | 67 +++++++++++++++++---------- 5 files changed, 233 insertions(+), 98 deletions(-) diff --git a/src/part4/cart.py b/src/part4/cart.py index aed9bb72..b2b082f0 100644 --- a/src/part4/cart.py +++ b/src/part4/cart.py @@ -9,10 +9,8 @@ an explanatory message to stdout and returns ``None``. """ -from .storage import Product - - -# TODO: при необходимости добавьте свои вспомогательные функции +from .crud import read_product, update_product +from .storage import QUANTITY_KEY, Product def add_to_cart( @@ -25,29 +23,41 @@ def add_to_cart( """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 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 - gains ``quantity`` units of ``product_id`` -- the product is added when - the cart had none of it. + 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. - product_names: The normalised names in ``storage``; a - quantity-only change never renames a product, so it is left - unchanged here. 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 quantity of ``product_id`` in the cart after the addition, or - ``None`` when the product is unknown or the store cannot cover the - request. + 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 + product = read_product(storage, product_id) + if product is None: + return None + + in_stock = product[QUANTITY_KEY] + 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_names, product_id, {QUANTITY_KEY: in_stock - quantity} + ) + + cart[product_id] = cart.get(product_id, 0) + quantity + return cart[product_id] def remove_from_cart( @@ -59,29 +69,57 @@ def remove_from_cart( ) -> int | None: """Move ``quantity`` units of ``product_id`` from ``cart`` back to the store. - If the cart holds no units of ``product_id``, a message is printed and - nothing changes. If it holds fewer units than requested, an explanatory - message is printed and nothing changes. If the product is missing from - ``storage``, a message is printed and nothing changes. Otherwise the cart loses - ``quantity`` units of the product -- the product is dropped from the - cart when its quantity reaches zero -- and the store record gains - ``quantity`` units back. + 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. - product_names: The normalised names in ``storage``; a - quantity-only change never renames a product, so it is left - unchanged here. 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 quantity of ``product_id`` left in the cart after the removal - (zero means the product was dropped from the cart), or ``None`` when - the cart does not hold the product or holds too few units. + 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 + + in_cart = cart.get(product_id) + if in_cart is None: + print(f"product {product_id} is not in the cart") + return None + + 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 + + product = read_product(storage, product_id) + if product is None: + return None + + update_product( + storage, + product_names, + product_id, + {QUANTITY_KEY: product[QUANTITY_KEY] + quantity}, + ) + + left_in_cart = in_cart - quantity + if left_in_cart: + cart[product_id] = left_in_cart + else: + del cart[product_id] + return left_in_cart diff --git a/src/part4/crud.py b/src/part4/crud.py index 471eff00..fa4a3d4a 100644 --- a/src/part4/crud.py +++ b/src/part4/crud.py @@ -23,16 +23,11 @@ than a scan over ``storage.values()``. """ -import uuid # noqa: F401 +import uuid from typing import Any -from .storage import ( # noqa: F401 - NAME_KEY, - PRICE_KEY, - QUANTITY_KEY, - Product, -) -from .utils import normalize_price, normalize_product_name # noqa: F401 +from .storage import NAME_KEY, PRICE_KEY, QUANTITY_KEY, Product +from .utils import normalize_price, normalize_product_name def generate_product_id() -> str: @@ -41,8 +36,7 @@ def generate_product_id() -> str: Returns: A freshly generated UUID, stringified. """ - # TODO: реализуйте функцию - return "" + return str(uuid.uuid4()) def create_product( @@ -73,8 +67,24 @@ def create_product( and ``product_names`` are left unchanged and an explanatory message is printed. """ - # TODO: реализуйте функцию - return "" + name = normalize_product_name(fields[NAME_KEY]) + + if not name: + print("product name must not be blank") + return None + + if name in product_names: + print(f"product name '{name}' is already taken") + return None + + product_id = generate_product_id() + storage[product_id] = { + NAME_KEY: name, + PRICE_KEY: normalize_price(fields[PRICE_KEY]), + QUANTITY_KEY: fields[QUANTITY_KEY], + } + product_names.add(name) + return product_id def read_product(storage: dict[str, Product], product_id: str) -> Product | None: @@ -88,8 +98,10 @@ def read_product(storage: dict[str, Product], product_id: str) -> Product | None The matching product record, or ``None`` when no product carries that identifier (a message is printed in that case). """ - # TODO: реализуйте функцию - return {} + product = storage.get(product_id) + if product is None: + print(f"no product with id {product_id}") + return product def update_product( @@ -129,8 +141,30 @@ def update_product( and an explanatory message is printed. Renaming a product to its own current name is allowed. """ - # TODO: реализуйте функцию - return {} + product = read_product(storage, product_id) + if product is None: + return None + + if NAME_KEY in fields: + name = normalize_product_name(fields[NAME_KEY]) + + if not name: + print("product name must not be blank") + return None + + if name != product[NAME_KEY] and name in product_names: + print(f"product name '{name}' is already taken") + return None + + fields[NAME_KEY] = name + product_names.discard(product[NAME_KEY]) + product_names.add(name) + + if PRICE_KEY in fields: + fields[PRICE_KEY] = normalize_price(fields[PRICE_KEY]) + + product.update(fields) + return product def delete_product( @@ -153,5 +187,11 @@ def delete_product( product carried that identifier (``storage`` and ``product_names`` are left unchanged and a message is printed). """ - # TODO: реализуйте функцию - return "" + product = read_product(storage, product_id) + if product is None: + return None + + product_names.discard(product[NAME_KEY]) + del storage[product_id] + + return product_id diff --git a/src/part4/run_admin.py b/src/part4/run_admin.py index 2247b29b..99810855 100644 --- a/src/part4/run_admin.py +++ b/src/part4/run_admin.py @@ -28,22 +28,33 @@ 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 +64,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 +75,17 @@ 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 build_fields(name_words: list[str], price: str, quantity: str) -> Product: + """Collect the console arguments of ``create`` or ``update`` into fields.""" + return { + NAME_KEY: " ".join(name_words), + PRICE_KEY: Decimal(price), + QUANTITY_KEY: int(quantity), + } def run_command( @@ -75,8 +96,6 @@ def run_command( Args: storage: The product store shared across the session; mutated in place by the ``create``, ``update`` and ``delete`` commands. - product_names: The normalised names already in ``storage``; kept - in sync by the ``create``, ``update`` and ``delete`` commands. line: One line of console input, already stripped of surrounding whitespace. @@ -86,24 +105,42 @@ def run_command( known command prints a notice and still returns ``True``. Raises: - ValueError: If the quantity argument of a CRUD command does not - parse as a base-10 integer. + 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 + match line.split(): + case ["help"]: + show_help() + case ["exit"]: + return False + case ["show"]: + print(get_storage_str_representation(storage)) + case ["create", *name_words, price, quantity] if name_words: + fields = build_fields(name_words, price, quantity) + print_result(create_product(storage, product_names, fields)) + case ["read", product_id]: + print_result(read_product(storage, product_id)) + case ["update", product_id, *name_words, price, quantity] if name_words: + fields = build_fields(name_words, price, quantity) + print_result(update_product(storage, product_names, product_id, fields)) + case ["delete", product_id]: + print_result(delete_product(storage, product_names, product_id)) + case _: + print(f"'{line}' is not a command") + return True def main() -> None: """Run the admin console until the ``exit`` command is entered. - A single product store, and the set of normalised names it holds, are - created empty and kept in memory for the whole session. Each iteration - reads one line from stdin, strips its surrounding whitespace and hands - it to :func:`run_command`. A line that names a command but whose price - or quantity argument does not parse is caught here and reported - without stopping the loop; only ``exit`` ends it. + A single product store is created empty and kept in memory for the + whole session. Each iteration reads one line from stdin, strips its + surrounding whitespace and hands it to :func:`run_command`. A line + that names a command but whose id, price or quantity argument does + not parse is caught here and reported without stopping the loop; only + ``exit`` ends it. """ storage: dict[str, Product] = {} product_names: set[str] = set() diff --git a/src/part4/storage.py b/src/part4/storage.py index 9acedf07..f0034782 100644 --- a/src/part4/storage.py +++ b/src/part4/storage.py @@ -39,7 +39,6 @@ type Product = dict[str, Any] # 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..13ab9e09 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, @@ -29,18 +29,9 @@ ) -# Number of fractional digits every stored price is rounded to. -# Quantisation step derived from PRICE_PRECISION, e.g. Decimal("0.01"). -# TODO: задайте число знаков после запятой и шаг квантования (используйте своё -# решение части 3) -PRICE_PRECISION: Final[int] = 0 -PRICE_STEP: Final = Decimal(0) - -# 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, ...]] = () +PRICE_PRECISION: Final[int] = 2 +PRICE_STEP: Final = Decimal(1).scaleb(-PRICE_PRECISION) +TABLE_HEADERS: Final[tuple[str, ...]] = ("ID", "name", "price", "quantity") def normalize_price(price: Decimal) -> Decimal: @@ -53,8 +44,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,11 +61,7 @@ def normalize_product_name(name: str) -> str: The result is an empty string when ``name`` holds nothing but whitespace. """ - # TODO: реализуйте функцию (используйте своё решение части 3) - return "" - - -# TODO: при необходимости добавьте свои вспомогательные функции + return " ".join(name.split()).lower() def get_storage_str_representation(storage: dict[str, Product]) -> str: @@ -96,5 +82,40 @@ def get_storage_str_representation(storage: dict[str, Product]) -> str: ``storage`` is empty only the header and separator rows are returned, sized to the header labels. """ - # TODO: реализуйте функцию (используйте своё решение части 3) - return "" + rows: list[tuple[str, ...]] = [TABLE_HEADERS] + for product_id, product in storage.items(): + rows.append( + ( + product_id, + product[NAME_KEY], + str(product[PRICE_KEY]), + str(product[QUANTITY_KEY]), + ) + ) + + widths = [] + for column in range(len(TABLE_HEADERS)): + widths.append(max(len(row[column]) for row in rows)) + + for product in storage.values(): + rows.append(tuple(str(field) for field in product)) + + result = [] + + line = "|" + for i in range(len(rows[0])): + line += " " + rows[0][i].ljust(widths[i]) + " |" + result.append(line) + + line = "|" + for width in widths: + line += "-" * (width + 2) + "|" + result.append(line) + + for row in rows[1:]: + line = "|" + for i in range(len(row)): + line += " " + row[i].ljust(widths[i]) + " |" + result.append(line) + + return "\n".join(result) From e5bbea1ed534ff40fc24bd8112d631d4f4cc8694 Mon Sep 17 00:00:00 2001 From: Lebedev Roman Date: Tue, 10 Nov 2026 19:59:24 +0300 Subject: [PATCH 7/8] feat: add 4 modules implementation --- src/part5/constants.py | 22 ++++----- src/part5/crud.py | 101 +++++++++++++++++++++++++++++----------- src/part5/decorators.py | 27 +++++++++-- src/part5/snowflake.py | 85 ++++++++++++++++++++++++--------- 4 files changed, 171 insertions(+), 64 deletions(-) diff --git a/src/part5/constants.py b/src/part5/constants.py index 984671bb..fb2c280c 100644 --- a/src/part5/constants.py +++ b/src/part5/constants.py @@ -15,23 +15,23 @@ from typing import Final -# TODO: замените заглушки (0) на корректные значения (используйте своё решение -# части 1). - # Twitter's original Snowflake epoch: 2010-11-04 01:42:54.657 UTC. -EPOCH_MS_DEFAULT: Final[int] = 0 +EPOCH_MS_DEFAULT: Final[int] = 1288834974657 # Node identifier used when the caller does not supply one. -NODE_ID_DEFAULT: Final[int] = 0 +NODE_ID_DEFAULT: Final[int] = 1 # Width of each field, in bits. -TIMESTAMP_BITS: Final[int] = 0 -NODE_ID_BITS: Final[int] = 0 -SEQUENCE_ID_BITS: Final[int] = 0 +TIMESTAMP_BITS: Final[int] = 41 +NODE_ID_BITS: Final[int] = 10 +SEQUENCE_ID_BITS: Final[int] = 12 # Largest value each field can hold. -TIMESTAMP_MS_MAX: Final[int] = 0 -NODE_ID_MAX: Final[int] = 0 -SEQUENCE_ID_MAX: Final[int] = 0 +TIMESTAMP_MS_MAX: Final[int] = 2**TIMESTAMP_BITS - 1 +NODE_ID_MAX: Final[int] = 2**NODE_ID_BITS - 1 +SEQUENCE_ID_MAX: Final[int] = 2**SEQUENCE_ID_BITS - 1 # Здесь можно добавить собственные вспомогательные константы +# (например, сдвиги полей при сборке идентификатора). +NODE_ID_SHIFT: Final[int] = SEQUENCE_ID_BITS +TIMESTAMP_SHIFT: Final[int] = SEQUENCE_ID_BITS + NODE_ID_BITS diff --git a/src/part5/crud.py b/src/part5/crud.py index 243b0528..9df36ff4 100644 --- a/src/part5/crud.py +++ b/src/part5/crud.py @@ -38,20 +38,20 @@ from collections.abc import Callable from typing import Any, Final -from .decorators import Registry -from .storage import Product +from .decorators import Registry, collect_stats +from .snowflake import get_snowflake_id_generator +from .storage import NAME_KEY, PRICE_KEY, QUANTITY_KEY, Product +from .utils import normalize_price, normalize_product_name STATS_REGISTRY: Final[Registry] = {} -# TODO: задайте ключи операций в реестре, см. TASK.md -CREATE_KEY: Final[str] = "" -READ_KEY: Final[str] = "" -UPDATE_KEY: Final[str] = "" -DELETE_KEY: Final[str] = "" +CREATE_KEY: Final[str] = "create" +READ_KEY: Final[str] = "read" +UPDATE_KEY: Final[str] = "update" +DELETE_KEY: Final[str] = "delete" -# TODO: при необходимости заведите здесь общий генератор Snowflake-идентификаторов -# для generate_snowflake_id (см. TASK.md) +_SNOWFLAKE_ID_GENERATOR = get_snowflake_id_generator() def generate_snowflake_id() -> int | None: @@ -65,11 +65,12 @@ def generate_snowflake_id() -> int | None: :func:`~src.part5.snowflake.get_snowflake_id_generator` for the cases. """ - # TODO: реализуйте функцию - return 0 + if _SNOWFLAKE_ID_GENERATOR is None: + return None + return _SNOWFLAKE_ID_GENERATOR() -# TODO: оберните операцию декоратором collect_stats +@collect_stats(CREATE_KEY, STATS_REGISTRY) def create_product( storage: dict[int, Product], product_names: set[str], @@ -104,11 +105,30 @@ def create_product( ``product_names`` are left unchanged and an explanatory message is printed (for the last case, by ``generate_id`` itself). """ - # TODO: реализуйте функцию - return 0 + name = normalize_product_name(fields[NAME_KEY]) + if not name: + print("product name must not be blank") + return None -# TODO: оберните операцию декоратором collect_stats + if name in product_names: + print(f"product name '{name}' is already taken") + return None + + product_id = generate_id() + if product_id is None: + return None + + storage[product_id] = { + NAME_KEY: name, + PRICE_KEY: normalize_price(fields[PRICE_KEY]), + QUANTITY_KEY: fields[QUANTITY_KEY], + } + product_names.add(name) + return product_id + + +@collect_stats(UPDATE_KEY, STATS_REGISTRY) def read_product(storage: dict[int, Product], product_id: int) -> Product | None: """Return the product stored under ``product_id``. @@ -120,11 +140,13 @@ def read_product(storage: dict[int, Product], product_id: int) -> Product | None The matching product record, or ``None`` when no product carries that identifier (a message is printed in that case). """ - # TODO: реализуйте функцию (используйте своё решение части 4) - return {} + product = storage.get(product_id) + if product is None: + print(f"no product with id {product_id}") + return product -# TODO: оберните операцию декоратором collect_stats +@collect_stats(UPDATE_KEY, STATS_REGISTRY) def update_product( storage: dict[int, Product], product_names: set[str], @@ -146,9 +168,9 @@ def update_product( one added. product_id: The identifier of the product to change. fields: A dictionary with any of the ``name``, ``price`` and - ``quantity`` keys (see :data:`~src.part5.storage.NAME_KEY`, - :data:`~src.part5.storage.PRICE_KEY` and - :data:`~src.part5.storage.QUANTITY_KEY`) holding the new + ``quantity`` keys (see :data:`~src.part4.storage.NAME_KEY`, + :data:`~src.part4.storage.PRICE_KEY` and + :data:`~src.part4.storage.QUANTITY_KEY`) holding the new values. ``price`` is a :class:`~decimal.Decimal` amount and is rounded to the stored money precision before it is saved. Mutated in place: a ``name`` or ``price`` value present in @@ -162,11 +184,33 @@ def update_product( and an explanatory message is printed. Renaming a product to its own current name is allowed. """ - # TODO: реализуйте функцию (используйте своё решение части 4) - return {} + product = read_product(storage, product_id) + if product is None: + return None + + if NAME_KEY in fields: + name = normalize_product_name(fields[NAME_KEY]) + if not name: + print("product name must not be blank") + return None -# TODO: оберните операцию декоратором collect_stats + if name != product[NAME_KEY] and name in product_names: + print(f"product name '{name}' is already taken") + return None + + fields[NAME_KEY] = name + product_names.discard(product[NAME_KEY]) + product_names.add(name) + + if PRICE_KEY in fields: + fields[PRICE_KEY] = normalize_price(fields[PRICE_KEY]) + + product.update(fields) + return product + + +@collect_stats(UPDATE_KEY, STATS_REGISTRY) def delete_product( storage: dict[int, Product], product_names: set[str], @@ -187,5 +231,10 @@ def delete_product( product carried that identifier (``storage`` and ``product_names`` are left unchanged and a message is printed). """ - # TODO: реализуйте функцию (используйте своё решение части 4) - return 0 + product = read_product(storage, product_id) + if product is None: + return None + + product_names.discard(product[NAME_KEY]) + del storage[product_id] + return product_id diff --git a/src/part5/decorators.py b/src/part5/decorators.py index 21a9f237..28c29625 100644 --- a/src/part5/decorators.py +++ b/src/part5/decorators.py @@ -14,12 +14,12 @@ """ from collections.abc import Callable +from functools import wraps from typing import Final -# TODO: задайте имена двух счётчиков, см. TASK.md -SUCCESS_KEY: Final[str] = "" -FAILS_KEY: Final[str] = "" +SUCCESS_KEY: Final[str] = "success" +FAILS_KEY: Final[str] = "fails" type Stats = dict[str, int] @@ -53,5 +53,22 @@ def collect_stats[**P, R]( ``None``. The decorated function has the same signature, name and docstring as the original (see :func:`functools.wraps`). """ - # TODO: реализуйте декоратор - return lambda function: function + def decorator( + function: Callable[P, R | None], + ) -> Callable[P, R | None]: + if key in registry: + print(f"stats key {key!r} is already registered") + return function + + registry[key] = {SUCCESS_KEY: 0, FAILS_KEY: 0} + + @wraps(function) + def wrapper(*args: P.args, **kwargs: P.kwargs) -> R | None: + result = function(*args, **kwargs) + counter_key = FAILS_KEY if result is None else SUCCESS_KEY + registry[key][counter_key] += 1 + return result + + return wrapper + + return decorator diff --git a/src/part5/snowflake.py b/src/part5/snowflake.py index 64e28468..3c6aa997 100644 --- a/src/part5/snowflake.py +++ b/src/part5/snowflake.py @@ -14,9 +14,18 @@ :func:`decode_node_id` and :func:`decode_sequence_id`. """ +import time from collections.abc import Callable -from .constants import EPOCH_MS_DEFAULT, NODE_ID_DEFAULT +from .constants import ( + EPOCH_MS_DEFAULT, + NODE_ID_DEFAULT, + NODE_ID_MAX, + NODE_ID_SHIFT, + SEQUENCE_ID_MAX, + TIMESTAMP_MS_MAX, + TIMESTAMP_SHIFT, +) def read_current_millis(epoch_ms: int) -> int: @@ -29,39 +38,33 @@ def read_current_millis(epoch_ms: int) -> int: The count of whole milliseconds between ``epoch_ms`` and now. Negative if ``epoch_ms`` lies in the future. """ - # TODO: реализуйте функцию (используйте своё решение части 1) - return 0 + return int(time.time() * 1000) - epoch_ms def decode_timestamp_ms(snowflake_id: int, epoch_ms: int = EPOCH_MS_DEFAULT) -> int: - """Read the timestamp field out of a Snowflake identifier. + """Read the number of milliseconds elapsed since the custom epoch. Args: - snowflake_id: An identifier produced against the same epoch. - epoch_ms: The epoch the identifier was generated against. Defaults to - the original Twitter epoch (2010-11-04 01:42:54.657 UTC). + epoch_ms: The custom epoch expressed as Unix milliseconds. Returns: - The absolute Unix time in milliseconds at which the identifier was - generated. + The count of whole milliseconds between ``epoch_ms`` and now. Negative + if ``epoch_ms`` lies in the future. """ - # TODO: реализуйте функцию (используйте своё решение части 1) - return 0 + return ((snowflake_id >> TIMESTAMP_SHIFT) & TIMESTAMP_MS_MAX) + epoch_ms def decode_node_id(snowflake_id: int) -> int: - """Read the node identifier field out of a Snowflake identifier. + """Read the number of milliseconds elapsed since the custom epoch. Args: - snowflake_id: An identifier produced by a generator from - :func:`get_snowflake_id_generator`. + epoch_ms: The custom epoch expressed as Unix milliseconds. Returns: - The node identifier packed into ``snowflake_id``, in the range - ``[0, NODE_ID_MAX]``. + The count of whole milliseconds between ``epoch_ms`` and now. Negative + if ``epoch_ms`` lies in the future. """ - # TODO: реализуйте функцию (используйте своё решение части 1) - return 0 + return (snowflake_id >> NODE_ID_SHIFT) & NODE_ID_MAX def decode_sequence_id(snowflake_id: int) -> int: @@ -75,8 +78,7 @@ def decode_sequence_id(snowflake_id: int) -> int: The per-millisecond sequence counter packed into ``snowflake_id``, in the range ``[0, SEQUENCE_ID_MAX]``. """ - # TODO: реализуйте функцию (используйте своё решение части 1) - return 0 + return snowflake_id & SEQUENCE_ID_MAX def get_snowflake_id_generator( @@ -108,5 +110,44 @@ def get_snowflake_id_generator( to stdout, if ``node_id`` is outside ``[0, NODE_ID_MAX]`` or ``epoch_ms`` lies in the future. """ - # TODO: реализуйте функцию - return lambda: 0 + if not 0 <= node_id <= NODE_ID_MAX: + print(f"node_id must be in [0, {NODE_ID_MAX}], got {node_id}") + return None + + if read_current_millis(epoch_ms) < 0: + print(f"epoch_ms must not be in the future, got {epoch_ms}") + return None + + last_timestamp = -1 + sequence_id = 0 + + def generate() -> int | None: + nonlocal last_timestamp, sequence_id + + timestamp_ms = read_current_millis(epoch_ms) + if timestamp_ms > last_timestamp: + last_timestamp = timestamp_ms + sequence_id = 0 + else: + timestamp_ms = last_timestamp + sequence_id += 1 + + if sequence_id > SEQUENCE_ID_MAX: + print( + f"sequence_id must be in [0, {SEQUENCE_ID_MAX}], got {sequence_id}" + ) + return None + + if timestamp_ms > TIMESTAMP_MS_MAX: + print( + f"timestamp {timestamp_ms} ms overflows {TIMESTAMP_MS_MAX} ms" + ) + return None + + return ( + (timestamp_ms << TIMESTAMP_SHIFT) + | (node_id << NODE_ID_SHIFT) + | sequence_id + ) + + return generate From e1b3fcd7e136eeff88241ab86efff3a973fca260 Mon Sep 17 00:00:00 2001 From: Lebedev Roman Date: Tue, 10 Nov 2026 20:50:16 +0300 Subject: [PATCH 8/8] Solve task 5 --- src/part5/cart.py | 94 +++++++++++++++++++++++++++-------------- src/part5/decorators.py | 1 + src/part5/run_admin.py | 94 ++++++++++++++++++++++++++++++++--------- src/part5/snowflake.py | 12 ++---- src/part5/storage.py | 7 ++- src/part5/utils.py | 76 +++++++++++++++++++++++---------- 6 files changed, 197 insertions(+), 87 deletions(-) diff --git a/src/part5/cart.py b/src/part5/cart.py index a2363b3a..be74c968 100644 --- a/src/part5/cart.py +++ b/src/part5/cart.py @@ -9,10 +9,8 @@ an explanatory message to stdout and returns ``None``. """ -from .storage import Product - - -# TODO: при необходимости добавьте свои вспомогательные функции +from .crud import read_product, update_product +from .storage import QUANTITY_KEY, Product def add_to_cart( @@ -25,30 +23,41 @@ def add_to_cart( """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.part5.crud.read_product`) and nothing + 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 gains ``quantity`` units of - ``product_id`` -- the product is added when the cart had none of it. + 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. - product_names: The normalised names in ``storage``; threaded - through to :func:`~src.part5.crud.update_product`, which - keeps it in sync (a quantity-only update never renames a - product, so it is left unchanged here). 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 quantity of ``product_id`` in the cart after the addition, or - ``None`` when the product is unknown or the store cannot cover the - request. + The cart line for ``product_id`` after the addition, or ``None`` + when the product is unknown or the store cannot cover the request. """ - # TODO: реализуйте функцию (используйте своё решение части 4) - return 0 + product = read_product(storage, product_id) + if product is None: + return None + + in_stock = product[QUANTITY_KEY] + 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_names, product_id, {QUANTITY_KEY: in_stock - quantity} + ) + + cart[product_id] = cart.get(product_id, 0) + quantity + return cart[product_id] def remove_from_cart( @@ -60,29 +69,52 @@ def remove_from_cart( ) -> int | None: """Move ``quantity`` units of ``product_id`` from ``cart`` back to the store. - If the cart holds no units of ``product_id``, a message is printed and - nothing changes. If it holds fewer units than requested, an explanatory - message is printed and nothing changes. Otherwise the cart loses - ``quantity`` units of the product -- the product is dropped from the - cart when its quantity reaches zero -- and the store record gains - ``quantity`` units back. + 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. - product_names: The normalised names in ``storage``; threaded - through to :func:`~src.part5.crud.update_product`, which - keeps it in sync (a quantity-only update never renames a - product, so it is left unchanged here). 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 quantity of ``product_id`` left in the cart after the removal - (zero means the product was dropped from the cart), or ``None`` when - the cart does not hold the product or holds too few units. + 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: реализуйте функцию (используйте своё решение части 4) - return 0 + in_cart = cart.get(product_id) + if in_cart is None: + print(f"product {product_id} is not in the cart") + return None + + 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_names, + product_id, + {QUANTITY_KEY: product[QUANTITY_KEY] + quantity}, + ) + + left_in_cart = in_cart - quantity + if left_in_cart: + cart[product_id] = left_in_cart + else: + del cart[product_id] + return left_in_cart diff --git a/src/part5/decorators.py b/src/part5/decorators.py index 28c29625..13bb874e 100644 --- a/src/part5/decorators.py +++ b/src/part5/decorators.py @@ -53,6 +53,7 @@ def collect_stats[**P, R]( ``None``. The decorated function has the same signature, name and docstring as the original (see :func:`functools.wraps`). """ + def decorator( function: Callable[P, R | None], ) -> Callable[P, R | None]: diff --git a/src/part5/run_admin.py b/src/part5/run_admin.py index d2e83947..3619b443 100644 --- a/src/part5/run_admin.py +++ b/src/part5/run_admin.py @@ -36,16 +36,36 @@ is left untouched and the loop keeps running; only ``exit`` stops it. """ -from decimal import InvalidOperation +from decimal import Decimal, InvalidOperation from typing import Final -from .storage import Product - - -# TODO: задайте приглашение и текст справки (используйте своё решение части 4, -# не забудьте добавить команду stats, см. TASK.md) -PROMPT: Final[str] = "" -HELP_TEXT: Final[str] = "" +from .crud import ( + STATS_REGISTRY, + create_product, + delete_product, + generate_snowflake_id, + read_product, + update_product, +) +from .storage import NAME_KEY, PRICE_KEY, QUANTITY_KEY, Product +from .utils import get_stats_str_representation, get_storage_str_representation + + +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 + stats print call statistics of the CRUD commands + +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: @@ -55,7 +75,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: реализуйте функцию (используйте своё решение части 4) + print(HELP_TEXT) def print_result(result: object) -> None: @@ -66,7 +86,17 @@ def print_result(result: object) -> None: operation already reported its own failure, so nothing is printed in that case. """ - # TODO: реализуйте функцию (используйте своё решение части 4) + if result is not None: + print(result) + + +def build_fields(name_words: list[str], price: str, quantity: str) -> Product: + """Collect the console arguments of ``create`` or ``update`` into fields.""" + return { + NAME_KEY: " ".join(name_words), + PRICE_KEY: Decimal(price), + QUANTITY_KEY: int(quantity), + } def run_command( @@ -77,8 +107,6 @@ def run_command( Args: storage: The product store shared across the session; mutated in place by the ``create``, ``update`` and ``delete`` commands. - product_names: The normalised names already in ``storage``; kept - in sync by the ``create``, ``update`` and ``delete`` commands. line: One line of console input, already stripped of surrounding whitespace. @@ -88,24 +116,48 @@ def run_command( known command prints a notice and still returns ``True``. Raises: - ValueError: If the id or quantity argument of a CRUD command does + 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 + match line.split(): + case ["help"]: + show_help() + case ["exit"]: + return False + case ["show"]: + print(get_storage_str_representation(storage)) + case ["stats"]: + print(get_stats_str_representation(STATS_REGISTRY)) + case ["create", *name_words, price, quantity] if name_words: + fields = build_fields(name_words, price, quantity) + print_result( + create_product(storage, product_names, fields, generate_snowflake_id) + ) + case ["read", product_id]: + print_result(read_product(storage, int(product_id))) + case ["update", product_id, *name_words, price, quantity] if name_words: + fields = build_fields(name_words, price, quantity) + print_result( + update_product(storage, product_names, int(product_id), fields) + ) + case ["delete", product_id]: + print_result(delete_product(storage, product_names, int(product_id))) + case _: + print(f"'{line}' is not a command") + return True def main() -> None: """Run the admin console until the ``exit`` command is entered. - A single product store, and the set of normalised names it holds, are - created empty and kept in memory for the whole session. Each iteration - reads one line from stdin, strips its surrounding whitespace and hands - it to :func:`run_command`. A line that names a command but whose price - or quantity argument does not parse is caught here and reported - without stopping the loop; only ``exit`` ends it. + A single product store is created empty and kept in memory for the + whole session. Each iteration reads one line from stdin, strips its + surrounding whitespace and hands it to :func:`run_command`. A line + that names a command but whose id, price or quantity argument does + not parse is caught here and reported without stopping the loop; only + ``exit`` ends it. """ storage: dict[int, Product] = {} product_names: set[str] = set() diff --git a/src/part5/snowflake.py b/src/part5/snowflake.py index 3c6aa997..04f0109e 100644 --- a/src/part5/snowflake.py +++ b/src/part5/snowflake.py @@ -133,21 +133,15 @@ def generate() -> int | None: sequence_id += 1 if sequence_id > SEQUENCE_ID_MAX: - print( - f"sequence_id must be in [0, {SEQUENCE_ID_MAX}], got {sequence_id}" - ) + print(f"sequence_id must be in [0, {SEQUENCE_ID_MAX}], got {sequence_id}") return None if timestamp_ms > TIMESTAMP_MS_MAX: - print( - f"timestamp {timestamp_ms} ms overflows {TIMESTAMP_MS_MAX} ms" - ) + print(f"timestamp {timestamp_ms} ms overflows {TIMESTAMP_MS_MAX} ms") return None return ( - (timestamp_ms << TIMESTAMP_SHIFT) - | (node_id << NODE_ID_SHIFT) - | sequence_id + (timestamp_ms << TIMESTAMP_SHIFT) | (node_id << NODE_ID_SHIFT) | sequence_id ) return generate diff --git a/src/part5/storage.py b/src/part5/storage.py index fa8ef3bb..e245929d 100644 --- a/src/part5/storage.py +++ b/src/part5/storage.py @@ -38,7 +38,6 @@ type Product = dict[str, Any] -# TODO: задайте имена полей записи (используйте своё решение части 4) -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/part5/utils.py b/src/part5/utils.py index b2ad4dc4..6b4e1acb 100644 --- a/src/part5/utils.py +++ b/src/part5/utils.py @@ -21,23 +21,22 @@ look, with the success rate of every command as a percentage. """ -from decimal import Decimal +from decimal import ROUND_HALF_UP, Decimal from typing import Final -from .decorators import Registry -from .storage import Product +from .decorators import FAILS_KEY, SUCCESS_KEY, Registry +from .storage import NAME_KEY, PRICE_KEY, QUANTITY_KEY, Product -# TODO: задайте число знаков после запятой и шаг квантования (используйте своё -# решение части 4) -PRICE_PRECISION: Final[int] = 0 -PRICE_STEP: Final = Decimal(0) - - -# TODO: задайте заголовки столбцов таблицы (используйте своё решение части 4) -TABLE_HEADERS: Final[tuple[str, ...]] = () -# TODO: задайте заголовки столбцов таблицы статистики, см. TASK.md -STATS_TABLE_HEADERS: Final[tuple[str, ...]] = () +PRICE_PRECISION: Final[int] = 2 +PRICE_STEP: Final = Decimal(1).scaleb(-PRICE_PRECISION) +TABLE_HEADERS: Final[tuple[str, ...]] = ("ID", "name", "price", "quantity") +STATS_TABLE_HEADERS: Final[tuple[str, ...]] = ( + "command name", + "success", + "fails", + "success rate", +) def normalize_price(price: Decimal) -> Decimal: @@ -50,8 +49,7 @@ def normalize_price(price: Decimal) -> Decimal: ``price`` quantised to :data:`PRICE_PRECISION` fractional digits, with halves rounded up. """ - # TODO: реализуйте функцию (используйте своё решение части 4) - return Decimal(0) + return price.quantize(PRICE_STEP, rounding=ROUND_HALF_UP) def normalize_product_name(name: str) -> str: @@ -68,11 +66,30 @@ def normalize_product_name(name: str) -> str: The result is an empty string when ``name`` holds nothing but whitespace. """ - # TODO: реализуйте функцию (используйте своё решение части 4) - return "" + return " ".join(name.split()).lower() + + +def _format_table(rows: list[tuple[str, ...]]) -> str: + widths = [max(len(row[column]) for row in rows) for column in range(len(rows[0]))] + + result = [] + line = "|" + for i, header in enumerate(rows[0]): + line += " " + header.ljust(widths[i]) + " |" + result.append(line) + + line = "|" + for width in widths: + line += "-" * (width + 2) + "|" + result.append(line) + for row in rows[1:]: + line = "|" + for i, value in enumerate(row): + line += " " + value.ljust(widths[i]) + " |" + result.append(line) -# TODO: при необходимости добавьте свои вспомогательные функции + return "\n".join(result) def get_storage_str_representation(storage: dict[int, Product]) -> str: @@ -93,8 +110,17 @@ def get_storage_str_representation(storage: dict[int, Product]) -> str: ``storage`` is empty only the header and separator rows are returned, sized to the header labels. """ - # TODO: реализуйте функцию (используйте своё решение части 4) - return "" + rows: list[tuple[str, ...]] = [TABLE_HEADERS] + for product_id, product in storage.items(): + rows.append( + ( + str(product_id), + str(product[NAME_KEY]), + str(product[PRICE_KEY]), + str(product[QUANTITY_KEY]), + ) + ) + return _format_table(rows) def get_stats_str_representation(registry: Registry) -> str: @@ -116,5 +142,11 @@ def get_stats_str_representation(registry: Registry) -> str: empty only the header and separator rows are returned, sized to the header labels. """ - # TODO: реализуйте функцию - return "" + rows: list[tuple[str, ...]] = [STATS_TABLE_HEADERS] + for key, stats in registry.items(): + success = stats[SUCCESS_KEY] + fails = stats[FAILS_KEY] + total = success + fails + success_rate = success / total * 100 if total else 0 + rows.append((key, str(success), str(fails), f"{success_rate:.2f}%")) + return _format_table(rows)