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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
## Задания

- [Часть 1. Генератор идентификаторов Twitter Snowflake](src/part1/TASK.md) — нужно написать генератор 64-битных числовых идентификаторов, в которые упакованы метка времени, номер узла и счётчик последовательности. Такие идентификаторы уникальны и примерно упорядочены по времени, а из самого идентификатора можно узнать, когда и на каком узле он был создан.
- [Часть 2. CRUD и корзина для интернет-магазина](src/part2/TASK.md) — нужно реализовать хранилище товаров в памяти (список кортежей), операции создания, чтения, изменения и удаления записей, аккуратную работу с ценами через `Decimal` и корзину покупателя, в которую товары переносятся со склада и обратно без потери количества.

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

Expand Down
213 changes: 213 additions & 0 deletions src/part2/TASK.md
Original file line number Diff line number Diff line change
@@ -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 <id>`).

### `update_product(storage: list[Product], product_id: int, fields: tuple[str, Decimal, int]) -> Product | None`

- **Вход:** идентификатор и кортеж `(наименование, цена, количество)` с новыми
значениями полей.
- **Выход:** изменённая запись
`(идентификатор, наименование, нормализованная цена, количество)` — сам
идентификатор сохраняется прежним, остальные три поля заменяются, цена
приводится через `normalize_price`. Если записи с таким идентификатором нет —
хранилище не меняется, печатается сообщение об ошибке с идентификатором
(`no product with id <id>`), возвращается `None`. Уникальность нового
наименования не проверяется.

### `delete_product(storage: list[Product], product_id: int) -> int | None`

- **Вход:** идентификатор.
- **Выход:** тот же идентификатор, если запись была найдена и удалена из
хранилища. Если записи нет — хранилище не меняется, печатается сообщение об
ошибке с идентификатором (`no product with id <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 <id>: <склад> available, <quantity> 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 <id> is not in the cart`), возвращается `None`, ничего не меняется.
2. Иначе количество в строке корзины сравнивается с запрошенным. Если в корзине
строго меньше, чем `quantity` — печатается сообщение об ошибке с
идентификатором (например:
`cart holds only <в корзине> unit(s) of product <id>, cannot remove <quantity>`),
возвращается `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` возвращают склад и корзину к исходному состоянию.
1 change: 1 addition & 0 deletions src/part2/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Homework 4: a CRUD interface over an in-memory list-of-tuples product store."""
Loading
Loading