Images whose sampler paths could not be traced offered nothing to choose. Add Link to a prompt: search the Library and record the reviewer's choice under Apply's capture guards and the one-prompt-per-file rules, with provenance that names a manual choice and never reads as file evidence. Disabled options name the server's reason, nothing selectable says why and leads with the picker, Leave unresolved is its own action, and each decision opens the next item instead of returning to Images (overturns ruling R7).
67 lines
2.5 KiB
Python
67 lines
2.5 KiB
Python
"""Body validation of Needs review decisions and manual links (#249 E3, #536)."""
|
|
|
|
from .annotations import _HEX64, _TOKEN, _UUID
|
|
from .prompts import bad_request
|
|
|
|
UNRESOLVED = "unresolved"
|
|
_GUARD_FIELDS = frozenset(
|
|
{
|
|
"expected_epoch",
|
|
"expected_revision",
|
|
"capture_token",
|
|
"sha256",
|
|
"dependency_fingerprint",
|
|
}
|
|
)
|
|
_DECISION_FIELDS = _GUARD_FIELDS | {"choice"}
|
|
_LINK_FIELDS = _GUARD_FIELDS | {"positive_id", "pair_id"}
|
|
|
|
|
|
def _guard_error(data: dict, fields: frozenset):
|
|
"""The first problem with a decision's shared fields, as a response."""
|
|
if set(data) != fields:
|
|
return bad_request("Request must contain exactly " + ", ".join(sorted(fields)))
|
|
epoch, revision = data["expected_epoch"], data["expected_revision"]
|
|
if not isinstance(epoch, str) or _UUID.fullmatch(epoch) is None:
|
|
return bad_request("expected_epoch must be a canonical UUID")
|
|
if type(revision) is not int or revision < 0:
|
|
return bad_request("expected_revision must be a non-negative integer")
|
|
token = data["capture_token"]
|
|
if token is not None and (
|
|
not isinstance(token, str) or _TOKEN.fullmatch(token) is None
|
|
):
|
|
return bad_request("Invalid capture token")
|
|
for name in ("sha256", "dependency_fingerprint"):
|
|
value = data[name]
|
|
if not isinstance(value, str) or _HEX64.fullmatch(value) is None:
|
|
return bad_request(f"{name} must be 64 lowercase hexadecimal characters")
|
|
return None
|
|
|
|
|
|
def _decision_guards(data: dict):
|
|
"""The validated decision fields, or ``(None, error)``."""
|
|
error = _guard_error(data, _DECISION_FIELDS)
|
|
if error is not None:
|
|
return None, error
|
|
choice = data["choice"]
|
|
if not isinstance(choice, str) or (
|
|
choice != UNRESOLVED and _HEX64.fullmatch(choice) is None
|
|
):
|
|
return None, bad_request("choice must be a candidate hash or unresolved")
|
|
return {**data}, None
|
|
|
|
|
|
def _link_guards(data: dict):
|
|
"""The validated manual link fields, or ``(None, error)``."""
|
|
error = _guard_error(data, _LINK_FIELDS)
|
|
if error is not None:
|
|
return None, error
|
|
if data["capture_token"] is None:
|
|
return None, bad_request("A link needs a capture of the file")
|
|
positive, pair = data["positive_id"], data["pair_id"]
|
|
if type(positive) is not int or positive < 1:
|
|
return None, bad_request("positive_id must be a positive integer")
|
|
if pair is not None and (type(pair) is not int or pair < 1):
|
|
return None, bad_request("pair_id must be a positive integer or null")
|
|
return {**data}, None
|