Skip to content

Commit

Permalink
Merge branch 'master' into readme-local
Browse files Browse the repository at this point in the history
  • Loading branch information
nrempel authored Jul 30, 2019
2 parents 676652b + 79306cc commit 7938a35
Show file tree
Hide file tree
Showing 21 changed files with 1,018 additions and 559 deletions.
25 changes: 18 additions & 7 deletions DevReadMe.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,30 +48,31 @@ Currently you must specify at least one _inbound_ and one _outbound_ transport.
For example:

```bash
aca-py --inbound-transport http 0.0.0.0 8000 \
--outbound-transport http
aca-py start --inbound-transport http 0.0.0.0 8000 \
--outbound-transport http
```

or

```bash
aca-py --inbound-transport http 0.0.0.0 8000 \
--inbound-transport ws 0.0.0.0 8001 \
--outbound-transport ws \
--outbound-transport http
aca-py start --inbound-transport http 0.0.0.0 8000 \
--inbound-transport ws 0.0.0.0 8001 \
--outbound-transport ws \
--outbound-transport http
```

Currently, Aries Cloud Agent Python ships with both inbound and outbound transport drivers for `http` and `websockets`. More information on how to develop your own transports will be coming soon.

Most configuration parameters are provided to the the agent at startup. Refer to the section below for details on all available command-line arguments.

## Command Line Arguments
### Command Line Arguments

