Source code for loom.core.authz._scope

"""Where a grant applies: a path of segments that covers every path below it."""

from __future__ import annotations

from collections.abc import Iterable
from dataclasses import dataclass

_SEPARATOR = "/"


def _segments_of(path: Iterable[str]) -> tuple[str, ...]:
    if isinstance(path, str):
        raise TypeError("A scope path is a sequence of segments, not a string; use Scope.parse().")
    return tuple(map(_segment, path))


def _segment(value: object) -> str:
    if not isinstance(value, str):
        raise TypeError(f"A scope segment must be a string: {value!r}.")
    if not value or _SEPARATOR in value:
        raise ValueError(
            f"A scope segment must be non-empty and free of {_SEPARATOR!r}: {value!r}."
        )
    return value


[docs] @dataclass(frozen=True, slots=True, order=True, init=False) class Scope: """A node in the product's resource hierarchy. loom gives the segments no meaning: the product decides what each level is. A scope covers itself and everything below it, so a grant on ``Scope.of("acme")`` reaches ``Scope.of("acme", "sales", "orders")`` but never ``Scope.of("globex")`` nor the root. ``str(scope)`` and :meth:`parse` are inverse and stable, so the text form is safe to store. Attributes: path: Segments from the root down; empty for the root. Example:: workspace = Scope.of("acme") table = workspace.child("sales", "orders") assert workspace.covers(table) assert Scope.parse(str(table)) == table """ path: tuple[str, ...] def __init__(self, path: Iterable[str]) -> None: """Create the scope at *path*. Args: path: Segments from the root down; empty for the root. Raises: TypeError: When *path* is a single string or holds a non-string. ValueError: When a segment is empty or contains ``/``. """ object.__setattr__(self, "path", _segments_of(path))
[docs] @classmethod def root(cls) -> Scope: """Return the scope that covers every other scope. Returns: The root scope. """ return cls(())
[docs] @classmethod def of(cls, *segments: str) -> Scope: """Build the scope at *segments* below the root. Args: *segments: One or more path segments, outermost first. Returns: The scope at that path. Raises: ValueError: When no segment is given (use :meth:`root`) or a segment is empty or contains ``/``. """ if not segments: raise ValueError( "Scope.of() needs at least one segment; use Scope.root() for the root." ) return cls(segments)
[docs] @classmethod def parse(cls, text: str) -> Scope: """Read a scope back from its ``str()`` form. Args: text: ``/`` for the root, or ``/a/b``. Returns: The scope *text* renders. Raises: ValueError: When *text* does not start with ``/``, ends with one (other than the root) or holds an empty segment. """ if not text.startswith(_SEPARATOR): raise ValueError(f"A scope text starts with {_SEPARATOR!r}: {text!r}.") if text == _SEPARATOR: return cls.root() return cls(text[1:].split(_SEPARATOR))
[docs] def child(self, *segments: str) -> Scope: """Return the scope at *segments* below this one. Args: *segments: Path segments to append, outermost first. Returns: The nested scope. Raises: ValueError: When a segment is empty or contains ``/``. """ return Scope((*self.path, *segments))
[docs] def covers(self, other: Scope) -> bool: """Report whether a grant on this scope reaches *other*. Args: other: The scope being accessed. Returns: ``True`` when *other* is this scope or lies below it. """ return other.path[: len(self.path)] == self.path
def __str__(self) -> str: """Render the scope as ``/`` or ``/a/b``; :meth:`parse` reads it back.""" return _SEPARATOR + _SEPARATOR.join(self.path)