Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/grpc/aio/_channel.py: 42%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

190 statements  

1# Copyright 2019 gRPC authors. 

2# 

3# Licensed under the Apache License, Version 2.0 (the "License"); 

4# you may not use this file except in compliance with the License. 

5# You may obtain a copy of the License at 

6# 

7# http://www.apache.org/licenses/LICENSE-2.0 

8# 

9# Unless required by applicable law or agreed to in writing, software 

10# distributed under the License is distributed on an "AS IS" BASIS, 

11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

12# See the License for the specific language governing permissions and 

13# limitations under the License. 

14"""Invocation-side implementation of gRPC Asyncio Python.""" 

15 

16import asyncio 

17from typing import Any, List, Optional, Sequence 

18import weakref 

19 

20import grpc 

21from grpc import _common 

22from grpc import _compression 

23from grpc import _grpcio_metadata 

24from grpc._cython import cygrpc 

25 

26from . import _base_call 

27from . import _base_channel 

28from ._call import StreamStreamCall 

29from ._call import StreamUnaryCall 

30from ._call import UnaryStreamCall 

31from ._call import UnaryUnaryCall 

32from ._interceptor import ClientInterceptor 

33from ._interceptor import InterceptedStreamStreamCall 

34from ._interceptor import InterceptedStreamUnaryCall 

35from ._interceptor import InterceptedUnaryStreamCall 

36from ._interceptor import InterceptedUnaryUnaryCall 

37from ._interceptor import StreamStreamClientInterceptor 

38from ._interceptor import StreamUnaryClientInterceptor 

39from ._interceptor import UnaryStreamClientInterceptor 

40from ._interceptor import UnaryUnaryClientInterceptor 

41from ._metadata import Metadata 

42from ._typing import ChannelArgumentType 

43from ._typing import DeserializingFunction 

44from ._typing import MetadataType 

45from ._typing import RequestIterableType 

46from ._typing import RequestType 

47from ._typing import ResponseType 

48from ._typing import SerializingFunction 

49from ._utils import _timeout_to_deadline 

50 

51_USER_AGENT = "grpc-python-asyncio/{}".format(_grpcio_metadata.__version__) 

52 

53 

54def _augment_channel_arguments( 

55 base_options: ChannelArgumentType, compression: Optional[grpc.Compression] 

56): 

57 compression_channel_argument = _compression.create_channel_option( 

58 compression 

59 ) 

60 user_agent_channel_argument = ( 

61 ( 

62 cygrpc.ChannelArgKey.primary_user_agent_string, 

63 _USER_AGENT, 

64 ), 

65 ) 

66 return ( 

67 tuple(base_options) 

68 + compression_channel_argument 

69 + user_agent_channel_argument 

70 ) 

71 

72 

73class _BaseMultiCallable: 

74 """Base class of all multi callable objects. 

75 

76 Handles the initialization logic and stores common attributes. 

77 """ 

78 

79 _loop: asyncio.AbstractEventLoop 

80 _channel: cygrpc.AioChannel 

81 _method: bytes 

82 _request_serializer: Optional[SerializingFunction] 

83 _response_deserializer: Optional[DeserializingFunction] 

84 _interceptors: Optional[Sequence[ClientInterceptor]] 

85 _references: List[Any] 

86 _loop: asyncio.AbstractEventLoop 

87 

88 # pylint: disable=too-many-arguments 

89 def __init__( 

90 self, 

91 channel: cygrpc.AioChannel, 

92 method: bytes, 

93 request_serializer: Optional[SerializingFunction], 

94 response_deserializer: Optional[DeserializingFunction], 

95 interceptors: Optional[Sequence[ClientInterceptor]], 

96 references: List[Any], 

97 loop: asyncio.AbstractEventLoop, 

98 ) -> None: 

99 self._loop = loop 

100 self._channel = channel 

101 self._method = method 

102 self._request_serializer = request_serializer 

103 self._response_deserializer = response_deserializer 

104 self._interceptors = interceptors 

105 self._references = references 

106 

107 if not self._references: 

108 error_msg = ( 

109 "MultiCallable must be attached to a Channel, unexpectedly" 

110 " found no references." 

111 ) 

112 raise ValueError(error_msg) 

113 if not isinstance(self._references[0], Channel): 

114 error_msg = ( 

115 "Invalid reference type. MultiCallable must be attached to a" 

116 " Channel." 

117 ) 

118 raise TypeError(error_msg) 

119 