| **argument** | **format example** | **effect** | **required** |
| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ |
| `--inbound-transport`, `-it` | `--inbound-transport http 0.0.0.0 8000` | Defines the inbound transport(s) to listen on. This parameter can be passed multiple times to create multiple interfaces. Supported internal transport types are `http` and `ws`. | `true` |
| `--outbound-transport`, `-ot` | `--outbound-transport http` | Defines the outbound transport(s) to support for outgoing messages. This parameter can be passed multiple times to supoort multiple transport types. Supported internal transport types are `http` and `ws`. | `true` |
| `--log-config` | `--log-config /path/to/log/config.ini` | Provides a custom [python logging config file](https://docs.python.org/3/library/logging.config.html#logging-config-fileformat) to use. By default, a [default logging config](config/default_logging_config.ini) is used. | `false` |
| `--log-file` | `--log-file /path/to/logfile` | Overrides the output destination for the root logger as defined by the log config file. | `false` |
| `--log-level` | `--log-level debug` | Specifies the python log level. | `false` |
| `--endpoint`, `-e` | `--endpoint https://example.com/agent-endpoint` | Specifies the endpoint for which other agents should contact this agent. This endpoint could point to a different agent if routing is configured. The endpoint is used in the formation of a connection with another agent. | `false` |
| `--label`, `-l` | `--label "My Agent"` | Specifies the label for this agent. This label is publicized to other agents as part of forming a connection. | `false` |
Expand Down Expand Up @@ -106,6 +107,16 @@ Most configuration parameters are provided to the the agent at startup. Refer to
| `--protocol` | `--protocol` | Instructs the agent to load an external protocol module. | `false` |
| `--webhook-url` | `--webhook-url` | Instructs the agent to send webhooks containing internal state changes to a URL. This is useful for a controller to monitor changes and prompt new behaviour using the admin API. | `false` |

### Provisioning a Wallet

It is possible to provision an Indy wallet before running an agent to avoid passing in the wallet seed on every invocation of `aca-py start`.

```bash
aca-py provision wallet --wallet-type indy --seed $SEED
```

For additional options, execute `aca-py provision --help`.

## Developing

### Prerequisites
Expand Down
102 changes: 1 addition & 101 deletions aries_cloudagent/__init__.py
Original file line number Diff line number Diff line change
@@ -1,101 +1 @@
"""Entrypoint."""

import asyncio
import functools
import os
import signal

from aiohttp import ClientSession

from .conductor import Conductor
from .config.argparse import get_settings, parse_args
from .config.default_context import DefaultContextBuilder
from .config.logging import LoggingConfigurator
from .postgres import load_postgres_plugin


async def get_genesis_transactions(genesis_url: str):
"""Get genesis transactions."""
headers = {}
headers["Content-Type"] = "application/json"
async with ClientSession() as client_session:
response = await client_session.get(genesis_url, headers=headers)
genesis_txns = await response.text()
return genesis_txns


async def start_app(conductor: Conductor):
"""Start up."""
await conductor.setup()
await conductor.start()


async def shutdown_app(conductor: Conductor):
"""Shut down."""
print("\nShutting down")
await conductor.stop()
tasks = [
task
for task in asyncio.Task.all_tasks()
if task is not asyncio.tasks.Task.current_task()
]
for task in tasks:
task.cancel()
await asyncio.gather(*tasks, return_exceptions=True)
asyncio.get_event_loop().stop()


def main():
"""Entrypoint."""
args = parse_args()
settings = get_settings(args)

# Set up logging
log_config = settings.get("log.config")
log_level = settings.get("log.level") or os.getenv("LOG_LEVEL")
LoggingConfigurator.configure(log_config, log_level)

# Fetch genesis transactions if necessary
if not settings.get("ledger.genesis_transactions") and settings.get(
"ledger.genesis_url"
):
loop = asyncio.get_event_loop()
settings["ledger.genesis_transactions"] = loop.run_until_complete(
get_genesis_transactions(settings["ledger.genesis_url"])
)

# Load postgres plug-in if necessary
if (
settings.get("wallet.type")
and settings.get("wallet.storage_type") == "postgres_storage"
):
if args.wallet_storage_type == "postgres_storage":
load_postgres_plugin()

# Support WEBHOOK_URL environment variable
webhook_url = os.environ.get("WEBHOOK_URL")
if webhook_url:
webhook_urls = list(settings.get("admin.webhook_urls") or [])
webhook_urls.append(webhook_url)
settings["admin.webhook_urls"] = webhook_urls

# Create the Conductor instance
context_builder = DefaultContextBuilder(settings)
conductor = Conductor(context_builder)

# Run the application
loop = asyncio.get_event_loop()
loop.add_signal_handler(
signal.SIGTERM,
functools.partial(asyncio.ensure_future, shutdown_app(conductor), loop=loop),
)
asyncio.ensure_future(start_app(conductor), loop=loop)

try:
loop.run_forever()
except KeyboardInterrupt:
loop.run_until_complete(shutdown_app(conductor))


if __name__ == "__main__":
main() # pragma: no cover
"""Aries Cloud Agent."""
35 changes: 35 additions & 0 deletions aries_cloudagent/commands/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Commands module common setup."""

from importlib import import_module
from typing import Sequence


def available_commands():
"""Index available commands."""
return [
{"name": "help", "summary": "Print available commands"},
{"name": "provision", "summary": "Provision an agent"},
{"name": "start", "summary": "Start a new agent process"},
]


def load_command(command: str):
"""Load the module corresponding with a named command."""
module = None
module_path = None
for cmd in available_commands():
if cmd["name"] == command:
module = cmd["name"]
if "module" in cmd:
module_path = cmd["module"]
break
if module and not module_path:
module_path = f"{__package__}.{module}"
if module_path:
return import_module(module_path)


def run_command(command: str, argv: Sequence[str] = None):
"""Execute a named command with command line arguments."""
module = load_command(command) or load_command("help")
module.execute(argv)
23 changes: 23 additions & 0 deletions aries_cloudagent/commands/help.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""Help command for indexing available commands."""

from argparse import ArgumentParser
from typing import Sequence


def execute(argv: Sequence[str] = None):
"""Execute the help command."""
from . import available_commands, load_command

parser = ArgumentParser()
subparsers = parser.add_subparsers()
for cmd in available_commands():
if cmd["name"] == "help":
continue
module = load_command(cmd["name"])
subparser = subparsers.add_parser(cmd["name"], help=cmd["summary"])
module.init_argument_parser(subparser)
parser.print_help()


if __name__ == "__main__":
execute()
84 changes: 84 additions & 0 deletions aries_cloudagent/commands/provision.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Provision command for setting up agent settings before starting."""

import asyncio
from argparse import ArgumentParser
from typing import Sequence

from ..config import argparse as arg
from ..config.default_context import DefaultContextBuilder
from ..config.util import common_config
from ..error import BaseError
from ..wallet.base import BaseWallet
from ..wallet.crypto import seed_to_did


class ProvisionError(BaseError):
"""Base exception for provisioning errors."""


def init_argument_parser(parser: ArgumentParser):
"""Initialize an argument parser with the module's arguments."""
return arg.load_argument_groups(
parser, *arg.group.get_registered(arg.CAT_PROVISION)
)


async def provision(category: str, settings: dict):
"""Perform provisioning."""
context_builder = DefaultContextBuilder(settings)
context = await context_builder.build()

if category == "wallet":
# Initialize wallet
wallet: BaseWallet = await context.inject(BaseWallet)
if wallet.type != "indy":
raise ProvisionError("Cannot provision a non-Indy wallet type")
if wallet.created:
print("Created new wallet")
else:
print("Opened existing wallet")
print("Wallet type:", wallet.type)
print("Wallet name:", wallet.name)
wallet_seed = context.settings.get("wallet.seed")
public_did_info = await wallet.get_public_did()
if public_did_info:
# If we already have a registered public did and it doesn't match
# the one derived from `wallet_seed` then we error out.
# TODO: Add a command to change public did explicitly
if wallet_seed and seed_to_did(wallet_seed) != public_did_info.did:
raise ProvisionError(
"New seed provided which doesn't match the registered"
+ f" public did {public_did_info.did}"
)
elif wallet_seed:
public_did_info = await wallet.create_public_did(seed=wallet_seed)
print("Created new public DID")
if public_did_info:
print("Public DID:", public_did_info.did)
print("Verkey:", public_did_info.verkey)
else:
print("No public DID")


def execute(argv: Sequence[str] = None):
"""Entrypoint."""
parser = ArgumentParser()
parser.prog += " provision"
parser.add_argument(
dest="provision_category",
type=str,
metavar=("<category>"),
choices=["wallet"],
help="The provision command to invoke",
)
get_settings = init_argument_parser(parser)
args = parser.parse_args(argv)
settings = get_settings(args)
common_config(settings)

loop = asyncio.get_event_loop()
loop.run_until_complete(provision(args.provision_category, settings))


if __name__ == "__main__":
execute()
87 changes: 87 additions & 0 deletions aries_cloudagent/commands/start.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""Entrypoint."""

import asyncio
import functools
import os
import signal
from argparse import ArgumentParser
from typing import Coroutine, Sequence

from ..conductor import Conductor
from ..config import argparse as arg
from ..config.default_context import DefaultContextBuilder
from ..config.util import common_config


async def start_app(conductor: Conductor):
"""Start up."""
await conductor.setup()
await conductor.start()


async def shutdown_app(conductor: Conductor):
"""Shut down."""
print("\nShutting down")
await conductor.stop()


def init_argument_parser(parser: ArgumentParser):
"""Initialize an argument parser with the module's arguments."""
return arg.load_argument_groups(parser, *arg.group.get_registered(arg.CAT_START))


def execute(argv: Sequence[str] = None):
"""Entrypoint."""
parser = ArgumentParser()
parser.prog += " start"
get_settings = init_argument_parser(parser)
args = parser.parse_args(argv)
settings = get_settings(args)
common_config(settings)

# Support WEBHOOK_URL environment variable
webhook_url = os.environ.get("WEBHOOK_URL")
if webhook_url:
webhook_urls = list(settings.get("admin.webhook_urls") or [])
webhook_urls.append(webhook_url)
settings["admin.webhook_urls"] = webhook_urls

# Create the Conductor instance
context_builder = DefaultContextBuilder(settings)
conductor = Conductor(context_builder)

# Run the application
run_loop(start_app(conductor), shutdown_app(conductor))


def run_loop(startup: Coroutine, shutdown: Coroutine):
"""Execute the application, handling signals and ctrl-c."""

async def done():
"""Run shutdown and clean up any outstanding tasks."""
await shutdown
tasks = [
task
for task in asyncio.Task.all_tasks()
if task is not asyncio.Task.current_task()
]
for task in tasks:
task.cancel()
if tasks:
await asyncio.gather(*tasks, return_exceptions=True)
asyncio.get_event_loop().stop()

loop = asyncio.get_event_loop()
loop.add_signal_handler(
signal.SIGTERM, functools.partial(asyncio.ensure_future, done(), loop=loop)
)
asyncio.ensure_future(startup, loop=loop)

try:
loop.run_forever()
except KeyboardInterrupt:
loop.run_until_complete(done())


if __name__ == "__main__":
execute()
Empty file.
Loading

0 comments on commit 7938a35

Please sign in to comment.