Skip to content

Latest commit

 

History

History
411 lines (325 loc) · 25.7 KB

TASK.md

File metadata and controls

411 lines (325 loc) · 25.7 KB

Задача: хранилище-словарь, UUID-идентификаторы, индекс имён и корзина-словарь

Легенда

Наше хранилище товаров уже умеет немало: CRUD-операции, нормализацию имён и цен, красивую таблицу и консоль администратора. Но устроено оно по-прежнему «на коленке» — как список кортежей, — и пока каталог маленький, этого хватает. Но когда в каталоге появляются тысячи товаров, начинают проявляться проблемы:

  • чтобы найти товар по идентификатору или проверить, не занято ли имя, надо пройти весь список целиком, и чем больше каталог, тем медленнее работает каждая операция;
  • идентификатор вычисляется по содержимому хранилища («максимальный плюс один»): его нельзя выдать независимо от остальных товаров, а два хранилища (например, на разных серверах) легко выдадут одинаковые номера;
  • поля кортежа различаются только позицией, поэтому в коде появляются «магические» индексы, а чтобы поменять одно поле, приходится пересобирать запись целиком.

Обычные решения этих проблем знакомы любой базе данных. Записи хранят как «ключ → значение» в хеш-таблице, где поиск по ключу не зависит от размера хранилища. Рядом держат индекс — в СУБД это, например, уникальный индекс по имени. А идентификаторы генерируют так, чтобы каждый узел мог выдавать их сам, не согласовывая номера с остальными: в части 1 вы уже писали для этого Snowflake, а UUID — самый распространённый стандартный вариант.

Именно это мы и напишем: переведём хранилище на словари, заведём UUID-идентификаторы и отдельный индекс имён, научим update_product менять только нужные поля и оформим корзину покупателя тоже в виде словаря. Получившееся хранилище станет основой, на которую опирается дальнейшая работа над магазином.

Контекст

Часть 4 продолжает часть 3: тот же интернет-магазин, те же CRUD-операции, та же нормализация имён и цен, та же отрисовка таблицей и та же консоль администратора. То, что в этой части не меняется, берите из своего решения части 3. Меняется устройство данных:

  • хранилище — уже не список кортежей, а словарь «идентификатор → запись», а сама запись — словарь с именованными полями;
  • идентификатор товара — строка с UUID, а не порядковое целое число;
  • рядом с хранилищем передаётся индекс имён — множество нормализованных имён, которые сейчас есть в хранилище. Проверка уникальности имени идёт по нему, без перебора всех записей;
  • update_product может менять любую часть полей, а не все три сразу;
  • корзина — тоже словарь «идентификатор товара → количество»; модуль cart.py из части 2, который в части 3 не использовался, снова входит в задание.

Модули:

  • storage.py — новая форма записи и новые константы;
  • utils.py — логика без изменений относительно части 3 (PRICE_PRECISION, PRICE_STEP, TABLE_HEADERS, normalize_price, normalize_product_name, get_storage_str_representation) — используйте своё решение части 3; меняется только тип хранилища (см. ниже);
  • crud.py — все функции дорабатываются под новое хранилище и индекс имён;
  • cart.py — корзина из части 2, переделанная под словари;
  • run_admin.py — консоль из части 3, переделанная под новые сигнатуры.

Если поведение не менялось, оно здесь не расписывается: реализуйте его так, как в своём решении части 3 (для корзины — части 2), см. описание части 3 и описание части 2.

Как и раньше, при неудаче функции crud.py и cart.py печатают сообщение в stdout, возвращают None и ничего не меняют.


Формат данных

Хранилище

storage: dict[str, Product] — словарь, где ключ — идентификатор товара (строка с UUID, например "3f2c…-…"), а значение — запись Product. Идентификатор хранится только в ключе, внутри записи его нет.

Запись Product — словарь ровно с тремя ключами:

Ключ Тип Что хранит
"name" str имя товара в нормализованной форме, уникальное в пределах хранилища
"price" Decimal цена за единицу, округлённая через normalize_price
"quantity" int сколько единиц товара на складе

Индекс имён

