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
187 changes: 86 additions & 101 deletions src/part3/crud.py
Original file line number Diff line number Diff line change
@@ -1,131 +1,116 @@
"""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 check_product(storage: list[Product], product_name: str) -> bool | None:
if not (product_name):
print("product name must not be blank")
return None

for product in storage:
if product_name == product[NAME_INDEX]:
print(f"product name '{product_name}' is already taken")
return None
return True


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

Args:
storage: The product store to inspect.
max_id = max(product[PRODUCT_ID_INDEX] for product in storage)

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
return max_id + 1


def create_product(
storage: list[Product], fields: tuple[str, Decimal, int]
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
product_name, product_price, product_quantity = fields

normalized_name = normalize_product_name(product_name)
if (status := check_product(storage, normalized_name)) is None:
return status

def read_product(storage: list[Product], product_id: int) -> Product | None:
"""Return the product stored under ``product_id``.
product_id = generate_product_id(storage)
normalized_price = normalize_price(product_price)

storage.append(
(
product_id,
normalized_name,
normalized_price,
product_quantity,
)
)

Args:
storage: The product store to search.
product_id: The identifier to look up.
return product_id

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:
for product in storage:
if product[PRODUCT_ID_INDEX] == product_id:
return product

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)
product_name, product_price, product_quantity = fields
normalized_name = normalize_product_name(product_name)

if not normalized_name:
print("product name must not be blank")
return None

product_index = None

for index, product in enumerate(storage):
if product[PRODUCT_ID_INDEX] == product_id:
product_index = index
break

if product_index is None:
print(f"no product with id {product_id}")
return None

for product in storage:
if (
product[PRODUCT_ID_INDEX] != product_id
and product[NAME_INDEX] == normalized_name
):
print(f"product name '{normalized_name}' is already taken")
return None

new_product: Product = (
product_id,
normalized_name,
normalize_price(product_price),
product_quantity,
)

storage[product_index] = 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
for index, product in enumerate(storage):
if product[PRODUCT_ID_INDEX] == product_id:
storage.pop(index)
return product_id

print(f"no product with id {product_id}")
return None
135 changes: 56 additions & 79 deletions src/part3/run_admin.py
Original file line number Diff line number Diff line change
@@ -1,107 +1,84 @@
"""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

def show_help() -> None:
"""Print the command reference to stdout.
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."""

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: реализуйте функцию
def print_result(result: object) -> None:
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.
parts = line.split()

match parts:
case ["help"]:
show_help()

case ["exit"]:
return False

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

case ["create", *args] if len(args) >= 3:
name = " ".join(args[:-2])
price = Decimal(args[-2])
quantity = int(args[-1])
print_result(create_product(storage, (name, price, quantity)))

case ["read", product_id]:
print_result(read_product(storage, int(product_id)))

case ["update", product_id, *args] if len(args) >= 3:
name = " ".join(args[:-2])
price = Decimal(args[-2])
quantity = int(args[-1])
result = update_product(
storage,
int(product_id),
(name, price, quantity),
)
print_result(result)

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 ["delete", product_id]:
print_result(delete_product(storage, int(product_id)))

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 _:
print(f"'{line}' is not a command")

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
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
Loading
Loading