Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 24 additions & 10 deletions docs/maintainer_features.rst
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,11 @@ Feature Inventory
* - Challenge requests
- ``client.challenges()``
- ``signify.app.challenging``
- Maintained for generate/respond/verify plus acceptance tracking.
- Maintained for TS-style challenge generation, response, verification, and acceptance tracking.
* - Contact requests
- ``client.contacts()``
- ``signify.app.contacting``
- Maintained as a read surface for resolved contacts.
- Maintained for TS-style contact list/get/add/update/delete, with one explicit legacy range-list path.
* - Credential requests
- ``client.credentials()``, ``client.ipex()``
- ``signify.app.credentialing``
Expand Down Expand Up @@ -244,14 +244,14 @@ Implementation:

Routes:

- ``GET /challenges``
- ``GET /challenges?strength={n}``
- ``POST /challenges_verify/{source}``
- ``PUT /challenges_verify/{source}``
- ``POST /identifiers/{name}/exchanges``

Responsibilities:

- Generate random challenge phrases.
- Generate random challenge phrases with explicit entropy strength.
- Send challenge responses through the peer exchange path.
- Ask KERIA to verify a signed response from a source AID.
- Mark accepted challenge responses as handled.
Expand All @@ -270,12 +270,19 @@ Implementation:
Routes:

- ``GET /contacts``
- ``GET /contacts/{prefix}``
- ``POST /contacts/{prefix}``
- ``PUT /contacts/{prefix}``
- ``DELETE /contacts/{prefix}``

Responsibilities:

- Return resolved contact records already known to the agent.
- Provide the maintained read path used by the integration helper layer after
OOBI resolution and workflow choreography.
- Support TS-style query/filter lookup through ``group``, ``filter_field``, and
``filter_value``.
- Support local metadata management for already-known remote contacts through
``get``, ``add``, ``update``, and ``delete``.
- Preserve one explicit legacy range-list path for older Python callers.

Primary tests:

Expand Down Expand Up @@ -460,19 +467,27 @@ and ``signify.app.escrowing.Escrows``

Routes:

- ``GET /operations``
- ``GET /operations/{name}``
- ``DELETE /operations/{name}``
- ``GET /notifications``
- ``PUT /notifications/{nid}``
- ``DELETE /notifications/{nid}``
- ``GET /escrows/rpy``

Responsibilities:

- Poll long-running operations started by identifier, registry, delegation, and
IPEX workflows.
- Fetch, list, poll, and remove long-running operations started by identifier,
registry, delegation, and IPEX workflows.
- Use ``Operations.wait(...)`` as the local convenience poller for completion,
dependency waits, timeout control, and optional caller-controlled
cancellation hooks.
- Inspect and acknowledge notification side effects created by peer and
multisig messaging.
multisig messaging. ``Notifications.mark()`` is the primary TS-style name;
``markAsRead()`` remains a compatibility alias.
- Read escrowed reply state for troubleshooting and support tooling.
``Escrows.listReply()`` is the primary TS-style name;
``getEscrowReply()`` remains a compatibility alias.

Primary tests:

Expand All @@ -491,7 +506,6 @@ Notable current gaps:

- no dedicated ``schemas()`` resource wrapper yet
- no dedicated ``config()`` resource wrapper yet
- ``operations()`` is currently narrow and only exposes ``get()``

When those surfaces are added, update this guide and the API reference in the
same change so the published docs remain aligned with the actual maintained
Expand Down
161 changes: 159 additions & 2 deletions src/signify/app/coring.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
long-running operations, OOBI retrieval and resolution, key-state reads, and
key-event reads.
"""
import time

from signify.app.clienting import SignifyClient


class Operations:
"""Resource wrapper for polling long-running KERIA operations."""
"""Resource wrapper for reading and removing long-running KERIA operations."""

def __init__(self, client: SignifyClient):
"""Create an operations resource bound to one Signify client."""
Expand All @@ -20,6 +22,162 @@ def get(self, name):
res = self.client.get(f"/operations/{name}")
return res.json()

def list(self, type=None):
"""List long-running operations, optionally filtered by operation type."""
params = {}
if type is not None:
params["type"] = type

res = self.client.get("/operations", params=params or None)
return res.json()

def delete(self, name):
"""Delete one long-running operation by operation name."""
self.client.delete(f"/operations/{name}")

def wait(
self,
op,
*,
timeout=None,
interval=0.01,
max_interval=10.0,
backoff=2.0,
check_abort=None,
options=None,
_deadline=None,
):
"""Poll an operation until it completes.

