Repository: hyperledger/aries-cloudagent-python

Issue:
Update demo/playground to pure python
The `demo\playground` has a requirements.txt and the dependencies need to be installed before using the scripts. The [script](https://github.com/hyperledger/aries-cloudagent-python/blob/bf871f7131d62761ad6df972449d7b3d57772960/demo/playground/scripts/mediator_ping_agents.py) is simple and could be updated to be requirements free.

This is a PR/maintenance issue, get dependabot alerts fairly frequently - so would be nice to remove that from our workload.

It is referenced in the [aries-mediator-service / redis-pq-demo](https://github.com/hyperledger/aries-mediator-service/tree/main/redis-pq-demo) to quickly illustrate pings through a mediator. It would be nice if interested parties didn't have to set up a virtual environment or pollute their python libs just to run this script.


Discussion:

Resolving patch:
diff --git a/demo/playground/scripts/mediator_ping_agents.py b/demo/playground/scripts/mediator_ping_agents.py
deleted file mode 100644
--- a/demo/playground/scripts/mediator_ping_agents.py
+++ /dev/null
@@ -1,285 +0,0 @@
-import base64
-import time
-
-import requests
-from random_word import RandomWords
-
-r = RandomWords()
-
-FABER_ADMIN_URL = "http://localhost:9011"
-ALICE_ADMIN_URL = "http://localhost:9012"
-ACME_ADMIN_URL = "http://localhost:9013"
-MULTI_ADMIN_URL = "http://localhost:9014"
-
-
-MEDIATOR_INVITATION_URL = "<paste your connection invitation here>"
-
-
-def wait_a_bit(secs: int = 1):
-    total = secs
-    print(f"... wait {total} seconds ...")
-    time.sleep(total)
-
-
-def initialize_mediation(url, headers=None):
-    if headers is None:
-        headers = {}
-    params = {
-        "alias": "mediator",
-        "auto_accept": "true",
-    }
-    response = requests.post(
-        f"{url}/connections/receive-invitation",
-        headers=headers,
-        params=params,
-        json=data,
-    )
-    print(f"response = {response}")
-    json = response.json()
-    print(f"json = {json}")
-    mediator_connection_id = json["connection_id"]
-    mediator_connection_state = json["state"]
-    mediation_id = None
-
-    # let's wait until active, then ask for mediation
-    attempts = 0
-    while mediator_connection_state != "active" and attempts < 5:
-        wait_a_bit(5)
-        response = requests.get(
-            f"{url}/connections/{mediator_connection_id}",
-            headers=headers,
-        )
-        print(f"response = {response}")
-        json = response.json()
-        print(f"json = {json}")
-        mediator_connection_state = json["state"]
-        print(f"mediator_connection_state = {mediator_connection_state}")
-        attempts = attempts + 1
-
-    if mediator_connection_state == "active":
-        response = requests.post(
-            f"{url}/mediation/request/{mediator_connection_id}",
-            headers=headers,
-            json={},
-        )
-        print(f"response = {response}")
-        mediation_request_json = response.json()
-        print(f"mediation request json = {mediation_request_json}")
-        mediation_id = mediation_request_json["mediation_id"]
-        print(f"mediation_id = {mediation_id}")
-        mediation_granted = False
-        attempts = 0
-        while not mediation_granted and attempts < 5:
-            wait_a_bit(5)
-            response = requests.get(
-                f"{url}/mediation/requests/{mediation_id}",
-                headers=headers,
-            )
-            print(f"response = {response}")
-            granted_json = response.json()
-            print(f"json = {granted_json}")
-            mediation_granted = granted_json["state"] == "granted"
-            print(f"mediation_granted = {mediation_granted}")
-            attempts = attempts + 1
-
-    return mediator_connection_id, mediation_id
-
-
-def create_invitation(my_label, alias, mediation_id, url, headers=None):
-    if headers is None:
-        headers = {}
-    data = {
-        "my_label": my_label,
-    }
-    if mediation_id:
-        data["mediation_id"] = mediation_id
-
-    params = {
-        "alias": alias,
-        "auto_accept": "true",
-    }
-
-    response = requests.post(
-        f"{url}/connections/create-invitation",
-        headers=headers,
-        params=params,
-        json=data,
-    )
-    print(f"response = {response}")
-    json = response.json()
-    print(f"create-invitation json = {json}")
-    invitation = json["invitation"]
-    connection_id = json["connection_id"]
-    recipient_keys = json["invitation"]["recipientKeys"]
-    return invitation, connection_id, recipient_keys
-
-
-def receive_invitation(invitation, alias, mediation_id, url, headers=None):
-    if headers is None:
-        headers = {}
-    data = invitation
-    params = {
-        "alias": alias,
-        "auto_accept": "true",
-    }
-    if mediation_id:
-        params["mediation_id"] = mediation_id
-
-    response = requests.post(
-        f"{url}/connections/receive-invitation",
-        headers=headers,
-        json=data,
-        params=params,
-    )
-    print(f"response = {response}")
-    json = response.json()
-    print(f"receive_invitation json = {json}")
-    connection_id = json["connection_id"]
-    return connection_id
-
-
-def fetch_connection(connection_id, url, headers=None):
-    if headers is None:
-        headers = {}
-    response = requests.get(
-        f"{url}/connections/{connection_id}",
-        headers=headers,
-    )
-    print(f"response = {response}")
-    json = response.json()
-    print(f"fetch_connection json = {json}")
-    return json["state"] == "active"
-
-
-def ping_connection(connection_id, alias, url, headers=None):
-    if headers is None:
-        headers = {}
-    response = requests.post(
-        f"{url}/connections/{connection_id}/send-ping",
-        headers=headers,
-        json={"comment": f"{alias} pinging..."},
-    )
-    print(f"ping_connection = {response}")
-
-
-def create_tenant(wallet_name, wallet_key, url, headers=None):
-    if headers is None:
-        headers = {}
-    # need to create the tenant and get the token
-    data = {
-        "key_management_mode": "managed",
-        "wallet_dispatch_type": "default",
-        "wallet_name": wallet_name,
-        "wallet_key": wallet_key,
-        "label": wallet_name,
-        "wallet_type": "askar",
-        "wallet_webhook_urls": [],
-    }
-    # multi-agent has no security for base wallet, just call multitenancy/wallet
-    # to create a new tenant
-    response = requests.post(f"{url}/multitenancy/wallet", headers=headers, json=data)
-    print(f"response = {response}")
-    json = response.json()
-    print(f"json = {json}")
-    wallet_id = json["wallet_id"]
-    token = json["token"]
-    _headers = {"Authorization": f"Bearer {token}"}
-
-    return wallet_id, token, _headers
-
-
-if __name__ == "__main__":
-    _url = MEDIATOR_INVITATION_URL
-    base64_message = _url.split("=", maxsplit=1)[1]
-    base64_bytes = base64_message.encode("ascii")
-    message_bytes = base64.b64decode(base64_bytes)
-    data = message_bytes.decode("ascii")
-    print(f"invitation_block = {data}")
-
-    print("\n... single tenants, initiate mediation ...\n")
-
-    faber_mediation_connection_id, faber_mediation_id = initialize_mediation(
-        FABER_ADMIN_URL
-    )
-    print("faber")
-    print(f"  mediation_connection_id={faber_mediation_connection_id}")
-    print(f"  mediation_id={faber_mediation_id}")
-
-    alice_mediation_connection_id, alice_mediation_id = initialize_mediation(
-        ALICE_ADMIN_URL
-    )
-    print("alice")
-    print(f"  mediation_connection_id={alice_mediation_connection_id}")
-    print(f"  mediation_id={alice_mediation_id}")
-
-    print("\n... single tenants, connect ...\n")
-
-    # faber create invitation for alice
-    (
-        faber_invitation,
-        faber_alice_connection_id,
-        faber_recipient_keys,
-    ) = create_invitation("faber", "alice", faber_mediation_id, FABER_ADMIN_URL)
-    alice_faber_connection_id = receive_invitation(
-        faber_invitation, "faber", alice_mediation_id, ALICE_ADMIN_URL
-    )
-
-    connection_active = False
-    while not connection_active:
-        wait_a_bit(1)
-        connection_active = fetch_connection(faber_alice_connection_id, FABER_ADMIN_URL)
-
-    print("\n... connections active, ping each other ...\n")
-
-    pings = 0
-    while connection_active and pings < 10:
-        ping_connection(faber_alice_connection_id, "faber", FABER_ADMIN_URL)
-        wait_a_bit(1)
-        ping_connection(alice_faber_connection_id, "alice", ALICE_ADMIN_URL)
-        wait_a_bit(1)
-        pings = pings + 1
-
-    print("\n... multitenant create tenant...\n")
-    # ok, now let's try with multitenant
-    multi_wallet_name = r.get_random_word()
-    multi_wallet_id, multi_token, multi_headers = create_tenant(
-        multi_wallet_name, "changeme", MULTI_ADMIN_URL
-    )
-
-    print("\n... multitenant, initiate mediation ...\n")
-
-    multi_mediation_connection_id, multi_mediation_id = initialize_mediation(
-        MULTI_ADMIN_URL, multi_headers
-    )
-    print("multi")
-    print(f"  mediation_connection_id={multi_mediation_connection_id}")
-    print(f"  mediation_id={multi_mediation_id}")
-
-    print("\n... multitenant, connect ...\n")
-    multi_mediation_id = None
-    (
-        multi_invitation,
-        multi_alice_connection_id,
-        multi_recipient_keys,
-    ) = create_invitation(
-        multi_wallet_name, "alice", multi_mediation_id, MULTI_ADMIN_URL, multi_headers
-    )
-
-    alice_multi_connection_id = receive_invitation(
-        multi_invitation, multi_wallet_name, alice_mediation_id, ALICE_ADMIN_URL
-    )
-
-    connection_active = False
-    while not connection_active:
-        wait_a_bit(1)
-        connection_active = fetch_connection(alice_multi_connection_id, ALICE_ADMIN_URL)
-
-    pings = 0
-    while connection_active and pings < 10:
-        ping_connection(
-            multi_alice_connection_id, "multi", MULTI_ADMIN_URL, multi_headers
-        )
-        wait_a_bit(1)
-        ping_connection(alice_multi_connection_id, "alice", ALICE_ADMIN_URL)
-        wait_a_bit(1)
-        pings = pings + 1
diff --git a/demo/playground/scripts/ping_agents.py b/demo/playground/scripts/ping_agents.py
deleted file mode 100644
--- a/demo/playground/scripts/ping_agents.py
+++ /dev/null
@@ -1,245 +0,0 @@
-import time
-
-import requests
-from random_word import RandomWords
-
-r = RandomWords()
-
-FABER_ADMIN_URL = "http://localhost:9011"
-ALICE_ADMIN_URL = "http://localhost:9012"
-ACME_ADMIN_URL = "http://localhost:9013"
-MULTI_ADMIN_URL = "http://localhost:9014"
-
-
-def wait_a_bit(secs: int = 1):
-    total = secs
-    print(f"... wait {total} seconds ...")
-    time.sleep(total)
-
-
-def create_invitation(my_label, alias, mediation_id, url, headers=None):
-    if headers is None:
-        headers = {}
-    data = {
-        "my_label": my_label,
-    }
-    if mediation_id:
-        data["mediation_id"] = mediation_id
-
-    params = {
-        "alias": alias,
-        "auto_accept": "true",
-    }
-
-    response = requests.post(
-        f"{url}/connections/create-invitation",
-        headers=headers,
-        params=params,
-        json=data,
-    )
-    print(f"response = {response}")
-    json = response.json()
-    print(f"create-invitation json = {json}")
-    invitation = json["invitation"]
-    connection_id = json["connection_id"]
-    recipient_keys = json["invitation"]["recipientKeys"]
-    return invitation, connection_id, recipient_keys
-
-
-def receive_invitation(invitation, alias, mediation_id, url, headers=None):
-    if headers is None:
-        headers = {}
-    data = invitation
-    params = {
-        "alias": alias,
-        "auto_accept": "true",
-    }
-    if mediation_id:
-        params["mediation_id"] = mediation_id
-
-    response = requests.post(
-        f"{url}/connections/receive-invitation",
-        headers=headers,
-        json=data,
-        params=params,
-    )
-    print(f"response = {response}")
-    json = response.json()
-    print(f"receive_invitation json = {json}")
-    connection_id = json["connection_id"]
-    return connection_id
-
-
-def fetch_connection(connection_id, url, headers=None):
-    if headers is None:
-        headers = {}
-    response = requests.get(
-        f"{url}/connections/{connection_id}",
-        headers=headers,
-    )
-    print(f"response = {response}")
-    json = response.json()
-    print(f"fetch_connection json = {json}")
-    return json["state"] == "active"
-
-
-def ping_connection(connection_id, alias, url, headers=None):
-    if headers is None:
-        headers = {}
-    response = requests.post(
-        f"{url}/connections/{connection_id}/send-ping",
-        headers=headers,
-        json={"comment": f"{alias} pinging..."},
-    )
-    print(f"ping_connection = {response}")
-
-
-def create_tenant(wallet_name, wallet_key, url, headers=None):
-    if headers is None:
-        headers = {}
-    # need to create the tenant and get the token
-    data = {
-        "key_management_mode": "managed",
-        "wallet_dispatch_type": "default",
-        "wallet_name": wallet_name,
-        "wallet_key": wallet_key,
-        "label": wallet_name,
-        "wallet_type": "askar",
-        "wallet_webhook_urls": [],
-    }
-    # multi-agent has no security for base wallet, just call multitenancy/wallet
-    # to create a new tenant
-    response = requests.post(f"{url}/multitenancy/wallet", headers=headers, json=data)
-    print(f"response = {response}")
-    json = response.json()
-    print(f"json = {json}")
-    wallet_id = json["wallet_id"]
-    token = json["token"]
-    _headers = {"Authorization": f"Bearer {token}"}
-
-    return wallet_id, token, _headers
-
-
-if __name__ == "__main__":
-    print("\n... single tenants, connect ...\n")
-
-    # faber create invitation for alice
-    (
-        faber_invitation,
-        faber_alice_connection_id,
-        faber_recipient_keys,
-    ) = create_invitation("faber", "alice", None, FABER_ADMIN_URL)
-    alice_faber_connection_id = receive_invitation(
-        faber_invitation, "faber", None, ALICE_ADMIN_URL
-    )
-
-    connection_active = False
-    attempts = 0
-    while not connection_active and attempts < 5:
-        wait_a_bit(1)
-        connection_active = fetch_connection(faber_alice_connection_id, FABER_ADMIN_URL)
-        attempts = attempts + 1
-
-    print("\n... connections active, ping each other ...\n")
-
-    pings = 0
-    while connection_active and pings < 5:
-        ping_connection(faber_alice_connection_id, "faber", FABER_ADMIN_URL)
-        wait_a_bit(1)
-        ping_connection(alice_faber_connection_id, "alice", ALICE_ADMIN_URL)
-        wait_a_bit(1)
-        pings = pings + 1
-
-    print("\n... multitenant create tenant(s)...\n")
-    # ok, now let's try with multitenant
-    multi_one_wallet_name = r.get_random_word()
-    multi_one_wallet_id, multi_one_token, multi_one_headers = create_tenant(
-        multi_one_wallet_name, "changeme", MULTI_ADMIN_URL
-    )
-
-    multi_two_wallet_name = r.get_random_word()
-    multi_two_wallet_id, multi_two_token, multi_two_headers = create_tenant(
-        multi_two_wallet_name, "changeme", MULTI_ADMIN_URL
-    )
-
-    print("\n... multitenant, connect ...\n")
-    (
-        multi_one_invitation,
-        multi_one_alice_connection_id,
-        multi_one_recipient_keys,
-    ) = create_invitation(
-        multi_one_wallet_name, "alice", None, MULTI_ADMIN_URL, multi_one_headers
-    )
-
-    alice_multi_one_connection_id = receive_invitation(
-        multi_one_invitation, multi_one_wallet_name, None, ALICE_ADMIN_URL
-    )
-
-    connection_active = False
-    attempts = 0
-    while not connection_active and attempts < 5:
-        wait_a_bit(1)
-        connection_active = fetch_connection(
-            alice_multi_one_connection_id, ALICE_ADMIN_URL
-        )
-        attempts = attempts + 1
-
-    pings = 0
-    while connection_active and pings < 5:
-        ping_connection(
-            multi_one_alice_connection_id,
-            multi_one_wallet_name,
-            MULTI_ADMIN_URL,
-            multi_one_headers,
-        )
-        wait_a_bit(1)
-        ping_connection(alice_multi_one_connection_id, "alice", ALICE_ADMIN_URL)
-        wait_a_bit(1)
-        pings = pings + 1
-
-    (
-        multi_two_invitation,
-        multi_two_one_connection_id,
-        multi_two_recipient_keys,
-    ) = create_invitation(
-        multi_two_wallet_name,
-        multi_one_wallet_name,
-        None,
-        MULTI_ADMIN_URL,
-        multi_two_headers,
-    )
-
-    multi_one_two_connection_id = receive_invitation(
-        multi_two_invitation,
-        multi_two_wallet_name,
-        None,
-        MULTI_ADMIN_URL,
-        multi_one_headers,
-    )
-
-    connection_active = False
-    attempts = 0
-    while not connection_active and attempts < 5:
-        wait_a_bit(1)
-        connection_active = fetch_connection(
-            multi_one_two_connection_id, MULTI_ADMIN_URL, multi_one_headers
-        )
-        attempts = attempts + 1
-
-    pings = 0
-    while connection_active and pings < 5:
-        ping_connection(
-            multi_two_one_connection_id,
-            multi_two_wallet_name,
-            MULTI_ADMIN_URL,
-            multi_two_headers,
-        )
-        wait_a_bit(1)
-        ping_connection(
-            multi_one_two_connection_id,
-            multi_one_wallet_name,
-            MULTI_ADMIN_URL,
-            multi_one_headers,
-        )
-        wait_a_bit(1)
-        pings = pings + 1


Test patch:
diff --git a/demo/playground/examples/Dockerfile.test.runner b/demo/playground/examples/Dockerfile.test.runner
new file mode 100644
--- /dev/null
+++ b/demo/playground/examples/Dockerfile.test.runner
@@ -0,0 +1,20 @@
+FROM python:3.9-slim
+WORKDIR /usr/src/app
+
+# install poetry
+RUN pip3 install --no-cache-dir poetry
+
+# Add docker-compose-wait tool
+ENV WAIT_VERSION 2.7.2
+ADD https://github.com/ufoscout/docker-compose-wait/releases/download/$WAIT_VERSION/wait /wait
+RUN chmod +x /wait
+
+# install dependencies
+COPY pyproject.toml .
+COPY poetry.lock .
+RUN poetry install --only main
+
+# add tests to image
+COPY tests/* tests/
+
+ENTRYPOINT ["/bin/sh", "-c", "/wait && poetry run pytest \"$@\"", "--"]
\ No newline at end of file
diff --git a/demo/playground/examples/tests/__init__.py b/demo/playground/examples/tests/__init__.py
new file mode 100644
--- /dev/null
+++ b/demo/playground/examples/tests/__init__.py
@@ -0,0 +1,222 @@
+# pylint: disable=redefined-outer-name
+
+from functools import wraps
+import logging
+import os
+import time
+
+import pytest
+import requests
+
+AUTO_ACCEPT = "false"
+
+FABER = os.getenv("FABER")
+ALICE = os.getenv("ALICE")
+ACME = os.getenv("ACME")
+MULTI = os.getenv("MULTI")
+
+# Create a named logger
+logger = logging.getLogger("playground_examples")
+logger.setLevel(logging.INFO)
+# Create a console handler
+console_handler = logging.StreamHandler()
+console_handler.setLevel(logging.INFO)
+# Set the formatter for the console handler
+formatter = logging.Formatter(
+    "%(asctime)s - %(levelname)s - %(message)s",
+)
+console_handler.setFormatter(formatter)
+# Add the console handler to the logger
+logger.addHandler(console_handler)
+
+
+def get(agent: str, path: str, **kwargs):
+    """Get."""
+    return requests.get(f"{agent}{path}", **kwargs)
+
+
+def post(agent: str, path: str, **kwargs):
+    """Post."""
+    return requests.post(f"{agent}{path}", **kwargs)
+
+
+def fail_if_not_ok(message: str):
+    """Fail the current test if wrapped call fails with message."""
+
+    def _fail_if_not_ok(func):
+        @wraps(func)
+        def _wrapper(*args, **kwargs):
+            response = func(*args, **kwargs)
+            if not response.ok:
+                pytest.fail(f"{message}: {response.content}")
+            return response
+
+        return _wrapper
+
+    return _fail_if_not_ok
+
+
+def unwrap_json_response(func):
+    """Unwrap a requests response object to json."""
+
+    @wraps(func)
+    def _wrapper(*args, **kwargs) -> dict:
+        response = func(*args, **kwargs)
+        return response.json()
+
+    return _wrapper
+
+
+class Agent:
+    """Class for interacting with Agent over Admin API"""
+
+    def __init__(self, url: str, headers=None):
+        self.url = url
+        self._headers = headers
+
+    @property
+    def headers(self):
+        """Accessor for the agents http headers.
+
+        Returns:
+            dict or None
+
+        """
+        return self._headers
+
+    @headers.setter
+    def headers(self, headers):
+        """Setter for agent's http headers.
+
+        Args:
+            headers: dict or None
+
+        """
+        self._headers = headers
+
+    @unwrap_json_response
+    @fail_if_not_ok("Create invitation failed")
+    def create_invitation(self, json=None, **kwargs):
+        """Create invitation."""
+        return post(
+            self.url,
+            "/connections/create-invitation",
+            params=kwargs,
+            headers=self._headers,
+            json=json,
+        )
+
+    @unwrap_json_response
+    @fail_if_not_ok("Receive invitation failed")
+    def receive_invite(self, invite: dict, **kwargs):
+        """Receive invitation."""
+        return post(
+            self.url,
+            "/connections/receive-invitation",
+            params=kwargs,
+            headers=self._headers,
+            json=invite,
+        )
+
+    @unwrap_json_response
+    @fail_if_not_ok("Accept invitation failed")
+    def accept_invite(self, connection_id: str, **kwargs):
+        """Accept invitation."""
+        return post(
+            self.url,
+            f"/connections/{connection_id}/accept-invitation",
+            params=kwargs,
+            headers=self._headers,
+        )
+
+    @unwrap_json_response
+    @fail_if_not_ok("Create invitation failed")
+    def list_connections(self, **kwargs):
+        """List connections."""
+        results = get(self.url, "/connections", params=kwargs, headers=self._headers)
+        return results
+
+    @unwrap_json_response
+    @fail_if_not_ok("Failed to get connection by id")
+    def get_connection(self, connection_id: str, **kwargs):
+        """Fetch a connection."""
+        return get(
+            self.url,
+            f"/connections/{connection_id}",
+            params=kwargs,
+            headers=self._headers,
+        )
+
+    @unwrap_json_response
+    @fail_if_not_ok("Failed to ping connection")
+    def ping_connection(self, connection_id: str, alias: str, **kwargs):
+        """ping connection."""
+        return post(
+            self.url,
+            f"/connections/{connection_id}/send-ping",
+            params=kwargs,
+            headers=self._headers,
+            json={"comment": f"{alias} pinging..."},
+        )
+
+    @unwrap_json_response
+    @fail_if_not_ok("Failure requesting mediation")
+    def request_for_mediation(self, connection_id: str, **kwargs):
+        """Request mediation from mediator."""
+        return post(
+            self.url,
+            f"/mediation/request/{connection_id}",
+            params=kwargs,
+            headers=self._headers,
+            json={},
+        )
+
+    @unwrap_json_response
+    @fail_if_not_ok("Failed to check mediation request")
+    def get_mediation_request(self, mediation_id: str, **kwargs):
+        """Fetch mediation request."""
+        return get(
+            self.url,
+            f"/mediation/requests/{mediation_id}",
+            params=kwargs,
+            headers=self._headers,
+        )
+
+    @unwrap_json_response
+    @fail_if_not_ok("Failure requesting mediation")
+    def create_tenant(self, wallet_name: str, wallet_key: str, **kwargs):
+        """Create a tenant in multitenanted acapy."""
+        data = {
+            "key_management_mode": "managed",
+            "wallet_dispatch_type": "default",
+            "wallet_name": wallet_name,
+            "wallet_key": wallet_key,
+            "label": wallet_name,
+            "wallet_type": "askar",
+            "wallet_webhook_urls": [],
+        }
+        # multi-agent has no security for base wallet, just call multitenancy/wallet
+        # to create a new tenant
+        return post(self.url, "/multitenancy/wallet", headers={}, json=data)
+
+    def get(self, path: str, return_json: bool = True, fail_with: str = None, **kwargs):
+        """Do get to agent endpoint."""
+        wrapped_get = get
+        if fail_with:
+            wrapped_get = fail_if_not_ok(fail_with)(wrapped_get)
+        if return_json:
+            wrapped_get = unwrap_json_response(wrapped_get)
+
+        return wrapped_get(self.url, path, **kwargs)
+
+    def post(
+        self, path: str, return_json: bool = True, fail_with: str = None, **kwargs
+    ):
+        """Do post to agent endpoint."""
+        wrapped_post = post
+        if fail_with:
+            wrapped_post = fail_if_not_ok(fail_with)(wrapped_post)
+        if return_json:
+            wrapped_post = unwrap_json_response(wrapped_post)
+
+        return wrapped_post(self.url, path, **kwargs)
diff --git a/demo/playground/examples/tests/test_mediator_ping_agents.py b/demo/playground/examples/tests/test_mediator_ping_agents.py
new file mode 100644
--- /dev/null
+++ b/demo/playground/examples/tests/test_mediator_ping_agents.py
@@ -0,0 +1,258 @@
+"""Integration tests for Basic Message Storage."""
+
+# pylint: disable=redefined-outer-name
+
+import base64
+import os
+import pytest
+import time
+import uuid
+
+import json as jsonlib
+
+from . import logger, Agent, FABER, ALICE, MULTI
+
+# add a blank line...
+logger.info("start testing mediated connections...")
+
+
+@pytest.fixture(scope="session")
+def faber():
+    """faber agent fixture."""
+    logger.info(f"faber = {FABER}")
+    yield Agent(FABER)
+
+
+@pytest.fixture(scope="session")
+def alice():
+    """resolver agent fixture."""
+    logger.info(f"alice = {ALICE}")
+    yield Agent(ALICE)
+
+
+@pytest.fixture(scope="session")
+def multi_one():
+    """resolver agent fixture."""
+    agent = Agent(MULTI)
+    wallet_name = f"multi_one_{str(uuid.uuid4())[0:8]}"
+    resp = agent.create_tenant(wallet_name, "changeme")
+    wallet_id = resp["wallet_id"]
+    token = resp["token"]
+    agent.headers = {"Authorization": f"Bearer {token}"}
+    yield agent
+
+
+@pytest.fixture(scope="session")
+def mediation_invite():
+    invitation_url = os.getenv("MEDIATOR_INVITATION_URL")
+    logger.info(f"MEDIATOR_INVITATION_URL = {invitation_url}")
+    base64_message = invitation_url.split("=", maxsplit=1)[1]
+    base64_bytes = base64_message.encode("ascii")
+    message_bytes = base64.b64decode(base64_bytes)
+    data = message_bytes.decode("ascii")
+    logger.info(f"invitation_block = {data}")
+    yield data
+
+
+def initialize_mediation(agent: Agent, invitation):
+    connection_id = None
+    mediator_connection_state = None
+    mediation_id = None
+    mediation_granted = False
+
+    invitation_json = agent.receive_invite(
+        invitation, auto_accept="true", alias="mediator"
+    )
+    connection_id = invitation_json["connection_id"]
+    logger.info(f"connection_id: {connection_id}")
+    time.sleep(2)
+
+    attempts = 0
+    while mediator_connection_state != "active" and attempts < 5:
+        time.sleep(1)
+        connection_json = agent.get_connection(connection_id)
+        mediator_connection_state = connection_json["state"]
+        logger.info(f"mediator_connection_state: {mediator_connection_state}")
+        attempts = attempts + 1
+
+    if mediator_connection_state == "active":
+        mediation_request_json = agent.request_for_mediation(connection_id)
+        mediation_id = mediation_request_json["mediation_id"]
+        logger.info(f"mediation_id: {mediation_id}")
+        mediation_granted = False
+        attempts = 0
+        while not mediation_granted and attempts < 5:
+            time.sleep(1)
+            granted_json = agent.get_mediation_request(mediation_id)
+            mediation_granted = granted_json["state"] == "granted"
+            logger.info(f"mediation_granted: {mediation_granted}")
+            attempts = attempts + 1
+
+    result = {
+        "connection_id": connection_id,
+        "mediation_id": mediation_id,
+        "mediator_connection_state": mediator_connection_state,
+        "mediation_granted": mediation_granted,
+    }
+    return result
+
+
+@pytest.fixture(scope="session")
+def faber_mediator(faber, mediation_invite):
+    logger.info(f"faber_mediator...")
+    result = initialize_mediation(faber, mediation_invite)
+    logger.info(f"...faber_mediator = {result}")
+    yield result
+
+
+@pytest.fixture(scope="session")
+def alice_mediator(alice, mediation_invite):
+    logger.info(f"alice_mediator...")
+    result = initialize_mediation(alice, mediation_invite)
+    logger.info(f"...alice_mediator = {result}")
+    yield result
+
+
+@pytest.fixture(scope="session")
+def multi_one_mediator(multi_one, mediation_invite):
+    logger.info(f"multi_one_mediator...")
+    result = initialize_mediation(multi_one, mediation_invite)
+    logger.info(f"...multi_one_mediator = {result}")
+    yield result
+
+
+@pytest.mark.skipif(
+    os.getenv("MEDIATOR_INVITATION_URL") in [None, "", " "],
+    reason="MEDIATOR_INVITATION_URL not set. Running tests that do not require mediator.",
+)
+def test_mediated_single_tenants(
+    faber, alice, faber_mediator, alice_mediator, mediation_invite
+):
+    assert faber_mediator["mediation_granted"] == True
+    assert alice_mediator["mediation_granted"] == True
+
+    resp = faber.create_invitation(
+        alias="alice",
+        auto_accept="true",
+        json={"my_label": "faber", "mediation_id": faber_mediator["mediation_id"]},
+    )
+    faber_alice_connection_id = resp["connection_id"]
+    logger.info(f"faber_alice_connection_id = {faber_alice_connection_id}")
+    assert faber_alice_connection_id
+    invite = resp["invitation"]
+    logger.info(f"invite = {invite}")
+    assert invite
+
+    mediation_invite_json = jsonlib.loads(mediation_invite)
+    logger.info(f"invitation service endpoint = {invite['serviceEndpoint']}")
+    logger.info(
+        f"mediator service endpoint = {mediation_invite_json['serviceEndpoint']}"
+    )
+    assert invite["serviceEndpoint"] == mediation_invite_json["serviceEndpoint"]
+
+    resp = alice.receive_invite(invite, alias="faber", auto_accept="true")
+    alice_faber_connection_id = resp["connection_id"]
+    logger.info(f"alice_faber_connection_id = {alice_faber_connection_id}")
+    assert alice_faber_connection_id
+
+    faber_alice_connection_active = False
+    attempts = 0
+    while not faber_alice_connection_active and attempts < 5:
+        time.sleep(1)
+        connection_resp = faber.get_connection(faber_alice_connection_id)
+        faber_alice_connection_active = connection_resp["state"] == "active"
+        logger.info(f"faber/alice active?  {faber_alice_connection_active}")
+        attempts = attempts + 1
+
+    alice_faber_connection_active = False
+    attempts = 0
+    while not alice_faber_connection_active and attempts < 5:
+        time.sleep(1)
+        connection_resp = alice.get_connection(alice_faber_connection_id)
+        alice_faber_connection_active = connection_resp["state"] == "active"
+        logger.info(f"alice/faber active?  {alice_faber_connection_active}")
+        attempts = attempts + 1
+
+    assert faber_alice_connection_active == True
+    assert alice_faber_connection_active == True
+
+    logger.info("faber alice pinging...")
+    pings = 0
+    while pings < 10:
+        resp = faber.ping_connection(faber_alice_connection_id, "faber")
+        logger.info(f"faber ping alice =  {resp}")
+        time.sleep(1)
+        alice.ping_connection(alice_faber_connection_id, "alice")
+        logger.info(f"alice ping faber =  {resp}")
+        time.sleep(1)
+        pings = pings + 1
+
+
+@pytest.mark.skipif(
+    os.getenv("MEDIATOR_INVITATION_URL") in [None, "", " "],
+    reason="MEDIATOR_INVITATION_URL not set. Running tests that do not require mediator.",
+)
+def test_mediated_multi_tenants(
+    multi_one, alice, multi_one_mediator, alice_mediator, mediation_invite
+):
+    assert multi_one_mediator["mediation_granted"] == True
+    assert alice_mediator["mediation_granted"] == True
+
+    resp = multi_one.create_invitation(
+        alias="alice",
+        auto_accept="true",
+        json={
+            "my_label": "multi_one",
+            "mediation_id": multi_one_mediator["mediation_id"],
+        },
+    )
+    multi_one_alice_connection_id = resp["connection_id"]
+    logger.info(f"multi_one_alice_connection_id = {multi_one_alice_connection_id}")
+    assert multi_one_alice_connection_id
+    invite = resp["invitation"]
+    logger.info(f"invite = {invite}")
+    assert invite
+
+    mediation_invite_json = jsonlib.loads(mediation_invite)
+    logger.info(f"invitation service endpoint = {invite['serviceEndpoint']}")
+    logger.info(
+        f"mediator service endpoint = {mediation_invite_json['serviceEndpoint']}"
+    )
+    assert invite["serviceEndpoint"] == mediation_invite_json["serviceEndpoint"]
+
+    resp = alice.receive_invite(invite, alias="multi_one", auto_accept="true")
+    alice_multi_one_connection_id = resp["connection_id"]
+    logger.info(f"alice_multi_one_connection_id = {alice_multi_one_connection_id}")
+    assert alice_multi_one_connection_id
+
+    multi_one_alice_connection_active = False
+    attempts = 0
+    while not multi_one_alice_connection_active and attempts < 5:
+        time.sleep(1)
+        connection_resp = multi_one.get_connection(multi_one_alice_connection_id)
+        multi_one_alice_connection_active = connection_resp["state"] == "active"
+        logger.info(f"multi_one/alice active?  {multi_one_alice_connection_active}")
+        attempts = attempts + 1
+
+    alice_multi_one_connection_active = False
+    attempts = 0
+    while not alice_multi_one_connection_active and attempts < 5:
+        time.sleep(1)
+        connection_resp = alice.get_connection(alice_multi_one_connection_id)
+        alice_multi_one_connection_active = connection_resp["state"] == "active"
+        logger.info(f"alice/multi_one active?  {alice_multi_one_connection_active}")
+        attempts = attempts + 1
+
+    assert multi_one_alice_connection_active == True
+    assert alice_multi_one_connection_active == True
+
+    logger.info("multi_one alice pinging...")
+    pings = 0
+    while pings < 10:
+        resp = multi_one.ping_connection(multi_one_alice_connection_id, "multi_one")
+        logger.info(f"multi_one ping alice =  {resp}")
+        time.sleep(1)
+        alice.ping_connection(alice_multi_one_connection_id, "alice")
+        logger.info(f"alice ping multi_one =  {resp}")
+        time.sleep(1)
+        pings = pings + 1
diff --git a/demo/playground/examples/tests/test_ping_agents.py b/demo/playground/examples/tests/test_ping_agents.py
new file mode 100644
--- /dev/null
+++ b/demo/playground/examples/tests/test_ping_agents.py
@@ -0,0 +1,164 @@
+"""Integration tests for Basic Message Storage."""
+
+# pylint: disable=redefined-outer-name
+
+import os
+import pytest
+import time
+import uuid
+
+from . import logger, Agent, FABER, ALICE, MULTI
+
+
+@pytest.fixture(scope="session")
+def faber():
+    """faber agent fixture."""
+    yield Agent(FABER)
+
+
+@pytest.fixture(scope="session")
+def alice():
+    """resolver agent fixture."""
+    yield Agent(ALICE)
+
+
+@pytest.fixture(scope="session")
+def multi_one():
+    """resolver agent fixture."""
+    agent = Agent(MULTI)
+    wallet_name = f"multi_one_{str(uuid.uuid4())[0:8]}"
+    resp = agent.create_tenant(wallet_name, "changeme")
+    wallet_id = resp["wallet_id"]
+    token = resp["token"]
+    agent.headers = {"Authorization": f"Bearer {token}"}
+    yield agent
+
+
+@pytest.fixture(scope="session", autouse=True)
+def alice_faber_connection(faber, alice):
+    """Established connection filter."""
+    logger.info("faber create invitation to alice")
+    invite = faber.create_invitation(auto_accept="true")["invitation"]
+    logger.info(f"invitation = {invite}")
+    logger.info(f"alice receive invitation")
+    resp = alice.receive_invite(invite, auto_accept="true")
+    result = resp["connection_id"]
+    logger.info(f"alice/faber connection_id = {result}")
+    return result
+
+
+@pytest.fixture(scope="session", autouse=True)
+def faber_alice_connection(faber, alice):
+    """Established connection filter."""
+    logger.info("alice create invitation to faber")
+    invite = alice.create_invitation(auto_accept="true")["invitation"]
+    logger.info(f"invitation = {invite}")
+    logger.info(f"faber receive invitation")
+    resp = faber.receive_invite(invite, auto_accept="true")
+    result = resp["connection_id"]
+    logger.info(f"faber/alice connection_id = {result}")
+    return result
+
+
+@pytest.fixture(scope="session", autouse=True)
+def alice_multi_one_connection(multi_one, alice):
+    """Established connection filter."""
+    logger.info("multi_one create invitation to alice")
+    invite = multi_one.create_invitation(auto_accept="true")["invitation"]
+    logger.info(f"invitation = {invite}")
+    logger.info(f"alice receive invitation")
+    resp = alice.receive_invite(invite, auto_accept="true")
+    result = resp["connection_id"]
+    logger.info(f"alice/multi_one connection_id = {result}")
+    return result
+
+
+@pytest.fixture(scope="session", autouse=True)
+def multi_one_alice_connection(multi_one, alice):
+    """Established connection filter."""
+    logger.info("alice create invitation to multi_one")
+    invite = alice.create_invitation(auto_accept="true")["invitation"]
+    logger.info(f"invitation = {invite}")
+    logger.info(f"faber receive invitation")
+    resp = multi_one.receive_invite(invite, auto_accept="true")
+    result = resp["connection_id"]
+    logger.info(f"multi_one/alice connection_id = {result}")
+    return result
+
+
+@pytest.mark.skipif(
+    os.getenv("MEDIATOR_INVITATION_URL") not in [None, "", " "],
+    reason="MEDIATOR_INVITATION_URL is set. Running only tests that require mediator.",
+)
+def test_single_tenants(faber, alice, faber_alice_connection, alice_faber_connection):
+    faber_alice_connection_active = False
+    attempts = 0
+    while not faber_alice_connection_active and attempts < 5:
+        time.sleep(1)
+        connection_resp = faber.get_connection(faber_alice_connection)
+        faber_alice_connection_active = connection_resp["state"] == "active"
+        logger.info(f"faber/alice active?  {faber_alice_connection_active}")
+        attempts = attempts + 1
+
+    alice_faber_connection_active = False
+    attempts = 0
+    while not alice_faber_connection_active and attempts < 5:
+        time.sleep(1)
+        connection_resp = alice.get_connection(alice_faber_connection)
+        alice_faber_connection_active = connection_resp["state"] == "active"
+        logger.info(f"alice/faber active?  {alice_faber_connection_active}")
+        attempts = attempts + 1
+
+    assert faber_alice_connection_active == True
+    assert alice_faber_connection_active == True
+
+    logger.info("faber alice pinging...")
+    pings = 0
+    while pings < 10:
+        resp = faber.ping_connection(faber_alice_connection, "faber")
+        logger.info(f"faber ping alice =  {resp}")
+        time.sleep(1)
+        alice.ping_connection(alice_faber_connection, "alice")
+        logger.info(f"alice ping faber =  {resp}")
+        time.sleep(1)
+        pings = pings + 1
+
+
+@pytest.mark.skipif(
+    os.getenv("MEDIATOR_INVITATION_URL") not in [None, "", " "],
+    reason="MEDIATOR_INVITATION_URL is set. Running only tests that require mediator.",
+)
+def test_multi_tenants(
+    multi_one, alice, multi_one_alice_connection, alice_multi_one_connection
+):
+    multi_one_alice_connection_active = False
+    attempts = 0
+    while not multi_one_alice_connection_active and attempts < 5:
+        time.sleep(1)
+        connection_resp = multi_one.get_connection(multi_one_alice_connection)
+        multi_one_alice_connection_active = connection_resp["state"] == "active"
+        logger.info(f"multi_one/alice active?  {multi_one_alice_connection_active}")
+        attempts = attempts + 1
+
+    alice_multi_one_connection_active = False
+    attempts = 0
+    while not alice_multi_one_connection_active and attempts < 5:
+        time.sleep(1)
+        connection_resp = alice.get_connection(alice_multi_one_connection)
+        alice_multi_one_connection_active = connection_resp["state"] == "active"
+        logger.info(f"alice/multi_one active?  {alice_multi_one_connection_active}")
+        attempts = attempts + 1
+
+    assert multi_one_alice_connection_active == True
+    assert alice_multi_one_connection_active == True
+
+    logger.info("multi_one alice pinging...")
+    pings = 0
+    while pings < 10:
+        resp = multi_one.ping_connection(multi_one_alice_connection, "multi_one")
+        logger.info(f"multi_one ping alice =  {resp}")
+        time.sleep(1)
+        alice.ping_connection(alice_multi_one_connection, "alice")
+        logger.info(f"alice ping multi_one =  {resp}")
+        time.sleep(1)
+        pings = pings + 1