Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- [Часть 2. CRUD и корзина для интернет-магазина](src/part2/TASK.md) — нужно реализовать хранилище товаров в памяти (список кортежей), операции создания, чтения, изменения и удаления записей, аккуратную работу с ценами через `Decimal` и корзину покупателя, в которую товары переносятся со склада и обратно без потери количества.
- [Часть 3. Нормализация имён и консоль администратора](src/part3/TASK.md) — нужно привести названия товаров к единому написанию и следить за их уникальностью, научить хранилище печатать себя выровненной текстовой таблицей и написать интерактивную консоль администратора, через которую можно создавать, просматривать, изменять и удалять товары.
- [Часть 4. Хранилище-словарь и UUID-идентификаторы](src/part4/TASK.md) — нужно перевести хранилище со списка кортежей на словарь, выдавать товарам идентификаторы в виде UUID, завести индекс имён для быстрой проверки уникальности, научить `update_product` менять только часть полей и оформить продуктовую корзину в виде словаря.
- [Часть 5. Snowflake-идентификаторы для интернет-магазина](src/part5/TASK.md) — нужно вернуться к Snowflake из части 1 и оформить его как генератор со своим счётчиком, перевести товары с UUID на целочисленные идентификаторы и написать параметрический декоратор, который считает успешные и неуспешные вызовы операций хранилища, а консоль администратора научить печатать эту статистику командой `stats`.

## Дедлайны

Expand All @@ -21,7 +22,8 @@
| [Часть 1. Генератор идентификаторов Twitter Snowflake](src/part1/TASK.md) | 25.09.2026 | дедлайн истек |
| [Часть 2. CRUD и корзина для интернет-магазина](src/part2/TASK.md) | 02.10.2026 | дедлайн истек |
| [Часть 3. Нормализация имён и консоль администратора](src/part3/TASK.md) | 12.10.2026 | дедлайн истек |
| [Часть 4. Хранилище-словарь и UUID-идентификаторы](src/part4/TASK.md) | 01.11.2026 | активно |
| [Часть 4. Хранилище-словарь и UUID-идентификаторы](src/part4/TASK.md) | 01.11.2026 | дедлайн истек |
| [Часть 5. Snowflake-идентификаторы для интернет-магазина](src/part5/TASK.md) | 15.11.2026 | активно |

## Критерии оценивания

Expand Down
557 changes: 557 additions & 0 deletions src/part5/TASK.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/part5/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Homework 8: a stateful Twitter Snowflake identifier generator and its bit layout."""
88 changes: 88 additions & 0 deletions src/part5/cart.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""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: при необходимости добавьте свои вспомогательные функции


def add_to_cart(
storage: dict[int, Product],
product_names: set[str],
cart: dict[int, int],
product_id: int,
quantity: int,
) -> int | 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.part5.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.

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.
"""
# TODO: реализуйте функцию (используйте своё решение части 4)
return 0