120 self._python_channel = self._references[0] 

121 

122 @staticmethod 

123 def _init_metadata( 

124 metadata: Optional[MetadataType] = None, 

125 compression: Optional[grpc.Compression] = None, 

126 ) -> Metadata: 

127 """Based on the provided values for <metadata> or <compression> initialise the final 

128 metadata, as it should be used for the current call. 

129 """ 

130 metadata = metadata or Metadata() 

131 if not isinstance(metadata, Metadata) and isinstance( 

132 metadata, Sequence 

133 ): 

134 metadata = Metadata.from_tuple(tuple(metadata)) 

135 if compression: 

136 metadata = Metadata( 

137 *_compression.augment_metadata(metadata, compression) 

138 ) 

139 return metadata 

140 

141 

142class UnaryUnaryMultiCallable( 

143 _BaseMultiCallable, _base_channel.UnaryUnaryMultiCallable 

144): 

145 def __call__( 

146 self, 

147 request: RequestType, 

148 *, 

149 timeout: Optional[float] = None, 

150 metadata: Optional[MetadataType] = None, 

151 credentials: Optional[grpc.CallCredentials] = None, 

152 wait_for_ready: Optional[bool] = None, 

153 compression: Optional[grpc.Compression] = None, 

154 ) -> _base_call.UnaryUnaryCall[RequestType, ResponseType]: 

155 metadata = self._init_metadata(metadata, compression) 

156 if not self._interceptors: 

157 call = UnaryUnaryCall( 

158 request, 

159 _timeout_to_deadline(timeout), 

160 metadata, 

161 credentials, 

162 wait_for_ready, 

163 self._channel, 

164 self._method, 

165 self._request_serializer, 

166 self._response_deserializer, 

167 self._loop, 

168 ) 

169 else: 

170 call = InterceptedUnaryUnaryCall( 

171 self._interceptors, 

172 request, 

173 timeout, 

174 metadata, 

175 credentials, 

176 wait_for_ready, 

177 self._channel, 

178 self._method, 

179 self._request_serializer, 

180 self._response_deserializer, 

181 self._loop, 

182 ) 

183 

184 self._python_channel._register_call(call) 

185 

186 return call 

187 

188 

189class UnaryStreamMultiCallable( 

190 _BaseMultiCallable, _base_channel.UnaryStreamMultiCallable 

191): 

192 def __call__( 

193 self, 

194 request: RequestType, 

195 *, 

196 timeout: Optional[float] = None, 

197 metadata: Optional[MetadataType] = None, 

198 credentials: Optional[grpc.CallCredentials] = None, 

199 wait_for_ready: Optional[bool] = None, 

200 compression: Optional[grpc.Compression] = None, 

201 ) -> _base_call.UnaryStreamCall[RequestType, ResponseType]: 

202 metadata = self._init_metadata(metadata, compression) 

203 

204 if not self._interceptors: 

205 call = UnaryStreamCall( 

206 request, 

207 _timeout_to_deadline(timeout), 

208 metadata, 

209 credentials, 

210 wait_for_ready, 

211 self._channel, 

212 self._method, 

213 self._request_serializer, 

214 self._response_deserializer, 

215 self._loop, 

216 ) 

217 else: 

218 call = InterceptedUnaryStreamCall( 

219 self._interceptors, 

220 request, 

221 timeout, 

222 metadata, 

223 credentials, 

224 wait_for_ready, 

225 self._channel, 

226 self._method, 

227 self._request_serializer, 

228 self._response_deserializer, 

229 self._loop, 

230 ) 

231 

232 self._python_channel._register_call(call) 

233 

234 return call 

235 

236 

237class StreamUnaryMultiCallable( 

238 _BaseMultiCallable, _base_channel.StreamUnaryMultiCallable 

239): 

240 def __call__( 

241 self, 

242 request_iterator: Optional[RequestIterableType] = None, 

243 timeout: Optional[float] = None, 

244 metadata: Optional[MetadataType] = None, 

245 credentials: Optional[grpc.CallCredentials] = None, 

246 wait_for_ready: Optional[bool] = None, 

247 compression: Optional[grpc.Compression] = None, 

248 ) -> _base_call.StreamUnaryCall: 

249 metadata = self._init_metadata(metadata, compression) 

250 

251 if not self._interceptors: 

252 call = StreamUnaryCall( 

253 request_iterator, 

254 _timeout_to_deadline(timeout), 

255 metadata, 

256 credentials, 

257 wait_for_ready, 

258 self._channel, 

259 self._method, 

260 self._request_serializer, 

261 self._response_deserializer, 

262 self._loop, 

263 ) 

264 else: 

265 call = InterceptedStreamUnaryCall( 

266 self._interceptors, 

267 request_iterator, 

268 timeout, 

269 metadata, 

270 credentials, 

271 wait_for_ready, 

272 self._channel, 

273 self._method, 

274 self._request_serializer, 

275 self._response_deserializer, 

276 self._loop, 

277 ) 

278 

279 self._python_channel._register_call(call) 

280 

281 return call 

282 

283 

284class StreamStreamMultiCallable( 

285 _BaseMultiCallable, _base_channel.StreamStreamMultiCallable 

286): 

287 def __call__( 

288 self, 

289 request_iterator: Optional[RequestIterableType] = None, 

290 timeout: Optional[float] = None, 

291 metadata: Optional[MetadataType] = None, 

292 credentials: Optional[grpc.CallCredentials] = None, 

293 wait_for_ready: Optional[bool] = None, 

294 compression: Optional[grpc.Compression] = None, 

295 ) -> _base_call.StreamStreamCall: 

296 metadata = self._init_metadata(metadata, compression) 

297 

298 if not self._interceptors: 

299 call = StreamStreamCall( 

300 request_iterator, 

301 _timeout_to_deadline(timeout), 

302 metadata, 

303 credentials, 

304 wait_for_ready, 

305 self._channel, 

306 self._method, 

307 self._request_serializer, 

308 self._response_deserializer, 

309 self._loop, 

310 ) 

311 else: 

312 call = InterceptedStreamStreamCall( 

313 self._interceptors, 

314 request_iterator, 

315 timeout, 

316 metadata, 

317 credentials, 

318 wait_for_ready, 

319 self._channel, 

320 self._method, 

321 self._request_serializer, 

322 self._response_deserializer, 

323 self._loop, 

324 ) 

325 

326 self._python_channel._register_call(call) 

327 

328 return call 

329 

330 

331class Channel(_base_channel.Channel): 

332 _loop: asyncio.AbstractEventLoop 

333 _channel: cygrpc.AioChannel 

334 _unary_unary_interceptors: List[UnaryUnaryClientInterceptor] 

335 _unary_stream_interceptors: List[UnaryStreamClientInterceptor] 

336 _stream_unary_interceptors: List[StreamUnaryClientInterceptor] 

337 _stream_stream_interceptors: List[StreamStreamClientInterceptor] 

338 

339 def __init__( 

340 self, 

341 target: str, 

342 options: ChannelArgumentType, 

343 credentials: Optional[cygrpc.ChannelCredentials], 

344 compression: Optional[grpc.Compression], 

345 interceptors: Optional[Sequence[ClientInterceptor]], 

346 ): 

347 """Constructor. 

348 

349 Args: 

350 target: The target to which to connect. 

351 options: Configuration options for the channel. 

352 credentials: A cygrpc.ChannelCredentials or None. 

353 compression: An optional value indicating the compression method to be 

354 used over the lifetime of the channel. 

355 interceptors: An optional list of interceptors that would be used for 

356 intercepting any RPC executed with that channel. 

357 """ 

358 self._unary_unary_interceptors = [] 

359 self._unary_stream_interceptors = [] 

360 self._stream_unary_interceptors = [] 

361 self._stream_stream_interceptors = [] 

362 

363 if interceptors is not None: 

364 for interceptor in interceptors: 

365 if isinstance(interceptor, UnaryUnaryClientInterceptor): 

366 self._unary_unary_interceptors.append(interceptor) 

367 elif isinstance(interceptor, UnaryStreamClientInterceptor): 

368 self._unary_stream_interceptors.append(interceptor) 

369 elif isinstance(interceptor, StreamUnaryClientInterceptor): 

370 self._stream_unary_interceptors.append(interceptor) 

371 elif isinstance(interceptor, StreamStreamClientInterceptor): 

372 self._stream_stream_interceptors.append(interceptor) 

373 else: 

374 raise ValueError( # noqa: TRY004 

375 "Interceptor {} must be ".format(interceptor) 

376 + "{} or ".format(UnaryUnaryClientInterceptor.__name__) 

377 + "{} or ".format(UnaryStreamClientInterceptor.__name__) 

378 + "{} or ".format(StreamUnaryClientInterceptor.__name__) 

379 + "{}. ".format(StreamStreamClientInterceptor.__name__) 

380 ) 

381 

382 self._loop = cygrpc.get_working_loop() 

383 self._channel = cygrpc.AioChannel( 

384 _common.encode(target), 

385 _augment_channel_arguments(options, compression), 

386 credentials, 

387 self._loop, 

388 ) 

389 self._active_calls = weakref.WeakSet() 

390 

391 def _register_call(self, call: _base_call.Call) -> None: 

392 """Register a call to be tracked by the channel.""" 

393 self._active_calls.add(call) 

394 call.add_done_callback(self._active_calls.discard) 

395 

396 async def __aenter__(self): 

397 return self 

398 

399 async def __aexit__(self, exc_type, exc_val, exc_tb): 

400 await self._close(None) 

401 

402 async def _close(self, grace): # pylint: disable=too-many-branches 

403 if self._channel.closed(): 

404 return 

405 

406 if grace and grace < 0: 

407 error_msg = f"grace must be non-negative, got {grace}." 

408 raise ValueError(error_msg) 

409 

410 # No new calls will be accepted by the Cython channel. 

411 self._channel.closing() 

412 

413 async def _wait_for_call_to_complete(call): 

414 try: 

415 await call.code() 

416 except Exception: # pylint: disable=broad-except 

417 # Ignore exceptions here as true RPC errors bubble up via 

418 # standard application paths. Silencing prevents channel close 

419 # from failing and suppresses asyncio noise warnings. 

420 pass 

421 

422 calls = list(self._active_calls) 

423 

424 if grace: 

425 call_tasks = [ 

426 self._loop.create_task(_wait_for_call_to_complete(call)) 

427 for call in calls 

428 if not call.done() 

429 ] 

430 if call_tasks: 

431 await asyncio.wait(call_tasks, timeout=grace) 

432 

433 # Time to cancel existing calls. 

434 for call in calls: 

435 call.cancel() 

436 

437 calls.clear() 

438 self._active_calls.clear() 

439 

440 # Destroy the channel 

441 self._channel.close() 

442 

443 async def close(self, grace: Optional[float] = None): 

444 await self._close(grace) 

445 

446 def __del__(self): 

447 if hasattr(self, "_channel") and not self._channel.closed(): 

448 self._channel.close() 

449 

450 def get_state( 

451 self, try_to_connect: bool = False 

452 ) -> grpc.ChannelConnectivity: 

453 result = self._channel.check_connectivity_state(try_to_connect) 

454 return _common.CYGRPC_CONNECTIVITY_STATE_TO_CHANNEL_CONNECTIVITY[result] 

455 

456 async def wait_for_state_change( 

457 self, 

458 last_observed_state: grpc.ChannelConnectivity, 

459 ) -> None: 

460 # We raise a RuntimeError if watch_connectivity_state returns False. 

461 # 

462 # The watch_connectivity_state method returns True when it observes a state change 

463 # and False when it times out (which shouldn't happen since no timeout is specified). 

464 # A channel close triggers a transition to SHUTDOWN, which resolves all pending watch 

465 # calls and makes them return True. Thus, watch_connectivity_state should only return 

466 # True under normal operation; returning False indicates an implementation issue. 

467 # 

468 # We do not use an assert statement here because asserts 

469 # can be optimized out under python -O. 

470 # See https://github.com/grpc/grpc/issues/42393 for context. 

471 resolved = await self._channel.watch_connectivity_state( 

472 last_observed_state.value[0], None 

473 ) 

474 if not resolved: 

475 error_msg = ( 

476 "gRPC channel connectivity state watch failed unexpectedly." 

477 ) 

478 raise RuntimeError(error_msg) 

479 

480 async def channel_ready(self) -> None: 

481 state = self.get_state(try_to_connect=True) 

482 while state != grpc.ChannelConnectivity.READY: 

483 await self.wait_for_state_change(state) 

484 state = self.get_state(try_to_connect=True) 

485 

486 # TODO(xuanwn): Implement this method after we have 

487 # observability for Asyncio. 

488 def _get_registered_call_handle(self, method: str) -> int: 

489 pass 

490 

491 # TODO(xuanwn): Implement _registered_method after we have 

492 # observability for Asyncio. 

493 # pylint: disable=arguments-differ,unused-argument 

494 def unary_unary( 

495 self, 

496 method: str, 

497 request_serializer: Optional[SerializingFunction] = None, 

498 response_deserializer: Optional[DeserializingFunction] = None, 

499 _registered_method: Optional[bool] = False, 

500 ) -> UnaryUnaryMultiCallable: 

