Skip to content

Commit

Permalink
python: Introduce a new connection API that is a bit less stateful.
Browse files Browse the repository at this point in the history
  • Loading branch information
da-tanabe committed Mar 17, 2021
1 parent 565f3b0 commit 391d920
Show file tree
Hide file tree
Showing 15 changed files with 1,182 additions and 36 deletions.
2 changes: 2 additions & 0 deletions python/dazl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
__all__ = [
"AIOPartyClient",
"Command",
"connect",
"ContractData",
"ContractId",
"CreateAndExerciseCommand",
Expand Down Expand Up @@ -50,6 +51,7 @@
exercise_by_key,
)
from .ledger import Command
from .ledger.grpc import connect
from .pretty.table import write_acs
from .prim import ContractData, ContractId, DazlError, FrozenDict as frozendict, Party
from .util.logging import setup_default_logger
Expand Down
5 changes: 3 additions & 2 deletions python/dazl/ledger/config/access.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import base64
from collections.abc import MutableSet as MutableSetBase, Set as SetBase
import json
from logging import Logger
import os
from pathlib import Path
from typing import (
Expand Down Expand Up @@ -64,12 +65,12 @@ def create_access(
application_name: Optional[str] = None,
oauth_token: Optional[str] = None,
oauth_token_file: Optional[str] = None,
logger: Optional[Logger] = None,
) -> AccessConfig:
"""
Create an appropriate instance of :class:`AccessConfig`.
See :meth:`Config.create` for a more detailed description of these parameters.
"""
# admin = None is effectively the same as admin = False in this context
is_property_based = read_as or act_as or admin or ledger_id or application_name
Expand All @@ -88,7 +89,7 @@ def create_access(
raise ConfigError("no oauth token access or read_as/act_as/admin was specified")

# how do they configure thee? let me count the ways...
if sum(map(int, (is_property_based, oauth_token, oauth_token_file))):
if sum(map(int, (bool(is_property_based), bool(oauth_token), bool(oauth_token_file)))) > 1:
raise ConfigError(
"must specify ONE of read_as/act_as/admin, oauth_token, or oauth_token_file"
)
Expand Down
2 changes: 2 additions & 0 deletions python/dazl/ledger/config/ssl.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Copyright (c) 2017-2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
from logging import Logger
from os import PathLike, fspath
from typing import TYPE_CHECKING, Optional

Expand All @@ -25,6 +26,7 @@ def __init__(
cert_file: "Optional[PathLike]" = None,
cert_key: "Optional[bytes]" = None,
cert_key_file: "Optional[PathLike]" = None,
logger: Optional[Logger] = None,
):
self._ca: Optional[bytes]
self._cert: Optional[bytes]
Expand Down
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.
"""
42 changes: 42 additions & 0 deletions python/dazl/ledger/grpc/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# 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, overload

from ...prim import Party, TimeDeltaLike
from ..config import Config
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.
@overload
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:
...


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

from grpc import (
AuthMetadataContext,
AuthMetadataPlugin,
AuthMetadataPluginCallback,
composite_channel_credentials,
metadata_call_credentials,
ssl_channel_credentials,
)
from grpc.aio import Channel, insecure_channel, secure_channel

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:
credentials = 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"):
options = []

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

callback(options, None)
2 changes: 1 addition & 1 deletion python/dazl/ledger/grpc/codec_aio.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ async def decode_created_event(self, event: G_CreatedEvent) -> CreateEvent:
key = await self.decode_value(template.key.type, event.key)

return CreateEvent(
cid, cdata, event.signatories, event.observers, event.agreement_text.Value, key
cid, cdata, event.signatories, event.observers, event.agreement_text.value, key
)

async def decode_archived_event(self, event: G_ArchivedEvent) -> ArchiveEvent:
Expand Down
Loading

0 comments on commit 391d920

Please sign in to comment.