yellowstone-fumarole-client 0.1.0rc2__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 (21) hide show
  1. yellowstone_fumarole_client-0.1.0rc2/PKG-INFO +115 -0
  2. yellowstone_fumarole_client-0.1.0rc2/README.md +94 -0
  3. yellowstone_fumarole_client-0.1.0rc2/pyproject.toml +36 -0
  4. yellowstone_fumarole_client-0.1.0rc2/yellowstone_fumarole_client/__init__.py +297 -0
  5. yellowstone_fumarole_client-0.1.0rc2/yellowstone_fumarole_client/config.py +26 -0
  6. yellowstone_fumarole_client-0.1.0rc2/yellowstone_fumarole_client/grpc_connectivity.py +197 -0
  7. yellowstone_fumarole_client-0.1.0rc2/yellowstone_fumarole_client/runtime/__init__.py +0 -0
  8. yellowstone_fumarole_client-0.1.0rc2/yellowstone_fumarole_client/runtime/aio.py +525 -0
  9. yellowstone_fumarole_client-0.1.0rc2/yellowstone_fumarole_client/runtime/state_machine.py +324 -0
  10. yellowstone_fumarole_client-0.1.0rc2/yellowstone_fumarole_client/utils/__init__.py +0 -0
  11. yellowstone_fumarole_client-0.1.0rc2/yellowstone_fumarole_client/utils/aio.py +30 -0
  12. yellowstone_fumarole_client-0.1.0rc2/yellowstone_fumarole_proto/__init__.py +0 -0
  13. yellowstone_fumarole_client-0.1.0rc2/yellowstone_fumarole_proto/fumarole_v2_pb2.py +122 -0
  14. yellowstone_fumarole_client-0.1.0rc2/yellowstone_fumarole_proto/fumarole_v2_pb2.pyi +328 -0
  15. yellowstone_fumarole_client-0.1.0rc2/yellowstone_fumarole_proto/fumarole_v2_pb2_grpc.py +400 -0
  16. yellowstone_fumarole_client-0.1.0rc2/yellowstone_fumarole_proto/geyser_pb2.py +144 -0
  17. yellowstone_fumarole_client-0.1.0rc2/yellowstone_fumarole_proto/geyser_pb2.pyi +501 -0
  18. yellowstone_fumarole_client-0.1.0rc2/yellowstone_fumarole_proto/geyser_pb2_grpc.py +355 -0
  19. yellowstone_fumarole_client-0.1.0rc2/yellowstone_fumarole_proto/solana_storage_pb2.py +75 -0
  20. yellowstone_fumarole_client-0.1.0rc2/yellowstone_fumarole_proto/solana_storage_pb2.pyi +238 -0
  21. yellowstone_fumarole_client-0.1.0rc2/yellowstone_fumarole_proto/solana_storage_pb2_grpc.py +24 -0
