yellowstone-fumarole-client 0.1.0__py3-none-any.whl
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.
- yellowstone_fumarole_client/__init__.py +297 -0
- yellowstone_fumarole_client/config.py +26 -0
- yellowstone_fumarole_client/grpc_connectivity.py +197 -0
- yellowstone_fumarole_client/runtime/__init__.py +0 -0
- yellowstone_fumarole_client/runtime/aio.py +525 -0
- yellowstone_fumarole_client/runtime/state_machine.py +326 -0
- yellowstone_fumarole_client/utils/__init__.py +0 -0
- yellowstone_fumarole_client/utils/aio.py +29 -0
- yellowstone_fumarole_client/utils/collections.py +37 -0
- yellowstone_fumarole_client-0.1.0.dist-info/METADATA +110 -0
- yellowstone_fumarole_client-0.1.0.dist-info/RECORD +22 -0
- yellowstone_fumarole_client-0.1.0.dist-info/WHEEL +4 -0
- yellowstone_fumarole_proto/__init__.py +0 -0
- yellowstone_fumarole_proto/fumarole_v2_pb2.py +122 -0
- yellowstone_fumarole_proto/fumarole_v2_pb2.pyi +328 -0
- yellowstone_fumarole_proto/fumarole_v2_pb2_grpc.py +400 -0
- yellowstone_fumarole_proto/geyser_pb2.py +144 -0
- yellowstone_fumarole_proto/geyser_pb2.pyi +501 -0
- yellowstone_fumarole_proto/geyser_pb2_grpc.py +355 -0
- yellowstone_fumarole_proto/solana_storage_pb2.py +75 -0
- yellowstone_fumarole_proto/solana_storage_pb2.pyi +238 -0
- yellowstone_fumarole_proto/solana_storage_pb2_grpc.py +24 -0
@@ -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 yellowstone_fumarole_client.config import FumaroleConfig
|
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.StreamStreamCall = self.stub.Subscribe(
|
169
|
+
control_plane_sink()
|
170
|
+
) # it's actually InterceptedStreamStreamCall, but grpc lib doesn't export it
|
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
|
+
)
|
@@ -0,0 +1,197 @@
|
|
1
|
+
import logging
|
2
|
+
from typing import Optional
|
3
|
+
import grpc
|
4
|
+
from yellowstone_fumarole_client.config import FumaroleConfig
|
5
|
+
from yellowstone_fumarole_proto.fumarole_v2_pb2_grpc import FumaroleStub
|
6
|
+
|
7
|
+
X_TOKEN_HEADER = "x-token"
|
8
|
+
|
9
|
+
|
10
|
+
def _triton_sign_request(
|
11
|
+
callback: grpc.AuthMetadataPluginCallback,
|
12
|
+
x_token: Optional[str],
|
13
|
+
error: Optional[Exception],
|
14
|
+
):
|
15
|
+
# WARNING: metadata is a 1d-tuple (<value>,), the last comma is necessary
|
16
|
+
metadata = ((X_TOKEN_HEADER, x_token),)
|
17
|
+
return callback(metadata, error)
|
18
|
+
|
19
|
+
|
20
|
+
class TritonAuthMetadataPlugin(grpc.AuthMetadataPlugin):
|
21
|
+
"""Metadata wrapper for raw access token credentials."""
|
22
|
+
|
23
|
+
def __init__(self, x_token: str):
|
24
|
+
self.x_token = x_token
|
25
|
+
|
26
|
+
def __call__(
|
27
|
+
self,
|
28
|
+
context: grpc.AuthMetadataContext,
|
29
|
+
callback: grpc.AuthMetadataPluginCallback,
|
30
|
+
):
|
31
|
+
return _triton_sign_request(callback, self.x_token, None)
|
32
|
+
|
33
|
+
|
34
|
+
def grpc_channel(endpoint: str, x_token=None, compression=None, *grpc_options):
|
35
|
+
options = [("grpc.max_receive_message_length", 111111110), *grpc_options]
|
36
|
+
if x_token is not None:
|
37
|
+
auth = TritonAuthMetadataPlugin(x_token)
|
38
|
+
# ssl_creds allow you to use our https endpoint
|
39
|
+
# grpc.ssl_channel_credentials with no arguments will look through your CA trust store.
|
40
|
+
ssl_creds = grpc.ssl_channel_credentials()
|
41
|
+
|
42
|
+
# call credentials will be sent on each request if setup with composite_channel_credentials.
|
43
|
+
call_creds: grpc.CallCredentials = grpc.metadata_call_credentials(auth)
|
44
|
+
|
45
|
+
# Combined creds will store the channel creds aswell as the call credentials
|
46
|
+
combined_creds = grpc.composite_channel_credentials(ssl_creds, call_creds)
|
47
|
+
|
48
|
+
return grpc.secure_channel(
|
49
|
+
endpoint,
|
50
|
+
credentials=combined_creds,
|
51
|
+
compression=compression,
|
52
|
+
options=options,
|
53
|
+
)
|
54
|
+
else:
|
55
|
+
return grpc.insecure_channel(endpoint, compression=compression, options=options)
|
56
|
+
|
57
|
+
|
58
|
+
# Because of a bug in grpcio library, multiple inheritance of ClientInterceptor subclasses does not work.
|
59
|
+
# You have to create a new class for each type of interceptor you want to use.
|
60
|
+
|
61
|
+
|
62
|
+
class MetadataInterceptor(
|
63
|
+
grpc.aio.UnaryStreamClientInterceptor,
|
64
|
+
grpc.aio.StreamUnaryClientInterceptor,
|
65
|
+
grpc.aio.StreamStreamClientInterceptor,
|
66
|
+
grpc.aio.UnaryUnaryClientInterceptor,
|
67
|
+
):
|
68
|
+
|
69
|
+
def __init__(self, metadata):
|
70
|
+
if isinstance(metadata, dict):
|
71
|
+
metadata = metadata.items()
|
72
|
+
self.metadata = list(metadata)
|
73
|
+
|
74
|
+
async def intercept_unary_unary(
|
75
|
+
self, continuation, client_call_details: grpc.aio.ClientCallDetails, request
|
76
|
+
):
|
77
|
+
# logging.debug("intercept_unary_unary")
|
78
|
+
new_details = client_call_details._replace(
|
79
|
+
metadata=self._merge_metadata(client_call_details.metadata)
|
80
|
+
)
|
81
|
+
return await continuation(new_details, request)
|
82
|
+
|
83
|
+
async def intercept_unary_stream(
|
84
|
+
self, continuation, client_call_details: grpc.aio.ClientCallDetails, request
|
85
|
+
):
|
86
|
+
# logging.debug("intercept_unary_stream")
|
87
|
+
new_details = client_call_details._replace(
|
88
|
+
metadata=self._merge_metadata(client_call_details.metadata)
|
89
|
+
)
|
90
|
+
return await continuation(new_details, request)
|
91
|
+
|
92
|
+
async def intercept_stream_unary(
|
93
|
+
self, continuation, client_call_details: grpc.aio.ClientCallDetails, request
|
94
|
+
):
|
95
|
+
# logging.debug("intercept_stream_unary")
|
96
|
+
new_details = client_call_details._replace(
|
97
|
+
metadata=self._merge_metadata(client_call_details.metadata)
|
98
|
+
)
|
99
|
+
return await continuation(new_details, request)
|
100
|
+
|
101
|
+
async def intercept_stream_stream(
|
102
|
+
self, continuation, client_call_details: grpc.aio.ClientCallDetails, request
|
103
|
+
):
|
104
|
+
# logging.debug("intercept_stream_stream")
|
105
|
+
new_details = client_call_details._replace(
|
106
|
+
metadata=self._merge_metadata(client_call_details.metadata)
|
107
|
+
)
|
108
|
+
return await continuation(new_details, request)
|
109
|
+
|
110
|
+
def unary_stream_interceptor(self) -> grpc.aio.UnaryStreamClientInterceptor:
|
111
|
+
this = self
|
112
|
+
|
113
|
+
class Interceptor(grpc.aio.UnaryStreamClientInterceptor):
|
114
|
+
async def intercept_unary_stream(self, *args):
|
115
|
+
return await this.intercept_unary_stream(*args)
|
116
|
+
|
117
|
+
return Interceptor()
|
118
|
+
|
119
|
+
def stream_unary_interceptor(self) -> grpc.aio.StreamUnaryClientInterceptor:
|
120
|
+
this = self
|
121
|
+
|
122
|
+
class Interceptor(grpc.aio.StreamUnaryClientInterceptor):
|
123
|
+
async def intercept_stream_unary(self, *args):
|
124
|
+
return await this.intercept_stream_unary(*args)
|
125
|
+
|
126
|
+
return Interceptor()
|
127
|
+
|
128
|
+
def stream_stream_interceptor(self) -> grpc.aio.StreamStreamClientInterceptor:
|
129
|
+
this = self
|
130
|
+
|
131
|
+
class Interceptor(grpc.aio.StreamStreamClientInterceptor):
|
132
|
+
async def intercept_stream_stream(self, *args):
|
133
|
+
return await this.intercept_stream_stream(*args)
|
134
|
+
|
135
|
+
return Interceptor()
|
136
|
+
|
137
|
+
def unary_unary_interceptor(self) -> grpc.aio.UnaryUnaryClientInterceptor:
|
138
|
+
this = self
|
139
|
+
|
140
|
+
class Interceptor(grpc.aio.UnaryUnaryClientInterceptor):
|
141
|
+
async def intercept_unary_unary(self, *args):
|
142
|
+
return await this.intercept_unary_unary(*args)
|
143
|
+
|
144
|
+
return Interceptor()
|
145
|
+
|
146
|
+
def interceptors(self) -> list[grpc.aio.ClientInterceptor]:
|
147
|
+
return [
|
148
|
+
self.unary_unary_interceptor(),
|
149
|
+
self.unary_stream_interceptor(),
|
150
|
+
self.stream_unary_interceptor(),
|
151
|
+
self.stream_stream_interceptor(),
|
152
|
+
]
|
153
|
+
|
154
|
+
def _merge_metadata(self, existing):
|
155
|
+
result = list(existing or []) + self.metadata
|
156
|
+
return result
|
157
|
+
|
158
|
+
|
159
|
+
class FumaroleGrpcConnector:
|
160
|
+
logger = logging.getLogger(__name__)
|
161
|
+
|
162
|
+
def __init__(self, config: FumaroleConfig, endpoint: str):
|
163
|
+
self.config = config
|
164
|
+
self.endpoint = endpoint
|
165
|
+
|
166
|
+
async def connect(self, *grpc_options) -> FumaroleStub:
|
167
|
+
options = [("grpc.max_receive_message_length", 111111110), *grpc_options]
|
168
|
+
interceptors = MetadataInterceptor(self.config.x_metadata).interceptors()
|
169
|
+
if self.config.x_token is not None:
|
170
|
+
auth = TritonAuthMetadataPlugin(self.config.x_token)
|
171
|
+
# ssl_creds allow you to use our https endpoint
|
172
|
+
# grpc.ssl_channel_credentials with no arguments will look through your CA trust store.
|
173
|
+
ssl_creds = grpc.ssl_channel_credentials()
|
174
|
+
|
175
|
+
# call credentials will be sent on each request if setup with composite_channel_credentials.
|
176
|
+
call_creds: grpc.CallCredentials = grpc.metadata_call_credentials(auth)
|
177
|
+
|
178
|
+
# Combined creds will store the channel creds aswell as the call credentials
|
179
|
+
combined_creds = grpc.composite_channel_credentials(ssl_creds, call_creds)
|
180
|
+
FumaroleGrpcConnector.logger.debug(
|
181
|
+
"Using secure channel with x-token authentication"
|
182
|
+
)
|
183
|
+
channel = grpc.aio.secure_channel(
|
184
|
+
self.endpoint,
|
185
|
+
credentials=combined_creds,
|
186
|
+
options=options,
|
187
|
+
interceptors=interceptors,
|
188
|
+
)
|
189
|
+
else:
|
190
|
+
FumaroleGrpcConnector.logger.debug(
|
191
|
+
"Using insecure channel without authentication"
|
192
|
+
)
|
193
|
+
channel = grpc.aio.insecure_channel(
|
194
|
+
self.endpoint, options=options, interceptors=interceptors
|
195
|
+
)
|
196
|
+
|
197
|
+
return FumaroleStub(channel)
|
File without changes
|