Skip to content
16 changes: 8 additions & 8 deletions src/part1/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,20 @@
# Все константы, зависящие от ёмкостей *_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**41
NODE_ID_MAX: Final[int] = 2**10
SEQUENCE_ID_MAX: Final[int] = 2**12

# Здесь можно добавить собственные вспомогательные константы
# (например, сдвиги полей при сборке идентификатора).
100 changes: 27 additions & 73 deletions src/part1/snowflake.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,102 +13,56 @@
: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_MAX,
SEQUENCE_ID_BITS,
SEQUENCE_ID_MAX,
TIMESTAMP_BITS,
TIMESTAMP_MS_MAX,
)


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 - EPOCH_MS_DEFAULT


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
creation_time = (snowflake_id >> SEQUENCE_ID_BITS >> NODE_ID_BITS) & (
(1 << TIMESTAMP_BITS) - 1
)
return creation_time + 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
return (snowflake_id >> SEQUENCE_ID_BITS) & ((1 << NODE_ID_BITS) - 1)


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 (snowflake_id) & ((1 << SEQUENCE_ID_BITS) - 1)


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
if not (0 <= node_id < NODE_ID_MAX):
print(f"node_id must be in [0, {NODE_ID_MAX}], but it is {node_id}")
return None
if not (0 <= sequence_id < SEQUENCE_ID_MAX):
print(f"sequence_id must be in [0, {SEQUENCE_ID_MAX}], but it is {sequence_id}")
return None

epoch_ms = int(time.time()) * 1000 - EPOCH_MS_DEFAULT
if not (0 <= epoch_ms < TIMESTAMP_MS_MAX):
print("overflow")
return None

return (
(epoch_ms << NODE_ID_MAX << SEQUENCE_ID_MAX) | (node_id << SEQUENCE_ID_MAX)
) | sequence_id
133 changes: 72 additions & 61 deletions src/part2/cart.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,7 @@
"""Shopping-cart operations layered on top of the product store.

The cart is a plain list of tuples, one :data:`CartLine` --
``(product_id, quantity)`` -- per distinct product. 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 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,
Expand All @@ -22,9 +11,34 @@

type CartLine = tuple[int, int]

# TODO: задайте позиции полей внутри кортежа CartLine

LINE_PRODUCT_ID_INDEX: Final = 0
LINE_QUANTITY_INDEX: Final = 0
LINE_QUANTITY_INDEX: Final = 1


def find_cart_line(cart: list[CartLine], product_id: int) -> CartLine | None:
for c_prod in cart:
if c_prod[LINE_PRODUCT_ID_INDEX] == product_id:
return c_prod
print(f"no product with id {product_id} in the cart")
return None


def change_cart_quantity(
cart: list[CartLine],
product_id: int,
quantity: int,
) -> int | None:
for ind in range(len(cart)):
if cart[ind][LINE_PRODUCT_ID_INDEX] == product_id:
new_c_prod: CartLine = (
product_id,
cart[ind][LINE_QUANTITY_INDEX] - quantity,
)
cart[ind] = new_c_prod
return ind
print(f"no product with id {product_id} in cart")
return None


def add_to_cart(
Expand All @@ -33,28 +47,25 @@ def add_to_cart(
product_id: int,
quantity: int,
) -> CartLine | None:
"""Move ``quantity`` units of ``product_id`` from the store into ``cart``.

The product is looked up in ``storage``. If it is missing, a message is
printed (by :func:`~src.part2.crud.read_product`) and nothing
changes. If the store holds fewer units than requested, an explanatory
message is printed and nothing changes. Otherwise the store record is
decremented by ``quantity`` and the cart line for ``product_id`` gains
``quantity`` units -- a new line is created when the cart had none.

Args:
storage: The product store to draw stock from; modified in place on
success.
cart: The cart to add the units to; modified in place on success.
product_id: The identifier of the product to add.
quantity: Number of units to move into the cart.

Returns:
The cart line for ``product_id`` after the addition, or ``None``
when the product is unknown or the store cannot cover the request.
"""
# TODO: реализуйте функцию
return (0, 0)
prod = read_product(storage, product_id)
if prod is None:
return None
if prod[QUANTITY_INDEX] < quantity:
print(
f"not enough stock for product {product_id}: {prod[QUANTITY_INDEX]} available, {quantity} requested"
)
return None
update_product(
storage,
product_id,
(prod[NAME_INDEX], prod[PRICE_INDEX], prod[QUANTITY_INDEX] - quantity),
)
for ind in range(len(cart)):
if cart[ind][LINE_PRODUCT_ID_INDEX] == product_id:
cart[ind] = (product_id, cart[ind][LINE_QUANTITY_INDEX] + quantity)
return cart[ind]
cart.append((product_id, quantity))
return cart[-1]


def remove_from_cart(
Expand All @@ -63,27 +74,27 @@ def remove_from_cart(
product_id: int,
quantity: int,
) -> CartLine | None:
"""Move ``quantity`` units of ``product_id`` from ``cart`` back to the store.

The cart line for ``product_id`` is looked up. If there is none, a
message is printed and nothing changes. If the line holds fewer units
than requested, an explanatory message is printed and nothing changes.
Otherwise the cart line loses ``quantity`` units -- the line is dropped
when it reaches zero -- and the store record gains ``quantity`` units
back.

Args:
storage: The product store to return stock to; modified in place on
success.
cart: The cart to take the units from; modified in place on
success.
product_id: The identifier of the product to remove.
quantity: Number of units to move back into the store.

Returns:
The cart line for ``product_id`` after the removal (a quantity of
zero means the line was dropped), or ``None`` when the cart has no
line for the product or holds too few units.
"""
# TODO: реализуйте функцию
return (0, 0)
c_prod = find_cart_line(cart, product_id)
if c_prod is None:
return None
if c_prod[LINE_QUANTITY_INDEX] < quantity:
print(
f"cart holds only {c_prod[LINE_QUANTITY_INDEX]} unit(s) of product {product_id}, cannot remove {quantity}"
)
return None
prod = read_product(storage, product_id)
if prod is None:
return None
update_product(
storage,
product_id,
(prod[NAME_INDEX], prod[PRICE_INDEX], prod[QUANTITY_INDEX] + quantity),
)
ind = change_cart_quantity(cart, product_id, quantity)
if ind is None:
return None
if cart[ind][LINE_QUANTITY_INDEX] == 0:
res = cart[ind]
del cart[ind]
return res
return cart[ind]
Loading
Loading