Skip to content

Commit eaa53e2

Browse files
committed
Merge main into multiplex-request-reply-over-a-single-subscription
2 parents d97c9a1 + ee9e99a commit eaa53e2

File tree

14 files changed

+291
-97
lines changed

14 files changed

+291
-97
lines changed

.github/workflows/check.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,24 @@ jobs:
9292

9393
- name: Run type check
9494
run: uv run ty check nats-core nats-server
95+
96+
build:
97+
runs-on: ubuntu-latest
98+
name: build
99+
strategy:
100+
matrix:
101+
package: [nats-py, nats-core, nats-server, nats-jetstream]
102+
steps:
103+
- name: Check out repository
104+
uses: actions/checkout@v5
105+
106+
- name: Set up Python
107+
uses: actions/setup-python@v6
108+
with:
109+
python-version: "3.13"
110+
111+
- name: Install uv
112+
uses: astral-sh/setup-uv@v6
113+
114+
- name: Build package
115+
run: uv build --package ${{ matrix.package }}

nats-core/pyproject.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[build-system]
2-
requires = ["setuptools>=61.0"]
3-
build-backend = "setuptools.build_meta"
2+
requires = ["uv_build>=0.9.28,<0.10.0"]
3+
build-backend = "uv_build"
44

55
[project]
66
name = "nats-core"
@@ -32,8 +32,8 @@ Documentation = "https://github.com/nats-io/nats.py"
3232
Issues = "https://github.com/nats-io/nats.py/issues"
3333
Source = "https://github.com/nats-io/nats.py"
3434

35-
[tool.setuptools.packages.find]
36-
where = ["src"]
35+
[tool.uv.build-backend]
36+
module-name = "nats.client"
3737

