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
143 changes: 82 additions & 61 deletions src/part2/cart.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,26 @@
"""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
from .storage import (
NAME_INDEX,
PRICE_INDEX,
PRODUCT_ID_INDEX,
QUANTITY_INDEX,
Product,
)


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) -> CartLine | None:
for line in cart:
if line[LINE_PRODUCT_ID_INDEX] == product_id:
return line
return None


def add_to_cart(
Expand All @@ -33,28 +29,39 @@ 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

if quantity <= 0:
print(f"quantity must be positive, got {quantity}")
return None

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

for i in range(len(storage)):
if storage[i][PRODUCT_ID_INDEX] == product_id:
storage[i] = (
storage[i][PRODUCT_ID_INDEX],
storage[i][NAME_INDEX],
storage[i][PRICE_INDEX],
storage[i][QUANTITY_INDEX] - quantity,
)
break

line = find_cart_line(cart, product_id)
if line is None:
cart.append((product_id, quantity))
return cart[-1]

idx = cart.index(line)
cart[idx] = (product_id, line[LINE_QUANTITY_INDEX] + quantity)
return cart[idx]


def remove_from_cart(
Expand All @@ -63,27 +70,41 @@ 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)

if quantity <= 0:
print(f"quantity must be positive, got {quantity}")
return None

line = find_cart_line(cart, product_id)
if line is None:
print(f"product {product_id} is not in the cart")
return None

if line[LINE_QUANTITY_INDEX] < quantity:
print(
f"cart holds only {line[LINE_QUANTITY_INDEX]} unit(s) of "
f"product {product_id}, cannot remove {quantity}"
)
return None

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

for i in range(len(storage)):
if storage[i][PRODUCT_ID_INDEX] == product_id:
storage[i] = (
storage[i][PRODUCT_ID_INDEX],
storage[i][NAME_INDEX],
storage[i][PRICE_INDEX],
storage[i][QUANTITY_INDEX] + quantity,
)
break

new_quantity = line[LINE_QUANTITY_INDEX] - quantity
idx = cart.index(line)
if new_quantity == 0:
del cart[idx]
return (product_id, 0)
cart[idx] = (product_id, new_quantity)
return cart[idx]
122 changes: 37 additions & 85 deletions src/part2/crud.py
Original file line number Diff line number Diff line change
@@ -1,120 +1,72 @@
"""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 PRODUCT_ID_MIN
return max(product[PRODUCT_ID_INDEX] for product in storage) + 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 _find_index(storage: list[Product], product_id: int) -> int | None:
for i, product in enumerate(storage):
if product[PRODUCT_ID_INDEX] == product_id:
return i
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.
``price`` is a :class:`~decimal.Decimal` amount and is rounded
to the stored money precision before it is saved.
name, price, quantity = fields
if any(product[NAME_INDEX] == name for product in storage):
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
new_id = generate_product_id(storage)
storage.append((new_id, name, normalize_price(price), quantity))
return new_id


def read_product(storage: list[Product], product_id: int) -> Product | None:
"""Return the product stored under ``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)
idx = _find_index(storage, product_id)
if idx is None:
print(f"no product with id {product_id}")
return None
return storage[idx]


def update_product(
storage: list[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.
idx = _find_index(storage, product_id)
if idx is None:
print(f"no product with id {product_id}")
return None

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)
name, price, quantity = fields
storage[idx] = (product_id, name, normalize_price(price), quantity)
return storage[idx]


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

idx = _find_index(storage, product_id)
if idx is None:
print(f"no product with id {product_id}")
return None

del storage[idx]
return product_id
32 changes: 6 additions & 26 deletions src/part2/storage.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,14 @@
"""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
Loading
Loading