From d7c88f38f9a06a724c401dbdf5e6db09bf297197 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 25 Sep 2026 21:48:48 +0300 Subject: [PATCH 01/19] Solve task 1 --- src/part1/constants.py | 48 ++++------------ src/part1/snowflake.py | 122 +++++++---------------------------------- 2 files changed, 29 insertions(+), 141 deletions(-) diff --git a/src/part1/constants.py b/src/part1/constants.py index 9abfbba2..86e0499a 100644 --- a/src/part1/constants.py +++ b/src/part1/constants.py @@ -1,38 +1,10 @@ -"""Bit layout and default epoch for the Twitter Snowflake ID generator. - -A Snowflake identifier is a 63-bit positive integer packed as follows, from -the most significant bit downwards: - - 1 bit unused sign bit, always zero - 41 bits milliseconds elapsed since a custom epoch - 10 bits node identifier - 12 bits per-millisecond sequence counter - -The shift and mask constants should be derived from the bit widths so that -changing a width keeps everything else consistent. -""" - -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] = 0 - -# Node identifier used when the caller does not supply one. -NODE_ID_DEFAULT: Final[int] = 0 - -# Width of each field, in bits. -TIMESTAMP_BITS: Final[int] = 0 -NODE_ID_BITS: Final[int] = 0 -SEQUENCE_ID_BITS: Final[int] = 0 - -# Largest value each field can hold. -TIMESTAMP_MS_MAX: Final[int] = 0 -NODE_ID_MAX: Final[int] = 0 -SEQUENCE_ID_MAX: Final[int] = 0 - -# Здесь можно добавить собственные вспомогательные константы -# (например, сдвиги полей при сборке идентификатора). +EPOCH_MS_DEFAULT = 1288834974657 +NODE_ID_DEFAULT = 1 +TIMESTAMP_BITS = 41 +NODE_ID_BITS = 10 +SEQUENCE_ID_BITS = 12 +TIMESTAMP_MS_MAX = 2 ** TIMESTAMP_BITS - 1 +NODE_ID_MAX = 2 ** NODE_ID_BITS - 1 +SEQUENCE_ID_MAX = 2 ** SEQUENCE_ID_BITS - 1 +NODE_ID_SHIFT = SEQUENCE_ID_BITS +TIMESTAMP_SHIFT = SEQUENCE_ID_BITS + NODE_ID_BITS \ No newline at end of file diff --git a/src/part1/snowflake.py b/src/part1/snowflake.py index d3abf050..14dca85b 100644 --- a/src/part1/snowflake.py +++ b/src/part1/snowflake.py @@ -1,114 +1,30 @@ -"""Twitter Snowflake identifier generator. - -Snowflake produces roughly time-ordered 64-bit integer identifiers without -coordination between nodes. Each identifier packs the milliseconds elapsed -since a custom epoch, a node identifier and a per-millisecond sequence counter -into a single 63-bit positive integer (see ``constants`` for the layout). - -The public entry point is :func:`generate_snowflake_id`. It is stateless: the -caller passes the sequence counter on every call and is responsible for -advancing it within a millisecond and resetting it when the clock ticks over. - -Each packed field can be read back on its own with :func:`decode_timestamp_ms`, -:func:`decode_node_id` and :func:`decode_sequence_id`. -""" - -import time # noqa: F401 - -from .constants import ( # noqa: F401 - EPOCH_MS_DEFAULT, - NODE_ID_DEFAULT, - NODE_ID_MAX, - SEQUENCE_ID_MAX, - TIMESTAMP_MS_MAX, -) +import time +from constants import EPOCH_MS_DEFAULT ,NODE_ID_DEFAULT, TIMESTAMP_BITS ,NODE_ID_BITS , \ +SEQUENCE_ID_BITS ,TIMESTAMP_MS_MAX ,NODE_ID_MAX ,SEQUENCE_ID_MAX ,NODE_ID_SHIFT ,TIMESTAMP_SHIFT def read_current_millis(epoch_ms: int) -> int: - """Read the number of milliseconds elapsed since the custom epoch. - - Args: - epoch_ms: The custom epoch expressed as Unix milliseconds. - - Returns: - The count of whole milliseconds between ``epoch_ms`` and now. May be - negative if ``epoch_ms`` lies in the future. - """ - # TODO: реализуйте функцию - 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. - - 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). - - Returns: - The absolute Unix time in milliseconds at which the identifier was - generated. - """ - # TODO: реализуйте функцию - return 0 + return snowflake_id // (2 ** TIMESTAMP_SHIFT) + epoch_ms -def decode_node_id(snowflake_id: int) -> int: - """Read the node identifier field out of a Snowflake identifier. - - Args: - snowflake_id: An identifier produced by :func:`generate_snowflake_id`. - - Returns: - The node identifier packed into ``snowflake_id``, in the range - ``[0, NODE_ID_MAX]``. - """ - # TODO: реализуйте функцию - return 0 +def decode_node_id(snowflake_id: int, epoch_ms: int = EPOCH_MS_DEFAULT) -> int: + return (snowflake_id - ((decode_timestamp_ms(snowflake_id, epoch_ms) - epoch_ms) * 2 ** TIMESTAMP_SHIFT)) // (2 ** NODE_ID_SHIFT) def decode_sequence_id(snowflake_id: int) -> int: - """Read the sequence counter field out of a Snowflake identifier. - - Args: - snowflake_id: An identifier produced by :func:`generate_snowflake_id`. - - Returns: - The per-millisecond sequence counter packed into ``snowflake_id``, in - the range ``[0, SEQUENCE_ID_MAX]``. - """ - # TODO: реализуйте функцию - return 0 - - -def generate_snowflake_id( - sequence_id: int, - node_id: int = NODE_ID_DEFAULT, - epoch_ms: int = EPOCH_MS_DEFAULT, -) -> int | None: - """Build and return a Snowflake identifier for the current millisecond. - - The function is stateless: the caller passes the per-millisecond sequence - counter explicitly. It is the caller's responsibility to increment - ``sequence_id`` for identifiers minted within the same millisecond and to - reset it once the clock advances. - - Args: - sequence_id: The per-millisecond sequence counter, in the range - ``[0, SEQUENCE_ID_MAX]``. - node_id: The identifier of this node, in the range ``[0, NODE_ID_MAX]``. - Optional; defaults to ``NODE_ID_DEFAULT``. - epoch_ms: The start of the epoch as Unix milliseconds. Optional; - defaults to the original Twitter epoch (2010-11-04 01:42:54.657 - UTC). - - Returns: - The Snowflake identifier as a positive 63-bit integer, or ``None`` if - ``node_id`` is outside ``[0, NODE_ID_MAX]``, ``sequence_id`` is outside - ``[0, SEQUENCE_ID_MAX]``, or the elapsed time no longer fits in the - timestamp field (roughly 69 years after ``epoch_ms``). In each of those - cases an explanatory message is printed to stdout first. - """ - # TODO: реализуйте функцию - return 0 + return snowflake_id - ((snowflake_id // 2 ** NODE_ID_SHIFT) * (2 ** NODE_ID_SHIFT)) + + +def generate_snowflake_id(sequence_id: int, node_id: int = NODE_ID_DEFAULT, epoch_ms: int = EPOCH_MS_DEFAULT) -> int | None: + current_millis = read_current_millis(epoch_ms) + if current_millis < 0 or current_millis > TIMESTAMP_MS_MAX: + raise ValueError('Превышен максимальный лимит времени') + if node_id < 0 or node_id > NODE_ID_MAX: + raise ValueError('Node Id выходит за допустимые пределы') + if sequence_id < 0 or sequence_id > SEQUENCE_ID_MAX: + raise ValueError('Sequence Id выходит за допустимые пределы') + return (current_millis * (2 ** TIMESTAMP_SHIFT)) + (node_id * (2 ** NODE_ID_SHIFT) + sequence_id) \ No newline at end of file From 86c89c4bc3d387310bc242c0af8de47940514e49 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 25 Sep 2026 22:09:56 +0300 Subject: [PATCH 02/19] Solve task 1 --- src/part1/snowflake.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/part1/snowflake.py b/src/part1/snowflake.py index 14dca85b..0d14a824 100644 --- a/src/part1/snowflake.py +++ b/src/part1/snowflake.py @@ -1,6 +1,14 @@ import time -from constants import EPOCH_MS_DEFAULT ,NODE_ID_DEFAULT, TIMESTAMP_BITS ,NODE_ID_BITS , \ -SEQUENCE_ID_BITS ,TIMESTAMP_MS_MAX ,NODE_ID_MAX ,SEQUENCE_ID_MAX ,NODE_ID_SHIFT ,TIMESTAMP_SHIFT + +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: @@ -22,9 +30,9 @@ def decode_sequence_id(snowflake_id: int) -> int: def generate_snowflake_id(sequence_id: int, node_id: int = NODE_ID_DEFAULT, epoch_ms: int = EPOCH_MS_DEFAULT) -> int | None: current_millis = read_current_millis(epoch_ms) if current_millis < 0 or current_millis > TIMESTAMP_MS_MAX: - raise ValueError('Превышен максимальный лимит времени') + raise ValueError("Превышен максимальный лимит времени") if node_id < 0 or node_id > NODE_ID_MAX: - raise ValueError('Node Id выходит за допустимые пределы') + raise ValueError("Node Id выходит за допустимые пределы") if sequence_id < 0 or sequence_id > SEQUENCE_ID_MAX: - raise ValueError('Sequence Id выходит за допустимые пределы') + raise ValueError("Sequence Id выходит за допустимые пределы") return (current_millis * (2 ** TIMESTAMP_SHIFT)) + (node_id * (2 ** NODE_ID_SHIFT) + sequence_id) \ No newline at end of file From 7bab98912b921a3534229157789ad5614e12798e Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 25 Sep 2026 22:17:25 +0300 Subject: [PATCH 03/19] Solve task 1 --- src/part1/constants.py | 8 ++++---- src/part1/snowflake.py | 20 +++++++++++++++----- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/part1/constants.py b/src/part1/constants.py index 86e0499a..faee44a5 100644 --- a/src/part1/constants.py +++ b/src/part1/constants.py @@ -3,8 +3,8 @@ TIMESTAMP_BITS = 41 NODE_ID_BITS = 10 SEQUENCE_ID_BITS = 12 -TIMESTAMP_MS_MAX = 2 ** TIMESTAMP_BITS - 1 -NODE_ID_MAX = 2 ** NODE_ID_BITS - 1 -SEQUENCE_ID_MAX = 2 ** SEQUENCE_ID_BITS - 1 +TIMESTAMP_MS_MAX = 2**TIMESTAMP_BITS - 1 +NODE_ID_MAX = 2**NODE_ID_BITS - 1 +SEQUENCE_ID_MAX = 2**SEQUENCE_ID_BITS - 1 NODE_ID_SHIFT = SEQUENCE_ID_BITS -TIMESTAMP_SHIFT = SEQUENCE_ID_BITS + NODE_ID_BITS \ No newline at end of file +TIMESTAMP_SHIFT = SEQUENCE_ID_BITS + NODE_ID_BITS diff --git a/src/part1/snowflake.py b/src/part1/snowflake.py index 0d14a824..19a4f087 100644 --- a/src/part1/snowflake.py +++ b/src/part1/snowflake.py @@ -16,18 +16,26 @@ def read_current_millis(epoch_ms: int) -> int: def decode_timestamp_ms(snowflake_id: int, epoch_ms: int = EPOCH_MS_DEFAULT) -> int: - return snowflake_id // (2 ** TIMESTAMP_SHIFT) + epoch_ms + return snowflake_id // (2**TIMESTAMP_SHIFT) + epoch_ms def decode_node_id(snowflake_id: int, epoch_ms: int = EPOCH_MS_DEFAULT) -> int: - return (snowflake_id - ((decode_timestamp_ms(snowflake_id, epoch_ms) - epoch_ms) * 2 ** TIMESTAMP_SHIFT)) // (2 ** NODE_ID_SHIFT) + return ( + snowflake_id + - ( + (decode_timestamp_ms(snowflake_id, epoch_ms) - epoch_ms) + * 2**TIMESTAMP_SHIFT + ) + ) // (2**NODE_ID_SHIFT) def decode_sequence_id(snowflake_id: int) -> int: - return snowflake_id - ((snowflake_id // 2 ** NODE_ID_SHIFT) * (2 ** NODE_ID_SHIFT)) + return snowflake_id - ((snowflake_id // 2**NODE_ID_SHIFT) * (2**NODE_ID_SHIFT)) -def generate_snowflake_id(sequence_id: int, node_id: int = NODE_ID_DEFAULT, epoch_ms: int = EPOCH_MS_DEFAULT) -> int | None: +def generate_snowflake_id( + sequence_id: int, node_id: int = NODE_ID_DEFAULT, epoch_ms: int = EPOCH_MS_DEFAULT +) -> int | None: current_millis = read_current_millis(epoch_ms) if current_millis < 0 or current_millis > TIMESTAMP_MS_MAX: raise ValueError("Превышен максимальный лимит времени") @@ -35,4 +43,6 @@ def generate_snowflake_id(sequence_id: int, node_id: int = NODE_ID_DEFAULT, epoc raise ValueError("Node Id выходит за допустимые пределы") if sequence_id < 0 or sequence_id > SEQUENCE_ID_MAX: raise ValueError("Sequence Id выходит за допустимые пределы") - return (current_millis * (2 ** TIMESTAMP_SHIFT)) + (node_id * (2 ** NODE_ID_SHIFT) + sequence_id) \ No newline at end of file + return (current_millis * (2**TIMESTAMP_SHIFT)) + ( + node_id * (2**NODE_ID_SHIFT) + sequence_id + ) From c552b886236370d819d7c344e0a85dc71c19724d Mon Sep 17 00:00:00 2001 From: phantom686 Date: Fri, 25 Sep 2026 22:27:53 +0300 Subject: [PATCH 04/19] Update snowflake.py --- src/part1/snowflake.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/part1/snowflake.py b/src/part1/snowflake.py index 19a4f087..ae1e7ee5 100644 --- a/src/part1/snowflake.py +++ b/src/part1/snowflake.py @@ -1,6 +1,6 @@ import time -from constants import ( +from .constants import ( EPOCH_MS_DEFAULT, NODE_ID_DEFAULT, NODE_ID_MAX, From 1e23410bc949fbef2638a3e25b21ffcf061e79a8 Mon Sep 17 00:00:00 2001 From: phantom686 Date: Fri, 25 Sep 2026 22:35:25 +0300 Subject: [PATCH 05/19] Update constants.py --- src/part1/constants.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/part1/constants.py b/src/part1/constants.py index faee44a5..960e4b82 100644 --- a/src/part1/constants.py +++ b/src/part1/constants.py @@ -1,10 +1,10 @@ -EPOCH_MS_DEFAULT = 1288834974657 -NODE_ID_DEFAULT = 1 -TIMESTAMP_BITS = 41 -NODE_ID_BITS = 10 -SEQUENCE_ID_BITS = 12 -TIMESTAMP_MS_MAX = 2**TIMESTAMP_BITS - 1 -NODE_ID_MAX = 2**NODE_ID_BITS - 1 -SEQUENCE_ID_MAX = 2**SEQUENCE_ID_BITS - 1 -NODE_ID_SHIFT = SEQUENCE_ID_BITS -TIMESTAMP_SHIFT = SEQUENCE_ID_BITS + NODE_ID_BITS +EPOCH_MS_DEFAULT: int = 1288834974657 +NODE_ID_DEFAULT: int = 1 +TIMESTAMP_BITS: int = 41 +NODE_ID_BITS: int = 10 +SEQUENCE_ID_BITS: int = 12 +TIMESTAMP_MS_MAX: int = 2**TIMESTAMP_BITS - 1 +NODE_ID_MAX: int = 2**NODE_ID_BITS - 1 +SEQUENCE_ID_MAX: int = 2**SEQUENCE_ID_BITS - 1 +NODE_ID_SHIFT: int = SEQUENCE_ID_BITS +TIMESTAMP_SHIFT: int = SEQUENCE_ID_BITS + NODE_ID_BITS From 3a6dc3dfa928505efc4315c091d3699c0de1bf4a Mon Sep 17 00:00:00 2001 From: phantom686 Date: Fri, 25 Sep 2026 22:40:01 +0300 Subject: [PATCH 06/19] Update snowflake.py --- src/part1/snowflake.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/part1/snowflake.py b/src/part1/snowflake.py index ae1e7ee5..80808ec6 100644 --- a/src/part1/snowflake.py +++ b/src/part1/snowflake.py @@ -12,25 +12,25 @@ def read_current_millis(epoch_ms: int) -> int: - return int(time.time() * 1000) - epoch_ms + return int(int(time.time() * 1000) - epoch_ms) def decode_timestamp_ms(snowflake_id: int, epoch_ms: int = EPOCH_MS_DEFAULT) -> int: - return snowflake_id // (2**TIMESTAMP_SHIFT) + epoch_ms + return int(snowflake_id // (2**TIMESTAMP_SHIFT) + epoch_ms) def decode_node_id(snowflake_id: int, epoch_ms: int = EPOCH_MS_DEFAULT) -> int: - return ( + return int(( snowflake_id - ( (decode_timestamp_ms(snowflake_id, epoch_ms) - epoch_ms) * 2**TIMESTAMP_SHIFT ) - ) // (2**NODE_ID_SHIFT) + ) // (2**NODE_ID_SHIFT)) def decode_sequence_id(snowflake_id: int) -> int: - return snowflake_id - ((snowflake_id // 2**NODE_ID_SHIFT) * (2**NODE_ID_SHIFT)) + return int(snowflake_id - ((snowflake_id // 2**NODE_ID_SHIFT) * (2**NODE_ID_SHIFT))) def generate_snowflake_id( @@ -43,6 +43,6 @@ def generate_snowflake_id( raise ValueError("Node Id выходит за допустимые пределы") if sequence_id < 0 or sequence_id > SEQUENCE_ID_MAX: raise ValueError("Sequence Id выходит за допустимые пределы") - return (current_millis * (2**TIMESTAMP_SHIFT)) + ( + return int((current_millis * (2**TIMESTAMP_SHIFT)) + ( node_id * (2**NODE_ID_SHIFT) + sequence_id - ) + )) From edd79d85900a747996b88077776f8c82fac6f9c3 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 25 Sep 2026 22:49:29 +0300 Subject: [PATCH 07/19] Solve task 1 --- pl-001-python-basics-task | 1 + src/part1/snowflake.py | 10 ++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) create mode 160000 pl-001-python-basics-task diff --git a/pl-001-python-basics-task b/pl-001-python-basics-task new file mode 160000 index 00000000..3a6dc3df --- /dev/null +++ b/pl-001-python-basics-task @@ -0,0 +1 @@ +Subproject commit 3a6dc3dfa928505efc4315c091d3699c0de1bf4a diff --git a/src/part1/snowflake.py b/src/part1/snowflake.py index 19a4f087..8fef484d 100644 --- a/src/part1/snowflake.py +++ b/src/part1/snowflake.py @@ -1,12 +1,14 @@ import time - -from constants import ( +from .constants import ( EPOCH_MS_DEFAULT, NODE_ID_DEFAULT, + TIMESTAMP_BITS, + NODE_ID_BITS, + SEQUENCE_ID_BITS, + TIMESTAMP_MS_MAX, NODE_ID_MAX, - NODE_ID_SHIFT, SEQUENCE_ID_MAX, - TIMESTAMP_MS_MAX, + NODE_ID_SHIFT, TIMESTAMP_SHIFT, ) From d173d13d14552d4914e10aa5deb2d83e1979a729 Mon Sep 17 00:00:00 2001 From: phantom686 Date: Fri, 25 Sep 2026 23:16:30 +0300 Subject: [PATCH 08/19] Update snowflake.py --- src/part1/snowflake.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/part1/snowflake.py b/src/part1/snowflake.py index e2f93e26..b3559d66 100644 --- a/src/part1/snowflake.py +++ b/src/part1/snowflake.py @@ -1,8 +1,4 @@ import time -<<<<<<< HEAD -======= - ->>>>>>> 3a6dc3dfa928505efc4315c091d3699c0de1bf4a from .constants import ( EPOCH_MS_DEFAULT, NODE_ID_DEFAULT, From d6563495528fe1bc8ceb96ec961604b3a7e32abe Mon Sep 17 00:00:00 2001 From: phantom686 Date: Fri, 25 Sep 2026 23:19:57 +0300 Subject: [PATCH 09/19] Update snowflake.py --- src/part1/snowflake.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/part1/snowflake.py b/src/part1/snowflake.py index b3559d66..f9bdf778 100644 --- a/src/part1/snowflake.py +++ b/src/part1/snowflake.py @@ -2,9 +2,6 @@ from .constants import ( EPOCH_MS_DEFAULT, NODE_ID_DEFAULT, - TIMESTAMP_BITS, - NODE_ID_BITS, - SEQUENCE_ID_BITS, TIMESTAMP_MS_MAX, NODE_ID_MAX, SEQUENCE_ID_MAX, From ff338aabc03a2e7b958e079293064586d3c0437c Mon Sep 17 00:00:00 2001 From: phantom686 Date: Fri, 25 Sep 2026 23:25:57 +0300 Subject: [PATCH 10/19] Update snowflake.py --- src/part1/snowflake.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/part1/snowflake.py b/src/part1/snowflake.py index f9bdf778..6518cb38 100644 --- a/src/part1/snowflake.py +++ b/src/part1/snowflake.py @@ -2,10 +2,10 @@ from .constants import ( EPOCH_MS_DEFAULT, NODE_ID_DEFAULT, - TIMESTAMP_MS_MAX, NODE_ID_MAX, - SEQUENCE_ID_MAX, NODE_ID_SHIFT, + SEQUENCE_ID_MAX, + TIMESTAMP_MS_MAX, TIMESTAMP_SHIFT, ) From bf59011930c5c7b174a582391fc83a1ede03085b Mon Sep 17 00:00:00 2001 From: phantom686 Date: Fri, 25 Sep 2026 23:29:12 +0300 Subject: [PATCH 11/19] Update snowflake.py --- src/part1/snowflake.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/part1/snowflake.py b/src/part1/snowflake.py index 6518cb38..80808ec6 100644 --- a/src/part1/snowflake.py +++ b/src/part1/snowflake.py @@ -1,4 +1,5 @@ import time + from .constants import ( EPOCH_MS_DEFAULT, NODE_ID_DEFAULT, From 66427ec52008d2bcc2c7fd329af847fcf8b63da3 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 25 Sep 2026 23:37:34 +0300 Subject: [PATCH 12/19] jj --- src/part1/snowflake.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/part1/snowflake.py b/src/part1/snowflake.py index 80808ec6..01d46be6 100644 --- a/src/part1/snowflake.py +++ b/src/part1/snowflake.py @@ -20,13 +20,16 @@ def decode_timestamp_ms(snowflake_id: int, epoch_ms: int = EPOCH_MS_DEFAULT) -> def decode_node_id(snowflake_id: int, epoch_ms: int = EPOCH_MS_DEFAULT) -> int: - return int(( - snowflake_id - - ( - (decode_timestamp_ms(snowflake_id, epoch_ms) - epoch_ms) - * 2**TIMESTAMP_SHIFT + return int( + ( + snowflake_id + - ( + (decode_timestamp_ms(snowflake_id, epoch_ms) - epoch_ms) + * 2**TIMESTAMP_SHIFT + ) ) - ) // (2**NODE_ID_SHIFT)) + // (2**NODE_ID_SHIFT) + ) def decode_sequence_id(snowflake_id: int) -> int: @@ -43,6 +46,7 @@ def generate_snowflake_id( raise ValueError("Node Id выходит за допустимые пределы") if sequence_id < 0 or sequence_id > SEQUENCE_ID_MAX: raise ValueError("Sequence Id выходит за допустимые пределы") - return int((current_millis * (2**TIMESTAMP_SHIFT)) + ( - node_id * (2**NODE_ID_SHIFT) + sequence_id - )) + return int( + (current_millis * (2**TIMESTAMP_SHIFT)) + + (node_id * (2**NODE_ID_SHIFT) + sequence_id) + ) From 98660af7722d311e01597f77664e7e685ab3f902 Mon Sep 17 00:00:00 2001 From: phantom686 Date: Sun, 1 Nov 2026 19:27:24 +0300 Subject: [PATCH 13/19] task4 --- pl-001-python-basics-task | 1 - 1 file changed, 1 deletion(-) delete mode 160000 pl-001-python-basics-task diff --git a/pl-001-python-basics-task b/pl-001-python-basics-task deleted file mode 160000 index 3a6dc3df..00000000 --- a/pl-001-python-basics-task +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3a6dc3dfa928505efc4315c091d3699c0de1bf4a From c065c3030cf85f130bc61e438b784a83ce95eecb Mon Sep 17 00:00:00 2001 From: phantom686 Date: Sun, 1 Nov 2026 19:28:24 +0300 Subject: [PATCH 14/19] task4 --- src/part4/cart.py | 97 +++++++-------------- src/part4/crud.py | 192 +++++++++++++++-------------------------- src/part4/run_admin.py | 177 ++++++++++++++++++------------------- src/part4/storage.py | 44 +--------- src/part4/utils.py | 115 +++++++----------------- 5 files changed, 220 insertions(+), 405 deletions(-) diff --git a/src/part4/cart.py b/src/part4/cart.py index aed9bb72..4ef1c35f 100644 --- a/src/part4/cart.py +++ b/src/part4/cart.py @@ -1,18 +1,4 @@ -"""Shopping-cart operations layered on top of the product store. - -The cart is a plain dictionary mapping each product identifier to the -number of units of that product in the cart. Moving units between the -store and the cart keeps the two sides in balance: :func:`add_to_cart` takes -units out of stock, :func:`remove_from_cart` puts them back. - -Like the CRUD layer, the failure path never raises -- the operation prints -an explanatory message to stdout and returns ``None``. -""" - -from .storage import Product - - -# TODO: при необходимости добавьте свои вспомогательные функции +from .storage import QUANTITY_KEY, Product def add_to_cart( @@ -22,32 +8,18 @@ def add_to_cart( product_id: str, quantity: int, ) -> int | None: - """Move ``quantity`` units of ``product_id`` from the store into ``cart``. + if product_id not in storage: + print(f"no product with id {product_id}") + return None - 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. + available = storage[product_id][QUANTITY_KEY] + if available < quantity: + print(f"not enough stock for product {product_id}: {available} available, {quantity} requested") + return 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. - """ - # TODO: реализуйте функцию - return 0 + storage[product_id][QUANTITY_KEY] -= quantity + cart[product_id] = cart.get(product_id, 0) + quantity + return cart[product_id] def remove_from_cart( @@ -57,31 +29,22 @@ def remove_from_cart( product_id: str, quantity: int, ) -> 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. - - 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. - """ - # TODO: реализуйте функцию - return 0 + if product_id not in cart: + print(f"product {product_id} is not in the cart") + return None + + in_cart = cart[product_id] + if in_cart < quantity: + print(f"cart holds only {in_cart} unit(s) of product {product_id}, cannot remove {quantity}") + return None + + if product_id not in storage: + print(f"no product with id {product_id}") + return None + + storage[product_id][QUANTITY_KEY] += quantity + cart[product_id] -= quantity + remaining = cart[product_id] + if remaining == 0: + del cart[product_id] + return remaining \ No newline at end of file diff --git a/src/part4/crud.py b/src/part4/crud.py index 471eff00..a2773a98 100644 --- a/src/part4/crud.py +++ b/src/part4/crud.py @@ -1,48 +1,17 @@ -"""Create/read/update/delete operations over the in-memory product store. - -Every operation takes the store -- a dictionary mapping product id to its -:data:`~src.part4.storage.Product` record -- as its first argument and -works on it in place. ``create_product``, ``update_product`` and -``delete_product`` additionally take ``product_names``, the ``set[str]`` -of normalised names currently in ``storage`` (see -:mod:`src.part4.storage`), and keep it in sync with ``storage`` on -every successful call. The failure path never raises: the operation -prints an explanatory message to stdout and returns ``None``. - -The identifier of a new product is a freshly generated UUID string -(:func:`generate_product_id`), independent of insertion order or of any -identifier already in ``storage``. - -Every product name that reaches the store is passed through -:func:`~src.part4.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 -- via an O(1) membership test against ``product_names`` rather -than a scan over ``storage.values()``. -""" - -import uuid # noqa: F401 +import uuid from typing import Any -from .storage import ( # noqa: F401 +from .storage import ( NAME_KEY, PRICE_KEY, QUANTITY_KEY, Product, ) -from .utils import normalize_price, normalize_product_name # noqa: F401 +from .utils import normalize_price, normalize_product_name def generate_product_id() -> str: - """Generate an identifier for a new product. - - Returns: - A freshly generated UUID, stringified. - """ - # TODO: реализуйте функцию - return "" + return str(uuid.uuid4()) def create_product( @@ -50,46 +19,35 @@ def create_product( product_names: set[str], fields: dict[str, Any], ) -> str | None: - """Add a new product to ``storage`` and return its new identifier. - - Args: - storage: The product store to add to; modified in place on - success. - product_names: The normalised names already in ``storage``; the - new name is added to it in place on success. - fields: A dictionary with the ``name``, ``price`` and ``quantity`` - keys (see :data:`~src.part4.storage.NAME_KEY`, - :data:`~src.part4.storage.PRICE_KEY` and - :data:`~src.part4.storage.QUANTITY_KEY`) 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`` - and ``product_names`` are left unchanged and an explanatory - message is printed. - """ - # TODO: реализуйте функцию - return "" + name = normalize_product_name(fields[NAME_KEY]) + fields[NAME_KEY] = name + if not name: + print("product name must not be blank") + return None -def read_product(storage: dict[str, Product], product_id: str) -> Product | None: - """Return the product stored under ``product_id``. + if name in product_names: + print(f"product name '{name}' is already taken") + return None + + norm_price = normalize_price(fields[PRICE_KEY]) + fields[PRICE_KEY] = norm_price - Args: - storage: The product store to search. - product_id: The identifier to look up. + product_id = generate_product_id() + storage[product_id] = { + NAME_KEY: name, + PRICE_KEY: norm_price, + QUANTITY_KEY: fields[QUANTITY_KEY], + } + product_names.add(name) + return product_id - Returns: - The matching product record, or ``None`` when no product carries - that identifier (a message is printed in that case). - """ - # TODO: реализуйте функцию - return {} + +def read_product(storage: dict[str, Product], product_id: str) -> Product | None: + if product_id not in storage: + print(f"no product with id {product_id}") + return None + return storage[product_id] def update_product( @@ -98,39 +56,40 @@ def update_product( product_id: str, fields: dict[str, Any], ) -> Product | None: - """Overwrite the fields of the product stored under ``product_id``. - - The identifier itself is preserved. ``fields`` may carry any subset of - ``name``, ``price`` and ``quantity``; a key that is missing keeps the - value the product already has, so a caller can update a single field - without repeating the other two. - - Args: - storage: The product store to modify; the matching record is - updated in place on success. - product_names: The normalised names already in ``storage``; on a - successful rename the old name is removed from it and the new - 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.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 - ``fields`` is overwritten with its normalised form. - - Returns: - The updated product record, or ``None`` when ``name`` is present - and blank once normalised, no product carries that identifier, or - the normalised name is already taken by another product. In every - ``None`` case ``storage`` and ``product_names`` are left unchanged - and an explanatory message is printed. Renaming a product to its - own current name is allowed. - """ - # TODO: реализуйте функцию - return {} + if product_id not in storage: + print(f"no product with id {product_id}") + return None + + if NAME_KEY in fields: + name = normalize_product_name(fields[NAME_KEY]) + fields[NAME_KEY] = name + if not name: + print("product name must not be blank") + return None + current_name = storage[product_id][NAME_KEY] + if name != current_name and name in product_names: + print(f"product name '{name}' is already taken") + return None + + if PRICE_KEY in fields: + norm_price = normalize_price(fields[PRICE_KEY]) + fields[PRICE_KEY] = norm_price + + if NAME_KEY in fields: + old_name = storage[product_id][NAME_KEY] + new_name = fields[NAME_KEY] + if new_name != old_name: + product_names.remove(old_name) + product_names.add(new_name) + storage[product_id][NAME_KEY] = new_name + + if PRICE_KEY in fields: + storage[product_id][PRICE_KEY] = fields[PRICE_KEY] + + if QUANTITY_KEY in fields: + storage[product_id][QUANTITY_KEY] = fields[QUANTITY_KEY] + + return storage[product_id] def delete_product( @@ -138,20 +97,11 @@ def delete_product( product_names: set[str], product_id: str, ) -> str | None: - """Remove the product stored under ``product_id`` from ``storage``. - - Args: - storage: The product store to remove from; modified in place on - success. - product_names: The normalised names already in ``storage``; the - departing product's name is removed from it 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`` and ``product_names`` - are left unchanged and a message is printed). - """ - # TODO: реализуйте функцию - return "" + if product_id not in storage: + print(f"no product with id {product_id}") + return None + + name = storage[product_id][NAME_KEY] + del storage[product_id] + product_names.discard(name) + return product_id \ No newline at end of file diff --git a/src/part4/run_admin.py b/src/part4/run_admin.py index 2247b29b..50a5bbf1 100644 --- a/src/part4/run_admin.py +++ b/src/part4/run_admin.py @@ -1,110 +1,101 @@ -"""Interactive admin console for the part4 in-memory product store. - -Running this module starts a read-eval-print loop over a single product -store that lives in memory for the lifetime of the process. Every line -typed at the prompt is stripped of surrounding whitespace and matched, -with a ``match`` statement, against the set of known 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. - -The four CRUD commands forward to :mod:`src.part4.crud`; whatever -that call returns is printed unless it is ``None``. A line whose first -word is not a known verb, or that carries the wrong number of arguments -for its verb, prints an "is not a command" notice. A line that names a -command correctly but whose price or quantity argument will not parse -prints a distinct "arguments are invalid" notice. Either way the store -is left untouched and the loop keeps running; only ``exit`` stops it. -""" - -from decimal import Decimal, InvalidOperation # noqa: F401 -from typing import Final - -from .crud import ( # noqa: F401 - create_product, - delete_product, - read_product, - update_product, +from decimal import Decimal, InvalidOperation +from typing import Any, Final + +from .crud import create_product, delete_product, read_product, update_product +from .storage import NAME_KEY, PRICE_KEY, QUANTITY_KEY, Product +from .utils import get_storage_str_representation + +PROMPT: Final = "admin> " +HELP_TEXT: Final = ( + "Available commands:\n" + " help show this message\n" + " exit leave the console\n" + " show print the whole store as a table\n" + " create add a product, print its new id\n" + " read print the product with that id\n" + " update overwrite that product's fields\n" + " delete remove the product with that id\n\n" + "For create and update the price and quantity are the last two words of the\n" + "line; everything before them is the product name, so it may contain spaces\n" + '(for example "Gibson SG Junior") and needs no quoting.' ) -from .storage import NAME_KEY, PRICE_KEY, QUANTITY_KEY, Product # noqa: F401 -from .utils import get_storage_str_representation # noqa: F401 - - -# TODO: задайте приглашение и текст справки (используйте своё решение части 3) -PROMPT: Final[str] = "" -HELP_TEXT: Final[str] = "" 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: реализуйте функцию (используйте своё решение части 3) - + 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: реализуйте функцию (используйте своё решение части 3) +def print_result(result: Any) -> None: + if result is not None: + print(result) def run_command( storage: dict[str, Product], product_names: set[str], 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. - 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. - - 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 the 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() + if not words: + print(f"{line!r} is not a command") + return True + + cmd = words[0] + + if cmd == "help" and len(words) == 1: + show_help() + return True + + if cmd == "exit" and len(words) == 1: + return False + + if (cmd == "show" or cmd == "list") and len(words) == 1: + result = get_storage_str_representation(storage) + print_result(result) + return True + + if cmd == "read" and len(words) == 2: + product_id = words[1] + result = read_product(storage, product_id) + print_result(result) + return True + + if cmd == "delete" and len(words) == 2: + product_id = words[1] + result = delete_product(storage, product_names, product_id) + print_result(result) + return True + + if cmd == "create" and len(words) >= 4: + name = " ".join(words[1:-2]) + price = Decimal(words[-2]) + qty = int(words[-1]) + fields = { + NAME_KEY: name, + PRICE_KEY: price, + QUANTITY_KEY: qty, + } + result = create_product(storage, product_names, fields) + print_result(result) + return True + + if cmd == "update" and len(words) >= 5: + product_id = words[1] + name = " ".join(words[2:-2]) + price = Decimal(words[-2]) + qty = int(words[-1]) + fields = { + NAME_KEY: name, + PRICE_KEY: price, + QUANTITY_KEY: qty, + } + result = update_product(storage, product_names, product_id, fields) + print_result(result) + return True + + print(f"{line!r} 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. - """ storage: dict[str, Product] = {} product_names: set[str] = set() running = True diff --git a/src/part4/storage.py b/src/part4/storage.py index 9acedf07..b9f70926 100644 --- a/src/part4/storage.py +++ b/src/part4/storage.py @@ -1,45 +1,7 @@ -"""In-memory product store for the shop CRUD exercise. - -The store is deliberately primitive: a plain dictionary, with no -persistence and no schema enforcement. It maps a product's ``id`` -- a -UUID string assigned by the create operation in :mod:`src.part4.crud` --- to its :data:`Product` record -- a dictionary with the keys ``name``, -``price`` and ``quantity`` -- where: - -* the ``storage`` key itself -- unique string identifier, assigned by the - create operation in :mod:`src.part4.crud`; it is not duplicated - inside the record; -* ``name`` -- human-readable product name, unique across the store, held - in normalised form (see - :func:`src.part4.utils.normalize_product_name`); -* ``price`` -- price of a single unit, held as a :class:`~decimal.Decimal` - amount rounded to a fixed number of decimal places (see - :func:`src.part4.utils.normalize_price`) so that money is never - subject to binary floating-point error; -* ``quantity`` -- number of units currently in stock. - -Field names are exposed as ``*_KEY`` constants so callers never hard-code -the dictionary keys. - -Every caller that mutates the store also threads along a second -structure, a ``set[str]`` of the normalised names currently in use -- -see, for example, the ``product_names`` parameter of -:func:`src.part4.crud.create_product`. Keeping that name index as a -plain set passed next to ``storage``, rather than bundling the two into -an object, lets a name-uniqueness check become an O(1) membership test -against the set instead of a linear scan over ``storage.values()``. The -set must always mirror the normalised names present in ``storage``; the -functions in :mod:`src.part4.crud` keep the two in sync on every -successful create, update and delete. -""" - from typing import Any, Final - 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" \ No newline at end of file diff --git a/src/part4/utils.py b/src/part4/utils.py index d2a940ea..8563c2d3 100644 --- a/src/part4/utils.py +++ b/src/part4/utils.py @@ -1,100 +1,49 @@ -"""Standalone helpers shared by the part4 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. -""" - -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, Product, ) - -# 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("0.01") +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: реализуйте функцию (используйте своё решение части 3) - return Decimal(0) + if not isinstance(price, Decimal): + price = Decimal(str(price)) + 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: реализуйте функцию (используйте своё решение части 3) - return "" - - -# TODO: при необходимости добавьте свои вспомогательные функции + return " ".join(name.strip().split()).lower() def get_storage_str_representation(storage: dict[str, 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 insertion order. When - ``storage`` is empty only the header and separator rows are - returned, sized to the header labels. - """ - # TODO: реализуйте функцию (используйте своё решение части 3) - return "" + rows: list[list[str]] = [] + for product_id, product in storage.items(): + rows.append([ + product_id, + str(product[NAME_KEY]), + str(product[PRICE_KEY]), + str(product[QUANTITY_KEY]), + ]) + + col_widths = [ + max(len(header), max((len(row[i]) for row in rows), default=0)) + for i, header in enumerate(TABLE_HEADERS) + ] + + header_line = "| " + " | ".join(h.ljust(col_widths[i]) for i, h in enumerate(TABLE_HEADERS)) + " |" + separator_line = "|-" + "-|-".join("-" * col_widths[i] for i in range(len(TABLE_HEADERS))) + "-|" + + lines = [header_line, separator_line] + for row in rows: + row_line = "| " + " | ".join(cell.ljust(col_widths[i]) for i, cell in enumerate(row)) + " |" + lines.append(row_line) + + return "\n".join(lines) From 5211ee8d2fb24aaaea1a9b4186b1d1338d7288a0 Mon Sep 17 00:00:00 2001 From: phantom686 Date: Sun, 1 Nov 2026 19:41:23 +0300 Subject: [PATCH 15/19] fix --- src/part4/cart.py | 10 +++++++--- src/part4/crud.py | 2 +- src/part4/storage.py | 2 +- src/part4/utils.py | 30 +++++++++++++++++++++--------- 4 files changed, 30 insertions(+), 14 deletions(-) diff --git a/src/part4/cart.py b/src/part4/cart.py index 4ef1c35f..25a0412d 100644 --- a/src/part4/cart.py +++ b/src/part4/cart.py @@ -14,7 +14,9 @@ def add_to_cart( available = storage[product_id][QUANTITY_KEY] if available < quantity: - print(f"not enough stock for product {product_id}: {available} available, {quantity} requested") + print( + f"not enough stock for product {product_id}: {available} available, {quantity} requested" + ) return None storage[product_id][QUANTITY_KEY] -= quantity @@ -35,7 +37,9 @@ def remove_from_cart( in_cart = cart[product_id] if in_cart < quantity: - print(f"cart holds only {in_cart} unit(s) of product {product_id}, cannot remove {quantity}") + print( + f"cart holds only {in_cart} unit(s) of product {product_id}, cannot remove {quantity}" + ) return None if product_id not in storage: @@ -47,4 +51,4 @@ def remove_from_cart( remaining = cart[product_id] if remaining == 0: del cart[product_id] - return remaining \ No newline at end of file + return remaining diff --git a/src/part4/crud.py b/src/part4/crud.py index a2773a98..632f915c 100644 --- a/src/part4/crud.py +++ b/src/part4/crud.py @@ -104,4 +104,4 @@ def delete_product( name = storage[product_id][NAME_KEY] del storage[product_id] product_names.discard(name) - return product_id \ No newline at end of file + return product_id diff --git a/src/part4/storage.py b/src/part4/storage.py index b9f70926..ba0d02f3 100644 --- a/src/part4/storage.py +++ b/src/part4/storage.py @@ -4,4 +4,4 @@ NAME_KEY: Final = "name" PRICE_KEY: Final = "price" -QUANTITY_KEY: Final = "quantity" \ No newline at end of file +QUANTITY_KEY: Final = "quantity" diff --git a/src/part4/utils.py b/src/part4/utils.py index 8563c2d3..17e7c40e 100644 --- a/src/part4/utils.py +++ b/src/part4/utils.py @@ -26,24 +26,36 @@ def normalize_product_name(name: str) -> str: def get_storage_str_representation(storage: dict[str, Product]) -> str: rows: list[list[str]] = [] for product_id, product in storage.items(): - rows.append([ - product_id, - str(product[NAME_KEY]), - str(product[PRICE_KEY]), - str(product[QUANTITY_KEY]), - ]) + rows.append( + [ + product_id, + str(product[NAME_KEY]), + str(product[PRICE_KEY]), + str(product[QUANTITY_KEY]), + ] + ) col_widths = [ max(len(header), max((len(row[i]) for row in rows), default=0)) for i, header in enumerate(TABLE_HEADERS) ] - header_line = "| " + " | ".join(h.ljust(col_widths[i]) for i, h in enumerate(TABLE_HEADERS)) + " |" - separator_line = "|-" + "-|-".join("-" * col_widths[i] for i in range(len(TABLE_HEADERS))) + "-|" + header_line = ( + "| " + + " | ".join(h.ljust(col_widths[i]) for i, h in enumerate(TABLE_HEADERS)) + + " |" + ) + separator_line = ( + "|-" + "-|-".join("-" * col_widths[i] for i in range(len(TABLE_HEADERS))) + "-|" + ) lines = [header_line, separator_line] for row in rows: - row_line = "| " + " | ".join(cell.ljust(col_widths[i]) for i, cell in enumerate(row)) + " |" + row_line = ( + "| " + + " | ".join(cell.ljust(col_widths[i]) for i, cell in enumerate(row)) + + " |" + ) lines.append(row_line) return "\n".join(lines) From 70df001d226162fe2fec4197e945651e34c19fab Mon Sep 17 00:00:00 2001 From: phantom686 Date: Sun, 1 Nov 2026 19:44:27 +0300 Subject: [PATCH 16/19] fix2 --- src/part4/run_admin.py | 1 + src/part4/storage.py | 1 + src/part4/utils.py | 1 + 3 files changed, 3 insertions(+) diff --git a/src/part4/run_admin.py b/src/part4/run_admin.py index 50a5bbf1..15eac7b1 100644 --- a/src/part4/run_admin.py +++ b/src/part4/run_admin.py @@ -5,6 +5,7 @@ from .storage import NAME_KEY, PRICE_KEY, QUANTITY_KEY, Product from .utils import get_storage_str_representation + PROMPT: Final = "admin> " HELP_TEXT: Final = ( "Available commands:\n" diff --git a/src/part4/storage.py b/src/part4/storage.py index ba0d02f3..fe192215 100644 --- a/src/part4/storage.py +++ b/src/part4/storage.py @@ -1,5 +1,6 @@ from typing import Any, Final + type Product = dict[str, Any] NAME_KEY: Final = "name" diff --git a/src/part4/utils.py b/src/part4/utils.py index 17e7c40e..e69414ae 100644 --- a/src/part4/utils.py +++ b/src/part4/utils.py @@ -8,6 +8,7 @@ Product, ) + PRICE_PRECISION: Final[int] = 2 PRICE_STEP: Final = Decimal("0.01") TABLE_HEADERS: Final[tuple[str, ...]] = ("ID", "name", "price", "quantity") From d0c081745fb7345bcd412e0494147eeab336eb5b Mon Sep 17 00:00:00 2001 From: phantom686 Date: Sun, 1 Nov 2026 19:49:04 +0300 Subject: [PATCH 17/19] fix3 --- src/part4/run_admin.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/part4/run_admin.py b/src/part4/run_admin.py index 15eac7b1..f2c097b8 100644 --- a/src/part4/run_admin.py +++ b/src/part4/run_admin.py @@ -5,7 +5,6 @@ from .storage import NAME_KEY, PRICE_KEY, QUANTITY_KEY, Product from .utils import get_storage_str_representation - PROMPT: Final = "admin> " HELP_TEXT: Final = ( "Available commands:\n" @@ -49,20 +48,17 @@ def run_command( return False if (cmd == "show" or cmd == "list") and len(words) == 1: - result = get_storage_str_representation(storage) - print_result(result) + print_result(get_storage_str_representation(storage)) return True if cmd == "read" and len(words) == 2: product_id = words[1] - result = read_product(storage, product_id) - print_result(result) + print_result(read_product(storage, product_id)) return True if cmd == "delete" and len(words) == 2: product_id = words[1] - result = delete_product(storage, product_names, product_id) - print_result(result) + print_result(delete_product(storage, product_names, product_id)) return True if cmd == "create" and len(words) >= 4: @@ -74,8 +70,7 @@ def run_command( PRICE_KEY: price, QUANTITY_KEY: qty, } - result = create_product(storage, product_names, fields) - print_result(result) + print_result(create_product(storage, product_names, fields)) return True if cmd == "update" and len(words) >= 5: @@ -88,8 +83,7 @@ def run_command( PRICE_KEY: price, QUANTITY_KEY: qty, } - result = update_product(storage, product_names, product_id, fields) - print_result(result) + print_result(update_product(storage, product_names, product_id, fields)) return True print(f"{line!r} is not a command") @@ -112,4 +106,4 @@ def main() -> None: if __name__ == "__main__": - main() + main() \ No newline at end of file From 6632e0e228873654826344e7eefed8e34f916fc4 Mon Sep 17 00:00:00 2001 From: phantom686 Date: Sun, 1 Nov 2026 19:51:08 +0300 Subject: [PATCH 18/19] fix4 --- src/part4/run_admin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/part4/run_admin.py b/src/part4/run_admin.py index f2c097b8..f652c4da 100644 --- a/src/part4/run_admin.py +++ b/src/part4/run_admin.py @@ -5,6 +5,7 @@ from .storage import NAME_KEY, PRICE_KEY, QUANTITY_KEY, Product from .utils import get_storage_str_representation + PROMPT: Final = "admin> " HELP_TEXT: Final = ( "Available commands:\n" From 38f860315983cdca90ee3e6124c7ffb19ec50d1f Mon Sep 17 00:00:00 2001 From: phantom686 Date: Sun, 1 Nov 2026 19:55:34 +0300 Subject: [PATCH 19/19] fix5 --- src/part4/run_admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/part4/run_admin.py b/src/part4/run_admin.py index f652c4da..a96bcfef 100644 --- a/src/part4/run_admin.py +++ b/src/part4/run_admin.py @@ -107,4 +107,4 @@ def main() -> None: if __name__ == "__main__": - main() \ No newline at end of file + main()