diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8be8894..89d7d3a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -49,6 +49,33 @@ jobs: with: token: ${{secrets.GITHUB_TOKEN}} deny: warnings + stubtest: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + components: clippy + override: true + - uses: actions/setup-python@v6 + with: + python-version: 3.x + - name: Install uv + uses: astral-sh/setup-uv@v7 + - id: setup-venv + name: Setup virtualenv + run: python -m venv .venv + - name: Build lib + uses: PyO3/maturin-action@v1 + with: + command: dev --uv + sccache: true + - name: Run stubtest + run: | + set -e + source .venv/bin/activate + stubtest --ignore-disjoint-bases natsrpy pytest: runs-on: ubuntu-latest steps: diff --git a/.gitignore b/.gitignore index c8f0442..4cb54d1 100644 --- a/.gitignore +++ b/.gitignore @@ -70,3 +70,5 @@ docs/_build/ # Pyenv .python-version +.venv/ +target/ diff --git a/Cargo.toml b/Cargo.toml index 2ef53aa..c637053 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ async-nats = "0.46" bytes = "1.11.1" futures-util = "0.3.32" log = "0.4.29" -pyo3 = { version = "0.28", features = ["abi3"] } +pyo3 = { version = "0.28", features = ["abi3", "experimental-inspect"] } pyo3-async-runtimes = { version = "0.28", features = ["tokio-runtime"] } pyo3-log = "0.13.3" thiserror = "2.0.18" diff --git a/pyproject.toml b/pyproject.toml index 72cc040..7065e87 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ email = "s3riussan@gmail.com" [dependency-groups] dev = [ "anyio>=4,<5", + "mypy>=1.19.1,<2", "pytest>=9,<10", "pytest-xdist>=3,<4", ] @@ -110,6 +111,8 @@ ignore = [ "SLF001", # Private member accessed "S311", # Standard pseudo-random generators are not suitable for security/cryptographic purposes "D101", # Missing docstring in public class + "PLR2004", # Magic value used in comparison + "B017", # Do not assert blind exception ] [tool.ruff.lint.pydocstyle] diff --git a/python/natsrpy/_natsrpy_rs/__init__.pyi b/python/natsrpy/_natsrpy_rs/__init__.pyi index ec386be..7539744 100644 --- a/python/natsrpy/_natsrpy_rs/__init__.pyi +++ b/python/natsrpy/_natsrpy_rs/__init__.pyi @@ -1,35 +1,66 @@ from collections.abc import Awaitable, Callable from datetime import timedelta -from typing import Any, overload +from typing import Any, final, overload -from natsrpy._natsrpy_rs.js import JetStream -from natsrpy._natsrpy_rs.message import Message +from typing_extensions import Self +from . import js + +@final +class Message: + """ + Simple NATS message. + + Attributes: + subject: subject where message was published + reply: subject where reply should be sent, if any + payload: message payload + headers: dictionary of message headers, + every value can be a simple value or a list. + status: status is used for reply messages to indicate the status of the reply. + It is None for regular messages. + description: message description is used for reply messages to + provide additional information about the status. + length: a length of the message payload in bytes. + """ + + subject: str + reply: str | None + payload: bytes + headers: dict[str, Any] + status: int | None + description: str | None + length: int + +@final class IteratorSubscription: def __aiter__(self) -> IteratorSubscription: ... async def __anext__(self) -> Message: ... + async def next(self, timeout: float | timedelta | None = None) -> Message: ... async def unsubscribe(self, limit: int | None = None) -> None: ... async def drain(self) -> None: ... +@final class CallbackSubscription: async def unsubscribe(self, limit: int | None = None) -> None: ... async def drain(self) -> None: ... +@final class Nats: - def __init__( - self, + def __new__( + cls, /, - addrs: list[str] = ["nats://localhost:4222"], + addrs: list[str] | None = None, user_and_pass: tuple[str, str] | None = None, nkey: str | None = None, token: str | None = None, custom_inbox_prefix: str | None = None, - read_buffer_capacity: int = 65535, - sender_capacity: int = 128, + read_buffer_capacity: int = ..., # 65535 bytes + sender_capacity: int = ..., # 128 bytes max_reconnects: int | None = None, - connection_timeout: float | timedelta = ..., - request_timeout: float | timedelta = ..., - ) -> None: ... + connection_timeout: float | timedelta = ..., # 5 sec + request_timeout: float | timedelta = ..., # 10 sec + ) -> Self: ... async def startup(self) -> None: ... async def shutdown(self) -> None: ... async def publish( @@ -41,7 +72,15 @@ class Nats: reply: str | None = None, err_on_disconnect: bool = False, ) -> None: ... - async def request(self, subject: str, payload: bytes) -> None: ... + async def request( + self, + subject: str, + payload: bytes | str | bytearray | memoryview, + *, + headers: dict[str, Any] | None = None, + inbox: str | None = None, + timeout: float | timedelta | None = None, + ) -> None: ... async def drain(self) -> None: ... async def flush(self) -> None: ... @overload @@ -56,6 +95,16 @@ class Nats: subject: str, callback: None = None, ) -> IteratorSubscription: ... - async def jetstream(self) -> JetStream: ... + async def jetstream( + self, + *, + domain: str | None = None, + api_prefix: str | None = None, + timeout: timedelta | None = None, + ack_timeout: timedelta | None = None, + concurrency_limit: int | None = None, + max_ack_inflight: int | None = None, + backpressure_on_inflight: bool | None = None, + ) -> js.JetStream: ... -__all__ = ["CallbackSubscription", "IteratorSubscription", "Message", "Nats"] +__all__ = ["CallbackSubscription", "IteratorSubscription", "Message", "Nats", "js"] diff --git a/python/natsrpy/_natsrpy_rs/js/__init__.pyi b/python/natsrpy/_natsrpy_rs/js/__init__.pyi index f09bb44..58c1250 100644 --- a/python/natsrpy/_natsrpy_rs/js/__init__.pyi +++ b/python/natsrpy/_natsrpy_rs/js/__init__.pyi @@ -1,18 +1,60 @@ from datetime import datetime, timedelta -from typing import Any +from typing import Any, Literal, final, overload +from . import consumers, kv, managers, object_store, stream from .managers import KVManager, ObjectStoreManager, StreamsManager +__all__ = [ + "JetStream", + "JetStreamMessage", + "Publication", + "consumers", + "kv", + "managers", + "object_store", + "stream", +] + +@final +class Publication: + stream: str + sequence: int + domain: str + duplicate: bool + value: str | None + +@final class JetStream: + @overload async def publish( self, subject: str, payload: str | bytes | bytearray | memoryview, *, headers: dict[str, str] | None = None, - reply: str | None = None, err_on_disconnect: bool = False, + wait: Literal[True], + ) -> Publication: ... + @overload + async def publish( + self, + subject: str, + payload: str | bytes | bytearray | memoryview, + *, + headers: dict[str, str] | None = None, + err_on_disconnect: bool = False, + wait: Literal[False] = False, ) -> None: ... + @overload + async def publish( + self, + subject: str, + payload: str | bytes | bytearray | memoryview, + *, + headers: dict[str, str] | None = None, + err_on_disconnect: bool = False, + wait: bool = False, + ) -> Publication | None: ... @property def kv(self) -> KVManager: ... @property @@ -20,6 +62,7 @@ class JetStream: @property def object_store(self) -> ObjectStoreManager: ... +@final class JetStreamMessage: @property def subject(self) -> str: ... diff --git a/python/natsrpy/_natsrpy_rs/js/consumers.pyi b/python/natsrpy/_natsrpy_rs/js/consumers.pyi index 2a96d23..61694eb 100644 --- a/python/natsrpy/_natsrpy_rs/js/consumers.pyi +++ b/python/natsrpy/_natsrpy_rs/js/consumers.pyi @@ -1,7 +1,22 @@ from datetime import timedelta +from typing import final from natsrpy._natsrpy_rs.js import JetStreamMessage +from typing_extensions import Self +__all__ = [ + "AckPolicy", + "DeliverPolicy", + "MessagesIterator", + "PriorityPolicy", + "PullConsumer", + "PullConsumerConfig", + "PushConsumer", + "PushConsumerConfig", + "ReplayPolicy", +] + +@final class DeliverPolicy: ALL: DeliverPolicy LAST: DeliverPolicy @@ -10,21 +25,25 @@ class DeliverPolicy: BY_START_TIME: DeliverPolicy LAST_PER_SUBJECT: DeliverPolicy +@final class AckPolicy: EXPLICIT: AckPolicy NONE: AckPolicy ALL: AckPolicy +@final class ReplayPolicy: INSTANT: ReplayPolicy ORIGINAL: ReplayPolicy +@final class PriorityPolicy: NONE: PriorityPolicy OVERFLOW: PriorityPolicy PINNED_CLIENT: PriorityPolicy PRIORITIZED: PriorityPolicy +@final class PullConsumerConfig: name: str | None durable_name: str | None @@ -55,8 +74,8 @@ class PullConsumerConfig: priority_groups: list[str] pause_until: int | None - def __init__( - self, + def __new__( + cls, name: str | None = None, durable_name: str | None = None, description: str | None = None, @@ -85,8 +104,9 @@ class PullConsumerConfig: priority_policy: PriorityPolicy | None = None, priority_groups: list[str] | None = None, pause_until: int | None = None, - ) -> None: ... + ) -> Self: ... +@final class PushConsumerConfig: deliver_subject: str name: str | None @@ -116,8 +136,8 @@ class PushConsumerConfig: inactive_threshold: timedelta pause_until: int | None - def __init__( - self, + def __new__( + cls, deliver_subject: str, name: str | None = None, durable_name: str | None = None, @@ -145,8 +165,9 @@ class PushConsumerConfig: backoff: list[timedelta] | None = None, inactive_threshold: timedelta | None = None, pause_until: int | None = None, - ) -> None: ... + ) -> Self: ... +@final class MessagesIterator: def __aiter__(self) -> MessagesIterator: ... async def __anext__(self) -> JetStreamMessage: ... @@ -155,9 +176,11 @@ class MessagesIterator: timeout: float | timedelta | None = None, ) -> JetStreamMessage: ... +@final class PushConsumer: async def messages(self) -> MessagesIterator: ... +@final class PullConsumer: async def fetch( self, diff --git a/python/natsrpy/_natsrpy_rs/js/kv.pyi b/python/natsrpy/_natsrpy_rs/js/kv.pyi index 94baf4e..f303a61 100644 --- a/python/natsrpy/_natsrpy_rs/js/kv.pyi +++ b/python/natsrpy/_natsrpy_rs/js/kv.pyi @@ -1,5 +1,14 @@ +from typing import final + from natsrpy._natsrpy_rs.js.stream import Placement, Republish, Source, StorageType +from typing_extensions import Self + +__all__ = [ + "KVConfig", + "KeyValue", +] +@final class KVConfig: """ KV bucket config. @@ -23,8 +32,8 @@ class KVConfig: placement: Placement | None limit_markers: float | None - def __init__( - self, + def __new__( + cls, bucket: str, description: str | None = None, max_value_size: int | None = None, @@ -40,8 +49,9 @@ class KVConfig: compression: bool | None = None, placement: Placement | None = None, limit_markers: float | None = None, - ) -> None: ... + ) -> Self: ... +@final class KeyValue: @property def stream_name(self) -> str: ... diff --git a/python/natsrpy/_natsrpy_rs/js/managers.pyi b/python/natsrpy/_natsrpy_rs/js/managers.pyi index 07a595b..a43d212 100644 --- a/python/natsrpy/_natsrpy_rs/js/managers.pyi +++ b/python/natsrpy/_natsrpy_rs/js/managers.pyi @@ -1,5 +1,5 @@ from datetime import timedelta -from typing import overload +from typing import final, overload from .consumers import ( PullConsumer, @@ -11,20 +11,30 @@ from .kv import KeyValue, KVConfig from .object_store import ObjectStore, ObjectStoreConfig from .stream import Stream, StreamConfig +__all__ = [ + "ConsumersManager", + "KVManager", + "ObjectStoreManager", + "StreamsManager", +] + +@final class StreamsManager: async def create(self, config: StreamConfig) -> Stream: ... async def create_or_update(self, config: StreamConfig) -> Stream: ... - async def get(self, bucket: str) -> Stream: ... - async def delete(self, bucket: str) -> None: ... + async def get(self, name: str) -> Stream: ... + async def delete(self, name: str) -> bool: ... async def update(self, config: StreamConfig) -> Stream: ... +@final class KVManager: async def create(self, config: KVConfig) -> KeyValue: ... async def create_or_update(self, config: KVConfig) -> KeyValue: ... async def get(self, bucket: str) -> KeyValue: ... - async def delete(self, bucket: str) -> None: ... + async def delete(self, bucket: str) -> bool: ... async def update(self, config: KVConfig) -> KeyValue: ... +@final class ConsumersManager: @overload async def create(self, config: PullConsumerConfig) -> PullConsumer: ... @@ -40,6 +50,7 @@ class ConsumersManager: async def pause(self, name: str, delay: float | timedelta) -> bool: ... async def resume(self, name: str) -> bool: ... +@final class ObjectStoreManager: async def create(self, config: ObjectStoreConfig) -> ObjectStore: ... async def get(self, bucket: str) -> ObjectStore: ... diff --git a/python/natsrpy/_natsrpy_rs/js/object_store.pyi b/python/natsrpy/_natsrpy_rs/js/object_store.pyi index ac596de..3d2eb1b 100644 --- a/python/natsrpy/_natsrpy_rs/js/object_store.pyi +++ b/python/natsrpy/_natsrpy_rs/js/object_store.pyi @@ -1,9 +1,16 @@ from datetime import timedelta +from typing import final -from typing_extensions import Writer +from typing_extensions import Self, Writer from .stream import Placement, StorageType +__all__ = [ + "ObjectStore", + "ObjectStoreConfig", +] + +@final class ObjectStoreConfig: bucket: str description: str | None @@ -14,8 +21,8 @@ class ObjectStoreConfig: compression: bool placement: Placement | None - def __init__( - self, + def __new__( + cls, bucket: str, description: str | None = None, max_age: float | timedelta | None = None, @@ -24,20 +31,21 @@ class ObjectStoreConfig: num_replicas: int | None = None, compression: bool | None = None, placement: Placement | None = None, - ) -> None: ... + ) -> Self: ... +@final class ObjectStore: async def get( self, name: str, writer: Writer[bytes], - chunk_size: int | None = 24576, # 24MB + chunk_size: int | None = ..., # 24MB ) -> None: ... async def put( self, name: str, value: bytes | str, - chunk_size: int = 24576, # 24MB + chunk_size: int = ..., # 24MB description: str | None = None, headers: dict[str, str | list[str]] | None = None, metadata: dict[str, str] | None = None, diff --git a/python/natsrpy/_natsrpy_rs/js/stream.pyi b/python/natsrpy/_natsrpy_rs/js/stream.pyi index bef603d..b2cb633 100644 --- a/python/natsrpy/_natsrpy_rs/js/stream.pyi +++ b/python/natsrpy/_natsrpy_rs/js/stream.pyi @@ -1,55 +1,80 @@ from datetime import datetime, timedelta -from typing import Any +from typing import Any, final + +from typing_extensions import Self from .managers import ConsumersManager +__all__ = [ + "ClusterInfo", + "Compression", + "ConsumerLimits", + "DiscardPolicy", + "External", + "PeerInfo", + "PersistenceMode", + "Placement", + "Republish", + "RetentionPolicy", + "Source", + "SourceInfo", + "StorageType", + "Stream", + "StreamConfig", + "StreamInfo", + "StreamMessage", + "StreamState", + "SubjectTransform", +] + +@final class StorageType: FILE: StorageType MEMORY: StorageType +@final class DiscardPolicy: OLD: DiscardPolicy NEW: DiscardPolicy +@final class RetentionPolicy: LIMITS: RetentionPolicy INTEREST: RetentionPolicy WORKQUEUE: RetentionPolicy +@final class Compression: S2: Compression NONE: Compression +@final class PersistenceMode: Default: PersistenceMode Async: PersistenceMode +@final class ConsumerLimits: inactive_threshold: timedelta max_ack_pending: int - def __init__(self, inactive_threshold: timedelta, max_ack_pending: int) -> None: ... + def __new__(cls, inactive_threshold: timedelta, max_ack_pending: int) -> Self: ... +@final class External: api_prefix: str delivery_prefix: str | None - def __init__( - self, - api_prefix: str, - delivery_prefix: str | None = None, - ) -> None: ... + def __new__(cls, api_prefix: str, delivery_prefix: str | None = None) -> Self: ... +@final class SubjectTransform: source: str destination: str - def __init__( - self, - source: str, - destination: str, - ) -> None: ... + def __new__(cls, source: str, destination: str) -> Self: ... +@final class Source: name: str filter_subject: str | None = None @@ -59,8 +84,8 @@ class Source: domain: str | None = None subject_transforms: SubjectTransform | None = None - def __init__( - self, + def __new__( + cls, name: str, filter_subject: str | None = None, external: External | None = None, @@ -68,30 +93,28 @@ class Source: start_time: int | None = None, domain: str | None = None, subject_transforms: SubjectTransform | None = None, - ) -> None: ... + ) -> Self: ... +@final class Placement: cluster: str | None tags: list[str] | None - def __init__( - self, + def __new__( + cls, cluster: str | None = None, tags: list[str] | None = None, - ) -> None: ... + ) -> Self: ... +@final class Republish: source: str destination: str headers_only: bool - def __init__( - self, - source: str, - destination: str, - headers_only: bool, - ) -> None: ... + def __new__(cls, source: str, destination: str, headers_only: bool) -> Self: ... +@final class StreamConfig: name: str subjects: list[str] @@ -133,8 +156,8 @@ class StreamConfig: allow_message_schedules: bool | None allow_message_counter: bool | None - def __init__( - self, + def __new__( + cls, name: str, subjects: list[str], max_bytes: int | None = None, @@ -174,8 +197,9 @@ class StreamConfig: allow_atomic_publish: bool | None = None, allow_message_schedules: bool | None = None, allow_message_counter: bool | None = None, - ) -> None: ... + ) -> Self: ... +@final class StreamMessage: subject: str sequence: int @@ -183,6 +207,7 @@ class StreamMessage: payload: bytes time: datetime +@final class StreamState: messages: int bytes: int @@ -195,6 +220,7 @@ class StreamState: deleted_count: int | None deleted: list[int] | None +@final class SourceInfo: name: str lag: int @@ -203,6 +229,7 @@ class SourceInfo: subject_transform_dest: str | None subject_transforms: list[SubjectTransform] +@final class PeerInfo: name: str current: bool @@ -210,6 +237,7 @@ class PeerInfo: offline: bool lag: int | None +@final class ClusterInfo: name: str | None raft_group: str | None @@ -219,6 +247,7 @@ class ClusterInfo: traffic_account: str | None replicas: list[PeerInfo] +@final class StreamInfo: config: StreamConfig created: float @@ -227,8 +256,13 @@ class StreamInfo: mirror: SourceInfo | None sources: list[SourceInfo] +@final class Stream: - async def direct_get(self, sequence: int) -> StreamMessage: + async def direct_get( + self, + sequence: int, + timeout: float | datetime | None = None, + ) -> StreamMessage: """ Get direct message from the stream. @@ -236,7 +270,7 @@ class Stream: :return: Message. """ - async def get_info(self) -> StreamInfo: + async def get_info(self, timeout: float | datetime | None = None) -> StreamInfo: """ Get information about the stream. @@ -248,6 +282,7 @@ class Stream: filter: str | None = None, sequence: int | None = None, keep: int | None = None, + timeout: float | datetime | None = None, ) -> int: """ Purge current stream. diff --git a/python/natsrpy/_natsrpy_rs/message.pyi b/python/natsrpy/_natsrpy_rs/message.pyi deleted file mode 100644 index a2d2a9f..0000000 --- a/python/natsrpy/_natsrpy_rs/message.pyi +++ /dev/null @@ -1,26 +0,0 @@ -from typing import Any - -class Message: - """ - Simple NATS message. - - Attributes: - subject: subject where message was published - reply: subject where reply should be sent, if any - payload: message payload - headers: dictionary of message headers, - every value can be a simple value or a list. - status: status is used for reply messages to indicate the status of the reply. - It is None for regular messages. - description: message description is used for reply messages to - provide additional information about the status. - length: a length of the message payload in bytes. - """ - - subject: str - reply: str | None - payload: bytes - headers: dict[str, Any] - status: int | None - description: str | None - length: int diff --git a/python/tests/conftest.py b/python/tests/conftest.py index 2daa3d5..852d413 100644 --- a/python/tests/conftest.py +++ b/python/tests/conftest.py @@ -3,6 +3,7 @@ import pytest from natsrpy import Nats +from natsrpy.js import JetStream @pytest.fixture(scope="session") @@ -29,3 +30,8 @@ async def nats(nats_url: str) -> AsyncGenerator[Nats, None]: yield nats await nats.shutdown() + + +@pytest.fixture(scope="session") +async def js(nats: Nats) -> JetStream: + return await nats.jetstream() diff --git a/python/tests/test_configs.py b/python/tests/test_configs.py new file mode 100644 index 0000000..9422260 --- /dev/null +++ b/python/tests/test_configs.py @@ -0,0 +1,162 @@ +from natsrpy.js import ( + AckPolicy, + DeliverPolicy, + DiscardPolicy, + KVConfig, + ObjectStoreConfig, + PullConsumerConfig, + PushConsumerConfig, + ReplayPolicy, + Republish, + RetentionPolicy, + StorageType, + StreamConfig, +) + + +async def test_stream_config_defaults() -> None: + config = StreamConfig(name="test", subjects=["test.>"]) + assert config.name == "test" + assert config.subjects == ["test.>"] + + +async def test_stream_config_setters() -> None: + config = StreamConfig(name="test", subjects=["test.>"]) + config.name = "new-name" + assert config.name == "new-name" + config.subjects = ["new.>"] + assert config.subjects == ["new.>"] + config.description = "a description" + assert config.description == "a description" + + +async def test_stream_config_all_options() -> None: + config = StreamConfig( + name="full-test", + subjects=["full.>"], + max_bytes=1024, + max_messages=100, + max_messages_per_subject=10, + discard=DiscardPolicy.NEW, + retention=RetentionPolicy.WORKQUEUE, + max_consumers=5, + storage=StorageType.MEMORY, + num_replicas=1, + no_ack=False, + description="full config", + allow_rollup=False, + deny_delete=False, + deny_purge=False, + allow_direct=True, + ) + assert config.name == "full-test" + assert config.subjects == ["full.>"] + assert config.max_bytes == 1024 + assert config.max_messages == 100 + assert config.max_messages_per_subject == 10 + assert config.discard == DiscardPolicy.NEW + assert config.retention == RetentionPolicy.WORKQUEUE + assert config.max_consumers == 5 + assert config.storage == StorageType.MEMORY + assert config.num_replicas == 1 + assert config.description == "full config" + assert config.allow_direct is True + + +async def test_pull_consumer_config_defaults() -> None: + config = PullConsumerConfig() + assert config.name is None + assert config.durable_name is None + assert config.description is None + + +async def test_pull_consumer_config_setters() -> None: + config = PullConsumerConfig(name="test-consumer") + config.name = "updated-name" + assert config.name == "updated-name" + config.description = "updated description" + assert config.description == "updated description" + + +async def test_pull_consumer_config_policies() -> None: + config = PullConsumerConfig( + ack_policy=AckPolicy.ALL, + deliver_policy=DeliverPolicy.LAST, + replay_policy=ReplayPolicy.ORIGINAL, + ) + assert config.ack_policy == AckPolicy.ALL + assert config.deliver_policy == DeliverPolicy.LAST + assert config.replay_policy == ReplayPolicy.ORIGINAL + + +async def test_push_consumer_config_defaults() -> None: + config = PushConsumerConfig(deliver_subject="test.subject") + assert config.deliver_subject == "test.subject" + assert config.name is None + + +async def test_push_consumer_config_setters() -> None: + config = PushConsumerConfig(deliver_subject="test.subject") + config.deliver_subject = "new.subject" + assert config.deliver_subject == "new.subject" + config.name = "push-consumer" + assert config.name == "push-consumer" + + +async def test_kv_config_defaults() -> None: + config = KVConfig(bucket="test-bucket") + assert config.bucket == "test-bucket" + assert config.description is None + + +async def test_kv_config_setters() -> None: + config = KVConfig(bucket="test-bucket") + config.bucket = "new-bucket" + assert config.bucket == "new-bucket" + config.description = "test desc" + assert config.description == "test desc" + config.history = 5 + assert config.history == 5 + + +async def test_kv_config_all_options() -> None: + config = KVConfig( + bucket="full-kv", + description="full kv config", + history=10, + storage=StorageType.MEMORY, + num_replicas=1, + ) + assert config.bucket == "full-kv" + assert config.description == "full kv config" + assert config.history == 10 + assert config.storage == StorageType.MEMORY + assert config.num_replicas == 1 + + +async def test_object_store_config_defaults() -> None: + config = ObjectStoreConfig(bucket="test-bucket") + assert config.bucket == "test-bucket" + assert config.description is None + + +async def test_object_store_config_setters() -> None: + config = ObjectStoreConfig(bucket="test-bucket") + config.bucket = "new-bucket" + assert config.bucket == "new-bucket" + config.description = "test desc" + assert config.description == "test desc" + + +async def test_republish_config() -> None: + r = Republish(source="src.>", destination="dest.>", headers_only=False) + assert r.source == "src.>" + assert r.destination == "dest.>" + assert r.headers_only is False + + r.source = "new.src.>" + assert r.source == "new.src.>" + r.destination = "new.dest.>" + assert r.destination == "new.dest.>" + r.headers_only = True + assert r.headers_only is True diff --git a/python/tests/test_consumers.py b/python/tests/test_consumers.py new file mode 100644 index 0000000..3d6d78f --- /dev/null +++ b/python/tests/test_consumers.py @@ -0,0 +1,304 @@ +import uuid + +from natsrpy.js import ( + AckPolicy, + DeliverPolicy, + JetStream, + PullConsumer, + PullConsumerConfig, + PushConsumer, + PushConsumerConfig, + ReplayPolicy, + StreamConfig, +) + + +async def test_pull_consumer_create(js: JetStream) -> None: + stream_name = f"test-pcreate-{uuid.uuid4()}" + config = StreamConfig(name=stream_name, subjects=[f"{stream_name}.>"]) + stream = await js.streams.create(config) + try: + consumer_config = PullConsumerConfig( + name=f"consumer-{uuid.uuid4()}", + ) + consumer = await stream.consumers.create(consumer_config) + assert isinstance(consumer, PullConsumer) + finally: + await js.streams.delete(stream_name) + + +async def test_pull_consumer_fetch_with_ack(js: JetStream) -> None: + stream_name = f"test-pack-{uuid.uuid4()}" + subj = f"{stream_name}.data" + config = StreamConfig(name=stream_name, subjects=[f"{stream_name}.>"]) + stream = await js.streams.create(config) + try: + await js.publish(subj, b"ack-msg", wait=True) + + consumer_config = PullConsumerConfig( + name=f"consumer-{uuid.uuid4()}", + ack_policy=AckPolicy.EXPLICIT, + ) + consumer = await stream.consumers.create(consumer_config) + messages = await consumer.fetch(max_messages=1, timeout=5.0) + assert len(messages) == 1 + await messages[0].ack() + finally: + await js.streams.delete(stream_name) + + +async def test_pull_consumer_nack(js: JetStream) -> None: + stream_name = f"test-pnack-{uuid.uuid4()}" + subj = f"{stream_name}.data" + config = StreamConfig(name=stream_name, subjects=[f"{stream_name}.>"]) + stream = await js.streams.create(config) + try: + await js.publish(subj, b"nack-msg", wait=True) + + consumer_config = PullConsumerConfig( + name=f"consumer-{uuid.uuid4()}", + ack_policy=AckPolicy.EXPLICIT, + ) + consumer = await stream.consumers.create(consumer_config) + messages = await consumer.fetch(max_messages=1, timeout=5.0) + assert len(messages) == 1 + await messages[0].nack() + finally: + await js.streams.delete(stream_name) + + +async def test_pull_consumer_term(js: JetStream) -> None: + stream_name = f"test-pterm-{uuid.uuid4()}" + subj = f"{stream_name}.data" + config = StreamConfig(name=stream_name, subjects=[f"{stream_name}.>"]) + stream = await js.streams.create(config) + try: + await js.publish(subj, b"term-msg", wait=True) + + consumer_config = PullConsumerConfig( + name=f"consumer-{uuid.uuid4()}", + ack_policy=AckPolicy.EXPLICIT, + ) + consumer = await stream.consumers.create(consumer_config) + messages = await consumer.fetch(max_messages=1, timeout=5.0) + assert len(messages) == 1 + await messages[0].term() + finally: + await js.streams.delete(stream_name) + + +async def test_pull_consumer_progress(js: JetStream) -> None: + stream_name = f"test-pprog-{uuid.uuid4()}" + subj = f"{stream_name}.data" + config = StreamConfig(name=stream_name, subjects=[f"{stream_name}.>"]) + stream = await js.streams.create(config) + try: + await js.publish(subj, b"progress-msg", wait=True) + + consumer_config = PullConsumerConfig( + name=f"consumer-{uuid.uuid4()}", + ack_policy=AckPolicy.EXPLICIT, + ) + consumer = await stream.consumers.create(consumer_config) + messages = await consumer.fetch(max_messages=1, timeout=5.0) + assert len(messages) == 1 + await messages[0].progress() + await messages[0].ack() + finally: + await js.streams.delete(stream_name) + + +async def test_pull_consumer_message_properties(js: JetStream) -> None: + stream_name = f"test-pmsgprop-{uuid.uuid4()}" + subj = f"{stream_name}.data" + config = StreamConfig(name=stream_name, subjects=[f"{stream_name}.>"]) + stream = await js.streams.create(config) + try: + await js.publish(subj, b"prop-msg", wait=True) + + consumer_name = f"consumer-{uuid.uuid4()}" + consumer_config = PullConsumerConfig(name=consumer_name) + consumer = await stream.consumers.create(consumer_config) + messages = await consumer.fetch(max_messages=1, timeout=5.0) + assert len(messages) == 1 + msg = messages[0] + assert msg.subject == subj + assert msg.payload == b"prop-msg" + assert msg.stream == stream_name + assert msg.consumer == consumer_name + assert msg.stream_sequence == 1 + assert msg.consumer_sequence == 1 + assert msg.delivered >= 1 + assert msg.pending >= 0 + assert msg.published is not None + r = repr(msg) + assert isinstance(r, str) + finally: + await js.streams.delete(stream_name) + + +async def test_pull_consumer_with_filter_subject(js: JetStream) -> None: + stream_name = f"test-pfilter-{uuid.uuid4()}" + config = StreamConfig(name=stream_name, subjects=[f"{stream_name}.>"]) + stream = await js.streams.create(config) + try: + await js.publish(f"{stream_name}.a", b"msg-a", wait=True) + await js.publish(f"{stream_name}.b", b"msg-b", wait=True) + + consumer_config = PullConsumerConfig( + name=f"consumer-{uuid.uuid4()}", + filter_subject=f"{stream_name}.a", + ) + consumer = await stream.consumers.create(consumer_config) + messages = await consumer.fetch(max_messages=1, timeout=5.0) + assert len(messages) == 1 + assert messages[0].payload == b"msg-a" + await messages[0].ack() + finally: + await js.streams.delete(stream_name) + + +async def test_pull_consumer_deliver_policy(js: JetStream) -> None: + stream_name = f"test-pdeliver-{uuid.uuid4()}" + subj = f"{stream_name}.data" + config = StreamConfig(name=stream_name, subjects=[f"{stream_name}.>"]) + stream = await js.streams.create(config) + try: + await js.publish(subj, b"old-msg", wait=True) + await js.publish(subj, b"new-msg", wait=True) + + consumer_config = PullConsumerConfig( + name=f"consumer-{uuid.uuid4()}", + deliver_policy=DeliverPolicy.LAST, + ) + consumer = await stream.consumers.create(consumer_config) + messages = await consumer.fetch(max_messages=1, timeout=5.0) + assert len(messages) == 1 + assert messages[0].payload == b"new-msg" + finally: + await js.streams.delete(stream_name) + + +async def test_pull_consumer_replay_policy(js: JetStream) -> None: + stream_name = f"test-preplay-{uuid.uuid4()}" + config = StreamConfig(name=stream_name, subjects=[f"{stream_name}.>"]) + stream = await js.streams.create(config) + try: + consumer_config = PullConsumerConfig( + name=f"consumer-{uuid.uuid4()}", + replay_policy=ReplayPolicy.INSTANT, + ) + consumer = await stream.consumers.create(consumer_config) + assert isinstance(consumer, PullConsumer) + finally: + await js.streams.delete(stream_name) + + +async def test_pull_consumer_durable(js: JetStream) -> None: + stream_name = f"test-pdurable-{uuid.uuid4()}" + config = StreamConfig(name=stream_name, subjects=[f"{stream_name}.>"]) + stream = await js.streams.create(config) + try: + durable_name = f"durable-{uuid.uuid4()}" + consumer_config = PullConsumerConfig( + durable_name=durable_name, + ) + consumer = await stream.consumers.create(consumer_config) + assert isinstance(consumer, PullConsumer) + + consumer2 = await stream.consumers.get_pull(durable_name) + assert isinstance(consumer2, PullConsumer) + finally: + await js.streams.delete(stream_name) + + +async def test_push_consumer_create(js: JetStream) -> None: + stream_name = f"test-pushcreate-{uuid.uuid4()}" + config = StreamConfig(name=stream_name, subjects=[f"{stream_name}.>"]) + stream = await js.streams.create(config) + try: + deliver_subj = uuid.uuid4().hex + consumer_config = PushConsumerConfig( + deliver_subject=deliver_subj, + name=f"consumer-{uuid.uuid4()}", + ) + consumer = await stream.consumers.create(consumer_config) + assert isinstance(consumer, PushConsumer) + finally: + await js.streams.delete(stream_name) + + +async def test_pull_consumer_messages(js: JetStream) -> None: + stream_name = f"test-pushmsg-{uuid.uuid4()}" + subj = f"{stream_name}.data" + config = StreamConfig(name=stream_name, subjects=[f"{stream_name}.>"]) + messages = [uuid.uuid4().hex.encode(), uuid.uuid4().hex.encode()] + stream = await js.streams.create(config) + try: + for message in messages: + await js.publish(subj, message, wait=True) + consumer_config = PullConsumerConfig(name=f"consumer-{uuid.uuid4()}") + consumer = await stream.consumers.create(consumer_config) + msgs_iter = await consumer.fetch(timeout=0.5) + for nats_msg, payload in zip(msgs_iter, messages, strict=True): + assert nats_msg.payload == payload + finally: + await js.streams.delete(stream_name) + + +async def test_push_consumer_messages(js: JetStream) -> None: + stream_name = f"test-pushmsg-{uuid.uuid4()}" + subj = f"{stream_name}.data" + config = StreamConfig(name=stream_name, subjects=[f"{stream_name}.>"]) + messages = [uuid.uuid4().hex.encode(), uuid.uuid4().hex.encode()] + stream = await js.streams.create(config) + try: + for message in messages: + await js.publish(subj, message, wait=True) + + deliver_subj = uuid.uuid4().hex + consumer_config = PushConsumerConfig( + deliver_subject=deliver_subj, + name=f"consumer-{uuid.uuid4()}", + ) + consumer = await stream.consumers.create(consumer_config) + msgs_iter = await consumer.messages() + for message in messages: + nats_msg = await msgs_iter.next(timeout=0.5) + assert message == nats_msg.payload + finally: + await js.streams.delete(stream_name) + + +async def test_consumer_config_properties() -> None: + config = PullConsumerConfig( + name="test-consumer", + description="test description", + ack_policy=AckPolicy.EXPLICIT, + deliver_policy=DeliverPolicy.ALL, + replay_policy=ReplayPolicy.INSTANT, + max_deliver=5, + max_ack_pending=100, + ) + assert config.name == "test-consumer" + assert config.description == "test description" + assert config.ack_policy == AckPolicy.EXPLICIT + assert config.deliver_policy == DeliverPolicy.ALL + assert config.replay_policy == ReplayPolicy.INSTANT + assert config.max_deliver == 5 + assert config.max_ack_pending == 100 + + +async def test_push_consumer_config_properties() -> None: + config = PushConsumerConfig( + deliver_subject="test.deliver", + name="test-push", + description="push test", + ack_policy=AckPolicy.EXPLICIT, + deliver_policy=DeliverPolicy.NEW, + ) + assert config.deliver_subject == "test.deliver" + assert config.name == "test-push" + assert config.description == "push test" + assert config.ack_policy == AckPolicy.EXPLICIT + assert config.deliver_policy == DeliverPolicy.NEW diff --git a/python/tests/test_jetstream.py b/python/tests/test_jetstream.py new file mode 100644 index 0000000..4fac22a --- /dev/null +++ b/python/tests/test_jetstream.py @@ -0,0 +1,50 @@ +import uuid + +import pytest +from natsrpy import Nats +from natsrpy.js import ( + JetStream, + StreamConfig, +) + + +async def test_jetstream_creation(nats: Nats) -> None: + js = await nats.jetstream() + assert isinstance(js, JetStream) + + +async def test_jetstream_has_streams_manager(js: JetStream) -> None: + assert js.streams is not None + + +async def test_jetstream_has_kv_manager(js: JetStream) -> None: + assert js.kv is not None + + +async def test_jetstream_has_object_store_manager(js: JetStream) -> None: + assert js.object_store is not None + + +@pytest.mark.parametrize("payload", [b"bytes-test", "str-test"]) +async def test_jetstream_publish(js: JetStream, payload: str | bytes) -> None: + stream_name = f"test-js-pub-{uuid.uuid4().hex}" + subj = f"{stream_name}.data" + config = StreamConfig(name=stream_name, subjects=[f"{stream_name}.>"]) + stream = await js.streams.create(config) + try: + await js.publish(subj, payload, wait=True) + info = await stream.get_info() + assert info.state.messages >= 1 + finally: + await js.streams.delete(stream_name) + + +async def test_jetstream_publish_with_headers(js: JetStream) -> None: + stream_name = f"test-js-pubhdr-{uuid.uuid4().hex[:8]}" + subj = f"{stream_name}.data" + config = StreamConfig(name=stream_name, subjects=[f"{stream_name}.>"]) + await js.streams.create(config) + try: + await js.publish(subj, b"with-headers", headers={"x-test": "value"}, wait=True) + finally: + await js.streams.delete(stream_name) diff --git a/python/tests/test_kv.py b/python/tests/test_kv.py new file mode 100644 index 0000000..aa63ef9 --- /dev/null +++ b/python/tests/test_kv.py @@ -0,0 +1,172 @@ +import uuid + +from natsrpy.js import ( + JetStream, + KeyValue, + KVConfig, + StorageType, +) + + +async def test_kv_create(js: JetStream) -> None: + bucket = f"test-kv-create-{uuid.uuid4().hex[:8]}" + config = KVConfig(bucket=bucket) + kv = await js.kv.create(config) + try: + assert isinstance(kv, KeyValue) + assert kv.name == bucket + finally: + await js.kv.delete(bucket) + + +async def test_kv_put_and_get(js: JetStream) -> None: + bucket = f"test-kv-pg-{uuid.uuid4().hex[:8]}" + config = KVConfig(bucket=bucket) + kv = await js.kv.create(config) + try: + revision = await kv.put("key1", b"value1") + assert isinstance(revision, int) + assert revision >= 1 + + value = await kv.get("key1") + assert value == b"value1" + finally: + await js.kv.delete(bucket) + + +async def test_kv_get_nonexistent(js: JetStream) -> None: + bucket = f"test-kv-noexist-{uuid.uuid4().hex[:8]}" + config = KVConfig(bucket=bucket) + kv = await js.kv.create(config) + try: + value = await kv.get("nonexistent-key") + assert value is None + finally: + await js.kv.delete(bucket) + + +async def test_kv_put_overwrite(js: JetStream) -> None: + bucket = f"test-kv-overwrite-{uuid.uuid4().hex[:8]}" + config = KVConfig(bucket=bucket) + kv = await js.kv.create(config) + try: + await kv.put("key1", b"original") + await kv.put("key1", b"updated") + value = await kv.get("key1") + assert value == b"updated" + finally: + await js.kv.delete(bucket) + + +async def test_kv_delete_key(js: JetStream) -> None: + bucket = f"test-kv-delkey-{uuid.uuid4().hex[:8]}" + config = KVConfig(bucket=bucket) + kv = await js.kv.create(config) + try: + await kv.put("key1", b"to-delete") + await kv.delete("key1") + value = await kv.get("key1") + assert value is None + finally: + await js.kv.delete(bucket) + + +async def test_kv_multiple_keys(js: JetStream) -> None: + bucket = f"test-kv-multi-{uuid.uuid4().hex[:8]}" + config = KVConfig(bucket=bucket) + kv = await js.kv.create(config) + try: + await kv.put("key1", b"value1") + await kv.put("key2", b"value2") + await kv.put("key3", b"value3") + assert await kv.get("key1") == b"value1" + assert await kv.get("key2") == b"value2" + assert await kv.get("key3") == b"value3" + finally: + await js.kv.delete(bucket) + + +async def test_kv_large_value(js: JetStream) -> None: + bucket = f"test-kv-large-{uuid.uuid4().hex[:8]}" + config = KVConfig(bucket=bucket) + kv = await js.kv.create(config) + try: + large_value = b"x" * 65536 + await kv.put("large", large_value) + value = await kv.get("large") + assert value == large_value + finally: + await js.kv.delete(bucket) + + +async def test_kv_properties(js: JetStream) -> None: + bucket = f"test-kv-props-{uuid.uuid4().hex[:8]}" + config = KVConfig(bucket=bucket) + kv = await js.kv.create(config) + try: + assert kv.name == bucket + assert isinstance(kv.stream_name, str) + assert len(kv.stream_name) > 0 + assert isinstance(kv.prefix, str) + assert isinstance(kv.use_jetstream_prefix, bool) + finally: + await js.kv.delete(bucket) + + +async def test_kv_create_or_update(js: JetStream) -> None: + bucket = f"test-kv-cou-{uuid.uuid4().hex[:8]}" + config = KVConfig(bucket=bucket) + kv1 = await js.kv.create_or_update(config) + try: + assert isinstance(kv1, KeyValue) + config.description = "updated" + kv2 = await js.kv.create_or_update(config) + assert isinstance(kv2, KeyValue) + finally: + await js.kv.delete(bucket) + + +async def test_kv_get_bucket(js: JetStream) -> None: + bucket = f"test-kv-getb-{uuid.uuid4().hex[:8]}" + config = KVConfig(bucket=bucket) + await js.kv.create(config) + try: + kv = await js.kv.get(bucket) + assert isinstance(kv, KeyValue) + assert kv.name == bucket + finally: + await js.kv.delete(bucket) + + +async def test_kv_delete_bucket(js: JetStream) -> None: + bucket = f"test-kv-delbucket-{uuid.uuid4().hex[:8]}" + config = KVConfig(bucket=bucket) + await js.kv.create(config) + result = await js.kv.delete(bucket) + assert result is True + + +async def test_kv_config_with_options(js: JetStream) -> None: + bucket = f"test-kv-opts-{uuid.uuid4().hex[:8]}" + config = KVConfig( + bucket=bucket, + description="test kv store", + history=5, + storage=StorageType.MEMORY, + ) + kv = await js.kv.create(config) + try: + assert kv.name == bucket + finally: + await js.kv.delete(bucket) + + +async def test_kv_config_properties() -> None: + config = KVConfig( + bucket="test-bucket", + description="test description", + history=10, + ) + assert config.bucket == "test-bucket" + assert config.description == "test description" + assert config.history == 10 diff --git a/python/tests/test_message.py b/python/tests/test_message.py new file mode 100644 index 0000000..cf1b245 --- /dev/null +++ b/python/tests/test_message.py @@ -0,0 +1,103 @@ +import uuid + +from natsrpy import Nats + + +async def test_message_subject(nats: Nats) -> None: + subj = uuid.uuid4().hex + sub = await nats.subscribe(subject=subj) + await nats.publish(subj, b"subject-test") + msg = await anext(sub) + assert msg.subject == subj + + +async def test_message_payload(nats: Nats) -> None: + subj = uuid.uuid4().hex + payload = b"payload-test-data" + sub = await nats.subscribe(subject=subj) + await nats.publish(subj, payload) + msg = await anext(sub) + assert msg.payload == payload + assert isinstance(msg.payload, bytes) + + +async def test_message_headers_empty(nats: Nats) -> None: + subj = uuid.uuid4().hex + sub = await nats.subscribe(subject=subj) + await nats.publish(subj, b"no-headers") + msg = await anext(sub) + assert msg.headers == {} + + +async def test_message_headers_string(nats: Nats) -> None: + subj = uuid.uuid4().hex + headers = {"content-type": "application/json", "x-id": "12345"} + sub = await nats.subscribe(subject=subj) + await nats.publish(subj, b"with-headers", headers=headers) + msg = await anext(sub) + assert msg.headers == headers + + +async def test_message_headers_multi_value(nats: Nats) -> None: + subj = uuid.uuid4().hex + headers = {"x-values": ["a", "b", "c"]} + sub = await nats.subscribe(subject=subj) + await nats.publish(subj, b"multi-headers", headers=headers) + msg = await anext(sub) + assert msg.headers == headers + + +async def test_message_reply_present(nats: Nats) -> None: + subj = uuid.uuid4().hex + reply_to = uuid.uuid4().hex + sub = await nats.subscribe(subject=subj) + await nats.publish(subj, b"reply-test", reply=reply_to) + msg = await anext(sub) + assert msg.reply == reply_to + + +async def test_message_reply_absent(nats: Nats) -> None: + subj = uuid.uuid4().hex + sub = await nats.subscribe(subject=subj) + await nats.publish(subj, b"no-reply") + msg = await anext(sub) + assert msg.reply is None + + +async def test_message_length(nats: Nats) -> None: + subj = uuid.uuid4().hex + payload = b"length-check" + sub = await nats.subscribe(subject=subj) + await nats.publish(subj, payload) + msg = await anext(sub) + # length is the total message length (includes subject + overhead), not just payload + assert msg.length >= len(payload) + + +async def test_message_length_empty(nats: Nats) -> None: + subj = uuid.uuid4().hex + sub = await nats.subscribe(subject=subj) + await nats.publish(subj, b"") + msg = await anext(sub) + # Even with empty payload, length includes overhead + assert msg.length >= 0 + + +async def test_message_repr(nats: Nats) -> None: + subj = uuid.uuid4().hex + sub = await nats.subscribe(subject=subj) + await nats.publish(subj, b"repr-test") + msg = await anext(sub) + r = repr(msg) + assert isinstance(r, str) + assert len(r) > 0 + + +async def test_message_large_payload(nats: Nats) -> None: + subj = uuid.uuid4().hex + payload = b"x" * 65536 + sub = await nats.subscribe(subject=subj) + await nats.publish(subj, payload) + msg = await anext(sub) + assert msg.payload == payload + assert msg.length >= 65536 diff --git a/python/tests/test_nats_client.py b/python/tests/test_nats_client.py new file mode 100644 index 0000000..7b14460 --- /dev/null +++ b/python/tests/test_nats_client.py @@ -0,0 +1,96 @@ +import uuid + +import pytest +from natsrpy import Nats + + +async def test_nats_default_constructor() -> None: + nats = Nats() + await nats.startup() + await nats.shutdown() + + +async def test_nats_custom_addrs() -> None: + nats = Nats(addrs=["localhost:4222"]) + await nats.startup() + await nats.shutdown() + + +async def test_nats_flush(nats: Nats) -> None: + subj = uuid.uuid4().hex + payload = b"flush-test" + sub = await nats.subscribe(subject=subj) + await nats.publish(subj, payload) + await nats.flush() + message = await anext(sub) + assert message.payload == payload + + +async def test_nats_drain(nats_url: str) -> None: + client = Nats(addrs=[nats_url]) + await client.startup() + subj = uuid.uuid4().hex + await client.subscribe(subject=subj) + await client.drain() + + +async def test_nats_startup_shutdown_cycle(nats_url: str) -> None: + client = Nats(addrs=[nats_url]) + await client.startup() + subj = uuid.uuid4().hex + payload = b"cycle-test" + sub = await client.subscribe(subject=subj) + await client.publish(subj, payload) + message = await anext(sub) + assert message.payload == payload + await client.shutdown() + + +async def test_nats_multiple_connections(nats_url: str) -> None: + client1 = Nats(addrs=[nats_url]) + client2 = Nats(addrs=[nats_url]) + await client1.startup() + await client2.startup() + + subj = uuid.uuid4().hex + payload = b"cross-client" + sub = await client2.subscribe(subject=subj) + await client1.publish(subj, payload) + message = await anext(sub) + assert message.payload == payload + + await client1.shutdown() + await client2.shutdown() + + +async def test_nats_publish_str_payload(nats: Nats) -> None: + subj = uuid.uuid4().hex + payload_str = "hello-string" + sub = await nats.subscribe(subject=subj) + await nats.publish(subj, payload_str) + message = await anext(sub) + assert message.payload == payload_str.encode() + + +async def test_nats_publish_empty_payload(nats: Nats) -> None: + subj = uuid.uuid4().hex + sub = await nats.subscribe(subject=subj) + await nats.publish(subj, b"") + message = await anext(sub) + assert message.payload == b"" + + +async def test_nats_publish_with_reply(nats: Nats) -> None: + subj = uuid.uuid4().hex + reply_subj = uuid.uuid4().hex + sub = await nats.subscribe(subject=subj) + await nats.publish(subj, b"with-reply", reply=reply_subj) + message = await anext(sub) + assert message.payload == b"with-reply" + assert message.reply == reply_subj + + +async def test_nats_connection_failure() -> None: + nats = Nats(addrs=["localhost:19999"]) + with pytest.raises(Exception): + await nats.startup() diff --git a/python/tests/test_object_store.py b/python/tests/test_object_store.py new file mode 100644 index 0000000..e97696e --- /dev/null +++ b/python/tests/test_object_store.py @@ -0,0 +1,146 @@ +import io +import uuid + +from natsrpy.js import ( + JetStream, + ObjectStore, + ObjectStoreConfig, + StorageType, +) + + +async def test_object_store_create(js: JetStream) -> None: + bucket = f"test-os-create-{uuid.uuid4().hex[:8]}" + config = ObjectStoreConfig(bucket=bucket) + store = await js.object_store.create(config) + try: + assert isinstance(store, ObjectStore) + finally: + await js.object_store.delete(bucket) + + +async def test_object_store_put_and_get(js: JetStream) -> None: + bucket = f"test-os-pg-{uuid.uuid4().hex[:8]}" + config = ObjectStoreConfig(bucket=bucket) + store = await js.object_store.create(config) + try: + await store.put("test-object", b"object-data") + writer = io.BytesIO() + await store.get("test-object", writer) + assert writer.getvalue() == b"object-data" + finally: + await js.object_store.delete(bucket) + + +async def test_object_store_put_large(js: JetStream) -> None: + bucket = f"test-os-large-{uuid.uuid4().hex[:8]}" + config = ObjectStoreConfig(bucket=bucket) + store = await js.object_store.create(config) + try: + large_data = b"x" * 100000 + await store.put("large-object", large_data) + writer = io.BytesIO() + await store.get("large-object", writer) + assert writer.getvalue() == large_data + finally: + await js.object_store.delete(bucket) + + +async def test_object_store_delete(js: JetStream) -> None: + bucket = f"test-os-del-{uuid.uuid4().hex[:8]}" + config = ObjectStoreConfig(bucket=bucket) + store = await js.object_store.create(config) + try: + await store.put("to-delete", b"delete-me") + await store.delete("to-delete") + finally: + await js.object_store.delete(bucket) + + +async def test_object_store_put_with_description(js: JetStream) -> None: + bucket = f"test-os-desc-{uuid.uuid4().hex[:8]}" + config = ObjectStoreConfig(bucket=bucket) + store = await js.object_store.create(config) + try: + await store.put( + "described-object", + b"data", + description="test description", + ) + writer = io.BytesIO() + await store.get("described-object", writer) + assert writer.getvalue() == b"data" + finally: + await js.object_store.delete(bucket) + + +async def test_object_store_put_with_headers(js: JetStream) -> None: + bucket = f"test-os-hdr-{uuid.uuid4().hex[:8]}" + config = ObjectStoreConfig(bucket=bucket) + store = await js.object_store.create(config) + try: + await store.put( + "header-object", + b"header-data", + headers={"x-custom": "value"}, + ) + writer = io.BytesIO() + await store.get("header-object", writer) + assert writer.getvalue() == b"header-data" + finally: + await js.object_store.delete(bucket) + + +async def test_object_store_overwrite(js: JetStream) -> None: + bucket = f"test-os-overwrite-{uuid.uuid4().hex[:8]}" + config = ObjectStoreConfig(bucket=bucket) + store = await js.object_store.create(config) + try: + await store.put("my-object", b"original") + await store.put("my-object", b"updated") + writer = io.BytesIO() + await store.get("my-object", writer) + assert writer.getvalue() == b"updated" + finally: + await js.object_store.delete(bucket) + + +async def test_object_store_get_existing_bucket(js: JetStream) -> None: + bucket = f"test-os-getb-{uuid.uuid4().hex[:8]}" + config = ObjectStoreConfig(bucket=bucket) + await js.object_store.create(config) + try: + store = await js.object_store.get(bucket) + assert isinstance(store, ObjectStore) + finally: + await js.object_store.delete(bucket) + + +async def test_object_store_delete_bucket(js: JetStream) -> None: + bucket = f"test-os-delbucket-{uuid.uuid4().hex[:8]}" + config = ObjectStoreConfig(bucket=bucket) + await js.object_store.create(config) + await js.object_store.delete(bucket) + + +async def test_object_store_config_with_options(js: JetStream) -> None: + bucket = f"test-os-opts-{uuid.uuid4().hex[:8]}" + config = ObjectStoreConfig( + bucket=bucket, + description="test object store", + storage=StorageType.MEMORY, + ) + store = await js.object_store.create(config) + try: + assert isinstance(store, ObjectStore) + finally: + await js.object_store.delete(bucket) + + +async def test_object_store_config_properties() -> None: + config = ObjectStoreConfig( + bucket="test-bucket", + description="test description", + ) + assert config.bucket == "test-bucket" + assert config.description == "test description" diff --git a/python/tests/test_request_reply.py b/python/tests/test_request_reply.py new file mode 100644 index 0000000..19989bc --- /dev/null +++ b/python/tests/test_request_reply.py @@ -0,0 +1,73 @@ +import asyncio +import uuid + +from natsrpy import Nats + + +async def test_request_sends_with_reply(nats: Nats) -> None: + subj = uuid.uuid4().hex + + received_payload: list[bytes] = [] + received_reply: list[str | None] = [] + + async def responder() -> None: + sub = await nats.subscribe(subject=subj) + msg = await anext(sub) + received_payload.append(msg.payload) + received_reply.append(msg.reply) + if msg.reply: + await nats.publish(msg.reply, b"reply-data") + + task = asyncio.create_task(responder()) + await asyncio.sleep(0.1) + + # request() sends a message and waits for a reply (though the response + # is not returned by the current implementation) + await nats.request(subj, b"request-payload") + await task + + assert received_payload == [b"request-payload"] + # request() should set a reply subject automatically + assert received_reply[0] is not None + + +async def test_request_with_headers(nats: Nats) -> None: + subj = uuid.uuid4().hex + + received_headers: list[dict[str, str]] = [] + + async def responder() -> None: + sub = await nats.subscribe(subject=subj) + msg = await anext(sub) + received_headers.append(msg.headers) + if msg.reply: + await nats.publish(msg.reply, b"reply") + + task = asyncio.create_task(responder()) + await asyncio.sleep(0.1) + + await nats.request(subj, b"data", headers={"x-custom": "value"}) + await task + + assert received_headers[0] == {"x-custom": "value"} + + +async def test_request_none_payload(nats: Nats) -> None: + subj = uuid.uuid4().hex + + received_payload: list[bytes] = [] + + async def responder() -> None: + sub = await nats.subscribe(subject=subj) + msg = await anext(sub) + received_payload.append(msg.payload) + if msg.reply: + await nats.publish(msg.reply, b"reply") + + task = asyncio.create_task(responder()) + await asyncio.sleep(0.1) + + await nats.request(subj, b"") + await task + + assert received_payload[0] == b"" diff --git a/python/tests/test_streams.py b/python/tests/test_streams.py new file mode 100644 index 0000000..2dfef3b --- /dev/null +++ b/python/tests/test_streams.py @@ -0,0 +1,254 @@ +import uuid + +from natsrpy.js import ( + DiscardPolicy, + JetStream, + RetentionPolicy, + StorageType, + Stream, + StreamConfig, +) + + +async def test_stream_create(js: JetStream) -> None: + name = f"test-create-{uuid.uuid4().hex[:8]}" + config = StreamConfig(name=name, subjects=[f"{name}.>"]) + stream = await js.streams.create(config) + try: + assert isinstance(stream, Stream) + finally: + await js.streams.delete(name) + + +async def test_stream_get(js: JetStream) -> None: + name = f"test-get-{uuid.uuid4().hex[:8]}" + config = StreamConfig(name=name, subjects=[f"{name}.>"]) + await js.streams.create(config) + try: + stream = await js.streams.get(name) + assert isinstance(stream, Stream) + finally: + await js.streams.delete(name) + + +async def test_stream_delete(js: JetStream) -> None: + name = f"test-del-{uuid.uuid4().hex[:8]}" + config = StreamConfig(name=name, subjects=[f"{name}.>"]) + await js.streams.create(config) + result = await js.streams.delete(name) + assert result is True + + +async def test_stream_create_or_update(js: JetStream) -> None: + name = f"test-cou-{uuid.uuid4().hex[:8]}" + config = StreamConfig(name=name, subjects=[f"{name}.>"]) + stream = await js.streams.create_or_update(config) + try: + assert isinstance(stream, Stream) + config.description = "updated" + stream2 = await js.streams.create_or_update(config) + assert isinstance(stream2, Stream) + finally: + await js.streams.delete(name) + + +async def test_stream_update(js: JetStream) -> None: + name = f"test-upd-{uuid.uuid4().hex[:8]}" + config = StreamConfig(name=name, subjects=[f"{name}.>"]) + await js.streams.create(config) + try: + config.description = "updated description" + stream = await js.streams.update(config) + assert isinstance(stream, Stream) + info = await stream.get_info() + assert info.config.description == "updated description" + finally: + await js.streams.delete(name) + + +async def test_stream_info(js: JetStream) -> None: + name = f"test-info-{uuid.uuid4().hex[:8]}" + config = StreamConfig(name=name, subjects=[f"{name}.>"]) + stream = await js.streams.create(config) + try: + info = await stream.get_info() + assert info.config.name == name + assert info.state.messages == 0 + assert info.state.bytes == 0 + assert info.state.consumer_count == 0 + assert str(info) is not None + finally: + await js.streams.delete(name) + + +async def test_stream_purge(js: JetStream) -> None: + name = f"test-purge-{uuid.uuid4().hex[:8]}" + subj = f"{name}.data" + config = StreamConfig(name=name, subjects=[f"{name}.>"]) + stream = await js.streams.create(config) + try: + await js.publish(subj, b"msg-1", wait=True) + await js.publish(subj, b"msg-2", wait=True) + await js.publish(subj, b"msg-3", wait=True) + info = await stream.get_info() + assert info.state.messages == 3 + purged = await stream.purge() + assert purged == 3 + info = await stream.get_info() + assert info.state.messages == 0 + finally: + await js.streams.delete(name) + + +async def test_stream_purge_with_filter(js: JetStream) -> None: + name = f"test-purgef-{uuid.uuid4().hex[:8]}" + config = StreamConfig(name=name, subjects=[f"{name}.>"]) + stream = await js.streams.create(config) + try: + await js.publish(f"{name}.a", b"a-msg", wait=True) + await js.publish(f"{name}.b", b"b-msg", wait=True) + purged = await stream.purge(filter=f"{name}.a") + assert purged == 1 + info = await stream.get_info() + assert info.state.messages == 1 + finally: + await js.streams.delete(name) + + +async def test_stream_direct_get(js: JetStream) -> None: + name = f"test-dget-{uuid.uuid4().hex[:8]}" + subj = f"{name}.data" + config = StreamConfig(name=name, subjects=[f"{name}.>"], allow_direct=True) + stream = await js.streams.create(config) + try: + await js.publish(subj, b"direct-get-msg", wait=True) + msg = await stream.direct_get(sequence=1) + assert msg.payload == b"direct-get-msg" + assert msg.subject == subj + assert msg.sequence == 1 + finally: + await js.streams.delete(name) + + +async def test_stream_message_repr(js: JetStream) -> None: + name = f"test-smrepr-{uuid.uuid4().hex[:8]}" + subj = f"{name}.data" + config = StreamConfig(name=name, subjects=[f"{name}.>"], allow_direct=True) + stream = await js.streams.create(config) + try: + await js.publish(subj, b"repr-test", wait=True) + msg = await stream.direct_get(sequence=1) + r = repr(msg) + assert isinstance(r, str) + assert len(r) > 0 + finally: + await js.streams.delete(name) + + +async def test_stream_config_memory_storage(js: JetStream) -> None: + name = f"test-mem-{uuid.uuid4().hex[:8]}" + config = StreamConfig( + name=name, + subjects=[f"{name}.>"], + storage=StorageType.MEMORY, + ) + stream = await js.streams.create(config) + try: + info = await stream.get_info() + assert info.config.storage == StorageType.MEMORY + finally: + await js.streams.delete(name) + + +async def test_stream_config_retention_policy(js: JetStream) -> None: + name = f"test-ret-{uuid.uuid4().hex[:8]}" + config = StreamConfig( + name=name, + subjects=[f"{name}.>"], + retention=RetentionPolicy.INTEREST, + ) + stream = await js.streams.create(config) + try: + info = await stream.get_info() + assert info.config.retention == RetentionPolicy.INTEREST + finally: + await js.streams.delete(name) + + +async def test_stream_config_discard_policy(js: JetStream) -> None: + name = f"test-disc-{uuid.uuid4().hex[:8]}" + config = StreamConfig( + name=name, + subjects=[f"{name}.>"], + max_messages=100, + discard=DiscardPolicy.NEW, + ) + stream = await js.streams.create(config) + try: + info = await stream.get_info() + assert info.config.discard == DiscardPolicy.NEW + finally: + await js.streams.delete(name) + + +async def test_stream_config_max_settings(js: JetStream) -> None: + name = f"test-max-{uuid.uuid4().hex[:8]}" + config = StreamConfig( + name=name, + subjects=[f"{name}.>"], + max_bytes=1048576, + max_messages=1000, + max_messages_per_subject=100, + max_message_size=4096, + ) + stream = await js.streams.create(config) + try: + info = await stream.get_info() + assert info.config.max_bytes == 1048576 + assert info.config.max_messages == 1000 + assert info.config.max_messages_per_subject == 100 + assert info.config.max_message_size == 4096 + finally: + await js.streams.delete(name) + + +async def test_stream_config_description(js: JetStream) -> None: + name = f"test-desc-{uuid.uuid4().hex[:8]}" + config = StreamConfig( + name=name, + subjects=[f"{name}.>"], + description="A test stream", + ) + stream = await js.streams.create(config) + try: + info = await stream.get_info() + assert info.config.description == "A test stream" + finally: + await js.streams.delete(name) + + +async def test_stream_consumers_manager(js: JetStream) -> None: + name = f"test-cmgr-{uuid.uuid4().hex[:8]}" + config = StreamConfig(name=name, subjects=[f"{name}.>"]) + stream = await js.streams.create(config) + try: + assert stream.consumers is not None + finally: + await js.streams.delete(name) + + +async def test_stream_state_after_publish(js: JetStream) -> None: + name = f"test-state-{uuid.uuid4().hex[:8]}" + subj = f"{name}.data" + config = StreamConfig(name=name, subjects=[f"{name}.>"]) + stream = await js.streams.create(config) + try: + await js.publish(subj, b"msg-1", wait=True) + await js.publish(subj, b"msg-2", wait=True) + info = await stream.get_info() + assert info.state.messages == 2 + assert info.state.first_sequence == 1 + assert info.state.last_sequence == 2 + assert info.state.bytes > 0 + finally: + await js.streams.delete(name) diff --git a/python/tests/test_subscriptions.py b/python/tests/test_subscriptions.py new file mode 100644 index 0000000..01f4b1f --- /dev/null +++ b/python/tests/test_subscriptions.py @@ -0,0 +1,120 @@ +import asyncio +import uuid + +from natsrpy import CallbackSubscription, IteratorSubscription, Nats + + +async def test_subscribe_returns_iterator(nats: Nats) -> None: + subj = uuid.uuid4().hex + sub = await nats.subscribe(subject=subj) + assert isinstance(sub, IteratorSubscription) + + +async def test_subscribe_with_callback(nats: Nats) -> None: + subj = uuid.uuid4().hex + received: list[bytes] = [] + event = asyncio.Event() + + async def callback(msg: object) -> None: + received.append(msg.payload) # type: ignore[attr-defined] + event.set() + + sub = await nats.subscribe(subject=subj, callback=callback) + assert isinstance(sub, CallbackSubscription) + await nats.publish(subj, b"callback-test") + await asyncio.wait_for(event.wait(), timeout=5.0) + assert received == [b"callback-test"] + + +async def test_iterator_next_with_timeout(nats: Nats) -> None: + subj = uuid.uuid4().hex + sub = await nats.subscribe(subject=subj) + await nats.publish(subj, b"timeout-test") + message = await sub.next(timeout=5.0) + assert message.payload == b"timeout-test" + + +async def test_iterator_aiter_protocol(nats: Nats) -> None: + subj = uuid.uuid4().hex + sub = await nats.subscribe(subject=subj) + payloads = [f"msg-{i}".encode() for i in range(3)] + for p in payloads: + await nats.publish(subj, p) + + received = [] + async for msg in sub: + received.append(msg.payload) + if len(received) == 3: + break + assert received == payloads + + +async def test_iterator_unsubscribe(nats: Nats) -> None: + subj = uuid.uuid4().hex + sub = await nats.subscribe(subject=subj) + await sub.unsubscribe() + + +async def test_iterator_unsubscribe_with_limit(nats: Nats) -> None: + subj = uuid.uuid4().hex + sub = await nats.subscribe(subject=subj) + await sub.unsubscribe(limit=2) + await nats.publish(subj, b"msg-1") + await nats.publish(subj, b"msg-2") + msg1 = await sub.next(timeout=5.0) + msg2 = await sub.next(timeout=5.0) + assert msg1.payload == b"msg-1" + assert msg2.payload == b"msg-2" + + +async def test_iterator_drain(nats_url: str) -> None: + client = Nats(addrs=[nats_url]) + await client.startup() + subj = uuid.uuid4().hex + sub = await client.subscribe(subject=subj) + await sub.drain() + await client.shutdown() + + +async def test_callback_receives_message(nats: Nats) -> None: + subj = uuid.uuid4().hex + event = asyncio.Event() + + async def callback(msg: object) -> None: + event.set() + + sub = await nats.subscribe(subject=subj, callback=callback) + assert isinstance(sub, CallbackSubscription) + await nats.publish(subj, b"trigger") + await asyncio.wait_for(event.wait(), timeout=5.0) + + +async def test_multiple_subscribers(nats: Nats) -> None: + subj = uuid.uuid4().hex + sub1 = await nats.subscribe(subject=subj) + sub2 = await nats.subscribe(subject=subj) + await nats.publish(subj, b"multi-sub") + msg1 = await anext(sub1) + msg2 = await anext(sub2) + assert msg1.payload == b"multi-sub" + assert msg2.payload == b"multi-sub" + + +async def test_wildcard_subscription(nats: Nats) -> None: + prefix = uuid.uuid4().hex + sub = await nats.subscribe(subject=f"{prefix}.*") + await nats.publish(f"{prefix}.one", b"wildcard-1") + await nats.publish(f"{prefix}.two", b"wildcard-2") + msg1 = await anext(sub) + msg2 = await anext(sub) + assert msg1.payload == b"wildcard-1" + assert msg2.payload == b"wildcard-2" + + +async def test_fullwild_subscription(nats: Nats) -> None: + prefix = uuid.uuid4().hex + sub = await nats.subscribe(subject=f"{prefix}.>") + await nats.publish(f"{prefix}.a.b.c", b"full-wild") + msg = await anext(sub) + assert msg.payload == b"full-wild" + assert msg.subject == f"{prefix}.a.b.c" diff --git a/src/exceptions/rust_err.rs b/src/exceptions/rust_err.rs index 236ff94..2b347f8 100644 --- a/src/exceptions/rust_err.rs +++ b/src/exceptions/rust_err.rs @@ -47,17 +47,19 @@ pub enum NatsrpyError { #[error(transparent)] CreateKeyValueError(#[from] async_nats::jetstream::context::CreateKeyValueError), #[error(transparent)] - KVEntryError(#[from] async_nats::jetstream::kv::EntryError), + CreateStreamError(#[from] async_nats::jetstream::context::CreateStreamError), #[error(transparent)] - KVPutError(#[from] async_nats::jetstream::kv::PutError), + GetStreamError(#[from] async_nats::jetstream::context::GetStreamError), #[error(transparent)] KVUpdateError(#[from] async_nats::jetstream::context::UpdateKeyValueError), #[error(transparent)] - DeleteError(#[from] async_nats::jetstream::kv::DeleteError), + JSPublishError(#[from] async_nats::jetstream::context::PublishError), #[error(transparent)] - CreateStreamError(#[from] async_nats::jetstream::context::CreateStreamError), + KVEntryError(#[from] async_nats::jetstream::kv::EntryError), #[error(transparent)] - GetStreamError(#[from] async_nats::jetstream::context::GetStreamError), + KVPutError(#[from] async_nats::jetstream::kv::PutError), + #[error(transparent)] + DeleteError(#[from] async_nats::jetstream::kv::DeleteError), #[error(transparent)] StreamDirectGetError(#[from] async_nats::jetstream::stream::DirectGetError), #[error(transparent)] diff --git a/src/js/consumers/push/consumer.rs b/src/js/consumers/push/consumer.rs index 9cdf968..7426ec5 100644 --- a/src/js/consumers/push/consumer.rs +++ b/src/js/consumers/push/consumer.rs @@ -60,6 +60,7 @@ impl MessagesIterator { slf } + #[pyo3(signature=(timeout=None))] pub fn next<'py>( &self, py: Python<'py>, diff --git a/src/js/jetstream.rs b/src/js/jetstream.rs index 00dbae0..212ad36 100644 --- a/src/js/jetstream.rs +++ b/src/js/jetstream.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use async_nats::{Subject, client::traits::Publisher, connection::State}; +use async_nats::{Subject, connection::State, jetstream::context::traits::Publisher}; use pyo3::{Bound, PyAny, Python, types::PyDict}; use tokio::sync::RwLock; @@ -24,6 +24,28 @@ impl JetStream { } } +#[pyo3::pyclass(from_py_object, get_all)] +#[derive(Clone, Debug)] +pub struct Publication { + pub stream: String, + pub sequence: u64, + pub domain: String, + pub duplicate: bool, + pub value: Option, +} + +impl From for Publication { + fn from(value: async_nats::jetstream::publish::PublishAck) -> Self { + Self { + stream: value.stream, + sequence: value.sequence, + domain: value.domain, + duplicate: value.duplicate, + value: value.value, + } + } +} + #[pyo3::pymethods] impl JetStream { #[getter] @@ -49,8 +71,8 @@ impl JetStream { payload, *, headers=None, - reply=None, - err_on_disconnect = false + err_on_disconnect = false, + wait = false, ))] pub fn publish<'py>( &self, @@ -58,8 +80,8 @@ impl JetStream { subject: String, payload: SendableValue, headers: Option>, - reply: Option, err_on_disconnect: bool, + wait: bool, ) -> NatsrpyResult> { let ctx = self.ctx.clone(); let data = payload.into(); @@ -72,16 +94,21 @@ impl JetStream { { return Err(NatsrpyError::Disconnected); } - ctx.read() + let publication = ctx + .read() .await - .publish_message(async_nats::message::OutboundMessage { + .publish_message(async_nats::jetstream::message::OutboundMessage { subject: Subject::from(subject), payload: data, headers: headermap, - reply: reply.map(Subject::from), }) .await?; - Ok(()) + + if wait { + Ok(Some(Publication::from(publication.await?))) + } else { + Ok(None) + } }) } } diff --git a/src/js/managers/consumers.rs b/src/js/managers/consumers.rs index 2ec1697..9433085 100644 --- a/src/js/managers/consumers.rs +++ b/src/js/managers/consumers.rs @@ -139,4 +139,11 @@ impl ConsumersManager { Ok(ctx.read().await.resume_consumer(&name).await?.paused) }) } + + pub fn delete<'py>(&self, py: Python<'py>, name: String) -> NatsrpyResult> { + let ctx = self.stream.clone(); + natsrpy_future(py, async move { + Ok(ctx.read().await.delete_consumer(&name).await?.success) + }) + } } diff --git a/src/js/mod.rs b/src/js/mod.rs index f6f16f9..011300d 100644 --- a/src/js/mod.rs +++ b/src/js/mod.rs @@ -10,7 +10,8 @@ pub mod stream; pub mod pymod { // Classes #[pymodule_export] - pub use super::jetstream::JetStream; + pub use super::jetstream::{JetStream, Publication}; + #[pymodule_export] pub use super::message::JetStreamMessage; diff --git a/src/js/stream.rs b/src/js/stream.rs index 24ddbb1..f78f891 100644 --- a/src/js/stream.rs +++ b/src/js/stream.rs @@ -7,7 +7,11 @@ use std::{collections::HashMap, ops::Deref, sync::Arc, time::Duration}; use crate::{ exceptions::rust_err::{NatsrpyError, NatsrpyResult}, js::managers::consumers::ConsumersManager, - utils::{headers::NatsrpyHeadermapExt, natsrpy_future, py_types::ToPyDate}, + utils::{ + futures::natsrpy_future_with_timeout, + headers::NatsrpyHeadermapExt, + py_types::{TimeValue, ToPyDate}, + }, }; use pyo3::{Bound, PyAny, Python}; use tokio::sync::RwLock; @@ -349,7 +353,7 @@ impl Source { start_sequence = None, start_time=None, domain=None, - subject_transforms = vec![] + subject_transforms = None ))] pub fn __new__( name: String, @@ -358,7 +362,7 @@ impl Source { start_sequence: Option, start_time: Option, domain: Option, - subject_transforms: Vec>, + subject_transforms: Option>>, ) -> NatsrpyResult { Ok(Self { name, @@ -367,6 +371,7 @@ impl Source { start_sequence, filter_subject, subject_transforms: subject_transforms + .unwrap_or_default() .into_iter() .map(|val| val.borrow().deref().clone()) .collect(), @@ -958,13 +963,15 @@ impl Stream { ConsumersManager::new(self.stream.clone()) } + #[pyo3(signature=(sequence, timeout=None))] pub fn direct_get<'py>( &self, py: Python<'py>, sequence: u64, + timeout: Option, ) -> NatsrpyResult> { let ctx = self.stream.clone(); - natsrpy_future(py, async move { + natsrpy_future_with_timeout(py, timeout, async move { let message = ctx.read().await.direct_get(sequence).await?; let result = Python::attach(move |gil| StreamMessage::from_nats_message(gil, &message))?; @@ -972,9 +979,14 @@ impl Stream { }) } - pub fn get_info<'py>(&self, py: Python<'py>) -> NatsrpyResult> { + #[pyo3(signature=(timeout=None))] + pub fn get_info<'py>( + &self, + py: Python<'py>, + timeout: Option, + ) -> NatsrpyResult> { let ctx = self.stream.clone(); - natsrpy_future(py, async move { + natsrpy_future_with_timeout(py, timeout, async move { StreamInfo::try_from(ctx.read().await.get_info().await?) }) } @@ -983,6 +995,7 @@ impl Stream { filter=None, sequence=None, keep=None, + timeout=None, ))] pub fn purge<'py>( &self, @@ -990,9 +1003,10 @@ impl Stream { filter: Option, sequence: Option, keep: Option, + timeout: Option, ) -> NatsrpyResult> { let ctx = self.stream.clone(); - natsrpy_future(py, async move { + natsrpy_future_with_timeout(py, timeout, async move { let mut purge_request = ctx.read().await.purge(); if let Some(filter) = filter { purge_request = purge_request.filter(filter); diff --git a/src/nats_cls.rs b/src/nats_cls.rs index a2b0d52..f6c4bfb 100644 --- a/src/nats_cls.rs +++ b/src/nats_cls.rs @@ -37,7 +37,7 @@ impl NatsCls { #[new] #[pyo3(signature = ( /, - addrs=vec![String::from("nats://localhost:4222")], + addrs=None, user_and_pass=None, nkey=None, token=None, @@ -49,7 +49,7 @@ impl NatsCls { request_timeout=TimeValue::FloatSecs(10.0), ))] fn __new__( - addrs: Vec, + addrs: Option>, user_and_pass: Option<(String, String)>, nkey: Option, token: Option, @@ -71,7 +71,7 @@ impl NatsCls { max_reconnects, connection_timeout, request_timeout, - addr: addrs, + addr: addrs.unwrap_or_else(|| vec![String::from("nats://localhost:4222")]), } } diff --git a/src/subscription.rs b/src/subscription.rs deleted file mode 100644 index 13cec8d..0000000 --- a/src/subscription.rs +++ /dev/null @@ -1,150 +0,0 @@ -use futures_util::StreamExt; -use std::sync::Arc; - -use pyo3::{Bound, Py, PyAny, PyRef, Python}; -use tokio::sync::Mutex; - -use crate::{ - exceptions::rust_err::{NatsrpyError, NatsrpyResult}, - utils::{futures::natsrpy_future_with_timeout, natsrpy_future, py_types::TimeValue}, -}; - -#[pyo3::pyclass] -pub struct Subscription { - inner: Option>>, - reading_task: Option, -} - -async fn process_message(message: async_nats::message::Message, py_callback: Py) { - let task = async || -> NatsrpyResult<()> { - let message = crate::message::Message::try_from(&message)?; - let awaitable = Python::attach(|gil| -> NatsrpyResult<_> { - let res = py_callback.call1(gil, (message,))?; - let rust_task = pyo3_async_runtimes::tokio::into_future(res.into_bound(gil))?; - Ok(rust_task) - })?; - awaitable.await?; - Ok(()) - }; - if let Err(err) = task().await { - log::error!("Cannot process message {message:?}. Error: {err}"); - } -} - -async fn start_py_sub( - sub: Arc>, - py_callback: Py, - locals: pyo3_async_runtimes::TaskLocals, -) { - while let Some(message) = sub.lock().await.next().await { - let py_cb = Python::attach(|py| py_callback.clone_ref(py)); - tokio::spawn(pyo3_async_runtimes::tokio::scope( - locals.clone(), - process_message(message, py_cb), - )); - } -} - -impl Subscription { - pub fn new(sub: async_nats::Subscriber, callback: Option>) -> NatsrpyResult { - let sub = Arc::new(Mutex::new(sub)); - let cb_sub = sub.clone(); - let task_locals = Python::attach(pyo3_async_runtimes::tokio::get_current_locals)?; - let task_handle = callback.map(move |cb| { - tokio::task::spawn(pyo3_async_runtimes::tokio::scope( - task_locals.clone(), - start_py_sub(cb_sub, cb, task_locals), - )) - .abort_handle() - }); - - Ok(Self { - inner: Some(sub), - reading_task: task_handle, - }) - } -} - -#[pyo3::pymethods] -impl Subscription { - #[must_use] - pub const fn __aiter__(slf: PyRef) -> PyRef { - slf - } - - pub fn next<'py>( - &self, - py: Python<'py>, - timeout: Option, - ) -> NatsrpyResult> { - let Some(inner) = self.inner.clone() else { - unreachable!("Subscription used after del") - }; - if self.reading_task.is_some() { - log::warn!( - "Callback is set. Getting messages from this subscription might produce unpredictable results." - ); - } - natsrpy_future_with_timeout(py, timeout, async move { - let Some(message) = inner.lock().await.next().await else { - return Err(NatsrpyError::AsyncStopIteration); - }; - crate::message::Message::try_from(message) - }) - } - - pub fn __anext__<'py>(&self, py: Python<'py>) -> NatsrpyResult> { - self.next(py, None) - } - - #[pyo3(signature=(limit=None))] - pub fn unsubscribe<'py>( - &self, - py: Python<'py>, - limit: Option, - ) -> NatsrpyResult> { - let Some(inner) = self.inner.clone() else { - unreachable!("Subscription used after del") - }; - natsrpy_future(py, async move { - if let Some(limit) = limit { - inner.lock().await.unsubscribe_after(limit).await?; - } else { - inner.lock().await.unsubscribe().await?; - } - Ok(()) - }) - } - - pub fn drain<'py>(&self, py: Python<'py>) -> NatsrpyResult> { - let Some(inner) = self.inner.clone() else { - unreachable!("Subscription used after del") - }; - natsrpy_future(py, async move { - inner.lock().await.drain().await?; - Ok(()) - }) - } -} - -/// This is required only because -/// in nats library they run async operation on Drop. -/// -/// Because of that we need to execute drop in async -/// runtime's context. -/// -/// And because we want to perform a drop, -/// we need somehow drop the inner variable, -/// but leave self intouch. That is exactly why we have -/// Option>. So we can just assign it to None -/// and it will perform a drop. -impl Drop for Subscription { - fn drop(&mut self) { - pyo3_async_runtimes::tokio::get_runtime().block_on(async move { - self.inner = None; - if let Some(reading) = self.reading_task.take() { - reading.abort(); - } - }); - } -} diff --git a/src/subscriptions/iterator.rs b/src/subscriptions/iterator.rs index e94f919..b38955f 100644 --- a/src/subscriptions/iterator.rs +++ b/src/subscriptions/iterator.rs @@ -30,6 +30,7 @@ impl IteratorSubscription { slf } + #[pyo3(signature=(timeout=None))] pub fn next<'py>( &self, py: Python<'py>, diff --git a/uv.lock b/uv.lock index 70fd9ac..bff886e 100644 --- a/uv.lock +++ b/uv.lock @@ -64,6 +64,146 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "librt" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/5f/63f5fa395c7a8a93558c0904ba8f1c8d1b997ca6a3de61bc7659970d66bf/librt-0.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81fd938344fecb9373ba1b155968c8a329491d2ce38e7ddb76f30ffb938f12dc", size = 65697, upload-time = "2026-02-17T16:11:06.903Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e0/0472cf37267b5920eff2f292ccfaede1886288ce35b7f3203d8de00abfe6/librt-0.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5db05697c82b3a2ec53f6e72b2ed373132b0c2e05135f0696784e97d7f5d48e7", size = 68376, upload-time = "2026-02-17T16:11:08.395Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8bd1359fdcd27ab897cd5963294fa4a7c83b20a8564678e4fd12157e56a5/librt-0.8.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d56bc4011975f7460bea7b33e1ff425d2f1adf419935ff6707273c77f8a4ada6", size = 197084, upload-time = "2026-02-17T16:11:09.774Z" }, + { url = "https://files.pythonhosted.org/packages/e2/fe/163e33fdd091d0c2b102f8a60cc0a61fd730ad44e32617cd161e7cd67a01/librt-0.8.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdc0f588ff4b663ea96c26d2a230c525c6fc62b28314edaaaca8ed5af931ad0", size = 207337, upload-time = "2026-02-17T16:11:11.311Z" }, + { url = "https://files.pythonhosted.org/packages/01/99/f85130582f05dcf0c8902f3d629270231d2f4afdfc567f8305a952ac7f14/librt-0.8.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97c2b54ff6717a7a563b72627990bec60d8029df17df423f0ed37d56a17a176b", size = 219980, upload-time = "2026-02-17T16:11:12.499Z" }, + { url = "https://files.pythonhosted.org/packages/6f/54/cb5e4d03659e043a26c74e08206412ac9a3742f0477d96f9761a55313b5f/librt-0.8.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f1125e6bbf2f1657d9a2f3ccc4a2c9b0c8b176965bb565dd4d86be67eddb4b6", size = 212921, upload-time = "2026-02-17T16:11:14.484Z" }, + { url = "https://files.pythonhosted.org/packages/b1/81/a3a01e4240579c30f3487f6fed01eb4bc8ef0616da5b4ebac27ca19775f3/librt-0.8.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8f4bb453f408137d7581be309b2fbc6868a80e7ef60c88e689078ee3a296ae71", size = 221381, upload-time = "2026-02-17T16:11:17.459Z" }, + { url = "https://files.pythonhosted.org/packages/08/b0/fc2d54b4b1c6fb81e77288ff31ff25a2c1e62eaef4424a984f228839717b/librt-0.8.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c336d61d2fe74a3195edc1646d53ff1cddd3a9600b09fa6ab75e5514ba4862a7", size = 216714, upload-time = "2026-02-17T16:11:19.197Z" }, + { url = "https://files.pythonhosted.org/packages/96/96/85daa73ffbd87e1fb287d7af6553ada66bf25a2a6b0de4764344a05469f6/librt-0.8.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:eb5656019db7c4deacf0c1a55a898c5bb8f989be904597fcb5232a2f4828fa05", size = 214777, upload-time = "2026-02-17T16:11:20.443Z" }, + { url = "https://files.pythonhosted.org/packages/12/9c/c3aa7a2360383f4bf4f04d98195f2739a579128720c603f4807f006a4225/librt-0.8.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c25d9e338d5bed46c1632f851babf3d13c78f49a225462017cf5e11e845c5891", size = 237398, upload-time = "2026-02-17T16:11:22.083Z" }, + { url = "https://files.pythonhosted.org/packages/61/19/d350ea89e5274665185dabc4bbb9c3536c3411f862881d316c8b8e00eb66/librt-0.8.1-cp310-cp310-win32.whl", hash = "sha256:aaab0e307e344cb28d800957ef3ec16605146ef0e59e059a60a176d19543d1b7", size = 54285, upload-time = "2026-02-17T16:11:23.27Z" }, + { url = "https://files.pythonhosted.org/packages/4f/d6/45d587d3d41c112e9543a0093d883eb57a24a03e41561c127818aa2a6bcc/librt-0.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:56e04c14b696300d47b3bc5f1d10a00e86ae978886d0cee14e5714fafb5df5d2", size = 61352, upload-time = "2026-02-17T16:11:24.207Z" }, + { url = "https://files.pythonhosted.org/packages/1d/01/0e748af5e4fee180cf7cd12bd12b0513ad23b045dccb2a83191bde82d168/librt-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:681dc2451d6d846794a828c16c22dc452d924e9f700a485b7ecb887a30aad1fd", size = 65315, upload-time = "2026-02-17T16:11:25.152Z" }, + { url = "https://files.pythonhosted.org/packages/9d/4d/7184806efda571887c798d573ca4134c80ac8642dcdd32f12c31b939c595/librt-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3b4350b13cc0e6f5bec8fa7caf29a8fb8cdc051a3bae45cfbfd7ce64f009965", size = 68021, upload-time = "2026-02-17T16:11:26.129Z" }, + { url = "https://files.pythonhosted.org/packages/ae/88/c3c52d2a5d5101f28d3dc89298444626e7874aa904eed498464c2af17627/librt-0.8.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ac1e7817fd0ed3d14fd7c5df91daed84c48e4c2a11ee99c0547f9f62fdae13da", size = 194500, upload-time = "2026-02-17T16:11:27.177Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5d/6fb0a25b6a8906e85b2c3b87bee1d6ed31510be7605b06772f9374ca5cb3/librt-0.8.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:747328be0c5b7075cde86a0e09d7a9196029800ba75a1689332348e998fb85c0", size = 205622, upload-time = "2026-02-17T16:11:28.242Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a6/8006ae81227105476a45691f5831499e4d936b1c049b0c1feb17c11b02d1/librt-0.8.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0af2bd2bc204fa27f3d6711d0f360e6b8c684a035206257a81673ab924aa11e", size = 218304, upload-time = "2026-02-17T16:11:29.344Z" }, + { url = "https://files.pythonhosted.org/packages/ee/19/60e07886ad16670aae57ef44dada41912c90906a6fe9f2b9abac21374748/librt-0.8.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d480de377f5b687b6b1bc0c0407426da556e2a757633cc7e4d2e1a057aa688f3", size = 211493, upload-time = "2026-02-17T16:11:30.445Z" }, + { url = "https://files.pythonhosted.org/packages/9c/cf/f666c89d0e861d05600438213feeb818c7514d3315bae3648b1fc145d2b6/librt-0.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d0ee06b5b5291f609ddb37b9750985b27bc567791bc87c76a569b3feed8481ac", size = 219129, upload-time = "2026-02-17T16:11:32.021Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ef/f1bea01e40b4a879364c031476c82a0dc69ce068daad67ab96302fed2d45/librt-0.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e2c6f77b9ad48ce5603b83b7da9ee3e36b3ab425353f695cba13200c5d96596", size = 213113, upload-time = "2026-02-17T16:11:33.192Z" }, + { url = "https://files.pythonhosted.org/packages/9b/80/cdab544370cc6bc1b72ea369525f547a59e6938ef6863a11ab3cd24759af/librt-0.8.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:439352ba9373f11cb8e1933da194dcc6206daf779ff8df0ed69c5e39113e6a99", size = 212269, upload-time = "2026-02-17T16:11:34.373Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9c/48d6ed8dac595654f15eceab2035131c136d1ae9a1e3548e777bb6dbb95d/librt-0.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:82210adabbc331dbb65d7868b105185464ef13f56f7f76688565ad79f648b0fe", size = 234673, upload-time = "2026-02-17T16:11:36.063Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/35b68b1db517f27a01be4467593292eb5315def8900afad29fabf56304ba/librt-0.8.1-cp311-cp311-win32.whl", hash = "sha256:52c224e14614b750c0a6d97368e16804a98c684657c7518752c356834fff83bb", size = 54597, upload-time = "2026-02-17T16:11:37.544Z" }, + { url = "https://files.pythonhosted.org/packages/71/02/796fe8f02822235966693f257bf2c79f40e11337337a657a8cfebba5febc/librt-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:c00e5c884f528c9932d278d5c9cbbea38a6b81eb62c02e06ae53751a83a4d52b", size = 61733, upload-time = "2026-02-17T16:11:38.691Z" }, + { url = "https://files.pythonhosted.org/packages/28/ad/232e13d61f879a42a4e7117d65e4984bb28371a34bb6fb9ca54ec2c8f54e/librt-0.8.1-cp311-cp311-win_arm64.whl", hash = "sha256:f7cdf7f26c2286ffb02e46d7bac56c94655540b26347673bea15fa52a6af17e9", size = 52273, upload-time = "2026-02-17T16:11:40.308Z" }, + { url = "https://files.pythonhosted.org/packages/95/21/d39b0a87ac52fc98f621fb6f8060efb017a767ebbbac2f99fbcbc9ddc0d7/librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a", size = 66516, upload-time = "2026-02-17T16:11:41.604Z" }, + { url = "https://files.pythonhosted.org/packages/69/f1/46375e71441c43e8ae335905e069f1c54febee63a146278bcee8782c84fd/librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9", size = 68634, upload-time = "2026-02-17T16:11:43.268Z" }, + { url = "https://files.pythonhosted.org/packages/0a/33/c510de7f93bf1fa19e13423a606d8189a02624a800710f6e6a0a0f0784b3/librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb", size = 198941, upload-time = "2026-02-17T16:11:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/dd/36/e725903416409a533d92398e88ce665476f275081d0d7d42f9c4951999e5/librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d", size = 209991, upload-time = "2026-02-17T16:11:45.462Z" }, + { url = "https://files.pythonhosted.org/packages/30/7a/8d908a152e1875c9f8eac96c97a480df425e657cdb47854b9efaa4998889/librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7", size = 224476, upload-time = "2026-02-17T16:11:46.542Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b8/a22c34f2c485b8903a06f3fe3315341fe6876ef3599792344669db98fcff/librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440", size = 217518, upload-time = "2026-02-17T16:11:47.746Z" }, + { url = "https://files.pythonhosted.org/packages/79/6f/5c6fea00357e4f82ba44f81dbfb027921f1ab10e320d4a64e1c408d035d9/librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9", size = 225116, upload-time = "2026-02-17T16:11:49.298Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a0/95ced4e7b1267fe1e2720a111685bcddf0e781f7e9e0ce59d751c44dcfe5/librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972", size = 217751, upload-time = "2026-02-17T16:11:50.49Z" }, + { url = "https://files.pythonhosted.org/packages/93/c2/0517281cb4d4101c27ab59472924e67f55e375bc46bedae94ac6dc6e1902/librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921", size = 218378, upload-time = "2026-02-17T16:11:51.783Z" }, + { url = "https://files.pythonhosted.org/packages/43/e8/37b3ac108e8976888e559a7b227d0ceac03c384cfd3e7a1c2ee248dbae79/librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0", size = 241199, upload-time = "2026-02-17T16:11:53.561Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/35812d041c53967fedf551a39399271bbe4257e681236a2cf1a69c8e7fa1/librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a", size = 54917, upload-time = "2026-02-17T16:11:54.758Z" }, + { url = "https://files.pythonhosted.org/packages/de/d1/fa5d5331b862b9775aaf2a100f5ef86854e5d4407f71bddf102f4421e034/librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444", size = 62017, upload-time = "2026-02-17T16:11:55.748Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7c/c614252f9acda59b01a66e2ddfd243ed1c7e1deab0293332dfbccf862808/librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d", size = 52441, upload-time = "2026-02-17T16:11:56.801Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3c/f614c8e4eaac7cbf2bbdf9528790b21d89e277ee20d57dc6e559c626105f/librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35", size = 66529, upload-time = "2026-02-17T16:11:57.809Z" }, + { url = "https://files.pythonhosted.org/packages/ab/96/5836544a45100ae411eda07d29e3d99448e5258b6e9c8059deb92945f5c2/librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583", size = 68669, upload-time = "2026-02-17T16:11:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/06/53/f0b992b57af6d5531bf4677d75c44f095f2366a1741fb695ee462ae04b05/librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c", size = 199279, upload-time = "2026-02-17T16:11:59.862Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ad/4848cc16e268d14280d8168aee4f31cea92bbd2b79ce33d3e166f2b4e4fc/librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04", size = 210288, upload-time = "2026-02-17T16:12:00.954Z" }, + { url = "https://files.pythonhosted.org/packages/52/05/27fdc2e95de26273d83b96742d8d3b7345f2ea2bdbd2405cc504644f2096/librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363", size = 224809, upload-time = "2026-02-17T16:12:02.108Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d0/78200a45ba3240cb042bc597d6f2accba9193a2c57d0356268cbbe2d0925/librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0", size = 218075, upload-time = "2026-02-17T16:12:03.631Z" }, + { url = "https://files.pythonhosted.org/packages/af/72/a210839fa74c90474897124c064ffca07f8d4b347b6574d309686aae7ca6/librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012", size = 225486, upload-time = "2026-02-17T16:12:04.725Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c1/a03cc63722339ddbf087485f253493e2b013039f5b707e8e6016141130fa/librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb", size = 218219, upload-time = "2026-02-17T16:12:05.828Z" }, + { url = "https://files.pythonhosted.org/packages/58/f5/fff6108af0acf941c6f274a946aea0e484bd10cd2dc37610287ce49388c5/librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b", size = 218750, upload-time = "2026-02-17T16:12:07.09Z" }, + { url = "https://files.pythonhosted.org/packages/71/67/5a387bfef30ec1e4b4f30562c8586566faf87e47d696768c19feb49e3646/librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d", size = 241624, upload-time = "2026-02-17T16:12:08.43Z" }, + { url = "https://files.pythonhosted.org/packages/d4/be/24f8502db11d405232ac1162eb98069ca49c3306c1d75c6ccc61d9af8789/librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a", size = 54969, upload-time = "2026-02-17T16:12:09.633Z" }, + { url = "https://files.pythonhosted.org/packages/5c/73/c9fdf6cb2a529c1a092ce769a12d88c8cca991194dfe641b6af12fa964d2/librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79", size = 62000, upload-time = "2026-02-17T16:12:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/d3/97/68f80ca3ac4924f250cdfa6e20142a803e5e50fca96ef5148c52ee8c10ea/librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0", size = 52495, upload-time = "2026-02-17T16:12:11.633Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f", size = 66081, upload-time = "2026-02-17T16:12:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c", size = 68309, upload-time = "2026-02-17T16:12:13.756Z" }, + { url = "https://files.pythonhosted.org/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc", size = 196804, upload-time = "2026-02-17T16:12:14.818Z" }, + { url = "https://files.pythonhosted.org/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c", size = 206907, upload-time = "2026-02-17T16:12:16.513Z" }, + { url = "https://files.pythonhosted.org/packages/d1/96/ef04902aad1424fd7299b62d1890e803e6ab4018c3044dca5922319c4b97/librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3", size = 221217, upload-time = "2026-02-17T16:12:17.906Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14", size = 214622, upload-time = "2026-02-17T16:12:19.108Z" }, + { url = "https://files.pythonhosted.org/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7", size = 221987, upload-time = "2026-02-17T16:12:20.331Z" }, + { url = "https://files.pythonhosted.org/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6", size = 215132, upload-time = "2026-02-17T16:12:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071", size = 215195, upload-time = "2026-02-17T16:12:23.073Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78", size = 237946, upload-time = "2026-02-17T16:12:24.275Z" }, + { url = "https://files.pythonhosted.org/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023", size = 50689, upload-time = "2026-02-17T16:12:25.766Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730", size = 57875, upload-time = "2026-02-17T16:12:27.465Z" }, + { url = "https://files.pythonhosted.org/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3", size = 48058, upload-time = "2026-02-17T16:12:28.556Z" }, + { url = "https://files.pythonhosted.org/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1", size = 68313, upload-time = "2026-02-17T16:12:29.659Z" }, + { url = "https://files.pythonhosted.org/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee", size = 70994, upload-time = "2026-02-17T16:12:31.516Z" }, + { url = "https://files.pythonhosted.org/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7", size = 220770, upload-time = "2026-02-17T16:12:33.294Z" }, + { url = "https://files.pythonhosted.org/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040", size = 235409, upload-time = "2026-02-17T16:12:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/89/1b/7bdf3e49349c134b25db816e4a3db6b94a47ac69d7d46b1e682c2c4949be/librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e", size = 246473, upload-time = "2026-02-17T16:12:36.656Z" }, + { url = "https://files.pythonhosted.org/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732", size = 238866, upload-time = "2026-02-17T16:12:37.849Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624", size = 250248, upload-time = "2026-02-17T16:12:39.445Z" }, + { url = "https://files.pythonhosted.org/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4", size = 240629, upload-time = "2026-02-17T16:12:40.889Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382", size = 239615, upload-time = "2026-02-17T16:12:42.446Z" }, + { url = "https://files.pythonhosted.org/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994", size = 263001, upload-time = "2026-02-17T16:12:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", size = 51328, upload-time = "2026-02-17T16:12:45.148Z" }, + { url = "https://files.pythonhosted.org/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", size = 58722, upload-time = "2026-02-17T16:12:46.85Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" }, +] + +[[package]] +name = "mypy" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, + { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, + { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, + { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +] + +[[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 = "natsrpy" source = { editable = "." } @@ -74,6 +214,7 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "anyio" }, + { name = "mypy" }, { name = "pytest" }, { name = "pytest-xdist" }, ] @@ -84,6 +225,7 @@ requires-dist = [{ name = "typing-extensions", specifier = ">=4.14.0" }] [package.metadata.requires-dev] dev = [ { name = "anyio", specifier = ">=4,<5" }, + { name = "mypy", specifier = ">=1.19.1,<2" }, { name = "pytest", specifier = ">=9,<10" }, { name = "pytest-xdist", specifier = ">=3,<4" }, ] @@ -97,6 +239,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] +[[package]] +name = "pathspec" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, +] + [[package]] name = "pluggy" version = "1.6.0"