Skip to content
Closed
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
160 changes: 57 additions & 103 deletions src/part3/crud.py
Original file line number Diff line number Diff line change
@@ -1,131 +1,85 @@
"""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``.

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
if len(storage) == 0:
return PRODUCT_ID_MIN
return max(p[PRODUCT_ID_INDEX] for p in storage) + 1


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 = normalize_product_name(fields[0])

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Магические константы
Пункт S3

price = normalize_price(fields[1])
quantity = fields[2]
if not name:
print("product name must not be blank")
return None
for cor in storage:
if cor[NAME_INDEX] == name:
print(f"product name '{name}' is already taken")
return None
new_id = generate_product_id(storage)
storage.append((new_id, name, 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: реализуйте функцию (используйте своё решение части 2)
return (0, "", Decimal(0), 0)
for cor in storage:
if cor[PRODUCT_ID_INDEX] == product_id:
return cor
print(f"no product with id {product_id}")
return None


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.

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 = normalize_product_name(fields[0])
price = normalize_price(fields[1])
quantity = fields[2]
if not name:
print("product name must not be blank")
return None
index_id = next(
(i for i, p in enumerate(storage) if p[PRODUCT_ID_INDEX] == product_id), None
)
if index_id is None:
print(f"no product with id {product_id}")
return None
index_name = next(

@Avshugan Avshugan Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ну это уже совсем не читаемый код. Лучше было бы написать нормальный for
Минус 1 балл

(
i
for i, p in enumerate(storage)
if (p[NAME_INDEX] == name) and (p[PRODUCT_ID_INDEX] != product_id)
),
None,
)
if index_name is not None:
print(f"product name '{name}' is already taken")
return None
fields_new = (product_id, name, price, quantity)
storage[index_id] = fields_new
return fields_new


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
index = next(
(i for i, p in enumerate(storage) if p[PRODUCT_ID_INDEX] == product_id), None
)
if index is None:
print(f"no product with id {product_id}")
return None
storage.pop(index)
return product_id
145 changes: 63 additions & 82 deletions src/part3/run_admin.py
Original file line number Diff line number Diff line change
@@ -1,107 +1,88 @@
"""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 <name...> <price> <quantity>`` -- add a product;
* ``read <id>`` -- print one product;
* ``update <id> <name...> <price> <quantity>`` -- overwrite a product;
* ``delete <id>`` -- 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


# TODO: задайте приглашение и текст справки
PROMPT: Final[str] = ""
HELP_TEXT: Final[str] = ""
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 <name...> <price> <quantity> add a product, print its new id
read <id> print the product with that id
update <id> <name...> <price> <quantity> overwrite that product's fields
delete <id> 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."""

def show_help() -> None:
"""Print the command reference to stdout.

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 show_help() -> None:
print(HELP_TEXT)


def print_result(result: object) -> None:
"""Print a CRUD result to stdout unless it is ``None``.

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: реализуйте функцию
if result is not None:
print(result)


def run_command(storage: list[Product], line: str) -> bool:
"""Parse one console line and carry out the command it names.

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.

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``.

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
parts = line.split()

match parts:
case ["exit"]:
return False

case ["help"]:
show_help()
return True

case ["show"]:
print(get_storage_str_representation(storage))
return True

case ["create", *name_parts, price_str, quantity_str] if name_parts:
name = " ".join(name_parts)
price = Decimal(price_str)
quantity = int(quantity_str)
print_result(create_product(storage, (name, price, quantity)))
return True

case ["read", product_id_str]:
product_id = int(product_id_str)
print_result(read_product(storage, product_id))
return True

case ["update", product_id_str, *name_parts, price_str, quantity_str] if (
name_parts
):
product_id = int(product_id_str)
name = " ".join(name_parts)
price = Decimal(price_str)
quantity = int(quantity_str)
print_result(update_product(storage, product_id, (name, price, quantity)))
return True

case ["delete", product_id_str]:
product_id = int(product_id_str)
print_result(delete_product(storage, product_id))
return True

case _:
print(f"'{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

Expand Down
31 changes: 4 additions & 27 deletions src/part3/storage.py
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading