Source code for cadrumo.core.text_bounds

"""The bounded scalar shapes that recur across every layer, stated once.

Most of this campaign's aliases name a DOMAIN concept -- a filing year, a share
of one, a country code -- and carry its bound as a consequence. These are
different: they name a SHAPE. ``NonEmptyStr`` says only that a string carries
something, and ``PositiveCount`` only that a count is at least one.

That is a weaker kind of canonicality and worth being honest about. The value
is not that the name teaches a reader something new; it is that the shape is
written down once instead of a hundred and eight times, so a payload cannot
quietly disagree with the model it projects about whether empty is allowed.
Where a real domain concept exists, its own alias is the better home and these
should not be reached for -- an evidence reference is
:obj:`EvidenceReference`, not a bounded string that happens to be non-empty.

``PositiveCount`` exists because pydantic's ``PositiveInt`` is not a drop-in
substitute at these sites. It is ``Gt(0)`` where these are ``Ge(1)``; for an
integer those admit the same values, but they serialise to different JSON
schemas -- ``exclusiveMinimum: 0`` against ``minimum: 1`` -- and these bounds
sit on a published envelope contract. ``NonNegativeInt`` has no such problem
and is used directly; only the positive case needs a local name.
"""

from __future__ import annotations

from decimal import Decimal
from typing import Annotated, Final

from pydantic import Field, StringConstraints

NonEmptyStr = Annotated[str, StringConstraints(min_length=1)]
"""A string that carries something. The weakest useful claim about text."""

PositiveCount = Annotated[int, Field(ge=1)]
"""A count of at least one, keeping the inclusive bound the wire contract shows."""

NonNegativeDecimal = Annotated[Decimal, Field(ge=Decimal("0"))]
"""A decimal quantity that cannot go below zero."""

PositiveDecimal = Annotated[Decimal, Field(gt=Decimal("0"))]
"""A decimal quantity that must be strictly above zero.

Separate from :obj:`NonNegativeDecimal` because the difference is the whole
point at the sites that need it: an exchange rate of zero is not a rate,
while a total of zero is a legitimate total.
"""

#: The first month AEAT numbers.
CALENDAR_MONTH_MIN: Final[int] = 1

#: The last month AEAT numbers.
CALENDAR_MONTH_MAX: Final[int] = 12

CalendarMonth = Annotated[int, Field(ge=CALENDAR_MONTH_MIN, le=CALENDAR_MONTH_MAX)]
"""A month of the year, numbered as AEAT numbers them."""


[docs] def is_calendar_month(value: int) -> bool: """Whether ``value`` names a month, inclusive at both ends. For the callers that cannot use the annotation because they must raise their OWN refusal: the profile answer reader raises a ``ProfileAnswerTypeError`` naming the answer key, the descendant record a ``ProfileValidationError``, the setup wizard returns a failed verdict carrying a locale key, and the CLI payload a plain ``ValueError`` pydantic wraps. Those differ on purpose -- the operator's first instructive surface should name the field they typed into -- so what is shared is the question, not the answer. The bound was open-coded as ``1 <= month <= 12`` at five sites, twice within a single module for the SAME field, which is how a bound stops being one rule and becomes five that happen to agree. """ return CALENDAR_MONTH_MIN <= value <= CALENDAR_MONTH_MAX
[docs] def is_canonical_month_set(months: tuple[int, ...]) -> bool: """Whether ``months`` is a canonical set: real months, no repeat, ascending. The three rules travel together and were written out twice -- once on the descendant record and once on the wire payload that projects it. Only the first of the three delegated; uniqueness and ordering were restated. Ascending order is ENFORCED rather than applied, so a record has exactly one representation of a given set and a save-then-reload cannot reorder it. A repeat is refused rather than collapsed, because a month either qualified or it did not and silently dropping the second mention would hide a transcription slip while changing nothing the operator can see. A predicate rather than a validator because the two callers raise different errors on purpose -- the record a ``ProfileValidationError`` naming the repeated months, the payload a plain ``ValueError`` pydantic wraps -- and the detail each builds is a message, not a rule. """ return ( all(is_calendar_month(month) for month in months) and len(set(months)) == len(months) and list(months) == sorted(months) )
type NonEmptyList[T] = Annotated[list[T], Field(min_length=1)] """A list that carries at least one element. The container is part of a payload's wire shape, so this keeps the list rather than quietly promoting it to a tuple: the strict models refuse the coercion, and a projection has no business changing what a caller may hand it. """ __all__ = [ "CALENDAR_MONTH_MAX", "CALENDAR_MONTH_MIN", "CalendarMonth", "NonEmptyList", "NonEmptyStr", "NonNegativeDecimal", "PositiveCount", "PositiveDecimal", "is_calendar_month", "is_canonical_month_set", ]