Source code for rics.cardinality._enum

from enum import Enum
from typing import Tuple, Union

# CardinalityLiteral = Literal["1:1", "1:N", "N:1", "M:N"]
CardinalityT = Union[str, "Cardinality"]


[docs]class Cardinality(Enum): """Enumeration type for cardinality relationships. Cardinalities are comparable using numerical operators, and can be thought of as comparing "preciseness". The less ambiguity there is for a given cardinality, the smaller it is in comparison to the others. The hierarchy is given by ``1:1 < 1:N = N:1 < M:N``. Note that ``1:N`` and ``N:1`` are considered equally precise. Examples: Comparing cardinalities >>> from rics.cardinality import Cardinality >>> Cardinality.ManyToOne <Cardinality.ManyToOne: 'N:1'> >>> Cardinality.OneToOne <Cardinality.OneToOne: '1:1'> >>> Cardinality.ManyToOne < Cardinality.OneToOne False """ OneToOne = "1:1" OneToMany = "1:N" ManyToOne = "N:1" ManyToMany = "M:N" @property def many_left(self) -> bool: """Many-relationship on the right, True for ``N:1`` and ``M:N``.""" return self == Cardinality.ManyToMany or self == Cardinality.ManyToOne # pragma: no cover @property def many_right(self) -> bool: """Many-relationship on the right, True for ``1:N`` and ``M:N``.""" return self == Cardinality.ManyToMany or self == Cardinality.OneToMany # pragma: no cover @property def one_left(self) -> bool: """One-relationship on the right, True for ``1:1`` and ``1:N``.""" return not self.many_left # pragma: no cover @property def one_right(self) -> bool: """One-relationship on the right, True for ``1:1`` and ``N:1``.""" return not self.many_right # pragma: no cover @property def inverse(self) -> "Cardinality": """Inverse cardinality. For symmetric cardinalities, ``self.inverse == self``. Returns: Inverse cardinality. See Also: :attr:`symmetric` """ if self == Cardinality.OneToMany: return Cardinality.ManyToOne if self == Cardinality.ManyToOne: return Cardinality.OneToMany return self @property def symmetric(self) -> bool: """Symmetry flag. For symmetric cardinalities, ``self.inverse == self``. Returns: Symmetry flag. See Also: :attr:`inverse` """ return self == Cardinality.OneToOne or self == Cardinality.ManyToMany def __ge__(self, other: "Cardinality") -> bool: """Equivalent to :meth:`set.issuperset`.""" return _is_superset(self, other) def __lt__(self, other: "Cardinality") -> bool: return not self >= other
[docs] @classmethod def from_counts(cls, left_count: int, right_count: int) -> "Cardinality": """Derive a `Cardinality` from counts. Args: left_count: Number of elements on the left-hand side. right_count: Number of elements on the right-hand side. Returns: A :class:`Cardinality`. Raises: ValueError: For counts < 1. """ return _from_counts(left_count, right_count)
[docs] @classmethod def parse(cls, arg: CardinalityT, strict: bool = False) -> "Cardinality": """Convert to cardinality. Args: arg: Argument to parse. strict: If True, `arg` must match exactly when it is given as a string. Returns: A :class:`Cardinality`. Raises: ValueError: If the argument could not be converted. """ return arg if isinstance(arg, Cardinality) else _from_generous_string(arg, strict)
######################################################################################################################## # Supporting functions # # Would rather have this in a "friend module", but that's not practical (before 3.10?) ######################################################################################################################## def _parsing_failure_message(arg: str, strict: bool) -> str: options = tuple([c.value for c in Cardinality]) alternatively = tuple([c.name for c in Cardinality]) strict_hint = "." if strict: try: strict = False Cardinality.parse(arg, strict=strict) strict_hint = f". Hint: set {strict=} to allow this input." except ValueError: pass return f"Could not convert {arg=} to Cardinality{strict_hint} Correct input {options=} or {repr(alternatively)}" _MATRIX = ( (Cardinality.ManyToMany, Cardinality.ManyToOne), (Cardinality.OneToMany, Cardinality.OneToOne), ) def _is_superset(c0: Cardinality, c1: Cardinality) -> bool: if c0 == c1: return True c0_i, c0_j = _pos(c0) c1_i, c1_j = _pos(c1) return c0_i <= c1_i and c0_j <= c1_j def _pos(cardinality: Cardinality) -> Tuple[int, int]: for i in range(2): for j in range(2): if _MATRIX[i][j] == cardinality: return i, j raise AssertionError("This should be impossible.") def _from_counts(left_count: int, right_count: int) -> Cardinality: if left_count < 1: raise ValueError(f"{left_count=} < 1") if right_count < 1: raise ValueError(f"{right_count=} < 1") one_left = left_count == 1 one_right = right_count == 1 return _MATRIX[int(one_left)][int(one_right)] def _from_generous_string(s: str, strict: bool) -> Cardinality: if not strict: s = s.strip().upper().replace("-", ":", 1).replace("*", "N", 2) if s == "N:N": s = "M:N" for c in Cardinality: if c.value == s: return c raise ValueError(_parsing_failure_message(s, strict))