From 23f5368d451806b2d16c760c5f9b9fc1af31f61f Mon Sep 17 00:00:00 2001 From: EvgrafovMichail Date: Fri, 25 Sep 2026 19:31:43 +0300 Subject: [PATCH 1/2] Add part2 of complex task --- src/part2/TASK.md | 213 ++++++++++++++++++++++++++++++++++++++++++ src/part2/__init__.py | 1 + src/part2/cart.py | 89 ++++++++++++++++++ src/part2/crud.py | 120 ++++++++++++++++++++++++ src/part2/storage.py | 34 +++++++ src/part2/utils.py | 29 ++++++ 6 files changed, 486 insertions(+) create mode 100644 src/part2/TASK.md create mode 100644 src/part2/__init__.py create mode 100644 src/part2/cart.py create mode 100644 src/part2/crud.py create mode 100644 src/part2/storage.py create mode 100644 src/part2/utils.py diff --git a/src/part2/TASK.md b/src/part2/TASK.md new file mode 100644 index 00000000..25736d9c --- /dev/null +++ b/src/part2/TASK.md @@ -0,0 +1,213 @@ +# Задача: CRUD и корзина для интернет-магазина + +## Легенда + +Наше хранилище данных уже умеет выдавать уникальные идентификаторы, но пока это +лишь «голая» база: записи лежат в памяти, и работать с ними приходится вручную. +Чтобы проверить, что хранилище годится для настоящей прикладной задачи, мы +построим на нём серверную часть простого интернет-магазина: каталог товаров и +корзину покупателя. + +Если писать такой код «в лоб», быстро возникают проблемы: + +- записи о товарах разбросаны по программе, и каждый кусок кода сам решает, как + их создавать, менять и удалять, — в итоге в каталоге появляются дубликаты и + «осиротевшие» идентификаторы; +- деньги хранят во `float`, и суммы вроде `0.1 + 0.2` начинают расходиться с + копейками, а в бухгалтерии такое недопустимо; +- товар «переезжает» между складом и корзиной в два отдельных действия, и если + одно из них не выполнилось, товар пропадает или появляется из воздуха. + +Именно эти вопросы решают классические CRUD-операции (create, read, update, +delete) над единым хранилищем, аккуратная работа с ценой через `Decimal` и +операции корзины, которые сохраняют общее количество товара. Их нам и предстоит +написать. Позже на этих функциях мы построим интерфейс работы с нашей базой. + +## Контекст + +Нужно реализовать серверную часть простого интернет-магазина: хранилище товаров, +операции CRUD над ним и работу с корзиной покупателя. Настоящая база данных не +используется — хранилище это список кортежей в оперативной памяти. + +Решение состоит из четырёх модулей: + +- `storage.py` — форма записи о товаре и связанные с ней константы; +- `utils.py` — округление денежных сумм; +- `crud.py` — создание, чтение, изменение и удаление записей о товарах; +- `cart.py` — перенос товаров между хранилищем и корзиной. + +Все функции, кроме `find_cart_line` и `change_cart_quantity`, на неуспешном пути +ничего не бросают: печатают сообщение об ошибке в stdout и возвращают `None`. + +## Формат записи о товаре + +Запись о товаре (`Product`) — кортеж из четырёх полей, перечисленных по порядку: + +| Поле | Тип | Что хранит | +| ------------- | --------- | ------------------------------------------------------------------ | +| идентификатор | `int` | уникальный положительный ключ, назначается при создании записи | +| наименование | `str` | человекочитаемое имя, уникальное в пределах хранилища | +| цена | `Decimal` | цена за единицу, округлённая до фиксированного числа знаков дроби | +| количество | `int` | число единиц товара на складе | + +Позиции полей задаются константами `*_INDEX`, чтобы обращение к кортежу не +использовало числовые литералы. + +Строка корзины (`CartLine`) — кортеж `(идентификатор товара, количество)`. В +корзине не более одной строки на каждый товар. + +## Модуль `storage.py` + +Определите: + +- `Product` — псевдоним типа `tuple[int, str, Decimal, int]`. +- `PRODUCT_ID_INDEX`, `NAME_INDEX`, `PRICE_INDEX`, `QUANTITY_INDEX` — позиции + полей в кортеже `Product`: `0`, `1`, `2`, `3`. Каждая аннотирована + `typing.Final`. +- `PRODUCT_ID_MIN: Final[int] = 1` — идентификатор, который получает первый + товар, добавленный в пустое хранилище. + +## Модуль `utils.py` + +- `PRICE_PRECISION: Final[int] = 2` — число знаков после запятой, до которого + округляется каждая сохраняемая цена. +- `PRICE_STEP: Final` — шаг квантования. Вычисляется из `PRICE_PRECISION` + (`Decimal(1).scaleb(-PRICE_PRECISION)`, то есть `Decimal("0.01")` при значении + по умолчанию), а не задаётся числом напрямую; при изменении `PRICE_PRECISION` + шаг меняется согласованно. + +### `normalize_price(price: Decimal) -> Decimal` + +- **Вход:** `price` — произвольная денежная сумма типа `Decimal`. +- **Выход:** та же сумма, приведённая к `PRICE_PRECISION` знакам после запятой; + ровно половина шага округляется вверх (режим `ROUND_HALF_UP`). + +Примеры: `Decimal("12.999") → Decimal("13.00")`, +`Decimal("2.3451") → Decimal("2.35")`, `Decimal("2.3449") → Decimal("2.34")`, +`Decimal("5") → Decimal("5.00")` (число знаков дроби в результате всегда равно +`PRICE_PRECISION`). + +## Модуль `crud.py` + +Каждая функция получает хранилище `storage: list[Product]` первым параметром и +меняет его на месте. + +### `generate_product_id(storage: list[Product]) -> int` + +- **Вход:** хранилище. +- **Выход:** если хранилище пустое — `PRODUCT_ID_MIN`; иначе — наибольший + идентификатор среди записей плюс `1`. Пропуски в нумерации (например, + оставшиеся после удаления) не переиспользуются — берётся именно максимум. + +### `create_product(storage: list[Product], fields: tuple[str, Decimal, int]) -> int | None` + +- **Вход:** `fields` — кортеж `(наименование, цена, количество)`; `цена` типа + `Decimal`. +- **Выход:** сгенерированный идентификатор новой записи, либо `None`. + +**Поведение.** + +1. Если в хранилище уже есть запись с таким же наименованием — новая запись не + создаётся, хранилище не меняется, печатается сообщение об ошибке, содержащее + это наименование, возвращается `None`. (Например: + `product name '<имя>' is already taken`.) +2. Иначе идентификатор вычисляется через `generate_product_id`, цена приводится + через `normalize_price`, кортеж + `(идентификатор, наименование, нормализованная цена, количество)` добавляется + в хранилище, возвращается идентификатор. + +### `read_product(storage: list[Product], product_id: int) -> Product | None` + +- **Вход:** целочисленный идентификатор. +- **Выход:** запись с этим идентификатором, либо `None`. Если записи нет — + печатается сообщение об ошибке, содержащее переданный идентификатор (например, + `no product with id `). + +### `update_product(storage: list[Product], product_id: int, fields: tuple[str, Decimal, int]) -> Product | None` + +- **Вход:** идентификатор и кортеж `(наименование, цена, количество)` с новыми + значениями полей. +- **Выход:** изменённая запись + `(идентификатор, наименование, нормализованная цена, количество)` — сам + идентификатор сохраняется прежним, остальные три поля заменяются, цена + приводится через `normalize_price`. Если записи с таким идентификатором нет — + хранилище не меняется, печатается сообщение об ошибке с идентификатором + (`no product with id `), возвращается `None`. Уникальность нового + наименования не проверяется. + +### `delete_product(storage: list[Product], product_id: int) -> int | None` + +- **Вход:** идентификатор. +- **Выход:** тот же идентификатор, если запись была найдена и удалена из + хранилища. Если записи нет — хранилище не меняется, печатается сообщение об + ошибке с идентификатором (`no product with id `), возвращается `None`. + +## Модуль `cart.py` + +- `CartLine` — псевдоним типа `tuple[int, int]` + (`(идентификатор товара, количество)`). +- `LINE_PRODUCT_ID_INDEX`, `LINE_QUANTITY_INDEX` — позиции полей строки корзины + (`0`, `1`), аннотированы `typing.Final`. + +### `add_to_cart(storage: list[Product], cart: list[CartLine], product_id: int, quantity: int) -> CartLine | None` + +Переносит `quantity` единиц товара `product_id` со склада в корзину. + +- **Вход:** хранилище, корзина, идентификатор товара, количество для добавления. +- **Выход:** строка корзины для `product_id` после добавления, либо `None`. + +**Поведение.** Проверки выполняются в этом порядке; при первой же неудаче +остальные действия не выполняются: + +1. Товар ищется в хранилище (через `read_product`). Если его нет — `read_product` + печатает сообщение с идентификатором, `add_to_cart` возвращает `None`, больше + ничего не меняется. +2. Иначе количество на складе сравнивается с запрошенным. Если на складе строго + меньше, чем `quantity` — печатается сообщение об ошибке, содержащее + идентификатор товара (например: + `not enough stock for product : <склад> available, requested`), + возвращается `None`, ничего не меняется. Запрос ровно на всё имеющееся + количество (`quantity == склад`) допустим. +3. Иначе количество товара в хранилище уменьшается на `quantity` (наименование и + цена не меняются), а в корзину добавляется `quantity` единиц: если строка для + товара уже была — увеличивается её количество, иначе создаётся новая строка. + Возвращается получившаяся строка корзины. + +### `remove_from_cart(storage: list[Product], cart: list[CartLine], product_id: int, quantity: int) -> CartLine | None` + +Переносит `quantity` единиц товара `product_id` из корзины обратно на склад. +Принимает тот же набор аргументов, что и `add_to_cart`. + +- **Выход:** строка корзины для `product_id` после удаления (количество `0` + означает, что строка удалена), либо `None`. + +**Поведение.** Проверки в этом порядке: + +1. Строка корзины для `product_id` ищется через `find_cart_line`. Если её нет — + печатается сообщение об ошибке, содержащее идентификатор (например: + `product is not in the cart`), возвращается `None`, ничего не меняется. +2. Иначе количество в строке корзины сравнивается с запрошенным. Если в корзине + строго меньше, чем `quantity` — печатается сообщение об ошибке с + идентификатором (например: + `cart holds only <в корзине> unit(s) of product , cannot remove `), + возвращается `None`, ничего не меняется. +3. Товар ищется в хранилище через `read_product`; если его там нет — + возвращается `None` (сообщение печатает `read_product`). +4. Иначе количество товара в хранилище увеличивается на `quantity`, а из строки + корзины вычитается `quantity` (наименование и цена не меняются); если + количество в строке стало `0`, строка удаляется. Возвращается получившаяся + строка корзины. + +## Инварианты + +- Для любой успешно созданной записи + `read_product(storage, create_product(storage, (name, price, qty)))` возвращает + `(id, name, normalize_price(price), qty)`. +- После `delete_product(storage, id)` вызов `read_product(storage, id)` + возвращает `None`. +- Цена в хранилище всегда нормализована: `normalize_price` применяется при + каждом создании и изменении записи. +- Сохранение количества: при успешном `add_to_cart` или `remove_from_cart` сумма + «количество товара на складе + количество этого товара в корзине» не меняется. +- `add_to_cart(...)` и следующий за ним `remove_from_cart(...)` с тем же + `quantity` возвращают склад и корзину к исходному состоянию. diff --git a/src/part2/__init__.py b/src/part2/__init__.py new file mode 100644 index 00000000..e953a49e --- /dev/null +++ b/src/part2/__init__.py @@ -0,0 +1 @@ +"""Homework 4: a CRUD interface over an in-memory list-of-tuples product store.""" diff --git a/src/part2/cart.py b/src/part2/cart.py new file mode 100644 index 00000000..063f9aea --- /dev/null +++ b/src/part2/cart.py @@ -0,0 +1,89 @@ +"""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 + NAME_INDEX, + PRICE_INDEX, + QUANTITY_INDEX, + Product, +) + + +type CartLine = tuple[int, int] + +# TODO: задайте позиции полей внутри кортежа CartLine +LINE_PRODUCT_ID_INDEX: Final = 0 +LINE_QUANTITY_INDEX: Final = 0 + + +def add_to_cart( + storage: list[Product], + cart: list[CartLine], + 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) + + +def remove_from_cart( + storage: list[Product], + cart: list[CartLine], + 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) diff --git a/src/part2/crud.py b/src/part2/crud.py new file mode 100644 index 00000000..d026cbcb --- /dev/null +++ b/src/part2/crud.py @@ -0,0 +1,120 @@ +"""Create/read/update/delete operations over the in-memory product store. + +Every operation takes the store -- a list of +:data:`~src.part2.storage.Product` tuples -- as its first argument and +works on it in place. The failure path never raises: the operation prints +an explanatory message to stdout and returns ``None``. + +The identifier of a new product is derived from the store itself +(:func:`generate_product_id`): one past the greatest identifier in use, or +:data:`~src.part2.storage.PRODUCT_ID_MIN` when the store is empty. +Product names are kept unique -- :func:`create_product` refuses a name that +is already taken. +""" + +from decimal import Decimal + +from .storage import ( # noqa: F401 + NAME_INDEX, + PRODUCT_ID_INDEX, + PRODUCT_ID_MIN, + Product, +) +from .utils import normalize_price # noqa: F401 + + +def generate_product_id(storage: list[Product]) -> int: + """Choose the identifier for the next product added to ``storage``. + + Args: + storage: The product store to inspect. + + Returns: + One past the greatest identifier currently held in ``storage``, or + :data:`~src.part2.storage.PRODUCT_ID_MIN` when ``storage`` is + empty. + """ + # TODO: реализуйте функцию + return 0 + + +def create_product( + storage: list[Product], fields: tuple[str, Decimal, int] +) -> int | None: + """Append a new product to ``storage`` and return its new identifier. + + Args: + storage: The product store to append to; modified in place on + success. + fields: A ``(name, price, quantity)`` tuple describing the product. + ``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 a + product with the same name already exists. In the ``None`` case + ``storage`` is left unchanged and a message naming the clashing + name is printed. + """ + # TODO: реализуйте функцию + return 0 + + +def read_product(storage: list[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_id, name, price, quantity)`` record, or + ``None`` when no product carries that identifier (a message is + printed in that case). + """ + # TODO: реализуйте функцию + return (0, "", Decimal(0), 0) + + +def update_product( + storage: list[Product], + product_id: int, + fields: tuple[str, Decimal, int], +) -> Product | None: + """Overwrite the fields of the product stored under ``product_id``. + + The identifier itself is preserved; only ``name``, ``price`` and + ``quantity`` are replaced. + + Args: + storage: The product store to modify; the matching record is + replaced in place on success. + product_id: The identifier of the product to change. + fields: A ``(name, price, quantity)`` tuple with the new values. + ``price`` is a :class:`~decimal.Decimal` amount and is rounded + to the stored money precision before it is saved. + + Returns: + The updated ``(product_id, name, price, quantity)`` record, or + ``None`` when no product carries that identifier (``storage`` is + left unchanged and a message is printed). + """ + # TODO: реализуйте функцию + return (0, "", Decimal(0), 0) + + +def delete_product(storage: list[Product], 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_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`` is left unchanged and + a message is printed). + """ + # TODO: реализуйте функцию + return 0 diff --git a/src/part2/storage.py b/src/part2/storage.py new file mode 100644 index 00000000..b63d5fc7 --- /dev/null +++ b/src/part2/storage.py @@ -0,0 +1,34 @@ +"""In-memory product store for the shop CRUD exercise. + +The store is deliberately primitive: a plain list of tuples, with no +persistence, no indexing and no schema enforcement. Each element is a +:data:`Product` record -- a ``(product_id, name, price, quantity)`` tuple +where: + +* ``product_id`` -- unique positive integer key, assigned by the create + operation in :mod:`src.part2.crud`; +* ``name`` -- human-readable product name, unique across the store; +* ``price`` -- price of a single unit, held as a :class:`~decimal.Decimal` + amount rounded to a fixed number of decimal places (see + :func:`src.part2.utils.normalize_price`) so that money is never + subject to binary floating-point error; +* ``quantity`` -- number of units currently in stock. + +Field positions are exposed as ``*_INDEX`` constants so callers never +hard-code tuple offsets. +""" + +from decimal import Decimal +from typing import Final + + +type Product = tuple[int, str, Decimal, int] + +# TODO: задайте позиции полей внутри кортежа Product +PRODUCT_ID_INDEX: Final = 0 +NAME_INDEX: Final = 0 +PRICE_INDEX: Final = 0 +QUANTITY_INDEX: Final = 0 + +# TODO: задайте идентификатор первого товара в пустом хранилище +PRODUCT_ID_MIN: Final[int] = 0 diff --git a/src/part2/utils.py b/src/part2/utils.py new file mode 100644 index 00000000..ec0348d3 --- /dev/null +++ b/src/part2/utils.py @@ -0,0 +1,29 @@ +"""Standalone helpers shared by the hw4 storage and CRUD modules. + +For now this is limited to money handling: :func:`normalize_price` rounds a +raw :class:`~decimal.Decimal` amount to the fixed number of fractional +digits (:data:`PRICE_PRECISION`) that every stored price uses, keeping +currency values free of binary floating-point error. +""" + +from decimal import ROUND_HALF_UP, Decimal # noqa: F401 +from typing import Final + + +# TODO: задайте число знаков после запятой и шаг квантования +PRICE_PRECISION: Final[int] = 0 +PRICE_STEP: Final = Decimal(0) + + +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: реализуйте функцию + return Decimal(0) From 285ed2e07db7f2292e3d15ddd2a5d50d90e8801d Mon Sep 17 00:00:00 2001 From: EvgrafovMichail Date: Fri, 25 Sep 2026 19:33:42 +0300 Subject: [PATCH 2/2] Add part2 reference to README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 6b8725a4..76d456ac 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ ## Задания - [Часть 1. Генератор идентификаторов Twitter Snowflake](src/part1/TASK.md) — нужно написать генератор 64-битных числовых идентификаторов, в которые упакованы метка времени, номер узла и счётчик последовательности. Такие идентификаторы уникальны и примерно упорядочены по времени, а из самого идентификатора можно узнать, когда и на каком узле он был создан. +- [Часть 2. CRUD и корзина для интернет-магазина](src/part2/TASK.md) — нужно реализовать хранилище товаров в памяти (список кортежей), операции создания, чтения, изменения и удаления записей, аккуратную работу с ценами через `Decimal` и корзину покупателя, в которую товары переносятся со склада и обратно без потери количества. ## Критерии оценивания