3838
[dependency-groups]
3939
dev = [

nats-core/src/nats/client/__init__.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -913,6 +913,25 @@ async def _force_flush(self) -> None:
913913
self._pending_messages.clear()
914914
self._pending_bytes = 0
915915

916+
async def _ping(self) -> None:
917+
"""Send a PING to the server."""
918+
self._pong_waker.clear()
919+
logger.debug("->> PING")
920+
self._pings_outstanding += 1
921+
self._last_ping_sent = asyncio.get_running_loop().time()
922+
await self._connection.write(encode_ping())
923+
924+
async def rtt(self, timeout: float | None = None) -> float:
925+
"""Calculate the round trip time between the client and server in seconds."""
926+
if self._status == ClientStatus.CLOSED:
927+
raise ConnectionError("connection is closed")
928+
929+
loop = asyncio.get_running_loop()
930+
start = loop.time()
931+
await self._ping()
932+
await asyncio.wait_for(self._pong_waker.wait(), timeout=timeout)
933+
return loop.time() - start
934+
916935
async def flush(self, timeout: float | None = None) -> None:
917936
"""Flush pending messages with optional timeout."""
918937
if self._status == ClientStatus.CLOSED:
@@ -922,11 +941,7 @@ async def flush(self, timeout: float | None = None) -> None:
922941
if self._pending_messages:
923942
await self._force_flush()
924943

925-
self._pong_waker.clear()
926-
logger.debug("->> PING")
927-
self._pings_outstanding += 1
928-
self._last_ping_sent = asyncio.get_event_loop().time()
929-
await self._connection.write(encode_ping())
944+
await self._ping()
930945
try:
931946
await asyncio.wait_for(self._pong_waker.wait(), timeout=timeout)
932947
except asyncio.TimeoutError:

nats-core/tests/test_client.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -858,6 +858,12 @@ async def responder():
858858

859859

860860
@pytest.mark.asyncio
861+
async def test_rtt(client):
862+
"""Test that rtt returns a positive round trip time."""
863+
rtt = await client.rtt()
864+
assert rtt > 0
865+
866+
861867
async def test_flush_ensures_message_delivery(client):
862868
"""Test that flush ensures all pending messages are delivered."""
863869
test_subject = f"test.flush.{uuid.uuid4()}"

nats-jetstream/pyproject.toml

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[build-system]
2-
requires = ["setuptools>=61.0"]
3-
build-backend = "setuptools.build_meta"
2+
requires = ["uv_build>=0.9.28,<0.10.0"]
3+
build-backend = "uv_build"
44

55
[project]
66
name = "nats-jetstream"
@@ -10,9 +10,7 @@ readme = "README.md"
1010
requires-python = ">=3.11"
1111
license = "MIT"
1212
keywords = ["nats", "messaging", "jetstream"]
13-
authors = [
14-
{ name = "Casper Beyer", email = "casper@synadia.com" },
15-
]
13+
authors = [{ name = "Casper Beyer", email = "casper@synadia.com" }]
1614
classifiers = [
1715
"Development Status :: 4 - Beta",
1816
"Programming Language :: Python",
@@ -22,14 +20,13 @@ classifiers = [
2220
"Programming Language :: Python :: Implementation :: CPython",
2321
"Programming Language :: Python :: Implementation :: PyPy",
2422
]
25-
dependencies = [
26-
"nats-core",
27-
]
23+
dependencies = ["nats-core"]
2824

2925
[dependency-groups]
30-
dev = [
31-
"nats-server",
32-
]
26+
dev = ["nats-server"]
27+
28+
[tool.uv.build-backend]
29+
module-name = "nats.jetstream"
3330

3431
[tool.uv.sources]
3532
nats-core = { workspace = true }
@@ -40,8 +37,6 @@ Documentation = "https://github.com/nats-io/nats.py"
4037
Issues = "https://github.com/nats-io/nats.py/issues"
4138
Source = "https://github.com/nats-io/nats.py"
4239

43-
[tool.setuptools.packages.find]
44-
where = ["src"]
4540

4641
[tool.pytest.ini_options]
4742
asyncio_default_fixture_loop_scope = "function"

nats-server/pyproject.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[build-system]
2-
requires = ["setuptools>=61.0"]
3-
build-backend = "setuptools.build_meta"
2+
requires = ["uv_build>=0.9.28,<0.10.0"]
3+
build-backend = "uv_build"
44

55
[project]
66
name = "nats-server"
@@ -30,8 +30,8 @@ Documentation = "https://github.com/nats-io/nats.py"
3030
Issues = "https://github.com/nats-io/nats.py/issues"
3131
Source = "https://github.com/nats-io/nats.py"
3232

33-
[tool.setuptools.packages.find]
34-
where = ["src"]
33+
[tool.uv.build-backend]
34+
module-name = "nats.server"
3535

3636
[tool.pytest.ini_options]
3737
asyncio_default_fixture_loop_scope = "function"

nats/examples/graceful_shutdown.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import asyncio
2+
import signal
3+
4+
import nats
5+
6+
7+
async def main():
8+
# Connect to NATS
9+
nc = await nats.connect("nats://127.0.0.1:4222")
10+
11+
# Use an event to coordinate graceful shutdown
12+
shutdown = asyncio.Event()
13+
14+
loop = asyncio.get_running_loop()
15+
for sig in (signal.SIGTERM, signal.SIGINT):
16+
loop.add_signal_handler(sig, shutdown.set)
17+
18+
async def handler(msg):
19+
print(f"Received: {msg.data.decode()} on {msg.subject}")
20+
21+
await nc.subscribe("foo", cb=handler)
22+
23+
# Publish messages until shutdown is signalled
24+
while not shutdown.is_set():
25+
await nc.publish("foo", b"bar")
26+
try:
27+
await asyncio.wait_for(shutdown.wait(), timeout=1)
28+
except asyncio.TimeoutError:
29+
pass
30+
31+
# Drain gracefully closes the connection after flushing
32+
await nc.drain()
33+
print("Shutdown complete")
34+
35+
36+
if __name__ == "__main__":
37+
asyncio.run(main())

nats/pyproject.toml

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[build-system]
2-
requires = ["setuptools>=68.0"]
3-
build-backend = "setuptools.build_meta"
2+
requires = ["uv_build>=0.9.28,<0.10.0"]
3+
build-backend = "uv_build"
44

55
[project]
66
name = "nats-py"
@@ -35,12 +35,6 @@ nkeys = ['nkeys']
3535
aiohttp = ['aiohttp']
3636
fast_parse = ['fast-mail-parser']
3737

38-
[tool.setuptools]
39-
zip-safe = true
40-
package-dir = {"" = "src"}
38+
[tool.uv.build-backend]
39+
module-name = "nats"
4140

42-
[tool.setuptools.packages.find]
43-
where = ["src"]
44-
include = ["nats*"]
45-
exclude = ["scripts", "tests"]
46-
namespaces = false

nats/src/nats/js/api.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ def _parse_utc_iso(time_string: str) -> datetime.datetime:
117117
if "." in s:
118118
date_part, frac_tz = s.split(".", 1)
119119
frac, tz = frac_tz.split("+")
120-
frac = frac[:6] # keep only microseconds
120+
frac = frac[:6].ljust(6, "0") # normalize to exactly 6 digits
121121
s = f"{date_part}.{frac}+{tz}"
122122
return datetime.datetime.fromisoformat(s).astimezone(datetime.timezone.utc)
123123

@@ -446,6 +446,7 @@ class StreamInfo(Base):
446446
sources: Optional[List[StreamSourceInfo]] = None
447447
cluster: Optional[ClusterInfo] = None
448448
did_create: Optional[bool] = None
449+
created: Optional[datetime.datetime] = None
449450

450451
@classmethod
451452
def from_response(cls, resp: Dict[str, Any]):
@@ -454,6 +455,8 @@ def from_response(cls, resp: Dict[str, Any]):
454455
cls._convert(resp, "mirror", StreamSourceInfo)
455456
cls._convert(resp, "sources", StreamSourceInfo)
456457
cls._convert(resp, "cluster", ClusterInfo)
458+
459+
cls._convert_utc_iso(resp, "created")
457460
return super().from_response(resp)
458461

459462

nats/src/nats/js/kv.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,7 @@ async def watch_updates(msg):
522522

523523
watcher._sub = await self._js.subscribe(
524524
subject,
525+
stream=self._stream,
525526
cb=watch_updates,
526527
ordered_consumer=True,
527528
deliver_policy=deliver_policy,

0 commit comments

Comments
 (0)