From b9acc76e67605609b57ca5b16c6c2fb3acb683d9 Mon Sep 17 00:00:00 2001 From: EvgrafovMichail Date: Fri, 18 Sep 2026 22:29:25 +0300 Subject: [PATCH 1/4] Add cache python files into .gitignore --- .gitignore | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..e16bbc4c --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Byte-compiled +__pycache__/ +*.py[cod] + +# Virtual environments +.venv/ + +# Linter / type-checker caches +.ruff_cache/ +.mypy_cache/ From 265be37af4df4dfcbed942437462aa2f7ae61cc7 Mon Sep 17 00:00:00 2001 From: EvgrafovMichail Date: Fri, 18 Sep 2026 22:55:02 +0300 Subject: [PATCH 2/4] Add .claude folder into .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index e16bbc4c..8ca254de 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ __pycache__/ # Linter / type-checker caches .ruff_cache/ .mypy_cache/ + +# Claude +.claude From 37545b45144e47653cff446f70cd11340c56a78c Mon Sep 17 00:00:00 2001 From: EvgrafovMichail Date: Fri, 18 Sep 2026 22:55:23 +0300 Subject: [PATCH 3/4] Add part 1 of complex task --- src/part1/TASK.md | 156 +++++++++++++++++++++++++++++++++++++++++ src/part1/__init__.py | 1 + src/part1/constants.py | 38 ++++++++++ src/part1/snowflake.py | 114 ++++++++++++++++++++++++++++++ 4 files changed, 309 insertions(+) create mode 100644 src/part1/TASK.md create mode 100644 src/part1/__init__.py create mode 100644 src/part1/constants.py create mode 100644 src/part1/snowflake.py diff --git a/src/part1/TASK.md b/src/part1/TASK.md new file mode 100644 index 00000000..ef8e0faf --- /dev/null +++ b/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/src/part1/__init__.py b/src/part1/__init__.py new file mode 100644 index 00000000..242b07ce --- /dev/null +++ b/src/part1/__init__.py @@ -0,0 +1 @@ +"""Homework 3: a Twitter Snowflake identifier generator and its bit layout.""" diff --git a/src/part1/constants.py b/src/part1/constants.py new file mode 100644 index 00000000..9abfbba2 --- /dev/null +++ b/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] = 0 + +# Node identifier used when the caller does not supply one. +NODE_ID_DEFAULT: Final[int] = 0 + +# Width of each field, in bits. +TIMESTAMP_BITS: Final[int] = 0 +NODE_ID_BITS: Final[int] = 0 +SEQUENCE_ID_BITS: Final[int] = 0 + +# Largest value each field can hold. +TIMESTAMP_MS_MAX: Final[int] = 0 +NODE_ID_MAX: Final[int] = 0 +SEQUENCE_ID_MAX: Final[int] = 0 + +# Здесь можно добавить собственные вспомогательные константы +# (например, сдвиги полей при сборке идентификатора). diff --git a/src/part1/snowflake.py b/src/part1/snowflake.py new file mode 100644 index 00000000..d3abf050 --- /dev/null +++ b/src/part1/snowflake.py @@ -0,0 +1,114 @@ +"""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 # noqa: F401 + +from .constants import ( # noqa: F401 + EPOCH_MS_DEFAULT, + NODE_ID_DEFAULT, + NODE_ID_MAX, + SEQUENCE_ID_MAX, + 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 0 + + +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 0 + + +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 0 + + +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 0 + + +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: реализуйте функцию + return 0 From 9add4bf82bcb0326c1a13e9dd0192483b81ec737 Mon Sep 17 00:00:00 2001 From: EvgrafovMichail Date: Fri, 18 Sep 2026 22:58:56 +0300 Subject: [PATCH 4/4] Add reference to part 1 of complex task --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 4c82de30..99bda938 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ # Практическое задание по курсу PL-001 В рамках задания мы напишем на Python игрушечный бэкенд для интернет-магазина с локальным хранилищем данных в памяти: научимся создавать, читать, изменять и удалять товары, работать с корзиной и управлять всем этим через простую консоль администратора. Задание выполняется поэтапно, и каждый следующий шаг опирается на предыдущий. + +## Задания + +- [Часть 1. Генератор идентификаторов Twitter Snowflake](src/part1/TASK.md) — нужно написать генератор 64-битных числовых идентификаторов, в которые упакованы метка времени, номер узла и счётчик последовательности. Такие идентификаторы уникальны и примерно упорядочены по времени, а из самого идентификатора можно узнать, когда и на каком узле он был создан.