From 716fc8e22e8aef7e8e0cb595094a067e01fd53fa Mon Sep 17 00:00:00 2001 From: Pavel Kirilin Date: Fri, 13 Mar 2026 14:54:43 +0100 Subject: [PATCH 1/8] Initial stream implementation. --- Cargo.lock | 2 +- Cargo.toml | 9 +- LICENSE | 21 ++++ README.md | 5 + pyproject.toml | 7 +- python/natsrpy/_inner/__init__.pyi | 20 ++-- python/natsrpy/_inner/js/__init__.pyi | 7 ++ python/natsrpy/_inner/js/kv.pyi | 10 ++ python/natsrpy/_inner/js/stream.pyi | 135 ++++++++++++++++++-------- python/natsrpy/js/__init__.py | 37 ++++++- python/natsrpy/js/stream.py | 18 +++- src/exceptions/rust_err.rs | 4 + src/js/jetstream.rs | 12 +++ src/js/kv.rs | 23 ++++- src/js/stream.rs | 88 +++++++++++++++-- 15 files changed, 331 insertions(+), 67 deletions(-) create mode 100644 LICENSE create mode 100644 README.md diff --git a/Cargo.lock b/Cargo.lock index db35df5..6797575 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -526,7 +526,7 @@ dependencies = [ [[package]] name = "natsrpy" -version = "0.0.1" +version = "0.0.0" dependencies = [ "async-nats", "bytes", diff --git a/Cargo.toml b/Cargo.toml index 815afb4..089506f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,17 @@ [package] name = "natsrpy" -version = "0.0.1" +# Don't update version manually. It's set during release workflow. +version = "0.0.0" edition = "2024" +description = "Python NATS client written in Rust" +repository = "https://github.com/taskiq-python/natsrpy" +license-file = "LICENSE" +publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] -name = "_inner" crate-type = ["cdylib"] +name = "_inner" [dependencies] async-nats = "0.46" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7b7a067 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022-present Pavel Kirilin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..bf02b03 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# Nats client + +This is a client library for [NATS](https://nats.io) written in rust. + +Credits for [Intree](https://intree.com) for supporting this project. diff --git a/pyproject.toml b/pyproject.toml index 89d3389..6b89d47 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,9 @@ [project] name = "natsrpy" requires-python = ">=3.8" +license-files = ["LICENSE"] +description = "Nats client library written in rust" +readme = "README.md" classifiers = [ "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", @@ -14,6 +17,6 @@ build-backend = "maturin" [tool.maturin] bindings = "pyo3" -python-source = "python" -module-name = "natsrpy._inner" features = ["pyo3/extension-module"] +module-name = "natsrpy._inner" +python-source = "python" diff --git a/python/natsrpy/_inner/__init__.pyi b/python/natsrpy/_inner/__init__.pyi index 6fe3937..c57aeb9 100644 --- a/python/natsrpy/_inner/__init__.pyi +++ b/python/natsrpy/_inner/__init__.pyi @@ -1,3 +1,5 @@ +from datetime import timedelta +from typing import Tuple from natsrpy._inner.js import JetStream from natsrpy._inner.message import Message @@ -10,15 +12,15 @@ class Nats: self, /, addrs: list[str] = ["nats://localhost:4222"], - user_and_pass=None, - nkey=None, - token=None, - custom_inbox_prefix=None, - read_buffer_capacity=65535, - sender_capacity=128, - max_reconnects=None, - connection_timeout=5.0, - request_timeout=10.0, + 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, + max_reconnects: int | None = None, + connection_timeout: timedelta = timedelta(seconds=5), + request_timeout: timedelta = timedelta(seconds=10), ) -> None: ... async def startup(self) -> None: ... async def publish( diff --git a/python/natsrpy/_inner/js/__init__.pyi b/python/natsrpy/_inner/js/__init__.pyi index f9dff63..1593b3e 100644 --- a/python/natsrpy/_inner/js/__init__.pyi +++ b/python/natsrpy/_inner/js/__init__.pyi @@ -1,4 +1,5 @@ from natsrpy._inner.js.kv import KeyValue, KVConfig +from natsrpy._inner.js.stream import Stream, StreamConfig class JetStream: async def publish( @@ -10,7 +11,13 @@ class JetStream: reply: str | None = None, err_on_disconnect: bool = False, ) -> None: ... + # KV async def create_kv(self, config: KVConfig) -> KeyValue: ... async def update_kv(self, config: KVConfig) -> KeyValue: ... async def get_kv(self, bucket: str) -> KeyValue: ... async def delete_kv(self, bucket: str) -> bool: ... + # Streams + async def create_stream(self, config: StreamConfig) -> Stream: ... + async def update_stream(self, config: StreamConfig) -> Stream: ... + async def get_stream(self, name: str) -> Stream: ... + async def delete_stream(self, name: str) -> Stream: ... diff --git a/python/natsrpy/_inner/js/kv.pyi b/python/natsrpy/_inner/js/kv.pyi index 684bcfc..f164eae 100644 --- a/python/natsrpy/_inner/js/kv.pyi +++ b/python/natsrpy/_inner/js/kv.pyi @@ -43,6 +43,16 @@ class KVConfig: ) -> None: ... class KeyValue: + @property + def stream_name(self) -> str: ... + @property + def prefix(self) -> str: ... + @property + def put_prefix(self) -> str | None: ... + @property + def use_jetstream_prefix(self) -> bool: ... + @property + def name(self) -> str: ... async def put(self, key: str, value: bytes) -> int: ... async def get(self, key: str) -> bytes | None: ... async def delete(self, key: str) -> int: ... diff --git a/python/natsrpy/_inner/js/stream.pyi b/python/natsrpy/_inner/js/stream.pyi index 68954cb..ce346f6 100644 --- a/python/natsrpy/_inner/js/stream.pyi +++ b/python/natsrpy/_inner/js/stream.pyi @@ -1,4 +1,5 @@ -from datetime import timedelta +from datetime import datetime, timedelta +from typing import Any class StorageType: FILE: "StorageType" @@ -90,45 +91,101 @@ class Republish: ) -> None: ... class StreamConfig: + name: str + subjects: list[str] + max_bytes: int | None + max_messages: int | None + max_messages_per_subject: int | None + discard: DiscardPolicy | None + discard_new_per_subject: bool | None + retention: RetentionPolicy | None + max_consumers: int | None + max_age: timedelta | None + max_message_size: int | None + storage: StorageType | None + num_replicas: int | None + no_ack: bool | None + duplicate_window: timedelta | None + template_owner: str | None + sealed: bool | None + description: str | None + allow_rollup: bool | None + deny_delete: bool | None + deny_purge: bool | None + republish: Republish | None + allow_direct: bool | None + mirror_direct: bool | None + mirror: Source | None + sources: list[Source] | None + metadata: dict[str, str] | None + subject_transform: SubjectTransform | None + compression: Compression | None + consumer_limits: ConsumerLimits | None + first_sequence: int | None + placement: Placement | None + persist_mode: PersistenceMode | None + pause_until: int | None + allow_message_ttl: bool | None + subject_delete_marker_ttl: timedelta | None + allow_atomic_publish: bool | None + allow_message_schedules: bool | None + allow_message_counter: bool | None + def __init__( self, name: str, - subjects, - max_bytes=None, - max_messages=None, - max_messages_per_subject=None, - discard=None, - discard_new_per_subject=None, - retention=None, - max_consumers=None, - max_age=None, - max_message_size=None, - storage=None, - num_replicas=None, - no_ack=None, - duplicate_window=None, - template_owner=None, - sealed=None, - description=None, - allow_rollup=None, - deny_delete=None, - deny_purge=None, - republish=None, - allow_direct=None, - mirror_direct=None, - mirror=None, - sources=None, - metadata=None, - subject_transform=None, - compression=None, - consumer_limits=None, - first_sequence=None, - placement=None, - persist_mode=None, - pause_until=None, - allow_message_ttl=None, - subject_delete_marker_ttl=None, - allow_atomic_publish=None, - allow_message_schedules=None, - allow_message_counter=None, + subjects: list[str], + max_bytes: int | None = None, + max_messages: int | None = None, + max_messages_per_subject: int | None = None, + discard: DiscardPolicy | None = None, + discard_new_per_subject: bool | None = None, + retention: RetentionPolicy | None = None, + max_consumers: int | None = None, + max_age: timedelta | None = None, + max_message_size: int | None = None, + storage: StorageType | None = None, + num_replicas: int | None = None, + no_ack: bool | None = None, + duplicate_window: timedelta | None = None, + template_owner: str | None = None, + sealed: bool | None = None, + description: str | None = None, + allow_rollup: bool | None = None, + deny_delete: bool | None = None, + deny_purge: bool | None = None, + republish: Republish | None = None, + allow_direct: bool | None = None, + mirror_direct: bool | None = None, + mirror: Source | None = None, + sources: list[Source] | None = None, + metadata: dict[str, str] | None = None, + subject_transform: SubjectTransform | None = None, + compression: Compression | None = None, + consumer_limits: ConsumerLimits | None = None, + first_sequence: int | None = None, + placement: Placement | None = None, + persist_mode: PersistenceMode | None = None, + pause_until: int | None = None, + allow_message_ttl: bool | None = None, + subject_delete_marker_ttl: timedelta | None = None, + allow_atomic_publish: bool | None = None, + allow_message_schedules: bool | None = None, + allow_message_counter: bool | None = None, ) -> None: ... + +class StreamMessage: + subject: str + sequence: int + headers: dict[str, Any] + payload: bytes + time: datetime + +class Stream: + async def direct_get(self, sequence: int) -> StreamMessage: + """ + Get direct message from a stream. + + Please note, that this method will throw an error + in case of stream being configured without `allow_direct=True`. + """ diff --git a/python/natsrpy/js/__init__.py b/python/natsrpy/js/__init__.py index ad8623c..bee2a53 100644 --- a/python/natsrpy/js/__init__.py +++ b/python/natsrpy/js/__init__.py @@ -1,3 +1,38 @@ from natsrpy._inner.js import JetStream +from natsrpy.js.stream import ( + StreamConfig, + Source, + Compression, + ConsumerLimits, + DiscardPolicy, + PersistenceMode, + Placement, + Republish, + RetentionPolicy, + StorageType, + Stream, + SubjectTransform, + External, + StreamMessage, +) +from natsrpy.js.kv import KVConfig, KeyValue -__all__ = ["JetStream"] +__all__ = [ + "JetStream", + "StreamConfig", + "Source", + "Compression", + "ConsumerLimits", + "DiscardPolicy", + "PersistenceMode", + "Placement", + "Republish", + "RetentionPolicy", + "StorageType", + "Stream", + "SubjectTransform", + "External", + "KVConfig", + "KeyValue", + "StreamMessage", +] diff --git a/python/natsrpy/js/stream.py b/python/natsrpy/js/stream.py index 589c47b..039ae2b 100644 --- a/python/natsrpy/js/stream.py +++ b/python/natsrpy/js/stream.py @@ -5,13 +5,29 @@ Source, StorageType, SubjectTransform, + DiscardPolicy, + PersistenceMode, + RetentionPolicy, + Compression, + ConsumerLimits, + StreamConfig, + Stream, + StreamMessage, ) __all__ = [ "External", + "Placement", "Republish", "Source", "StorageType", "SubjectTransform", - "Placement", + "DiscardPolicy", + "PersistenceMode", + "RetentionPolicy", + "Compression", + "ConsumerLimits", + "StreamConfig", + "Stream", + "StreamMessage", ] diff --git a/src/exceptions/rust_err.rs b/src/exceptions/rust_err.rs index 9d8a2ed..78659fb 100644 --- a/src/exceptions/rust_err.rs +++ b/src/exceptions/rust_err.rs @@ -50,6 +50,10 @@ pub enum NatsrpyError { DeleteError(#[from] async_nats::jetstream::kv::DeleteError), #[error(transparent)] CreateStreamError(#[from] async_nats::jetstream::context::CreateStreamError), + #[error(transparent)] + GetStreamError(#[from] async_nats::jetstream::context::GetStreamError), + #[error(transparent)] + StreamDirectGetError(#[from] async_nats::jetstream::stream::DirectGetError), } impl From for pyo3::PyErr { diff --git a/src/js/jetstream.rs b/src/js/jetstream.rs index 9ee59c3..45d6222 100644 --- a/src/js/jetstream.rs +++ b/src/js/jetstream.rs @@ -134,4 +134,16 @@ impl JetStream { )) }) } + + pub fn get_stream<'py>( + &self, + py: Python<'py>, + name: String, + ) -> NatsrpyResult> { + let ctx = self.ctx.clone(); + natsrpy_future(py, async move { + let js = ctx.read().await; + Ok(super::stream::Stream::new(js.get_stream(name).await?)) + }) + } } diff --git a/src/js/kv.rs b/src/js/kv.rs index 5c4545b..8a15bf5 100644 --- a/src/js/kv.rs +++ b/src/js/kv.rs @@ -99,9 +99,7 @@ impl TryFrom for async_nats::jetstream::kv::Config { description: value.description.unwrap_or_default(), max_value_size: value.max_value_size.unwrap_or_default(), history: value.history.unwrap_or_default(), - max_age: value - .max_age - .unwrap_or_default(), + max_age: value.max_age.unwrap_or_default(), max_bytes: value.max_bytes.unwrap_or_default(), storage: value.storage.unwrap_or_default().into(), num_replicas: value.num_replicas.unwrap_or_default(), @@ -130,15 +128,32 @@ impl TryFrom for async_nats::jetstream::kv::Config { } } -#[pyclass] +#[pyclass(from_py_object)] +#[derive(Clone)] pub struct KeyValue { + #[pyo3(get)] + name: String, + #[pyo3(get)] + stream_name: String, + #[pyo3(get)] + prefix: String, + #[pyo3(get)] + put_prefix: Option, + #[pyo3(get)] + use_jetstream_prefix: bool, store: Arc>, } impl KeyValue { #[must_use] pub fn new(store: async_nats::jetstream::kv::Store) -> Self { + // store. Self { + name: store.name.clone(), + stream_name: store.stream_name.clone(), + prefix: store.prefix.clone(), + put_prefix: store.put_prefix.clone(), + use_jetstream_prefix: store.use_jetstream_prefix, store: Arc::new(RwLock::new(store)), } } diff --git a/src/js/stream.rs b/src/js/stream.rs index 7dd8921..bdafe57 100644 --- a/src/js/stream.rs +++ b/src/js/stream.rs @@ -1,12 +1,15 @@ -use std::{ - collections::HashMap, - ops::Deref, - sync::{Arc, RwLock}, - time::Duration, +use pyo3::{ + Py, + types::{PyBytes, PyDateTime, PyTzInfo}, }; +use std::{collections::HashMap, ops::Deref, sync::Arc, time::Duration}; +use tokio::sync::RwLock; -use crate::exceptions::rust_err::{NatsrpyError, NatsrpyResult}; -use pyo3::{Bound, pyclass, pymethods}; +use crate::{ + exceptions::rust_err::{NatsrpyError, NatsrpyResult}, + utils::{headers::NatsrpyHeadermapExt, natsrpy_future}, +}; +use pyo3::{Bound, PyAny, Python, pyclass, pymethods, types::PyDict}; #[pyclass(from_py_object)] #[derive(Clone, Copy, Default, PartialEq, Eq)] @@ -547,6 +550,58 @@ impl TryFrom for async_nats::jetstream::stream::Config { } } +#[pyclass(get_all)] +#[derive(Debug)] +pub struct StreamMessage { + pub subject: String, + pub sequence: u64, + pub headers: Py, + pub payload: Py, + pub time: Py, +} + +impl StreamMessage { + pub fn from_nats_message( + py: Python, + msg: async_nats::jetstream::message::StreamMessage, + ) -> NatsrpyResult { + let time = msg.time.to_utc(); + let tz_info = PyTzInfo::utc(py)?; + let time = PyDateTime::new( + py, + time.year(), + time.month().into(), + time.day(), + time.hour(), + time.minute(), + time.second(), + time.microsecond(), + Some(tz_info.deref()), + )?; + Ok(Self { + subject: msg.subject.to_string(), + payload: PyBytes::new(py, &msg.payload).unbind(), + headers: msg.headers.to_pydict(py)?, + sequence: msg.sequence, + time: time.unbind(), + }) + } +} + +#[pymethods] +impl StreamMessage { + #[must_use] + pub fn __repr__(&self) -> String { + format!( + r#"StreamMessage"#, + subject = self.subject, + sequence = self.sequence, + payload = self.payload, + headers = self.headers, + ) + } +} + #[pyclass(from_py_object)] #[derive(Debug, Clone)] pub struct Stream { @@ -562,11 +617,28 @@ impl Stream { } } +#[pymethods] +impl Stream { + pub fn direct_get<'py>( + &self, + py: Python<'py>, + sequence: u64, + ) -> NatsrpyResult> { + let ctx = self.stream.clone(); + natsrpy_future(py, async move { + let message = ctx.read().await.direct_get(sequence).await?; + let result = Python::attach(move |gil| StreamMessage::from_nats_message(gil, message))?; + Ok(result) + }) + } +} + #[pyo3::pymodule(submodule, name = "stream")] pub mod pymod { #[pymodule_export] pub use super::{ Compression, ConsumerLimits, DiscardPolicy, External, PersistenceMode, Placement, - Republish, RetentionPolicy, Source, StorageType, Stream, StreamConfig, SubjectTransform, + Republish, RetentionPolicy, Source, StorageType, Stream, StreamConfig, StreamMessage, + SubjectTransform, }; } From 0e5369370e4fcc7e3992bbfe2c211eb3fd7b6e46 Mon Sep 17 00:00:00 2001 From: Pavel Kirilin Date: Fri, 13 Mar 2026 14:59:30 +0100 Subject: [PATCH 2/8] pre-commit fixes. --- .github/workflows/test.yml | 2 +- README.md | 24 ++++++++++++++++++++++++ src/js/stream.rs | 23 +++++++++++++---------- src/nats_cls.rs | 2 +- 4 files changed, 39 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9e4c607..102ecbc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -48,4 +48,4 @@ jobs: - uses: auguwu/clippy-action@1.4.0 with: token: ${{secrets.GITHUB_TOKEN}} - deny: warnings + deny: warnings diff --git a/README.md b/README.md index bf02b03..e26294a 100644 --- a/README.md +++ b/README.md @@ -3,3 +3,27 @@ This is a client library for [NATS](https://nats.io) written in rust. Credits for [Intree](https://intree.com) for supporting this project. + + +## installation + +This package can be installed from pypi: + +```bash +pip install natsrpy +``` + +Or alternatively you ca build it yourself using maturin, and stable rust. + +## Development + +We use stable rust and pyo3 for writing python extension module. + +In order to run the project use maturin: + +```bash +# To create .venv folder +uv venv +# To build and install the package in a virtual environment +maturin dev --uv +``` diff --git a/src/js/stream.rs b/src/js/stream.rs index bdafe57..bcdd688 100644 --- a/src/js/stream.rs +++ b/src/js/stream.rs @@ -98,7 +98,7 @@ impl From for async_nats::jetstream::stream::PersistenceMode { } #[pyclass(from_py_object)] -#[derive(Clone, Debug, PartialEq, Default)] +#[derive(Clone, Debug, PartialEq, Eq, Default)] pub struct ConsumerLimits { pub inactive_threshold: Duration, pub max_ack_pending: i64, @@ -107,7 +107,8 @@ pub struct ConsumerLimits { #[pymethods] impl ConsumerLimits { #[new] - pub fn __new__(inactive_threshold: Duration, max_ack_pending: i64) -> Self { + #[must_use] + pub const fn __new__(inactive_threshold: Duration, max_ack_pending: i64) -> Self { Self { inactive_threshold, max_ack_pending, @@ -135,7 +136,8 @@ pub struct Republish { #[pymethods] impl Republish { #[new] - pub fn __new__(source: String, destination: String, headers_only: bool) -> Self { + #[must_use] + pub const fn __new__(source: String, destination: String, headers_only: bool) -> Self { Self { source, destination, @@ -387,7 +389,7 @@ impl StreamConfig { allow_message_schedules=None, allow_message_counter=None, ))] - pub fn __new__( + pub const fn __new__( name: String, subjects: Vec, max_bytes: Option, @@ -430,12 +432,12 @@ impl StreamConfig { ) -> NatsrpyResult { Ok(Self { name, + subjects, max_bytes, max_messages, max_messages_per_subject, discard, discard_new_per_subject, - subjects, retention, max_consumers, max_age, @@ -523,9 +525,9 @@ impl TryFrom for async_nats::jetstream::stream::Config { // Values that require conversion between python -> rust types. conf.republish = value.republish.map(Into::into); - conf.storage = value.storage.map(Into::into).unwrap_or(conf.storage); - conf.retention = value.retention.map(Into::into).unwrap_or(conf.retention); - conf.discard = value.discard.map(Into::into).unwrap_or(conf.discard); + conf.storage = value.storage.map_or(conf.storage, Into::into); + conf.retention = value.retention.map_or(conf.retention, Into::into); + conf.discard = value.discard.map_or(conf.discard, Into::into); conf.mirror = value.mirror.map(TryInto::try_into).transpose()?; conf.sources = value .sources @@ -543,7 +545,7 @@ impl TryFrom for async_nats::jetstream::stream::Config { conf.persist_mode = value.persist_mode.map(Into::into); conf.pause_until = value .pause_until - .map(|val| time::OffsetDateTime::from_unix_timestamp(val)) + .map(time::OffsetDateTime::from_unix_timestamp) .transpose()?; Ok(conf) @@ -576,7 +578,7 @@ impl StreamMessage { time.minute(), time.second(), time.microsecond(), - Some(tz_info.deref()), + Some(&*tz_info), )?; Ok(Self { subject: msg.subject.to_string(), @@ -608,6 +610,7 @@ pub struct Stream { stream: Arc>>, } impl Stream { + #[must_use] pub fn new( stream: async_nats::jetstream::stream::Stream, ) -> Self { diff --git a/src/nats_cls.rs b/src/nats_cls.rs index 8490425..b634614 100644 --- a/src/nats_cls.rs +++ b/src/nats_cls.rs @@ -171,7 +171,7 @@ impl NatsCls { payload: data, headers: headermap, inbox, - timeout: timeout.map(|val| Some(val)), + timeout: timeout.map(Some), }; session.send_request(subject, request).await?; Ok(()) From 26f119d22ba1a4ce43606aff8b30b5a8c791cc18 Mon Sep 17 00:00:00 2001 From: Pavel Kirilin Date: Fri, 13 Mar 2026 15:04:20 +0100 Subject: [PATCH 3/8] Added testing description. --- README.md | 10 +++++++++- pyproject.toml | 7 +++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e26294a..0d184b8 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Or alternatively you ca build it yourself using maturin, and stable rust. ## Development -We use stable rust and pyo3 for writing python extension module. +We use stable rust and pyo3 for writing python extension module. In order to run the project use maturin: @@ -27,3 +27,11 @@ uv venv # To build and install the package in a virtual environment maturin dev --uv ``` + +For lints please use `pre-commit`. + +```bash +pre-commit run -a +``` + +For tests we use pytest. diff --git a/pyproject.toml b/pyproject.toml index 6b89d47..a96700c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,9 @@ [project] name = "natsrpy" -requires-python = ">=3.8" -license-files = ["LICENSE"] description = "Nats client library written in rust" readme = "README.md" +requires-python = ">=3.8" +license-files = ["LICENSE"] classifiers = [ "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", @@ -11,6 +11,9 @@ classifiers = [ ] dynamic = ["version"] +[dependency-groups] +dev = ["pytest>=9,<10"] + [build-system] requires = ["maturin>=1.12,<2.0"] build-backend = "maturin" From ba26d934af94ec49e9253a2c38562035da274c1e Mon Sep 17 00:00:00 2001 From: Pavel Kirilin Date: Fri, 13 Mar 2026 16:47:36 +0100 Subject: [PATCH 4/8] Added CI + some fixes. --- .github/workflows/test.yml | 37 ++++- README.md | 2 - docker-compose.yaml | 15 ++ pyproject.toml | 90 +++++++++++- python/natsrpy/__init__.py | 2 +- python/natsrpy/_inner/__init__.pyi | 19 +-- python/natsrpy/_inner/js/stream.pyi | 22 +-- python/natsrpy/_inner/message.pyi | 3 - python/natsrpy/js/__init__.py | 24 ++-- python/natsrpy/js/stream.py | 28 ++-- python/tests/__init__.py | 0 python/tests/conftest.py | 31 +++++ python/tests/test_publish.py | 31 +++++ src/nats_cls.rs | 4 +- uv.lock | 206 ++++++++++++++++++++++++++++ 15 files changed, 455 insertions(+), 59 deletions(-) create mode 100644 docker-compose.yaml create mode 100644 python/tests/__init__.py create mode 100644 python/tests/conftest.py create mode 100644 python/tests/test_publish.py create mode 100644 uv.lock diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 102ecbc..464bb95 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,7 +1,7 @@ name: "Testing package" on: - pull_request: + push: jobs: py-lint: @@ -47,5 +47,36 @@ jobs: override: true - uses: auguwu/clippy-action@1.4.0 with: - token: ${{secrets.GITHUB_TOKEN}} - deny: warnings + token: ${{secrets.GITHUB_TOKEN}} + deny: warnings + pytest: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + components: clippy + override: true + - uses: actions/setup-python@v6 + with: + python-version: 3.x + - id: setup-uv + uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + version: "latest" + python-version: 3.x + - id: prepare-container + name: Prepare docker container + run: docker compose up -d --wait + - id: setup-venv + name: Setup virtualenv + run: uv venv + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + comand: dev --uv + sccache: true + - name: Run pytest + run: uv run pytest -vv -n auto python/tests diff --git a/README.md b/README.md index 0d184b8..13dd71e 100644 --- a/README.md +++ b/README.md @@ -33,5 +33,3 @@ For lints please use `pre-commit`. ```bash pre-commit run -a ``` - -For tests we use pytest. diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..62c8076 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,15 @@ +services: + nats: + image: nats:2.12.5-alpine + command: -m 8222 --jetstream + healthcheck: + test: + - CMD + - "sh" + - "-c" + - "wget http://localhost:8222/healthz -q -O - | xargs | grep ok || exit 1" + interval: 5s + timeout: 3s + retries: 20 + ports: + - 4222:4222 diff --git a/pyproject.toml b/pyproject.toml index a96700c..d5dfe5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "natsrpy" description = "Nats client library written in rust" readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.10" license-files = ["LICENSE"] classifiers = [ "Programming Language :: Python :: Implementation :: CPython", @@ -12,7 +12,11 @@ classifiers = [ dynamic = ["version"] [dependency-groups] -dev = ["pytest>=9,<10"] +dev = [ + "anyio>=4,<5", + "pytest>=9,<10", + "pytest-xdist>=3,<4", +] [build-system] requires = ["maturin>=1.12,<2.0"] @@ -23,3 +27,85 @@ bindings = "pyo3" features = ["pyo3/extension-module"] module-name = "natsrpy._inner" python-source = "python" + +[tool.pytest] +anyio_mode = "auto" + +[tool.mypy] +python_version = "3.10" +strict = true +ignore_missing_imports = true +allow_subclassing_any = true +allow_untyped_calls = true +packages = ["taskiq_nats"] +pretty = true +implicit_reexport = true +allow_untyped_decorators = true +warn_return_any = false + +[tool.ruff] +target-version="py310" +exclude = [".venv/"] +line-length = 88 + +[tool.ruff.lint] +mccabe = { max-complexity = 10 } +# List of enabled rulsets. +# See https://docs.astral.sh/ruff/rules/ for more information. +select = [ + "E", # Error + "F", # Pyflakes + "W", # Pycodestyle + "C90", # McCabe complexity + "I", # Isort + "N", # pep8-naming + "D", # Pydocstyle + "ANN", # Pytype annotations + "S", # Bandit + "B", # Bugbear + "COM", # Commas + "C4", # Comprehensions + "ISC", # Implicit string concat + "PIE", # Unnecessary code + "T20", # Catch prints + "PYI", # validate pyi files + "Q", # Checks for quotes + "RSE", # Checks raise statements + "RET", # Checks return statements + "SLF", # Self checks + "SIM", # Simplificator + "PTH", # Pathlib checks + "ERA", # Checks for commented out code + "PL", # PyLint checks + "RUF", # Specific to Ruff checks + "UP", # Pyupgrade +] +ignore = [ + "D105", # Missing docstring in magic method + "D107", # Missing docstring in __init__ + "D212", # Multi-line docstring summary should start at the first line + "D401", # First line should be in imperative mood + "D104", # Missing docstring in public package + "D100", # Missing docstring in public module + "ANN401", # typing.Any are disallowed in `**kwargs + "PLR0913", # Too many arguments for function call + "D106", # Missing docstring in public nested class + "PYI021", # Docstrings should not be included in stubs +] + +[tool.ruff.lint.per-file-ignores] +"python/tests/*" = [ + "S101", # Use of assert detected + "S301", # Use of pickle detected + "D103", # Missing docstring in public function + "SLF001", # Private member accessed + "S311", # Standard pseudo-random generators are not suitable for security/cryptographic purposes + "D101", # Missing docstring in public class +] + +[tool.ruff.lint.pydocstyle] +convention = "pep257" +ignore-decorators = ["typing.overload"] + +[tool.ruff.lint.pylint] +allow-magic-value-types = ["int", "str", "float"] diff --git a/python/natsrpy/__init__.py b/python/natsrpy/__init__.py index 6dfc866..84b6e8e 100644 --- a/python/natsrpy/__init__.py +++ b/python/natsrpy/__init__.py @@ -1,7 +1,7 @@ from natsrpy._inner import Message, Nats, Subscription __all__ = [ + "Message", "Nats", "Subscription", - "Message", ] diff --git a/python/natsrpy/_inner/__init__.pyi b/python/natsrpy/_inner/__init__.pyi index c57aeb9..532e71c 100644 --- a/python/natsrpy/_inner/__init__.pyi +++ b/python/natsrpy/_inner/__init__.pyi @@ -1,10 +1,11 @@ from datetime import timedelta -from typing import Tuple +from typing import Any + from natsrpy._inner.js import JetStream from natsrpy._inner.message import Message class Subscription: - def __aiter__(self) -> "Subscription": ... + def __aiter__(self) -> Subscription: ... async def __anext__(self) -> Message: ... class Nats: @@ -12,31 +13,31 @@ class Nats: self, /, addrs: list[str] = ["nats://localhost:4222"], - user_and_pass: Tuple[str, 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, max_reconnects: int | None = None, - connection_timeout: timedelta = timedelta(seconds=5), - request_timeout: timedelta = timedelta(seconds=10), + connection_timeout: timedelta = ..., + request_timeout: timedelta = ..., ) -> None: ... async def startup(self) -> None: ... + async def shutdown(self) -> None: ... async def publish( self, subject: str, payload: bytes, *, - headers: dict[str, str] | None = None, + headers: dict[str, Any] | None = None, reply: str | None = None, err_on_disconnect: bool = False, ) -> None: ... async def request(self, subject: str, payload: bytes) -> None: ... async def drain(self) -> None: ... async def flush(self) -> None: ... - async def close(self) -> None: ... - async def subscribe(self, topic: str) -> Subscription: ... + async def subscribe(self, subject: str) -> Subscription: ... async def jetstream(self) -> JetStream: ... -__all__ = ["Subscription", "Nats", "Message"] +__all__ = ["Message", "Nats", "Subscription"] diff --git a/python/natsrpy/_inner/js/stream.pyi b/python/natsrpy/_inner/js/stream.pyi index ce346f6..003a26e 100644 --- a/python/natsrpy/_inner/js/stream.pyi +++ b/python/natsrpy/_inner/js/stream.pyi @@ -2,25 +2,25 @@ from datetime import datetime, timedelta from typing import Any class StorageType: - FILE: "StorageType" - MEMORY: "StorageType" + FILE: StorageType + MEMORY: StorageType class DiscardPolicy: - OLD: "DiscardPolicy" - NEW: "DiscardPolicy" + OLD: DiscardPolicy + NEW: DiscardPolicy class RetentionPolicy: - LIMITS: "RetentionPolicy" - INTEREST: "RetentionPolicy" - WORKQUEUE: "RetentionPolicy" + LIMITS: RetentionPolicy + INTEREST: RetentionPolicy + WORKQUEUE: RetentionPolicy class Compression: - S2: "Compression" - NONE: "Compression" + S2: Compression + NONE: Compression class PersistenceMode: - Default: "PersistenceMode" - Async: "PersistenceMode" + Default: PersistenceMode + Async: PersistenceMode class ConsumerLimits: inactive_threshold: timedelta diff --git a/python/natsrpy/_inner/message.pyi b/python/natsrpy/_inner/message.pyi index de3824d..31911b5 100644 --- a/python/natsrpy/_inner/message.pyi +++ b/python/natsrpy/_inner/message.pyi @@ -8,6 +8,3 @@ class Message: status: int | None description: str | None length: int - - def __repr__(self) -> str: ... - def __str__(self) -> str: ... diff --git a/python/natsrpy/js/__init__.py b/python/natsrpy/js/__init__.py index bee2a53..79c8773 100644 --- a/python/natsrpy/js/__init__.py +++ b/python/natsrpy/js/__init__.py @@ -1,38 +1,38 @@ from natsrpy._inner.js import JetStream +from natsrpy.js.kv import KeyValue, KVConfig from natsrpy.js.stream import ( - StreamConfig, - Source, Compression, ConsumerLimits, DiscardPolicy, + External, PersistenceMode, Placement, Republish, RetentionPolicy, + Source, StorageType, Stream, - SubjectTransform, - External, + StreamConfig, StreamMessage, + SubjectTransform, ) -from natsrpy.js.kv import KVConfig, KeyValue __all__ = [ - "JetStream", - "StreamConfig", - "Source", "Compression", "ConsumerLimits", "DiscardPolicy", + "External", + "JetStream", + "KVConfig", + "KeyValue", "PersistenceMode", "Placement", "Republish", "RetentionPolicy", + "Source", "StorageType", "Stream", - "SubjectTransform", - "External", - "KVConfig", - "KeyValue", + "StreamConfig", "StreamMessage", + "SubjectTransform", ] diff --git a/python/natsrpy/js/stream.py b/python/natsrpy/js/stream.py index 039ae2b..01e0520 100644 --- a/python/natsrpy/js/stream.py +++ b/python/natsrpy/js/stream.py @@ -1,33 +1,33 @@ from natsrpy._inner.js.stream import ( + Compression, + ConsumerLimits, + DiscardPolicy, External, + PersistenceMode, Placement, Republish, + RetentionPolicy, Source, StorageType, - SubjectTransform, - DiscardPolicy, - PersistenceMode, - RetentionPolicy, - Compression, - ConsumerLimits, - StreamConfig, Stream, + StreamConfig, StreamMessage, + SubjectTransform, ) __all__ = [ + "Compression", + "ConsumerLimits", + "DiscardPolicy", "External", + "PersistenceMode", "Placement", "Republish", + "RetentionPolicy", "Source", "StorageType", - "SubjectTransform", - "DiscardPolicy", - "PersistenceMode", - "RetentionPolicy", - "Compression", - "ConsumerLimits", - "StreamConfig", "Stream", + "StreamConfig", "StreamMessage", + "SubjectTransform", ] diff --git a/python/tests/__init__.py b/python/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/tests/conftest.py b/python/tests/conftest.py new file mode 100644 index 0000000..2daa3d5 --- /dev/null +++ b/python/tests/conftest.py @@ -0,0 +1,31 @@ +import os +from collections.abc import AsyncGenerator + +import pytest +from natsrpy import Nats + + +@pytest.fixture(scope="session") +def anyio_backend() -> str: + """ + Anyio backend. + + Backend for anyio pytest plugin. + :return: backend name. + """ + return "asyncio" + + +@pytest.fixture(scope="session") +def nats_url() -> str: + return os.environ.get("NATS_URL", "localhost:4222") + + +@pytest.fixture(scope="session") +async def nats(nats_url: str) -> AsyncGenerator[Nats, None]: + nats = Nats(addrs=[nats_url]) + await nats.startup() + + yield nats + + await nats.shutdown() diff --git a/python/tests/test_publish.py b/python/tests/test_publish.py new file mode 100644 index 0000000..7454fb1 --- /dev/null +++ b/python/tests/test_publish.py @@ -0,0 +1,31 @@ +import uuid +from typing import Any + +import pytest +from natsrpy import Nats + + +async def test_publish_simple(nats: Nats) -> None: + subj = uuid.uuid4().hex + payload = uuid.uuid4().hex.encode() + sub = await nats.subscribe(subject=subj) + await nats.publish(subj, payload) + message = await anext(sub) + assert message.payload == payload + + +@pytest.mark.parametrize( + "headers", + [ + {"test": "string-headers"}, + {"test": ["multi", "value"]}, + ], +) +async def test_publis_headers(nats: Nats, headers: dict[str, Any]) -> None: + subj = uuid.uuid4().hex + payload = uuid.uuid4().hex.encode() + sub = await nats.subscribe(subject=subj) + await nats.publish(subj, payload, headers=headers) + message = await anext(sub) + assert message.payload == payload + assert message.headers == headers diff --git a/src/nats_cls.rs b/src/nats_cls.rs index b634614..254a493 100644 --- a/src/nats_cls.rs +++ b/src/nats_cls.rs @@ -265,7 +265,7 @@ impl NatsCls { })?) } - pub fn close<'py>(&self, py: Python<'py>) -> PyResult> { + pub fn shutdown<'py>(&self, py: Python<'py>) -> PyResult> { log::debug!("Closing nats session"); let session = self.nats_session.clone(); Ok(natsrpy_future(py, async move { @@ -300,7 +300,7 @@ impl Drop for NatsCls { let mut write_guard = self.nats_session.write().await; if let Some(session) = write_guard.as_ref() { log::warn!( - "NATS session was not closed before dropping. Draining session in drop. Please call `.close()` function before dropping the session to avoid this warning." + "NATS session was not closed before dropping. Draining session in drop. Please call `.shutdown()` function before dropping the session to avoid this warning." ); session.drain().await.ok(); } diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..265b25d --- /dev/null +++ b/uv.lock @@ -0,0 +1,206 @@ +version = 1 +revision = 2 +requires-python = ">=3.10" + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +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 = "natsrpy" +source = { editable = "." } + +[package.dev-dependencies] +dev = [ + { name = "anyio" }, + { name = "pytest" }, + { name = "pytest-xdist" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "anyio", specifier = ">=4,<5" }, + { name = "pytest", specifier = ">=9,<10" }, + { name = "pytest-xdist", specifier = ">=3,<4" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +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 = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] From d56186c790346ee872f1e9e38534315ddc873025 Mon Sep 17 00:00:00 2001 From: Pavel Kirilin Date: Fri, 13 Mar 2026 16:53:34 +0100 Subject: [PATCH 5/8] Fixed CI. --- .github/workflows/test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 464bb95..52d5377 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -66,7 +66,6 @@ jobs: with: enable-cache: true version: "latest" - python-version: 3.x - id: prepare-container name: Prepare docker container run: docker compose up -d --wait From c7a48fae6a346b4d117ea6603e43aaa14d1482d4 Mon Sep 17 00:00:00 2001 From: Pavel Kirilin Date: Fri, 13 Mar 2026 18:52:26 +0100 Subject: [PATCH 6/8] Some udpates. --- .github/workflows/test.yml | 20 +++---- .pre-commit-config.yaml | 14 ++++- README.md | 3 ++ pyproject.toml | 104 ++++++++++++++++++++----------------- src/js/stream.rs | 5 +- src/lib.rs | 2 +- 6 files changed, 81 insertions(+), 67 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 52d5377..dcd81aa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ jobs: - mypy runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v4 with: @@ -25,7 +25,7 @@ jobs: fmt: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v6 - uses: actions-rs/toolchain@v1 with: toolchain: stable @@ -39,7 +39,7 @@ jobs: checks: write runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v6 - uses: actions-rs/toolchain@v1 with: toolchain: stable @@ -48,11 +48,10 @@ jobs: - uses: auguwu/clippy-action@1.4.0 with: token: ${{secrets.GITHUB_TOKEN}} - deny: warnings pytest: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v6 - uses: actions-rs/toolchain@v1 with: toolchain: stable @@ -61,21 +60,16 @@ jobs: - uses: actions/setup-python@v6 with: python-version: 3.x - - id: setup-uv - uses: astral-sh/setup-uv@v7 - with: - enable-cache: true - version: "latest" - id: prepare-container name: Prepare docker container run: docker compose up -d --wait - id: setup-venv name: Setup virtualenv - run: uv venv - - name: Build wheels + run: python -m venv .venv + - name: Build lib uses: PyO3/maturin-action@v1 with: - comand: dev --uv + command: dev sccache: true - name: Run pytest run: uv run pytest -vv -n auto python/tests diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e5cfcf7..3f94ac8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,10 +39,10 @@ repos: args: - fmt - - id: clippy + - id: clippy-fix types: - rust - name: rust clippy + name: fixing fixable clippy language: system pass_filenames: false entry: cargo @@ -51,6 +51,16 @@ repos: - --fix - --allow-dirty + - id: clippy-check + types: + - rust + name: rust clippy + language: system + pass_filenames: false + entry: cargo + args: + - clippy + - id: check types: - rust diff --git a/README.md b/README.md index 13dd71e..34311a8 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ +[![PyPI](https://img.shields.io/pypi/v/natsrpy?style=for-the-badge)](https://pypi.org/project/scyllapy/) +[![PyPI - Downloads](https://img.shields.io/pypi/dm/natsrpy?style=for-the-badge)](https://pypistats.org/packages/scyllapy) + # Nats client This is a client library for [NATS](https://nats.io) written in rust. diff --git a/pyproject.toml b/pyproject.toml index d5dfe5a..63f07da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,12 +5,20 @@ readme = "README.md" requires-python = ">=3.10" license-files = ["LICENSE"] classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Rust", + "Topic :: System :: Networking", ] dynamic = ["version"] +[[project.authors]] +name = "Pavel Kirilin" +email = "s3riussan@gmail.com" + [dependency-groups] dev = [ "anyio>=4,<5", @@ -28,23 +36,21 @@ features = ["pyo3/extension-module"] module-name = "natsrpy._inner" python-source = "python" -[tool.pytest] -anyio_mode = "auto" - [tool.mypy] python_version = "3.10" strict = true ignore_missing_imports = true -allow_subclassing_any = true -allow_untyped_calls = true -packages = ["taskiq_nats"] +packages = ["natsrpy"] pretty = true implicit_reexport = true allow_untyped_decorators = true warn_return_any = false +[tool.pytest] +anyio_mode = "auto" + [tool.ruff] -target-version="py310" +target-version = "py310" exclude = [".venv/"] line-length = 88 @@ -53,54 +59,54 @@ mccabe = { max-complexity = 10 } # List of enabled rulsets. # See https://docs.astral.sh/ruff/rules/ for more information. select = [ - "E", # Error - "F", # Pyflakes - "W", # Pycodestyle - "C90", # McCabe complexity - "I", # Isort - "N", # pep8-naming - "D", # Pydocstyle - "ANN", # Pytype annotations - "S", # Bandit - "B", # Bugbear - "COM", # Commas - "C4", # Comprehensions - "ISC", # Implicit string concat - "PIE", # Unnecessary code - "T20", # Catch prints - "PYI", # validate pyi files - "Q", # Checks for quotes - "RSE", # Checks raise statements - "RET", # Checks return statements - "SLF", # Self checks - "SIM", # Simplificator - "PTH", # Pathlib checks - "ERA", # Checks for commented out code - "PL", # PyLint checks - "RUF", # Specific to Ruff checks - "UP", # Pyupgrade + "E", # Error + "F", # Pyflakes + "W", # Pycodestyle + "C90", # McCabe complexity + "I", # Isort + "N", # pep8-naming + "D", # Pydocstyle + "ANN", # Pytype annotations + "S", # Bandit + "B", # Bugbear + "COM", # Commas + "C4", # Comprehensions + "ISC", # Implicit string concat + "PIE", # Unnecessary code + "T20", # Catch prints + "PYI", # validate pyi files + "Q", # Checks for quotes + "RSE", # Checks raise statements + "RET", # Checks return statements + "SLF", # Self checks + "SIM", # Simplificator + "PTH", # Pathlib checks + "ERA", # Checks for commented out code + "PL", # PyLint checks + "RUF", # Specific to Ruff checks + "UP", # Pyupgrade ] ignore = [ - "D105", # Missing docstring in magic method - "D107", # Missing docstring in __init__ - "D212", # Multi-line docstring summary should start at the first line - "D401", # First line should be in imperative mood - "D104", # Missing docstring in public package - "D100", # Missing docstring in public module - "ANN401", # typing.Any are disallowed in `**kwargs - "PLR0913", # Too many arguments for function call - "D106", # Missing docstring in public nested class - "PYI021", # Docstrings should not be included in stubs + "D105", # Missing docstring in magic method + "D107", # Missing docstring in __init__ + "D212", # Multi-line docstring summary should start at the first line + "D401", # First line should be in imperative mood + "D104", # Missing docstring in public package + "D100", # Missing docstring in public module + "ANN401", # typing.Any are disallowed in `**kwargs + "PLR0913", # Too many arguments for function call + "D106", # Missing docstring in public nested class + "PYI021", # Docstrings should not be included in stubs ] [tool.ruff.lint.per-file-ignores] "python/tests/*" = [ - "S101", # Use of assert detected - "S301", # Use of pickle detected - "D103", # Missing docstring in public function - "SLF001", # Private member accessed - "S311", # Standard pseudo-random generators are not suitable for security/cryptographic purposes - "D101", # Missing docstring in public class + "S101", # Use of assert detected + "S301", # Use of pickle detected + "D103", # Missing docstring in public function + "SLF001", # Private member accessed + "S311", # Standard pseudo-random generators are not suitable for security/cryptographic purposes + "D101", # Missing docstring in public class ] [tool.ruff.lint.pydocstyle] diff --git a/src/js/stream.rs b/src/js/stream.rs index bcdd688..4e43090 100644 --- a/src/js/stream.rs +++ b/src/js/stream.rs @@ -565,7 +565,7 @@ pub struct StreamMessage { impl StreamMessage { pub fn from_nats_message( py: Python, - msg: async_nats::jetstream::message::StreamMessage, + msg: &async_nats::jetstream::message::StreamMessage, ) -> NatsrpyResult { let time = msg.time.to_utc(); let tz_info = PyTzInfo::utc(py)?; @@ -630,7 +630,8 @@ impl Stream { let ctx = self.stream.clone(); natsrpy_future(py, async move { let message = ctx.read().await.direct_get(sequence).await?; - let result = Python::attach(move |gil| StreamMessage::from_nats_message(gil, message))?; + let result = + Python::attach(move |gil| StreamMessage::from_nats_message(gil, &message))?; Ok(result) }) } diff --git a/src/lib.rs b/src/lib.rs index aca93a6..a22d823 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -#![warn( +#![deny( // Base lints. clippy::all, // Some pedantic lints. From 50e4eb3d699f46aa095728303de5079a4dbd3a91 Mon Sep 17 00:00:00 2001 From: Pavel Kirilin Date: Fri, 13 Mar 2026 18:55:53 +0100 Subject: [PATCH 7/8] Fixed pytest CI. --- .github/workflows/test.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dcd81aa..d60cc35 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -60,6 +60,8 @@ jobs: - uses: actions/setup-python@v6 with: python-version: 3.x + - name: Install uv + uses: astral-sh/setup-uv@v7 - id: prepare-container name: Prepare docker container run: docker compose up -d --wait @@ -69,7 +71,10 @@ jobs: - name: Build lib uses: PyO3/maturin-action@v1 with: - command: dev + command: dev --uv sccache: true - name: Run pytest - run: uv run pytest -vv -n auto python/tests + run: | + set -e + source .venv/bin/activate + pytest -vv -n auto python/tests From 8a3d1fe39edb32b0f5e78a7bf782fac17f2c3215 Mon Sep 17 00:00:00 2001 From: Pavel Kirilin Date: Fri, 13 Mar 2026 18:58:04 +0100 Subject: [PATCH 8/8] Added docker teardown to github workflow. --- .github/workflows/test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d60cc35..cfc4ba7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -78,3 +78,6 @@ jobs: set -e source .venv/bin/activate pytest -vv -n auto python/tests + - name: Teardown docker + if: always() + run: docker compose down