product_names: set[str] — множество нормализованных имён всех товаров, которые сейчас лежат в хранилище. Индекс всегда должен совпадать с набором имён в storage. Каждая функция, которая меняет хранилище, получает индекс вторым аргументом и после каждого успешного вызова поддерживает его в актуальном виде. Хранилище и индекс передаются двумя отдельными аргументами, в один объект их не объединяют.

Корзина

cart: dict[str, int] — словарь «идентификатор товара → количество единиц в корзине». Товары с нулевым количеством в корзине не хранятся: если количество падает до нуля, ключ удаляется.


Модуль storage.py

  • Product — псевдоним типа dict[str, Any].
  • NAME_KEY, PRICE_KEY, QUANTITY_KEY — имена полей записи: "name", "price", "quantity". Каждая константа аннотирована typing.Final. Код обращается к полям записи только через эти константы, а не через строковые литералы.

По сравнению с частью 3 константы PRODUCT_ID_INDEX, NAME_INDEX, PRICE_INDEX, QUANTITY_INDEX и PRODUCT_ID_MIN больше не нужны — не определяйте их.


Модуль utils.py

Поведение всех функций прежнее. Меняется только тип хранилища в get_storage_str_representation(storage: dict[str, Product]) -> str:

  • в столбец ID попадает ключ словаря (строка UUID, выводится как есть); столбцы name, price, quantity берутся из соответствующих полей записи (price и quantity — через str(...));
  • строки товаров идут в порядке вставки в словарь;
  • формат таблицы, расчёт ширины столбцов и вид пустой таблицы — как в части 3.

Модуль crud.py

generate_product_id() -> str

  • Вход: ничего. Хранилище больше не передаётся.
  • Выход: новый случайный UUID в виде строки. Результат можно разобрать через uuid.UUID(...), а два вызова дают разные значения. Идентификатор не зависит ни от порядка вставки, ни от уже занятых идентификаторов.

Подсказка. Не придумывайте генерацию сами: в стандартной библиотеке есть модуль uuid, а в нём функция uuid4(), которая возвращает случайный UUID. Не забудьте превратить результат в строку.

Примеры. Сами значения случайны, поэтому в комментариях указаны не они, а свойства результата:

product_id = generate_product_id()

print(product_id)  # например: 3f2c9a4e-8b1d-4c7a-9e55-0d6a1b7c2f10
print(type(product_id))  # <class 'str'>
print(len(product_id))  # 36
print(uuid.UUID(product_id).version)  # 4 (строка разбирается как UUID)

Два вызова подряд дают разные идентификаторы:

first_id = generate_product_id()
second_id = generate_product_id()

print(first_id == second_id)  # False

create_product(storage: dict[str, Product], product_names: set[str], fields: dict[str, Any]) -> str | None

  • Вход: хранилище, индекс имён и fields — словарь, где обязательно есть все три ключа: NAME_KEY, PRICE_KEY (Decimal) и QUANTITY_KEY (int).
  • Выход: идентификатор нового товара или None.

Шаги выполняются в таком порядке. Шаги 2 и 3 — проверки: если любая из них не проходит, хранилище и индекс не меняются.

  1. Имя пропускается через normalize_product_name.
  2. Если нормализованное имя пустое, печатается product name must not be blank и возвращается None.
  3. Если нормализованное имя уже есть в product_names, печатается product name '<имя>' is already taken и возвращается None. Имена сравниваются по нормализованной форме, поэтому " Cordless Drill " конфликтует с "cordless drill".
  4. Иначе через generate_product_id() создаётся новый идентификатор, и под ним в storage кладётся запись {name: нормализованное имя, price: normalize_price(цена), quantity: количество}. Нормализованное имя добавляется в product_names, функция возвращает идентификатор.

Примеры. Каждый пример — отдельный фрагмент скрипта, который начинается с пустого хранилища и пустого индекса. Идентификатор случаен, поэтому у вас в выводе будет другой UUID — это нормально.

Успешное создание: имя нормализуется, цена округляется, имя попадает в индекс:

storage: dict[str, Product] = {}
product_names: set[str] = set()

product_id = create_product(
    storage,
    product_names,
    {NAME_KEY: "  Cordless   Drill ", PRICE_KEY: Decimal("89.999"), QUANTITY_KEY: 12},
)

