Compare commits
7 Commits
dbc15bc328
...
88f360a47d
Author | SHA1 | Date |
---|---|---|
Jonatan Nevo | 88f360a47d | |
Vladimir Magamedov | 62f968a4c8 | |
Vladimir Magamedov | e9adb679ec | |
Jonatan Nevo | 7e2ed1a412 | |
Jonatan Nevo | afe3af9bac | |
Jonatan Nevo | db09b3d4ae | |
Jonatan Nevo | a1d1bb4ad0 |
|
@ -4,6 +4,9 @@ Changelog
|
|||
0.4.8
|
||||
~~~~~
|
||||
|
||||
- Fixed ``authority`` header for the case when Channel's ``host`` argument
|
||||
is the IPv6 address
|
||||
- Fixed ``Channel`` for the case when ``ssl`` module is not available
|
||||
- Dropped Python 3.7 support
|
||||
- Added "wheel" packaging format
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from .const import Status
|
||||
from .exceptions import GRPCError
|
||||
|
||||
__version__ = '0.4.8rc1'
|
||||
__version__ = '0.4.8rc2'
|
||||
|
||||
__all__ = (
|
||||
'Status',
|
||||
|
|
|
@ -4,6 +4,7 @@ import http
|
|||
import time
|
||||
import asyncio
|
||||
import warnings
|
||||
import ipaddress
|
||||
|
||||
from types import TracebackType
|
||||
from typing import Generic, Optional, Union, Type, List, Sequence, Any, cast
|
||||
|
@ -27,7 +28,7 @@ from .protocol import H2Protocol, AbstractHandler, Stream as _Stream, Peer
|
|||
from .metadata import Deadline, USER_AGENT, decode_grpc_message, encode_timeout
|
||||
from .metadata import encode_metadata, decode_metadata, _MetadataLike, _Metadata
|
||||
from .metadata import _STATUS_DETAILS_KEY, decode_bin_value
|
||||
from .exceptions import GRPCError, ProtocolError, StreamTerminatedError
|
||||
from .exceptions import GRPCError, ProtocolError, StreamTerminatedError, HTTPDetails
|
||||
from .encoding.base import GRPC_CONTENT_TYPE, CodecBase, StatusDetailsCodecBase
|
||||
from .encoding.proto import ProtoCodec, ProtoStatusDetailsCodec
|
||||
from .encoding.proto import _googleapis_available
|
||||
|
@ -294,13 +295,15 @@ class Stream(StreamIterator[_RecvType], Generic[_SendType, _RecvType]):
|
|||
if status is not None and status != _H2_OK:
|
||||
grpc_status = _H2_TO_GRPC_STATUS_MAP.get(status, Status.UNKNOWN)
|
||||
raise GRPCError(grpc_status,
|
||||
'Received :status = {!r}'.format(status))
|
||||
'Received :status = {!r}'.format(status),
|
||||
http_details=HTTPDetails(status, headers_map))
|
||||
|
||||
def _raise_for_content_type(self, headers_map: Dict[str, str]) -> None:
|
||||
content_type = headers_map.get('content-type')
|
||||
if content_type is None:
|
||||
raise GRPCError(Status.UNKNOWN,
|
||||
'Missing content-type header')
|
||||
'Missing content-type header',
|
||||
http_details=HTTPDetails(headers_map.get(":status"), headers_map))
|
||||
|
||||
base_content_type, _, sub_type = content_type.partition('+')
|
||||
sub_type = sub_type or ProtoCodec.__content_subtype__
|
||||
|
@ -310,19 +313,23 @@ class Stream(StreamIterator[_RecvType], Generic[_SendType, _RecvType]):
|
|||
):
|
||||
raise GRPCError(Status.UNKNOWN,
|
||||
'Invalid content-type: {!r}'
|
||||
.format(content_type))
|
||||
.format(content_type),
|
||||
http_details=HTTPDetails(headers_map.get(":status"), headers_map))
|
||||
|
||||
def _process_grpc_status(
|
||||
self, headers_map: Dict[str, str],
|
||||
) -> Tuple[Status, Optional[str], Any]:
|
||||
grpc_status = headers_map.get('grpc-status')
|
||||
if grpc_status is None:
|
||||
raise GRPCError(Status.UNKNOWN, 'Missing grpc-status header')
|
||||
raise GRPCError(Status.UNKNOWN,
|
||||
'Missing grpc-status header',
|
||||
http_details=HTTPDetails(headers_map.get(":status"), headers_map))
|
||||
try:
|
||||
status = Status(int(grpc_status))
|
||||
except ValueError:
|
||||
raise GRPCError(Status.UNKNOWN, ('Invalid grpc-status: {!r}'
|
||||
.format(grpc_status)))
|
||||
raise GRPCError(Status.UNKNOWN,
|
||||
'Invalid grpc-status: {!r}'.format(grpc_status),
|
||||
http_details=HTTPDetails(headers_map.get(":status"), headers_map))
|
||||
else:
|
||||
message, details = None, None
|
||||
if status is not Status.OK:
|
||||
|
@ -339,10 +346,15 @@ class Stream(StreamIterator[_RecvType], Generic[_SendType, _RecvType]):
|
|||
return status, message, details
|
||||
|
||||
def _raise_for_grpc_status(
|
||||
self, status: Status, message: Optional[str], details: Any,
|
||||
self,
|
||||
status: Status,
|
||||
message: Optional[str],
|
||||
details: Any,
|
||||
headers: Dict[str, str] = None
|
||||
) -> None:
|
||||
if status is not Status.OK:
|
||||
raise GRPCError(status, message, details)
|
||||
http_status = headers.get(":status") if headers is not None else None
|
||||
raise GRPCError(status, message, details, HTTPDetails(http_status, headers))
|
||||
|
||||
async def recv_initial_metadata(self) -> None:
|
||||
"""Coroutine to wait for headers with initial metadata from the server.
|
||||
|
@ -390,7 +402,7 @@ class Stream(StreamIterator[_RecvType], Generic[_SendType, _RecvType]):
|
|||
)
|
||||
self.trailing_metadata = tm
|
||||
|
||||
self._raise_for_grpc_status(status, message, details)
|
||||
self._raise_for_grpc_status(status, message, details, headers_map)
|
||||
else:
|
||||
im = decode_metadata(headers)
|
||||
im, = await self._dispatch.recv_initial_metadata(im)
|
||||
|
@ -523,20 +535,22 @@ class Stream(StreamIterator[_RecvType], Generic[_SendType, _RecvType]):
|
|||
await self.recv_trailing_metadata()
|
||||
|
||||
def _maybe_raise(self) -> None:
|
||||
headers_map = {}
|
||||
if self._stream.headers is not None:
|
||||
self._raise_for_status(dict(self._stream.headers))
|
||||
headers_map = dict(self._stream.headers)
|
||||
self._raise_for_status(headers_map)
|
||||
if self._stream.trailers is not None:
|
||||
status, message, details = self._process_grpc_status(
|
||||
dict(self._stream.trailers),
|
||||
)
|
||||
self._raise_for_grpc_status(status, message, details)
|
||||
self._raise_for_grpc_status(status, message, details, headers)
|
||||
elif self._stream.headers is not None:
|
||||
headers_map = dict(self._stream.headers)
|
||||
if 'grpc-status' in headers_map:
|
||||
status, message, details = self._process_grpc_status(
|
||||
headers_map,
|
||||
)
|
||||
self._raise_for_grpc_status(status, message, details)
|
||||
self._raise_for_grpc_status(status, message, details, headers_map)
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
|
@ -683,9 +697,8 @@ class Channel:
|
|||
self._codec = codec
|
||||
self._status_details_codec = status_details_codec
|
||||
self._ssl = ssl or None
|
||||
self._authority = '{}:{}'.format(self._host, self._port)
|
||||
self._scheme = 'https' if self._ssl else 'http'
|
||||
self._authority = '{}:{}'.format(self._host, self._port)
|
||||
self._authority = self._get_authority(self._host, self._port)
|
||||
self._h2_config = H2Configuration(
|
||||
client_side=True,
|
||||
header_encoding='ascii',
|
||||
|
@ -779,6 +792,15 @@ class Channel:
|
|||
ctx.set_alpn_protocols(['h2'])
|
||||
return ctx
|
||||
|
||||
def _get_authority(self, host: str, port: int) -> str:
|
||||
try:
|
||||
ipv6_address = ipaddress.IPv6Address(host)
|
||||
except ipaddress.AddressValueError:
|
||||
pass
|
||||
else:
|
||||
host = f"[{ipv6_address}]"
|
||||
return "{}:{}".format(host, port)
|
||||
|
||||
def request(
|
||||
self,
|
||||
name: str,
|
||||
|
|
|
@ -1,8 +1,15 @@
|
|||
from typing import Optional, Any
|
||||
from typing import Optional, Any, Dict
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from .const import Status
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class HTTPDetails:
|
||||
status: str = field(default="")
|
||||
headers: Dict[str, str] = field(default_factory=dict)
|
||||
|
||||
|
||||
class GRPCError(Exception):
|
||||
"""Expected error, may be raised during RPC call
|
||||
|
||||
|
@ -26,11 +33,13 @@ class GRPCError(Exception):
|
|||
`(e.g. server returned unsupported` ``:content-type`` `header)`
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
status: Status,
|
||||
message: Optional[str] = None,
|
||||
details: Any = None,
|
||||
self,
|
||||
status: Status,
|
||||
message: Optional[str] = None,
|
||||
details: Any = None,
|
||||
http_details: HTTPDetails = None
|
||||
) -> None:
|
||||
super().__init__(status, message, details)
|
||||
#: :py:class:`~grpclib.const.Status` of the error
|
||||
|
@ -39,6 +48,8 @@ class GRPCError(Exception):
|
|||
self.message = message
|
||||
#: Error details
|
||||
self.details = details
|
||||
#: Http details
|
||||
self.http_details = http_details
|
||||
|
||||
|
||||
class ProtocolError(Exception):
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import os
|
||||
import socket
|
||||
import tempfile
|
||||
import ipaddress
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -46,19 +47,27 @@ class ClientServer:
|
|||
channel = None
|
||||
channel_ctx = None
|
||||
|
||||
def __init__(self, *, host="127.0.0.1"):
|
||||
self.host = host
|
||||
|
||||
async def __aenter__(self):
|
||||
host = '127.0.0.1'
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.bind(('127.0.0.1', 0))
|
||||
_, port = s.getsockname()
|
||||
try:
|
||||
ipaddress.IPv6Address(self.host)
|
||||
except ipaddress.AddressValueError:
|
||||
family = socket.AF_INET
|
||||
else:
|
||||
family = socket.AF_INET6
|
||||
with socket.socket(family, socket.SOCK_STREAM) as s:
|
||||
s.bind((self.host, 0))
|
||||
_, port, *_ = s.getsockname()
|
||||
|
||||
dummy_service = DummyService()
|
||||
|
||||
self.server = Server([dummy_service])
|
||||
await self.server.start(host, port)
|
||||
await self.server.start(self.host, port)
|
||||
self.server_ctx = await self.server.__aenter__()
|
||||
|
||||
self.channel = Channel(host=host, port=port)
|
||||
self.channel = Channel(host=self.host, port=port)
|
||||
self.channel_ctx = await self.channel.__aenter__()
|
||||
dummy_stub = DummyServiceStub(self.channel)
|
||||
return dummy_service, dummy_stub
|
||||
|
@ -211,3 +220,12 @@ async def test_stream_stream_advanced():
|
|||
assert await stream.recv_message() == DummyReply(value='baz')
|
||||
|
||||
assert await stream.recv_message() is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(not socket.has_ipv6, reason="No IPv6 support")
|
||||
async def test_ipv6():
|
||||
async with ClientServer(host="::1") as (handler, stub):
|
||||
reply = await stub.UnaryUnary(DummyRequest(value='ping'))
|
||||
assert reply == DummyReply(value='pong')
|
||||
assert handler.log == [DummyRequest(value='ping')]
|
||||
|
|
Loading…
Reference in New Issue