Skip to content
Merged
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@

- [Часть 1. Генератор идентификаторов Twitter Snowflake](src/part1/TASK.md) — нужно написать генератор 64-битных числовых идентификаторов, в которые упакованы метка времени, номер узла и счётчик последовательности. Такие идентификаторы уникальны и примерно упорядочены по времени, а из самого идентификатора можно узнать, когда и на каком узле он был создан.
- [Часть 2. CRUD и корзина для интернет-магазина](src/part2/TASK.md) — нужно реализовать хранилище товаров в памяти (список кортежей), операции создания, чтения, изменения и удаления записей, аккуратную работу с ценами через `Decimal` и корзину покупателя, в которую товары переносятся со склада и обратно без потери количества.
- [Часть 3. Нормализация имён и консоль администратора](src/part3/TASK.md) — нужно привести названия товаров к единому написанию и следить за их уникальностью, научить хранилище печатать себя выровненной текстовой таблицей и написать интерактивную консоль администратора, через которую можно создавать, просматривать, изменять и удалять товары.

## Дедлайны

| Задание | Дедлайн (включительно) | Статус |
|---|---|---|
| [Часть 1. Генератор идентификаторов Twitter Snowflake](src/part1/TASK.md) | 25.09.2026 | дедлайн истек |
| [Часть 2. CRUD и корзина для интернет-магазина](src/part2/TASK.md) | 02.10.2026 | дедлайн истек |
| [Часть 3. Нормализация имён и консоль администратора](src/part3/TASK.md) | 12.10.2026 | активно |

## Критерии оценивания

Expand Down
2 changes: 1 addition & 1 deletion src/part2/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Standalone helpers shared by the hw4 storage and CRUD modules.
"""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
Expand Down
459 changes: 459 additions & 0 deletions src/part3/TASK.md

Large diffs are not rendered by default.

Empty file added src/part3/__init__.py
Empty file.
131 changes: 131 additions & 0 deletions src/part3/crud.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"""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
NAME_INDEX,
PRODUCT_ID_INDEX,
PRODUCT_ID_MIN,
Product,
)
from .utils import normalize_price, normalize_product_name # noqa: F401


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


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


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)


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)


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
119 changes: 119 additions & 0 deletions src/part3/run_admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"""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 typing import Final

from .crud import ( # noqa: F401
create_product,
delete_product,
read_product,
update_product,
)
from .storage import Product
from .utils import get_storage_str_representation # noqa: F401


# TODO: задайте приглашение и текст справки
PROMPT: Final[str] = ""
HELP_TEXT: Final[str] = ""


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


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

while running:
line = input(PROMPT).strip()

try:
running = run_command(storage, line)

except (ValueError, InvalidOperation):
print(f"{line!r} names a command but its arguments are invalid")


if __name__ == "__main__":
main()
35 changes: 35 additions & 0 deletions src/part3/storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""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

# TODO: задайте идентификатор первого товара в пустом хранилище
PRODUCT_ID_MIN: Final[int] = 0
Loading
Loading