From 8d824458e70a6593f08702f6e052e299c78aada7 Mon Sep 17 00:00:00 2001 From: Darina ramazanova Date: Thu, 24 Sep 2026 17:52:10 +0300 Subject: [PATCH 01/10] Solve task 1 --- src/part1/constants.py | 16 ++++++++-------- src/part1/snowflake.py | 30 ++++++++++++++++++++++++------ 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/part1/constants.py b/src/part1/constants.py index 9abfbba2..588fe9f8 100644 --- a/src/part1/constants.py +++ b/src/part1/constants.py @@ -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 # Здесь можно добавить собственные вспомогательные константы # (например, сдвиги полей при сборке идентификатора). diff --git a/src/part1/snowflake.py b/src/part1/snowflake.py index d3abf050..7dca1200 100644 --- a/src/part1/snowflake.py +++ b/src/part1/snowflake.py @@ -13,13 +13,16 @@ :func:`decode_node_id` and :func:`decode_sequence_id`. """ -import time # noqa: F401 +import time from .constants import ( # noqa: F401 EPOCH_MS_DEFAULT, + NODE_ID_BITS, NODE_ID_DEFAULT, NODE_ID_MAX, + SEQUENCE_ID_BITS, SEQUENCE_ID_MAX, + TIMESTAMP_BITS, TIMESTAMP_MS_MAX, ) @@ -35,7 +38,7 @@ 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: @@ -51,7 +54,7 @@ def decode_timestamp_ms(snowflake_id: int, epoch_ms: int = EPOCH_MS_DEFAULT) -> generated. """ # TODO: реализуйте функцию - return 0 + return epoch_ms + (snowflake_id >> (NODE_ID_BITS + SEQUENCE_ID_BITS)) def decode_node_id(snowflake_id: int) -> int: @@ -65,7 +68,7 @@ def decode_node_id(snowflake_id: int) -> int: ``[0, NODE_ID_MAX]``. """ # TODO: реализуйте функцию - return 0 + return (snowflake_id >> SEQUENCE_ID_BITS) & NODE_ID_MAX def decode_sequence_id(snowflake_id: int) -> int: @@ -79,7 +82,7 @@ def decode_sequence_id(snowflake_id: int) -> int: the range ``[0, SEQUENCE_ID_MAX]``. """ # TODO: реализуйте функцию - return 0 + return snowflake_id & SEQUENCE_ID_MAX def generate_snowflake_id( @@ -111,4 +114,19 @@ def generate_snowflake_id( cases an explanatory message is printed to stdout first. """ # TODO: реализуйте функцию - return 0 + if not (0 <= node_id <= NODE_ID_MAX): + print(f"node_id must be in [0, {NODE_ID_MAX}], have {node_id}") + return None + + if not (0 <= sequence_id <= SEQUENCE_ID_MAX): + print(f"sequence_id must be in [0; {SEQUENCE_ID_MAX}], have {sequence_id}") + return None + + vremya = read_current_millis(epoch_ms) + if vremya > TIMESTAMP_MS_MAX: + print("Timestamps overflows") + return None + + return (vremya << (NODE_ID_BITS + SEQUENCE_ID_BITS)) | ( + node_id << (SEQUENCE_ID_BITS | sequence_id) + ) From 300a25afc903531c736f0f39a196e0cbcac4c5e8 Mon Sep 17 00:00:00 2001 From: Darina ramazanova Date: Fri, 2 Oct 2026 23:54:56 +0300 Subject: [PATCH 02/10] =?UTF-8?q?=D0=92=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD?= =?UTF-8?q?=D0=B8=D0=BBa=20=D0=B7=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pl-001-python-basics-task | 1 + src/part1/lesson 4.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+) create mode 160000 pl-001-python-basics-task create mode 100644 src/part1/lesson 4.py diff --git a/pl-001-python-basics-task b/pl-001-python-basics-task new file mode 160000 index 00000000..339b23f1 --- /dev/null +++ b/pl-001-python-basics-task @@ -0,0 +1 @@ +Subproject commit 339b23f1a96d511fc3bd06b3871bbfdbb6143c73 diff --git a/src/part1/lesson 4.py b/src/part1/lesson 4.py new file mode 100644 index 00000000..4d09c809 --- /dev/null +++ b/src/part1/lesson 4.py @@ -0,0 +1,15 @@ +def merge_sorted_lists(first: list[int], second: list[int]) -> list[int]: + a=[] + for i in range(len(first)): + a.append(first[i]) + for j in range(len(second)): + a.append(second[j]) + return sorted(a) +assert merge_sorted_lists([], []) == [] +assert merge_sorted_lists([1, 2], []) == [1, 2] +assert merge_sorted_lists([], [1, 2]) == [1, 2] +assert merge_sorted_lists([1, 2, 3], [4, 5]) == [1, 2, 3, 4, 5] +assert merge_sorted_lists([4, 5], [1, 2, 3]) == [1, 2, 3, 4, 5] +assert merge_sorted_lists([1, 1], [1]) == [1, 1, 1] + +print("All tests passed") \ No newline at end of file From f82f25a0c7cc7c0134e2bb2ab986f399f8a315f3 Mon Sep 17 00:00:00 2001 From: Darina ramazanova Date: Thu, 8 Oct 2026 04:57:37 +0300 Subject: [PATCH 03/10] =?UTF-8?q?=D0=A1=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB?= =?UTF-8?q?=D0=B0=20=D1=87=D0=B0=D1=81=D1=82=D1=8C=203?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/part1/{lesson 4.py => lesson_4.py} | 0 src/part3/crud.py | 59 ++++++++++++++++++++--- src/part3/run_admin.py | 65 +++++++++++++++++++++++--- src/part3/storage.py | 8 ++-- src/part3/utils.py | 43 +++++++++++++---- 5 files changed, 150 insertions(+), 25 deletions(-) rename src/part1/{lesson 4.py => lesson_4.py} (100%) diff --git a/src/part1/lesson 4.py b/src/part1/lesson_4.py similarity index 100% rename from src/part1/lesson 4.py rename to src/part1/lesson_4.py diff --git a/src/part3/crud.py b/src/part3/crud.py index 68811d7e..b9ba934c 100644 --- a/src/part3/crud.py +++ b/src/part3/crud.py @@ -20,13 +20,13 @@ 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: @@ -41,7 +41,9 @@ def generate_product_id(storage: list[Product]) -> int: empty. """ # TODO: реализуйте функцию (используйте своё решение части 2) - return 0 + if not storage: + return PRODUCT_ID_MIN + return max(product[PRODUCT_ID_INDEX] for product in storage) + 1 def create_product( @@ -65,7 +67,20 @@ def create_product( is left unchanged and an explanatory message is printed. """ # TODO: реализуйте функцию - return 0 + name, price, quantity = fields + name = normalize_product_name(name) + if not name: + print("product name must not be blank") + return None + for product in storage: + if product[NAME_INDEX] == name: + print(f"product name '{name}' is already taken") + return None + product_id = generate_product_id(storage) + price = normalize_price(price) + new_product = (product_id, name, price, quantity) + storage.append(new_product) + return product_id def read_product(storage: list[Product], product_id: int) -> Product | None: @@ -81,7 +96,11 @@ def read_product(storage: list[Product], product_id: int) -> Product | None: printed in that case). """ # TODO: реализуйте функцию (используйте своё решение части 2) - return (0, "", Decimal(0), 0) + 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( @@ -111,7 +130,28 @@ def update_product( product to its own current name is allowed. """ # TODO: реализуйте функцию - return (0, "", Decimal(0), 0) + name, price, quantity = fields + name = normalize_product_name(name) + if not name: + print("product name must not be blank") + return None + target_index = None + for i, product in enumerate(storage): + if product[PRODUCT_ID_INDEX] == product_id: + target_index = i + break + if target_index is None: + print(f"no product with id {product_id}") + return None + for product in storage: + if (product[NAME_INDEX] == name + and product[PRODUCT_ID_INDEX] != product_id): + print(f"product name '{name}' is already taken") + return None + price = normalize_price(price) + updated_product = (product_id, name, price, quantity) + storage[target_index] = updated_product + return updated_product def delete_product(storage: list[Product], product_id: int) -> int | None: @@ -128,4 +168,9 @@ def delete_product(storage: list[Product], product_id: int) -> int | None: a message is printed). """ # TODO: реализуйте функцию (используйте своё решение части 2) - return 0 + for i, product in enumerate(storage): + if product[PRODUCT_ID_INDEX] == product_id: + del storage[i] + return product_id + print(f"no product with id {product_id}") + return None diff --git a/src/part3/run_admin.py b/src/part3/run_admin.py index c5feeb0b..614ad226 100644 --- a/src/part3/run_admin.py +++ b/src/part3/run_admin.py @@ -29,7 +29,7 @@ it. """ -from decimal import Decimal, InvalidOperation # noqa: F401 +from decimal import Decimal, InvalidOperation from typing import Final from .crud import ( # noqa: F401 @@ -39,12 +39,23 @@ 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 add a product, print its new id + read print the product with that id + update overwrite that product's fields + delete 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: @@ -55,6 +66,7 @@ def show_help() -> None: note on how multi-word names are parsed. """ # TODO: реализуйте функцию + print(HELP_TEXT) def print_result(result: object) -> None: @@ -66,6 +78,9 @@ def print_result(result: object) -> None: printed in that case. """ # TODO: реализуйте функцию + if result is not None: + print(result) + def run_command(storage: list[Product], line: str) -> bool: @@ -89,13 +104,51 @@ def run_command(storage: list[Product], line: str) -> bool: ``update`` does not parse as a decimal number. """ # TODO: реализуйте функцию - return False + parts = line.split() + if not parts: + return True + command = parts[0] + if command == "help": + show_help() + elif command == "exit": + return False + elif command == "show": + print(get_storage_str_representation(storage)) + elif command == "read" or command == "delete": + if len(parts) != 2: + print(f"'{line}' is not a command") + return True + product_id = int(parts[1]) + print_result(read_product(storage, product_id)) + elif command == "create": + if len(parts) < 4: + print(f"'{line}' is not a command") + return True + name = " ".join(parts[1:-2]) + price = Decimal(parts[-2]) + quantity = int(parts[-1]) + result = create_product(storage, (name, price, quantity)) + print_result(result) + elif command == "update": + if len(parts) < 5: + print(f"'{line}' is not a command") + return True + product_id = int(parts[1]) + name = " ".join(parts[2:-2]) + price = Decimal(parts[-2]) + quantity = int(parts[-1]) + result = update_product(storage, product_id, (name, price, quantity)) + print_result(result) + else: + 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 + A sngle 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 diff --git a/src/part3/storage.py b/src/part3/storage.py index c5e34be5..36e9af7f 100644 --- a/src/part3/storage.py +++ b/src/part3/storage.py @@ -27,9 +27,9 @@ # 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 diff --git a/src/part3/utils.py b/src/part3/utils.py index 318ded0b..040662a8 100644 --- a/src/part3/utils.py +++ b/src/part3/utils.py @@ -18,24 +18,30 @@ the longest value it holds in that particular call. """ -from decimal import ROUND_HALF_UP, Decimal # noqa: F401 +from decimal import ROUND_HALF_UP, Decimal from typing import Final -from .storage import Product +from .storage import ( + NAME_INDEX, + PRICE_INDEX, + PRODUCT_ID_INDEX, + QUANTITY_INDEX, + Product, +) # Number of fractional digits every stored price is rounded to. # Quantisation step derived from PRICE_PRECISION, e.g. Decimal("0.01"). # TODO: задайте число знаков после запятой и шаг квантования (используйте своё # решение части 2) -PRICE_PRECISION: Final[int] = 0 -PRICE_STEP: Final = Decimal(0) +PRICE_PRECISION: Final[int] = 2 +PRICE_STEP: Final = Decimal("0.01") # Column headers of the table produced by get_storage_str_representation, # left to right. The width of each column is not fixed here -- it is # measured per call from the data (see the function). # TODO: задайте заголовки столбцов таблицы -TABLE_HEADERS: Final[tuple[str, ...]] = () +TABLE_HEADERS: Final[tuple[str, ...]] = ("ID", "name", "price", "quatity") def normalize_price(price: Decimal) -> Decimal: @@ -49,7 +55,7 @@ def normalize_price(price: Decimal) -> Decimal: with halves rounded up. """ # TODO: реализуйте функцию (используйте своё решение части 2) - return Decimal(0) + return price.quantize(PRICE_STEP, rounding=ROUND_HALF_UP) def normalize_product_name(name: str) -> str: @@ -67,7 +73,7 @@ def normalize_product_name(name: str) -> str: whitespace. """ # TODO: реализуйте функцию - return "" + return " ".join(name.split()).lower() # TODO: при необходимости добавьте свои вспомогательные функции @@ -92,4 +98,25 @@ def get_storage_str_representation(storage: list[Product]) -> str: the header labels. """ # TODO: реализуйте функцию - return "" + rows_data = [] + for product in storage: + rows_data.append(( + str(product[PRODUCT_ID_INDEX]), + str(product[NAME_INDEX]), + str(product[PRICE_INDEX]), + str(product[QUANTITY_INDEX]) + )) + widths = [len(header) for header in TABLE_HEADERS] + for row in rows_data: + for i, cell in enumerate(row): + widths[i] = max(widths[i], len(cell)) + header_cells = [f"{header:<{widths[i]}}" for i, header in enumerate(TABLE_HEADERS)] + header_line = "| " + " | ".join(header_cells) + " |" + separator_cells = ["-" * (widths[i] + 2) for i in range(len(TABLE_HEADERS))] + separator_line = "|" + "|".join(separator_cells) + "|" + data_lines = [] + for row in rows_data: + cells = [f"{cell:<{widths[i]}}" for i, cell in enumerate(row)] + data_lines.append("| " + " | ".join(cells) + " |") + result_lines = [header_line, separator_line] + data_lines + return "\n".join(result_lines) From 51eaf8eb0610ab5a4fa0360252d02072001317f7 Mon Sep 17 00:00:00 2001 From: Darina ramazanova Date: Thu, 8 Oct 2026 05:10:21 +0300 Subject: [PATCH 04/10] =?UTF-8?q?=D1=81=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB?= =?UTF-8?q?=D0=B0=20=D1=87=D0=B0=D1=81=D1=82=D1=8C=203(=D0=BF=D0=BE=D0=BF?= =?UTF-8?q?=D1=8B=D1=82=D0=BA=D0=B0=202)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/part1/lesson_4.py | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 src/part1/lesson_4.py diff --git a/src/part1/lesson_4.py b/src/part1/lesson_4.py deleted file mode 100644 index 4d09c809..00000000 --- a/src/part1/lesson_4.py +++ /dev/null @@ -1,15 +0,0 @@ -def merge_sorted_lists(first: list[int], second: list[int]) -> list[int]: - a=[] - for i in range(len(first)): - a.append(first[i]) - for j in range(len(second)): - a.append(second[j]) - return sorted(a) -assert merge_sorted_lists([], []) == [] -assert merge_sorted_lists([1, 2], []) == [1, 2] -assert merge_sorted_lists([], [1, 2]) == [1, 2] -assert merge_sorted_lists([1, 2, 3], [4, 5]) == [1, 2, 3, 4, 5] -assert merge_sorted_lists([4, 5], [1, 2, 3]) == [1, 2, 3, 4, 5] -assert merge_sorted_lists([1, 1], [1]) == [1, 1, 1] - -print("All tests passed") \ No newline at end of file From be4c522a3cd713f854bbb47090d5ec5443bf5467 Mon Sep 17 00:00:00 2001 From: Darina ramazanova Date: Thu, 8 Oct 2026 05:14:44 +0300 Subject: [PATCH 05/10] =?UTF-8?q?=D1=87=D0=B0=D1=81=D1=82=D1=8C=203(=D0=BF?= =?UTF-8?q?=D0=BE=D0=BF=D1=8B=D1=82=D0=BA=D0=B0=203)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/part3/crud.py | 5 ++--- src/part3/run_admin.py | 1 - src/part3/utils.py | 16 +++++++++------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/part3/crud.py b/src/part3/crud.py index b9ba934c..01eef2e4 100644 --- a/src/part3/crud.py +++ b/src/part3/crud.py @@ -96,7 +96,7 @@ def read_product(storage: list[Product], product_id: int) -> Product | None: printed in that case). """ # TODO: реализуйте функцию (используйте своё решение части 2) - for product in storage: + for product in storage: if product[PRODUCT_ID_INDEX] == product_id: return product print(f"no product with id {product_id}") @@ -144,8 +144,7 @@ def update_product( print(f"no product with id {product_id}") return None for product in storage: - if (product[NAME_INDEX] == name - and product[PRODUCT_ID_INDEX] != product_id): + if product[NAME_INDEX] == name and product[PRODUCT_ID_INDEX] != product_id: print(f"product name '{name}' is already taken") return None price = normalize_price(price) diff --git a/src/part3/run_admin.py b/src/part3/run_admin.py index 614ad226..dd326feb 100644 --- a/src/part3/run_admin.py +++ b/src/part3/run_admin.py @@ -82,7 +82,6 @@ def print_result(result: object) -> None: print(result) - def run_command(storage: list[Product], line: str) -> bool: """Parse one console line and carry out the command it names. diff --git a/src/part3/utils.py b/src/part3/utils.py index 040662a8..136bfc33 100644 --- a/src/part3/utils.py +++ b/src/part3/utils.py @@ -100,12 +100,14 @@ def get_storage_str_representation(storage: list[Product]) -> str: # TODO: реализуйте функцию rows_data = [] for product in storage: - rows_data.append(( - str(product[PRODUCT_ID_INDEX]), - str(product[NAME_INDEX]), - str(product[PRICE_INDEX]), - str(product[QUANTITY_INDEX]) - )) + rows_data.append( + ( + str(product[PRODUCT_ID_INDEX]), + str(product[NAME_INDEX]), + str(product[PRICE_INDEX]), + str(product[QUANTITY_INDEX]), + ) + ) widths = [len(header) for header in TABLE_HEADERS] for row in rows_data: for i, cell in enumerate(row): @@ -118,5 +120,5 @@ def get_storage_str_representation(storage: list[Product]) -> str: for row in rows_data: cells = [f"{cell:<{widths[i]}}" for i, cell in enumerate(row)] data_lines.append("| " + " | ".join(cells) + " |") - result_lines = [header_line, separator_line] + data_lines + result_lines = [header_line, separator_line] + data_lines return "\n".join(result_lines) From 49c76a08444f72d77f17745fed8c043f6bcbe089 Mon Sep 17 00:00:00 2001 From: Darina ramazanova Date: Thu, 8 Oct 2026 05:19:21 +0300 Subject: [PATCH 06/10] =?UTF-8?q?=D1=87=D0=B0=D1=81=D1=82=D1=8C=203=20(?= =?UTF-8?q?=D0=BF=D0=BE=D0=BF=D1=8B=D1=82=D0=BA=D0=B0=204)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/part3/run_admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/part3/run_admin.py b/src/part3/run_admin.py index dd326feb..d4ee4b70 100644 --- a/src/part3/run_admin.py +++ b/src/part3/run_admin.py @@ -136,8 +136,8 @@ def run_command(storage: list[Product], line: str) -> bool: name = " ".join(parts[2:-2]) price = Decimal(parts[-2]) quantity = int(parts[-1]) - result = update_product(storage, product_id, (name, price, quantity)) - print_result(result) + updated = update_product(storage, product_id, (name, price, quantity)) + print_result(updated) else: print(f"'{line}' is not a command") From 942fe698c7d52b28be9524fc82ee95d58833baf0 Mon Sep 17 00:00:00 2001 From: Darina ramazanova Date: Sun, 1 Nov 2026 22:47:36 +0300 Subject: [PATCH 07/10] =?UTF-8?q?=D1=81=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB?= =?UTF-8?q?=D0=B0=20=D0=B7=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5=204?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/part4/cart.py | 40 ++++++++++++++++++++-- src/part4/crud.py | 65 ++++++++++++++++++++++++++++++----- src/part4/run_admin.py | 78 ++++++++++++++++++++++++++++++++++++++---- src/part4/storage.py | 7 ++-- src/part4/utils.py | 42 ++++++++++++++++++----- 5 files changed, 202 insertions(+), 30 deletions(-) diff --git a/src/part4/cart.py b/src/part4/cart.py index aed9bb72..cf684749 100644 --- a/src/part4/cart.py +++ b/src/part4/cart.py @@ -9,7 +9,7 @@ an explanatory message to stdout and returns ``None``. """ -from .storage import Product +from .storage import QUANTITY_KEY, Product # TODO: при необходимости добавьте свои вспомогательные функции @@ -47,7 +47,19 @@ def add_to_cart( request. """ # TODO: реализуйте функцию - return 0 + if product_id not in storage: + print(f"no product with id {product_id}") + return None + available = storage[product_id][QUANTITY_KEY] + if available < quantity: + print( + f"not enough stock for product {product_id}: " + f"{available} available, {quantity} requested" + ) + return None + storage[product_id][QUANTITY_KEY] -= quantity + cart[product_id] = cart.get(product_id, 0) + quantity + return cart[product_id] def remove_from_cart( @@ -84,4 +96,26 @@ def remove_from_cart( the cart does not hold the product or holds too few units. """ # TODO: реализуйте функцию - return 0 + if product_id not in cart: + print(f"product {product_id} is not in the cart") + return None + + in_cart = cart[product_id] + if in_cart < quantity: + print( + f"cart holds only {in_cart} unit(s) of product {product_id}, " + f"cannot remove {quantity}" + ) + return None + + if product_id not in storage: + print(f"no product with id {product_id}") + return None + + storage[product_id][QUANTITY_KEY] += quantity + cart[product_id] -= quantity + + if cart[product_id] == 0: + del cart[product_id] + return 0 + return cart[product_id] diff --git a/src/part4/crud.py b/src/part4/crud.py index 471eff00..d42f542b 100644 --- a/src/part4/crud.py +++ b/src/part4/crud.py @@ -23,16 +23,16 @@ than a scan over ``storage.values()``. """ -import uuid # noqa: F401 +import uuid from typing import Any -from .storage import ( # noqa: F401 +from .storage import ( NAME_KEY, PRICE_KEY, QUANTITY_KEY, Product, ) -from .utils import normalize_price, normalize_product_name # noqa: F401 +from .utils import normalize_price, normalize_product_name def generate_product_id() -> str: @@ -42,7 +42,7 @@ def generate_product_id() -> str: A freshly generated UUID, stringified. """ # TODO: реализуйте функцию - return "" + return str(uuid.uuid4()) def create_product( @@ -74,7 +74,21 @@ def create_product( message is printed. """ # TODO: реализуйте функцию - return "" + name = normalize_product_name(str(fields[NAME_KEY])) + if not name: + print("product name must not be blank") + return None + if name in product_names: + print(f"product name '{name}' is already taken") + return None + product_id = generate_product_id() + storage[product_id] = { + NAME_KEY: name, + PRICE_KEY: normalize_price(fields[PRICE_KEY]), + QUANTITY_KEY: fields[QUANTITY_KEY], + } + product_names.add(name) + return product_id def read_product(storage: dict[str, Product], product_id: str) -> Product | None: @@ -89,7 +103,11 @@ def read_product(storage: dict[str, Product], product_id: str) -> Product | None that identifier (a message is printed in that case). """ # TODO: реализуйте функцию - return {} + if product_id not in storage: + print(f"no product with id {product_id}") + return None + return storage[product_id] + def update_product( @@ -130,7 +148,30 @@ def update_product( own current name is allowed. """ # TODO: реализуйте функцию - return {} + if product_id not in storage: + print(f"no product with id {product_id}") + return None + + old_name = storage[product_id][NAME_KEY] + + if NAME_KEY in fields: + new_name = normalize_product_name(str(fields[NAME_KEY])) + if not new_name: + print("product name must not be blank") + return None + if new_name != old_name and new_name in product_names: + print(f"product name '{new_name}' is already taken") + return None + product_names.remove(old_name) + product_names.add(new_name) + storage[product_id][NAME_KEY] = new_name + if PRICE_KEY in fields: + storage[product_id][PRICE_KEY] = normalize_price(fields[PRICE_KEY]) + + if QUANTITY_KEY in fields: + storage[product_id][QUANTITY_KEY] = fields[QUANTITY_KEY] + + return storage[product_id] def delete_product( @@ -154,4 +195,12 @@ def delete_product( are left unchanged and a message is printed). """ # TODO: реализуйте функцию - return "" + if product_id not in storage: + print(f"no product with id {product_id}") + return None + + name = storage[product_id][NAME_KEY] + product_names.remove(name) + del storage[product_id] + return product_id + diff --git a/src/part4/run_admin.py b/src/part4/run_admin.py index 2247b29b..685d1e81 100644 --- a/src/part4/run_admin.py +++ b/src/part4/run_admin.py @@ -28,22 +28,33 @@ 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 NAME_KEY, PRICE_KEY, QUANTITY_KEY, Product # noqa: F401 -from .utils import get_storage_str_representation # noqa: F401 +from .storage import NAME_KEY, PRICE_KEY, QUANTITY_KEY, Product +from .utils import get_storage_str_representation # TODO: задайте приглашение и текст справки (используйте своё решение части 3) -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 add a product, print its new id + read print the product with that id + update overwrite that product's fields + delete 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: @@ -54,6 +65,7 @@ def show_help() -> None: note on how multi-word names are parsed. """ # TODO: реализуйте функцию (используйте своё решение части 3) + print(HELP_TEXT) def print_result(result: object) -> None: @@ -65,6 +77,8 @@ def print_result(result: object) -> None: printed in that case. """ # TODO: реализуйте функцию (используйте своё решение части 3) + if result is not None: + print(result) def run_command( @@ -92,7 +106,57 @@ def run_command( ``update`` does not parse as a decimal number. """ # TODO: реализуйте функцию - return False + parts = line.split() + if not parts: + return True + + command = parts[0] + + match command: + case "help": + show_help() + case "exit": + return False + case "show": + print(get_storage_str_representation(storage)) + case "read": + if len(parts) != 2: + print(f"'{line}' is not a command") + return True + print_result(read_product(storage, parts[1])) + case "delete": + if len(parts) != 2: + print(f"'{line}' is not a command") + return True + print_result(delete_product(storage, product_names, parts[1])) + case "create": + if len(parts) < 4: + print(f"'{line}' is not a command") + return True + name = " ".join(parts[1:-2]) + price = Decimal(parts[-2]) + quantity = int(parts[-1]) + + fields = {NAME_KEY: name, PRICE_KEY: price, QUANTITY_KEY: quantity} + result = create_product(storage, product_names, fields) + print_result(result) + case "update": + if len(parts) < 5: + print(f"'{line}' is not a command") + return True + product_id = parts[1] + name = " ".join(parts[2:-2]) + price = Decimal(parts[-2]) + quantity = int(parts[-1]) + + fields = {NAME_KEY: name, PRICE_KEY: price, QUANTITY_KEY: quantity} + updated = update_product(storage, product_names, product_id, fields) + print_result(updated) + case _: + print(f"'{line}' is not a command") + + return True + def main() -> None: diff --git a/src/part4/storage.py b/src/part4/storage.py index 9acedf07..a0fa9b53 100644 --- a/src/part4/storage.py +++ b/src/part4/storage.py @@ -32,7 +32,6 @@ functions in :mod:`src.part4.crud` keep the two in sync on every successful create, update and delete. """ - from typing import Any, Final @@ -40,6 +39,6 @@ # Name of each field inside a Product record. # TODO: задайте имена полей записи -NAME_KEY: Final = "" -PRICE_KEY: Final = "" -QUANTITY_KEY: Final = "" +NAME_KEY: Final = "name" +PRICE_KEY: Final = "price" +QUANTITY_KEY: Final = "quantity" diff --git a/src/part4/utils.py b/src/part4/utils.py index d2a940ea..88e2eb1c 100644 --- a/src/part4/utils.py +++ b/src/part4/utils.py @@ -18,10 +18,10 @@ the longest value it holds in that particular call. """ -from decimal import ROUND_HALF_UP, Decimal # noqa: F401 +from decimal import ROUND_HALF_UP, Decimal from typing import Final -from .storage import ( # noqa: F401 +from .storage import ( NAME_KEY, PRICE_KEY, QUANTITY_KEY, @@ -33,14 +33,14 @@ # Quantisation step derived from PRICE_PRECISION, e.g. Decimal("0.01"). # TODO: задайте число знаков после запятой и шаг квантования (используйте своё # решение части 3) -PRICE_PRECISION: Final[int] = 0 -PRICE_STEP: Final = Decimal(0) +PRICE_PRECISION: Final[int] = 2 +PRICE_STEP: Final = Decimal("0.01") # Column headers of the table produced by get_storage_str_representation, # left to right. The width of each column is not fixed here -- it is # measured per call from the data (see the function). # TODO: задайте заголовки столбцов таблицы (используйте своё решение части 3) -TABLE_HEADERS: Final[tuple[str, ...]] = () +TABLE_HEADERS: Final[tuple[str, ...]] = ("ID", "name", "price", "quantity") def normalize_price(price: Decimal) -> Decimal: @@ -54,7 +54,7 @@ def normalize_price(price: Decimal) -> Decimal: with halves rounded up. """ # TODO: реализуйте функцию (используйте своё решение части 3) - return Decimal(0) + return price.quantize(PRICE_STEP, rounding=ROUND_HALF_UP) def normalize_product_name(name: str) -> str: @@ -72,7 +72,7 @@ def normalize_product_name(name: str) -> str: whitespace. """ # TODO: реализуйте функцию (используйте своё решение части 3) - return "" + return " ".join(name.split()).lower() # TODO: при необходимости добавьте свои вспомогательные функции @@ -97,4 +97,30 @@ def get_storage_str_representation(storage: dict[str, Product]) -> str: returned, sized to the header labels. """ # TODO: реализуйте функцию (используйте своё решение части 3) - return "" + rows_data = [] + for product_id, product in storage.items(): + rows_data.append(( + str(product_id), + str(product[NAME_KEY]), + str(product[PRICE_KEY]), + str(product[QUANTITY_KEY]) + )) + widths = [len(header) for header in TABLE_HEADERS] + for row in rows_data: + for i, cell in enumerate(row): + widths[i] = max(widths[i], len(cell)) + + header_cells = [f"{header:<{widths[i]}}" for i, header in enumerate(TABLE_HEADERS)] + header_line = "| " + " | ".join(header_cells) + " |" + + separator_cells = ["-" * (widths[i] + 2) for i in range(len(TABLE_HEADERS))] + separator_line = "|" + "|".join(separator_cells) + "|" + + data_lines = [] + for row in rows_data: + cells = [f"{cell:<{widths[i]}}" for i, cell in enumerate(row)] + data_lines.append("| " + " | ".join(cells) + " |") + + result_lines = [header_line, separator_line] + data_lines + return "\n".join(result_lines) + From 8af4a6e30f7ed30557152eeb162aff645f1d77cb Mon Sep 17 00:00:00 2001 From: Darina ramazanova Date: Sun, 1 Nov 2026 23:07:55 +0300 Subject: [PATCH 08/10] =?UTF-8?q?4=20=D1=87=D0=B0=D1=81=D1=82=D1=8C(=D0=BF?= =?UTF-8?q?=D0=BE=D0=BF=D1=8B=D1=82=D0=BA=D0=B0=202)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/part4/cart.py | 2 +- src/part4/crud.py | 6 ++---- src/part4/run_admin.py | 9 ++++----- src/part4/storage.py | 1 + src/part4/utils.py | 17 +++++++++-------- 5 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/part4/cart.py b/src/part4/cart.py index cf684749..8b7b5e79 100644 --- a/src/part4/cart.py +++ b/src/part4/cart.py @@ -99,7 +99,7 @@ def remove_from_cart( if product_id not in cart: print(f"product {product_id} is not in the cart") return None - + in_cart = cart[product_id] if in_cart < quantity: print( diff --git a/src/part4/crud.py b/src/part4/crud.py index d42f542b..c0caed9f 100644 --- a/src/part4/crud.py +++ b/src/part4/crud.py @@ -107,7 +107,6 @@ def read_product(storage: dict[str, Product], product_id: str) -> Product | None print(f"no product with id {product_id}") return None return storage[product_id] - def update_product( @@ -167,7 +166,7 @@ def update_product( storage[product_id][NAME_KEY] = new_name if PRICE_KEY in fields: storage[product_id][PRICE_KEY] = normalize_price(fields[PRICE_KEY]) - + if QUANTITY_KEY in fields: storage[product_id][QUANTITY_KEY] = fields[QUANTITY_KEY] @@ -198,9 +197,8 @@ def delete_product( if product_id not in storage: print(f"no product with id {product_id}") return None - + name = storage[product_id][NAME_KEY] product_names.remove(name) del storage[product_id] return product_id - diff --git a/src/part4/run_admin.py b/src/part4/run_admin.py index 685d1e81..9e3aed66 100644 --- a/src/part4/run_admin.py +++ b/src/part4/run_admin.py @@ -109,7 +109,7 @@ def run_command( parts = line.split() if not parts: return True - + command = parts[0] match command: @@ -136,7 +136,7 @@ def run_command( name = " ".join(parts[1:-2]) price = Decimal(parts[-2]) quantity = int(parts[-1]) - + fields = {NAME_KEY: name, PRICE_KEY: price, QUANTITY_KEY: quantity} result = create_product(storage, product_names, fields) print_result(result) @@ -148,15 +148,14 @@ def run_command( name = " ".join(parts[2:-2]) price = Decimal(parts[-2]) quantity = int(parts[-1]) - + fields = {NAME_KEY: name, PRICE_KEY: price, QUANTITY_KEY: quantity} updated = update_product(storage, product_names, product_id, fields) print_result(updated) case _: print(f"'{line}' is not a command") - + return True - def main() -> None: diff --git a/src/part4/storage.py b/src/part4/storage.py index a0fa9b53..a25f6253 100644 --- a/src/part4/storage.py +++ b/src/part4/storage.py @@ -32,6 +32,7 @@ functions in :mod:`src.part4.crud` keep the two in sync on every successful create, update and delete. """ + from typing import Any, Final diff --git a/src/part4/utils.py b/src/part4/utils.py index 88e2eb1c..c0a0cbfa 100644 --- a/src/part4/utils.py +++ b/src/part4/utils.py @@ -99,12 +99,14 @@ def get_storage_str_representation(storage: dict[str, Product]) -> str: # TODO: реализуйте функцию (используйте своё решение части 3) rows_data = [] for product_id, product in storage.items(): - rows_data.append(( - str(product_id), - str(product[NAME_KEY]), - str(product[PRICE_KEY]), - str(product[QUANTITY_KEY]) - )) + rows_data.append( + ( + str(product_id), + str(product[NAME_KEY]), + str(product[PRICE_KEY]), + str(product[QUANTITY_KEY]), + ) + ) widths = [len(header) for header in TABLE_HEADERS] for row in rows_data: for i, cell in enumerate(row): @@ -112,7 +114,7 @@ def get_storage_str_representation(storage: dict[str, Product]) -> str: header_cells = [f"{header:<{widths[i]}}" for i, header in enumerate(TABLE_HEADERS)] header_line = "| " + " | ".join(header_cells) + " |" - + separator_cells = ["-" * (widths[i] + 2) for i in range(len(TABLE_HEADERS))] separator_line = "|" + "|".join(separator_cells) + "|" @@ -123,4 +125,3 @@ def get_storage_str_representation(storage: dict[str, Product]) -> str: result_lines = [header_line, separator_line] + data_lines return "\n".join(result_lines) - From e49a08fbe39fd409976980c3017deebe69898583 Mon Sep 17 00:00:00 2001 From: Darina ramazanova Date: Wed, 4 Nov 2026 22:39:45 +0300 Subject: [PATCH 09/10] Part 1 --- pl-001-python-basics-task | 1 - 1 file changed, 1 deletion(-) delete mode 160000 pl-001-python-basics-task diff --git a/pl-001-python-basics-task b/pl-001-python-basics-task deleted file mode 160000 index 339b23f1..00000000 --- a/pl-001-python-basics-task +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 339b23f1a96d511fc3bd06b3871bbfdbb6143c73 From 1c316064f602acebc22d6a293a756a4a0d330c9c Mon Sep 17 00:00:00 2001 From: Darina ramazanova Date: Wed, 4 Nov 2026 22:41:28 +0300 Subject: [PATCH 10/10] Part 1 --- .../.github/workflows/ci.yml | 31 ++ .../.github/workflows/label-by-group.yml | 52 +++ pl-001-python-basics-task/.gitignore | 13 + pl-001-python-basics-task/.python-version | 1 + pl-001-python-basics-task/README.md | 108 +++++++ .../manuals/local_checks.md | 90 ++++++ .../manuals/submit_homework.md | 27 ++ pl-001-python-basics-task/pyproject.toml | 35 +++ pl-001-python-basics-task/run_local_checks | 24 ++ .../run_local_checks.bat | 23 ++ pl-001-python-basics-task/src/__init__.py | 0 pl-001-python-basics-task/src/part1/TASK.md | 156 +++++++++ .../src/part1/__init__.py | 1 + .../src/part1/constants.py | 38 +++ .../src/part1/snowflake.py | 132 ++++++++ pl-001-python-basics-task/src/part2/TASK.md | 213 +++++++++++++ .../src/part2/__init__.py | 1 + pl-001-python-basics-task/src/part2/cart.py | 154 +++++++++ pl-001-python-basics-task/src/part2/crud.py | 148 +++++++++ .../src/part2/storage.py | 34 ++ pl-001-python-basics-task/src/part2/utils.py | 29 ++ pl-001-python-basics-task/uv.lock | 297 ++++++++++++++++++ 22 files changed, 1607 insertions(+) create mode 100644 pl-001-python-basics-task/.github/workflows/ci.yml create mode 100644 pl-001-python-basics-task/.github/workflows/label-by-group.yml create mode 100644 pl-001-python-basics-task/.gitignore create mode 100644 pl-001-python-basics-task/.python-version create mode 100644 pl-001-python-basics-task/README.md create mode 100644 pl-001-python-basics-task/manuals/local_checks.md create mode 100644 pl-001-python-basics-task/manuals/submit_homework.md create mode 100644 pl-001-python-basics-task/pyproject.toml create mode 100644 pl-001-python-basics-task/run_local_checks create mode 100644 pl-001-python-basics-task/run_local_checks.bat create mode 100644 pl-001-python-basics-task/src/__init__.py create mode 100644 pl-001-python-basics-task/src/part1/TASK.md create mode 100644 pl-001-python-basics-task/src/part1/__init__.py create mode 100644 pl-001-python-basics-task/src/part1/constants.py create mode 100644 pl-001-python-basics-task/src/part1/snowflake.py create mode 100644 pl-001-python-basics-task/src/part2/TASK.md create mode 100644 pl-001-python-basics-task/src/part2/__init__.py create mode 100644 pl-001-python-basics-task/src/part2/cart.py create mode 100644 pl-001-python-basics-task/src/part2/crud.py create mode 100644 pl-001-python-basics-task/src/part2/storage.py create mode 100644 pl-001-python-basics-task/src/part2/utils.py create mode 100644 pl-001-python-basics-task/uv.lock diff --git a/pl-001-python-basics-task/.github/workflows/ci.yml b/pl-001-python-basics-task/.github/workflows/ci.yml new file mode 100644 index 00000000..f8715e9f --- /dev/null +++ b/pl-001-python-basics-task/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: CI + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: read + +jobs: + checks: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + + - name: Install dependencies + run: uv sync --locked + + - name: Ruff lint + run: uv run ruff check src + + - name: Ruff format check + run: uv run ruff format --check src + + - name: MyPy + run: uv run mypy --strict src diff --git a/pl-001-python-basics-task/.github/workflows/label-by-group.yml b/pl-001-python-basics-task/.github/workflows/label-by-group.yml new file mode 100644 index 00000000..97d8e60d --- /dev/null +++ b/pl-001-python-basics-task/.github/workflows/label-by-group.yml @@ -0,0 +1,52 @@ +name: Label PR by group + +on: + pull_request_target: + types: [opened, edited, reopened] + +permissions: + pull-requests: write + +jobs: + label-selection: + runs-on: ubuntu-latest + steps: + - name: Set label from PR title + uses: actions/github-script@v7 + with: + script: | + const title = context.payload.pull_request.title; + + // Expected title format: ". . " + const parts = title.split(".").map((part) => part.trim()); + + if (parts.length !== 3 || parts.some((part) => part.length === 0)) { + console.log(`Title "${title}" does not match the expected template, skipping.`); + return; + } + + const [hwNumber, , groupNumber] = parts; + + if (!/^\d+$/.test(hwNumber)) { + console.log(`Title "${title}" does not match the expected template, skipping.`); + return; + } + + const { data: labels } = await github.rest.issues.listLabelsForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + }); + + const matchingLabel = labels.find((label) => label.name === groupNumber); + + if (!matchingLabel) { + console.log(`No label named "${groupNumber}" found, skipping.`); + return; + } + + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + labels: [matchingLabel.name], + }); diff --git a/pl-001-python-basics-task/.gitignore b/pl-001-python-basics-task/.gitignore new file mode 100644 index 00000000..8ca254de --- /dev/null +++ b/pl-001-python-basics-task/.gitignore @@ -0,0 +1,13 @@ +# Byte-compiled +__pycache__/ +*.py[cod] + +# Virtual environments +.venv/ + +# Linter / type-checker caches +.ruff_cache/ +.mypy_cache/ + +# Claude +.claude diff --git a/pl-001-python-basics-task/.python-version b/pl-001-python-basics-task/.python-version new file mode 100644 index 00000000..e4fba218 --- /dev/null +++ b/pl-001-python-basics-task/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/pl-001-python-basics-task/README.md b/pl-001-python-basics-task/README.md new file mode 100644 index 00000000..76d456ac --- /dev/null +++ b/pl-001-python-basics-task/README.md @@ -0,0 +1,108 @@ +# Практическое задание по курсу PL-001 + +В рамках задания мы напишем на Python игрушечный бэкенд для интернет-магазина с локальным хранилищем данных в памяти: научимся создавать, читать, изменять и удалять товары, работать с корзиной и управлять всем этим через простую консоль администратора. Задание выполняется поэтапно, и каждый следующий шаг опирается на предыдущий. + +## Мануалы + +- [Запуск локальных проверок](manuals/local_checks.md) — как установить uv на macOS и Windows и запустить скрипты, которые локально выполняют те же проверки кода, что и CI на GitHub. +- [Сдача домашнего задания](manuals/submit_homework.md) — как загрузить решение в свой форк, оформить Pull Request в исходный репозиторий и что нужно для того, чтобы работа была принята на проверку. + +## Задания + +- [Часть 1. Генератор идентификаторов Twitter Snowflake](src/part1/TASK.md) — нужно написать генератор 64-битных числовых идентификаторов, в которые упакованы метка времени, номер узла и счётчик последовательности. Такие идентификаторы уникальны и примерно упорядочены по времени, а из самого идентификатора можно узнать, когда и на каком узле он был создан. +- [Часть 2. CRUD и корзина для интернет-магазина](src/part2/TASK.md) — нужно реализовать хранилище товаров в памяти (список кортежей), операции создания, чтения, изменения и удаления записей, аккуратную работу с ценами через `Decimal` и корзину покупателя, в которую товары переносятся со склада и обратно без потери количества. + +## Критерии оценивания + +Максимальный балл за каждое задание — **10**. Правильность решения проверяется закрытыми тестами. + +### Условия принятия работы + +Работа принимается к оцениванию, только если выполнены **все** условия: + +- код проходит проверки [ruff](https://docs.astral.sh/ruff/) и [MyPy](https://mypy-lang.org/) (запустить их можно [локально](manuals/local_checks.md)); +- работа сдана до дедлайна. + +Если хотя бы одно условие не выполнено, работа не принимается и оценивается в 0 баллов. + +### Как считается балл за правильность + +Тесты разбиты на функциональные блоки: каждый блок соответствует функции или классу из задания и имеет свой вес (более сложные блоки весят больше). Внутри блока тесты делятся на две группы: + +| Группа тестов | Доля веса блока | Что проверяет | +|---|---|---| +| Базовые (типичные входные данные) | 70% | Основная логика реализована верно | +| Граничные (пустой ввод, нулевые и предельные значения, повторы и т. п.) | 30% | Обработка особых случаев | + +Формулы: + +``` +балл_блока = 0.7 · доля_пройденных_базовых + 0.3 · доля_пройденных_граничных +балл_за_правильность = 10 · Σ(вес_блока · балл_блока) / Σ(вес_блока) +``` + +Дополнительные правила: + +- **Нереализованный функционал.** Блок, в котором функционал не реализован (заглушка, `pass`, `NotImplementedError`), получает 0 баллов. Остальные блоки оцениваются как обычно, дополнительного штрафа за нереализованное нет. +- **Ошибки выполнения.** Тест, в котором возникло необработанное исключение, считается проваленным. +- **Зависание.** У каждого теста есть ограничение по времени выполнения. Если решение не укладывается в него (например, из-за бесконечного цикла), тест считается проваленным. + +### Штрафы за оформление + +Кроме правильности, оценивается качество кода. За нарушения ниже из балла за правильность вычитаются штрафы. Каждое нарушение штрафуется **один раз**, сколько бы раз оно ни встретилось в работе. + +У каждого нарушения есть уникальный код: `S` — серьёзные, `M` — средние, `T` — типовые. При проверке преподаватель указывает в комментарии к работе только коды (например, `S1, M2, T1`), а расшифровку и штраф можно найти в таблицах ниже. + +#### Серьёзные нарушения (−1 балл за каждое) + +| Код | Нарушение | Что имеется в виду | +|---|---|---| +| `S1` | Копипаста | Вместо вынесения повторяющейся логики в функцию, метод или цикл один и тот же (или почти тот же) код скопирован несколько раз. Мешает расширяемости: любое изменение приходится вносить во все копии | +| `S2` | Слишком длинные функции | Функция делает много разных вещей одновременно (вычисления, проверки, преобразования, форматирование в одном месте) и не читается целиком. Ориентир: более 30–40 строк или несколько несвязанных обязанностей. Такой код тяжело читать и поддерживать | +| `S3` | Магические значения | Числа и строки с неочевидным смыслом прописаны прямо в коде, а не вынесены в именованные константы. Как и копипаста, мешает расширяемости: значение приходится искать и менять по всему коду | +| `S4` | Слишком широкий перехват исключений | Голый `except:` или `except Exception:` без необходимости; молча проглоченные ошибки (`except ...: pass`). Так можно пропустить что-то важное | +| `S5` | Изменение входных данных | Функция неожиданно изменяет переданные ей аргументы, хотя из названия и описания это не следует. Неожиданное поведение, источник серьёзных ошибок | + +#### Средние нарушения (−0.5 балла за каждое) + +| Код | Нарушение | Что имеется в виду | +|---|---|---| +| `M1` | Большая вложенность | Более трёх уровней вложенности (`if`, `for`, `while`, `try`, `with`) внутри одной функции. Лечится ранним возвратом (`return`, `continue`) и выносом логики в отдельные функции | +| `M2` | Неинформативные имена | Имена вроде `a`, `tmp`, `data2`, `x1`, а также имена, не отражающие суть, или не соответствующие PEP 8 (например, `camelCase` для функций и переменных) | +| `M3` | Избыточные глобальные переменные | Изменяемое глобальное состояние там, где достаточно параметров, возвращаемых значений или атрибутов класса | +| `M4` | Мёртвый код | Код, который нигде не используется: неиспользуемые функции, классы, переменные, константы, импорты, недостижимые ветки. Сюда же относятся крупные куски закомментированного кода «на будущее» и оставленный отладочный `print`. Затрудняет чтение, но на работу программы не влияет | + +#### Типовые нарушения хорошего тона (−0.25 балла за каждое) + +| Код | Нарушение | +|---|---| +| `T1` | `else` или `elif` после `return`, `raise`, `continue` или `break` | +| `T2` | Явное приведение к `bool` в условиях (`if bool(x):`) и сравнение с `True` / `False` (`if x == True:`) | +| `T3` | Сравнение с синглтонами и членами enum'ов через `==` / `!=` вместо `is` / `is not` | +| `T4` | Проверка пустоты коллекции через `len(x) == 0` вместо `if not x` | +| `T5` | Обход через `for i in range(len(items))`, когда индекс не нужен (вместо `for item in items` или `enumerate`) | +| `T6` | `if cond: return True else: return False` вместо `return cond` | +| `T7` | Изменяемое значение по умолчанию у аргумента (`def f(x=[])`) | +| `T8` | Ручная конкатенация строк вместо f-строк | +| `T9` | Избыточные комментарии, пересказывающие очевидный код | + +### Штраф за списывание + +Если работа подозревается в списывании, студент получает **−5 баллов**. Эти баллы не вычитаются из оценки за правильность, а **заменяют** итоговую оценку: итоговой оценкой становится −5, независимо от результата тестов. + +Студент может оспорить этот балл на занятии, доказав, что не списывал, а написал код самостоятельно (например, объяснив решение и ответив на вопросы по нему). Если подозрение снимается, работа оценивается по обычным критериям. + +### Итоговая оценка + +``` +если есть подозрение в списывании: + итоговая_оценка = −5 +иначе: + итоговая_оценка = max(0, балл_за_правильность − штрафы_за_оформление) +``` + +Итоговая оценка за домашнее задание округляется до ближайшего целого по правилам математики: дробная часть от 0.5 и выше округляется вверх, меньше 0.5 — вниз (например, 7.5 → 8, 7.4 → 7). + +### Как оспорить оценку + +Если вы не согласны с оценкой, напишите преподавателю. Укажите, с каким блоком или с какой частью критериев вы не согласны, и приведите аргументы. Оценка пересматривается по тем же критериям, которые описаны выше. diff --git a/pl-001-python-basics-task/manuals/local_checks.md b/pl-001-python-basics-task/manuals/local_checks.md new file mode 100644 index 00000000..4554d610 --- /dev/null +++ b/pl-001-python-basics-task/manuals/local_checks.md @@ -0,0 +1,90 @@ +# Запуск локальных проверок + +Перед тем как открыть Pull Request, прогоните у себя те же проверки, что запускаются в CI на GitHub. Так вы найдёте ошибки заранее, не дожидаясь результата CI. + +## Что проверяется + +Скрипты устанавливают зависимости, затем проверяют код в папке `src`: линтером, форматированием и статическим анализатором типов. Если какая-то проверка не пройдена, скрипт останавливается с ошибкой. + +## Установка uv + +[uv](https://docs.astral.sh/uv/) — менеджер пакетов и окружений для Python. Проверки запускаются через него, поэтому вам потребуется его установить. + +### macOS + +Через официальный установщик: + +```bash +curl -LsSf https://astral.sh/uv/install.sh | sh +``` + +Или через Homebrew: + +```bash +brew install uv +``` + +### Windows + +Через PowerShell: + +```powershell +powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" +``` + +Или через winget: + +```powershell +winget install --id=astral-sh.uv -e +``` + +### Проверка установки + +Закройте и снова откройте терминал (иначе он не увидит обновлённый `PATH`), затем выполните: + +```bash +uv --version +``` + +Если команда напечатала версию, всё готово. Другие способы установки описаны в [документации uv](https://docs.astral.sh/uv/getting-started/installation/). + +## Запуск проверок + +Скрипты лежат в корне репозитория. Их можно запускать из любой папки, они сами переходят в корень проекта. Если `uv` не установлен, скрипт напечатает сообщение об этом и завершится. + +### macOS / Linux + +```bash +./run_local_checks +``` + +Если появилась ошибка `Permission denied`, сделайте скрипт исполняемым и запустите снова: + +```bash +chmod +x run_local_checks +./run_local_checks +``` + +### Windows + +В PowerShell или в командной строке (cmd): + +```powershell +.\run_local_checks.bat +``` + +Если у вас установлен Git Bash, можно запустить и bash-версию: + +```bash +./run_local_checks +``` + +## Как читать результат + +Скрипт печатает заголовок каждого шага (`==> Ruff lint` и так далее). При успехе в конце выводится `All checks passed.` + +Если какой-то шаг упал, исправьте замечания из его вывода и запустите скрипт снова. Форматирование можно исправить автоматически: + +```bash +uv run ruff format src +``` diff --git a/pl-001-python-basics-task/manuals/submit_homework.md b/pl-001-python-basics-task/manuals/submit_homework.md new file mode 100644 index 00000000..7078cc82 --- /dev/null +++ b/pl-001-python-basics-task/manuals/submit_homework.md @@ -0,0 +1,27 @@ +# Сдача домашнего задания + +Чтобы работа была принята на проверку, выполните шаги ниже по порядку. + +## 1. Выполните задание + +Выполните все пункты задания и оформите решение в соответствующих файлах. Перед отправкой [запустите локальные проверки](local_checks.md), чтобы заранее найти ошибки в оформлении кода. + +## 2. Залейте решение в свой форк + +Решение должно лежать в вашем форке репозитория на GitHub. Если форка ещё нет, создайте его кнопкой **Fork** на странице исходного репозитория, затем закоммитьте изменения и отправьте их в форк: + +```bash +git add . +git commit -m "Solve task 1" +git push +``` + +## 3. Откройте Pull Request + +Откройте Pull Request из своего форка в [исходный репозиторий](https://github.com/python-department/pl-001-python-basics-task). + +Оформите его так, чтобы название соответствовало шаблону: `номер задания. ФИО студента. номер группы`. Пример: `1. Иванов Иван Иванович. 123`. + +## 4. Дождитесь результатов CI + +Pull Request будет проверен только в том случае, если он оформлен корректно (название и лейбл соответствуют требованиям), а все проверки CI пройдены успешно. Если какая-то проверка упала, исправьте замечания, запушьте изменения в тот же форк и дождитесь нового запуска CI. diff --git a/pl-001-python-basics-task/pyproject.toml b/pl-001-python-basics-task/pyproject.toml new file mode 100644 index 00000000..06fbe913 --- /dev/null +++ b/pl-001-python-basics-task/pyproject.toml @@ -0,0 +1,35 @@ +[project] +name = "pl-001-python-basics-task" +version = "0.1.0" +description = "PL-001 course complex task" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [] + +[dependency-groups] +dev = [ + "mypy==2.3.1", + "ruff==0.16.8", +] + +[tool.ruff] +line-length = 88 +src = ["src"] +include = ["*.py", "*.pyi"] + +[tool.ruff.format] +quote-style = "double" +skip-magic-trailing-comma = false + +[tool.ruff.lint] +extend-select = ["I", "Q"] + +[tool.ruff.lint.flake8-quotes] +inline-quotes = "double" +multiline-quotes = "double" +docstring-quotes = "double" +avoid-escape = true + +[tool.ruff.lint.isort] +lines-after-imports = 2 +split-on-trailing-comma = true diff --git a/pl-001-python-basics-task/run_local_checks b/pl-001-python-basics-task/run_local_checks new file mode 100644 index 00000000..7345200f --- /dev/null +++ b/pl-001-python-basics-task/run_local_checks @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Run the same checks locally as in CI (.github/workflows/ci.yml). +set -euo pipefail + +cd "$(dirname "$0")" + +if ! command -v uv >/dev/null 2>&1; then + echo "Error: uv is not installed. Install it: https://docs.astral.sh/uv/getting-started/installation/" >&2 + exit 1 +fi + +echo "==> Install dependencies" +uv sync --locked + +echo "==> Ruff lint" +uv run ruff check src + +echo "==> Ruff format check" +uv run ruff format --check src + +echo "==> MyPy" +uv run mypy --strict src + +echo "All checks passed." diff --git a/pl-001-python-basics-task/run_local_checks.bat b/pl-001-python-basics-task/run_local_checks.bat new file mode 100644 index 00000000..e1b1ce9e --- /dev/null +++ b/pl-001-python-basics-task/run_local_checks.bat @@ -0,0 +1,23 @@ +@echo off +rem Run the same checks locally as in CI (.github/workflows/ci.yml). +cd /d "%~dp0" + +where uv >nul 2>nul +if errorlevel 1 ( + echo Error: uv is not installed. Install it: https://docs.astral.sh/uv/getting-started/installation/ 1>&2 + exit /b 1 +) + +echo ==^> Install dependencies +uv sync --locked || exit /b 1 + +echo ==^> Ruff lint +uv run ruff check src || exit /b 1 + +echo ==^> Ruff format check +uv run ruff format --check src || exit /b 1 + +echo ==^> MyPy +uv run mypy --strict src || exit /b 1 + +echo All checks passed. diff --git a/pl-001-python-basics-task/src/__init__.py b/pl-001-python-basics-task/src/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pl-001-python-basics-task/src/part1/TASK.md b/pl-001-python-basics-task/src/part1/TASK.md new file mode 100644 index 00000000..ef8e0faf --- /dev/null +++ b/pl-001-python-basics-task/src/part1/TASK.md @@ -0,0 +1,156 @@ +# Задача: генератор идентификаторов Twitter Snowflake + +## Легенда + +В ходе курса мы шаг за шагом построим собственное хранилище данных — небольшую +базу, в которую можно складывать записи и потом быстро находить их по ключу. +Начнём с фундамента: у каждой записи должен быть уникальный идентификатор. + +Казалось бы, можно просто нумеровать записи 1, 2, 3, … Пока хранилище живёт на +одной машине, так и происходит. Но реальные системы почти всегда распределены: +записи создаются на десятках серверов одновременно. Тогда возникают проблемы: + +- единый счётчик становится «бутылочным горлышком»: каждому серверу приходится + обращаться к нему за очередным номером; +- если серверы нумеруют записи независимо друг от друга, идентификаторы + неизбежно начнут совпадать; +- случайные идентификаторы (например, UUID) не совпадают, но плохо упорядочены: + по ним нельзя понять, какая запись появилась раньше, а индексы по таким + ключам работают медленнее. + +В 2010 году с этой же проблемой столкнулся Twitter: твиты создавались сотнями +тысяч в секунду на множестве машин, а идентификаторы должны были быть уникальными, +компактными (помещаться в 64 бита) и примерно упорядоченными по времени. Решение +получило название **Snowflake** («снежинка» — как и снежинки, такие идентификаторы +не повторяются). Идея проста: вместо одного числа-счётчика упаковать в одно +целое несколько независимых величин, и тогда серверам не нужно договариваться друг +с другом. + +Именно такой генератор нам и предстоит написать. В будущем он станет основой нашего +хранилища: каждая запись получит свой Snowflake-идентификатор, по которому можно +будет узнать, когда и на каком узле она была создана. + +## Контекст + +Нужно реализовать генератор 64-битных числовых идентификаторов в стиле [Twitter +Snowflake](https://en.wikipedia.org/wiki/Snowflake_ID). Такие идентификаторы примерно упорядочены по времени и могут +выдаваться разными узлами без какой-либо координации между ними. Каждый +идентификатор объединяет в одном целом положительном числе три величины: + +- сколько миллисекунд прошло с некоторого «нулевого момента» (пользовательской + эпохи); +- номер узла, который выдал идентификатор; +- порядковый номер идентификатора внутри одной миллисекунды. + +Реализация состоит из двух модулей: `constants.py` (формат идентификатора и +значения по умолчанию) и `snowflake.py` (генерация и разбор идентификаторов). + +## Формат идентификатора + +Идентификатор — целое число, которое всегда неотрицательно и помещается в +знаковый 64-битный тип. Логически оно состоит из четырёх частей, перечисленных +от старших разрядов к младшим: + +| Часть | Ёмкость | Что хранит | +| ------------------------- | ------- | --------------------------------------------------------------- | +| знаковый разряд | 1 бит | всегда 0 — идентификатор положительный | +| метка времени | 41 бит | число миллисекунд, прошедших с момента пользовательской эпохи | +| номер узла | 10 бит | идентификатор машины, выдавшей идентификатор | +| счётчик последовательности | 12 бит | порядковый номер идентификатора в пределах одной миллисекунды | + +Из ёмкости каждой части вытекает диапазон допустимых значений: часть шириной +`n` бит вмещает значения от `0` до `2**n - 1` включительно. + +## Модуль `constants.py` + +Необходимо определить следующие константы: + +- `EPOCH_MS_DEFAULT` — пользовательская эпоха по умолчанию, заданная в + миллисекундах Unix-времени. Значение — оригинальная эпоха Twitter, + 2010-11-04 01:42:54.657 UTC, то есть `1288834974657`. +- `NODE_ID_DEFAULT` — номер узла, который используется, когда вызывающий код не + передал свой. Значение — `1`. +- `TIMESTAMP_BITS`, `NODE_ID_BITS`, `SEQUENCE_ID_BITS` — ёмкости соответствующих + частей идентификатора в битах: `41`, `10` и `12`. +- `TIMESTAMP_MS_MAX`, `NODE_ID_MAX`, `SEQUENCE_ID_MAX` — наибольшее значение, + которое помещается в соответствующую часть; вычисляются из её ёмкости, а не + задаются числом напрямую. + +Любые дополнительные вспомогательные константы, которые понадобятся вам для +сборки и разбора идентификатора, добавьте на своё усмотрение. Изменение любой из +ёмкостей `*_BITS` должно автоматически согласованно менять зависящие от неё +константы. + +## Модуль `snowflake.py` + +### `read_current_millis(epoch_ms: int) -> int` + +- **Вход:** `epoch_ms` — момент пользовательской эпохи в миллисекундах + Unix-времени. +- **Выход:** число целых миллисекунд, прошедших между `epoch_ms` и текущим + моментом. + +### `decode_timestamp_ms(snowflake_id: int, epoch_ms: int = EPOCH_MS_DEFAULT) -> int` + +- **Вход:** `snowflake_id` — ранее сгенерированный идентификатор; `epoch_ms` — + эпоха, относительно которой он был создан (по умолчанию `EPOCH_MS_DEFAULT`). +- **Выход:** абсолютное Unix-время в миллисекундах, в которое был создан + идентификатор, то есть хранящаяся в нём метка времени плюс `epoch_ms`. + +### `decode_node_id(snowflake_id: int) -> int` + +- **Вход:** идентификатор. +- **Выход:** номер узла, записанный в идентификаторе. Всегда в диапазоне + `[0, NODE_ID_MAX]`. + +### `decode_sequence_id(snowflake_id: int) -> int` + +- **Вход:** идентификатор. +- **Выход:** счётчик последовательности, записанный в идентификаторе. Всегда в + диапазоне `[0, SEQUENCE_ID_MAX]`. + +### `generate_snowflake_id(sequence_id: int, node_id: int = NODE_ID_DEFAULT, epoch_ms: int = EPOCH_MS_DEFAULT) -> int | None` + +Собирает идентификатор для текущей миллисекунды. + +Функция не хранит состояния: значение `sequence_id` на каждом вызове передаёт +вызывающий код. Именно он отвечает за то, чтобы увеличивать счётчик для +идентификаторов, выданных в одну и ту же миллисекунду, и обнулять его, когда +часы перешли на следующую миллисекунду. + +**Входные параметры:** + +- `sequence_id` — счётчик в пределах миллисекунды; ожидается в диапазоне + `[0, SEQUENCE_ID_MAX]`. +- `node_id` — номер текущего узла; ожидается в диапазоне `[0, NODE_ID_MAX]`. + Необязательный, по умолчанию `NODE_ID_DEFAULT`. +- `epoch_ms` — начало эпохи в миллисекундах Unix-времени. Необязательный, по + умолчанию `EPOCH_MS_DEFAULT`. + +**Поведение и результат.** Проверки выполняются в перечисленном порядке; при +первой же неудачной проверке остальные не выполняются, в stdout печатается +поясняющее сообщение и функция возвращает `None`: + +1. Если `node_id` вне `[0, NODE_ID_MAX]` — сообщение содержит текст + `node_id must be in`, с указанием допустимого диапазона и полученного + значения. +2. Иначе если `sequence_id` вне `[0, SEQUENCE_ID_MAX]` — сообщение содержит + текст `sequence_id must be in`, с указанием допустимого диапазона и + полученного значения. +3. Иначе вычисляется число миллисекунд, прошедших с эпохи. Если оно превышает + `TIMESTAMP_MS_MAX` (то есть с момента эпохи прошло больше ~69 лет) — + сообщение содержит слово `overflows`. + +Если все проверки пройдены, функция возвращает идентификатор — положительное +целое, помещающееся в знаковый 64-битный тип (`0 < id < 2**63`), в котором +закодированы текущая метка времени, `node_id` и `sequence_id`. + +### Взаимная обратимость + +Для любого набора аргументов, на котором `generate_snowflake_id` возвращает не +`None`, декодеры возвращают исходные значения: + +- `decode_node_id(generate_snowflake_id(s, n, e)) == n` +- `decode_sequence_id(generate_snowflake_id(s, n, e)) == s` +- `decode_timestamp_ms(generate_snowflake_id(s, n, e), e)` равно абсолютному + Unix-времени того момента, в который был вызван генератор. diff --git a/pl-001-python-basics-task/src/part1/__init__.py b/pl-001-python-basics-task/src/part1/__init__.py new file mode 100644 index 00000000..242b07ce --- /dev/null +++ b/pl-001-python-basics-task/src/part1/__init__.py @@ -0,0 +1 @@ +"""Homework 3: a Twitter Snowflake identifier generator and its bit layout.""" diff --git a/pl-001-python-basics-task/src/part1/constants.py b/pl-001-python-basics-task/src/part1/constants.py new file mode 100644 index 00000000..588fe9f8 --- /dev/null +++ b/pl-001-python-basics-task/src/part1/constants.py @@ -0,0 +1,38 @@ +"""Bit layout and default epoch for the Twitter Snowflake ID generator. + +A Snowflake identifier is a 63-bit positive integer packed as follows, from +the most significant bit downwards: + + 1 bit unused sign bit, always zero + 41 bits milliseconds elapsed since a custom epoch + 10 bits node identifier + 12 bits per-millisecond sequence counter + +The shift and mask constants should be derived from the bit widths so that +changing a width keeps everything else consistent. +""" + +from typing import Final + + +# TODO: замените заглушки (0) на корректные значения, см. TASK.md. +# Все константы, зависящие от ёмкостей *_BITS, должны вычисляться из них. + +# Twitter's original Snowflake epoch: 2010-11-04 01:42:54.657 UTC. +EPOCH_MS_DEFAULT: Final[int] = 1288834974657 + +# Node identifier used when the caller does not supply one. +NODE_ID_DEFAULT: Final[int] = 1 + +# Width of each field, in bits. +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] = (2**TIMESTAMP_BITS) - 1 +NODE_ID_MAX: Final[int] = (2**NODE_ID_BITS) - 1 +SEQUENCE_ID_MAX: Final[int] = (2**SEQUENCE_ID_BITS) - 1 + +# Здесь можно добавить собственные вспомогательные константы +# (например, сдвиги полей при сборке идентификатора). diff --git a/pl-001-python-basics-task/src/part1/snowflake.py b/pl-001-python-basics-task/src/part1/snowflake.py new file mode 100644 index 00000000..7dca1200 --- /dev/null +++ b/pl-001-python-basics-task/src/part1/snowflake.py @@ -0,0 +1,132 @@ +"""Twitter Snowflake identifier generator. + +Snowflake produces roughly time-ordered 64-bit integer identifiers without +coordination between nodes. Each identifier packs the milliseconds elapsed +since a custom epoch, a node identifier and a per-millisecond sequence counter +into a single 63-bit positive integer (see ``constants`` for the layout). + +The public entry point is :func:`generate_snowflake_id`. It is stateless: the +caller passes the sequence counter on every call and is responsible for +advancing it within a millisecond and resetting it when the clock ticks over. + +Each packed field can be read back on its own with :func:`decode_timestamp_ms`, +:func:`decode_node_id` and :func:`decode_sequence_id`. +""" + +import time + +from .constants import ( # noqa: F401 + EPOCH_MS_DEFAULT, + NODE_ID_BITS, + NODE_ID_DEFAULT, + NODE_ID_MAX, + SEQUENCE_ID_BITS, + SEQUENCE_ID_MAX, + TIMESTAMP_BITS, + TIMESTAMP_MS_MAX, +) + + +def read_current_millis(epoch_ms: int) -> int: + """Read the number of milliseconds elapsed since the custom epoch. + + Args: + epoch_ms: The custom epoch expressed as Unix milliseconds. + + Returns: + The count of whole milliseconds between ``epoch_ms`` and now. May be + negative if ``epoch_ms`` lies in the future. + """ + # TODO: реализуйте функцию + return int(time.time() * 1000) - epoch_ms + + +def decode_timestamp_ms(snowflake_id: int, epoch_ms: int = EPOCH_MS_DEFAULT) -> int: + """Read the timestamp field out of a Snowflake identifier. + + Args: + snowflake_id: An identifier produced against the same epoch. + epoch_ms: The epoch the identifier was generated against. Defaults to + the original Twitter epoch (2010-11-04 01:42:54.657 UTC). + + Returns: + The absolute Unix time in milliseconds at which the identifier was + generated. + """ + # TODO: реализуйте функцию + return epoch_ms + (snowflake_id >> (NODE_ID_BITS + SEQUENCE_ID_BITS)) + + +def decode_node_id(snowflake_id: int) -> int: + """Read the node identifier field out of a Snowflake identifier. + + Args: + snowflake_id: An identifier produced by :func:`generate_snowflake_id`. + + Returns: + The node identifier packed into ``snowflake_id``, in the range + ``[0, NODE_ID_MAX]``. + """ + # TODO: реализуйте функцию + return (snowflake_id >> SEQUENCE_ID_BITS) & NODE_ID_MAX + + +def decode_sequence_id(snowflake_id: int) -> int: + """Read the sequence counter field out of a Snowflake identifier. + + Args: + snowflake_id: An identifier produced by :func:`generate_snowflake_id`. + + Returns: + The per-millisecond sequence counter packed into ``snowflake_id``, in + the range ``[0, SEQUENCE_ID_MAX]``. + """ + # TODO: реализуйте функцию + return snowflake_id & SEQUENCE_ID_MAX + + +def generate_snowflake_id( + sequence_id: int, + node_id: int = NODE_ID_DEFAULT, + epoch_ms: int = EPOCH_MS_DEFAULT, +) -> int | None: + """Build and return a Snowflake identifier for the current millisecond. + + The function is stateless: the caller passes the per-millisecond sequence + counter explicitly. It is the caller's responsibility to increment + ``sequence_id`` for identifiers minted within the same millisecond and to + reset it once the clock advances. + + Args: + sequence_id: The per-millisecond sequence counter, in the range + ``[0, SEQUENCE_ID_MAX]``. + node_id: The identifier of this node, in the range ``[0, NODE_ID_MAX]``. + Optional; defaults to ``NODE_ID_DEFAULT``. + epoch_ms: The start of the epoch as Unix milliseconds. Optional; + defaults to the original Twitter epoch (2010-11-04 01:42:54.657 + UTC). + + Returns: + The Snowflake identifier as a positive 63-bit integer, or ``None`` if + ``node_id`` is outside ``[0, NODE_ID_MAX]``, ``sequence_id`` is outside + ``[0, SEQUENCE_ID_MAX]``, or the elapsed time no longer fits in the + timestamp field (roughly 69 years after ``epoch_ms``). In each of those + cases an explanatory message is printed to stdout first. + """ + # TODO: реализуйте функцию + if not (0 <= node_id <= NODE_ID_MAX): + print(f"node_id must be in [0, {NODE_ID_MAX}], have {node_id}") + return None + + if not (0 <= sequence_id <= SEQUENCE_ID_MAX): + print(f"sequence_id must be in [0; {SEQUENCE_ID_MAX}], have {sequence_id}") + return None + + vremya = read_current_millis(epoch_ms) + if vremya > TIMESTAMP_MS_MAX: + print("Timestamps overflows") + return None + + return (vremya << (NODE_ID_BITS + SEQUENCE_ID_BITS)) | ( + node_id << (SEQUENCE_ID_BITS | sequence_id) + ) diff --git a/pl-001-python-basics-task/src/part2/TASK.md b/pl-001-python-basics-task/src/part2/TASK.md new file mode 100644 index 00000000..25736d9c --- /dev/null +++ b/pl-001-python-basics-task/src/part2/TASK.md @@ -0,0 +1,213 @@ +# Задача: CRUD и корзина для интернет-магазина + +## Легенда + +Наше хранилище данных уже умеет выдавать уникальные идентификаторы, но пока это +лишь «голая» база: записи лежат в памяти, и работать с ними приходится вручную. +Чтобы проверить, что хранилище годится для настоящей прикладной задачи, мы +построим на нём серверную часть простого интернет-магазина: каталог товаров и +корзину покупателя. + +Если писать такой код «в лоб», быстро возникают проблемы: + +- записи о товарах разбросаны по программе, и каждый кусок кода сам решает, как + их создавать, менять и удалять, — в итоге в каталоге появляются дубликаты и + «осиротевшие» идентификаторы; +- деньги хранят во `float`, и суммы вроде `0.1 + 0.2` начинают расходиться с + копейками, а в бухгалтерии такое недопустимо; +- товар «переезжает» между складом и корзиной в два отдельных действия, и если + одно из них не выполнилось, товар пропадает или появляется из воздуха. + +Именно эти вопросы решают классические CRUD-операции (create, read, update, +delete) над единым хранилищем, аккуратная работа с ценой через `Decimal` и +операции корзины, которые сохраняют общее количество товара. Их нам и предстоит +написать. Позже на этих функциях мы построим интерфейс работы с нашей базой. + +## Контекст + +Нужно реализовать серверную часть простого интернет-магазина: хранилище товаров, +операции CRUD над ним и работу с корзиной покупателя. Настоящая база данных не +используется — хранилище это список кортежей в оперативной памяти. + +Решение состоит из четырёх модулей: + +- `storage.py` — форма записи о товаре и связанные с ней константы; +- `utils.py` — округление денежных сумм; +- `crud.py` — создание, чтение, изменение и удаление записей о товарах; +- `cart.py` — перенос товаров между хранилищем и корзиной. + +Все функции, кроме `find_cart_line` и `change_cart_quantity`, на неуспешном пути +ничего не бросают: печатают сообщение об ошибке в stdout и возвращают `None`. + +## Формат записи о товаре + +Запись о товаре (`Product`) — кортеж из четырёх полей, перечисленных по порядку: + +| Поле | Тип | Что хранит | +| ------------- | --------- | ------------------------------------------------------------------ | +| идентификатор | `int` | уникальный положительный ключ, назначается при создании записи | +| наименование | `str` | человекочитаемое имя, уникальное в пределах хранилища | +| цена | `Decimal` | цена за единицу, округлённая до фиксированного числа знаков дроби | +| количество | `int` | число единиц товара на складе | + +Позиции полей задаются константами `*_INDEX`, чтобы обращение к кортежу не +использовало числовые литералы. + +Строка корзины (`CartLine`) — кортеж `(идентификатор товара, количество)`. В +корзине не более одной строки на каждый товар. + +## Модуль `storage.py` + +Определите: + +- `Product` — псевдоним типа `tuple[int, str, Decimal, int]`. +- `PRODUCT_ID_INDEX`, `NAME_INDEX`, `PRICE_INDEX`, `QUANTITY_INDEX` — позиции + полей в кортеже `Product`: `0`, `1`, `2`, `3`. Каждая аннотирована + `typing.Final`. +- `PRODUCT_ID_MIN: Final[int] = 1` — идентификатор, который получает первый + товар, добавленный в пустое хранилище. + +## Модуль `utils.py` + +- `PRICE_PRECISION: Final[int] = 2` — число знаков после запятой, до которого + округляется каждая сохраняемая цена. +- `PRICE_STEP: Final` — шаг квантования. Вычисляется из `PRICE_PRECISION` + (`Decimal(1).scaleb(-PRICE_PRECISION)`, то есть `Decimal("0.01")` при значении + по умолчанию), а не задаётся числом напрямую; при изменении `PRICE_PRECISION` + шаг меняется согласованно. + +### `normalize_price(price: Decimal) -> Decimal` + +- **Вход:** `price` — произвольная денежная сумма типа `Decimal`. +- **Выход:** та же сумма, приведённая к `PRICE_PRECISION` знакам после запятой; + ровно половина шага округляется вверх (режим `ROUND_HALF_UP`). + +Примеры: `Decimal("12.999") → Decimal("13.00")`, +`Decimal("2.3451") → Decimal("2.35")`, `Decimal("2.3449") → Decimal("2.34")`, +`Decimal("5") → Decimal("5.00")` (число знаков дроби в результате всегда равно +`PRICE_PRECISION`). + +## Модуль `crud.py` + +Каждая функция получает хранилище `storage: list[Product]` первым параметром и +меняет его на месте. + +### `generate_product_id(storage: list[Product]) -> int` + +- **Вход:** хранилище. +- **Выход:** если хранилище пустое — `PRODUCT_ID_MIN`; иначе — наибольший + идентификатор среди записей плюс `1`. Пропуски в нумерации (например, + оставшиеся после удаления) не переиспользуются — берётся именно максимум. + +### `create_product(storage: list[Product], fields: tuple[str, Decimal, int]) -> int | None` + +- **Вход:** `fields` — кортеж `(наименование, цена, количество)`; `цена` типа + `Decimal`. +- **Выход:** сгенерированный идентификатор новой записи, либо `None`. + +**Поведение.** + +1. Если в хранилище уже есть запись с таким же наименованием — новая запись не + создаётся, хранилище не меняется, печатается сообщение об ошибке, содержащее + это наименование, возвращается `None`. (Например: + `product name '<имя>' is already taken`.) +2. Иначе идентификатор вычисляется через `generate_product_id`, цена приводится + через `normalize_price`, кортеж + `(идентификатор, наименование, нормализованная цена, количество)` добавляется + в хранилище, возвращается идентификатор. + +### `read_product(storage: list[Product], product_id: int) -> Product | None` + +- **Вход:** целочисленный идентификатор. +- **Выход:** запись с этим идентификатором, либо `None`. Если записи нет — + печатается сообщение об ошибке, содержащее переданный идентификатор (например, + `no product with id `). + +### `update_product(storage: list[Product], product_id: int, fields: tuple[str, Decimal, int]) -> Product | None` + +- **Вход:** идентификатор и кортеж `(наименование, цена, количество)` с новыми + значениями полей. +- **Выход:** изменённая запись + `(идентификатор, наименование, нормализованная цена, количество)` — сам + идентификатор сохраняется прежним, остальные три поля заменяются, цена + приводится через `normalize_price`. Если записи с таким идентификатором нет — + хранилище не меняется, печатается сообщение об ошибке с идентификатором + (`no product with id `), возвращается `None`. Уникальность нового + наименования не проверяется. + +### `delete_product(storage: list[Product], product_id: int) -> int | None` + +- **Вход:** идентификатор. +- **Выход:** тот же идентификатор, если запись была найдена и удалена из + хранилища. Если записи нет — хранилище не меняется, печатается сообщение об + ошибке с идентификатором (`no product with id `), возвращается `None`. + +## Модуль `cart.py` + +- `CartLine` — псевдоним типа `tuple[int, int]` + (`(идентификатор товара, количество)`). +- `LINE_PRODUCT_ID_INDEX`, `LINE_QUANTITY_INDEX` — позиции полей строки корзины + (`0`, `1`), аннотированы `typing.Final`. + +### `add_to_cart(storage: list[Product], cart: list[CartLine], product_id: int, quantity: int) -> CartLine | None` + +Переносит `quantity` единиц товара `product_id` со склада в корзину. + +- **Вход:** хранилище, корзина, идентификатор товара, количество для добавления. +- **Выход:** строка корзины для `product_id` после добавления, либо `None`. + +**Поведение.** Проверки выполняются в этом порядке; при первой же неудаче +остальные действия не выполняются: + +1. Товар ищется в хранилище (через `read_product`). Если его нет — `read_product` + печатает сообщение с идентификатором, `add_to_cart` возвращает `None`, больше + ничего не меняется. +2. Иначе количество на складе сравнивается с запрошенным. Если на складе строго + меньше, чем `quantity` — печатается сообщение об ошибке, содержащее + идентификатор товара (например: + `not enough stock for product : <склад> available, requested`), + возвращается `None`, ничего не меняется. Запрос ровно на всё имеющееся + количество (`quantity == склад`) допустим. +3. Иначе количество товара в хранилище уменьшается на `quantity` (наименование и + цена не меняются), а в корзину добавляется `quantity` единиц: если строка для + товара уже была — увеличивается её количество, иначе создаётся новая строка. + Возвращается получившаяся строка корзины. + +### `remove_from_cart(storage: list[Product], cart: list[CartLine], product_id: int, quantity: int) -> CartLine | None` + +Переносит `quantity` единиц товара `product_id` из корзины обратно на склад. +Принимает тот же набор аргументов, что и `add_to_cart`. + +- **Выход:** строка корзины для `product_id` после удаления (количество `0` + означает, что строка удалена), либо `None`. + +**Поведение.** Проверки в этом порядке: + +1. Строка корзины для `product_id` ищется через `find_cart_line`. Если её нет — + печатается сообщение об ошибке, содержащее идентификатор (например: + `product is not in the cart`), возвращается `None`, ничего не меняется. +2. Иначе количество в строке корзины сравнивается с запрошенным. Если в корзине + строго меньше, чем `quantity` — печатается сообщение об ошибке с + идентификатором (например: + `cart holds only <в корзине> unit(s) of product , cannot remove `), + возвращается `None`, ничего не меняется. +3. Товар ищется в хранилище через `read_product`; если его там нет — + возвращается `None` (сообщение печатает `read_product`). +4. Иначе количество товара в хранилище увеличивается на `quantity`, а из строки + корзины вычитается `quantity` (наименование и цена не меняются); если + количество в строке стало `0`, строка удаляется. Возвращается получившаяся + строка корзины. + +## Инварианты + +- Для любой успешно созданной записи + `read_product(storage, create_product(storage, (name, price, qty)))` возвращает + `(id, name, normalize_price(price), qty)`. +- После `delete_product(storage, id)` вызов `read_product(storage, id)` + возвращает `None`. +- Цена в хранилище всегда нормализована: `normalize_price` применяется при + каждом создании и изменении записи. +- Сохранение количества: при успешном `add_to_cart` или `remove_from_cart` сумма + «количество товара на складе + количество этого товара в корзине» не меняется. +- `add_to_cart(...)` и следующий за ним `remove_from_cart(...)` с тем же + `quantity` возвращают склад и корзину к исходному состоянию. diff --git a/pl-001-python-basics-task/src/part2/__init__.py b/pl-001-python-basics-task/src/part2/__init__.py new file mode 100644 index 00000000..e953a49e --- /dev/null +++ b/pl-001-python-basics-task/src/part2/__init__.py @@ -0,0 +1 @@ +"""Homework 4: a CRUD interface over an in-memory list-of-tuples product store.""" diff --git a/pl-001-python-basics-task/src/part2/cart.py b/pl-001-python-basics-task/src/part2/cart.py new file mode 100644 index 00000000..f5f09a1d --- /dev/null +++ b/pl-001-python-basics-task/src/part2/cart.py @@ -0,0 +1,154 @@ +"""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 +from .storage import ( + NAME_INDEX, + PRICE_INDEX, + QUANTITY_INDEX, + Product, +) + + +type CartLine = tuple[int, int] + +# TODO: задайте позиции полей внутри кортежа CartLine +LINE_PRODUCT_ID_INDEX: Final = 0 +LINE_QUANTITY_INDEX: Final = 1 + + +def find_line(cart: list[CartLine], product_id: int) -> int | None: + for i, line in enumerate(cart): + if line[LINE_PRODUCT_ID_INDEX] == product_id: + return i + return None + + +def add_to_cart( + storage: list[Product], + cart: list[CartLine], + 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: реализуйте функцию + + product = read_product(storage, product_id) + + if product is None: + return None + + left_product = product[QUANTITY_INDEX] + if left_product < quantity: + print( + f"not enought stock for product {product_id}:{left_product} available, {quantity} requested" + ) + return None + + update_product( + storage, + product_id, + (product[NAME_INDEX], product[PRICE_INDEX], left_product - quantity), + ) + line_index = find_line(cart, product_id) + + if line_index is not None: + new_quatity = cart[line_index][LINE_QUANTITY_INDEX] + quantity + new_line = (product_id, new_quatity) + cart[line_index] = new_line + return new_line + + new_line = (product_id, quantity) + cart.append(new_line) + return new_line + + +def remove_from_cart( + storage: list[Product], + cart: list[CartLine], + 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: реализуйте функцию + + line_index = find_line(cart, product_id) + if line_index is None: + print(f"product {product_id} is not in the cart") + return None + + cart_quantity = cart[line_index][LINE_QUANTITY_INDEX] + if cart_quantity < quantity: + print( + f"cart holds only {cart_quantity} unit of product {product_id}, cannot remove {quantity}" + ) + return None + + product = read_product(storage, product_id) + if product is None: + return None + + left_product = product[QUANTITY_INDEX] + update_product( + storage, + product_id, + (product[PRICE_INDEX], left_product + quantity), + ) + new_quantity = cart[line_index][LINE_QUANTITY_INDEX] - quantity + if new_quantity == 0: + del cart[line_index] + return (product_id, 0) + + new_line = (product_id, new_quantity) + cart[line_index] = new_line + return new_line diff --git a/pl-001-python-basics-task/src/part2/crud.py b/pl-001-python-basics-task/src/part2/crud.py new file mode 100644 index 00000000..1efec830 --- /dev/null +++ b/pl-001-python-basics-task/src/part2/crud.py @@ -0,0 +1,148 @@ +"""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 ( + NAME_INDEX, + PRODUCT_ID_INDEX, + PRODUCT_ID_MIN, + Product, +) +from .utils import normalize_price + + +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.part2.storage.PRODUCT_ID_MIN` when ``storage`` is + empty. + """ + # TODO: реализуйте функцию + if not storage: + return PRODUCT_ID_MIN + return max(product[PRODUCT_ID_INDEX] for product 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. + ``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 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: реализуйте функцию + quantity, price, name = fields + for product in storage: + if product[NAME_INDEX] == name: + print(f"product name {name} is already taken") + return None + id_product = generate_product_id(storage) + normalized_price = normalize_price(price) + new_product = (id_product, name, normalized_price, quantity) + storage.append(new_product) + return id_product + + +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: реализуйте функцию + 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 no product carries that identifier (``storage`` is + left unchanged and a message is printed). + """ + # TODO: реализуйте функцию + price, name, quatity = fields + for i, product in enumerate(storage): + if product[PRODUCT_ID_INDEX] == product_id: + normalized_price = normalize_price(price) + updated_product = (product_id, name, normalized_price, quatity) + storage[i] = updated_product + return updated_product + print(f"no product with id {product_id}") + return None + + +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: реализуйте функцию + for product in storage: + if product[PRODUCT_ID_INDEX] == product_id: + storage.remove(product) + return product_id + print(f"no product with id {product_id}") + return None diff --git a/pl-001-python-basics-task/src/part2/storage.py b/pl-001-python-basics-task/src/part2/storage.py new file mode 100644 index 00000000..35821c5f --- /dev/null +++ b/pl-001-python-basics-task/src/part2/storage.py @@ -0,0 +1,34 @@ +"""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 = 1 +PRICE_INDEX: Final = 2 +QUANTITY_INDEX: Final = 3 + +# TODO: задайте идентификатор первого товара в пустом хранилище +PRODUCT_ID_MIN: Final[int] = 1 diff --git a/pl-001-python-basics-task/src/part2/utils.py b/pl-001-python-basics-task/src/part2/utils.py new file mode 100644 index 00000000..ab162ad5 --- /dev/null +++ b/pl-001-python-basics-task/src/part2/utils.py @@ -0,0 +1,29 @@ +"""Standalone helpers shared by the hw4 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 +digits (:data:`PRICE_PRECISION`) that every stored price uses, keeping +currency values free of binary floating-point error. +""" + +from decimal import ROUND_HALF_UP, Decimal +from typing import Final + + +# TODO: задайте число знаков после запятой и шаг квантования +PRICE_PRECISION: Final[int] = 2 +PRICE_STEP: Final = Decimal(1).scaleb(-PRICE_PRECISION) + + +def normalize_price(price: Decimal) -> Decimal: + """Round a price to the precision every stored record uses. + + Args: + price: The raw price amount. + + Returns: + ``price`` quantised to :data:`PRICE_PRECISION` fractional digits, + with halves rounded up. + """ + # TODO: реализуйте функцию + return Decimal(price.quantize(PRICE_STEP, rounding=ROUND_HALF_UP)) diff --git a/pl-001-python-basics-task/uv.lock b/pl-001-python-basics-task/uv.lock new file mode 100644 index 00000000..2a56c481 --- /dev/null +++ b/pl-001-python-basics-task/uv.lock @@ -0,0 +1,297 @@ +version = 1 +revision = 2 +requires-python = ">=3.12" +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version < '3.15'", +] + +[[package]] +name = "ast-serialize" +version = "0.11.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/1e/4f6082cdd6e5a29093513e9a3eabc5ed1c5331a9a84386b2fece80a00a48/ast_serialize-0.11.2.tar.gz", hash = "sha256:976a5bd75845d22f4b52905ddf53ab669ef1b14dba7735f5512841a2ef2b5450", size = 954387, upload-time = "2026-09-13T18:48:55.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/2e/beec3364eef4b01793a676d8cd16e9014c42044a5505000ceae3955e33fa/ast_serialize-0.11.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f6a8dfc5ab204a706f6e5d39c6f77c18c27ef084fa2081803a64a9160ce89277", size = 897089, upload-time = "2026-09-13T18:47:22.69Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d7/ef56443df2891c6ba2c4019c2cb3dcaf97c9948da6d963068e04e8dac6ea/ast_serialize-0.11.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:cb073bfa15742699d408ac50f60878383b5665ae1791d1b6799ea6f08633cd77", size = 1235218, upload-time = "2026-09-13T18:47:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/42/8d/cff58d17ba1d0272ff0b7ab5d3bdfcf8f47317eb0f47c001d394bffebf95/ast_serialize-0.11.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1d6ad94edbe93bf1dabc06c9f37d55b898fdabc456aa6d7ced5e23c14f795f32", size = 1216399, upload-time = "2026-09-13T18:47:26.202Z" }, + { url = "https://files.pythonhosted.org/packages/de/d2/a1da7675af5f42335c36e4da6d86ef4fd7168cead18de81df0a2d6faeb1a/ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40b2801cf2221bd922d9f69d2f0ebc373c3db47207315d525b2d87fa161a2af4", size = 1282064, upload-time = "2026-09-13T18:47:27.787Z" }, + { url = "https://files.pythonhosted.org/packages/97/89/5a400a13b2c9c0152ebb5ad45408a3fe5e4e60e325d3ac4e5cf6e915a0cc/ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd666cebd6ab3b3c0fd348a6202c26e18a401ee34293c3804d3472266bc146f6", size = 1285864, upload-time = "2026-09-13T18:47:29.667Z" }, + { url = "https://files.pythonhosted.org/packages/02/b8/80a381c70fd49f0316fb0383c4f9e4c13e81b010b64889bd45898ce8f5f4/ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0d01f61352c96370febf6c0dbd488dee9183a731fb2702170da9163ae317cded", size = 1554755, upload-time = "2026-09-13T18:47:31.257Z" }, + { url = "https://files.pythonhosted.org/packages/90/97/dcaa34a32d2db789221c125b3eb10feb5089715fe53d9874d627afc26231/ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a0fd40c668b0fa19b8fdb61d9e63d547e2e19cfbfe053a51ef0b6c37070298a8", size = 1301807, upload-time = "2026-09-13T18:47:32.714Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/84a22420cb312642d7d31547c644d09a3d101418c6d6b9ef2ec30735cf11/ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efa819d7c14c8e4153dcd84671826331538be7cbe460383fc6386f5eea5bd234", size = 1301941, upload-time = "2026-09-13T18:47:34.418Z" }, + { url = "https://files.pythonhosted.org/packages/20/8a/aa5f3dcf1aed9678c25982f40d366004e3c0cac47bc0c240f6b837dcbb1f/ast_serialize-0.11.2-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:a9ffa8a197a721f07a352d0be6185f5b3e6f9aaebfdb66169ed652108531ae3b", size = 1307910, upload-time = "2026-09-13T18:47:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/78/79/91a5102797fe3dc992171382d8579bcb33cbd1424b864ad3117ac43fb3fe/ast_serialize-0.11.2-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00119a8fb8c1dc0f1fab023f4d8071fa49e3b0208ee54d589fd463c16ab0124e", size = 1356258, upload-time = "2026-09-13T18:47:37.984Z" }, + { url = "https://files.pythonhosted.org/packages/51/52/54eeef9918e187ced417c4363eecea66975314cd5b9c91759eef7f7b714b/ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0de02520c11391a026e62987a9aa2c3c2ff01545155059ddf0c4bdf2c5ecbe9f", size = 1459057, upload-time = "2026-09-13T18:47:39.891Z" }, + { url = "https://files.pythonhosted.org/packages/e4/cb/fd84b52b15d42f2423319cffd1fb7f1e9df5d5198e69ab0b449c450254cf/ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b4e4558956b6a0fb35e18fba58f7d1810b1f2c0e6b52352572cd5dfb6b4ef33a", size = 1562447, upload-time = "2026-09-13T18:47:41.727Z" }, + { url = "https://files.pythonhosted.org/packages/be/92/9fb34f2e64b84a63cca92fb86bd0847b995a63b67477f44c20502fb60352/ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6061a54f39e82a9f2cbcb9c268fc441890e4818a6636473caa4f4063254e0750", size = 1556423, upload-time = "2026-09-13T18:47:43.357Z" }, + { url = "https://files.pythonhosted.org/packages/75/0f/c43c44449e7ebc4e83ebd48750088fb06234622faa2d62d2a6dc8970d2a3/ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:85fbb01e83967a126d71f679f2b9528ef0912cb0854aa1a4657314c34e255b57", size = 1687156, upload-time = "2026-09-13T18:47:44.995Z" }, + { url = "https://files.pythonhosted.org/packages/2b/a7/9e520f4a79b639da9ee20c1e747c3d739329e902fc55ac38065f25419f56/ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:7aaaffc32905159774a107d3cf33dad59bd41b7a0d1bc9885532186753ee7439", size = 1481008, upload-time = "2026-09-13T18:47:46.602Z" }, + { url = "https://files.pythonhosted.org/packages/48/a8/bdd3989f19de09cffcd8179c131f6741a5a8619705fc75b09541ff61530b/ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:08eda88a0f290a36c38cab33df8bf7e35eb95bc802ca5beb2c8fcda471a7d10c", size = 1501597, upload-time = "2026-09-13T18:47:48.265Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8a/ca2dce2950875a4ef1d7c298196f803b0adcdb7c15ed0cecc71d84bccd70/ast_serialize-0.11.2-cp314-cp314t-win32.whl", hash = "sha256:76cc294246e60a914326b4ca88c6a5ea89c064906614aaf1537ce82f09e9449f", size = 1119503, upload-time = "2026-09-13T18:47:49.896Z" }, + { url = "https://files.pythonhosted.org/packages/5a/12/3f38e3613d07c46f9f81c5b1352748c6552397cc52825502e2c6ae44c6ea/ast_serialize-0.11.2-cp314-cp314t-win_amd64.whl", hash = "sha256:43b51e6ebe6549bf21416c3c78ee886147b80875a87cc6f69e303dde0d75be0b", size = 1156828, upload-time = "2026-09-13T18:47:51.454Z" }, + { url = "https://files.pythonhosted.org/packages/22/90/f89a4f67428a261daafdb69a0d0132c27933268702d1ba47e0b61c51aff1/ast_serialize-0.11.2-cp314-cp314t-win_arm64.whl", hash = "sha256:8df32ad4ff7843734a6c2f067ee974f6d3109ee5a2c3e1a9d2f79347bd282a9a", size = 1128298, upload-time = "2026-09-13T18:47:53.008Z" }, + { url = "https://files.pythonhosted.org/packages/0b/55/a1962188abf0e62d84d55892bb044347e434711763b9a1d4ad867a70c1be/ast_serialize-0.11.2-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:ab924ba260efd7509492f272d4e236d24564033f20c005d7c63a107c6a76fc85", size = 1235457, upload-time = "2026-09-13T18:47:54.554Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ad/439c2959150718446af76fbe2f4000f35eba9869ef8564f3d9a3d0b1c370/ast_serialize-0.11.2-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:a586be418eb70a9f1396cea29ddac8f4b9bf277fb73ea2340db31e218bc00f32", size = 1215705, upload-time = "2026-09-13T18:47:56.178Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d8/2c6542fc3e7c56a0a25d8d12d034d5a2d2e1900e292567b1c1dca8e83124/ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8532f20916fa3189d4d785ef2a62d93c4d651ec9c5bffda66d2fc36898351f34", size = 1282530, upload-time = "2026-09-13T18:47:57.619Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ad/6f6755cd0842db46c3b10b1e4735f14aad78d71dea4753eb46933101711b/ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ee732ae167e686d1d3c00f98d7d82b23138304694f0441b14d7ddf9c0f8a921c", size = 1287792, upload-time = "2026-09-13T18:47:59.227Z" }, + { url = "https://files.pythonhosted.org/packages/03/40/5da672f5dd23fb7dc0c884c97711e56a3540f2fe3c4355a81f8beb385911/ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:75a1c7f46b9c19fc0ae01ca6fd076301628faa2ed7a8edbd55c6353c483946a3", size = 1557971, upload-time = "2026-09-13T18:48:00.96Z" }, + { url = "https://files.pythonhosted.org/packages/df/c7/2bb25684f697801eb72866fdb94ed5edbff3867ce878b0e542a4a5b9dab9/ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2fdf31a0bb85ea2575cc91669f005e6647d2efed491231c4dc1497bc9a5b3aa6", size = 1303230, upload-time = "2026-09-13T18:48:02.337Z" }, + { url = "https://files.pythonhosted.org/packages/d8/85/754681846f26e0ff1da729b1ffe3171e93c22f0aa6ec3cea5b14e3703846/ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b78e6fdef3b06c86ed263e1962fee5a7b9d2d158e738b212d13b2c605ee12f5", size = 1302271, upload-time = "2026-09-13T18:48:03.915Z" }, + { url = "https://files.pythonhosted.org/packages/fb/dc/f5521d8cb44b69095c3982ae3658a12c403e0efa19e51aeb9c8a79dff60c/ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:8d62a47714c8bc432b9fabcc29989c815c5da17327d35151f2fd0d85c2a7a5ff", size = 1309529, upload-time = "2026-09-13T18:48:05.562Z" }, + { url = "https://files.pythonhosted.org/packages/73/0d/649182c7fd7c4f782279bed514de2dd67e48a5afecb605a098d64fdc01fd/ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8a5ffa70e76191dcf240d3c43e20c93b3bfd26f54d89148c762d57837f5bcd2c", size = 1356869, upload-time = "2026-09-13T18:48:07.534Z" }, + { url = "https://files.pythonhosted.org/packages/bf/cc/aff4d84c16afa742d13a75384127c7d24594dc8c304f0558a15924fd51af/ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:bfbe47a3a7c368f28836e78b2440a3643ac0ec4c67d9fe53588e1448f0a3d35d", size = 1460006, upload-time = "2026-09-13T18:48:09.162Z" }, + { url = "https://files.pythonhosted.org/packages/94/a7/891cbec2e5e0d7159196159d3ff0646622f3120ff4576c839ac2dd56c719/ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:7f1823275b246f9c7d373be6879e4eec09686948895d4ad083f4b27fd7e4da70", size = 1562935, upload-time = "2026-09-13T18:48:10.978Z" }, + { url = "https://files.pythonhosted.org/packages/45/c4/2c8c4498340ea9aff87a9fd408309aa25d56dd51d7bbddfdb46a3c31424a/ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:57c0f5cb0021a5beb1e5e4d6e840ae2f23a28909703ef4d256a144cc1ad3d437", size = 1557109, upload-time = "2026-09-13T18:48:12.616Z" }, + { url = "https://files.pythonhosted.org/packages/0d/8b/c5d4e5226fa18885fe17f949aee3ab1aeb8389c384d946ec1b7c9489cc94/ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:cd320a5c4f1f2742af97eea22954f776379175c5ef2504801e9a155f2ff9a4d7", size = 1691603, upload-time = "2026-09-13T18:48:14.293Z" }, + { url = "https://files.pythonhosted.org/packages/73/d6/1d2ca472586f9e3416a289a22f36eeb6dd6f47d77b1a4aba358405babbc7/ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:13b13afe32e845c86a573497729e1b7ddeb26c572c78bf50ece51da23b8fad5e", size = 1483053, upload-time = "2026-09-13T18:48:15.789Z" }, + { url = "https://files.pythonhosted.org/packages/0e/16/d3703a7c1e3c76b144ac9349a54d3926d0749918a8dc13a66cede208b8ec/ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:9d80a81ec84660422579bdb8e789f656a794b48c7a1ae1261f6bd8bc1897d17d", size = 1502499, upload-time = "2026-09-13T18:48:17.405Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e4/d974e55c2e247ef26ed1df01c74940583db9a5b3a8bcaad5732c6e2047fb/ast_serialize-0.11.2-cp315-abi3.abi3t-win32.whl", hash = "sha256:af8c003ce721b0099dd55cef4ba733500fc3054ea0cc8565d8957aaf7cccdeb4", size = 1119739, upload-time = "2026-09-13T18:48:19.005Z" }, + { url = "https://files.pythonhosted.org/packages/0d/00/d229443488e095054d5e0c0cc20689a2633b899d735849ff1b2c8e4f0cbf/ast_serialize-0.11.2-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:554d117cb916d8032d85007c654d179efbbfd446174c048062778136a922944f", size = 1158602, upload-time = "2026-09-13T18:48:20.524Z" }, + { url = "https://files.pythonhosted.org/packages/11/75/389fc1a6cfa0c4b2ce522f47d8401329d8bb11732e516d46465960fef1d9/ast_serialize-0.11.2-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:d60515335750d431e462af6e722bb55720a5e7827192777bddfd9c4376065a4d", size = 1128842, upload-time = "2026-09-13T18:48:22.052Z" }, + { url = "https://files.pythonhosted.org/packages/45/7d/c4f36898f19c728d091cdfdf960c9488e8d82bbe2e49ba13c05f43907a5d/ast_serialize-0.11.2-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:89499a439955931281986e97ca4dd3c064bf0d2e0027c0017344eb86667733a1", size = 897204, upload-time = "2026-09-13T18:48:23.695Z" }, + { url = "https://files.pythonhosted.org/packages/b1/54/f67120006fc73a55b6d057d4662d061fbb4eceafce3047c76ca8b382eb11/ast_serialize-0.11.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:daadf1c3e0224621607ffe16f1379e4bd372271ed2e1db8a67878f0bab3ef7e4", size = 1240734, upload-time = "2026-09-13T18:48:25.287Z" }, + { url = "https://files.pythonhosted.org/packages/9a/7e/8f2ab68bddbe58a66fbbaad87beeae3e7d7edddb17263d1fc423936cf34d/ast_serialize-0.11.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1844ed9a487fb3de7325c52ddb33f2918b66b65cd54d3f8d83d23785ffe99fa4", size = 1228053, upload-time = "2026-09-13T18:48:26.788Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1b/8a69ab68f4c1603819f0481d756abdd8caf27cec7f1d77caa71007ebe997/ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b17869f4ba261a5fa468a753328a548f4dbaf74b4eadae9e28aff66df7f1425b", size = 1292542, upload-time = "2026-09-13T18:48:28.295Z" }, + { url = "https://files.pythonhosted.org/packages/d1/ce/872f2e00f0467c289e483f0a34543463347243a2d0632748d89fcee5e0dc/ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:feb16d9c2a720e0120c58dd5d6e7b3c7c86b43249b60a3bc212bcb8fa031e2dd", size = 1294791, upload-time = "2026-09-13T18:48:29.969Z" }, + { url = "https://files.pythonhosted.org/packages/3a/82/36277c12af861c64b375c316135d8feffe3f400568463a8d2b2de4c2c4fb/ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3109fe4805384effc8d0f8e41fbf875aa8f389af91b4348c1cfb60ea6e4cb82", size = 1567583, upload-time = "2026-09-13T18:48:31.85Z" }, + { url = "https://files.pythonhosted.org/packages/b0/d7/ec643df91cea8bcbcb4e8011d6a8b08e5119b84f9554879f3e3c786d29d1/ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abdb3e49ba053c3486ac1263bee9f16cc9a4a8abd9f8c90bfc21e3669f3ad9d1", size = 1312878, upload-time = "2026-09-13T18:48:33.495Z" }, + { url = "https://files.pythonhosted.org/packages/04/6f/4c992cd7841ba589fefb14ddc9aff2f6db7f2a615d4074f9ad04115b5ce0/ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7004ba572f09be34342ccb98dcd4bad5707d3d81adc8cb4c3f685d2a2c51bbc", size = 1312642, upload-time = "2026-09-13T18:48:35.294Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e3/22aaa209c231a83cfea004fd67dee7a7a54da3f169c6c460b14b96887385/ast_serialize-0.11.2-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:59c25f47524efa052971b860e128b1add0c94ede7dd16b2962952c85c3582365", size = 1319776, upload-time = "2026-09-13T18:48:36.866Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f7/d4685fb54d10108ce44d3bc893ef670854d61645d47ed96d73524db90c23/ast_serialize-0.11.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f3a367e0e05ed2d1b747ceb07aa728a8c204cc008b589127e9bd4f40053d7575", size = 1365324, upload-time = "2026-09-13T18:48:38.412Z" }, + { url = "https://files.pythonhosted.org/packages/42/3a/250643ffad02bda520c50a9a5f02a5d43259a06f34ce393c91761d134d7e/ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:00bbf1f6669f813b48925b759f7ae4591067d456d443924055cab386e7e0a719", size = 1467653, upload-time = "2026-09-13T18:48:40.348Z" }, + { url = "https://files.pythonhosted.org/packages/c7/da/af66a646b9b7f8fdec95ce83fc7b1fe538b06864bc79bd554ac4fae2e6ea/ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ec1c20f89c3e0d83576e3c06f79375ce936266591fe0d5fd969914af3185cbaa", size = 1571914, upload-time = "2026-09-13T18:48:41.968Z" }, + { url = "https://files.pythonhosted.org/packages/34/82/77a9714564b9e8800087a8afec41527c65c39e49282baae2ac847b9c1c6a/ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:c58bb119b73657fdc5569692f316e1e25ca114bd62f7782eb527c6be438ba3a9", size = 1569862, upload-time = "2026-09-13T18:48:43.701Z" }, + { url = "https://files.pythonhosted.org/packages/65/06/fa77b52f46b9bd6dcd8ff2b880e3781f8c1a316bb1342bc3de92907c6f96/ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:f739e0b601be7300c5697a2573d9200bd1db74b34ab111ef9537b9d5dcd7f106", size = 1699020, upload-time = "2026-09-13T18:48:45.261Z" }, + { url = "https://files.pythonhosted.org/packages/e1/09/239c83153c7e0798e5867d6909cb06f53dccfef02f6999c8e2e21ecb98c3/ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:cae5addfbb54cc1d47fe947ef9138e9d83849ed1cbc72b819cf36d96a2315b07", size = 1492869, upload-time = "2026-09-13T18:48:46.922Z" }, + { url = "https://files.pythonhosted.org/packages/2f/eb/6108fb9a43fc7ab5529856e38e33c6e3e064fbfe375fdcbb208c7cd5438d/ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2fa3be25f7f5351b1b39c9f8a52779b2dbf21199efbae564b4746422e8edca4e", size = 1511621, upload-time = "2026-09-13T18:48:48.667Z" }, + { url = "https://files.pythonhosted.org/packages/8a/82/60367e58ef346a41ebc90d3f28593c1b8f5c2cb5314c7b2bbd98910ee131/ast_serialize-0.11.2-cp39-abi3-win32.whl", hash = "sha256:d70556a2f9230a44c99a655774cde823f056efc34466eabfb4085f0cb1ea9f99", size = 1125873, upload-time = "2026-09-13T18:48:50.661Z" }, + { url = "https://files.pythonhosted.org/packages/23/bf/b419c3205ce1143ba7c69baef4f0ba43c14d8712113bf34f9e0d27d609be/ast_serialize-0.11.2-cp39-abi3-win_amd64.whl", hash = "sha256:b9065dd23131a23b41f5bab3bf4e9b3c350a3fe8e36e8200eded9b729fcea484", size = 1165434, upload-time = "2026-09-13T18:48:52.169Z" }, + { url = "https://files.pythonhosted.org/packages/91/a7/c8bbb2173f7a7131b3b2412035b2d814ab5ef2ce9799bd06f07c451640e4/ast_serialize-0.11.2-cp39-abi3-win_arm64.whl", hash = "sha256:dab599cbdcb7b45b18c41fad746645580b3a24357082b7f0e8921cd373804f27", size = 1136031, upload-time = "2026-09-13T18:48:54.04Z" }, +] + +[[package]] +name = "librt" +version = "0.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/9b/356320fbae2ac8467e21c5e73e1389c80468e4998c62cc7d3536cc51b614/librt-0.15.0.tar.gz", hash = "sha256:4e66cbe84437497d951b799d3e1551291b6fb3d643820a7014b3655d57a59162", size = 214338, upload-time = "2026-08-07T10:49:42.663Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/39/99c25030e782bdfb7a21be8c05254806a2e4bbb05c8d50c2a2130acbfa05/librt-0.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e87bc679f86a99aa3b26e3c78eeb821a247c9a28eae48eaafcc32c3bf4c3bb9e", size = 151021, upload-time = "2026-08-07T10:47:00.057Z" }, + { url = "https://files.pythonhosted.org/packages/14/43/f4b1bd1b2888798a1409808889a25ea1ba49eaabce7d681ed27734c2df9d/librt-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71599e011ac880e8e45d46047d714871894c7d4ab6f25626f8d4f89da21f368d", size = 155267, upload-time = "2026-08-07T10:47:01.311Z" }, + { url = "https://files.pythonhosted.org/packages/0c/db/3ad9c965c72f1e1d6beeec44ec10a54e17be8ae042fbb4baade16cbadced/librt-0.15.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c802434092b769b1d613ed2e13fac15fbfce1934a74bd10283b03c0fae231cd1", size = 503136, upload-time = "2026-08-07T10:47:02.45Z" }, + { url = "https://files.pythonhosted.org/packages/4b/07/5888a6d76acd62ebce66c61b74d94e9370b9c32929f111e487bb6546f8ed/librt-0.15.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5500eeae393a184d14e1f35645962c27129d20c81afa4069e6ef826ebc2b3aaa", size = 496670, upload-time = "2026-08-07T10:47:03.675Z" }, + { url = "https://files.pythonhosted.org/packages/29/39/ab57cc2f5b276156da02bb7f5a8921bada1cb1993ffec99acf811c602c23/librt-0.15.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6ecfc32dfb46fb7b565bcd6abf9412acf978775a998273d22888a6d7953730dd", size = 513688, upload-time = "2026-08-07T10:47:04.981Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b9/bdbb0b648b5c2befb031f4c6f3b1dd857415e8fb492a25a3c764a6681e6c/librt-0.15.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cc46cfd15022e35084355478c9ac809d90b1152222706ac9a7655ec21df6fa", size = 531904, upload-time = "2026-08-07T10:47:06.211Z" }, + { url = "https://files.pythonhosted.org/packages/93/26/473c2e4b6c104e9e58e27ce95fc8005c8bd4fc36cae4f254371125a92db8/librt-0.15.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5f51401d102c885b9ca509e62c79b1dbff286e1b9b047fde6f763780789356d", size = 524427, upload-time = "2026-08-07T10:47:07.592Z" }, + { url = "https://files.pythonhosted.org/packages/26/60/03b3abb82b41714671b907bf6989b228e31e6a8af52dec82b5b0728dc250/librt-0.15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cc30523e3f1a23fb7511cc659834a0d01a1042bb9de359bc1c131cc4ec6c9656", size = 543155, upload-time = "2026-08-07T10:47:08.866Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0e/9bb1f0a4affbd0a1888f4f79dc03ed2a299d9a2c26c59ab2a97dcbf11903/librt-0.15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:59fe030d8ae4a57e3fb7756bf35a858de74e04066fc8555c53d0af979132af81", size = 546890, upload-time = "2026-08-07T10:47:10.327Z" }, + { url = "https://files.pythonhosted.org/packages/dc/84/6937a280d461f7de6e031ffb02edc2b7c3c90d49d630565ce8ff27cbc5f2/librt-0.15.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5a6526a2a956bbb1e4ae3568c82e650fc99119c66bb011ea60715744955a2b4d", size = 555163, upload-time = "2026-08-07T10:47:11.798Z" }, + { url = "https://files.pythonhosted.org/packages/bc/95/2a2853c1ee014bf102116e7f897a04beeaeb2461b45b79af98bdfb95f1ef/librt-0.15.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:85ea21ec6730194d67156b0e0b5430ccb1d61f8b8b907e39b37f9812b74a13f0", size = 535812, upload-time = "2026-08-07T10:47:13.279Z" }, + { url = "https://files.pythonhosted.org/packages/c9/4c/cf9601c1b4c5f09280acd5d83abdb2e68527a2be8257136eb42304218622/librt-0.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1e47b8ba865d7ede071a91a7163073bbaeb72541f1ef8a07d512c45c7b5007f2", size = 573688, upload-time = "2026-08-07T10:47:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/47/6d/9ac7cbec46189a7625af4b5acbd25f10d827f4141b2002181848c8418923/librt-0.15.0-cp312-cp312-win32.whl", hash = "sha256:a5207ec414d1c4a2a7231b2086970dc036f94293cdf338190984958a013a42f1", size = 106138, upload-time = "2026-08-07T10:47:15.973Z" }, + { url = "https://files.pythonhosted.org/packages/38/d0/2ae99c83be86ce23f925ac1aeeedc777e97f427c4a8d190c70d0a16e9a87/librt-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:73b30cfa976659b3917c8f6153bdb0591c6a9ec6583599fd24a689b690622022", size = 126974, upload-time = "2026-08-07T10:47:17.049Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ef/dd24f9635c730b86b87587967dda7516b1845e8b17684603d31607fed598/librt-0.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:a54cf9e0ef47b96af580849db5471142200568ce1e02cbf416addab551369570", size = 112292, upload-time = "2026-08-07T10:47:18.222Z" }, + { url = "https://files.pythonhosted.org/packages/e7/42/467b53a601b406ccd7b97c1fd54b59cb34f9185ad5ce7e9d5c3c4e8961c8/librt-0.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:db13ca398005abcbe538deda87b686d9bd08b7001cf40c4c06b444960ae10a26", size = 151029, upload-time = "2026-08-07T10:47:19.312Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e6/36c2299b7a94b84fdd01220d8a777a71be5be0925bb0dbdf71c0a06a34d9/librt-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa1f1995789dca3698bc550aaceb09a51bd5df0a057ff84ff15296cd1975b801", size = 155194, upload-time = "2026-08-07T10:47:20.398Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b6/ed5071f9325845e670bd36012757419767fbf56af77ed483077b9e4db541/librt-0.15.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55456ea87d8df21808446d03817be2f65e20391c1c615d9187440dff28cd08dc", size = 502568, upload-time = "2026-08-07T10:47:21.652Z" }, + { url = "https://files.pythonhosted.org/packages/7f/81/6450c67c3615d87704bcbc21323fafc69c799b06a044c447529f725d4b01/librt-0.15.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5a86a5a08c2235316bdb359d5dbb6ce0abfca7fac06363103e2c5af571d92f95", size = 496153, upload-time = "2026-08-07T10:47:22.925Z" }, + { url = "https://files.pythonhosted.org/packages/e1/d6/5f52b722bc75076954b3bfd49be15ea362df4d580c6fb315d0f617100d30/librt-0.15.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e56b6a368529bed262da40ce13f8fef590db0479819cca84f16a1f01ac356d0b", size = 513336, upload-time = "2026-08-07T10:47:24.213Z" }, + { url = "https://files.pythonhosted.org/packages/8d/e2/c08fd1d36ce63ea5a12b85c5d37f4550b5f86a692167e41e5a74222607ae/librt-0.15.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:234d8d394721fa0d786af15ebf1f3fb7f3ed82fd1cd0cde45c2f247b5d4281d2", size = 531661, upload-time = "2026-08-07T10:47:25.507Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d8/d9482fcbeb177b9eb87bb3899eeb3b42be690313c652f9e146b1d0681fb2/librt-0.15.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d8363d7accb0286ac3a0e633f396e93800dafb8150494505daf9515bbda591f3", size = 524487, upload-time = "2026-08-07T10:47:26.79Z" }, + { url = "https://files.pythonhosted.org/packages/10/cc/075171517b41f861753034fbb151b42cfc83bcc853849f24f5e66fd60ccf/librt-0.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0f0ee3644d951f31055ad07d77d92520e84505dd7a432cc4cd501dd70ee06785", size = 543201, upload-time = "2026-08-07T10:47:27.999Z" }, + { url = "https://files.pythonhosted.org/packages/b0/03/42c2330f37eeb475b6affeedd06518f60035f323af3a839335e3fc9fef2d/librt-0.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2cfd1a81a648806e6a7717be4cc4d1bb392fa229752bf8444ba365e381e984d6", size = 546467, upload-time = "2026-08-07T10:47:29.396Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/1ad4c5638f7e64d8560328bd25c54b409a661bdb6ff254b38ff90744288d/librt-0.15.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a6cd22c9da0d866558e46a041f1cc0c2bbb26b61b137b2347fa834c332e1d101", size = 555139, upload-time = "2026-08-07T10:47:30.815Z" }, + { url = "https://files.pythonhosted.org/packages/49/41/39fa7d15db1204cd1cbe6514680fbdc243adf754a0885061308f43afc013/librt-0.15.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6d5225ef8801e4ea5e482fa9b5dfb891dd9ef6f6d870f1f25d449ca2c70ac218", size = 536050, upload-time = "2026-08-07T10:47:32.222Z" }, + { url = "https://files.pythonhosted.org/packages/1e/88/c6dcf0dd8e26dc0c9a499a2abab8646c86dcaf9ecea9524cb46d3686331a/librt-0.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d28a05796b99f749bf8794f17ba9ba1612d0076b802e9cfc62c554634e9ce3b", size = 573700, upload-time = "2026-08-07T10:47:33.527Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9b/ab54c71a7918a7c34fa5327fb61390a77446a07a146fbfb1165250a61035/librt-0.15.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:2067ff438048cead9d223ca5675bae2a25e520a7c3e6c1498bf9c6892d22caab", size = 82194, upload-time = "2026-08-07T10:47:34.835Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b2/4f9a243bb892395f3becb80789ade13771701091f9f07ab8230247953ba8/librt-0.15.0-cp313-cp313-win32.whl", hash = "sha256:1cd3b721f24c206398b9e26da3c3a9c011e6e89d06f318ba8ebefc30f1003890", size = 106231, upload-time = "2026-08-07T10:47:36.251Z" }, + { url = "https://files.pythonhosted.org/packages/bf/af/64aff4885a40b93132382f2c314647d722574605416504379184ef3045ea/librt-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:f395a4a9a03ac062dbe9a9f82e0c720502e590a38feee6a757bc82e9c63afbd8", size = 126996, upload-time = "2026-08-07T10:47:37.453Z" }, + { url = "https://files.pythonhosted.org/packages/27/83/335bccf6c7cb9028cb0b54aead27d9ece3f01f83bc6baa2abace5da655c1/librt-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:0a15cb554761247d84a3ec0cbdf4078d70725384f0e4662c0fa3b26266eb60ad", size = 112188, upload-time = "2026-08-07T10:47:38.729Z" }, + { url = "https://files.pythonhosted.org/packages/a8/93/949053fb462eecc4a9a5ee770a81f4b40be7b79538b245545d4aebc6b58b/librt-0.15.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f5de7feedc56337a088eb15cd9fafa9938367362221d8cc62c642b7f94821993", size = 149833, upload-time = "2026-08-07T10:47:39.86Z" }, + { url = "https://files.pythonhosted.org/packages/61/ca/8281aa6cd560a3420e4497729f6b704b53be3eeaaef82d5aeadddaf7441f/librt-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c0eb900c0e91f4aebe680845242e614f1864edfd44106380d0752ac29522bf8", size = 154088, upload-time = "2026-08-07T10:47:41.065Z" }, + { url = "https://files.pythonhosted.org/packages/dd/02/1a1662dceaba6a086360891448d5ce9a7d3555976cae59a31a39d744b9c7/librt-0.15.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8c9a650a188e38bac005048cbe6342e81407782944d01934540ab75e417df21", size = 494215, upload-time = "2026-08-07T10:47:42.388Z" }, + { url = "https://files.pythonhosted.org/packages/69/84/99211619dc656370a3740c33d2b0b6d5a3fb1e73689314f6ed477a397dc4/librt-0.15.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:92bfed8deec93df30286b9fe9e3b1dd17329cc076a192b4ee5ec223841d54953", size = 491173, upload-time = "2026-08-07T10:47:43.683Z" }, + { url = "https://files.pythonhosted.org/packages/d4/aa/5448d0b05f4579b635d3899176817ebf561af0e57bacd425b5b1887264c1/librt-0.15.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec4b19788f835711a2072f9dbe6b03b3bf32ed1f0fb30cf399bdd59d9f0c33fa", size = 505512, upload-time = "2026-08-07T10:47:45.314Z" }, + { url = "https://files.pythonhosted.org/packages/95/82/01940e40b83c43a546c4a3c896cf34ca272a9690899d55914e4827b3dcce/librt-0.15.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4c7bacb70930f3d0a56f4ecf1be474a1f0d941b01dd73b756f3c256d42cb879", size = 523073, upload-time = "2026-08-07T10:47:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/88/fa/759c0030f3ee371439eb26de34fc745807caf0abb878af7af4b8b7c3dd3d/librt-0.15.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e79f05e4a08b4d880342673312bbc895b56df7765605796f15902eb5367d3ae", size = 515080, upload-time = "2026-08-07T10:47:48.319Z" }, + { url = "https://files.pythonhosted.org/packages/0b/27/894e072228fcb159703c655da69f8cd10dbed489c36e3df7dd032a2483be/librt-0.15.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a417149c0cba4d50b61e992e5a15e69eaf96746609b461cc4ed168aeef6b79dd", size = 534164, upload-time = "2026-08-07T10:47:49.875Z" }, + { url = "https://files.pythonhosted.org/packages/98/a3/0078e91c1f36f8815db17827de15650b9a3fe56c55fbf998c854b34e40d3/librt-0.15.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:da7a94d6a3411f579d72aa3e3bc5fbca7ed4549f3dbd7e5de3aa567333374285", size = 540616, upload-time = "2026-08-07T10:47:51.408Z" }, + { url = "https://files.pythonhosted.org/packages/86/33/81a29b796dd52a45e9ef7974c7732926e8f10f15b8d2be505665979f896d/librt-0.15.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:856f743ae607f2c1380eccb566c0038a9fb3eabf0fc2be2704d76d9f73557239", size = 545890, upload-time = "2026-08-07T10:47:52.818Z" }, + { url = "https://files.pythonhosted.org/packages/05/82/8be1baa1350e5d30cfd70ae79d0a6f4dc5862ef47f7bb2808aabc9bb86e5/librt-0.15.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:779a6e7c894737e5983e7790a9c78c4000c30e23c9aada08081bdbea53b0fa60", size = 523287, upload-time = "2026-08-07T10:47:54.165Z" }, + { url = "https://files.pythonhosted.org/packages/c6/4f/d1be6a01a35c20ef734e0e44113f87d4af756a9354a89dcfbe3b4f8af5e1/librt-0.15.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:96bb17dbe8bab3c0954fbebfc69ed395599de75b6bbc35e3270a878e15d4dd65", size = 565868, upload-time = "2026-08-07T10:47:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/67/88/649cfa33f5825927b160610f670bdab012a64d627eddb94fa795ea4292fd/librt-0.15.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:7220697efaa6e5348fc3d18ee7f8563d4bfecd9872b37ffb915bfc1d08840622", size = 81619, upload-time = "2026-08-07T10:47:56.886Z" }, + { url = "https://files.pythonhosted.org/packages/22/31/8e88a8d5e48fc8d1a817787fb6811dfff6499acd6c8683dd83934aa6ede0/librt-0.15.0-cp314-cp314-win32.whl", hash = "sha256:f54598964d357b1c5ab77cf5d92f21e598fe0e23cdbe9618480807f81b4eba15", size = 100138, upload-time = "2026-08-07T10:47:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/80/92/20fd6c4b6a1b1a564b076d55cd3d427d8428217d7638dc25a654cc4791d4/librt-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:3ff5893a2c23d886aa9ce786de5ac6ddc74aeeaf90743682b74d920e117d2e28", size = 121258, upload-time = "2026-08-07T10:47:59.564Z" }, + { url = "https://files.pythonhosted.org/packages/fc/28/6af430b44d9ebb897b865a3c363b6dcace51357be2347cc0f8f869656a86/librt-0.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:3722a099730704c9a3d70c879fc0f51daec25fe5f1555672d97bc595abeafb95", size = 106467, upload-time = "2026-08-07T10:48:01.097Z" }, + { url = "https://files.pythonhosted.org/packages/7e/aa/b42bb798942ced219f6d63b27e07f91237887a8d0bd0921666db79a13790/librt-0.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:38c0c7d4b6fc06c3324b3f9162c8391bfc4fd9dde53afe1033ce7edb48d5a714", size = 159523, upload-time = "2026-08-07T10:48:02.442Z" }, + { url = "https://files.pythonhosted.org/packages/75/03/1b53cd4ef904e73b1d828a5f90143bf94a2967d7cfff0b9ccf93e12aa9b4/librt-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8b2fdd7ead3c995c37940a790690660d0ca006c302db26cc51933f6766866fc3", size = 161638, upload-time = "2026-08-07T10:48:03.725Z" }, + { url = "https://files.pythonhosted.org/packages/ac/c4/9f9c9fba097d49e9e694c2b4dc331df31884645ecbc58a93b4b5fc69d2c5/librt-0.15.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fde98cf1fc4bac144ce23c2c4c017b924ba714509ea9334977b0b27050c837d", size = 701795, upload-time = "2026-08-07T10:48:05.135Z" }, + { url = "https://files.pythonhosted.org/packages/4c/05/0966840bda0380c8ae167b9043c6230202941cc90ea29c48e096964c765e/librt-0.15.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:e3b461183c5fa7681b48560f91515f53a953122fb30c71e07abc67d7ddf58c38", size = 682147, upload-time = "2026-08-07T10:48:06.555Z" }, + { url = "https://files.pythonhosted.org/packages/18/af/1c47ca573c30ea47d195aec26133af522fea1104afaace028d7b32247ea8/librt-0.15.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4bbcc257e3babea20a91715c361b24554ec4e8f51aa578568afc230799fe1a19", size = 696397, upload-time = "2026-08-07T10:48:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/2e/0f/1aed6223d4f9f9d1171a8596ff100ea4c3f7699fea7a4ba657c3e60daa6c/librt-0.15.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b845b8d48088fad0cadc84be4b8fda63203be7e9237b71015b3925443c1f35ab", size = 722542, upload-time = "2026-08-07T10:48:09.569Z" }, + { url = "https://files.pythonhosted.org/packages/c6/22/9e3a929aea456c97d69e6ef3884efea56d4807f97399471cc946baebd8af/librt-0.15.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b30e600e8f337b9bd7f39b86d9fdfedc73cc46e3d0f745931a23a234220bb7e2", size = 729709, upload-time = "2026-08-07T10:48:11.129Z" }, + { url = "https://files.pythonhosted.org/packages/e9/1b/c327ef6018e3a9ca0b8e7c5eddeeb331ba8f9b76c24e126d37d0f6d62faf/librt-0.15.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:64b0c8c35aa4c4ed79896359f3e0b285cbe4e610042106500da4811c322cc108", size = 752891, upload-time = "2026-08-07T10:48:12.558Z" }, + { url = "https://files.pythonhosted.org/packages/d7/d1/d5f1ea02c56930087009e39db9b70660a663e76c730b27b925d786718457/librt-0.15.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0da0d94cb802f32a0524653e7201f2cef72d5f700a5407678f5290483d4fcd08", size = 745301, upload-time = "2026-08-07T10:48:14.55Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3c/5f7c585d15ebb2250c73e7c0ee4e9e47be72c65d520c07ddbcdc62037674/librt-0.15.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4a6369168d371207339b1e50d4532b06a7121586141f82599505a3f315751d47", size = 747921, upload-time = "2026-08-07T10:48:16.453Z" }, + { url = "https://files.pythonhosted.org/packages/7f/52/1443a446486eba966bcbca1696b472e4f210320ec42f490a47f48fbf0fdc/librt-0.15.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c434e072557ade9cbc642d052c89d031efe47d5c9614523619d0d74a02378e81", size = 727561, upload-time = "2026-08-07T10:48:18.089Z" }, + { url = "https://files.pythonhosted.org/packages/79/91/2270a9380f11725cf83ce1925a5e32dd1dde2be9bba597f25c10a38644e7/librt-0.15.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7eec6a42018bc1d45763b1c162d3d2bf7c3b9a1b0ed30d3e91dcba390efefcc", size = 774417, upload-time = "2026-08-07T10:48:19.611Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/f4b1548d4f5b99186737fe27aec238e9823e8d5d23bf4df007c030689dc5/librt-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:6912fa5e635d74529ac7cdb1bdf6ca3af4453da8d1edbe0110ee1cb4ad407ebf", size = 104381, upload-time = "2026-08-07T10:48:21.048Z" }, + { url = "https://files.pythonhosted.org/packages/80/b6/134afad262def1de04c0843c376d02135f1168af43f22e09a52bd8394727/librt-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8e11699ed745931c395acd3621b07062e0f840efa6935aad87a64ed0995f0915", size = 127034, upload-time = "2026-08-07T10:48:22.561Z" }, + { url = "https://files.pythonhosted.org/packages/99/5f/1b6846b20572bd699c9e9ec321a5f781845bee477df2aa2a43b28bc40119/librt-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5d2a91724463bfed4f573cd7a9fdc856d2e230d0c0e5a61416a93481dccd8605", size = 110827, upload-time = "2026-08-07T10:48:23.804Z" }, + { url = "https://files.pythonhosted.org/packages/c6/44/4de9f4ddadb009a55c7758eb5736d62534a7daaf27bd71bc50e64b606b06/librt-0.15.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:8443e38dcfcfdbcf5add5118c623efd788d65ac2e25756d6251a54a06a4d0aca", size = 149843, upload-time = "2026-08-07T10:48:25.148Z" }, + { url = "https://files.pythonhosted.org/packages/1f/eb/5d9ab71e30119c44094e0275f38b47dd327aea0f843a080396677029d508/librt-0.15.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6d15a29033c57490cfe2069097c6fc4049e4e65ffbb749be7dc453b7c4c68965", size = 154510, upload-time = "2026-08-07T10:48:26.485Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9c/8505d1b8f5e8c19587bd03f7429993b3e9ce5c06819d856bfb11d919374c/librt-0.15.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d2c05c729b589e734c09578bf5964be48a911765484840d017bbc84f49d4c4ad", size = 497543, upload-time = "2026-08-07T10:48:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/1d/9a/3a8390775cb095765aded027ac9c63e7c8ea74e731498607544c6505de0e/librt-0.15.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:fa60887537e1d0cd2d9982269d33a709bf54b195cd2b9364fc0a758022af5bd9", size = 480452, upload-time = "2026-08-07T10:48:29.531Z" }, + { url = "https://files.pythonhosted.org/packages/e7/40/258a4a7117ee915d66de5cd9b8ade65a440993161107ce3a686f1859955c/librt-0.15.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d8bc24219b24c0af375718942ab75e3544b2763085f40f965be4326734ae8328", size = 507768, upload-time = "2026-08-07T10:48:31.007Z" }, + { url = "https://files.pythonhosted.org/packages/6b/c6/2f4dd296c97a0b85b98894519b279408ec9dd602d4f692b1ea0e25dee670/librt-0.15.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86a21a7bd3fe3a419512ef424cc1c020f6771d0b29cfddff36d1635a855e63f0", size = 525122, upload-time = "2026-08-07T10:48:32.7Z" }, + { url = "https://files.pythonhosted.org/packages/49/dd/29eab42be13b2bf0ea8cb227135a45d44693e30a7e8b92871981ff56b82b/librt-0.15.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dbab647e88d90b3167b91efe7091e248653688ed4337e4f90907a722c7361bb9", size = 520371, upload-time = "2026-08-07T10:48:34.294Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/4bad71adeca8fe208b775c2a35417fa5a2584c8f4791daaf89a89450fea1/librt-0.15.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d8edcf6f550e918dca779c069b9e156385c60b406f99fc7641f32c52f7193659", size = 537258, upload-time = "2026-08-07T10:48:35.88Z" }, + { url = "https://files.pythonhosted.org/packages/4c/63/59dba6143fdcc7240c54458b629f3250000a61b8945890fc9efd451b19c5/librt-0.15.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:8b62076030baa2d8b1501a46bf0e19c27a489aa90671c55665bff7887f7660b0", size = 527432, upload-time = "2026-08-07T10:48:37.466Z" }, + { url = "https://files.pythonhosted.org/packages/ec/21/21a24c6a2327d8362580efebe77286bf47b0f4062ec5ea41766e609d3c7d/librt-0.15.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:d00d20d1818e82a07a0ee0aa89a98b17ed7916b92441090b683719cb20a59b6d", size = 548108, upload-time = "2026-08-07T10:48:39.384Z" }, + { url = "https://files.pythonhosted.org/packages/5a/6d/fc68c89a7971418b41f9a873623ff935cb864097544c6a2f8ce491c8ef5d/librt-0.15.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4e6ee93fc3cf848dcbf0cce2eca73d8e7dcd0cc2b6df3a529d57750b30a4c55c", size = 529681, upload-time = "2026-08-07T10:48:41.392Z" }, + { url = "https://files.pythonhosted.org/packages/65/7e/c2d98766124400d722063a630b0fde38a9fc768705d37eecca15c47dc192/librt-0.15.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:32896a0af72508ea979e0acb4e4c04cbeeae04938167950d535c83c45597167d", size = 567736, upload-time = "2026-08-07T10:48:43.124Z" }, + { url = "https://files.pythonhosted.org/packages/55/6c/f8c34a95e3a515c6e1c192b89511e7253c89a7760c6b500d57ffdb8d2dc8/librt-0.15.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:ec3ba415afaf951f6951b1dd16d3c8e4f540065fc382d7e70b823a79567ca374", size = 81673, upload-time = "2026-08-07T10:48:44.645Z" }, + { url = "https://files.pythonhosted.org/packages/c9/9e/e23fa8e78679ec45728188650b39e8ff476c83b691c96f749217df3b1b7c/librt-0.15.0-cp315-cp315-win32.whl", hash = "sha256:d2813ba2503764f0450680c533d13df7cff9b49df1411062eded5f67db4195b9", size = 100081, upload-time = "2026-08-07T10:48:46.171Z" }, + { url = "https://files.pythonhosted.org/packages/e1/dc/3eb4c5e297343f0620a55532cd7c8d764d3001fa2159212dadf480464827/librt-0.15.0-cp315-cp315-win_amd64.whl", hash = "sha256:b87d67e33afaf265262f2a66db578284b88ee2e6fcd224579cb5c15518677ad8", size = 121228, upload-time = "2026-08-07T10:48:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/97/70/43abce19f04e49762f8ec834c8fafee13cc40fd6b94a72a24e534febfcd0/librt-0.15.0-cp315-cp315-win_arm64.whl", hash = "sha256:713bd7df21170b982e729e46870f31d6b437bd1a9b4648cffb529bd3c2ec5c4b", size = 106487, upload-time = "2026-08-07T10:48:49.095Z" }, + { url = "https://files.pythonhosted.org/packages/de/15/83f2deddb9368b8951ec8c9477269b5b9b8bd9bbf15e57402d0f38817dca/librt-0.15.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:3de789c82752730f94782a5ee518baf9c05edf85733aeaf73bb6e518755cdf54", size = 159448, upload-time = "2026-08-07T10:48:50.649Z" }, + { url = "https://files.pythonhosted.org/packages/06/bf/043097353f9b3c73b583d07f6b8e552795463f4bfc8caf85e42eee50c26a/librt-0.15.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:e0b5deec9a8664eb722c797241970fd4aa1894d25fda36a1ddac0f7407606bd6", size = 161686, upload-time = "2026-08-07T10:48:52.174Z" }, + { url = "https://files.pythonhosted.org/packages/f4/2a/8ae77f9719d42ce71cd708560a3557b38ac3c17a0383e57f87084de45bbe/librt-0.15.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5563302a8359bc2295bb7084d1a8ed1519df96afb30eb2aa4e0bff7b54228988", size = 710668, upload-time = "2026-08-07T10:48:53.782Z" }, + { url = "https://files.pythonhosted.org/packages/61/34/c0436ea134deb9a0d6da80a396a2739a81cb31e0418f7227239e23140898/librt-0.15.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:22d6263b9d39d7bbb286fa791945646e3218f1be2d693e36fb630f1d0e59cd13", size = 679396, upload-time = "2026-08-07T10:48:55.645Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/001e0d99aa9250d5cd5715a9081291a20656083459f9019cda15255329e1/librt-0.15.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39ffd14646190c454f0d86e0d256b33f00a87a26ab410e619773b841d0e41416", size = 704313, upload-time = "2026-08-07T10:48:57.46Z" }, + { url = "https://files.pythonhosted.org/packages/2d/53/b34fa9d0ff00f136f4d58ebb4c411ff634baed1eb412bb602a2bc8dcafcb/librt-0.15.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c47318cd3a61401452de11282242937e3e057c4fd3dbaf601e269d0928a06c0a", size = 729847, upload-time = "2026-08-07T10:48:59.231Z" }, + { url = "https://files.pythonhosted.org/packages/86/ac/fa4d7a424665040e95baf480a6d523446057684b6758624c85338e8a23b2/librt-0.15.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a56a1d4f859a82ca5b99fc4b82c9b027b15e3c455c5cd99e7d0719f27bb20b6c", size = 742736, upload-time = "2026-08-07T10:49:01.151Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/e17a9bb5de6fb8c3186ed1a7d68d21618b027ac2d3633e03d3b6109c67ae/librt-0.15.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:077471b3182db4e17c36ae91555f36a4d2c00080b267f749bcad34a478a9a302", size = 763454, upload-time = "2026-08-07T10:49:03.039Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ec/ecd02cd30935b931b9cdbfed6ab5a099c51b280b4e7baa274da80978ed27/librt-0.15.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:411ca4d1b905b860ceba7570dd6717a71dedaddcc4b0f77ece710aa41ee11f8d", size = 743296, upload-time = "2026-08-07T10:49:04.941Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b5/b3c2b8353ce820a4854f78d19321344242f89fa71c975b71132ba9bf242a/librt-0.15.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:1256589e0b0adb31751d685a68bce29d73407ddf4ef05d4188f49d5dcf9566d9", size = 756217, upload-time = "2026-08-07T10:49:06.825Z" }, + { url = "https://files.pythonhosted.org/packages/3c/52/6cc22542ba59146b05cca2a656f9ff8bb67e38e63d12c3b0cc183d837bf1/librt-0.15.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:f42b74a53e5f26a0ba0007411a7455b66c67ce4022a39cc1f56fc4efd65bcbab", size = 741934, upload-time = "2026-08-07T10:49:08.839Z" }, + { url = "https://files.pythonhosted.org/packages/40/32/a04b72b1aa86e3be23b2ecff8c1aad2dcc955bd3956d6d26e7e34267e57a/librt-0.15.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:291bf73caf78b9e88d6fae9bfd693207ff7d832e2fdbe2cf8e746bc13f5f892b", size = 783763, upload-time = "2026-08-07T10:49:10.661Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f0/89eb11dffbe9279ff37144dec786927314502ae0b114f1449dc78c458aab/librt-0.15.0-cp315-cp315t-win32.whl", hash = "sha256:c16d15ee371643ab48dc8248a3e680ebbeca573a13af2c3dd0c985b142d77162", size = 104313, upload-time = "2026-08-07T10:49:12.305Z" }, + { url = "https://files.pythonhosted.org/packages/6d/4a/1f1978c200f563beda63c36adff2d65bbecb81e365e8e69e572f5f70fbc6/librt-0.15.0-cp315-cp315t-win_amd64.whl", hash = "sha256:dbd605739f228912dc49027cb764456b9757750bdc2b6b7773164db7096c6fd1", size = 126889, upload-time = "2026-08-07T10:49:13.881Z" }, + { url = "https://files.pythonhosted.org/packages/38/a6/800800bfed7b1fb10fc3f3d557785c3854e80d3f7a9800d784b176a1fc2d/librt-0.15.0-cp315-cp315t-win_arm64.whl", hash = "sha256:84d244b00604d17df3fc7736c327892d6bba66181254aa4087be807b6c342bdc", size = 110700, upload-time = "2026-08-07T10:49:15.499Z" }, +] + +[[package]] +name = "mypy" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ast-serialize" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/6a/878cc1097d4035f82bd516658d0c528d2a9955bc7b363afcbd0b07fea11b/mypy-2.3.1.tar.gz", hash = "sha256:47c1b1207258513a9d93495f69c8be9de73916186f0e52703e8c461b7a623419", size = 3992554, upload-time = "2026-08-15T03:03:38.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/da/d6effc4f808a842d91edc22535dc9e799d2ff6e91449168b7f47a0771f54/mypy-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a32bbbb940af990d3be0b8af321c7b6815bb1b3b48142fe7459b9cc5f58959ff", size = 14047547, upload-time = "2026-08-15T03:02:57.707Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e6/478229701dab76f26485fc8ff5d6f241f393da22447400bbc56f6946aebe/mypy-2.3.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff715e45b2231a8e85de1d163d1b42791e4d7aab8f5145f85fee1b710b735aff", size = 14216515, upload-time = "2026-08-15T03:01:26.496Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fe/7c42327a3b21e84681f691982cbfe43f334a3685f3b683b72c376476c4fa/mypy-2.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:858fc57d3d91fa728e33e7ad71def60fc6272694607b306cd3292db53ae39080", size = 15307789, upload-time = "2026-08-15T03:03:31.62Z" }, + { url = "https://files.pythonhosted.org/packages/59/f4/7e597edbe01b5a56fa958ce541302dcaabfed979966f1dffedbea0ea0fc2/mypy-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:851833db876e7b650f93719c74b7879a08e338979c96054fdfc3bfd90a486355", size = 15548831, upload-time = "2026-08-15T03:03:15.55Z" }, + { url = "https://files.pythonhosted.org/packages/a3/52/cb31e084bc0314a1e384bdd677a4b80e55af04ccac077545e2238b9d320a/mypy-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:4c5095a327483591c94e0c8d3ef9e50d4ab1369b541eae007c1f23bc2a41f6bb", size = 11226359, upload-time = "2026-08-15T03:03:29.002Z" }, + { url = "https://files.pythonhosted.org/packages/7a/47/88fcf6217b43fa2da81a8c2611370af18141536a4f0294bbf98b457d456d/mypy-2.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:bbfe022634a2a195406bd469e888d2eaf193b02ba7e607391cd7640374aaae3b", size = 10214707, upload-time = "2026-08-15T03:02:48.807Z" }, + { url = "https://files.pythonhosted.org/packages/de/cf/862010ee800ca9c2bd0c4c0dacf0f092e5411824a09b8f97ad4be8fe250e/mypy-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:114dff494000f18bd10d5d95d84b8567b26da60279ecbe838131841df20e635d", size = 13964542, upload-time = "2026-08-15T03:02:21.43Z" }, + { url = "https://files.pythonhosted.org/packages/75/5a/3f3a2107b41e3e92e617e25daaee121413b91e9784bea733131ed4fecc5d/mypy-2.3.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8637731bb5eee3671eb2c3200827aa3564ed8a9309ecee4d1afe77e6d031bdb", size = 14168922, upload-time = "2026-08-15T03:03:00.351Z" }, + { url = "https://files.pythonhosted.org/packages/8b/41/04dc4fe7e63d7820fa4eff272e95157d30cbea921388f3ab3fe77794cd0b/mypy-2.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c80fbc405ed8020f5ff3802dc18cf060197bcdd3fbdd6a26ef2fd34dfdd5226", size = 15244791, upload-time = "2026-08-15T03:02:31.089Z" }, + { url = "https://files.pythonhosted.org/packages/96/fc/c3053b26b9054949285aa868cb6af8c10e7591541cacd79c5dcc06a1fcf9/mypy-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:84081f538ce27375045c02e3d7f81bd11d853400621ae245d87ce7b6c420ec74", size = 15501627, upload-time = "2026-08-15T03:03:34.128Z" }, + { url = "https://files.pythonhosted.org/packages/70/4e/d77daab008bbc4e5001374d7928f4a260d28f0e6747af444fc4763f7a310/mypy-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:e9144ac16fde007096f9563eb2041b4433c2d705c4218edeb79e7e9d01035ee6", size = 11243961, upload-time = "2026-08-15T03:02:11.952Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f8/7eb68c136e4abd30569fe31ef2bfcb7eceae9952cab80017c04cd09f5d0c/mypy-2.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:77ad9529e67dca28e511f5cd5671436584ce91f6d3bac159a353158187b986ac", size = 10213219, upload-time = "2026-08-15T03:02:26.361Z" }, + { url = "https://files.pythonhosted.org/packages/be/c4/42a49d44aeff804edf1b19acce0b49e8bd1a9c57dee9605dd8d980aa43d7/mypy-2.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:192abaedf75da1bc0b1cef104927e70ec49c1ef0031cc4825c7ee10a438ed24d", size = 13986778, upload-time = "2026-08-15T03:01:33.69Z" }, + { url = "https://files.pythonhosted.org/packages/45/13/9331fd2dfed7194d66c5304072894a8be3e51e9deda6863c1eceaa35a43d/mypy-2.3.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf678dffd16efcda2c15cbd30e9ecc0081388e29ea23687a88e686ed92638dc3", size = 14188467, upload-time = "2026-08-15T03:02:40.554Z" }, + { url = "https://files.pythonhosted.org/packages/78/f7/f4a34edab45667c5465855dc585a20e87978ffa8aee711445b7239d120c6/mypy-2.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e036f06b41630f4c8a1d48f9ac6aa26acc65f8be089973f5519da643318f03f", size = 15225538, upload-time = "2026-08-15T03:03:09.761Z" }, + { url = "https://files.pythonhosted.org/packages/40/05/534b3590757bd05794f73e07f6666c2a77b8597ffed795c94ce570096aa0/mypy-2.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71af9c8a894e862b58e92abb08e53b05a384a1e5e5d6dc7cda59126211a53d82", size = 15480805, upload-time = "2026-08-15T03:01:41.134Z" }, + { url = "https://files.pythonhosted.org/packages/55/da/bdfba852e2562f599624af5bb7d29e36b0b4f526f2b8bac85efe0dd1803d/mypy-2.3.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3c80cd23d85368bdd9f37d5231dfd97d35bcbf5bf41af96ef3a9b078ad1957f9", size = 7761712, upload-time = "2026-08-15T03:02:36.008Z" }, + { url = "https://files.pythonhosted.org/packages/98/31/60fc64a74cdba4f2a5d642d32317993e479163e1ac7d91b695e5d15e2264/mypy-2.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:4956f34d145e145562a0a0bf367f642bbc85c04ec2baf47ae015947c3169a85d", size = 11423968, upload-time = "2026-08-15T03:02:06.931Z" }, + { url = "https://files.pythonhosted.org/packages/a9/23/eb5950b24cd26ba3b78f87707a275568d633c77dae8e61c9661be6055ca6/mypy-2.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:cfb12e360242d23d91f5e978d94f58ea66acf5804c4fb6f2f794a20d4cb1b595", size = 10399323, upload-time = "2026-08-15T03:02:33.671Z" }, + { url = "https://files.pythonhosted.org/packages/82/c7/f80f4e46c0b9a00eb5f78a79d49dda8bdf56a5230f7257fb33e76be04da7/mypy-2.3.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5f1c50bb05b64e2026b52867e8d21106f01313c744a2c4ecc34c90d12e8d6e2", size = 15121308, upload-time = "2026-08-15T03:01:46.053Z" }, + { url = "https://files.pythonhosted.org/packages/5d/74/9b04f17c7074cc5188f02fb63a2ca1d43fedf479e84fe3091c39061a1d7f/mypy-2.3.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:667196b352f4cf304ded4c10f90cfc179263a1acfb3cdcfa984bdfd340d498bc", size = 15536590, upload-time = "2026-08-15T03:01:35.941Z" }, + { url = "https://files.pythonhosted.org/packages/26/04/c837ef6208e567774e2ed1f863f8ba6ec4817b1b6dd426315e5d559b6ec9/mypy-2.3.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9c53e395c12cad2c6d4b67d5da7c6057638a132d85c08b73646b18f802a0045", size = 16791074, upload-time = "2026-08-15T03:01:31.073Z" }, + { url = "https://files.pythonhosted.org/packages/37/68/48730230afa45192d5bd429a6a2ff24a6f8dedda90fdf2b221792b54518f/mypy-2.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:18162b128c3f9c703cd35f5537446900b0d21a2549aa7a95d21380d2ef643fb0", size = 17069183, upload-time = "2026-08-15T03:02:28.566Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ea/ca23fc9c20eeda09a15c9cbcf50015d0e73f409f6ead059e42aa69a608ff/mypy-2.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:30c0477d4aab7b7f39c8397dc877f2c96b9fe5588ec379f372c56eb63d599f63", size = 12154679, upload-time = "2026-08-15T03:02:04.809Z" }, + { url = "https://files.pythonhosted.org/packages/3b/67/8d982126034990869466f73b8db80dcb2234a7ac39b4dad093e047a79835/mypy-2.3.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6941ab3619377bc3f32ca02876b07d27f216f5201604b664d3937ea0fdd23bb4", size = 10969159, upload-time = "2026-08-15T03:02:38.152Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f7/41e7f2d8117fbc7a7587286162ffe2f688984b69c46ed63cf5f2e4fc3bae/mypy-2.3.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6f041a6de52c9217ca125e78ba0a335cb7fd98a1c0580978e49ab2b126f70b57", size = 13990694, upload-time = "2026-08-15T03:03:21.919Z" }, + { url = "https://files.pythonhosted.org/packages/06/85/8f665811a0c8f3bf6fa1d9acd665ec2d97a2bcc453ae68dcd92340941cd6/mypy-2.3.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5159ae60f5dbc3a498af5ba8365505808ac8031bc63f9e00304ad545d40bdd9b", size = 14203518, upload-time = "2026-08-15T03:01:48.455Z" }, + { url = "https://files.pythonhosted.org/packages/2d/82/91b866c8546b120bff83b73a439d90d2d63ef3aff113599e6b8e4d566848/mypy-2.3.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47a8a7a0a7f6f6e63995c0ac36fa0c07b127413fdc81f0439b7f3dccafd33561", size = 15220224, upload-time = "2026-08-15T03:01:23.577Z" }, + { url = "https://files.pythonhosted.org/packages/c8/78/c226c99208ee40de7c768369fa533f933afa003dfdc606ff021450724e91/mypy-2.3.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2329c0501293d4e1f33bc15d04d6304d65a1cdda967ee93a05c1e681a3923133", size = 15501512, upload-time = "2026-08-15T03:02:09.453Z" }, + { url = "https://files.pythonhosted.org/packages/a9/e7/7cfb3f106c393979f4cc37ad6c0586044d50401e3c35b0c003e4f3ba6bc9/mypy-2.3.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:bb26deed807bdb0457cf3e3f1cd7c4a1cf9d66864eaf1b4a61e06805d4c6b1f9", size = 7761913, upload-time = "2026-08-15T03:01:55.65Z" }, + { url = "https://files.pythonhosted.org/packages/99/3c/52affefa273b97939a1f474ae4a349c8718635c15b941112dfab4291b0c1/mypy-2.3.1-cp315-cp315-win_amd64.whl", hash = "sha256:375d7013876a8233b2d05be185bfa09f689696cd999ce8b1cfe6acac5c80e8a3", size = 11422533, upload-time = "2026-08-15T03:03:24.101Z" }, + { url = "https://files.pythonhosted.org/packages/2a/b7/75643e70c72a5b346d8a9b1543c967ea8824df2ee3fb7ccba652c272b7bb/mypy-2.3.1-cp315-cp315-win_arm64.whl", hash = "sha256:586b3612214cceabb3c0f588c97e7d1e535393f06a60e912e994f6b3ace97523", size = 10397931, upload-time = "2026-08-15T03:02:55.265Z" }, + { url = "https://files.pythonhosted.org/packages/10/ce/53be21f2d4adfcd26f63f1184a13ed797015ab463853f117e2e11e4d726f/mypy-2.3.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:ef0c6335cda9d807f8193d8ff6204a72bc909fa9882aacbca14f43cdb7188306", size = 15118669, upload-time = "2026-08-15T03:02:51.479Z" }, + { url = "https://files.pythonhosted.org/packages/62/43/20de757cd42989d291a17fad607742c4c74e875ce5cea00e5a5225020ac1/mypy-2.3.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e598c8c66401d26b150872154a286e6d484cf2789c3bb28a7556806298423021", size = 15545627, upload-time = "2026-08-15T03:03:05.132Z" }, + { url = "https://files.pythonhosted.org/packages/7e/fc/092bdf77ad280eaf501422f0f3b966012b528076cc13e41a774861c907d1/mypy-2.3.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eda22fd4efa9dcd39331d1dede9b5b8b8a7fd69af07592e778433da98610d29e", size = 16764157, upload-time = "2026-08-15T03:02:23.958Z" }, + { url = "https://files.pythonhosted.org/packages/94/5c/c94c4d62d909b07f552d0d9356d7acc943825558e602a64822ffa2231536/mypy-2.3.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:2a0ba2e57847849fb0d1fcdabb32786d223095ed8bc121dfe322bcdb3d9c46bc", size = 17073258, upload-time = "2026-08-15T03:02:14.573Z" }, + { url = "https://files.pythonhosted.org/packages/c0/f7/511a88b89e478053c02d22039bb8f3ce4183efe8fd7a4f0a5910a8bb0a32/mypy-2.3.1-cp315-cp315t-win_amd64.whl", hash = "sha256:3f7e865dd51f235f60a2dbcd8728a1c095f5ca28f095d48a725b84cd935735c4", size = 12135505, upload-time = "2026-08-15T03:02:16.714Z" }, + { url = "https://files.pythonhosted.org/packages/71/bf/02573b56964ecb0f7c644f915f53c325ae15c3faec521c5adf11599a32df/mypy-2.3.1-cp315-cp315t-win_arm64.whl", hash = "sha256:8ad80807dc3ab8ea978b1b2b6e4a657194ace1d4ef03e0e731aff1abd517da29", size = 10962647, upload-time = "2026-08-15T03:01:43.712Z" }, + { url = "https://files.pythonhosted.org/packages/8e/41/9675c7a1e78edecfba0b79e587a52594c56e189368261dc7b3a7fffb9527/mypy-2.3.1-py3-none-any.whl", hash = "sha256:6ed5c7e3419083268e5c9258bd1c1ef91af44a9e89374dbcaf37b775716e72eb", size = 2754338, upload-time = "2026-08-15T03:02:53.4Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "pl-001-python-basics-task" +version = "0.1.0" +source = { virtual = "." } + +[package.dev-dependencies] +dev = [ + { name = "mypy" }, + { name = "ruff" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "mypy", specifier = "==2.3.1" }, + { name = "ruff", specifier = "==0.16.8" }, +] + +[[package]] +name = "ruff" +version = "0.16.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/78/449cb84790bd5cc3823b2652ee405a4558856e5c4195aee3a16bf7b3eb5d/ruff-0.16.8.tar.gz", hash = "sha256:9247bf92b5f04d825c8639a4fe423ec2e4222acd9222e58412b0dab7e442798b", size = 4938814, upload-time = "2026-09-16T15:54:46.688Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/25/6071aabc530e9be7e2c195e8fe3f7aea2735405b6cf447212832d7811831/ruff-0.16.8-py3-none-linux_armv6l.whl", hash = "sha256:6ffbd6d87383c1edf5f6fa890f10200950240d7c1a16052a19a09d3a2307dd38", size = 10048966, upload-time = "2026-09-16T15:53:57.605Z" }, + { url = "https://files.pythonhosted.org/packages/54/98/07f90ecbc74dd5fb5764f11f2bc774d6a7cffef92d2ff5f5b4e9e23c754e/ruff-0.16.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:42ed6b878ed61e3acca92f2730a17acff39286944ea82398544696366a6f925e", size = 10165498, upload-time = "2026-09-16T15:54:01.14Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1f/e6a712e3b47cad4a40600134105ed193cb773f618a42eb7ba323cb812cc0/ruff-0.16.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7ea781c7f2afba8c6a505ea0fb3f994020249e0c450635f5381286fea6b46170", size = 9830004, upload-time = "2026-09-16T15:54:03.998Z" }, + { url = "https://files.pythonhosted.org/packages/23/f2/311a08776d75d81c7676e20b6b020ae63cbe881fcdc7a8dd64e6e18bdd93/ruff-0.16.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8efeae3bbe414a5efefda11a792dfb51ef90ac48d50c4830de2f644caf3e8659", size = 9986558, upload-time = "2026-09-16T15:54:06.804Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ed/37b6cb3d3ba8c73e68ae3eb1d502383beb5aa05a582bb7bb3a922f929f54/ruff-0.16.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a79b795469fef7fc6e908b218eed2eb17332afd85031db6480dc864560e69b2", size = 9877332, upload-time = "2026-09-16T15:54:09.552Z" }, + { url = "https://files.pythonhosted.org/packages/22/cc/40873a8f36ad084cc540d55fcca7077264d5b13b24659e9180c176fb2b08/ruff-0.16.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fdc5563cdc50555e6fba39322850860e9267c1b3d12c26a74729d8604c3c812", size = 10507125, upload-time = "2026-09-16T15:54:12.152Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e4/fc91a642b78ccbab6b9477720f3644ae7a10a9bcce69a934679cd64f62bc/ruff-0.16.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34508983c70665578dab88f5223d8e6228307e1135398ca8bfc8b7e9501e282b", size = 11336694, upload-time = "2026-09-16T15:54:15.489Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3d/bbd2a9a600a4e73dc3e7548a249c8d1671273464b55822c6fae50f602dff/ruff-0.16.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:644bb578569e0ffc575741232bd385dacdd6fbe123f1a729e7a225f54aa3957f", size = 10774448, upload-time = "2026-09-16T15:54:18.16Z" }, + { url = "https://files.pythonhosted.org/packages/1a/41/d83af9879a7b6e8bf5fe16b1da0b134049d2f5d3afac12defb0897cb84bd/ruff-0.16.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15e7d226246961db9235098333caa13063906d3851136b84c2900b82f5daa1df", size = 10323796, upload-time = "2026-09-16T15:54:20.743Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/cefd07bfe914b84943ea769ade8d607bd22750b965d3228eefd7cebd15d0/ruff-0.16.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a2bf6bc3e9ebdd4449abc6f06cf64b98051a2c61cf94d2fe9596518c881f1a1e", size = 10514115, upload-time = "2026-09-16T15:54:23.497Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9d/76a2e26c79a23be6e6e3664c57bec9e9fc8de155cfb9e4b67ea91b64f9d7/ruff-0.16.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ca111ba0849539165e9e59d2b442542f3c1e8060ebbdea82494f1ffbccb1e1f", size = 10072582, upload-time = "2026-09-16T15:54:26.185Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d4/f42edddb39668af1a559ceafa3823aedd65633a48dc9768e775485faa2c1/ruff-0.16.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:359a1e5b495448ee1e91018064382ebc86f90e8aac2fed222c7d0e4e8df85fd2", size = 9879644, upload-time = "2026-09-16T15:54:29.278Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/913e3195d95e0378786c6656945c865f534a3560e29139da4882aff630d1/ruff-0.16.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:59e8f5681349474110b24d62e93cfda6593f5fa3473446ca3705200cac1a08b9", size = 10231569, upload-time = "2026-09-16T15:54:32.036Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c4/8aa6ea0bdcedbd1bf87397e2fc4ed8406448ea5842f8660bc6e5f163039d/ruff-0.16.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:efa3e7a16d1baaa79957888dfdf8be9ef2e44db81cb032af06d76632ab59e773", size = 10663666, upload-time = "2026-09-16T15:54:34.838Z" }, + { url = "https://files.pythonhosted.org/packages/3d/02/7f10ef4700bc223c30a3fdd10631a29830c45524b810a3c7ed947af64591/ruff-0.16.8-py3-none-win32.whl", hash = "sha256:55793ba85c69921e89be061426d91a78652d6e50317c962240922747a4eb713f", size = 10093472, upload-time = "2026-09-16T15:54:37.47Z" }, + { url = "https://files.pythonhosted.org/packages/1e/5d/a509c07d714b6da88f2c518b4637cf6f1d46b074be8f0f1e5fb9ff5126fe/ruff-0.16.8-py3-none-win_amd64.whl", hash = "sha256:a6b85621fd3c81e31fc5f5add09c9c078b430db3595ca632efafdec9e64ebfaa", size = 10586899, upload-time = "2026-09-16T15:54:40.488Z" }, + { url = "https://files.pythonhosted.org/packages/fe/a0/50787329e4f20bf9dc9f6230015d46ec69c51a97ace5bc202dae4755365d/ruff-0.16.8-py3-none-win_arm64.whl", hash = "sha256:d075e820af612102ce217f07cc93e69f9490b10ec13ea85fa87bd03d996cef8a", size = 10386316, upload-time = "2026-09-16T15:54:43.332Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +]