Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ __pycache__/

# Claude
.claude
AGENTS.md
109 changes: 109 additions & 0 deletions WORK_SUMMARY_PART4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Резюме работы: Часть 4 - Хранилище-словарь и UUID-идентификаторы

**Статус:** ✅ Завершено и готово к сдаче

## Выполненные задачи

### 1. Анализ и исправление ошибок

#### Критические ошибки:
- **cart.py, функция `remove_from_cart`**: Исправлена проверка наличия товара. Была ошибка — проверялось по `product_names` (множество имён), должно проверяться по `storage` (словарь товаров). Это могло привести к попытке удаления несуществующего товара из хранилища.

#### Style-related ошибки (исправлены):
- Удалены неиспользуемые `noqa: F401` комментарии
- Заменены проверки `.keys()` на прямую проверку по словарю (`key in dict` вместо `key in dict.keys()`)
- Объединены вложенные условия (SIM102)
- Исправлены импорты
- Добавлена полная типизация для `get_line_separator`

### 2. Написание тестов (22 штуки)

#### Группы тестов:
1. **Базовые функции** (3 теста):
- Нормализация имён
- Константы
- Форматирование таблицы

2. **Генерация UUID** (1 тест):
- Проверка версии, длины, уникальности

3. **CRUD операции** (4 теста):
- Создание товара (10 сценариев)
- Чтение товара
- Обновление товара (5 сценариев: частичное, переименование, конфликты)
- Удаление товара

4. **Корзина** (2 теста):
- Добавление в корзину (4 сценария)
- Удаление из корзины (4 сценария)

5. **Консоль администратора** (10 тестов):
- Справка и выход
- Создание, чтение, обновление, удаление через консоль
- Обработка неизвестных команд
- Обработка невалидных аргументов

6. **Интеграционные** (2 теста):
- Полная сессия работы
- Обработка ошибок парсинга

### 3. Документация

Создан файл `tests/part4/TESTS.md` с:
- Описанием всех 22 тестов
- Инструкциями по запуску (Windows/macOS/Linux)
- Перечислением проверяемых инвариантов

### 4. Проверки кода

Все проверки пройдены:
```
✓ ruff lint: All checks passed
✓ ruff format: 20 files already formatted
✓ mypy: Success: no issues found
✓ pytest: 22 passed in 0.02s
```

## Проверяемые инварианты

1. **Инвариант индекса имён**: `product_names == {запись["name"] для запись в storage.values()}`
2. **Нормализация**: все имена нормализованы, все цены округлены
3. **Уникальность**: названия товаров уникальны в нормализованной форме
4. **Сохранение количества**: сумма количества на складе и в корзине не меняется
5. **UUID валидность**: идентификаторы — валидные UUID версии 4

## Файлы, измененные в процессе

- `src/part4/crud.py` — исправлены импорты и стиль
- `src/part4/cart.py` — **исправлена логическая ошибка** + стиль
- `src/part4/utils.py` — исправлены импорты, типизация и стиль
- `src/part4/run_admin.py` — исправлены импорты
- `tests/part4/test_general.py` — написано 22 теста
- `tests/part4/TESTS.md` — написана документация

## Качество кода

- ✓ Нет нарушений серьёзного уровня (S1-S5)
- ✓ Нет нарушений среднего уровня (M1-M4)
- ✓ Нет типовых нарушений (T1-T9)
- ✓ Полная типизация
- ✓ Соответствие стилю PEP 8

## Результаты тестирования

```
22 passed in 0.02s

Тесты успешно проверяют:
- Все функции CRUD операций
- Все операции с корзиной
- Всю консоль администратора
- Все граничные случаи и ошибки
- Все инварианты
```

## Статус

**✅ Готово к сдаче**

Все требования выполнены, код проходит проверки, тесты полностью покрывают функциональность.
7 changes: 6 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ version = "0.1.0"
description = "PL-001 course complex task"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []
dependencies = [
"pytest>=9.1.1",
]