Python callers should prefer the explicit keyword arguments:
``timeout`` in seconds, ``interval`` in seconds, ``max_interval`` in
seconds, ``backoff`` as the exponential multiplier, and
``check_abort(current_op)`` for caller-controlled cancellation. The
TS-style ``options`` dict remains supported as a compatibility path.
"""
if options is not None:
return self._wait_with_options(op, options=options)

deadline = _deadline
if deadline is None and timeout is not None:
deadline = time.monotonic() + timeout

depends = self._depends(op)
if depends is not None and depends.get("done") is False:
self.wait(
depends,
interval=interval,
max_interval=max_interval,
backoff=backoff,
check_abort=check_abort,
_deadline=deadline,
)

if op.get("done") is True:
return op

retries = 0

while True:
op = self.get(op["name"])

if op.get("done") is True:
return op

self._raise_if_timed_out(deadline, op)
self._check_abort(check_abort, op)

delay = min(max_interval, interval * (backoff ** retries))
retries += 1

if deadline is not None:
remaining = deadline - time.monotonic()
self._raise_if_timed_out(deadline, op)
delay = min(delay, remaining)

time.sleep(delay)

@staticmethod
def _depends(op):
"""Return the dependent operation payload when present."""
metadata = op.get("metadata")
if isinstance(metadata, dict):
depends = metadata.get("depends")
if isinstance(depends, dict):
return depends

depends = op.get("depends")
if isinstance(depends, dict):
return depends

return None

@staticmethod
def _throw_if_aborted(signal):
"""Raise the caller-provided abort signal when it has fired."""
if signal is None:
return

if callable(signal):
signal()
return

if hasattr(signal, "throw_if_aborted"):
signal.throw_if_aborted()
return

if hasattr(signal, "throwIfAborted"):
signal.throwIfAborted()

@staticmethod
def _check_abort(check_abort, op):
"""Run an optional caller-provided cancellation hook."""
if check_abort is not None:
check_abort(op)

@staticmethod
def _raise_if_timed_out(deadline, op):
"""Raise ``TimeoutError`` when the wait deadline has expired."""
if deadline is not None and time.monotonic() >= deadline:
raise TimeoutError(
f"timed out waiting for operation {op['name']}; last_value={op!r}"
)

def _wait_with_options(self, op, *, options):
"""Compatibility wrapper for the TS-style wait options dictionary."""
options = {} if options is None else options
signal = options.get("signal")
min_sleep = options.get("minSleep", 10)
max_sleep = options.get("maxSleep", 10000)
increase_factor = options.get("increaseFactor", 50)

depends = self._depends(op)
if depends is not None and depends.get("done") is False:
self._wait_with_options(depends, options=options)

if op.get("done") is True:
return op

retries = 0

while True:
op = self.get(op["name"])

if hasattr(signal, "current_op"):
signal.current_op = op

delay = max(
min_sleep,
min(max_sleep, 2 ** retries * increase_factor),
)
retries += 1

if op.get("done") is True:
return op

time.sleep(delay / 1000)
self._throw_if_aborted(signal)


class Oobis:
"""Resource wrapper for OOBI retrieval and resolution."""
Expand Down Expand Up @@ -92,4 +250,3 @@ def get(self, pre):
"""Fetch KERI events for one AID prefix."""
res = self.client.get(f"/events?pre={pre}")
return res.json()

6 changes: 5 additions & 1 deletion src/signify/app/escrowing.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@ def __init__(self, client: SignifyClient):
"""Create an escrow resource bound to one Signify client."""
self.client = client

def getEscrowReply(self, route=None):
def listReply(self, route=None):
"""Return escrowed reply records, optionally filtered by route."""
params = {}
if route is not None:
params['route'] = route

res = self.client.get(f"/escrows/rpy", params=params)
return res.json()

def getEscrowReply(self, route=None):
"""Compatibility alias for :meth:`listReply`."""
return self.listReply(route=route)
12 changes: 8 additions & 4 deletions src/signify/app/notifying.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,22 @@ def list(self, start=0, end=24):

return dict(start=start, end=end, total=total, notes=res.json())

def markAsRead(self, nid):
"""Mark one notification as read.
def mark(self, said):
"""Mark one notification as read using the TS-compatible name.

Parameters:
nid (str): qb64 SAID of notification to mark as read
said (str): qb64 SAID of notification to mark as read

Returns:
bool: ``True`` when KERIA accepts the update request.
"""
res = self.client.put(f"/notifications/{nid}", json={})
res = self.client.put(f"/notifications/{said}", json={})
return res.status_code == 202

def markAsRead(self, nid):
"""Compatibility alias for :meth:`mark`."""
return self.mark(nid)

def delete(self, nid):
"""Delete one notification.

Expand Down
Loading
Loading