Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

python: Next-gen dazl connection API #108

Merged
merged 3 commits into from
Mar 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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