diff --git a/pyproject.toml b/pyproject.toml index 06fbe913..83913475 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,3 +33,10 @@ avoid-escape = true [tool.ruff.lint.isort] lines-after-imports = 2 split-on-trailing-comma = true + +[tool.mypy] +strict = true +mypy_path = "src" +namespace_packages = true +explicit_package_bases = true + diff --git a/src/part1/__init__.py b/src/part1/__init__.py index 242b07ce..e69de29b 100644 --- a/src/part1/__init__.py +++ b/src/part1/__init__.py @@ -1 +0,0 @@ -"""Homework 3: a Twitter Snowflake identifier generator and its bit layout.""" diff --git a/src/part1/constants.py b/src/part1/constants.py index 9abfbba2..0339d24f 100644 --- a/src/part1/constants.py +++ b/src/part1/constants.py @@ -1,38 +1,13 @@ -"""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. +EPOCH_MS_DEFAULT: Final[int] = 1288834974657 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 - -# Здесь можно добавить собственные вспомогательные константы -# (например, сдвиги полей при сборке идентификатора). +TIMESTAMP_BITS: Final[int] = 41 +NODE_ID_BITS: Final[int] = 10 +SEQUENCE_ID_BITS: Final[int] = 12 +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 +NODE_ID_SHIFT: Final[int] = SEQUENCE_ID_BITS +TIMESTAMP_SHIFT: Final[int] = SEQUENCE_ID_BITS + NODE_ID_BITS diff --git a/src/part1/snowflake.py b/src/part1/snowflake.py index d3abf050..a3f3fe8f 100644 --- a/src/part1/snowflake.py +++ b/src/part1/snowflake.py @@ -1,85 +1,31 @@ -"""Twitter Snowflake identifier generator. +import time -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 +from part1.constants import ( EPOCH_MS_DEFAULT, NODE_ID_DEFAULT, NODE_ID_MAX, + NODE_ID_SHIFT, SEQUENCE_ID_MAX, TIMESTAMP_MS_MAX, + TIMESTAMP_SHIFT, ) 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 + return time.time_ns() // 1000000 - 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 0 + stored_ms = snowflake_id >> TIMESTAMP_SHIFT + return stored_ms + epoch_ms 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 + return (snowflake_id >> NODE_ID_SHIFT) & 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 0 + return snowflake_id & SEQUENCE_ID_MAX def generate_snowflake_id( @@ -87,28 +33,17 @@ def generate_snowflake_id( node_id: int = NODE_ID_DEFAULT, epoch_ms: int = EPOCH_MS_DEFAULT, ) -> int | None: - """Build and return a Snowflake identifier for the current millisecond. + if not (0 <= node_id <= NODE_ID_MAX): + print(f"node_id must be in [0, {NODE_ID_MAX}], got {node_id}") + return None - 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. + if not (0 <= sequence_id <= SEQUENCE_ID_MAX): + print(f"sequence_id must be in [0, {SEQUENCE_ID_MAX}], got {sequence_id}") + return None - 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). + elapsed_ms = read_current_millis(epoch_ms) + if elapsed_ms > TIMESTAMP_MS_MAX: + print(f"timestamp overflows: {elapsed_ms} > {TIMESTAMP_MS_MAX}") + return None - 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 + return (elapsed_ms << TIMESTAMP_SHIFT) | (node_id << NODE_ID_SHIFT) | sequence_id