From b046005ce14cc7d2f311d01ddb9e6d2e30e4b726 Mon Sep 17 00:00:00 2001 From: Luminol Date: Mon, 12 Oct 2026 21:57:02 +0300 Subject: [PATCH 1/2] Solve Task 3 --- src/__init__.py | 0 src/part1/TASK.md | 156 ------------------------------ src/part2/TASK.md | 213 ----------------------------------------- src/part2/__init__.py | 1 - src/part2/cart.py | 136 ++++++++++++++------------ src/part2/crud.py | 130 +++++++++---------------- src/part2/storage.py | 30 +----- src/part2/utils.py | 28 ++---- src/part3/crud.py | 166 +++++++++++++------------------- src/part3/run_admin.py | 136 +++++++++++--------------- src/part3/storage.py | 31 +----- src/part3/utils.py | 115 +++++++++++----------- 12 files changed, 312 insertions(+), 830 deletions(-) delete mode 100644 src/__init__.py delete mode 100644 src/part1/TASK.md delete mode 100644 src/part2/TASK.md delete mode 100644 src/part2/__init__.py diff --git a/src/__init__.py b/src/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/part1/TASK.md b/src/part1/TASK.md deleted file mode 100644 index ef8e0faf..00000000 --- a/src/part1/TASK.md +++ /dev/null @@ -1,156 +0,0 @@ -# Задача: генератор идентификаторов Twitter Snowflake - -## Легенда - -В ходе курса мы шаг за шагом построим собственное хранилище данных — небольшую -базу, в которую можно складывать записи и потом быстро находить их по ключу. -Начнём с фундамента: у каждой записи должен быть уникальный идентификатор. - -Казалось бы, можно просто нумеровать записи 1, 2, 3, … Пока хранилище живёт на -одной машине, так и происходит. Но реальные системы почти всегда распределены: -записи создаются на десятках серверов одновременно. Тогда возникают проблемы: - -- единый счётчик становится «бутылочным горлышком»: каждому серверу приходится - обращаться к нему за очередным номером; -- если серверы нумеруют записи независимо друг от друга, идентификаторы - неизбежно начнут совпадать; -- случайные идентификаторы (например, UUID) не совпадают, но плохо упорядочены: - по ним нельзя понять, какая запись появилась раньше, а индексы по таким - ключам работают медленнее. - -В 2010 году с этой же проблемой столкнулся Twitter: твиты создавались сотнями -тысяч в секунду на множестве машин, а идентификаторы должны были быть уникальными, -компактными (помещаться в 64 бита) и примерно упорядоченными по времени. Решение -получило название **Snowflake** («снежинка» — как и снежинки, такие идентификаторы -не повторяются). Идея проста: вместо одного числа-счётчика упаковать в одно -целое несколько независимых величин, и тогда серверам не нужно договариваться друг -с другом. - -Именно такой генератор нам и предстоит написать. В будущем он станет основой нашего -хранилища: каждая запись получит свой Snowflake-идентификатор, по которому можно -будет узнать, когда и на каком узле она была создана. - -## Контекст - -Нужно реализовать генератор 64-битных числовых идентификаторов в стиле [Twitter -Snowflake](https://en.wikipedia.org/wiki/Snowflake_ID). Такие идентификаторы примерно упорядочены по времени и могут -выдаваться разными узлами без какой-либо координации между ними. Каждый -идентификатор объединяет в одном целом положительном числе три величины: - -- сколько миллисекунд прошло с некоторого «нулевого момента» (пользовательской - эпохи); -- номер узла, который выдал идентификатор; -- порядковый номер идентификатора внутри одной миллисекунды. - -Реализация состоит из двух модулей: `constants.py` (формат идентификатора и -значения по умолчанию) и `snowflake.py` (генерация и разбор идентификаторов). - -## Формат идентификатора - -Идентификатор — целое число, которое всегда неотрицательно и помещается в -знаковый 64-битный тип. Логически оно состоит из четырёх частей, перечисленных -от старших разрядов к младшим: - -| Часть | Ёмкость | Что хранит | -| ------------------------- | ------- | --------------------------------------------------------------- | -| знаковый разряд | 1 бит | всегда 0 — идентификатор положительный | -| метка времени | 41 бит | число миллисекунд, прошедших с момента пользовательской эпохи | -| номер узла | 10 бит | идентификатор машины, выдавшей идентификатор | -| счётчик последовательности | 12 бит | порядковый номер идентификатора в пределах одной миллисекунды | - -Из ёмкости каждой части вытекает диапазон допустимых значений: часть шириной -`n` бит вмещает значения от `0` до `2**n - 1` включительно. - -## Модуль `constants.py` - -Необходимо определить следующие константы: - -- `EPOCH_MS_DEFAULT` — пользовательская эпоха по умолчанию, заданная в - миллисекундах Unix-времени. Значение — оригинальная эпоха Twitter, - 2010-11-04 01:42:54.657 UTC, то есть `1288834974657`. -- `NODE_ID_DEFAULT` — номер узла, который используется, когда вызывающий код не - передал свой. Значение — `1`. -- `TIMESTAMP_BITS`, `NODE_ID_BITS`, `SEQUENCE_ID_BITS` — ёмкости соответствующих - частей идентификатора в битах: `41`, `10` и `12`. -- `TIMESTAMP_MS_MAX`, `NODE_ID_MAX`, `SEQUENCE_ID_MAX` — наибольшее значение, - которое помещается в соответствующую часть; вычисляются из её ёмкости, а не - задаются числом напрямую. - -Любые дополнительные вспомогательные константы, которые понадобятся вам для -сборки и разбора идентификатора, добавьте на своё усмотрение. Изменение любой из -ёмкостей `*_BITS` должно автоматически согласованно менять зависящие от неё -константы. - -## Модуль `snowflake.py` - -### `read_current_millis(epoch_ms: int) -> int` - -- **Вход:** `epoch_ms` — момент пользовательской эпохи в миллисекундах - Unix-времени. -- **Выход:** число целых миллисекунд, прошедших между `epoch_ms` и текущим - моментом. - -### `decode_timestamp_ms(snowflake_id: int, epoch_ms: int = EPOCH_MS_DEFAULT) -> int` - -- **Вход:** `snowflake_id` — ранее сгенерированный идентификатор; `epoch_ms` — - эпоха, относительно которой он был создан (по умолчанию `EPOCH_MS_DEFAULT`). -- **Выход:** абсолютное Unix-время в миллисекундах, в которое был создан - идентификатор, то есть хранящаяся в нём метка времени плюс `epoch_ms`. - -### `decode_node_id(snowflake_id: int) -> int` - -- **Вход:** идентификатор. -- **Выход:** номер узла, записанный в идентификаторе. Всегда в диапазоне - `[0, NODE_ID_MAX]`. - -### `decode_sequence_id(snowflake_id: int) -> int` - -- **Вход:** идентификатор. -- **Выход:** счётчик последовательности, записанный в идентификаторе. Всегда в - диапазоне `[0, SEQUENCE_ID_MAX]`. - -### `generate_snowflake_id(sequence_id: int, node_id: int = NODE_ID_DEFAULT, epoch_ms: int = EPOCH_MS_DEFAULT) -> int | None` - -Собирает идентификатор для текущей миллисекунды. - -Функция не хранит состояния: значение `sequence_id` на каждом вызове передаёт -вызывающий код. Именно он отвечает за то, чтобы увеличивать счётчик для -идентификаторов, выданных в одну и ту же миллисекунду, и обнулять его, когда -часы перешли на следующую миллисекунду. - -**Входные параметры:** - -- `sequence_id` — счётчик в пределах миллисекунды; ожидается в диапазоне - `[0, SEQUENCE_ID_MAX]`. -- `node_id` — номер текущего узла; ожидается в диапазоне `[0, NODE_ID_MAX]`. - Необязательный, по умолчанию `NODE_ID_DEFAULT`. -- `epoch_ms` — начало эпохи в миллисекундах Unix-времени. Необязательный, по - умолчанию `EPOCH_MS_DEFAULT`. - -**Поведение и результат.** Проверки выполняются в перечисленном порядке; при -первой же неудачной проверке остальные не выполняются, в stdout печатается -поясняющее сообщение и функция возвращает `None`: - -1. Если `node_id` вне `[0, NODE_ID_MAX]` — сообщение содержит текст - `node_id must be in`, с указанием допустимого диапазона и полученного - значения. -2. Иначе если `sequence_id` вне `[0, SEQUENCE_ID_MAX]` — сообщение содержит - текст `sequence_id must be in`, с указанием допустимого диапазона и - полученного значения. -3. Иначе вычисляется число миллисекунд, прошедших с эпохи. Если оно превышает - `TIMESTAMP_MS_MAX` (то есть с момента эпохи прошло больше ~69 лет) — - сообщение содержит слово `overflows`. - -Если все проверки пройдены, функция возвращает идентификатор — положительное -целое, помещающееся в знаковый 64-битный тип (`0 < id < 2**63`), в котором -закодированы текущая метка времени, `node_id` и `sequence_id`. - -### Взаимная обратимость - -Для любого набора аргументов, на котором `generate_snowflake_id` возвращает не -`None`, декодеры возвращают исходные значения: - -- `decode_node_id(generate_snowflake_id(s, n, e)) == n` -- `decode_sequence_id(generate_snowflake_id(s, n, e)) == s` -- `decode_timestamp_ms(generate_snowflake_id(s, n, e), e)` равно абсолютному - Unix-времени того момента, в который был вызван генератор. diff --git a/src/part2/TASK.md b/src/part2/TASK.md deleted file mode 100644 index 25736d9c..00000000 --- a/src/part2/TASK.md +++ /dev/null @@ -1,213 +0,0 @@ -# Задача: 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 deleted file mode 100644 index e953a49e..00000000 --- a/src/part2/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""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 index 063f9aea..88cd6789 100644 --- a/src/part2/cart.py +++ b/src/part2/cart.py @@ -1,18 +1,7 @@ -"""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 +from .crud import read_product, update_product +from .storage import ( NAME_INDEX, PRICE_INDEX, QUANTITY_INDEX, @@ -22,9 +11,17 @@ type CartLine = tuple[int, int] -# TODO: задайте позиции полей внутри кортежа CartLine + LINE_PRODUCT_ID_INDEX: Final = 0 -LINE_QUANTITY_INDEX: Final = 0 +LINE_QUANTITY_INDEX: Final = 1 + + +def find_cart_line(cart: list[CartLine], product_id: int) -> int | None: + for cartline in cart: + if cartline[LINE_PRODUCT_ID_INDEX] == product_id: + return cart.index(cartline) + + return None def add_to_cart( @@ -33,28 +30,34 @@ def add_to_cart( 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) + product = read_product(storage, product_id) + if product is None: + return None + + storage_quantity = product[QUANTITY_INDEX] + if quantity > storage_quantity: + print( + f"not enough stock for product {product_id}: {product[QUANTITY_INDEX]} available, {quantity} requested" + ) + return None + + fields = ( + product[NAME_INDEX], + product[PRICE_INDEX], + product[QUANTITY_INDEX] - quantity, + ) + + update_product(storage, product_id, fields) + + line_index = find_cart_line(cart, product_id) + if line_index is not None: + line: CartLine = (product_id, cart[line_index][LINE_QUANTITY_INDEX] + quantity) + return cart[line_index] + + line = (product_id, quantity) + cart.append(line) + + return line def remove_from_cart( @@ -63,27 +66,38 @@ def remove_from_cart( 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) + line_index = find_cart_line(cart, product_id) + if line_index is None: + print(f"{product_id} is not in the cart") + return None + + line = cart[line_index] + if line[LINE_QUANTITY_INDEX] < quantity: + print( + f"cart holds only {line[LINE_QUANTITY_INDEX]} unit(s) of product {product_id}, cannot remove {quantity}" + ) + return None + + product = read_product(storage, product_id) + if product is None: + return None + + fields = ( + product[NAME_INDEX], + product[PRICE_INDEX], + product[QUANTITY_INDEX] + quantity, + ) + + update_product(storage, product_id, fields) + + line_index = find_cart_line(cart, product_id) + + if line_index is not None: + new_quantity = cart[line_index][LINE_QUANTITY_INDEX] - quantity + + new_line = (product_id, new_quantity) + + if new_quantity == 0 and line_index is not None: + del cart[line_index] + + return new_line diff --git a/src/part2/crud.py b/src/part2/crud.py index d026cbcb..22a79655 100644 --- a/src/part2/crud.py +++ b/src/part2/crud.py @@ -1,79 +1,55 @@ -"""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 +from .storage import ( NAME_INDEX, PRODUCT_ID_INDEX, PRODUCT_ID_MIN, Product, ) -from .utils import normalize_price # noqa: F401 +from .utils import normalize_price def generate_product_id(storage: list[Product]) -> int: - """Choose the identifier for the next product added to ``storage``. + if not storage: + return int(PRODUCT_ID_MIN) + else: + return int(max(storage)[PRODUCT_ID_INDEX] + 1) - 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 product_search(storage: list[Product], product_id: int) -> Product | None: + for product in storage: + if product[PRODUCT_ID_INDEX] == product_id: + return product + + return None def create_product( storage: list[Product], fields: tuple[str, Decimal, int] ) -> int | None: - """Append a new product to ``storage`` and return its new identifier. + name, price, quantity = fields - 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. + for product in storage: + if name == product[NAME_INDEX]: + print(f"product name {name} is already taken") + return None - 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 + product_id = generate_product_id(storage) + product_price = normalize_price(price) + new_product: Product = (product_id, name, product_price, quantity) + storage.append(new_product) -def read_product(storage: list[Product], product_id: int) -> Product | None: - """Return the product stored under ``product_id``. + return 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 read_product(storage: list[Product], product_id: int) -> Product | None: + product = product_search(storage, product_id) + if product is not None: + return product + + print(f"no product with id {product_id}") + return None def update_product( @@ -81,40 +57,24 @@ def update_product( product_id: int, fields: tuple[str, Decimal, int], ) -> Product | None: - """Overwrite the fields of the product stored under ``product_id``. + name, price, quantity = fields - The identifier itself is preserved; only ``name``, ``price`` and - ``quantity`` are replaced. + product = product_search(storage, product_id) + if product is not None: + new_price = normalize_price(price) + new_product = (product_id, name, new_price, quantity) + storage[storage.index(product)] = new_product + return new_product - 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) + print(f"no product with id {product_id}") + return None 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 + product = product_search(storage, product_id) + if product is not None: + storage.remove(product) + return product_id + + print(f"no product with id {product_id}") + return None diff --git a/src/part2/storage.py b/src/part2/storage.py index b63d5fc7..81651a75 100644 --- a/src/part2/storage.py +++ b/src/part2/storage.py @@ -1,34 +1,12 @@ -"""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 +NAME_INDEX: Final = 1 +PRICE_INDEX: Final = 2 +QUANTITY_INDEX: Final = 3 -# TODO: задайте идентификатор первого товара в пустом хранилище -PRODUCT_ID_MIN: Final[int] = 0 +PRODUCT_ID_MIN: Final[int] = 1 diff --git a/src/part2/utils.py b/src/part2/utils.py index 11bd7a76..ff03fdfb 100644 --- a/src/part2/utils.py +++ b/src/part2/utils.py @@ -1,29 +1,13 @@ -"""Standalone helpers shared by the part2 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 decimal import ROUND_HALF_UP, Decimal from typing import Final -# TODO: задайте число знаков после запятой и шаг квантования -PRICE_PRECISION: Final[int] = 0 -PRICE_STEP: Final = Decimal(0) +PRICE_PRECISION: Final[int] = 2 +PRICE_STEP: Final = Decimal(1).scaleb(-PRICE_PRECISION) 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. + """Для себя: метод number.quantize(step, rounding=round) округляет число + до состояния number.step знаков после запятой с точностью округления round """ - # TODO: реализуйте функцию - return Decimal(0) + return price.quantize(PRICE_STEP, rounding=ROUND_HALF_UP) diff --git a/src/part3/crud.py b/src/part3/crud.py index 68811d7e..1b8f69d0 100644 --- a/src/part3/crud.py +++ b/src/part3/crud.py @@ -1,87 +1,62 @@ -"""Create/read/update/delete operations over the in-memory product store. - -Every operation takes the store -- a list of -:data:`~src.part3.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.part3.storage.PRODUCT_ID_MIN` when the store is empty. - -Every product name that reaches the store is passed through -:func:`~src.part3.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. -""" - from decimal import Decimal -from .storage import ( # noqa: F401 +from .storage import ( NAME_INDEX, PRODUCT_ID_INDEX, PRODUCT_ID_MIN, Product, ) -from .utils import normalize_price, normalize_product_name # noqa: F401 +from .utils import ( + normalize_price, + normalize_product_name, +) def generate_product_id(storage: list[Product]) -> int: - """Choose the identifier for the next product added to ``storage``. + if not storage: + return PRODUCT_ID_MIN + else: + return max(storage)[PRODUCT_ID_INDEX] + 1 - Args: - storage: The product store to inspect. - Returns: - One past the greatest identifier currently held in ``storage``, or - :data:`~src.part3.storage.PRODUCT_ID_MIN` when ``storage`` is - empty. - """ - # TODO: реализуйте функцию (используйте своё решение части 2) - return 0 +def product_search(storage: list[Product], product_id: int) -> Product | None: + for product in storage: + if product[PRODUCT_ID_INDEX] == product_id: + return product + + return None 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. - ``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. - - Returns: - The identifier generated for the new product, or ``None`` when - ``name`` is blank once normalised or a product with the same - normalised name already exists. In the ``None`` case ``storage`` - is left unchanged and an explanatory message is printed. - """ - # TODO: реализуйте функцию - return 0 + name, price, quantity = fields + name = normalize_product_name(name) + if not name: + print("product name must not be blank") + return None -def read_product(storage: list[Product], product_id: int) -> Product | None: - """Return the product stored under ``product_id``. + if any(product[NAME_INDEX] == name for product in storage): + print(f"product name '{name}' is already taken") + return None + + product_id = generate_product_id(storage) + product_price = normalize_price(price) + + new_product: Product = (product_id, name, product_price, quantity) + storage.append(new_product) + + return 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: реализуйте функцию (используйте своё решение части 2) - return (0, "", Decimal(0), 0) +def read_product(storage: list[Product], product_id: int) -> Product | None: + product = product_search(storage, product_id) + if product is not None: + return product + + print(f"no product with id {product_id}") + return None def update_product( @@ -89,43 +64,34 @@ def update_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 ``name`` is blank once normalised, no product - carries that identifier, or the normalised name is already taken - by another product. In every ``None`` case ``storage`` is left - unchanged and an explanatory message is printed. Renaming a - product to its own current name is allowed. - """ - # TODO: реализуйте функцию - return (0, "", Decimal(0), 0) + name, price, quantity = fields + name = normalize_product_name(name) + + if not name: + print("product name must not be blank") + return None + + searched_product = product_search(storage, product_id) + if searched_product is None: + print(f"no product with id {product_id}") + return None + + for product in storage: + if name == product[NAME_INDEX] and product_id != product[PRODUCT_ID_INDEX]: + print(f"product name '{name}' is already taken") + return None + + new_price = normalize_price(price) + new_product = (product_id, name, new_price, quantity) + storage[storage.index(searched_product)] = new_product + return new_product 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: реализуйте функцию (используйте своё решение части 2) - return 0 + product = product_search(storage, product_id) + if product is not None: + storage.remove(product) + return product_id + + print(f"no product with id {product_id}") + return None diff --git a/src/part3/run_admin.py b/src/part3/run_admin.py index c5feeb0b..cb0e8f41 100644 --- a/src/part3/run_admin.py +++ b/src/part3/run_admin.py @@ -1,107 +1,83 @@ -"""Interactive admin console for the part3 in-memory product store. - -Running this module starts a read-eval-print loop over a single product -store that lives in memory for the lifetime of the process. Every line -typed at the prompt is stripped of surrounding whitespace and matched -against the set of known commands: - -* ``help`` -- print the list of commands and what each one does; -* ``exit`` -- leave the loop and end the program; -* ``show`` -- print the whole store as a text table; -* ``create `` -- add a product; -* ``read `` -- print one product; -* ``update `` -- overwrite a product; -* ``delete `` -- remove a product. - -For ``create`` and ``update`` the price and quantity are always the last -two words of the line; everything between the verb (and, for ``update``, -the id) and those two words is joined with single spaces into the -product name, so a multi-word name such as ``Gibson SG Junior`` needs no -quoting. - -The four CRUD commands forward to :mod:`src.part3.crud`; whatever -that call returns is printed unless it is ``None``. A line whose first -word is not a known verb, or that carries the wrong number of arguments -for its verb, prints an "is not a command" notice. A line that names a -command correctly but whose id, price or quantity argument will not -parse prints a distinct "arguments are invalid" notice. Either way the -store is left untouched and the loop keeps running; only ``exit`` stops -it. -""" - -from decimal import Decimal, InvalidOperation # noqa: F401 +from decimal import Decimal, InvalidOperation from typing import Final -from .crud import ( # noqa: F401 +from .crud import ( create_product, delete_product, read_product, update_product, ) from .storage import Product -from .utils import get_storage_str_representation # noqa: F401 +from .utils import get_storage_str_representation + + +PROMPT: Final[str] = "admin>" +HELP_TEXT: Final[str] = """Available commands: + help show this message + exit leave the console + show print the whole store as a table + create add a product, print its new id + read print the product with that id + update overwrite that product's fields + delete remove the product with that id + +For create and update the price and quantity are the last two words of the +line; everything before them is the product name, so it may contain spaces +(for example "Gibson SG Junior") and needs no quoting. +""" -# TODO: задайте приглашение и текст справки -PROMPT: Final[str] = "" -HELP_TEXT: Final[str] = "" +def show_help() -> None: + print(HELP_TEXT) -def show_help() -> None: - """Print the command reference to stdout. +def print_result(result: object) -> None: + if result is not None: + print(result) + return + return - The text is the module-level :data:`HELP_TEXT` constant, printed as - is: a heading, one line per command with a short description, and a - note on how multi-word names are parsed. - """ - # TODO: реализуйте функцию +def run_command(storage: list[Product], line: str) -> bool: + input_line = line.strip() + match input_line.split(): + case ["help"]: + show_help() + return True -def print_result(result: object) -> None: - """Print a CRUD result to stdout unless it is ``None``. + case ["exit"]: + return False - Args: - result: The value returned by a CRUD operation. ``None`` means the - operation already reported its own failure, so nothing is - printed in that case. - """ - # TODO: реализуйте функцию + case ["show"]: + print_result(get_storage_str_representation(storage)) + return True + case ["create", first_name_part, *rest_name, price, quantity]: + name = [first_name_part] + rest_name + fields = (" ".join(name), Decimal(price), int(quantity)) + print_result(create_product(storage, fields)) + return True -def run_command(storage: list[Product], line: str) -> bool: - """Parse one console line and carry out the command it names. + case ["read", product_id]: + print_result(read_product(storage, int(product_id))) + return True - Args: - storage: The product store shared across the session; mutated in - place by the ``create``, ``update`` and ``delete`` commands. - line: One line of console input, already stripped of surrounding - whitespace. + case ["update", product_id, first_name_part, *rest_name, price, quantity]: + name = [first_name_part] + rest_name + fields = (" ".join(name), Decimal(price), int(quantity)) + print_result(update_product(storage, int(product_id), fields)) + return True - Returns: - ``True`` to keep the read-eval-print loop running, or ``False`` - once the ``exit`` command has been seen. A line that is not a - known command prints a notice and still returns ``True``. + case ["delete", product_id]: + print_result(delete_product(storage, int(product_id))) + return True - Raises: - ValueError: If an id or quantity argument of a CRUD command does - not parse as a base-10 integer. - decimal.InvalidOperation: If the price argument of ``create`` or - ``update`` does not parse as a decimal number. - """ - # TODO: реализуйте функцию - return False + case _: + print(f"'{input_line}' is not a command") + return True def main() -> None: - """Run the admin console until the ``exit`` command is entered. - - A single product store is created empty and kept in memory for the - whole session. Each iteration reads one line from stdin, strips its - surrounding whitespace and hands it to :func:`run_command`. A line - that names a command but whose id, price or quantity argument does - not parse is caught here and reported without stopping the loop; only - ``exit`` ends it. - """ storage: list[Product] = [] running = True diff --git a/src/part3/storage.py b/src/part3/storage.py index c5e34be5..81651a75 100644 --- a/src/part3/storage.py +++ b/src/part3/storage.py @@ -1,35 +1,12 @@ -"""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.part3.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.part3.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 (используйте своё решение -# части 2) PRODUCT_ID_INDEX: Final = 0 -NAME_INDEX: Final = 0 -PRICE_INDEX: Final = 0 -QUANTITY_INDEX: Final = 0 +NAME_INDEX: Final = 1 +PRICE_INDEX: Final = 2 +QUANTITY_INDEX: Final = 3 -# TODO: задайте идентификатор первого товара в пустом хранилище -PRODUCT_ID_MIN: Final[int] = 0 +PRODUCT_ID_MIN: Final[int] = 1 diff --git a/src/part3/utils.py b/src/part3/utils.py index 318ded0b..32935d78 100644 --- a/src/part3/utils.py +++ b/src/part3/utils.py @@ -18,78 +18,75 @@ the longest value it holds in that particular call. """ -from decimal import ROUND_HALF_UP, Decimal # noqa: F401 +from decimal import ROUND_HALF_UP, Decimal from typing import Final -from .storage import Product +from .storage import ( + NAME_INDEX, + PRICE_INDEX, + PRODUCT_ID_INDEX, + QUANTITY_INDEX, + Product, +) -# Number of fractional digits every stored price is rounded to. -# Quantisation step derived from PRICE_PRECISION, e.g. Decimal("0.01"). -# TODO: задайте число знаков после запятой и шаг квантования (используйте своё -# решение части 2) -PRICE_PRECISION: Final[int] = 0 -PRICE_STEP: Final = Decimal(0) +PRICE_PRECISION: Final[int] = 2 +PRICE_STEP: Final = Decimal(1).scaleb(-PRICE_PRECISION) -# Column headers of the table produced by get_storage_str_representation, -# left to right. The width of each column is not fixed here -- it is -# measured per call from the data (see the function). -# TODO: задайте заголовки столбцов таблицы -TABLE_HEADERS: Final[tuple[str, ...]] = () +TABLE_HEADERS: Final[tuple[str, ...]] = ("ID", "name", "price", "quantity") 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: реализуйте функцию (используйте своё решение части 2) - return Decimal(0) + return price.quantize(PRICE_STEP, rounding=ROUND_HALF_UP) def normalize_product_name(name: str) -> str: - """Reduce a product name to the canonical form the store keeps. - - Args: - name: The raw product name. - - Returns: - ``name`` with every leading and trailing whitespace character - removed, every internal run of whitespace (spaces, tabs and the - like) collapsed to a single space, and the rest lower-cased. For - example ``"abc def\tghi\tjkl"`` becomes ``"abc def ghi jkl"``. - The result is an empty string when ``name`` holds nothing but - whitespace. - """ - # TODO: реализуйте функцию - return "" + return " ".join(name.split()).lower() -# TODO: при необходимости добавьте свои вспомогательные функции +def number_length(value: Decimal | float) -> int: + return len(str(value)) def get_storage_str_representation(storage: list[Product]) -> str: - """Render the product store as a text table with data-sized columns. - - The table has the columns named by :data:`TABLE_HEADERS` -- id, name, - price and quantity. Each column is made exactly as wide as the longest - value it carries in this call (its header counted), so the columns - line up when the string is printed to a terminal and no value is ever - truncated. - - Args: - storage: The product store to render. - - Returns: - A multi-line string: a header row, a dashed separator row, then - one row per product in ``storage`` in list order. When ``storage`` - is empty only the header and separator rows are returned, sized to - the header labels. - """ - # TODO: реализуйте функцию - return "" + coloumns_lengths_sets: list[set[int]] = [set(), set(), set(), set()] + coloumns_lengths_numbers: list[int] = [0, 0, 0, 0] + + for index in range(len(TABLE_HEADERS)): + coloumns_lengths_sets[index].add(len(TABLE_HEADERS[index])) + + if storage: + for product in storage: + coloumns_lengths_sets[PRODUCT_ID_INDEX].add( + number_length(product[PRODUCT_ID_INDEX]) + ) + coloumns_lengths_sets[NAME_INDEX].add(len(product[NAME_INDEX])) + coloumns_lengths_sets[PRICE_INDEX].add(number_length(product[PRICE_INDEX])) + coloumns_lengths_sets[QUANTITY_INDEX].add( + number_length(product[QUANTITY_INDEX]) + ) + + for coloumn_index in range(len(coloumns_lengths_numbers)): + coloumns_lengths_numbers[coloumn_index] = max( + coloumns_lengths_sets[coloumn_index] + ) + + header_parts = [ + TABLE_HEADERS[index].ljust(coloumns_lengths_numbers[index]) + for index in range(len(TABLE_HEADERS)) + ] + header_line = f"| {' | '.join(header_parts)} |" + + separating_parts = ["-" * (length + 2) for length in coloumns_lengths_numbers] + separating_line = f"|{'|'.join(separating_parts)}|" + + table = [header_line, separating_line] + + if not storage: + return "\n".join(table) + + for product in storage: + line = f"| {' | '.join([str(product[index]).ljust(coloumns_lengths_numbers[index]) for index in range(len(product))])} |" + table.append(line) + + return "\n".join(table) From a7d0088d1ddb6cec66082db600c0e5f8234e650b Mon Sep 17 00:00:00 2001 From: Luminol Date: Mon, 12 Oct 2026 22:07:03 +0300 Subject: [PATCH 2/2] Solve Task 3 --- pyproject.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 06fbe913..4e3290da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,3 +33,8 @@ avoid-escape = true [tool.ruff.lint.isort] lines-after-imports = 2 split-on-trailing-comma = true + +[tool.mypy] +strict = true +mypy_path = "src" +explicit_package_bases = true