@@ -0,0 +1,115 @@
1
+ Metadata-Version: 2.1
2
+ Name: yellowstone-fumarole-client
3
+ Version: 0.1.0rc2
4
+ Summary: Yellowstone Fumarole Python Client
5
+ Home-page: https://github.com/rpcpool/yellowstone-fumarole
6
+ Author: Louis-Vincent
7
+ Author-email: louis-vincent@triton.one
8
+ Requires-Python: >=3.13,<4.0
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Requires-Dist: asyncio (>=3.4.3,<4.0.0)
12
+ Requires-Dist: base58 (>=2.1.1,<3.0.0)
13
+ Requires-Dist: click (>=8.1.7,<9.0.0)
14
+ Requires-Dist: grpcio (>=1.71.1,<2.0.0)
15
+ Requires-Dist: protobuf (>=5.29.1,<6.0.0)
16
+ Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
17
+ Requires-Dist: tabulate (>=0.9.0,<0.10.0)
18
+ Requires-Dist: toml (>=0.10.2,<0.11.0)
19
+ Project-URL: Repository, https://github.com/rpcpool/yellowstone-fumarole
20
+ Description-Content-Type: text/markdown
21
+
22
+ # Fumarole Python SDK
23
+
24
+ This module contains Fumarole SDK for `python` programming language.
25
+
26
+ ## Configuration
27
+
28
+ ```yaml
29
+ endpoint: <"https://fumarole.endpoint.rpcpool.com">
30
+ x-token: <YOUR X-TOKEN secret here>
31
+ ```
32
+
33
+ ## Manage consumer group
34
+
35
+ Refer to [fume CLI](https://crates.io/crates/yellowstone-fumarole-cli) to manage your consumer groups.
36
+
37
+ ## Examples
38
+
39
+ ```python
40
+
41
+ from typing import Optional
42
+ import uuid
43
+ import asyncio
44
+ import logging
45
+ from os import environ
46
+ from collections import defaultdict
47
+ from yellowstone_fumarole_client.config import FumaroleConfig
48
+ from yellowstone_fumarole_client import FumaroleClient
49
+ from yellowstone_fumarole_proto.fumarole_v2_pb2 import CreateConsumerGroupRequest
50
+ from yellowstone_fumarole_proto.geyser_pb2 import (
51
+ SubscribeRequest,
52
+ SubscribeRequestFilterAccounts,
53
+ SubscribeRequestFilterTransactions,
54
+ SubscribeRequestFilterBlocksMeta,
55
+ SubscribeRequestFilterEntry,
56
+ SubscribeRequestFilterSlots,
57
+ )
58
+ from yellowstone_fumarole_proto.geyser_pb2 import (
59
+ SubscribeUpdate,
60
+ SubscribeUpdateTransaction,
61
+ SubscribeUpdateBlockMeta,
62
+ SubscribeUpdateAccount,
63
+ SubscribeUpdateEntry,
64
+ SubscribeUpdateSlot,
65
+ )
66
+
67
+ async def dragonsmouth_like_session(fumarole_config):
68
+ with open("~/.fumarole/config.yaml") as f:
69
+ fumarole_config = FumaroleConfig.from_yaml(f)
70
+
71
+ client: FumaroleClient = await FumaroleClient.connect(fumarole_config)
72
+ await client.delete_all_consumer_groups()
73
+
74
+ # --- This is optional ---
75
+ resp = await client.create_consumer_group(
76
+ CreateConsumerGroupRequest(
77
+ consumer_group_name="test",
78
+ )
79
+ )
80
+ assert resp.consumer_group_id, "Failed to create consumer group"
81
+ # --- END OF OPTIONAL BLOCK ---
82
+
83
+ session = await client.dragonsmouth_subscribe(
84
+ consumer_group_name="test",
85
+ request=SubscribeRequest(
86
+ # accounts={"fumarole": SubscribeRequestFilterAccounts()},
87
+ transactions={"fumarole": SubscribeRequestFilterTransactions()},
88
+ blocks_meta={"fumarole": SubscribeRequestFilterBlocksMeta()},
89
+ entry={"fumarole": SubscribeRequestFilterEntry()},
90
+ slots={"fumarole": SubscribeRequestFilterSlots()},
91
+ ),
92
+ )
93
+ dragonsmouth_source = session.source
94
+ handle = session.fumarole_handle
95
+ block_map = defaultdict(BlockConstruction)
96
+ while True:
97
+ tasks = [asyncio.create_task(dragonsmouth_source.get()), handle]
98
+ done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
99
+ for t in done:
100
+ if tasks[0] == t:
101
+ result: SubscribeUpdate = t.result()
102
+ if result.HasField("block_meta"):
103
+ block_meta: SubscribeUpdateBlockMeta = result.block_meta
104
+ elif result.HasField("transaction"):
105
+ tx: SubscribeUpdateTransaction = result.transaction
106
+ elif result.HasField("account"):
107
+ account: SubscribeUpdateAccount = result.account
108
+ elif result.HasField("entry"):
109
+ entry: SubscribeUpdateEntry = result.entry
110
+ elif result.HasField("slot"):
111
+ result: SubscribeUpdateSlot = result.slot
112
+ else:
113
+ result = t.result()
114
+ raise RuntimeError("failed to get dragonsmouth source: %s" % result)
115
+ ```
@@ -0,0 +1,94 @@
1
+ # Fumarole Python SDK
2
+
3
+ This module contains Fumarole SDK for `python` programming language.
4
+
5
+ ## Configuration
6
+
7
+ ```yaml
8
+ endpoint: <"https://fumarole.endpoint.rpcpool.com">
9
+ x-token: <YOUR X-TOKEN secret here>
10
+ ```
11
+
12
+ ## Manage consumer group
13
+
14
+ Refer to [fume CLI](https://crates.io/crates/yellowstone-fumarole-cli) to manage your consumer groups.
15
+
16
+ ## Examples
17
+
18
+ ```python
19
+
20
+ from typing import Optional
21
+ import uuid
22
+ import asyncio
23
+ import logging
24
+ from os import environ
25
+ from collections import defaultdict
26
+ from yellowstone_fumarole_client.config import FumaroleConfig
27
+ from yellowstone_fumarole_client import FumaroleClient
28
+ from yellowstone_fumarole_proto.fumarole_v2_pb2 import CreateConsumerGroupRequest
29
+ from yellowstone_fumarole_proto.geyser_pb2 import (
30
+ SubscribeRequest,
31
+ SubscribeRequestFilterAccounts,
32
+ SubscribeRequestFilterTransactions,
33
+ SubscribeRequestFilterBlocksMeta,
34
+ SubscribeRequestFilterEntry,
35
+ SubscribeRequestFilterSlots,
36
+ )
37
+ from yellowstone_fumarole_proto.geyser_pb2 import (
38
+ SubscribeUpdate,
39
+ SubscribeUpdateTransaction,
40
+ SubscribeUpdateBlockMeta,
41
+ SubscribeUpdateAccount,
42
+ SubscribeUpdateEntry,
43
+ SubscribeUpdateSlot,
44
+ )
45
+
46
+ async def dragonsmouth_like_session(fumarole_config):
47
+ with open("~/.fumarole/config.yaml") as f:
48
+ fumarole_config = FumaroleConfig.from_yaml(f)
49
+
50
+ client: FumaroleClient = await FumaroleClient.connect(fumarole_config)
51
+ await client.delete_all_consumer_groups()
52
+
53
+ # --- This is optional ---
54
+ resp = await client.create_consumer_group(
55
+ CreateConsumerGroupRequest(
56
+ consumer_group_name="test",
57
+ )
58
+ )
59
+ assert resp.consumer_group_id, "Failed to create consumer group"
60
+ # --- END OF OPTIONAL BLOCK ---
61
+
62
+ session = await client.dragonsmouth_subscribe(
63
+ consumer_group_name="test",
64
+ request=SubscribeRequest(
65
+ # accounts={"fumarole": SubscribeRequestFilterAccounts()},
66
+ transactions={"fumarole": SubscribeRequestFilterTransactions()},
67
+ blocks_meta={"fumarole": SubscribeRequestFilterBlocksMeta()},
68
+ entry={"fumarole": SubscribeRequestFilterEntry()},
69
+ slots={"fumarole": SubscribeRequestFilterSlots()},
70
+ ),
71
+ )
72
+ dragonsmouth_source = session.source
73
+ handle = session.fumarole_handle
74
+ block_map = defaultdict(BlockConstruction)
75
+ while True:
76
+ tasks = [asyncio.create_task(dragonsmouth_source.get()), handle]
77
+ done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
78
+ for t in done:
79
+ if tasks[0] == t:
80
+ result: SubscribeUpdate = t.result()
81
+ if result.HasField("block_meta"):
82
+ block_meta: SubscribeUpdateBlockMeta = result.block_meta
83
+ elif result.HasField("transaction"):
84
+ tx: SubscribeUpdateTransaction = result.transaction
85
+ elif result.HasField("account"):
86
+ account: SubscribeUpdateAccount = result.account
87
+ elif result.HasField("entry"):
88
+ entry: SubscribeUpdateEntry = result.entry
89
+ elif result.HasField("slot"):
90
+ result: SubscribeUpdateSlot = result.slot
91
+ else:
92
+ result = t.result()
93
+ raise RuntimeError("failed to get dragonsmouth source: %s" % result)
94
+ ```
@@ -0,0 +1,36 @@
1
+ [tool.poetry]
2
+ name = "yellowstone-fumarole-client"
3
+ version = "0.1.0rc2"
4
+ homepage = "https://github.com/rpcpool/yellowstone-fumarole"
5
+ repository = "https://github.com/rpcpool/yellowstone-fumarole"
6
+ description = "Yellowstone Fumarole Python Client"
7
+ authors = ["Louis-Vincent <louis-vincent@triton.one>", "Triton One <help@triton.one>"]
8
+ readme = "README.md"
9
+
10
+ packages = [
11
+ { include = "yellowstone_fumarole_proto" },
12
+ { include = "yellowstone_fumarole_client" },
13
+ ]
14
+
15
+ [tool.poetry.dependencies]
16
+ python = "^3.13"
17
+ click = "^8.1.7"
18
+ grpcio = "^1.71.1"
19
+ protobuf = "^5.29.1"
20
+ toml = "^0.10.2"
21
+ base58 = "^2.1.1"
22
+ tabulate = "^0.9.0"
23
+ asyncio = "^3.4.3"
24
+ pyyaml = "^6.0.2"
25
+
26
+ [tool.poetry.group.test.dependencies]
27
+ pytest = "^8.3.4"
28
+
29
+ [tool.poetry.group.dev.dependencies]
30
+ grpcio-tools = "^1.68.1"
31
+ black = "^24.10.0"
32
+ pytest-asyncio = "^0.26.0"
33
+
34
+ [build-system]
35
+ requires = ["poetry-core"]
36
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,297 @@
1
+ import asyncio
2
+ import logging
3
+ from yellowstone_fumarole_client.grpc_connectivity import (
4
+ FumaroleGrpcConnector,
5
+ )
6
+ from typing import Dict, Optional
7
+ from dataclasses import dataclass
8
+ from . import config
9
+ from yellowstone_fumarole_client.runtime.aio import (
10
+ AsyncioFumeDragonsmouthRuntime,
11
+ FumaroleSM,
12
+ DEFAULT_GC_INTERVAL,
13
+ DEFAULT_SLOT_MEMORY_RETENTION,
14
+ GrpcSlotDownloader,
15
+ )
16
+ from yellowstone_fumarole_proto.geyser_pb2 import SubscribeRequest, SubscribeUpdate
17
+ from yellowstone_fumarole_proto.fumarole_v2_pb2 import (
18
+ ControlResponse,
19
+ VersionRequest,
20
+ VersionResponse,
21
+ JoinControlPlane,
22
+ ControlCommand,
23
+ ListConsumerGroupsRequest,
24
+ ListConsumerGroupsResponse,
25
+ GetConsumerGroupInfoRequest,
26
+ ConsumerGroupInfo,
27
+ DeleteConsumerGroupRequest,
28
+ DeleteConsumerGroupResponse,
29
+ CreateConsumerGroupRequest,
30
+ CreateConsumerGroupResponse,
31
+ )
32
+ from yellowstone_fumarole_proto.fumarole_v2_pb2_grpc import FumaroleStub
33
+ import grpc
34
+
35
+ __all__ = [
36
+ "FumaroleClient",
37
+ "FumaroleConfig",
38
+ "FumaroleSubscribeConfig",
39
+ "DragonsmouthAdapterSession",
40
+ "DEFAULT_DRAGONSMOUTH_CAPACITY",
41
+ "DEFAULT_COMMIT_INTERVAL",
42
+ "DEFAULT_MAX_SLOT_DOWNLOAD_ATTEMPT",
43
+ "DEFAULT_CONCURRENT_DOWNLOAD_LIMIT_PER_TCP",
44
+ ]
45
+
46
+ # Constants
47
+ DEFAULT_DRAGONSMOUTH_CAPACITY = 10000
48
+ DEFAULT_COMMIT_INTERVAL = 5.0 # seconds
49
+ DEFAULT_MAX_SLOT_DOWNLOAD_ATTEMPT = 3
50
+ DEFAULT_CONCURRENT_DOWNLOAD_LIMIT_PER_TCP = 10
51
+
52
+ # Error classes
53
+
54
+
55
+ # FumaroleSubscribeConfig
56
+ @dataclass
57
+ class FumaroleSubscribeConfig:
58
+ """Configuration for subscribing to a dragonsmouth stream."""
59
+
60
+ # The maximum number of concurrent download tasks per TCP connection.
61
+ concurrent_download_limit: int = DEFAULT_CONCURRENT_DOWNLOAD_LIMIT_PER_TCP
62
+
63
+ # The interval at which to commit the slot memory.
64
+ commit_interval: float = DEFAULT_COMMIT_INTERVAL
65
+
66
+ # The maximum number of failed slot download attempts before giving up.
67
+ max_failed_slot_download_attempt: int = DEFAULT_MAX_SLOT_DOWNLOAD_ATTEMPT
68
+
69
+ # The maximum number of slots to download concurrently.
70
+ data_channel_capacity: int = DEFAULT_DRAGONSMOUTH_CAPACITY
71
+
72
+ # The interval at which to perform garbage collection on the slot memory.
73
+ gc_interval: int = DEFAULT_GC_INTERVAL
74
+
75
+ # The retention period for slot memory in seconds.
76
+ slot_memory_retention: int = DEFAULT_SLOT_MEMORY_RETENTION
77
+
78
+
79
+ # DragonsmouthAdapterSession
80
+ @dataclass
81
+ class DragonsmouthAdapterSession:
82
+ """Session for interacting with the dragonsmouth-like stream."""
83
+
84
+ # The queue for sending SubscribeRequest update to the dragonsmouth stream.
85
+ sink: asyncio.Queue
86
+
87
+ # The queue for receiving SubscribeUpdate from the dragonsmouth stream.
88
+ source: asyncio.Queue
89
+
90
+ # The task handle for the fumarole runtime.
91
+ fumarole_handle: asyncio.Task
92
+
93
+
94
+ # FumaroleClient
95
+ class FumaroleClient:
96
+ """Fumarole client for interacting with the Fumarole server."""
97
+
98
+ logger = logging.getLogger(__name__)
99
+
100
+ def __init__(self, connector: FumaroleGrpcConnector, stub: FumaroleStub):
101
+ self.connector = connector
102
+ self.stub = stub
103
+
104
+ @staticmethod
105
+ async def connect(config: config.FumaroleConfig) -> "FumaroleClient":
106
+ """Connect to the Fumarole server using the provided configuration.
107
+ Args:
108
+ config (FumaroleConfig): Configuration for the Fumarole client.
109
+ """
110
+ endpoint = config.endpoint
111
+ connector = FumaroleGrpcConnector(config=config, endpoint=endpoint)
112
+ FumaroleClient.logger.debug(f"Connecting to {endpoint}")
113
+ client = await connector.connect()
114
+ FumaroleClient.logger.debug(f"Connected to {endpoint}")
115
+ return FumaroleClient(connector=connector, stub=client)
116
+
117
+ async def version(self) -> VersionResponse:
118
+ """Get the version of the Fumarole server."""
119
+ request = VersionRequest()
120
+ response = await self.stub.version(request)
121
+ return response
122
+
123
+ async def dragonsmouth_subscribe(
124
+ self, consumer_group_name: str, request: SubscribeRequest
125
+ ) -> DragonsmouthAdapterSession:
126
+ """Subscribe to a dragonsmouth stream with default configuration.
127
+
128
+ Args:
129
+ consumer_group_name (str): The name of the consumer group.
130
+ request (SubscribeRequest): The request to subscribe to the dragonsmouth stream.
131
+ """
132
+ return await self.dragonsmouth_subscribe_with_config(
133
+ consumer_group_name, request, FumaroleSubscribeConfig()
134
+ )
135
+
136
+ async def dragonsmouth_subscribe_with_config(
137
+ self,
138
+ consumer_group_name: str,
139
+ request: SubscribeRequest,
140
+ config: FumaroleSubscribeConfig,
141
+ ) -> DragonsmouthAdapterSession:
142
+ """Subscribe to a dragonsmouth stream with custom configuration.
143
+
144
+ Args:
145
+ consumer_group_name (str): The name of the consumer group.
146
+ request (SubscribeRequest): The request to subscribe to the dragonsmouth stream.
147
+ config (FumaroleSubscribeConfig): The configuration for the dragonsmouth subscription.
148
+ """
149
+ dragonsmouth_outlet = asyncio.Queue(maxsize=config.data_channel_capacity)
150
+ fume_control_plane_q = asyncio.Queue(maxsize=100)
151
+
152
+ initial_join = JoinControlPlane(consumer_group_name=consumer_group_name)
153
+ initial_join_command = ControlCommand(initial_join=initial_join)
154
+ await fume_control_plane_q.put(initial_join_command)
155
+
156
+ FumaroleClient.logger.debug(
157
+ f"Sent initial join command: {initial_join_command}"
158
+ )
159
+
160
+ async def control_plane_sink():
161
+ while True:
162
+ try:
163
+ update = await fume_control_plane_q.get()
164
+ yield update
165
+ except asyncio.QueueShutDown:
166
+ break
167
+
168
+ fume_control_plane_stream_rx: grpc.aio.StreamStreamMultiCallable = (
169
+ self.stub.Subscribe(control_plane_sink())
170
+ )
171
+
172
+ control_response: ControlResponse = await fume_control_plane_stream_rx.read()
173
+ init = control_response.init
174
+ if init is None:
175
+ raise ValueError(f"Unexpected initial response: {control_response}")
176
+
177
+ # Once we have the initial response, we can spin a task to read from the stream
178
+ # and put the updates into the queue.
179
+ # This is a bit of a hack, but we need a Queue not a StreamStreamMultiCallable
180
+ # because Queue are cancel-safe, while Stream are not, or at least didn't find any docs about it.
181
+ fume_control_plane_rx_q = asyncio.Queue(maxsize=100)
182
+
183
+ async def control_plane_source():
184
+ while True:
185
+ try:
186
+ async for update in fume_control_plane_stream_rx:
187
+ await fume_control_plane_rx_q.put(update)
188
+ except asyncio.QueueShutDown:
189
+ break
190
+
191
+ _cp_src_task = asyncio.create_task(control_plane_source())
192
+
193
+ FumaroleClient.logger.debug(f"Control response: {control_response}")
194
+
195
+ last_committed_offset = init.last_committed_offsets.get(0)
196
+ if last_committed_offset is None:
197
+ raise ValueError("No last committed offset")
198
+
199
+ sm = FumaroleSM(last_committed_offset, config.slot_memory_retention)
200
+ subscribe_request_queue = asyncio.Queue(maxsize=100)
201
+
202
+ data_plane_client = await self.connector.connect()
203
+
204
+ grpc_slot_downloader = GrpcSlotDownloader(
205
+ client=data_plane_client,
206
+ )
207
+
208
+ rt = AsyncioFumeDragonsmouthRuntime(
209
+ sm=sm,
210
+ slot_downloader=grpc_slot_downloader,
211
+ subscribe_request_update_q=subscribe_request_queue,
212
+ subscribe_request=request,
213
+ consumer_group_name=consumer_group_name,
214
+ control_plane_tx_q=fume_control_plane_q,
215
+ control_plane_rx_q=fume_control_plane_rx_q,
216
+ dragonsmouth_outlet=dragonsmouth_outlet,
217
+ commit_interval=config.commit_interval,
218
+ gc_interval=config.gc_interval,
219
+ max_concurrent_download=config.concurrent_download_limit,
220
+ )
221
+
222
+ fumarole_handle = asyncio.create_task(rt.run())
223
+ FumaroleClient.logger.debug(f"Fumarole handle created: {fumarole_handle}")
224
+ return DragonsmouthAdapterSession(
225
+ sink=subscribe_request_queue,
226
+ source=dragonsmouth_outlet,
227
+ fumarole_handle=fumarole_handle,
228
+ )
229
+
230
+ async def list_consumer_groups(
231
+ self,
232
+ ) -> ListConsumerGroupsResponse:
233
+ """Lists all consumer groups."""
234
+ return await self.stub.ListConsumerGroups(ListConsumerGroupsRequest())
235
+
236
+ async def get_consumer_group_info(
237
+ self, consumer_group_name: str
238
+ ) -> Optional[ConsumerGroupInfo]:
239
+ """Gets information about a consumer group by name.
240
+ Returns None if the consumer group does not exist.
241
+
242
+ Args:
243
+ consumer_group_name (str): The name of the consumer group to retrieve information for.
244
+ """
245
+ try:
246
+ return await self.stub.GetConsumerGroupInfo(
247
+ GetConsumerGroupInfoRequest(consumer_group_name=consumer_group_name)
248
+ )
249
+ except grpc.aio.AioRpcError as e:
250
+ if e.code() == grpc.StatusCode.NOT_FOUND:
251
+ return None
252
+ else:
253
+ raise
254
+
255
+ async def delete_consumer_group(
256
+ self, consumer_group_name: str
257
+ ) -> DeleteConsumerGroupResponse:
258
+ """Delete a consumer group by name.
259
+
260
+ NOTE: this operation is idempotent, meaning that if the consumer group does not exist, it will not raise an error.
261
+ Args:
262
+ consumer_group_name (str): The name of the consumer group to delete.
263
+ """
264
+ return await self.stub.DeleteConsumerGroup(
265
+ DeleteConsumerGroupRequest(consumer_group_name=consumer_group_name)
266
+ )
267
+
268
+ async def delete_all_consumer_groups(
269
+ self,
270
+ ) -> DeleteConsumerGroupResponse:
271
+ """Deletes all consumer groups."""
272
+ consumer_group_list = await self.list_consumer_groups()
273
+
274
+ tasks = []
275
+
276
+ async with asyncio.TaskGroup() as tg:
277
+ for group in consumer_group_list.consumer_groups:
278
+ cg_name = group.consumer_group_name
279
+ task = tg.create_task(self.delete_consumer_group(cg_name))
280
+ tasks.append((cg_name, task))
281
+
282
+ # Raise an error if any task fails
283
+ for cg_name, task in tasks:
284
+ result = task.result()
285
+ if not result.success:
286
+ raise RuntimeError(
287
+ f"Failed to delete consumer group {cg_name}: {result.error}"
288
+ )
289
+
290
+ async def create_consumer_group(
291
+ self, request: CreateConsumerGroupRequest
292
+ ) -> CreateConsumerGroupResponse:
293
+ """Creates a new consumer group.
294
+ Args:
295
+ request (CreateConsumerGroupRequest): The request to create a consumer group.
296
+ """
297
+ return await self.stub.CreateConsumerGroup(request)
@@ -0,0 +1,26 @@
1
+ from dataclasses import dataclass
2
+ from typing import Dict, Optional
3
+ import yaml
4
+
5
+
6
+ @dataclass
7
+ class FumaroleConfig:
8
+ endpoint: str
9
+ x_token: Optional[str] = None
10
+ max_decoding_message_size_bytes: int = 512_000_000
11
+ x_metadata: Dict[str, str] = None
12
+
13
+ def __post_init__(self):
14
+ self.x_metadata = self.x_metadata or {}
15
+
16
+ @classmethod
17
+ def from_yaml(cls, fileobj) -> "FumaroleConfig":
18
+ data = yaml.safe_load(fileobj)
19
+ return cls(
20
+ endpoint=data["endpoint"],
21
+ x_token=data.get("x-token") or data.get("x_token"),
22
+ max_decoding_message_size_bytes=data.get(
23
+ "max_decoding_message_size_bytes", cls.max_decoding_message_size_bytes
24
+ ),
25
+ x_metadata=data.get("x-metadata", {}),
26
+ )