print(product_id)  # 3f2c9a4e-8b1d-4c7a-9e55-0d6a1b7c2f10
print(storage)
# {
#     '3f2c9a4e-8b1d-4c7a-9e55-0d6a1b7c2f10': {
#         'name': 'cordless drill',
#         'price': Decimal('90.00'),
#         'quantity': 12
#     }
# }
print(product_names)  # {'cordless drill'}

Имя уже есть в индексе (сравнение идёт по нормализованной форме), хранилище и индекс не меняются:

storage: dict[str, Product] = {}
product_names: set[str] = set()

create_product(
    storage,
    product_names,
    {NAME_KEY: "cordless drill", PRICE_KEY: Decimal("50"), QUANTITY_KEY: 3},
)
result = create_product(
    storage,
    product_names,
    {NAME_KEY: "  CORDLESS   DRILL  ", PRICE_KEY: Decimal("75"), QUANTITY_KEY: 1},
)
# печатает: product name 'cordless drill' is already taken

print(result)  # None
print(len(storage))  # 1
print(product_names)  # {'cordless drill'}

read_product(storage: dict[str, Product], product_id: str) -> Product | None

  • Вход: хранилище и идентификатор (строка).
  • Выход: запись, которая лежит под этим ключом. Если такого ключа нет, печатается no product with id <id> и возвращается None.

update_product(storage: dict[str, Product], product_names: set[str], product_id: str, fields: dict[str, Any]) -> Product | None

Частичное обновление: в fields может быть любое подмножество ключей NAME_KEY, PRICE_KEY и QUANTITY_KEY. Поля без ключа в fields сохраняют текущее значение, поэтому, например, можно поменять одно количество, не передавая имя и цену. Идентификатор (ключ в storage) не меняется.

Шаги выполняются в таком порядке. Шаги 1 и 2 содержат проверки: если любая из них не проходит, хранилище и индекс не меняются. Порядок изменился по сравнению с частью 3: сначала проверяется, есть ли товар с таким идентификатором.

  1. Если ключа product_id в storage нет, печатается no product with id <id> и возвращается None.
  2. Если в fields есть имя, оно нормализуется через normalize_product_name.
    • Если нормализованное имя пустое, печатается product name must not be blank и возвращается None.
    • Если нормализованное имя отличается от текущего имени товара и уже есть в product_names, печатается product name '<имя>' is already taken и возвращается None. Переименование товара в его же текущее имя конфликтом не считается.
  3. Если в fields есть цена, она округляется через normalize_price.
  4. Поля из fields записываются в запись товара. Если имя изменилось, старое имя удаляется из product_names, а новое добавляется. Обновление без переименования индекс не меняет.
  5. Возвращается обновлённая запись, то есть тот же объект, который лежит в storage[product_id].

Побочный эффект: переданный словарь fields меняется на месте. Имя и цена в нём заменяются нормализованными значениями.

Пример. Сначала меняется одно количество, затем товар переименовывается вместе с новой ценой. Идентификатор случаен, поэтому у вас в выводе будет другой UUID — это нормально:

storage: dict[str, Product] = {}
product_names: set[str] = set()

product_id = create_product(
    storage,
    product_names,
    {NAME_KEY: "Cordless Drill", PRICE_KEY: Decimal("89.99"), QUANTITY_KEY: 12},
)
print(product_id)  # 3f2c9a4e-8b1d-4c7a-9e55-0d6a1b7c2f10

# Частичное обновление: меняется только количество, индекс имён остаётся прежним
print(update_product(storage, product_names, product_id, {QUANTITY_KEY: 7}))
# {
#     'name': 'cordless drill',
#     'price': Decimal('89.99'),
#     'quantity': 7
# }
print(product_names)  # {'cordless drill'}

# Переименование: имя и цена нормализуются, в индексе старое имя заменяется новым
print(
    update_product(
        storage,
        product_names,
        product_id,
        {NAME_KEY: "  Hammer   Drill ", PRICE_KEY: Decimal("109.999")},
    )
)
# {
#     'name': 'hammer drill',
#     'price': Decimal('110.00'),
#     'quantity': 7
# }
print(product_names)  # {'hammer drill'}

delete_product(storage: dict[str, Product], product_names: set[str], product_id: str) -> str | None

  • Вход: хранилище, индекс имён, идентификатор.
  • Выход: product_id, если товар был удалён. В этом случае его имя убирается из product_names. Если ключа нет, печатается no product with id <id> и возвращается None; хранилище и индекс не меняются.

