167 lines
5.0 KiB
Python
167 lines
5.0 KiB
Python
"""Bundled portable utilities — Python ports of small npm `utils/` files.
|
|
|
|
This module collects narrow, dependency-free helpers from
|
|
``utils/array.ts``, ``utils/set.ts``, ``utils/objectGroupBy.ts``,
|
|
``utils/xml.ts``, and ``utils/uuid.ts``. Keeping them in one file mirrors
|
|
how a user of the npm SDK would reach for these as a small toolbox.
|
|
|
|
Design notes:
|
|
- Function names mirror upstream where idiomatic in Python; ``every`` is
|
|
renamed ``every_in`` to avoid shadowing the built-in name in callers.
|
|
- ``object_group_by`` returns a plain ``dict`` rather than a ``Mapping`` so
|
|
callers can mutate it the same way the JS object would behave.
|
|
- ``create_agent_id`` mirrors the upstream format exactly:
|
|
``a{label-}{16 hex chars}``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import secrets
|
|
from collections.abc import Callable, Iterable
|
|
from typing import TypeVar
|
|
|
|
A = TypeVar('A')
|
|
T = TypeVar('T')
|
|
K = TypeVar('K')
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# array.ts
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def intersperse(items: Iterable[A], separator: Callable[[int], A]) -> list[A]:
|
|
"""Insert ``separator(i)`` between consecutive items.
|
|
|
|
Mirrors ``utils/array.ts`` ``intersperse``: the separator callable
|
|
receives the 1-based index of the item it precedes.
|
|
"""
|
|
out: list[A] = []
|
|
for i, item in enumerate(items):
|
|
if i:
|
|
out.append(separator(i))
|
|
out.append(item)
|
|
return out
|
|
|
|
|
|
def count(items: Iterable[T], predicate: Callable[[T], object]) -> int:
|
|
"""Count items where ``predicate(item)`` is truthy."""
|
|
return sum(1 for x in items if predicate(x))
|
|
|
|
|
|
def uniq(items: Iterable[T]) -> list[T]:
|
|
"""Return unique items preserving first-seen order.
|
|
|
|
Note: upstream JS uses ``[...new Set(xs)]`` which preserves insertion
|
|
order for primitive values; this matches that behavior.
|
|
"""
|
|
seen: set[T] = set()
|
|
out: list[T] = []
|
|
for item in items:
|
|
if item not in seen:
|
|
seen.add(item)
|
|
out.append(item)
|
|
return out
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# objectGroupBy.ts
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def object_group_by(
|
|
items: Iterable[T],
|
|
key_selector: Callable[[T, int], K],
|
|
) -> dict[K, list[T]]:
|
|
"""Group items by ``key_selector(item, index)``.
|
|
|
|
Mirrors ``Object.groupBy`` semantics from the TC39 proposal.
|
|
"""
|
|
out: dict[K, list[T]] = {}
|
|
for index, item in enumerate(items):
|
|
key = key_selector(item, index)
|
|
bucket = out.get(key)
|
|
if bucket is None:
|
|
bucket = []
|
|
out[key] = bucket
|
|
bucket.append(item)
|
|
return out
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# set.ts
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def difference(a: set[T], b: set[T]) -> set[T]:
|
|
"""Items in ``a`` but not ``b``."""
|
|
return {item for item in a if item not in b}
|
|
|
|
|
|
def intersects(a: set[T], b: set[T]) -> bool:
|
|
"""Whether ``a`` and ``b`` share at least one element."""
|
|
if not a or not b:
|
|
return False
|
|
return any(item in b for item in a)
|
|
|
|
|
|
def every_in(a: set[T], b: set[T]) -> bool:
|
|
"""Whether every element of ``a`` is in ``b`` (renamed from ``every``)."""
|
|
return all(item in b for item in a)
|
|
|
|
|
|
def union(a: set[T], b: set[T]) -> set[T]:
|
|
"""Set union."""
|
|
return a | b
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# xml.ts
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def escape_xml(value: str) -> str:
|
|
"""Escape ``& < >`` for safe interpolation between XML/HTML tags."""
|
|
return value.replace('&', '&').replace('<', '<').replace('>', '>')
|
|
|
|
|
|
def escape_xml_attr(value: str) -> str:
|
|
"""Escape ``& < > " '`` for safe interpolation into an attribute value."""
|
|
return escape_xml(value).replace('"', '"').replace("'", ''')
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# uuid.ts
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_UUID_RE = re.compile(
|
|
r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$',
|
|
re.IGNORECASE,
|
|
)
|
|
|
|
|
|
def validate_uuid(maybe_uuid: object) -> str | None:
|
|
"""Return the input as a UUID string if it matches the canonical format."""
|
|
if not isinstance(maybe_uuid, str):
|
|
return None
|
|
return maybe_uuid if _UUID_RE.match(maybe_uuid) else None
|
|
|
|
|
|
def create_agent_id(label: str | None = None) -> str:
|
|
"""Generate an agent ID with the upstream ``a{label-}{hex16}`` format."""
|
|
suffix = secrets.token_hex(8)
|
|
return f'a{label}-{suffix}' if label else f'a{suffix}'
|
|
|
|
|
|
__all__ = [
|
|
'intersperse',
|
|
'count',
|
|
'uniq',
|
|
'object_group_by',
|
|
'difference',
|
|
'intersects',
|
|
'every_in',
|
|
'union',
|
|
'escape_xml',
|
|
'escape_xml_attr',
|
|
'validate_uuid',
|
|
'create_agent_id',
|
|
]
|