diff --git a/python/dazl/__init__.py b/python/dazl/__init__.py index 99ea761c..dbb190d7 100644 --- a/python/dazl/__init__.py +++ b/python/dazl/__init__.py @@ -4,12 +4,43 @@ """ This module contains the Python API for interacting with the Ledger API. """ +from ast import literal_eval +from configparser import ConfigParser +from pathlib import Path + +import pkg_resources + +__all__ = [ + "AIOPartyClient", + "Command", + "ContractData", + "ContractId", + "CreateAndExerciseCommand", + "CreateCommand", + "DazlError", + "ExerciseByKeyCommand", + "ExerciseCommand", + "LOG", + "Network", + "Party", + "SimplePartyClient", + "__version__", + "async_network", + "create", + "create_and_exercise", + "exercise", + "exercise_by_key", + "frozendict", + "run", + "setup_default_logger", + "simple_client", + "write_acs", +] + + from ._logging import LOG from .client import AIOPartyClient, Network, SimplePartyClient, async_network, run, simple_client -from .pretty.table import write_acs -from .prim import ContractData, ContractId, DazlError, FrozenDict as frozendict, Party -from .protocols.commands import ( - Command, +from .client.commands import ( CreateAndExerciseCommand, CreateCommand, ExerciseByKeyCommand, @@ -19,6 +50,9 @@ exercise, exercise_by_key, ) +from .pretty.table import write_acs +from .prim import ContractData, ContractId, DazlError, FrozenDict as frozendict, Party +from .protocols.commands import Command from .util.logging import setup_default_logger try: @@ -45,12 +79,6 @@ def _get_version() -> str: 2. Use the value from the local pyproject.toml file (this is used when running dazl from source). """ - from ast import literal_eval - from configparser import ConfigParser - from pathlib import Path - - import pkg_resources - try: return pkg_resources.require("dazl")[0].version except pkg_resources.DistributionNotFound: diff --git a/python/dazl/client/_events.py b/python/dazl/client/_events.py index 17ae6300..5ce42555 100644 --- a/python/dazl/client/_events.py +++ b/python/dazl/client/_events.py @@ -3,8 +3,8 @@ from typing import Awaitable, Callable, TypeVar, Union -from ..protocols.commands import EventHandlerResponse from ..protocols.events import BaseEvent +from .commands import EventHandlerResponse E = TypeVar("E", bound=BaseEvent) T = TypeVar("T") diff --git a/python/dazl/client/_party_client_impl.py b/python/dazl/client/_party_client_impl.py index 9e77b790..03dd171c 100644 --- a/python/dazl/client/_party_client_impl.py +++ b/python/dazl/client/_party_client_impl.py @@ -1,6 +1,5 @@ # Copyright (c) 2017-2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. # SPDX-License-Identifier: Apache-2.0 - from asyncio import Future, ensure_future, gather, get_event_loop, wait from concurrent.futures import ALL_COMPLETED from dataclasses import asdict, dataclass, field, fields, replace @@ -20,17 +19,12 @@ TypeVar, ) import uuid +import warnings from .. import LOG from ..damlast.daml_lf_1 import TypeConName from ..prim import ContractId, Party, TimeDeltaLike, to_timedelta from ..protocols import LedgerClient, LedgerNetwork -from ..protocols.commands import ( - CommandBuilder, - CommandDefaults, - CommandPayload, - EventHandlerResponse, -) from ..protocols.events import ( ActiveContractSetEvent, BaseEvent, @@ -52,6 +46,7 @@ from ._conn_settings import OAuthSettings, connection_settings from ._writer_verify import ValidateSerializer from .bots import Bot, BotCallback, BotCollection, BotFilter +from .commands import CommandBuilder, CommandDefaults, CommandPayload, EventHandlerResponse from .config import NetworkConfig, PartyConfig from .ledger import LedgerMetadata from .state import ( @@ -455,7 +450,7 @@ def _process_transaction_stream_event(self, event: Any, raise_events: bool) -> F def write_commands( self, - commands: EventHandlerResponse, + commands: "EventHandlerResponse", ignore_errors: bool = False, workflow_id: "Optional[str]" = None, deduplication_time: "Optional[TimeDeltaLike]" = None, @@ -481,7 +476,9 @@ def write_commands( """ if workflow_id is None: workflow_id = uuid.uuid4().hex - cb = CommandBuilder.coerce(commands, atomic_default=True) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + cb = CommandBuilder.coerce(commands, atomic_default=True) cb.defaults( workflow_id=workflow_id, deduplication_time=( diff --git a/python/dazl/client/_writer_verify.py b/python/dazl/client/_writer_verify.py index 34eeecd0..59138476 100644 --- a/python/dazl/client/_writer_verify.py +++ b/python/dazl/client/_writer_verify.py @@ -2,17 +2,13 @@ # SPDX-License-Identifier: Apache-2.0 from typing import Any +import warnings from ..damlast.daml_lf_1 import TypeConName from ..prim import ContractId -from ..protocols.commands import ( - AbstractSerializer, - CreateAndExerciseCommand, - CreateCommand, - ExerciseByKeyCommand, - ExerciseCommand, -) +from ..protocols.commands import AbstractSerializer from ..values import CanonicalMapper +from .commands import CreateAndExerciseCommand, CreateCommand, ExerciseByKeyCommand, ExerciseCommand class ValidateSerializer(AbstractSerializer): @@ -29,12 +25,16 @@ class ValidateSerializer(AbstractSerializer): def serialize_create_command( self, name: "TypeConName", template_args: "Any" ) -> "CreateCommand": - return CreateCommand(template=name, arguments=template_args) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + return CreateCommand(template=name, arguments=template_args) def serialize_exercise_command( self, contract_id: "ContractId", choice_name: str, choice_args: "Any" ) -> "ExerciseCommand": - return ExerciseCommand(contract=contract_id, choice=choice_name, arguments=choice_args) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + return ExerciseCommand(contract=contract_id, choice=choice_name, arguments=choice_args) def serialize_exercise_by_key_command( self, @@ -43,12 +43,14 @@ def serialize_exercise_by_key_command( choice_name: str, choice_arguments: "Any", ) -> "ExerciseByKeyCommand": - return ExerciseByKeyCommand( - template=template_name, - contract_key=key_arguments, - choice=choice_name, - choice_argument=choice_arguments, - ) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + return ExerciseByKeyCommand( + template=template_name, + contract_key=key_arguments, + choice=choice_name, + choice_argument=choice_arguments, + ) def serialize_create_and_exercise_command( self, @@ -57,9 +59,11 @@ def serialize_create_and_exercise_command( choice_name: str, choice_arguments: "Any", ) -> "CreateAndExerciseCommand": - return CreateAndExerciseCommand( - template=template_name, - arguments=create_arguments, - choice=choice_name, - choice_argument=choice_arguments, - ) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + return CreateAndExerciseCommand( + template=template_name, + arguments=create_arguments, + choice=choice_name, + choice_argument=choice_arguments, + ) diff --git a/python/dazl/client/api.py b/python/dazl/client/api.py index a24e8e77..b6567f90 100644 --- a/python/dazl/client/api.py +++ b/python/dazl/client/api.py @@ -43,7 +43,6 @@ from ..damlast.protocols import SymbolLookup from ..metrics import MetricEvents from ..prim import ContractData, ContractId, Party, TimeDeltaLike, to_party -from ..protocols.commands import EventHandlerResponse from ..protocols.events import ( ContractArchiveEvent, ContractCreateEvent, @@ -70,6 +69,7 @@ from ._network_client_impl import _NetworkImpl from ._party_client_impl import _PartyClientImpl from .bots import Bot, BotCollection +from .commands import EventHandlerResponse from .config import AnonymousNetworkConfig, NetworkConfig, PartyConfig from .events import EventKey from .ledger import LedgerMetadata diff --git a/python/dazl/client/bots.py b/python/dazl/client/bots.py index 86f672b1..4f8086f6 100644 --- a/python/dazl/client/bots.py +++ b/python/dazl/client/bots.py @@ -30,9 +30,10 @@ from .. import LOG from ..prim import Party -from ..protocols.commands import Command, CommandBuilder +from ..protocols.commands import Command from ..protocols.events import BaseEvent from ..util.asyncio_util import LongRunningAwaitable, Signal, completed, failed, propagate +from .commands import CommandBuilder from .events import EventKey if TYPE_CHECKING: diff --git a/python/dazl/client/commands.py b/python/dazl/client/commands.py new file mode 100644 index 00000000..7f9f0792 --- /dev/null +++ b/python/dazl/client/commands.py @@ -0,0 +1,559 @@ +# Copyright (c) 2017-2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Command types that are used in the dazl v5 API. + +These symbols are primarily kept for backwards compatibility. +""" +from dataclasses import dataclass, fields +from datetime import timedelta +from typing import Any, Collection, List, Mapping, Optional, Sequence, Union +import uuid +import warnings + +from ..damlast.daml_lf_1 import TypeConName +from ..prim import ContractData, ContractId, Party +from ..protocols import commands as pcmd + +__all__ = [ + "CommandBuilder", + "CommandDefaults", + "CommandPayload", + "CommandsOrCommandSequence", + "CreateAndExerciseCommand", + "CreateCommand", + "EventHandlerResponse", + "ExerciseByKeyCommand", + "ExerciseCommand", + "create", + "create_and_exercise", + "exercise", + "exercise_by_key", + "flatten_command_sequence", +] + + +class CreateCommand(pcmd.CreateCommand): + """ + A command that creates a contract without any predecessors. + """ + + def __init__( + self, template: "Union[str, TypeConName]", arguments: "Optional[ContractData]" = None + ): + warnings.warn( + "dazl.client.commands.CreateCommand is deprecated; " + "prefer calling dazl.protocols.ledgerapi.Connection.create or " + "dazl.client.PartyClient.submit_create, " + "or use dazl.protocols.commands.CreateCommand instead.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__(template, arguments) + + @property + def template_type(self) -> "TypeConName": + """ + Use :prop:`template_id` instead. + """ + warnings.warn( + "CreateCommand.template_type is deprecated; use CreateCommand.template_id instead.", + DeprecationWarning, + stacklevel=2, + ) + return self.template_id + + @property + def arguments(self) -> "Mapping[str, Any]": + """ + Use :prop:`payload` instead. + """ + warnings.warn( + "CreateCommand.arguments is deprecated; use CreateCommand.payload instead.", + DeprecationWarning, + stacklevel=2, + ) + return self.payload + + +class CreateAndExerciseCommand(pcmd.CreateAndExerciseCommand): + """ + A command that exercises a choice on a newly-created contract, in a single transaction. + """ + + def __init__( + self, + template: "Union[str, TypeConName]", + arguments: "Mapping[str, Any]", + choice: str, + choice_argument: "Optional[Any]" = None, + ): + warnings.warn( + "dazl.client.commands.CreateAndExerciseCommand is deprecated; " + "prefer calling dazl.protocols.ledgerapi.Connection.create_and_exercise or " + "dazl.client.PartyClient.submit_create_and_exercise, " + "or use dazl.protocols.commands.CreateAndExerciseCommand instead.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__(template, arguments, choice, choice_argument) + + @property + def template_type(self) -> "TypeConName": + """ + Use :prop:`template_id` instead. + """ + warnings.warn( + "CreateAndExerciseCommand.template_type is deprecated; " + "use CreateAndExerciseCommand.template_id instead.", + DeprecationWarning, + stacklevel=2, + ) + return self.template_id + + @property + def arguments(self) -> "Any": + """ + Use :prop:`payload` instead. + """ + warnings.warn( + "CreateAndExerciseCommand.arguments is deprecated; " + "use CreateAndExerciseCommand.payload instead.", + DeprecationWarning, + stacklevel=2, + ) + return self.payload + + @property + def choice_argument(self) -> "Any": + """ + Use :prop:`argument` instead. + """ + warnings.warn( + "CreateAndExerciseCommand.choice_argument is deprecated; " + "use CreateAndExerciseCommand.argument instead.", + DeprecationWarning, + stacklevel=2, + ) + return self.argument + + +class ExerciseCommand(pcmd.ExerciseCommand): + """ + A command that exercises a choice on a contract identified by its contract ID. + """ + + def __init__(self, contract: "ContractId", choice: str, arguments: "Optional[Any]" = None): + warnings.warn( + "dazl.client.commands.ExerciseCommand is deprecated; " + "prefer calling dazl.protocols.ledgerapi.Connection.exercise or " + "dazl.client.PartyClient.submit_exercise, " + "or use dazl.protocols.commands.ExerciseCommand instead.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__(contract, choice, arguments) + + @property + def contract(self) -> "ContractId": + """ + Use :prop:`contract_id` instead. + """ + warnings.warn( + "ExerciseCommand.contract is deprecated; use ExerciseCommand.contract_id instead.", + DeprecationWarning, + stacklevel=2, + ) + return self.contract_id + + def arguments(self) -> "Any": + """ + Use :prop:`argument` instead. + """ + warnings.warn( + "ExerciseCommand.arguments is deprecated; use ExerciseCommand.argument instead.", + DeprecationWarning, + stacklevel=2, + ) + return self.argument + + +class ExerciseByKeyCommand(pcmd.ExerciseByKeyCommand): + def __init__( + self, + template: "Union[str, TypeConName]", + contract_key: "Any", + choice: str, + choice_argument: "Any", + ): + warnings.warn( + "dazl.client.commands.ExerciseByKeyCommand is deprecated; " + "prefer calling dazl.protocols.ledgerapi.Connection.exercise_by_key or " + "dazl.client.PartyClient.submit_exercise_by_key, " + "or use dazl.protocols.commands.ExerciseByKeyCommand instead.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__(template, contract_key, choice, choice_argument) + + @property + def template_type(self) -> "TypeConName": + """ + Use :prop:`template_id` instead. + """ + warnings.warn( + "ExerciseByKeyCommand.template_type is deprecated; " + "use ExerciseByKeyCommand.template_id instead.", + DeprecationWarning, + stacklevel=2, + ) + return self.template_id + + @property + def contract_key(self) -> "Any": + """ + Use :prop:`argument` instead. + """ + warnings.warn( + "ExerciseByKeyCommand.contract_key is deprecated; " + "use ExerciseByKeyCommand.key instead.", + DeprecationWarning, + stacklevel=2, + ) + + return self.key + + @property + def choice_argument(self) -> "Any": + """ + Use :prop:`argument` instead. + """ + warnings.warn( + "ExerciseByKeyCommand.choice_argument is deprecated; " + "use ExerciseByKeyCommand.argument instead.", + DeprecationWarning, + stacklevel=2, + ) + return self.argument + + +CommandsOrCommandSequence = Union[None, pcmd.Command, Sequence[Optional[pcmd.Command]]] + + +# noinspection PyDeprecation +class CommandBuilder: + """ + Builder class for generating commands to be sent to the ledger. + """ + + @classmethod + def coerce(cls, obj, atomic_default=False) -> "CommandBuilder": + """ + Create a :class:`CommandBuilder` from the objects that an event handler is allowed to + return. + + :param obj: + :param atomic_default: + :return: + """ + warnings.warn( + "CommandBuilder is deprecated; " + "prefer calling dazl.protocols.ledgerapi.Connection.commands, " + "dazl.client.PartyClient.submit, or construct commands explicitly instead.", + DeprecationWarning, + stacklevel=2, + ) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + if isinstance(obj, CommandBuilder): + return obj + + builder = CommandBuilder(atomic_default=atomic_default) + if obj is not None: + builder.append(obj) + return builder + + def __init__(self, atomic_default: bool = False): + warnings.warn( + "CommandBuilder is deprecated; " + "prefer calling dazl.protocols.ledgerapi.Connection.commands, " + "dazl.client.PartyClient.submit, or construct commands explicitly instead.", + DeprecationWarning, + stacklevel=2, + ) + self._atomic_default = atomic_default + self._commands = [[]] # type: List[List[pcmd.Command]] + self._defaults = CommandDefaults() + + def defaults( + self, + party: "Optional[Party]" = None, + ledger_id: "Optional[str]" = None, + workflow_id: "Optional[str]" = None, + application_id: "Optional[str]" = None, + command_id: "Optional[str]" = None, + deduplication_time: "Optional[timedelta]" = None, + ) -> None: + if party is not None: + self._defaults.default_party = party + if ledger_id is not None: + self._defaults.ledger_id = ledger_id + if workflow_id is not None: + self._defaults.default_workflow_id = workflow_id + if application_id is not None: + self._defaults.default_application_id = application_id + if command_id is not None: + self._defaults.default_command_id = command_id + if deduplication_time is not None: + self._defaults.default_deduplication_time = deduplication_time + + def create(self, template, arguments=None) -> "CommandBuilder": + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + return self.append(create(template, arguments=arguments)) + + def exercise(self, contract, choice, arguments=None) -> "CommandBuilder": + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + return self.append(exercise(contract, choice, arguments=arguments)) + + def create_and_exercise( + self, template, create_arguments, choice_name, choice_arguments=None + ) -> "CommandBuilder": + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + return self.append( + create_and_exercise(template, create_arguments, choice_name, choice_arguments) + ) + + def append(self, *commands: "CommandsOrCommandSequence") -> "CommandBuilder": + """ + Append one or more commands, or list of commands to the :class:`CommandBuilder` in flight. + This method respects the value of ``atomic_default`` that this object was constructed with. + In order to force commands to be submitted either atomically, use :meth:`append_atomically`. + To allow these commands to be submitted in parallel use :meth:`append_nonatomically`. + + :param commands: One or more commands, or list of commands to be submitted to the ledger. + :return: This object. + """ + if self._atomic_default: + # a command builder that defaults to being atomic will put all commands in a single + # transaction; build on the very first transaction + self._commands[0].extend(flatten_command_sequence(commands)) + return self + else: + return self.append_nonatomically(*commands) + + def append_atomically( + self, *commands: "Union[pcmd.Command, Sequence[pcmd.Command]]" + ) -> "CommandBuilder": + self._commands.extend([flatten_command_sequence(commands)]) + return self + + def append_nonatomically( + self, *commands: "Union[pcmd.Command, Sequence[pcmd.Command]]" + ) -> "CommandBuilder": + self._commands.extend([[cmd] for cmd in flatten_command_sequence(commands)]) + return self + + def build(self, defaults: "Optional[CommandDefaults]" = None) -> "Collection[CommandPayload]": + """ + Return a collection of commands. + """ + if defaults is None: + raise ValueError("defaults must currently be specified") + + command_id = ( + defaults.default_command_id or self._defaults.default_command_id or uuid.uuid4().hex + ) + + return [ + CommandPayload( + party=defaults.default_party or self._defaults.default_party, + ledger_id=defaults.default_ledger_id or self._defaults.default_ledger_id, + workflow_id=defaults.default_workflow_id or self._defaults.default_workflow_id, + application_id=defaults.default_application_id + or self._defaults.default_application_id, + command_id=command_id, + deduplication_time=defaults.default_deduplication_time + or self._defaults.default_deduplication_time, + commands=commands, + ) + for i, commands in enumerate(self._commands) + if commands + ] + + def __format__(self, format_spec): + if format_spec == "c": + return str(self._commands) + else: + return repr(self) + + def __repr__(self): + return f"CommandBuilder({self._commands})" + + +def flatten_command_sequence( + commands: "Sequence[CommandsOrCommandSequence]", +) -> "List[pcmd.Command]": + """ + Convert a list of mixed commands, ``None``, and list of commands into an ordered sequence of + non-``None`` commands. + """ + ret = [] # type: List[pcmd.Command] + errors = [] + + for i, obj in enumerate(commands): + if obj is not None: + if isinstance(obj, pcmd.Command): + ret.append(obj) + else: + try: + cmd_iter = iter(obj) + except TypeError: + errors.append(((i,), obj)) + continue + for j, cmd in enumerate(cmd_iter): + if isinstance(cmd, pcmd.Command): + ret.append(cmd) + else: + errors.append(((i, j), cmd)) + if errors: + raise ValueError( + f"Failed to interpret some elements as Commands in the list: " f"$[{index}] = {command}" + for index, command in errors + ) + return ret + + +@dataclass +class CommandDefaults: + """ + Values to use for a :class:`Command` when no value is specified with the creation of the + command. + """ + + default_party: Optional[Party] = None + default_ledger_id: Optional[str] = None + default_workflow_id: Optional[str] = None + default_application_id: Optional[str] = None + default_command_id: Optional[str] = None + default_deduplication_time: Optional[timedelta] = None + + +@dataclass(frozen=True) +class CommandPayload: + """ + A request to mutate active state of the ledger. + + .. attribute:: CommandPayload.party + The party submitting the request. + .. attribute:: CommandPayload.application_id: + An optional application ID to accompany the request. + .. attribute:: CommandPayload.command_id: + A hash that represents the BIM commitment. + .. attribute:: CommandPayload.deduplication_time: + The maximum time interval before the client should consider this command expired. + .. attribute:: CommandPayload.commands + A sequence of commands to submit to the ledger. These commands are submitted atomically + (in other words, they all succeed or they all fail). + .. attribute:: CommandPayload.deduplication_time: + The length of the time window during which all commands with the same party and command ID + will be deduplicated. Duplicate commands submitted before the end of this window return an + ``ALREADY_EXISTS`` error. + """ + + party: Party + ledger_id: str + workflow_id: str + application_id: str + command_id: str + commands: "Sequence[pcmd.Command]" + deduplication_time: "Optional[timedelta]" = None + + def __post_init__(self): + missing_fields = [ + field.name + for field in fields(self) + if field.name != "deduplication_time" and getattr(self, field.name) is None + ] + if missing_fields: + raise ValueError( + f"Some fields are set to None when they are required: " f"{missing_fields}" + ) + + +# noinspection PyDeprecation +def create(template, arguments=None): + warnings.warn( + "dazl.client.commands.create is deprecated; " + "prefer calling dazl.protocols.ledgerapi.Connection.create or " + "dazl.client.PartyClient.submit_create, " + "or use dazl.protocols.commands.CreateCommand instead.", + DeprecationWarning, + stacklevel=2, + ) + if not isinstance(template, str): + raise ValueError( + "template must be a string name, a template type, or an instantiated template" + ) + + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + return CreateCommand(template, arguments) + + +# noinspection PyDeprecation +def create_and_exercise(template, create_arguments, choice_name, choice_argument): + warnings.warn( + "dazl.client.commands.CreateAndExerciseCommand is deprecated; " + "prefer calling dazl.protocols.ledgerapi.Connection.create_and_exercise or " + "dazl.client.PartyClient.submit_create_and_exercise, " + "or use dazl.protocols.commands.CreateAndExerciseCommand instead.", + DeprecationWarning, + stacklevel=2, + ) + + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + return CreateAndExerciseCommand(template, create_arguments, choice_name, choice_argument) + + +# noinspection PyDeprecation +def exercise(contract, choice, arguments=None): + warnings.warn( + "dazl.client.commands.exercise is deprecated; " + "prefer calling dazl.protocols.ledgerapi.Connection.exercise or " + "dazl.client.PartyClient.submit_exercise, " + "or use dazl.protocols.commands.ExerciseCommand instead.", + DeprecationWarning, + stacklevel=2, + ) + + if not isinstance(choice, str): + raise ValueError( + "choice must be a string name, a template type, or an instantiated template" + ) + + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + return ExerciseCommand(contract, choice, arguments) + + +# noinspection PyDeprecation +def exercise_by_key(template, contract_key, choice_name, choice_argument): + warnings.warn( + "dazl.client.commands.ExerciseByKeyCommand is deprecated; " + "prefer calling dazl.protocols.ledgerapi.Connection.exercise_by_key or " + "dazl.client.PartyClient.submit_exercise_by_key, " + "or use dazl.protocols.commands.ExerciseByKeyCommand instead.", + DeprecationWarning, + stacklevel=2, + ) + + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + return ExerciseByKeyCommand(template, contract_key, choice_name, choice_argument) + + +EventHandlerResponse = Union[CommandsOrCommandSequence, CommandBuilder, CommandPayload] diff --git a/python/dazl/model/writing.py b/python/dazl/model/writing.py index 6a5044c9..2cef0f01 100644 --- a/python/dazl/model/writing.py +++ b/python/dazl/model/writing.py @@ -6,25 +6,27 @@ ``dazl.protocol.commands``. """ -from ..protocols.commands import ( - AbstractSerializer, - Command, +from ..client.commands import ( CommandBuilder, CommandDefaults, CommandPayload, CommandsOrCommandSequence, - CreateAndExerciseCommand, - CreateCommand, EventHandlerResponse, - ExerciseByKeyCommand, - ExerciseCommand, - Serializer, create, create_and_exercise, exercise, exercise_by_key, flatten_command_sequence, ) +from ..protocols.commands import ( + AbstractSerializer, + Command, + CreateAndExerciseCommand, + CreateCommand, + ExerciseByKeyCommand, + ExerciseCommand, + Serializer, +) __all__ = [ "AbstractSerializer", diff --git a/python/dazl/protocols/_base.py b/python/dazl/protocols/_base.py index 43b5e0f9..8ab61d7b 100644 --- a/python/dazl/protocols/_base.py +++ b/python/dazl/protocols/_base.py @@ -13,13 +13,13 @@ from .. import LOG from ..damlast.protocols import SymbolLookup from ..prim import Party -from ..protocols.commands import CommandPayload from ..scheduler import Invoker from ..util.typing import safe_cast, safe_optional_cast from .events import BaseEvent, ContractFilter, TransactionFilter if TYPE_CHECKING: from ..client._conn_settings import HTTPConnectionSettings + from ..client.commands import CommandPayload from ..client.ledger import LedgerMetadata __all__ = ["LedgerConnectionOptions", "LedgerNetwork", "LedgerClient"] diff --git a/python/dazl/protocols/commands.py b/python/dazl/protocols/commands.py index af8ceda2..b949e0b7 100644 --- a/python/dazl/protocols/commands.py +++ b/python/dazl/protocols/commands.py @@ -17,21 +17,25 @@ .. autoclass:: ExerciseCommand :members: """ -from dataclasses import dataclass, fields -from datetime import timedelta -from typing import Any, Collection, List, Mapping, Optional, Sequence, Union -import uuid +from typing import Any, Mapping, NoReturn, Optional, Sequence, Union from ..damlast.daml_lf_1 import Type, TypeConName from ..damlast.daml_types import con from ..damlast.lookup import find_choice, parse_type_con_name from ..damlast.protocols import SymbolLookup -from ..prim import ContractId, Party +from ..prim import ContractData, ContractId from ..util.typing import safe_cast from ..values import ValueMapper -CommandsOrCommandSequence = Union[None, "Command", Sequence[Optional["Command"]]] -EventHandlerResponse = Union[CommandsOrCommandSequence, "CommandBuilder", "CommandPayload"] +__all__ = [ + "Command", + "CreateCommand", + "CreateAndExerciseCommand", + "ExerciseCommand", + "ExerciseByKeyCommand", + "Serializer", + "AbstractSerializer", +] class Command: @@ -39,361 +43,221 @@ class Command: Base class for write-side commands. """ + def __setattr__(self, key, value) -> "NoReturn": + """ + Raise :class:`AttributeError`; instances of this class are immutable. + """ + raise AttributeError("Command instances are read-only") + -@dataclass(init=False, frozen=True) class CreateCommand(Command): """ A command that creates a contract without any predecessors. - - .. attribute:: CreateCommand.template - - Refers to the type of a template. This can be passed in as a ``str`` to the constructor, - where it assumed to represent the ID or name of a template. - - .. attribute:: CreateCommand.arguments - - The arguments to the create (as a ``dict``). - """ - - __slots__ = ("template_type", "arguments") - - template_type: "TypeConName" - arguments: "Mapping[str, Any]" - - def __init__(self, template: "Union[str, TypeConName]", arguments=None): - object.__setattr__(self, "template_type", validate_template_id(template)) - object.__setattr__(self, "arguments", arguments or dict()) - - def __repr__(self): - return f"" - - -@dataclass(init=False, frozen=True) -class ExerciseCommand(Command): - """ - A command that exercises a choice on a pre-existing contract. - - .. attribute:: ExerciseCommand.contract - - The :class:`ContractId` on which a choice is being exercised. - - .. attribute:: ExerciseCommand.choice - - Refers to a choice (either a :class:`ChoiceRef` or a :class:`ChoiceMetadata`). - This can be passed in as a ``str`` to the constructor, where it assumed to represent the - name of a choice. - - .. attribute:: ExerciseCommand.arguments - - The arguments to the exercise choice (as a ``dict``). - - Note that when an ``ExerciseCommand`` is created, an additional ``template_id`` parameter can - be supplied to the constructor to aid in disambiguation of the specific choice being invoked. - In some situations involving composite commands, a ``template_id`` must eventually be supplied - before a choice can be exercised. If this ``template_id`` is specified, the ``contract`` and - ``choice`` are both tagged with this ID. - - Instance methods: - - .. automethod:: replace """ - __slots__ = ("contract", "choice", "arguments") + __slots__ = ("_template_id", "_payload") - contract: "ContractId" - choice: str - arguments: "Optional[Any]" + def __init__(self, template_id: "Union[str, TypeConName]", payload: "ContractData"): + """ + Initialize a :class:`CreateCommand`. - def __init__(self, contract: "ContractId", choice: str, arguments: "Optional[Any]" = None): - object.__setattr__(self, "contract", safe_cast(ContractId, contract)) - object.__setattr__(self, "choice", safe_cast(str, choice)) - object.__setattr__(self, "arguments", dict(arguments) if arguments is not None else dict()) + :param template_id: + The template of the contract to be created. + :param payload: + The template arguments for the contract to be created. + """ + object.__setattr__(self, "_template_id", validate_template_id(template_id)) + object.__setattr__(self, "_payload", payload) - def __repr__(self): - return f"" + @property + def template_id(self) -> "TypeConName": + """ + Return the template of the contract to be created. + """ + return self._template_id + @property + def payload(self) -> "Mapping[str, Any]": + """ + Return the template arguments for the contract to be created. + """ + return self._payload -@dataclass(init=False, frozen=True) -class ExerciseByKeyCommand(Command): - template_type: "TypeConName" - contract_key: "Any" - choice: str - choice_argument: "Any" + def __repr__(self) -> str: + return f"CreateCommand({self.template_id}, {self.payload})" - def __init__( - self, - template: "Union[str, TypeConName]", - contract_key: "Any", - choice: str, - choice_argument: "Any", - ): - object.__setattr__(self, "template_type", validate_template_id(template)) - object.__setattr__(self, "contract_key", contract_key) - object.__setattr__(self, "choice", choice) - object.__setattr__(self, "choice_argument", choice_argument) + def __eq__(self, other: "Any") -> bool: + return ( + isinstance(other, CreateCommand) + and self.template_id == other.template_id + and self.payload == other.payload + ) -@dataclass(init=False, frozen=True) class CreateAndExerciseCommand(Command): - template_type: "TypeConName" - arguments: "Mapping[str, Any]" - choice: str - choice_argument: "Any" + __slots__ = ("_template_id", "_payload", "_choice", "_argument") - def __init__( - self, - template: "Union[str, TypeConName]", - arguments: "Mapping[str, Any]", - choice: str, - choice_argument: "Any", - ): - object.__setattr__(self, "template_type", validate_template_id(template)) - object.__setattr__(self, "arguments", arguments) - object.__setattr__(self, "choice", choice) - object.__setattr__(self, "choice_argument", choice_argument) + def __init__(self, template_id, payload, choice, argument: "Optional[Any]" = None): + object.__setattr__(self, "_template_id", validate_template_id(template_id)) + object.__setattr__(self, "_payload", payload) + object.__setattr__(self, "_choice", choice) + object.__setattr__(self, "_argument", argument) + @property + def template_id(self) -> "TypeConName": + return self._template_id -class CommandBuilder: - """ - Builder class for generating commands to be sent to the ledger. - """ + @property + def payload(self) -> "ContractData": + return self._payload - @classmethod - def coerce(cls, obj, atomic_default=False) -> "CommandBuilder": + @property + def choice(self) -> str: """ - Create a :class:`CommandBuilder` from the objects that an event handler is allowed to - return. + Return the choice to exercise. + """ + return self._choice - :param obj: - :param atomic_default: - :return: + @property + def argument(self) -> "Any": """ - if isinstance(obj, CommandBuilder): - return obj + Return the choice arguments. + """ + return self._argument + + def __eq__(self, other: "Any") -> bool: + return ( + isinstance(other, CreateAndExerciseCommand) + and self.template_id == other.template_id + and self.payload == other.payload + and self.choice == other.choice + and self.argument == other.argument + ) - builder = CommandBuilder(atomic_default=atomic_default) - if obj is not None: - builder.append(obj) - return builder - def __init__(self, atomic_default=False): - self._atomic_default = atomic_default - self._commands = [[]] # type: List[List[Command]] - self._defaults = CommandDefaults() +class ExerciseCommand(Command): + """ + A command that exercises a choice on a contract identified by its contract ID. + """ - def defaults( - self, - party: Optional[Party] = None, - ledger_id: Optional[str] = None, - workflow_id: Optional[str] = None, - application_id: Optional[str] = None, - command_id: Optional[str] = None, - deduplication_time: Optional[timedelta] = None, - ) -> None: - if party is not None: - self._defaults.default_party = party - if ledger_id is not None: - self._defaults.ledger_id = ledger_id - if workflow_id is not None: - self._defaults.default_workflow_id = workflow_id - if application_id is not None: - self._defaults.default_application_id = application_id - if command_id is not None: - self._defaults.default_command_id = command_id - if deduplication_time is not None: - self._defaults.default_deduplication_time = deduplication_time - - def create(self, template, arguments=None) -> "CommandBuilder": - return self.append(create(template, arguments=arguments)) - - def exercise(self, contract, choice, arguments=None) -> "CommandBuilder": - return self.append(exercise(contract, choice, arguments=arguments)) - - def create_and_exercise( - self, template, create_arguments, choice_name, choice_arguments=None - ) -> "CommandBuilder": - return self.append( - create_and_exercise(template, create_arguments, choice_name, choice_arguments) - ) + __slots__ = ("_choice", "_contract_id", "_argument") - def append(self, *commands: CommandsOrCommandSequence) -> "CommandBuilder": + def __init__(self, contract_id: "ContractId", choice: str, argument: "Optional[Any]" = None): """ - Append one or more commands, or list of commands to the :class:`CommandBuilder` in flight. - This method respects the value of ``atomic_default`` that this object was constructed with. - In order to force commands to be submitted either atomically, use :meth:`append_atomically`. - To allow these commands to be submitted in parallel use :meth:`append_nonatomically`. - - :param commands: One or more commands, or list of commands to be submitted to the ledger. - :return: This object. + Initialize an :class:`ExerciseCommand`. + + :param contract_id: + The contract ID of the contract to exercise. + :param choice: + The choice to exercise. + :param argument: + Tue choice arguments. Can be omitted for choices that take no arguments. """ - if self._atomic_default: - # a command builder that defaults to being atomic will put all commands in a single - # transaction; build on the very first transaction - self._commands[0].extend(flatten_command_sequence(commands)) - return self - else: - return self.append_nonatomically(*commands) - - def append_atomically(self, *commands: Union[Command, Sequence[Command]]) -> "CommandBuilder": - self._commands.extend([flatten_command_sequence(commands)]) - return self - - def append_nonatomically( - self, *commands: Union[Command, Sequence[Command]] - ) -> "CommandBuilder": - self._commands.extend([[cmd] for cmd in flatten_command_sequence(commands)]) - return self + object.__setattr__(self, "choice", safe_cast(str, choice)) + object.__setattr__(self, "contract", safe_cast(ContractId, contract_id)) + object.__setattr__(self, "arguments", dict(argument) if argument is not None else dict()) - def build(self, defaults: "Optional[CommandDefaults]" = None) -> "Collection[CommandPayload]": + @property + def contract_id(self) -> "ContractId": """ - Return a collection of commands. + Return the contract ID of the contract to exercise. """ - if defaults is None: - raise ValueError("defaults must currently be specified") + return self._contract_id - command_id = ( - defaults.default_command_id or self._defaults.default_command_id or uuid.uuid4().hex - ) - - return [ - CommandPayload( - party=defaults.default_party or self._defaults.default_party, - ledger_id=defaults.default_ledger_id or self._defaults.default_ledger_id, - workflow_id=defaults.default_workflow_id or self._defaults.default_workflow_id, - application_id=defaults.default_application_id - or self._defaults.default_application_id, - command_id=command_id, - deduplication_time=defaults.default_deduplication_time - or self._defaults.default_deduplication_time, - commands=commands, - ) - for i, commands in enumerate(self._commands) - if commands - ] + @property + def choice(self) -> str: + """ + Return the choice to exercise. + """ + return self._choice - def __format__(self, format_spec): - if format_spec == "c": - return str(self._commands) - else: - return repr(self) + @property + def argument(self) -> "Any": + """ + Return the choice arguments. + """ + return self._argument def __repr__(self): - return f"CommandBuilder({self._commands})" - - -def flatten_command_sequence(commands: Sequence[CommandsOrCommandSequence]) -> List[Command]: - """ - Convert a list of mixed commands, ``None``, and list of commands into an ordered sequence of - non-``None`` commands. - """ - ret = [] # type: List[Command] - errors = [] - - for i, obj in enumerate(commands): - if obj is not None: - if isinstance(obj, Command): - ret.append(obj) - else: - try: - cmd_iter = iter(obj) - except TypeError: - errors.append(((i,), obj)) - continue - for j, cmd in enumerate(cmd_iter): - if isinstance(cmd, Command): - ret.append(cmd) - else: - errors.append(((i, j), cmd)) - if errors: - raise ValueError( - f"Failed to interpret some elements as Commands in the list: " f"$[{index}] = {command}" - for index, command in errors + return f"ExerciseCommand({self.choice!r}, {self.contract_id}, {self.argument}>" + + def __eq__(self, other: "Any") -> bool: + return ( + isinstance(other, ExerciseCommand) + and self.choice == other.choice + and self.contract_id == other.contract_id + and self.argument == other.argument ) - return ret - - -@dataclass -class CommandDefaults: - """ - Values to use for a :class:`Command` when no value is specified with the creation of the - command. - """ - default_party: Optional[Party] = None - default_ledger_id: Optional[str] = None - default_workflow_id: Optional[str] = None - default_application_id: Optional[str] = None - default_command_id: Optional[str] = None - default_deduplication_time: Optional[timedelta] = None - -@dataclass(frozen=True) -class CommandPayload: +class ExerciseByKeyCommand(Command): """ - A request to mutate active state of the ledger. - - .. attribute:: CommandPayload.party - The party submitting the request. - .. attribute:: CommandPayload.application_id: - An optional application ID to accompany the request. - .. attribute:: CommandPayload.command_id: - A hash that represents the BIM commitment. - .. attribute:: CommandPayload.deduplication_time: - The maximum time interval before the client should consider this command expired. - .. attribute:: CommandPayload.commands - A sequence of commands to submit to the ledger. These commands are submitted atomically - (in other words, they all succeed or they all fail). - .. attribute:: CommandPayload.deduplication_time: - The length of the time window during which all commands with the same party and command ID - will be deduplicated. Duplicate commands submitted before the end of this window return an - ``ALREADY_EXISTS`` error. + A command that exercises a choice on a contract identified by its contract key. """ - party: Party - ledger_id: str - workflow_id: str - application_id: str - command_id: str - commands: "Sequence[Command]" - deduplication_time: "Optional[timedelta]" = None - - def __post_init__(self): - missing_fields = [ - field.name - for field in fields(self) - if field.name != "deduplication_time" and getattr(self, field.name) is None - ] - if missing_fields: - raise ValueError( - f"Some fields are set to None when they are required: " f"{missing_fields}" - ) + __slots__ = ("_template_id", "_key", "_choice", "_argument") + def __init__( + self, + template_id: "Union[str, TypeConName]", + key: "Any", + choice: str, + argument: "Optional[Any]", + ): + """ + Initialize an :class:`ExerciseByKeyCommand`. + + :param template_id: + The contract template type. + :param key: + The contract key of the contract to exercise. + :param choice: + The choice to exercise. + :param argument: + Tue choice arguments. Can be omitted for choices that take no arguments. + """ + object.__setattr__(self, "_template_id", validate_template_id(template_id)) + object.__setattr__(self, "_key", key) + object.__setattr__(self, "_choice", choice) + object.__setattr__(self, "_argument", argument) -def create(template, arguments=None): - if not isinstance(template, str): - raise ValueError( - "template must be a string name, a template type, or an instantiated template" - ) + @property + def template_id(self) -> "TypeConName": + """ + Return the contract template type. + """ + return self._template_id - return CreateCommand(template, arguments) + @property + def key(self) -> "Any": + """ + Return the contract key of the contract to exercise. + """ + return self._key + @property + def choice(self) -> str: + """ + Return the choice to exercise. + """ + return self._choice -def exercise(contract, choice, arguments=None): - if not isinstance(choice, str): - raise ValueError( - "choice must be a string name, a template type, or an instantiated template" + @property + def argument(self) -> "Any": + """ + Return the choice arguments. + """ + return self._argument + + def __eq__(self, other: "Any") -> bool: + return ( + isinstance(other, ExerciseByKeyCommand) + and self.template_id == other.template_id + and self.key == other.key + and self.choice == other.choice + and self.argument == other.argument ) - return ExerciseCommand(contract, choice, arguments) - - -def exercise_by_key(template, contract_key, choice_name, choice_argument): - return ExerciseByKeyCommand(template, contract_key, choice_name, choice_argument) - - -def create_and_exercise(template, create_arguments, choice_name, choice_argument): - return CreateAndExerciseCommand(template, create_arguments, choice_name, choice_argument) - class Serializer: """ @@ -430,32 +294,32 @@ def serialize_commands(self, commands: "Sequence[Command]") -> "Sequence[Any]": def serialize_command(self, command: "Command") -> "Any": if isinstance(command, CreateCommand): - name = self.lookup.template_name(command.template_type) - value = self.serialize_value(con(name), command.arguments) + name = self.lookup.template_name(command.template_id) + value = self.serialize_value(con(name), command.payload) return self.serialize_create_command(name, value) elif isinstance(command, ExerciseCommand): - template = self.lookup.template(command.contract.value_type) + template = self.lookup.template(command.contract_id.value_type) choice = find_choice(template, command.choice) - args = self.serialize_value(choice.arg_binder.type, command.arguments) - return self.serialize_exercise_command(command.contract, choice.name, args) + args = self.serialize_value(choice.arg_binder.type, command.argument) + return self.serialize_exercise_command(command.contract_id, choice.name, args) elif isinstance(command, CreateAndExerciseCommand): - name = self.lookup.template_name(command.template_type) + name = self.lookup.template_name(command.template_id) template = self.lookup.template(name) - create_value = self.serialize_value(con(name), command.arguments) + create_value = self.serialize_value(con(name), command.payload) choice = find_choice(template, command.choice) - choice_args = self.serialize_value(choice.arg_binder.type, command.choice_argument) + choice_args = self.serialize_value(choice.arg_binder.type, command.argument) return self.serialize_create_and_exercise_command( name, create_value, choice.name, choice_args ) elif isinstance(command, ExerciseByKeyCommand): - name = self.lookup.template_name(command.template_type) + name = self.lookup.template_name(command.template_id) template = self.lookup.template(name) - key_value = self.serialize_value(template.key.type, command.contract_key) + key_value = self.serialize_value(template.key.type, command.key) choice = find_choice(template, command.choice) - choice_args = self.serialize_value(choice.arg_binder.type, command.choice_argument) + choice_args = self.serialize_value(choice.arg_binder.type, command.argument) return self.serialize_exercise_by_key_command(name, key_value, choice.name, choice_args) else: diff --git a/python/dazl/protocols/v1/grpc.py b/python/dazl/protocols/v1/grpc.py index dd49c07b..1fa94144 100644 --- a/python/dazl/protocols/v1/grpc.py +++ b/python/dazl/protocols/v1/grpc.py @@ -18,7 +18,6 @@ from ...util.io import read_file_bytes from ...util.typing import safe_cast from .._base import LedgerClient, LedgerConnectionOptions, _LedgerConnection -from ..commands import CommandPayload from ..errors import ConnectionTimeoutError, UserTerminateRequest from ..events import BaseEvent, ContractFilter, TransactionFilter from .pb_parse_event import ( @@ -31,6 +30,7 @@ if TYPE_CHECKING: from ...client._conn_settings import HTTPConnectionSettings + from ...client.commands import CommandPayload from ...client.ledger import LedgerMetadata @@ -42,7 +42,7 @@ def __init__(self, connection: "GRPCv1Connection", ledger: "LedgerMetadata", par self.ledger = safe_cast(LedgerMetadata, ledger) self.party = to_party(party) - def commands(self, commands: CommandPayload) -> None: + def commands(self, commands: "CommandPayload") -> None: serializer = self.ledger.serializer request = serializer.serialize_command_request(commands) return self.connection.invoker.run_in_executor( diff --git a/python/dazl/protocols/v1/pb_ser_command.py b/python/dazl/protocols/v1/pb_ser_command.py index eafa8691..2eb48ace 100644 --- a/python/dazl/protocols/v1/pb_ser_command.py +++ b/python/dazl/protocols/v1/pb_ser_command.py @@ -4,14 +4,20 @@ """ Conversion methods to Ledger API Protobuf-generated types from dazl/Pythonic types. """ -from typing import Any +from typing import TYPE_CHECKING, Any # noinspection PyPep8Naming from . import model as G from ...damlast.daml_lf_1 import TypeConName from ...prim import ContractId, timedelta_to_duration from ...values.protobuf import ProtobufEncoder, set_value -from ..commands import AbstractSerializer, CommandPayload +from ..commands import AbstractSerializer + +if TYPE_CHECKING: + from ...client.commands import CommandPayload + + +__all__ = ["as_identifier", "ProtobufSerializer"] def as_identifier(tref: "TypeConName") -> "G.Identifier": @@ -32,7 +38,7 @@ class ProtobufSerializer(AbstractSerializer): # COMMAND serializers ################################################################################################ - def serialize_command_request(self, command_payload: CommandPayload) -> G.SubmitRequest: + def serialize_command_request(self, command_payload: "CommandPayload") -> G.SubmitRequest: commands = [self.serialize_command(command) for command in command_payload.commands] return G.SubmitRequest( commands=G.Commands( diff --git a/python/tests/unit/test_command_builder.py b/python/tests/unit/test_command_builder.py index 5042ea31..2ad29acf 100644 --- a/python/tests/unit/test_command_builder.py +++ b/python/tests/unit/test_command_builder.py @@ -4,16 +4,10 @@ from unittest import TestCase +from dazl.client.commands import CommandBuilder, CommandDefaults, CommandPayload, create from dazl.damlast.lookup import parse_type_con_name from dazl.prim import ContractId, Party -from dazl.protocols.commands import ( - CommandBuilder, - CommandDefaults, - CommandPayload, - CreateCommand, - ExerciseCommand, - create, -) +from dazl.protocols.commands import CreateCommand, ExerciseCommand SOME_TEMPLATE_NAME = parse_type_con_name("Sample:Untyped") SOME_PARTY = Party("SomeParty")