diff --git a/src/part1/constants.py b/src/part1/constants.py index 9abfbba2..960e4b82 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: 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 diff --git a/src/part1/snowflake.py b/src/part1/snowflake.py index d3abf050..80808ec6 100644 --- a/src/part1/snowflake.py +++ b/src/part1/snowflake.py @@ -1,114 +1,48 @@ -"""Twitter Snowflake identifier generator. +import time -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 +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: - """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(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 int(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 int(( + 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 + return int(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, + 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 + 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 int((current_millis * (2**TIMESTAMP_SHIFT)) + ( + node_id * (2**NODE_ID_SHIFT) + sequence_id + )) diff --git a/src/part2/cart.py b/src/part2/cart.py new file mode 100644 index 00000000..0961aa8d --- /dev/null +++ b/src/part2/cart.py @@ -0,0 +1,47 @@ +from crud import read_product, update_product +from storage import ( + Product, + NAME_INDEX, + PRICE_INDEX, + QUANTITY_INDEX, +) +def find_cart_line(cart: list[CartLine], product_id: int) -> int | None: + for i, line in enumerate(cart): + if line[LINE_PRODUCT_ID_INDEX] == product_id: + return i + return None + +CartLine = tuple[int, int] +LINE_PRODUCT_ID_INDEX: Final = 0 +LINE_QUANTITY_INDEX: Final = 1 +def add_to_cart(storage: list[Product], cart: list[CartLine], product_id: int, quantity: int) -> CartLine | None: + product = read_product(storage, product_id) + if product is None: + return None + stock = product[QUANTITY_INDEX] + if stock < quantity: + print(f"not enough stock for product <{product_id}>: <{stock}> available, <{quantity}> requested") + return None + new_stock = stock - quantity + update_product(storage, product_id, (product[NAME_INDEX], product[PRICE_INDEX], new_stock)) + +def remove_from_cart(storage: list[Product], cart: list[CartLine], product_id: int, quantity: int) -> CartLine | None: + idx = find_cart_line(cart, product_id) + if idx is None: + print(f"product <{product_id}> is not in the cart") + return None + cart_qty = cart[idx][LINE_QUANTITY_INDEX] + if cart_qty < quantity: + print(f"cart holds only <{cart_qty}> unit(s) of product <{product_id}>, cannot remove ") + return None + product = read_product(storage, product_id) + if product is None: + return None + new_stock = product[QUANTITY_INDEX] + quantity + update_product(storage, product_id, (product[NAME_INDEX], product[PRICE_INDEX], new_stock)) + new_cart_qty = cart_qty - quantity + new_line = (product_id, new_cart_qty) + if new_cart_qty == 0: + cart.pop(idx) + else: + cart[idx] = new_line \ No newline at end of file diff --git a/src/part2/crud.py b/src/part2/crud.py new file mode 100644 index 00000000..7e64e79f --- /dev/null +++ b/src/part2/crud.py @@ -0,0 +1,47 @@ +from decimal import Decimal +from utils import normalize_price +from storage import ( + Product, + PRODUCT_ID_MIN, + PRODUCT_ID_INDEX, + NAME_INDEX, + PRICE_INDEX, + QUANTITY_INDEX, +) +def generate_product_id(storage: list[Product]) -> int: + if not storage: + return PRODUCT_ID_MIN + return max(i[PRODUCT_ID_INDEX] for i in storage) + 1 +def create_product(storage: list[Product], fields: tuple[str, Decimal, int]) -> int | None: + name, price, qty = fields + for i in storage: + if i[NAME_INDEX] == name: + print(f"product name '<{name}>' is already taken") + return 0 + new_id = generate_product_id(storage) + norm_price = normalize_price(price) + storage.append((new_id, name, norm_price, qty)) + return new_id +def read_product(storage: list[Product], product_id: int) -> Product | None: + for i in storage: + if i[PRODUCT_ID_INDEX] == product_id: + return i + print(f"no product with id <{product_id}>") + return None +def update_product(storage: list[Product], product_id: int, fields: tuple[str, Decimal, int]) -> Product | None: + name, price, qty = fields + for i, p in enumerate(storage): + if p[PRODUCT_ID_INDEX] == product_id: + norm_price = normalize_price(price) + updated_product = (product_id, name, norm_price, qty) + storage[i] = update_product + return update_product + print(f"no product with id <{product_id}>") + return None + def delete_product(storage: list[Product], product_id: int) -> int | None: + for i, p in enumerate(storage): + if p[PRODUCT_ID_INDEX] == product_id: + storage.pop(i) + return product_id + print(f"no product with id <{product_id}>") + return None \ No newline at end of file diff --git a/src/part2/storage.py b/src/part2/storage.py new file mode 100644 index 00000000..8996af0a --- /dev/null +++ b/src/part2/storage.py @@ -0,0 +1,7 @@ +from typing import Final +Product = tuple[int, str, Decimal, int] +PRODUCT_ID_INDEX: Final = 0 +NAME_INDEX: Final = 1 +PRICE_INDEX: Final = 2 +QUANTITY_INDEX: Final = 3 +PRODUCT_ID_MIN: Final[int] = 1 diff --git a/src/part2/utils.py b/src/part2/utils.py new file mode 100644 index 00000000..8344f413 --- /dev/null +++ b/src/part2/utils.py @@ -0,0 +1,8 @@ +from typing import Final +from decimal import Decimal, ROUND_HALF_UP + + +PRICE_PRECISION: Final[int] = 2 +PRICE_STEP: Final = Decimal(1).scaleb(-PRICE_PRECISION) +def normalize_price(price: Decimal) -> Decimal: + return price.quantize(PRICE_STEP, rounding=ROUND_HALF_UP) \ No newline at end of file