def remove_from_cart(
storage: dict[int, Product],
product_names: set[str],
cart: dict[int, int],
product_id: int,
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. 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``; 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.
"""
# TODO: реализуйте функцию (используйте своё решение части 4)
return 0
37 changes: 37 additions & 0 deletions src/part5/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""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) на корректные значения (используйте своё решение
# части 1).

# 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

# Здесь можно добавить собственные вспомогательные константы
191 changes: 191 additions & 0 deletions src/part5/crud.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
"""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.part5.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.part5.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 not chosen by :func:`create_product`
itself: the caller hands in a zero-argument ``generate_id`` function that
returns the next identifier (or ``None`` when it cannot issue one), so the
identifier scheme can be swapped without touching the CRUD code. The module
supplies :func:`generate_snowflake_id`, backed by a Twitter Snowflake
generator (see :mod:`src.part5.snowflake`); the identifiers it issues
grow with creation time, so they are roughly ordered by insertion.

Every product name that reaches the store is passed through
:func:`~src.part5.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()``.

The four store operations (everything except :func:`generate_snowflake_id`)
should be decorated with :func:`~src.part5.decorators.collect_stats`, which
counts their successful calls and their failed ones (those returning
``None``) in the module-level :data:`STATS_REGISTRY`, filed under
:data:`CREATE_KEY`, :data:`READ_KEY`, :data:`UPDATE_KEY` and
:data:`DELETE_KEY`. The counters live for the lifetime of the process and
are shared by every store.
"""

from collections.abc import Callable
from typing import Any, Final

from .decorators import Registry
from .storage import Product


STATS_REGISTRY: Final[Registry] = {}

# TODO: задайте ключи операций в реестре, см. TASK.md
CREATE_KEY: Final[str] = ""
READ_KEY: Final[str] = ""
UPDATE_KEY: Final[str] = ""
DELETE_KEY: Final[str] = ""

# TODO: при необходимости заведите здесь общий генератор Snowflake-идентификаторов
# для generate_snowflake_id (см. TASK.md)


def generate_snowflake_id() -> int | None:
"""Issue the next product identifier from the shared Snowflake generator.

This is the identifier source callers pass to :func:`create_product`.

Returns:
A positive 63-bit Snowflake integer, or ``None`` (after a message
has been printed) when no identifier can be issued right now -- see
:func:`~src.part5.snowflake.get_snowflake_id_generator` for the
cases.
"""
# TODO: реализуйте функцию
return 0


# TODO: оберните операцию декоратором collect_stats
def create_product(
storage: dict[int, Product],
product_names: set[str],
fields: dict[str, Any],
generate_id: Callable[[], int | None],
) -> int | 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.part5.storage.NAME_KEY`,
:data:`~src.part5.storage.PRICE_KEY` and
:data:`~src.part5.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.
generate_id: A zero-argument function returning the identifier for
the new product, or ``None`` if it cannot issue one (for
example :func:`generate_snowflake_id`). It is only expected to
return an identifier that is not yet a key of ``storage``.

Returns:
The identifier returned by ``generate_id`` for the new product, or
``None`` when ``name`` is blank once normalised, a product with
the same normalised name already exists, or ``generate_id``
returned ``None``. In the ``None`` case ``storage`` and
``product_names`` are left unchanged and an explanatory message is
printed (for the last case, by ``generate_id`` itself).
"""
# TODO: реализуйте функцию
return 0


# TODO: оберните операцию декоратором collect_stats
def read_product(storage: dict[int, Product], product_id: int) -> Product | None:
"""Return the product stored under ``product_id``.

Args:
storage: The product store to search.
product_id: The identifier to look up.

Returns:
The matching product record, or ``None`` when no product carries
that identifier (a message is printed in that case).
"""
# TODO: реализуйте функцию (используйте своё решение части 4)
return {}


# TODO: оберните операцию декоратором collect_stats
def update_product(
storage: dict[int, Product],
product_names: set[str],
product_id: int,
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.part5.storage.NAME_KEY`,
:data:`~src.part5.storage.PRICE_KEY` and
:data:`~src.part5.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: реализуйте функцию (используйте своё решение части 4)
return {}


# TODO: оберните операцию декоратором collect_stats
def delete_product(
storage: dict[int, Product],
product_names: set[str],
product_id: int,
) -> int | None:
"""Remove the product stored under ``product_id`` from ``storage``.

Args:
storage: The product store to remove from; modified in place on
success.
product_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: реализуйте функцию (используйте своё решение части 4)
return 0
57 changes: 57 additions & 0 deletions src/part5/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""Decorators that add bookkeeping around the part5 CRUD operations.

The module provides :func:`collect_stats`, a parametric decorator that counts
how often a function succeeds and how often it fails. The counters are kept in
a *registry* shared with the caller -- a plain dictionary mapping a string key
to a :data:`Stats` record -- so that other code (for example the ``stats``
command of :mod:`src.part5.run_admin`) can read them without knowing
anything about the decorated functions.

A call counts as a failure when the decorated function returns ``None`` -- the
convention every operation in :mod:`src.part5.crud` uses to report that it
printed an explanation and changed nothing. Any other return value, including
a falsy one such as ``0``, counts as a success.
"""

from collections.abc import Callable
from typing import Final


# TODO: задайте имена двух счётчиков, см. TASK.md
SUCCESS_KEY: Final[str] = ""
FAILS_KEY: Final[str] = ""


type Stats = dict[str, int]
type Registry = dict[str, Stats]


def collect_stats[**P, R](
key: str, registry: Registry
) -> Callable[[Callable[P, R | None]], Callable[P, R | None]]:
"""Build a decorator that counts successful and failed calls of a function.

Decorating registers a fresh record ``{"success": 0, "fails": 0}`` in
``registry`` under ``key``. Every call of the decorated function then
updates that record in place: a call that returns ``None`` adds one to the
failure counter, any other result adds one to the success counter. The
result is passed through unchanged, and a call that raises an exception
is not counted at all.

If ``key`` is already in ``registry``, a message is printed, the existing
record is left untouched and the function is returned undecorated, so its
calls never mix into the counters of whoever registered the key first.

Args:
key: The registry key to keep this function's counters under, for
example the name of the console command it implements.
registry: The dictionary that receives the counters; modified in
place when the function is decorated and on every later call.

Returns:
A decorator for a function whose result is either some value or
``None``. The decorated function has the same signature, name and
docstring as the original (see :func:`functools.wraps`).
"""
# TODO: реализуйте декоратор
return lambda function: function
Loading
Loading