nest-plugins-reference 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. nest_plugins_reference-0.1.0/.gitignore +10 -0
  2. nest_plugins_reference-0.1.0/PKG-INFO +32 -0
  3. nest_plugins_reference-0.1.0/README.md +13 -0
  4. nest_plugins_reference-0.1.0/nest_plugins_reference/__init__.py +4 -0
  5. nest_plugins_reference-0.1.0/nest_plugins_reference/auth/__init__.py +1 -0
  6. nest_plugins_reference-0.1.0/nest_plugins_reference/auth/jwt_auth.py +102 -0
  7. nest_plugins_reference-0.1.0/nest_plugins_reference/comms/__init__.py +1 -0
  8. nest_plugins_reference-0.1.0/nest_plugins_reference/comms/nest_native.py +114 -0
  9. nest_plugins_reference-0.1.0/nest_plugins_reference/coordination/__init__.py +1 -0
  10. nest_plugins_reference-0.1.0/nest_plugins_reference/coordination/contract_net.py +95 -0
  11. nest_plugins_reference-0.1.0/nest_plugins_reference/datafacts/__init__.py +1 -0
  12. nest_plugins_reference-0.1.0/nest_plugins_reference/datafacts/datafacts_v1.py +78 -0
  13. nest_plugins_reference-0.1.0/nest_plugins_reference/identity/__init__.py +1 -0
  14. nest_plugins_reference-0.1.0/nest_plugins_reference/identity/did_key.py +101 -0
  15. nest_plugins_reference-0.1.0/nest_plugins_reference/memory/__init__.py +1 -0
  16. nest_plugins_reference-0.1.0/nest_plugins_reference/memory/blackboard.py +80 -0
  17. nest_plugins_reference-0.1.0/nest_plugins_reference/negotiation/__init__.py +1 -0
  18. nest_plugins_reference-0.1.0/nest_plugins_reference/negotiation/alternating_offers.py +98 -0
  19. nest_plugins_reference-0.1.0/nest_plugins_reference/payments/__init__.py +1 -0
  20. nest_plugins_reference-0.1.0/nest_plugins_reference/payments/prepaid_credits.py +99 -0
  21. nest_plugins_reference-0.1.0/nest_plugins_reference/privacy/__init__.py +1 -0
  22. nest_plugins_reference-0.1.0/nest_plugins_reference/privacy/noop.py +59 -0
  23. nest_plugins_reference-0.1.0/nest_plugins_reference/py.typed +0 -0
  24. nest_plugins_reference-0.1.0/nest_plugins_reference/registry/__init__.py +1 -0
  25. nest_plugins_reference-0.1.0/nest_plugins_reference/registry/in_memory.py +87 -0
  26. nest_plugins_reference-0.1.0/nest_plugins_reference/transport/__init__.py +1 -0
  27. nest_plugins_reference-0.1.0/nest_plugins_reference/transport/in_memory.py +111 -0
  28. nest_plugins_reference-0.1.0/nest_plugins_reference/trust/__init__.py +1 -0
  29. nest_plugins_reference-0.1.0/nest_plugins_reference/trust/score_average.py +93 -0
  30. nest_plugins_reference-0.1.0/pyproject.toml +32 -0
  31. nest_plugins_reference-0.1.0/tests/test_imports.py +9 -0
  32. nest_plugins_reference-0.1.0/tests/test_plugins.py +481 -0
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ .venv/
5
+ .ruff_cache/
6
+ .pytest_cache/
7
+ *.egg-info/
8
+ dist/
9
+ build/
10
+ uv.lock
@@ -0,0 +1,32 @@
1
+ Metadata-Version: 2.4
2
+ Name: nest-plugins-reference
3
+ Version: 0.1.0
4
+ Summary: NEST reference plugins: default implementations for all 12 layers
5
+ Project-URL: Homepage, https://github.com/mariagorskikh/nest
6
+ Project-URL: Repository, https://github.com/mariagorskikh/nest
7
+ License-Expression: Apache-2.0
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: License :: OSI Approved :: Apache Software License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
13
+ Classifier: Topic :: Software Development :: Testing
14
+ Classifier: Typing :: Typed
15
+ Requires-Python: >=3.12
16
+ Requires-Dist: nest-core
17
+ Requires-Dist: nest-sdk
18
+ Description-Content-Type: text/markdown
19
+
20
+ # nest-plugins-reference
21
+
22
+ NEST reference plugins: default implementations for all 12 layers
23
+
24
+ Part of [NEST](https://github.com/mariagorskikh/nest) (Network Environment for Swarm Testing), built at MIT Media Lab.
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ pip install nest-plugins-reference
30
+ ```
31
+
32
+ See the [main repository](https://github.com/mariagorskikh/nest) for full documentation.
@@ -0,0 +1,13 @@
1
+ # nest-plugins-reference
2
+
3
+ NEST reference plugins: default implementations for all 12 layers
4
+
5
+ Part of [NEST](https://github.com/mariagorskikh/nest) (Network Environment for Swarm Testing), built at MIT Media Lab.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install nest-plugins-reference
11
+ ```
12
+
13
+ See the [main repository](https://github.com/mariagorskikh/nest) for full documentation.
@@ -0,0 +1,4 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """NEST reference plugins: default implementations for all 12 layers."""
3
+
4
+ __version__ = "0.1.0"
@@ -0,0 +1 @@
1
+ # SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,102 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """JWT auth plugin — sign tokens with HMAC-SHA256 for simulation.
3
+
4
+ Example::
5
+
6
+ auth = JwtAuth(secret=b"my-secret")
7
+ token = await auth.issue(AgentId("a1"), ["read", "write"])
8
+ ctx = await auth.verify(token)
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import hashlib
14
+ import hmac
15
+ import json
16
+ import time
17
+
18
+ from nest_core.types import AgentId, AuthContext, Token
19
+
20
+
21
+ class JwtAuth:
22
+ """Simplified JWT-style auth using HMAC-SHA256.
23
+
24
+ Example::
25
+
26
+ auth = JwtAuth(secret=b"secret")
27
+ token = await auth.issue(AgentId("a1"), ["read"])
28
+ """
29
+
30
+ def __init__(self, secret: bytes = b"nest-default-secret", clock: float | None = None) -> None:
31
+ self._secret = secret
32
+ self._clock = clock
33
+ self._revoked: set[str] = set()
34
+
35
+ def _now(self) -> float:
36
+ if self._clock is not None:
37
+ return self._clock
38
+ return time.time()
39
+
40
+ def _sign(self, payload: str) -> str:
41
+ return hmac.new(self._secret, payload.encode(), hashlib.sha256).hexdigest()
42
+
43
+ async def issue(self, subject: AgentId, scopes: list[str]) -> Token:
44
+ """Issue a token for a subject with given scopes.
45
+
46
+ Example::
47
+
48
+ token = await auth.issue(AgentId("a1"), ["read", "write"])
49
+ """
50
+ now = self._now()
51
+ payload = json.dumps(
52
+ {
53
+ "sub": str(subject),
54
+ "scopes": scopes,
55
+ "iat": now,
56
+ "exp": now + 3600,
57
+ },
58
+ sort_keys=True,
59
+ )
60
+ sig = self._sign(payload)
61
+ return Token(f"{payload}|{sig}")
62
+
63
+ async def verify(self, token: Token) -> AuthContext:
64
+ """Verify a token and return its context.
65
+
66
+ Example::
67
+
68
+ ctx = await auth.verify(token)
69
+ assert ctx.subject == AgentId("a1")
70
+ """
71
+ raw = str(token)
72
+ if raw in self._revoked:
73
+ msg = "Token has been revoked"
74
+ raise ValueError(msg)
75
+
76
+ parts = raw.rsplit("|", 1)
77
+ if len(parts) != 2:
78
+ msg = "Invalid token format"
79
+ raise ValueError(msg)
80
+
81
+ payload_str, sig = parts
82
+ expected = self._sign(payload_str)
83
+ if not hmac.compare_digest(sig, expected):
84
+ msg = "Invalid token signature"
85
+ raise ValueError(msg)
86
+
87
+ data = json.loads(payload_str)
88
+ return AuthContext(
89
+ subject=AgentId(data["sub"]),
90
+ scopes=data["scopes"],
91
+ issued_at=data["iat"],
92
+ expires_at=data["exp"],
93
+ )
94
+
95
+ async def revoke(self, token: Token) -> None:
96
+ """Revoke a token.
97
+
98
+ Example::
99
+
100
+ await auth.revoke(token)
101
+ """
102
+ self._revoked.add(str(token))
@@ -0,0 +1 @@
1
+ # SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,114 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Nest-native communication plugin — minimal JSON envelope.
3
+
4
+ Example::
5
+
6
+ comms = NestNativeComms(AgentId("a1"), transport, registry)
7
+ raw = comms.serialize(msg)
8
+ msg2 = comms.deserialize(raw)
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import base64
14
+ import json
15
+ from typing import Any
16
+
17
+ from nest_core.types import (
18
+ AgentCard,
19
+ AgentId,
20
+ Message,
21
+ MessageId,
22
+ Query,
23
+ Response,
24
+ )
25
+
26
+
27
+ class NestNativeComms:
28
+ """Minimal JSON-based communication protocol.
29
+
30
+ Example::
31
+
32
+ comms = NestNativeComms(AgentId("a1"), transport=t, registry=r)
33
+ resp = await comms.send(AgentId("a2"), msg)
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ agent_id: AgentId,
39
+ transport: Any = None,
40
+ registry: Any = None,
41
+ ) -> None:
42
+ self._agent_id = agent_id
43
+ self._transport = transport
44
+ self._registry = registry
45
+
46
+ def serialize(self, msg: Message) -> bytes:
47
+ """Serialize a Message to JSON bytes.
48
+
49
+ Example::
50
+
51
+ raw = comms.serialize(msg)
52
+ """
53
+ data = {
54
+ "id": str(msg.id),
55
+ "sender": str(msg.sender),
56
+ "receiver": str(msg.receiver),
57
+ "payload": base64.b64encode(msg.payload).decode("ascii"),
58
+ "correlation_id": str(msg.correlation_id) if msg.correlation_id else None,
59
+ "timestamp": msg.timestamp,
60
+ "metadata": msg.metadata,
61
+ }
62
+ return json.dumps(data, sort_keys=True).encode("utf-8")
63
+
64
+ def deserialize(self, raw: bytes) -> Message:
65
+ """Deserialize JSON bytes back to a Message.
66
+
67
+ Example::
68
+
69
+ msg = comms.deserialize(raw)
70
+ """
71
+ data = json.loads(raw)
72
+ return Message(
73
+ id=MessageId(data["id"]),
74
+ sender=AgentId(data["sender"]),
75
+ receiver=AgentId(data["receiver"]),
76
+ payload=base64.b64decode(data["payload"]),
77
+ correlation_id=data.get("correlation_id"),
78
+ timestamp=data.get("timestamp"),
79
+ metadata=data.get("metadata", {}),
80
+ )
81
+
82
+ async def send(self, to: AgentId, msg: Message) -> Response:
83
+ """Send a message via the transport layer.
84
+
85
+ Example::
86
+
87
+ resp = await comms.send(AgentId("a2"), msg)
88
+ """
89
+ raw = self.serialize(msg)
90
+ if self._transport is not None:
91
+ await self._transport.send(to, raw)
92
+ return Response(success=True)
93
+
94
+ async def advertise(self, card: AgentCard) -> None:
95
+ """Advertise an agent card to the registry.
96
+
97
+ Example::
98
+
99
+ await comms.advertise(my_card)
100
+ """
101
+ if self._registry is not None:
102
+ await self._registry.register(card)
103
+
104
+ async def discover(self, query: Query) -> list[AgentCard]:
105
+ """Discover agents via the registry.
106
+
107
+ Example::
108
+
109
+ cards = await comms.discover(Query(capabilities=["sell"]))
110
+ """
111
+ if self._registry is not None:
112
+ result: list[AgentCard] = await self._registry.lookup(query)
113
+ return result
114
+ return []
@@ -0,0 +1 @@
1
+ # SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,95 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Contract Net coordination plugin — classic FIPA Contract Net Protocol.
3
+
4
+ The Round object is shared between manager and workers. Bids are stored
5
+ in the round's metadata so any party can resolve them.
6
+
7
+ Example::
8
+
9
+ coord = ContractNet(AgentId("manager"))
10
+ rnd = await coord.propose(task)
11
+ bid = await coord.participate(rnd)
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import uuid
17
+
18
+ from nest_core.types import (
19
+ AgentId,
20
+ Bid,
21
+ Money,
22
+ Outcome,
23
+ Round,
24
+ Task,
25
+ Vote,
26
+ )
27
+
28
+
29
+ class ContractNet:
30
+ """FIPA Contract Net Protocol implementation.
31
+
32
+ Example::
33
+
34
+ coord = ContractNet(AgentId("a1"))
35
+ rnd = await coord.propose(Task(id="t1", description="work"))
36
+ """
37
+
38
+ def __init__(self, agent_id: AgentId) -> None:
39
+ self._agent_id = agent_id
40
+
41
+ async def propose(self, task: Task) -> Round:
42
+ """Propose a task for bidding.
43
+
44
+ Example::
45
+
46
+ rnd = await coord.propose(task)
47
+ """
48
+ round_id = str(uuid.uuid4())
49
+ rnd = Round(
50
+ id=round_id,
51
+ task=task,
52
+ participants=[],
53
+ metadata={"bids": []},
54
+ )
55
+ return rnd
56
+
57
+ async def participate(self, round: Round) -> Vote | Bid:
58
+ """Submit a bid for a round.
59
+
60
+ Example::
61
+
62
+ bid = await coord.participate(rnd)
63
+ """
64
+ bid = Bid(
65
+ bidder=self._agent_id,
66
+ round_id=round.id,
67
+ amount=Money(amount=1),
68
+ )
69
+ bids: list[dict[str, object]] = round.metadata.setdefault("bids", [])
70
+ bids.append({"bidder": str(bid.bidder), "amount": bid.amount.amount})
71
+ if self._agent_id not in round.participants:
72
+ round.participants.append(self._agent_id)
73
+ return bid
74
+
75
+ async def resolve(self, round: Round) -> Outcome:
76
+ """Resolve a round by selecting the lowest bidder.
77
+
78
+ Example::
79
+
80
+ outcome = await coord.resolve(rnd)
81
+ """
82
+ bids: list[dict[str, object]] = round.metadata.get("bids", [])
83
+ winner: AgentId | None = None
84
+ if bids:
85
+ best = min(bids, key=lambda b: int(str(b["amount"])))
86
+ winner = AgentId(str(best["bidder"]))
87
+ return Outcome(round_id=round.id, winner=winner, task=round.task)
88
+
89
+ async def commit(self, outcome: Outcome) -> None:
90
+ """Commit to an outcome.
91
+
92
+ Example::
93
+
94
+ await coord.commit(outcome)
95
+ """
@@ -0,0 +1 @@
1
+ # SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,78 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """DataFacts v1 plugin — dataset metadata registry.
3
+
4
+ Example::
5
+
6
+ df = DataFactsV1()
7
+ url = await df.publish(DatasetMetadata(name="weather", owner=AgentId("a1")))
8
+ meta = await df.fetch(url)
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import time
14
+
15
+ from nest_core.types import AccessGrant, AgentId, DataFactsUrl, DatasetMetadata
16
+
17
+
18
+ class DataFactsV1:
19
+ """In-memory DataFacts metadata registry.
20
+
21
+ Example::
22
+
23
+ df = DataFactsV1()
24
+ url = await df.publish(meta)
25
+ """
26
+
27
+ def __init__(self) -> None:
28
+ self._datasets: dict[DataFactsUrl, DatasetMetadata] = {}
29
+ self._grants: dict[DataFactsUrl, list[AccessGrant]] = {}
30
+ self._timestamps: dict[DataFactsUrl, float] = {}
31
+
32
+ async def publish(self, dataset: DatasetMetadata) -> DataFactsUrl:
33
+ """Publish dataset metadata and return its URL.
34
+
35
+ Example::
36
+
37
+ url = await df.publish(DatasetMetadata(name="weather", owner=AgentId("a1")))
38
+ """
39
+ url = DataFactsUrl(f"df://{dataset.name}")
40
+ self._datasets[url] = dataset
41
+ self._timestamps[url] = time.time()
42
+ return url
43
+
44
+ async def fetch(self, url: DataFactsUrl) -> DatasetMetadata:
45
+ """Fetch metadata for a dataset URL.
46
+
47
+ Example::
48
+
49
+ meta = await df.fetch(DataFactsUrl("df://weather"))
50
+ """
51
+ meta = self._datasets.get(url)
52
+ if meta is None:
53
+ msg = f"Dataset not found: {url}"
54
+ raise KeyError(msg)
55
+ return meta
56
+
57
+ async def request_access(self, url: DataFactsUrl, requester: AgentId) -> AccessGrant:
58
+ """Request access to a dataset (always grants in v1).
59
+
60
+ Example::
61
+
62
+ grant = await df.request_access(url, AgentId("a2"))
63
+ """
64
+ grant = AccessGrant(url=url, grantee=requester, tier="read")
65
+ self._grants.setdefault(url, []).append(grant)
66
+ return grant
67
+
68
+ async def verify_freshness(self, url: DataFactsUrl) -> bool:
69
+ """Check if a dataset was published within the last hour.
70
+
71
+ Example::
72
+
73
+ fresh = await df.verify_freshness(url)
74
+ """
75
+ ts = self._timestamps.get(url)
76
+ if ts is None:
77
+ return False
78
+ return (time.time() - ts) < 3600
@@ -0,0 +1 @@
1
+ # SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,101 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """DID:key identity plugin — Ed25519 key-based identity.
3
+
4
+ Uses hashlib for deterministic key derivation in simulation (no real crypto
5
+ needed for testing). For real deployments, swap to a proper Ed25519 library.
6
+
7
+ Example::
8
+
9
+ identity = DidKeyIdentity(AgentId("a1"), seed=b"secret")
10
+ sig = identity.sign(b"payload")
11
+ ok = identity.verify(b"payload", sig, AgentId("a1"))
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import hashlib
17
+ import hmac
18
+
19
+ from nest_core.types import AgentId, AgentIdentity, Signature
20
+
21
+
22
+ class DidKeyIdentity:
23
+ """Ed25519-style identity using HMAC-SHA256 for simulation.
24
+
25
+ Example::
26
+
27
+ ident = DidKeyIdentity(AgentId("a1"), seed=b"seed")
28
+ sig = ident.sign(b"hello")
29
+ """
30
+
31
+ def __init__(self, agent_id: AgentId, seed: bytes = b"") -> None:
32
+ self._agent_id = agent_id
33
+ self._seed = seed
34
+ key_material = hashlib.sha256(seed + agent_id.encode()).digest()
35
+ self._private_key = key_material
36
+ self._public_key = hashlib.sha256(key_material).digest()
37
+ self._known_keys: dict[AgentId, bytes] = {agent_id: self._public_key}
38
+ self._private_keys: dict[AgentId, bytes] = {agent_id: self._private_key}
39
+
40
+ def register_peer(
41
+ self,
42
+ agent_id: AgentId,
43
+ public_key: bytes,
44
+ private_key: bytes | None = None,
45
+ ) -> None:
46
+ """Register a peer's public key (and optionally private key) for verification.
47
+
48
+ Example::
49
+
50
+ ident.register_peer(AgentId("a2"), peer_pk)
51
+ """
52
+ self._known_keys[agent_id] = public_key
53
+ if private_key is not None:
54
+ self._private_keys[agent_id] = private_key
55
+
56
+ @property
57
+ def public_key(self) -> bytes:
58
+ """This agent's public key.
59
+
60
+ Example::
61
+
62
+ pk = ident.public_key
63
+ """
64
+ return self._public_key
65
+
66
+ def sign(self, payload: bytes) -> Signature:
67
+ """Sign a payload with this agent's private key.
68
+
69
+ Example::
70
+
71
+ sig = ident.sign(b"data")
72
+ """
73
+ sig_bytes = hmac.new(self._private_key, payload, hashlib.sha256).digest()
74
+ return Signature(signer=self._agent_id, value=sig_bytes, algorithm="hmac-sha256")
75
+
76
+ def verify(self, payload: bytes, sig: Signature, agent: AgentId) -> bool:
77
+ """Verify a signature from a given agent.
78
+
79
+ Example::
80
+
81
+ ok = ident.verify(b"data", sig, AgentId("a1"))
82
+ """
83
+ private_key = self._private_keys.get(agent)
84
+ if private_key is None:
85
+ return False
86
+ expected = hmac.new(private_key, payload, hashlib.sha256).digest()
87
+ return hmac.compare_digest(sig.value, expected)
88
+
89
+ async def resolve(self, agent: AgentId) -> AgentIdentity:
90
+ """Resolve an agent ID to its identity record.
91
+
92
+ Example::
93
+
94
+ info = await ident.resolve(AgentId("a1"))
95
+ """
96
+ pk = self._known_keys.get(agent, b"")
97
+ return AgentIdentity(
98
+ agent_id=agent,
99
+ public_key=pk,
100
+ method="did:key",
101
+ )
@@ -0,0 +1 @@
1
+ # SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,80 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Blackboard memory plugin — shared key-value store.
3
+
4
+ Example::
5
+
6
+ mem = Blackboard()
7
+ await mem.write("key", b"value")
8
+ val = await mem.read("key")
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import asyncio
14
+ from collections.abc import AsyncIterator
15
+
16
+
17
+ class Blackboard:
18
+ """Shared key-value blackboard for agent state.
19
+
20
+ Example::
21
+
22
+ bb = Blackboard()
23
+ await bb.write("counter", b"42")
24
+ val = await bb.read("counter")
25
+ """
26
+
27
+ def __init__(self) -> None:
28
+ self._store: dict[str, bytes] = {}
29
+ self._subscribers: dict[str, list[asyncio.Queue[bytes]]] = {}
30
+
31
+ async def read(self, key: str) -> bytes | None:
32
+ """Read a value by key.
33
+
34
+ Example::
35
+
36
+ val = await bb.read("counter")
37
+ """
38
+ return self._store.get(key)
39
+
40
+ async def write(self, key: str, value: bytes) -> None:
41
+ """Write a value for a key, notifying subscribers.
42
+
43
+ Example::
44
+
45
+ await bb.write("counter", b"42")
46
+ """
47
+ self._store[key] = value
48
+ for q in self._subscribers.get(key, []):
49
+ await q.put(value)
50
+
51
+ async def subscribe(self, key: str) -> AsyncIterator[bytes]:
52
+ """Subscribe to changes for a key.
53
+
54
+ Example::
55
+
56
+ async for val in bb.subscribe("counter"):
57
+ print(val)
58
+ """
59
+ q: asyncio.Queue[bytes] = asyncio.Queue()
60
+ self._subscribers.setdefault(key, []).append(q)
61
+ try:
62
+ while True:
63
+ yield await q.get()
64
+ finally:
65
+ self._subscribers[key].remove(q)
66
+
67
+ async def cas(self, key: str, expected: bytes, new: bytes) -> bool:
68
+ """Compare-and-swap: update only if current value matches expected.
69
+
70
+ Example::
71
+
72
+ ok = await bb.cas("counter", b"42", b"43")
73
+ """
74
+ current = self._store.get(key)
75
+ if current == expected:
76
+ self._store[key] = new
77
+ for q in self._subscribers.get(key, []):
78
+ await q.put(new)
79
+ return True
80
+ return False
@@ -0,0 +1 @@
1
+ # SPDX-License-Identifier: Apache-2.0