{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Мини-проект: склад\n",
    "\n",
    "Класс Stock ведёт остатки «Кофейной лавки» как встроенный контейнер: len, in, цикл по позициям, stock[\"Молоко\"] и сложение поставок плюсом.\n",
    "\n",
    "Курс ООП в Python · урок `oop-p7-stock` · [урок на сайте](https://ds-edu-trainer.vercel.app/courses/oop/p7-stock).\n",
    "\n",
    "Выполняйте ячейки по порядку (Shift+Enter), начиная с первой — она готовит урок. После каждого упражнения идёт\n",
    "ячейка проверки: она печатает ✓ / ✗ и итог."
   ],
   "id": "intro"
  },
  {
   "cell_type": "code",
   "metadata": {
    "cellView": "form",
    "jupyter": {
     "source_hidden": true
    }
   },
   "source": [
    "# @title Подготовка урока — выполните эту ячейку первой\n",
    "# Проверяет версии пакетов, записывает файлы данных в папку data/ и определяет _check — проверку упражнений.\n",
    "import base64, os, zlib\n",
    "\n",
    "import sys\n",
    "\n",
    "if sys.version_info < (3, 10):\n",
    "    print(f\"Урок написан для Python 3.10 и новее, у вас {sys.version.split()[0]} — часть кода может не работать\")\n",
    "\n",
    "# ─── Проверка упражнений: тот же раннер, что у задач на сайте ───\n",
    "# ─── Запуск тестов ───────────────────────────────────────────────────────────\n",
    "\n",
    "_TIMEOUT = 2.0  # секунд на тест; отдельный тест может задать свой: test_x.timeout = 10\n",
    "\n",
    "\n",
    "def _call_with_timeout(target, seconds: float) -> bool:\n",
    "    \"\"\"Запускает target() в отдельном потоке. Возвращает False, если за seconds он не завершился.\"\"\"\n",
    "    import sys\n",
    "    import threading\n",
    "\n",
    "    if sys.platform == \"emscripten\":  # Python в браузере: потоков нет, таймаут обеспечивает страница\n",
    "        target()\n",
    "        return True\n",
    "    # Большой стек нужен, чтобы глубокая рекурсия давала RecursionError, а не аварийное завершение Python.\n",
    "    for megabytes in (256, 64, 0):\n",
    "        try:\n",
    "            threading.stack_size(megabytes * 1024 * 1024)\n",
    "            worker = threading.Thread(target=target, daemon=True)\n",
    "            worker.start()\n",
    "        except (ValueError, RuntimeError, MemoryError):\n",
    "            continue\n",
    "        worker.join(seconds)\n",
    "        return not worker.is_alive()\n",
    "    target()  # потоки недоступны — запускаем без таймаута\n",
    "    return True\n",
    "\n",
    "\n",
    "def _test_info(fn) -> dict:\n",
    "    \"\"\"Описание теста, его лимит времени и сообщение на случай зависания.\"\"\"\n",
    "    limit = getattr(fn, \"timeout\", _TIMEOUT)\n",
    "    hint = getattr(fn, \"timeout_hint\", \"\")\n",
    "    message = (\n",
    "        f\"тест не завершился за {limit:g} с — возможно, бесконечный цикл или рекурсия: \"\n",
    "        \"проверьте, что границы/аргументы изменяются на каждом шаге\" + (f\". {hint}\" if hint else \"\")\n",
    "    )\n",
    "    return {\"title\": (fn.__doc__ or fn.__name__).strip(), \"timeout\": limit, \"timeout_message\": message}\n",
    "\n",
    "\n",
    "def _error_result(title: str, error: BaseException, user_file: str | None) -> dict:\n",
    "    \"\"\"Результат упавшего теста: статус, сообщение, тип исключения и строка в коде решения.\"\"\"\n",
    "    line = None\n",
    "    if user_file:  # самый глубокий кадр в коде решения; ошибка внутри самих тестов — без номера строки\n",
    "        tb = error.__traceback__\n",
    "        while tb is not None:\n",
    "            if tb.tb_frame.f_code.co_filename == user_file:\n",
    "                line = tb.tb_lineno\n",
    "            tb = tb.tb_next\n",
    "    status, message = \"error\", f\"{type(error).__name__}: {error}\"\n",
    "    if isinstance(error, NotImplementedError):\n",
    "        status, message, line = \"not_written\", \"функция ещё не написана\", None\n",
    "    elif isinstance(error, AssertionError):\n",
    "        status, message = \"failed\", str(error) or \"проверка не прошла\"\n",
    "    elif isinstance(error, RecursionError):\n",
    "        message = \"RecursionError: слишком глубокая рекурсия — проверьте базовый случай и то, что задача уменьшается\"\n",
    "    elif isinstance(error, MemoryError) or (isinstance(error, OverflowError) and \"index-sized\" in str(error)):\n",
    "        message = f\"{type(error).__name__}: не хватило памяти — возможно, создаётся слишком большая структура данных\"\n",
    "    return {\"title\": title, \"status\": status, \"message\": message, \"error_type\": type(error).__name__, \"line\": line}\n",
    "\n",
    "\n",
    "def _run_tests(verbose: bool = True, on_start=None, on_result=None, user_file: str | None = None) -> list[dict]:\n",
    "    \"\"\"Запускает тесты задачи (_TESTS — функции из tests.py) по порядку, каждый — с таймаутом.\n",
    "\n",
    "    Возвращает список результатов: {\"title\", \"status\", \"message\", \"error_type\", \"line\"}, где status —\n",
    "    passed · failed · error · not_written · timeout · not_run. Если тест завис, при verbose=True печатает\n",
    "    итог и завершает процесс: зависший поток остановить нельзя. on_start(i) и on_result(i, результат)\n",
    "    вызываются по мере выполнения; user_file — имя файла с кодом решения для номеров строк в ошибках.\n",
    "    \"\"\"\n",
    "    import os\n",
    "    import sys\n",
    "\n",
    "    if verbose:\n",
    "        try:  # ✓ и кириллица в консоли Windows со старой кодировкой\n",
    "            sys.stdout.reconfigure(encoding=\"utf-8\")\n",
    "        except Exception:  # noqa: BLE001 — reconfigure может не быть (перенаправленный вывод, браузер)\n",
    "            pass\n",
    "\n",
    "    tests = list(_TESTS)\n",
    "    results: list[dict] = []\n",
    "    not_written = hung = False\n",
    "    for index, fn in enumerate(tests):\n",
    "        info = _test_info(fn)\n",
    "        outcome: dict = {}\n",
    "\n",
    "        def target(fn=fn, outcome=outcome):\n",
    "            try:\n",
    "                fn()\n",
    "            except BaseException as e:\n",
    "                outcome[\"error\"] = e\n",
    "\n",
    "        if on_start:\n",
    "            on_start(index)\n",
    "        if not _call_with_timeout(target, info[\"timeout\"]):\n",
    "            results.append({\"title\": info[\"title\"], \"status\": \"timeout\", \"message\": info[\"timeout_message\"], \"error_type\": None, \"line\": None})\n",
    "            for other in tests[index + 1:]:\n",
    "                results.append({\"title\": _test_info(other)[\"title\"], \"status\": \"not_run\", \"message\": \"не запускался: предыдущий тест завис\", \"error_type\": None, \"line\": None})\n",
    "            hung = True\n",
    "            break\n",
    "        error = outcome.get(\"error\")\n",
    "        if error is None:\n",
    "            result = {\"title\": info[\"title\"], \"status\": \"passed\", \"message\": \"\", \"error_type\": None, \"line\": None}\n",
    "        else:\n",
    "            result = _error_result(info[\"title\"], error, user_file)\n",
    "            not_written = not_written or result[\"status\"] == \"not_written\"\n",
    "        results.append(result)\n",
    "        if on_result:\n",
    "            on_result(index, result)\n",
    "\n",
    "    if not_written:  # пока функция не написана, «даром» пройденные тесты не засчитываются\n",
    "        for r in results:\n",
    "            if r[\"status\"] == \"passed\":\n",
    "                r.update(status=\"not_written\", message=\"не засчитан, пока функция не написана\")\n",
    "    for r in results:\n",
    "        if len(r[\"message\"]) > 300:\n",
    "            r[\"message\"] = r[\"message\"][:300] + \"…\"\n",
    "\n",
    "    if verbose:\n",
    "        if not_written:\n",
    "            print(\"Функция ещё не написана — замените raise NotImplementedError своим кодом.\\n\")\n",
    "        for r in results:\n",
    "            mark = {\"passed\": \"✓\", \"not_run\": \"–\"}.get(r[\"status\"], \"✗\")\n",
    "            print(f\"{mark} {r['title']}\")\n",
    "            if r[\"message\"]:\n",
    "                print(f\"    {r['message']}\")\n",
    "        passed = sum(r[\"status\"] == \"passed\" for r in results)\n",
    "        print(\"─\" * 40)\n",
    "        print(f\"Прошло {passed} из {len(results)}\" + (\" — всё верно!\" if passed == len(results) else \"\"))\n",
    "        if hung:\n",
    "            print(\"Выполнение остановлено: один из тестов завис.\")\n",
    "            sys.stdout.flush()\n",
    "            os._exit(1)\n",
    "    return results\n",
    "\n",
    "\n",
    "def _check(*tests, names=()):\n",
    "    \"\"\"Запускает проверки упражнения и печатает ✓ / ✗ и итог — как на сайте.\"\"\"\n",
    "    for name in names:\n",
    "        if \".\" in name:  # «Класс.метод» из заготовки: класс должен быть объявлен\n",
    "            if not isinstance(globals().get(name.split(\".\")[0]), type):\n",
    "                print(f\"✗ класс {name.split('.')[0]} не объявлен — выполните ячейку упражнения\")\n",
    "                return\n",
    "            continue\n",
    "        if name not in globals():\n",
    "            print(f\"✗ переменная {name} не создана — выполните ячейку упражнения\")\n",
    "            return\n",
    "        if globals()[name] is Ellipsis:\n",
    "            print(f\"✗ {name} пока равна ... — замените многоточие своим кодом\")\n",
    "            return\n",
    "    globals()[\"_TESTS\"] = list(tests)\n",
    "    _run_tests()"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "setup"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "На складе «Кофейной лавки» — зёрна, молоко, сиропы и стаканы. Утром приходит поставка, днём бариста\n",
    "расходует запасы, вечером управляющий смотрит, чего осталось мало. Сделаем класс `Stock`, с которым\n",
    "всё это пишется так же коротко, как со словарём: `len(stock)`, `\"Сироп\" in stock`, `stock[\"Молоко\"]`,\n",
    "`for name in stock`, а поставка добавляется оператором `+`.\n",
    "\n",
    "## Шаг 1. Склад-контейнер"
   ],
   "id": "text-10"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 1. Класс Stock\n",
    "\n",
    "Допишите класс `Stock`. `Stock({\"Зёрна\": 8, \"Молоко\": 12})` хранит **копию** словаря «позиция → количество»\n",
    "во внутреннем атрибуте `_items`.\n",
    "\n",
    "- `len(stock)` — число позиций, у которых количество больше нуля;\n",
    "- `stock[\"Молоко\"]` — количество позиции; если позиции нет — 0 (склад не ругается, а говорит «ноль»);\n",
    "- `\"Молоко\" in stock` — `True`, если позиция есть и её количество больше нуля;\n",
    "- `for name in stock` — названия позиций с количеством больше нуля, по алфавиту.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "Копию словаря даёт `dict(items)`; количество отсутствующей позиции — метод словаря со значением по умолчанию.\n",
    "\n",
    "</details>\n",
    "<details><summary>Подсказка 2</summary>\n",
    "\n",
    "`__iter__` — генератор по `sorted(self._items)` с условием на количество.\n",
    "\n",
    "</details>"
   ],
   "id": "stock-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "class Stock:\n",
    "    def __init__(self, items):\n",
    "        ...\n",
    "\n",
    "    def __len__(self):\n",
    "        ...\n",
    "\n",
    "    def __getitem__(self, name):\n",
    "        ...\n",
    "\n",
    "    def __contains__(self, name):\n",
    "        ...\n",
    "\n",
    "    def __iter__(self):\n",
    "        ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "stock"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 1 — выполните после своего решения\n",
    "def _stock():\n",
    "    return Stock({\"Молоко\": 12, \"Зёрна\": 8, \"Сливки\": 0})\n",
    "\n",
    "\n",
    "def test_copy():\n",
    "    \"_items — копия словаря\"\n",
    "    data = {\"Молоко\": 12}\n",
    "    stock = Stock(data)\n",
    "    assert getattr(stock, \"_items\", None) == data, \"количества хранятся во внутреннем словаре _items\"\n",
    "    data[\"Молоко\"] = 0\n",
    "    assert stock[\"Молоко\"] == 12, \"склад изменился вместе с чужим словарём — храните копию: dict(items)\"\n",
    "\n",
    "\n",
    "def test_len_getitem():\n",
    "    \"len и квадратные скобки\"\n",
    "    stock = _stock()\n",
    "    assert len(stock) == 2, f\"len = {len(stock)}, а позиций с количеством больше нуля две — сливок 0, их не считаем\"\n",
    "    assert stock[\"Молоко\"] == 12, f\"stock[\\\"Молоко\\\"] = {stock['Молоко']!r}\"\n",
    "    try:\n",
    "        got = stock[\"Сироп\"]\n",
    "    except KeyError:\n",
    "        assert False, \"stock[\\\"Сироп\\\"] выбросил KeyError, а для отсутствующей позиции нужно 0\"\n",
    "    assert got == 0, f\"stock[\\\"Сироп\\\"] = {got!r}, а нужно 0\"\n",
    "\n",
    "\n",
    "def test_in():\n",
    "    \"in — есть и больше нуля\"\n",
    "    stock = _stock()\n",
    "    assert (\"Молоко\" in stock) is True, \"\\\"Молоко\\\" in stock должно быть True\"\n",
    "    assert (\"Сливки\" in stock) is False, \"сливок 0 — \\\"Сливки\\\" in stock должно быть False\"\n",
    "    assert (\"Сироп\" in stock) is False, \"сиропа нет — \\\"Сироп\\\" in stock должно быть False\"\n",
    "\n",
    "\n",
    "def test_iter():\n",
    "    \"цикл — названия по алфавиту, без нулевых\"\n",
    "    names = list(_stock())\n",
    "    assert names == [\"Зёрна\", \"Молоко\"], f\"цикл по складу дал {names}, а нужно ['Зёрна', 'Молоко'] — по алфавиту и без позиций с нулём\"\n",
    "\n",
    "_check(test_copy, test_len_getitem, test_in, test_iter, names=[\"Stock.__init__\",\"Stock.__len__\",\"Stock.__getitem__\",\"Stock.__contains__\",\"Stock.__iter__\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "stock-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "class Stock:\n",
    "    def __init__(self, items):\n",
    "        self._items = dict(items)\n",
    "\n",
    "    def __len__(self):\n",
    "        return len([name for name in self._items if self._items[name] > 0])\n",
    "\n",
    "    def __getitem__(self, name):\n",
    "        return self._items.get(name, 0)\n",
    "\n",
    "    def __contains__(self, name):\n",
    "        return self[name] > 0\n",
    "\n",
    "    def __iter__(self):\n",
    "        for name in sorted(self._items):\n",
    "            if self._items[name] > 0:\n",
    "                yield name\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "stock-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Шаг 2. Поставка и расход"
   ],
   "id": "text-30"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 2. Сложение и списание\n",
    "\n",
    "Добавьте в класс (скопируйте его из шага 1 целиком):\n",
    "\n",
    "- `__add__`: `stock + supply` — **новый** склад, где количества одинаковых позиций сложены; оба слагаемых не\n",
    "  меняются; сложение с чужим типом — `NotImplemented`;\n",
    "- метод `take(name, amount)` — списать `amount` единиц позиции; если на складе меньше —\n",
    "  `ValueError(\"на складе мало: <название>\")`, и ничего не меняется.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "В `__add__`: начните с `dict(self._items)` и прибавьте к нему количества из `other._items` через `get(name, 0)`.\n",
    "\n",
    "</details>"
   ],
   "id": "supply-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "class Stock:\n",
    "    # скопируйте сюда методы из шага 1\n",
    "\n",
    "    def __add__(self, other):\n",
    "        ...\n",
    "\n",
    "    def take(self, name, amount):\n",
    "        ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "supply"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 2 — выполните после своего решения\n",
    "def _pair():\n",
    "    try:\n",
    "        a, b = Stock({\"Молоко\": 2, \"Зёрна\": 3}), Stock({\"Молоко\": 10, \"Сироп\": 2})\n",
    "    except TypeError:\n",
    "        assert False, \"Stock({...}) не создаётся — скопируйте __init__ из шага 1\"\n",
    "    for name in [\"__len__\", \"__getitem__\", \"__contains__\", \"__iter__\"]:\n",
    "        assert name in Stock.__dict__, f\"в классе нет {name} — скопируйте класс из шага 1 целиком\"\n",
    "    assert a[\"Молоко\"] == 2 and a[\"Сироп\"] == 0 and list(a) == [\"Зёрна\", \"Молоко\"], \"методы шага 1 работают не так, как там — в скопированном классе ошибка из шага 1\"\n",
    "    return a, b\n",
    "\n",
    "\n",
    "def test_add():\n",
    "    \"сложение складов\"\n",
    "    a, b = _pair()\n",
    "    got = a + b\n",
    "    assert isinstance(got, Stock), f\"a + b дал {type(got).__name__}, а нужен новый Stock\"\n",
    "    assert (got[\"Молоко\"], got[\"Зёрна\"], got[\"Сироп\"]) == (12, 3, 2), f\"после сложения молоко, зёрна, сироп = {got['Молоко']}, {got['Зёрна']}, {got['Сироп']}\"\n",
    "    assert a[\"Молоко\"] == 2 and b[\"Молоко\"] == 10, \"сложение изменило слагаемые — создавайте новый склад\"\n",
    "    assert Stock.__dict__[\"__add__\"](a, 5) is NotImplemented, \"сложение с чужим типом должно вернуть NotImplemented\"\n",
    "\n",
    "\n",
    "def test_take():\n",
    "    \"take списывает или выбрасывает ValueError\"\n",
    "    a, _ = _pair()\n",
    "    try:\n",
    "        a.take(\"Сироп\", 1)\n",
    "    except KeyError:\n",
    "        assert False, \"take для отсутствующей позиции упал с KeyError — её количество 0: берите его через self[name] или get\"\n",
    "    except ValueError:\n",
    "        pass\n",
    "    a.take(\"Зёрна\", 2)\n",
    "    assert a[\"Зёрна\"] == 1, f\"после take(\\\"Зёрна\\\", 2) осталось {a['Зёрна']}, а нужно 1\"\n",
    "    for name, amount in [(\"Зёрна\", 5), (\"Сироп\", 1)]:\n",
    "        try:\n",
    "            a.take(name, amount)\n",
    "        except ValueError as e:\n",
    "            assert str(e) == f\"на складе мало: {name}\", f\"сообщение {str(e)!r}\"\n",
    "        else:\n",
    "            assert False, f\"take({name!r}, {amount}) не выбросил ValueError — на складе меньше\"\n",
    "    assert a[\"Зёрна\"] == 1 and a[\"Сироп\"] == 0, \"после неудачного списания склад не должен меняться\"\n",
    "\n",
    "_check(test_add, test_take, names=[\"Stock.__add__\",\"Stock.take\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "supply-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "class Stock:\n",
    "    def __init__(self, items):\n",
    "        self._items = dict(items)\n",
    "\n",
    "    def __len__(self):\n",
    "        return len([name for name in self._items if self._items[name] > 0])\n",
    "\n",
    "    def __getitem__(self, name):\n",
    "        return self._items.get(name, 0)\n",
    "\n",
    "    def __contains__(self, name):\n",
    "        return self[name] > 0\n",
    "\n",
    "    def __iter__(self):\n",
    "        for name in sorted(self._items):\n",
    "            if self._items[name] > 0:\n",
    "                yield name\n",
    "\n",
    "    def __add__(self, other):\n",
    "        if not isinstance(other, Stock):\n",
    "            return NotImplemented\n",
    "        result = dict(self._items)\n",
    "        for name, amount in other._items.items():\n",
    "            result[name] = result.get(name, 0) + amount\n",
    "        return Stock(result)\n",
    "\n",
    "    def take(self, name, amount):\n",
    "        if self[name] < amount:\n",
    "            raise ValueError(f\"на складе мало: {name}\")\n",
    "        self._items[name] -= amount\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "supply-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Шаг 3. Утро"
   ],
   "id": "text-43"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 3. Поставка пришла\n",
    "\n",
    "На складе с вечера — `leftover`, утром пришла поставка `delivery`. Сохраните в `stock` склад после поставки,\n",
    "в `n_positions` — сколько на нём позиций, а в `milk` — сколько молока.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "Склад после поставки — ровно то, ради чего писали `__add__`; число позиций и молоко — встроенные операции склада.\n",
    "\n",
    "</details>"
   ],
   "id": "morning-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "leftover = Stock({\"Зёрна\": 3, \"Молоко\": 2, \"Сироп\": 1, \"Стаканы\": 40, \"Сливки\": 0})\n",
    "delivery = Stock({\"Зёрна\": 5, \"Молоко\": 10, \"Сироп\": 2, \"Стаканы\": 100})\n",
    "stock = ...\n",
    "n_positions = ...\n",
    "milk = ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "morning"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 3 — выполните после своего решения\n",
    "def test_stock():\n",
    "    \"stock — склад после поставки\"\n",
    "    assert isinstance(stock, Stock), f\"stock — это {type(stock).__name__}, а нужен Stock: leftover + delivery\"\n",
    "    assert stock[\"Стаканы\"] == 140, f\"стаканов на складе {stock['Стаканы']}, а должно быть 140 — 40 с вечера и 100 из поставки\"\n",
    "\n",
    "\n",
    "def test_numbers():\n",
    "    \"n_positions и milk\"\n",
    "    assert n_positions != 5, \"5 — вместе со сливками, а их 0: len(stock) считает только позиции больше нуля\"\n",
    "    assert n_positions == 4, f\"n_positions = {n_positions!r}, а позиций на складе 4\"\n",
    "    assert milk == 12, f\"milk = {milk!r}, а молока 12\"\n",
    "\n",
    "_check(test_stock, test_numbers, names=[\"stock\",\"n_positions\",\"milk\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "morning-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "leftover = Stock({\"Зёрна\": 3, \"Молоко\": 2, \"Сироп\": 1, \"Стаканы\": 40, \"Сливки\": 0})\n",
    "delivery = Stock({\"Зёрна\": 5, \"Молоко\": 10, \"Сироп\": 2, \"Стаканы\": 100})\n",
    "stock = leftover + delivery\n",
    "n_positions = len(stock)\n",
    "milk = stock[\"Молоко\"]\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "morning-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Шаг 4. День"
   ],
   "id": "text-52"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 4. Расход за день\n",
    "\n",
    "В заготовке склад `stock` — такой, каким он стал утром после поставки (если выполнить ячейку ещё раз, он\n",
    "создастся заново). В `usage` — что бариста взяли со склада за день, по порядку. Спишите всё по очереди методом `take`;\n",
    "списания, на которые не хватило запасов, пропускайте, а их названия сохраните в `short` — в порядке\n",
    "попыток.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "Цикл по парам `(name, amount)` и `try: stock.take(name, amount)`, `except ValueError: short.append(name)`.\n",
    "\n",
    "</details>"
   ],
   "id": "day-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "stock = Stock({\"Зёрна\": 8, \"Молоко\": 12, \"Сироп\": 3, \"Стаканы\": 140, \"Сливки\": 0})  # склад утром, после поставки\n",
    "usage = [\n",
    "    (\"Молоко\", 5), (\"Стаканы\", 60), (\"Сироп\", 2), (\"Зёрна\", 3),\n",
    "    (\"Стаканы\", 78), (\"Сироп\", 2), (\"Молоко\", 4), (\"Сироп\", 1),\n",
    "]\n",
    "short = ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "day"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 4 — выполните после своего решения\n",
    "def test_short():\n",
    "    \"short — неудавшиеся списания\"\n",
    "    assert isinstance(short, list), f\"short — это {type(short).__name__}, а нужен список названий\"\n",
    "    assert short == [\"Сироп\"], f\"short = {short}, а не хватило только сиропа — один раз. Если ячейка выполнялась несколько раз, склад уже потрачен прошлыми запусками — нажмите «Выполнить все выше» и выполните её один раз\"\n",
    "\n",
    "\n",
    "def test_left():\n",
    "    \"остатки после дня\"\n",
    "    left = {name: stock[name] for name in [\"Зёрна\", \"Молоко\", \"Сироп\", \"Стаканы\"]}\n",
    "    assert left == {\"Зёрна\": 5, \"Молоко\": 3, \"Сироп\": 0, \"Стаканы\": 2}, f\"остатки после дня: {left} — спишите все списания из usage по порядку\"\n",
    "\n",
    "_check(test_short, test_left, names=[\"short\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "day-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "stock = Stock({\"Зёрна\": 8, \"Молоко\": 12, \"Сироп\": 3, \"Стаканы\": 140, \"Сливки\": 0})  # склад утром, после поставки\n",
    "usage = [\n",
    "    (\"Молоко\", 5), (\"Стаканы\", 60), (\"Сироп\", 2), (\"Зёрна\", 3),\n",
    "    (\"Стаканы\", 78), (\"Сироп\", 2), (\"Молоко\", 4), (\"Сироп\", 1),\n",
    "]\n",
    "short = []\n",
    "for name, amount in usage:\n",
    "    try:\n",
    "        stock.take(name, amount)\n",
    "    except ValueError:\n",
    "        short.append(name)\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "day-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Шаг 5. Вечерний отчёт"
   ],
   "id": "text-63"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Упражнение 5. Что заказать\n",
    "\n",
    "Сохраните в `low` названия позиций, которых осталось меньше 5, — в порядке обхода склада циклом, а в\n",
    "`out_of_syrup` — `True`, если сиропа на складе не осталось.\n",
    "\n",
    "<details><summary>Подсказка 1</summary>\n",
    "\n",
    "Цикл по складу выдаёт названия, а количество даёт `stock[name]`.\n",
    "\n",
    "</details>\n",
    "<details><summary>Подсказка 2</summary>\n",
    "\n",
    "«Сиропа не осталось» — `\"Сироп\" not in stock`.\n",
    "\n",
    "</details>"
   ],
   "id": "report-task"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "low = ...\n",
    "out_of_syrup = ..."
   ],
   "execution_count": null,
   "outputs": [],
   "id": "report"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Проверка упражнения 5 — выполните после своего решения\n",
    "def test_low():\n",
    "    \"low — чего мало\"\n",
    "    assert isinstance(low, list), f\"low — это {type(low).__name__}, а нужен список названий\"\n",
    "    assert \"Сироп\" not in low, \"сиропа 0 — цикл по складу его не выдаёт, а в low нужны позиции, которые ещё есть, но мало\"\n",
    "    assert low == [\"Молоко\", \"Стаканы\"], f\"low = {low}, а меньше 5 осталось молока и стаканов\"\n",
    "\n",
    "\n",
    "def test_syrup():\n",
    "    \"out_of_syrup\"\n",
    "    assert out_of_syrup is True, f\"out_of_syrup = {out_of_syrup!r}, а сиропа не осталось\"\n",
    "\n",
    "_check(test_low, test_syrup, names=[\"low\",\"out_of_syrup\"])"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "report-check"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details><summary>Решение</summary>\n",
    "\n",
    "```python\n",
    "low = [name for name in stock if stock[name] < 5]\n",
    "out_of_syrup = \"Сироп\" not in stock\n",
    "```\n",
    "\n",
    "</details>"
   ],
   "id": "report-solution"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Итог"
   ],
   "id": "text-73"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Склад вечером\n",
    "print(f\"позиций: {len(stock)}\")\n",
    "for name in stock:\n",
    "    mark = \" ← заказать\" if name in low else \"\"\n",
    "    print(f\"{name}: {stock[name]}{mark}\")\n",
    "print(\"не хватило:\", \", \".join(short), \"| сироп закончился:\", out_of_syrup)"
   ],
   "execution_count": null,
   "outputs": [],
   "id": "summary"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "*После решения всех упражнений урока:*\n",
    "\n",
    "Утренняя поставка закрыла почти всё, но сиропа хватило ненадолго: 3 бутылки к вечеру ушли полностью, и одно\n",
    "списание не прошло. Мало осталось молока и стаканов — их стоит заказать первыми."
   ],
   "id": "component-78"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрите на код шагов 3–5: в нём нет ни одного обращения к `_items`. Склад ведёт себя как встроенный\n",
    "контейнер, а как он хранит данные — его внутреннее дело. В последнем модуле перед итоговым проектом появятся\n",
    "dataclass — способ писать такие классы данных ещё короче."
   ],
   "id": "text-82"
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  },
  "colab": {
   "provenance": []
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
