Skip to content

Commit

Permalink
python: Backport the v8 Connection class to the v7 branch.
Browse files Browse the repository at this point in the history
  • Loading branch information
da-tanabe committed Apr 19, 2021
1 parent 7a62567 commit e9ddfcb
Show file tree
Hide file tree
Showing 16 changed files with 1,105 additions and 24 deletions.
75 changes: 75 additions & 0 deletions python/dazl/ledger/_offsets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Copyright (c) 2017-2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""
Utilities for working with ledger offsets.
This API is _not_ public!
"""
from __future__ import annotations

from typing import Any, Optional, Union

__all__ = [
"LedgerOffsetRange",
"UNTIL_END",
"FROM_BEGINNING_UNTIL_FOREVER",
"from_offset_until_forever",
]


class End:
"""
Marker object that denotes the current end of the ledger.
"""

def __hash__(self):
return 0

def __repr__(self):
return "END"


END = End()


class LedgerOffsetRange:
"""
Denotes an offset range on a ledger. This API is _not_ considered public.
The gRPC Ledger API and HTTP JSON API do not expose ledger offsets with the same semantics,
so this class actually represents the commonality between these two interfaces.
"""

def __init__(self, begin: Union[None, str], end: Union[None, End], /):
"""
Initialize a :class:`LedgerOffsetRange`.
:param begin:
The start of the stream. If ``None``, then read from the beginning of the ledger.
Otherwise, must be a legal ledger offset.
:param end:
The end of the stream. If ``None``, then keep reading from the stream forever; if
``END``, then terminate when reaching the _current_ end of stream. Note that offsets
are *not* allowed here, as the HTTP JSON API does not provide a mechanism for reading
*to* a specific transaction offset.
"""
self.begin = begin
self.end = end

def __eq__(self, other: Any) -> bool:
return (
isinstance(other, LedgerOffsetRange)
and self.begin == other.begin
and self.end == other.end
)

def __hash__(self):
return hash(self.begin) ^ hash(self.end)


UNTIL_END = LedgerOffsetRange(None, END)
FROM_BEGINNING_UNTIL_FOREVER = LedgerOffsetRange(None, None)


def from_offset_until_forever(offset: Optional[str]) -> LedgerOffsetRange:
return LedgerOffsetRange(offset, None)
22 changes: 22 additions & 0 deletions python/dazl/ledger/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Copyright (c) 2017-2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

__all__ = ["CallbackReturnWarning", "ProtocolWarning"]


class CallbackReturnWarning(Warning):
"""
Raised when a user callback on a stream returns a value. These objects have no meaning and are
ignored by dazl.
This warning is raised primarily because older versions of dazl interpreted returning commands
from a callback as a request to send commands to the underlying ledger, and this is not
supported in newer APIs.
"""


class ProtocolWarning(Warning):
"""
Warnings that are raised when dazl detects incompatibilities between the Ledger API server-side
implementation and dazl.
"""
13 changes: 13 additions & 0 deletions python/dazl/ledger/grpc/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,15 @@
# Copyright (c) 2017-2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

from ..config import Config
from .conn_aio import Connection

__all__ = ["connect", "Connection"]


def connect(**kwargs):
"""
Connect to a gRPC Ledger API implementation and return a connection that uses asyncio.
"""
config = Config.create(**kwargs)
return Connection(config)
31 changes: 31 additions & 0 deletions python/dazl/ledger/grpc/__init__.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Copyright (c) 2017-2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

from os import PathLike
from typing import Collection, Optional, Union

from ...prim import Party, TimeDeltaLike
from .conn_aio import Connection

__all__ = ["connect", "Connection"]

# TODO: Figure out clever ways to make this function's type signature easier to maintain while
# preserving its ease of use to callers.
def connect(
url: str,
*,
read_as: "Union[None, Party, Collection[Party]]" = None,
act_as: "Union[None, Party, Collection[Party]]" = None,
admin: "Optional[bool]" = False,
ledger_id: "Optional[str]" = None,
application_name: "Optional[str]" = None,
oauth_token: "Optional[str]" = None,
ca: "Optional[bytes]" = None,
ca_file: "Optional[PathLike]" = None,
cert: "Optional[bytes]" = None,
cert_file: "Optional[PathLike]" = None,
cert_key: "Optional[bytes]" = None,
cert_key_file: "Optional[PathLike]" = None,
connect_timeout: "Optional[TimeDeltaLike]" = None,
enable_http_proxy: "bool" = True,
) -> Connection: ...
71 changes: 71 additions & 0 deletions python/dazl/ledger/grpc/channel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Copyright (c) 2017-2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
from typing import List, Tuple, Union, cast
from urllib.parse import urlparse

from grpc import (
AuthMetadataContext,
AuthMetadataPlugin,
AuthMetadataPluginCallback,
ChannelCredentials,
composite_channel_credentials,
metadata_call_credentials,
ssl_channel_credentials,
)
from grpc.aio import Channel, insecure_channel, secure_channel # type: ignore

from ..config import Config

__all__ = ["create_channel"]


def create_channel(config: "Config") -> "Channel":
"""
Create a :class:`Channel` for the specified configuration.
"""
u = urlparse(config.url.url)

options = [
("grpc.max_send_message_length", -1),
("grpc.max_receive_message_length", -1),
]
if not config.url.use_http_proxy:
options.append(("grpc.enable_http_proxy", 0))

if (u.scheme in ("https", "grpcs")) or config.ssl:
credentials = ssl_channel_credentials(
root_certificates=config.ssl.ca,
private_key=config.ssl.cert_key,
certificate_chain=config.ssl.cert,
)
if config.access.token:
# The grpc Credential objects do not actually define a formal interface, and are
# used interchangeably in the code.
#
# Additionally there are some incorrect rules in the grpc-stubs typing rules that force
# us to work around the type system.
credentials = cast(
ChannelCredentials,
composite_channel_credentials(
credentials, metadata_call_credentials(GrpcAuth(config))
),
)
return secure_channel(u.netloc, credentials, options)
else:
return insecure_channel(u.netloc, options)


class GrpcAuth(AuthMetadataPlugin):
def __init__(self, config: "Config"):
self._config = config

def __call__(self, context: "AuthMetadataContext", callback: "AuthMetadataPluginCallback"):
# This overly verbose type signature is here to satisfy mypy and grpc-stubs
options = [] # type: List[Tuple[str, Union[str, bytes]]]

# TODO: Add support here for refresh tokens
token = self._config.access.token
if token:
options.append(("Authorization", "Bearer " + self._config.access.token))

callback(tuple(options), None)
8 changes: 3 additions & 5 deletions python/dazl/ledger/grpc/codec_aio.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
# References:
# * https://github.com/digital-asset/daml/blob/main/ledger-service/http-json/src/main/scala/com/digitalasset/http/CommandService.scala

from __future__ import annotations

from collections.abc import Mapping as _Mapping
from typing import Any, List, Optional, Sequence, Set, Tuple, Union

Expand Down Expand Up @@ -119,7 +117,7 @@ async def encode_command(self, cmd: Command) -> G_Command:
raise ValueError(f"unknown Command type: {cmd!r}")

async def encode_create_command(
self, template_id: Any, payload: ContractData
self, template_id: Union[str, Any], payload: ContractData
) -> G_CreateCommand:
item_type = await self._loader.do_with_retry(
lambda: self._lookup.template_name(template_id)
Expand Down Expand Up @@ -149,7 +147,7 @@ async def encode_exercise_command(

async def encode_create_and_exercise_command(
self,
template_id: TypeConName,
template_id: Union[str, TypeConName],
payload: ContractData,
choice_name: str,
argument: Optional[Any] = None,
Expand All @@ -171,7 +169,7 @@ async def encode_create_and_exercise_command(

async def encode_exercise_by_key_command(
self,
template_id: TypeConName,
template_id: Union[str, TypeConName],
choice_name: str,
key: Any,
argument: Optional[ContractData] = None,
Expand Down
Loading

0 comments on commit e9ddfcb

Please sign in to comment.