Модуль cart.py

По сравнению с частью 2 здесь нет типа CartLine и констант LINE_*_INDEX: корзина — обычный словарь dict[str, int] (см. «Формат данных»). Вспомогательные функции можно добавлять на своё усмотрение. Обе функции возвращают теперь не строку корзины, а количество единиц товара в корзине после операции. Обе принимают индекс имён вместе с хранилищем. Сам индекс при этом не меняется: при изменении одного количества имя товара не меняется.

add_to_cart(storage: dict[str, Product], product_names: set[str], cart: dict[str, int], product_id: str, quantity: int) -> int | None

Переносит quantity единиц товара со склада в корзину. Проверки идут в таком порядке (если какая-то не проходит, склад и корзина не меняются):

  1. Если товара с таким идентификатором нет в хранилище, печатается no product with id <id> и возвращается None.
  2. Если на складе строго меньше quantity единиц, печатается not enough stock for product <id>: <на складе> available, <quantity> requested и возвращается None. Можно запросить ровно столько, сколько есть на складе: тогда остаток станет 0, но запись останется в хранилище.

Если обе проверки пройдены, количество товара на складе уменьшается на quantity, остальные поля записи не меняются. В корзине количество этого товара увеличивается на quantity; если товара в корзине не было, для него появляется ключ. Возвращается новое количество товара в корзине.

remove_from_cart(storage: dict[str, Product], product_names: set[str], cart: dict[str, int], product_id: str, quantity: int) -> int | None

Переносит quantity единиц товара из корзины обратно на склад. Проверки идут в таком порядке (если какая-то не проходит, склад и корзина не меняются):

  1. Если product_id нет в корзине, печатается product <id> is not in the cart и возвращается None.
  2. Если в корзине строго меньше quantity единиц, печатается cart holds only <в корзине> unit(s) of product <id>, cannot remove <quantity> и возвращается None.
  3. Если товара с таким идентификатором нет в хранилище, печатается no product with id <id> и возвращается None.

Если все три проверки пройдены, количество товара на складе увеличивается на quantity, остальные поля записи не меняются, а в корзине количество уменьшается на quantity. Если в корзине осталось 0, ключ удаляется. Возвращается оставшееся в корзине количество; 0 значит, что товар из корзины убран.


Модуль run_admin.py

Набор команд, формат ввода, константы PROMPT и HELP_TEXT, функции show_help и print_result, сообщения '<строка>' is not a command и '<строка>' names a command but its arguments are invalid, а также обработка ValueError и decimal.InvalidOperation такие же, как в части 3. Отличия:

  • run_command(storage: dict[str, Product], product_names: set[str], line: str) -> bool дополнительно принимает индекс имён и передаёт его в create_product, update_product и delete_product.
  • <id> в командах read, update и delete не приводится к int, а передаётся строкой как есть. Поэтому неверный идентификатор уже не вызывает исключение: crud.py просто печатает no product with id <id>.
  • Для create и update аргументы собираются в словарь {NAME_KEY: имя, PRICE_KEY: Decimal(цена), QUANTITY_KEY: int(количество)}. Консольный update всегда передаёт все три поля.
  • main() создаёт пустое хранилище {} и пустой индекс set(), держит их в памяти всю сессию и передаёт в каждый вызов run_command. Остальное поведение цикла такое же, как в части 3.

Инварианты

  • product_names всегда равно множеству {запись["name"] for запись in storage.values()}: каждый успешный create_product, update_product с переименованием и delete_product поддерживает это равенство, а неуспешный вызов не меняет ни хранилище, ни индекс.
  • Все сохранённые имена нормализованы и уникальны с точностью до нормализации.
  • Все сохранённые цены прошли через normalize_price.
  • Для успешно созданного товара read_product(storage, id) возвращает {"name": normalize_product_name(name), "price": normalize_price(price), "quantity": quantity}.
  • После delete_product(storage, product_names, id) вызов read_product(storage, id) возвращает None.
  • Количество сохраняется: после успешного add_to_cart или remove_from_cart сумма «на складе + в корзине» для этого товара не меняется. Если после add_to_cart вызвать remove_from_cart с тем же quantity, склад и корзина вернутся в исходное состояние.