perf(core): define bounded reference search
This commit is contained in:
@@ -53,6 +53,8 @@ class ReferenceOptionResponse(BaseModel):
|
||||
class ReferenceOptionListResponse(BaseModel):
|
||||
options: list[ReferenceOptionResponse] = Field(default_factory=list)
|
||||
provider_available: bool = True
|
||||
next_cursor: str | None = None
|
||||
has_more: bool = False
|
||||
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
|
||||
@@ -12,6 +12,7 @@ from govoplan_core.core.access import (
|
||||
)
|
||||
|
||||
|
||||
CAPABILITY_ACCESS_REFERENCE_OPTIONS = "access.reference_options"
|
||||
ReferenceAvailability = Literal["available", "inactive", "unavailable"]
|
||||
|
||||
|
||||
@@ -48,9 +49,17 @@ class ReferenceSearchRequest:
|
||||
query: str = ""
|
||||
selected_values: tuple[str, ...] = ()
|
||||
limit: int = 50
|
||||
cursor: str | None = None
|
||||
context: Mapping[str, object] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ReferenceSearchPage:
|
||||
options: tuple[ReferenceOption, ...] = ()
|
||||
next_cursor: str | None = None
|
||||
has_more: bool = False
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class ReferenceOptionProvider(Protocol):
|
||||
"""Optional module-neutral provider for typed reference selectors."""
|
||||
@@ -61,7 +70,7 @@ class ReferenceOptionProvider(Protocol):
|
||||
principal: object,
|
||||
*,
|
||||
request: ReferenceSearchRequest,
|
||||
) -> Sequence[ReferenceOption]:
|
||||
) -> ReferenceSearchPage:
|
||||
...
|
||||
|
||||
|
||||
@@ -89,38 +98,115 @@ def access_scope_reference_options(
|
||||
selected_values: Sequence[str] = (),
|
||||
limit: int = 50,
|
||||
administrative: bool = False,
|
||||
session: object | None = None,
|
||||
reference_kind: str | None = None,
|
||||
) -> tuple[ReferenceOption, ...]:
|
||||
"""Return canonical user-account or group references for definition scopes."""
|
||||
|
||||
return access_scope_reference_page(
|
||||
registry,
|
||||
principal,
|
||||
scope_type=scope_type,
|
||||
query=query,
|
||||
selected_values=selected_values,
|
||||
limit=limit,
|
||||
administrative=administrative,
|
||||
session=session,
|
||||
reference_kind=reference_kind,
|
||||
).options
|
||||
|
||||
|
||||
def access_scope_reference_page(
|
||||
registry: object | None,
|
||||
principal: object,
|
||||
*,
|
||||
scope_type: str,
|
||||
query: str = "",
|
||||
selected_values: Sequence[str] = (),
|
||||
limit: int = 50,
|
||||
cursor: str | None = None,
|
||||
administrative: bool = False,
|
||||
session: object | None = None,
|
||||
reference_kind: str | None = None,
|
||||
) -> ReferenceSearchPage:
|
||||
"""Search bounded Access references while retaining selected values."""
|
||||
|
||||
clean_type = str(scope_type or "").strip().casefold()
|
||||
if clean_type not in {"user", "group"}:
|
||||
raise ValueError("Scope reference type must be user or group.")
|
||||
clean_kind = str(reference_kind or clean_type).strip().casefold()
|
||||
if clean_kind not in (
|
||||
{"user", "membership"} if clean_type == "user" else {"group"}
|
||||
):
|
||||
raise ValueError("Scope reference kind is incompatible with the scope type.")
|
||||
normalized_limit = max(1, min(int(limit), 200))
|
||||
selected = tuple(
|
||||
dict.fromkeys(
|
||||
str(value).strip() for value in selected_values if str(value).strip()
|
||||
)
|
||||
)[:200]
|
||||
tenant_id = str(getattr(principal, "tenant_id", "") or "")
|
||||
provider = reference_option_provider(
|
||||
registry,
|
||||
CAPABILITY_ACCESS_REFERENCE_OPTIONS,
|
||||
)
|
||||
directory = _access_directory(registry)
|
||||
if directory is None:
|
||||
return _fallback_scope_options(
|
||||
if provider is not None:
|
||||
if not tenant_id:
|
||||
return ReferenceSearchPage(
|
||||
options=tuple(
|
||||
_unavailable_option(value, kind=clean_kind)
|
||||
for value in selected
|
||||
)
|
||||
)
|
||||
page = provider.search_reference_options(
|
||||
session,
|
||||
principal,
|
||||
scope_type=clean_type,
|
||||
query=query,
|
||||
selected_values=selected,
|
||||
limit=normalized_limit,
|
||||
request=ReferenceSearchRequest(
|
||||
kind=clean_kind,
|
||||
tenant_id=tenant_id,
|
||||
query=str(query or "").strip().casefold(),
|
||||
selected_values=selected,
|
||||
limit=normalized_limit,
|
||||
cursor=str(cursor).strip() if cursor else None,
|
||||
context={"administrative": administrative},
|
||||
),
|
||||
)
|
||||
return ReferenceSearchPage(
|
||||
options=_bounded_page_options(
|
||||
page.options,
|
||||
selected_values=selected,
|
||||
limit=normalized_limit,
|
||||
kind=clean_kind,
|
||||
),
|
||||
next_cursor=page.next_cursor,
|
||||
has_more=page.has_more,
|
||||
)
|
||||
|
||||
directory = _access_directory(registry)
|
||||
if directory is None:
|
||||
return ReferenceSearchPage(
|
||||
options=_fallback_scope_options(
|
||||
principal,
|
||||
scope_type=clean_type,
|
||||
reference_kind=clean_kind,
|
||||
query=query,
|
||||
selected_values=selected,
|
||||
limit=normalized_limit,
|
||||
)
|
||||
)
|
||||
|
||||
tenant_id = str(getattr(principal, "tenant_id", "") or "")
|
||||
if not tenant_id:
|
||||
return tuple(
|
||||
_unavailable_option(value, kind=clean_type) for value in selected
|
||||
return ReferenceSearchPage(
|
||||
options=tuple(
|
||||
_unavailable_option(value, kind=clean_kind) for value in selected
|
||||
)
|
||||
)
|
||||
if clean_type == "user":
|
||||
candidates = _user_options(
|
||||
directory.users_for_tenant(tenant_id),
|
||||
principal=principal,
|
||||
administrative=administrative,
|
||||
value_kind=clean_kind,
|
||||
)
|
||||
else:
|
||||
candidates = _group_options(
|
||||
@@ -128,17 +214,26 @@ def access_scope_reference_options(
|
||||
principal=principal,
|
||||
administrative=administrative,
|
||||
)
|
||||
return _filter_and_retain_options(
|
||||
candidates,
|
||||
query=query,
|
||||
selected_values=selected,
|
||||
limit=normalized_limit,
|
||||
kind=clean_type,
|
||||
return ReferenceSearchPage(
|
||||
options=_filter_and_retain_options(
|
||||
candidates,
|
||||
query=query,
|
||||
selected_values=selected,
|
||||
limit=normalized_limit,
|
||||
kind=clean_kind,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def access_scope_reference_provider_available(registry: object | None) -> bool:
|
||||
return _access_directory(registry) is not None
|
||||
return (
|
||||
reference_option_provider(
|
||||
registry,
|
||||
CAPABILITY_ACCESS_REFERENCE_OPTIONS,
|
||||
)
|
||||
is not None
|
||||
or _access_directory(registry) is not None
|
||||
)
|
||||
|
||||
|
||||
def validate_access_scope_reference(
|
||||
@@ -209,6 +304,7 @@ def _user_options(
|
||||
*,
|
||||
principal: object,
|
||||
administrative: bool,
|
||||
value_kind: str = "user",
|
||||
) -> tuple[ReferenceOption, ...]:
|
||||
principal_account_id = str(getattr(principal, "account_id", "") or "")
|
||||
options: dict[str, ReferenceOption] = {}
|
||||
@@ -218,11 +314,12 @@ def _user_options(
|
||||
inactive = user.status != "active"
|
||||
label = user.display_name or user.email or user.account_id
|
||||
detail_parts = [part for part in (user.email, "Inactive" if inactive else None) if part]
|
||||
options[user.account_id] = ReferenceOption(
|
||||
value=user.account_id,
|
||||
value = user.id if value_kind == "membership" else user.account_id
|
||||
options[value] = ReferenceOption(
|
||||
value=value,
|
||||
label=label,
|
||||
description=" · ".join(detail_parts) or None,
|
||||
kind="user",
|
||||
kind=value_kind,
|
||||
availability="inactive" if inactive else "available",
|
||||
disabled=inactive,
|
||||
source_module="access",
|
||||
@@ -296,28 +393,58 @@ def _filter_and_retain_options(
|
||||
return tuple(matches)
|
||||
|
||||
|
||||
def _bounded_page_options(
|
||||
options: Sequence[ReferenceOption],
|
||||
*,
|
||||
selected_values: Sequence[str],
|
||||
limit: int,
|
||||
kind: str,
|
||||
) -> tuple[ReferenceOption, ...]:
|
||||
by_value = {option.value: option for option in options}
|
||||
selected = set(selected_values)
|
||||
bounded = [
|
||||
option for option in options if option.value not in selected
|
||||
][:limit]
|
||||
returned = {option.value for option in bounded}
|
||||
for value in selected_values:
|
||||
if value in returned:
|
||||
continue
|
||||
bounded.append(by_value.get(value) or _unavailable_option(value, kind=kind))
|
||||
returned.add(value)
|
||||
return tuple(bounded)
|
||||
|
||||
|
||||
def _fallback_scope_options(
|
||||
principal: object,
|
||||
*,
|
||||
scope_type: str,
|
||||
reference_kind: str | None = None,
|
||||
query: str,
|
||||
selected_values: Sequence[str],
|
||||
limit: int,
|
||||
) -> tuple[ReferenceOption, ...]:
|
||||
candidates: list[ReferenceOption] = []
|
||||
if scope_type == "user":
|
||||
account_id = str(getattr(principal, "account_id", "") or "")
|
||||
if account_id:
|
||||
kind = str(reference_kind or "user")
|
||||
value = str(
|
||||
(
|
||||
getattr(principal, "membership_id", "")
|
||||
if kind == "membership"
|
||||
else getattr(principal, "account_id", "")
|
||||
)
|
||||
or ""
|
||||
)
|
||||
if value:
|
||||
candidates.append(
|
||||
ReferenceOption(
|
||||
value=account_id,
|
||||
value=value,
|
||||
label=(
|
||||
str(getattr(principal, "display_name", "") or "")
|
||||
or str(getattr(principal, "email", "") or "")
|
||||
or account_id
|
||||
or value
|
||||
),
|
||||
description="Current user · Access directory unavailable",
|
||||
kind="user",
|
||||
kind=kind,
|
||||
source_module="core",
|
||||
provenance={"fallback": True},
|
||||
)
|
||||
@@ -358,10 +485,13 @@ def _unavailable_option(value: str, *, kind: str) -> ReferenceOption:
|
||||
|
||||
|
||||
__all__ = [
|
||||
"CAPABILITY_ACCESS_REFERENCE_OPTIONS",
|
||||
"ReferenceAvailability",
|
||||
"ReferenceOption",
|
||||
"ReferenceOptionProvider",
|
||||
"ReferenceSearchPage",
|
||||
"ReferenceSearchRequest",
|
||||
"access_scope_reference_page",
|
||||
"access_scope_reference_options",
|
||||
"access_scope_reference_provider_available",
|
||||
"reference_option_provider",
|
||||
|
||||
Reference in New Issue
Block a user