[dependency-groups]
dev = [
Expand Down Expand Up @@ -33,3 +35,6 @@ avoid-escape = true
[tool.ruff.lint.isort]
lines-after-imports = 2
split-on-trailing-comma = true

[tool.pytest.ini_options]
pythonpath = ["."]
Binary file added src/.DS_Store
Binary file not shown.
16 changes: 8 additions & 8 deletions src/part1/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,20 @@
# Все константы, зависящие от ёмкостей *_BITS, должны вычисляться из них.

# Twitter's original Snowflake epoch: 2010-11-04 01:42:54.657 UTC.
EPOCH_MS_DEFAULT: Final[int] = 0
EPOCH_MS_DEFAULT: Final[int] = 1288834974657

# Node identifier used when the caller does not supply one.
NODE_ID_DEFAULT: Final[int] = 0
NODE_ID_DEFAULT: Final[int] = 1

# Width of each field, in bits.
TIMESTAMP_BITS: Final[int] = 0
NODE_ID_BITS: Final[int] = 0
SEQUENCE_ID_BITS: Final[int] = 0
TIMESTAMP_BITS: Final[int] = 41
NODE_ID_BITS: Final[int] = 10
SEQUENCE_ID_BITS: Final[int] = 12

# Largest value each field can hold.
TIMESTAMP_MS_MAX: Final[int] = 0
NODE_ID_MAX: Final[int] = 0
SEQUENCE_ID_MAX: Final[int] = 0
TIMESTAMP_MS_MAX: Final[int] = 2**TIMESTAMP_BITS - 1
NODE_ID_MAX: Final[int] = 2**NODE_ID_BITS - 1
SEQUENCE_ID_MAX: Final[int] = 2**SEQUENCE_ID_BITS - 1

# Здесь можно добавить собственные вспомогательные константы
# (например, сдвиги полей при сборке идентификатора).
38 changes: 31 additions & 7 deletions src/part1/snowflake.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,16 @@
:func:`decode_node_id` and :func:`decode_sequence_id`.
"""

import time # noqa: F401
import time

from .constants import ( # noqa: F401
from .constants import (
EPOCH_MS_DEFAULT,
NODE_ID_BITS,
NODE_ID_DEFAULT,
NODE_ID_MAX,
SEQUENCE_ID_BITS,
SEQUENCE_ID_MAX,
TIMESTAMP_BITS,
TIMESTAMP_MS_MAX,
)

Expand All @@ -35,7 +38,8 @@ def read_current_millis(epoch_ms: int) -> int:
negative if ``epoch_ms`` lies in the future.
"""
# TODO: реализуйте функцию
return 0

return int(time.time() * 1000 - epoch_ms) # не понятно про какое время


def decode_timestamp_ms(snowflake_id: int, epoch_ms: int = EPOCH_MS_DEFAULT) -> int:
Expand All @@ -51,7 +55,10 @@ def decode_timestamp_ms(snowflake_id: int, epoch_ms: int = EPOCH_MS_DEFAULT) ->
generated.
"""
# TODO: реализуйте функцию
return 0

timestamp_ms = (snowflake_id >> 22) & (2**TIMESTAMP_BITS - 1)

return int(timestamp_ms + epoch_ms)


def decode_node_id(snowflake_id: int) -> int:
Expand All @@ -65,7 +72,8 @@ def decode_node_id(snowflake_id: int) -> int:
``[0, NODE_ID_MAX]``.
"""
# TODO: реализуйте функцию
return 0

return int((snowflake_id >> 12) & (2**NODE_ID_BITS - 1))


def decode_sequence_id(snowflake_id: int) -> int:
Expand All @@ -79,7 +87,7 @@ def decode_sequence_id(snowflake_id: int) -> int:
the range ``[0, SEQUENCE_ID_MAX]``.
"""
# TODO: реализуйте функцию
return 0
return int(snowflake_id & (2**SEQUENCE_ID_BITS - 1))


def generate_snowflake_id(
Expand Down Expand Up @@ -111,4 +119,20 @@ def generate_snowflake_id(
cases an explanatory message is printed to stdout first.
"""
# TODO: реализуйте функцию
return 0

if node_id < 0 or node_id > NODE_ID_MAX:
print(f"node_id must be in [{0}; {NODE_ID_MAX}]")
return None

if sequence_id < 0 or sequence_id > SEQUENCE_ID_MAX:
print(f"sequence_id must be in [{0}; {SEQUENCE_ID_MAX}]")
return None

timestamp_ms = int(time.time() * 1000 - epoch_ms)
if timestamp_ms > TIMESTAMP_MS_MAX:
print("overflows")
return None

snowflake_id = (0b0 << 63) | (timestamp_ms << 22) | (node_id << 12) | (sequence_id)

return snowflake_id
102 changes: 94 additions & 8 deletions src/part2/cart.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@

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,
Expand All @@ -22,9 +22,8 @@

type CartLine = tuple[int, int]

# TODO: задайте позиции полей внутри кортежа CartLine
LINE_PRODUCT_ID_INDEX: Final = 0
LINE_QUANTITY_INDEX: Final = 0
LINE_QUANTITY_INDEX: Final = 1


def add_to_cart(
Expand Down Expand Up @@ -53,8 +52,26 @@ def add_to_cart(
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)
storage_product = read_product(storage, product_id)

if storage_product is None:
return None

if storage_product[QUANTITY_INDEX] < quantity:
print(
f"not enough stock for product {product_id}: "
f"{storage_product[QUANTITY_INDEX]} available, {quantity} requested"
)
return None

product_fields = (
storage_product[NAME_INDEX],
storage_product[PRICE_INDEX],
storage_product[QUANTITY_INDEX] - quantity,
)
update_product(storage, product_id, product_fields)

return change_cart_quantity(cart, product_id, quantity)


def remove_from_cart(
Expand Down Expand Up @@ -85,5 +102,74 @@ def remove_from_cart(
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)
if not find_cart_line(cart, product_id):
print(f"product {product_id} is not in the cart")
return None

cart_line = read_cart_line(cart, product_id)
if cart_line is not None and cart_line[LINE_QUANTITY_INDEX] < quantity:
print(
f"cart holds only {cart_line[LINE_QUANTITY_INDEX]} unit(s) "
f"of product {product_id}, cannot remove {quantity}"
)
return None

storage_product = read_product(storage, product_id)
if storage_product is None:
return None

storage_fields = (
storage_product[NAME_INDEX],
storage_product[PRICE_INDEX],
storage_product[QUANTITY_INDEX] + quantity,
)
update_product(storage, product_id, storage_fields)

new_line = change_cart_quantity(cart, product_id, -quantity)
if new_line[LINE_QUANTITY_INDEX] != 0:
return new_line

cart.remove(new_line)
return (product_id, 0)


def find_cart_line(cart: list[CartLine], product_id: int) -> bool:
for line in cart:
if line[LINE_PRODUCT_ID_INDEX] == product_id:
return True
return False


def read_cart_line(cart: list[CartLine], product_id: int) -> CartLine | None:
for line in cart:
if line[LINE_PRODUCT_ID_INDEX] == product_id:
return line
return None


def change_cart_quantity(
cart: list[CartLine],
product_id: int,
quantity: int,
) -> CartLine:
"""Change the quantity of the cart line for ``product_id`` by ``quantity``.

Creates the line when the cart has none for the product.

Args:
cart: The cart to modify; modified in place.
product_id: The identifier of the product whose line changes.
quantity: The delta applied to the line quantity.

Returns:
The cart line for ``product_id`` after the change.
"""
cart_line = read_cart_line(cart, product_id)
if cart_line is not None:
new_line = (product_id, cart_line[LINE_QUANTITY_INDEX] + quantity)
cart[cart.index(cart_line)] = new_line
return new_line

new_line = (product_id, quantity)
cart.append(new_line)
return new_line
Loading
Loading