Files

88 lines
3.1 KiB
Python

"""Which flow an argument names: its id or its key, id first.
The API takes ids and only ids. People type keys, and scripts written before
ids existed pass keys, so the CLI, the SDK and the MCP tools answer the same
question here, with the same words.
Pure: the caller fetches what it may see with its own credentials and hands
it over as ``(id, key)`` pairs. The SDK carries no engine code, so this lives
with it, and the server's MCP tools import it from here.
"""
from __future__ import annotations
import difflib
from collections.abc import Iterable
from dataclasses import dataclass
from fluksio.sdk import SyncError
__all__ = ["Resolved", "Unresolved", "resolve"]
#: The explicit forms. A colon is legal in neither an id nor a key, so they
#: cannot clash with a real value.
ID = "id:"
KEY = "key:"
class Unresolved(SyncError):
"""Nothing, or more than one thing, answers to the argument."""
@dataclass(frozen=True)
class Resolved:
id: str
#: Empty for an explicit id nobody lists any more.
key: str
#: Set when the argument was one flow's id and another's key, and the
#: id was taken. Read-only callers show it; a change never gets this far.
warning: str = ""
def resolve(
value: str, flows: Iterable[tuple[str, str]], *, mutation: bool = False
) -> Resolved:
"""The flow *value* names among *flows*.
``id:<x>`` is taken as said, listed or not: history outlives its flow,
and the engine answers for an id nobody holds. ``key:<x>`` is the flow
holding that key. A plain string is an id if one has it, else a key.
A plain string that is one flow's id and another's key picks the id with
a warning, or is refused when *mutation* says the call changes something.
A key several flows answer to, which history can hold, is always refused.
"""
keys: dict[str, str] = {}
for flow_id, key in flows:
keys.setdefault(flow_id, key)
if value.startswith(ID):
named = value[len(ID) :]
return Resolved(named, keys.get(named, ""))
explicit = value.startswith(KEY)
key = value[len(KEY) :] if explicit else value
holders = [flow_id for flow_id, held in keys.items() if held == key]
if not explicit and value in keys:
others = [flow_id for flow_id in holders if flow_id != value]
if not others:
return Resolved(value, keys[value])
both = (
f"'{value}' is the id of '{keys[value]}' and the key of "
f"'{', '.join(others)}'."
)
if mutation:
raise Unresolved(f"{both} Pass id:{value} or key:{value}.")
return Resolved(
value, keys[value], f"{both} Using the id. Pass key:{value} for the other."
)
if len(holders) == 1:
return Resolved(holders[0], key)
if holders:
raise Unresolved(
f"Several flows have had the key '{key}': {', '.join(holders)}. "
"Pass id:<one of them>."
)
close = difflib.get_close_matches(key, sorted(set(keys.values())), n=3)
hint = f" Closest keys: {', '.join(close)}." if close else ""
what = "key" if explicit else "id or key"
raise Unresolved(f"No flow with the {what} '{key}'.{hint}")