501 return UnaryUnaryMultiCallable( 

502 self._channel, 

503 _common.encode(method), 

504 request_serializer, 

505 response_deserializer, 

506 self._unary_unary_interceptors, 

507 [self], 

508 self._loop, 

509 ) 

510 

511 # TODO(xuanwn): Implement _registered_method after we have 

512 # observability for Asyncio. 

513 # pylint: disable=arguments-differ,unused-argument 

514 def unary_stream( 

515 self, 

516 method: str, 

517 request_serializer: Optional[SerializingFunction] = None, 

518 response_deserializer: Optional[DeserializingFunction] = None, 

519 _registered_method: Optional[bool] = False, 

520 ) -> UnaryStreamMultiCallable: 

521 return UnaryStreamMultiCallable( 

522 self._channel, 

523 _common.encode(method), 

524 request_serializer, 

525 response_deserializer, 

526 self._unary_stream_interceptors, 

527 [self], 

528 self._loop, 

529 ) 

530 

531 # TODO(xuanwn): Implement _registered_method after we have 

532 # observability for Asyncio. 

533 # pylint: disable=arguments-differ,unused-argument 

534 def stream_unary( 

535 self, 

536 method: str, 

537 request_serializer: Optional[SerializingFunction] = None, 

538 response_deserializer: Optional[DeserializingFunction] = None, 

539 _registered_method: Optional[bool] = False, 

540 ) -> StreamUnaryMultiCallable: 

541 return StreamUnaryMultiCallable( 

542 self._channel, 

543 _common.encode(method), 

544 request_serializer, 

545 response_deserializer, 

546 self._stream_unary_interceptors, 

547 [self], 

548 self._loop, 

549 ) 

550 

551 # TODO(xuanwn): Implement _registered_method after we have 

552 # observability for Asyncio. 

553 # pylint: disable=arguments-differ,unused-argument 

554 def stream_stream( 

555 self, 

556 method: str, 

557 request_serializer: Optional[SerializingFunction] = None, 

558 response_deserializer: Optional[DeserializingFunction] = None, 

559 _registered_method: Optional[bool] = False, 

560 ) -> StreamStreamMultiCallable: 

561 return StreamStreamMultiCallable( 

562 self._channel, 

563 _common.encode(method), 

564 request_serializer, 

565 response_deserializer, 

566 self._stream_stream_interceptors, 

567 [self], 

568 self._loop, 

569 ) 

570 

571 

572def insecure_channel( 

573 target: str, 

574 options: Optional[ChannelArgumentType] = None, 

575 compression: Optional[grpc.Compression] = None, 

576 interceptors: Optional[Sequence[ClientInterceptor]] = None, 

577): 

578 """Creates an insecure asynchronous Channel to a server. 

579 

580 Args: 

581 target: The server address 

582 options: An optional list of key-value pairs (:term:`channel_arguments` 

583 in gRPC Core runtime) to configure the channel. 

584 compression: An optional value indicating the compression method to be 

585 used over the lifetime of the channel. 

586 interceptors: An optional sequence of interceptors that will be executed for 

587 any call executed with this channel. 

588 

589 Returns: 

590 A Channel. 

591 """ 

592 return Channel( 

593 target, 

594 () if options is None else options, 

595 None, 

596 compression, 

597 interceptors, 

598 ) 

599 

600 

601def secure_channel( 

602 target: str, 

603 credentials: grpc.ChannelCredentials, 

604 options: Optional[ChannelArgumentType] = None, 

605 compression: Optional[grpc.Compression] = None, 

606 interceptors: Optional[Sequence[ClientInterceptor]] = None, 

607): 

608 """Creates a secure asynchronous Channel to a server. 

609 

610 Args: 

611 target: The server address. 

612 credentials: A ChannelCredentials instance. 

613 options: An optional list of key-value pairs (:term:`channel_arguments` 

614 in gRPC Core runtime) to configure the channel. 

615 compression: An optional value indicating the compression method to be 

616 used over the lifetime of the channel. 

617 interceptors: An optional sequence of interceptors that will be executed for 

618 any call executed with this channel. 

619 

620 Returns: 

621 An aio.Channel. 

622 """ 

623 return Channel( 

624 target, 

625 () if options is None else options, 

626 credentials._credentials, 

627 compression, 

628 interceptors, 

629 )