Skip to content

kumiki.ticket

Flat import path

kumiki/__init__.py re-exports everything on this page via from kumiki import *, so every name below is also available directly as kumiki.AccessoryTicket -- you do not need to import from the submodule path shown in the heading above.

kumiki.ticket

Ticket system for hierarchical naming and metadata.

Tickets are immutable labels that can be attached to timbers, joints, accessories, and feature concepts. The path field encodes hierarchy using '/' as a separator, e.g. "posts/frontleft" or "door/boards/1". Folders are implicit in the path and will be rendered as actual folders in the layer view.

AccessoryTicket dataclass

AccessoryTicket(path: str = UNNAMED_TICKET_PATH)

Bases: Ticket

Ticket metadata for accessories (pegs, wedges, hardware, etc.).

BoardTicket dataclass

BoardTicket(path: str = UNNAMED_TICKET_PATH, material: Optional[str] = None, reference_faces: Optional[tuple[str, ...]] = None, tags: tuple[TimberTag, ...] = ())

Bases: TimberTicket

Ticket metadata for board-like members.

GenericTag dataclass

GenericTag(name: str)

Bases: TimberTag

User-space label. Bare strings become these.

JointTicket dataclass

JointTicket(path: str = UNNAMED_TICKET_PATH, joint_type: Optional[str] = None, tags: tuple = ())

Bases: Ticket

Concept ticket metadata for joints.

joint_type class-attribute instance-attribute

joint_type: Optional[str] = None

tags class-attribute instance-attribute

tags: tuple = ()

__post_init__

__post_init__() -> None
Source code in kumiki/ticket.py
def __post_init__(self) -> None:
    if self.tags:
        warnings.warn(
            f"JointTicket({self.path!r}) was given tags {tuple(self.tags)!r}. Joints do "
            "not carry tags yet, so these are discarded. Move them to the timbers the "
            "joint is cut on, or drop them."
        )
        object.__setattr__(self, "tags", ())

Member

Bases: str, Enum

The structural role a timber plays in a frame.

Roles form a tree (see _MEMBER_PARENT): a summer beam is a beam, and a beam is horizontal. A timber wears at most one of these; ask about the broader roles with is_a rather than tagging them as well.

Closed on purpose: the drawing system will key default marking instructions off these names, and a role with no instructions of its own can fall back to its parent's. Adding a role is a line here and a line in _MEMBER_PARENT; a label that is not a structural role belongs in a GenericTag.

VERTICAL class-attribute instance-attribute

VERTICAL = 'vertical'

POST class-attribute instance-attribute

POST = 'post'

CORNER_POST class-attribute instance-attribute

CORNER_POST = 'corner_post'

QUEEN_POST class-attribute instance-attribute

QUEEN_POST = 'queen_post'

KING_POST class-attribute instance-attribute

KING_POST = 'king_post'

STUD class-attribute instance-attribute

STUD = 'stud'

HORIZONTAL class-attribute instance-attribute

HORIZONTAL = 'horizontal'

SILL class-attribute instance-attribute

SILL = 'sill'

MUDSILL class-attribute instance-attribute

MUDSILL = 'mudsill'

JOIST class-attribute instance-attribute

JOIST = 'joist'

RIM_JOIST class-attribute instance-attribute

RIM_JOIST = 'rim_joist'

FLOOR_JOIST class-attribute instance-attribute

FLOOR_JOIST = 'floor_joist'

GIRDER class-attribute instance-attribute

GIRDER = 'girder'

BEAM class-attribute instance-attribute

BEAM = 'beam'

SUMMER_BEAM class-attribute instance-attribute

SUMMER_BEAM = 'summer_beam'

TIE_BEAM class-attribute instance-attribute

TIE_BEAM = 'tie_beam'

RIDGE_BEAM class-attribute instance-attribute

RIDGE_BEAM = 'ridge_beam'

PLATE class-attribute instance-attribute

PLATE = 'plate'

TOP_PLATE class-attribute instance-attribute

TOP_PLATE = 'top_plate'

COLLAR_TIE class-attribute instance-attribute

COLLAR_TIE = 'collar_tie'

GIRT class-attribute instance-attribute

GIRT = 'girt'

PURLIN class-attribute instance-attribute

PURLIN = 'purlin'

RAFTER class-attribute instance-attribute

RAFTER = 'rafter'

PRINCIPAL_RAFTER class-attribute instance-attribute

PRINCIPAL_RAFTER = 'principal_rafter'

COMMON_RAFTER class-attribute instance-attribute

COMMON_RAFTER = 'common_rafter'

BRACE class-attribute instance-attribute

BRACE = 'brace'

KNEE_BRACE class-attribute instance-attribute

KNEE_BRACE = 'knee_brace'

is_a

is_a(other: Union[Member, str]) -> bool

True if this role is other or a kind of it. POST.is_a(POST) is True.

Source code in kumiki/ticket.py
def is_a(self, other: Union["Member", str]) -> bool:
    """True if this role is *other* or a kind of it. POST.is_a(POST) is True."""
    target = Member(other)
    current: Optional["Member"] = self
    while current is not None:
        if current is target:
            return True
        current = _MEMBER_PARENT[current]
    return False

MemberTag dataclass

MemberTag(name: str)

Bases: TimberTag

Names this timber's structural role. The name must be a Member value.

member property

member: Member

__post_init__

__post_init__() -> None
Source code in kumiki/ticket.py
def __post_init__(self) -> None:
    try:
        member = Member(self.name)
    except ValueError:
        allowed = ", ".join(m.value for m in Member)
        raise ValueError(
            f"MemberTag name must be one of the Member values, got {self.name!r}. "
            f"Allowed: {allowed}"
        ) from None
    # Member is a str enum, so store the plain value and keep MemberTag
    # equality the same whether it was built from Member.POST or "post".
    object.__setattr__(self, "name", member.value)

is_a

is_a(other: Union[Member, str]) -> bool

True if this tag's role is other or a kind of it.

Source code in kumiki/ticket.py
def is_a(self, other: Union[Member, str]) -> bool:
    """True if this tag's role is *other* or a kind of it."""
    return self.member.is_a(other)

SliceTag dataclass

SliceTag(name: str)

Bases: TimberTag

Names a slice section this timber belongs to.

Ticket dataclass

Ticket(path: str = UNNAMED_TICKET_PATH)

Bases: ABC

Base ticket shared by all ticket categories.

The category is represented by the concrete subclass rather than an enum field.

hierarchical identifier using '/' as separator.

e.g. "posts/frontleft", "door/boards/1" The last segment is the display name; preceding segments are folder names.

path class-attribute instance-attribute

path: str = UNNAMED_TICKET_PATH

kumiki_id class-attribute instance-attribute

kumiki_id: KumikiId = field(default_factory=_next_kumiki_id, init=False, compare=False, repr=False)

get_name

get_name() -> str

Return the display name: the last segment of the path.

Source code in kumiki/ticket.py
def get_name(self) -> str:
    """Return the display name: the last segment of the path."""
    return self.path.rsplit("/", 1)[-1]

TimberTag dataclass

TimberTag(name: str)

Bases: ABC

A label on a timber ticket.

The kind is the concrete subclass. A tag carries nothing but its name; anything shared between timbers wearing the same tag is looked up by that name elsewhere.

name instance-attribute

name: str

TimberTicket dataclass

TimberTicket(path: str = UNNAMED_TICKET_PATH, material: Optional[str] = None, reference_faces: Optional[tuple[str, ...]] = None, tags: tuple[TimberTag, ...] = ())

Bases: Ticket

Ticket metadata for physical timber members.

material class-attribute instance-attribute

material: Optional[str] = None

reference_faces class-attribute instance-attribute

reference_faces: Optional[tuple[str, ...]] = None

tags class-attribute instance-attribute

tags: tuple[TimberTag, ...] = ()

__post_init__

__post_init__() -> None
Source code in kumiki/ticket.py
def __post_init__(self) -> None:
    object.__setattr__(self, "tags", normalize_timber_tags(self.tags))

with_tags

with_tags(*tags: Union[TimberTag, str]) -> Self

Return a copy of this ticket carrying these tags as well as its own.

Source code in kumiki/ticket.py
def with_tags(self, *tags: Union[TimberTag, str]) -> Self:
    """Return a copy of this ticket carrying these tags as well as its own."""
    return self._replace_tags((*self.tags, *tags))

with_member

with_member(member: Union[Member, str]) -> Self

Return a copy in this member role, replacing whatever role it had.

with_tags cannot do this: a second role is an error, not an addition.

Source code in kumiki/ticket.py
def with_member(self, member: Union[Member, str]) -> Self:
    """Return a copy in this member role, replacing whatever role it had.

    with_tags cannot do this: a second role is an error, not an addition.
    """
    kept = tuple(tag for tag in self.tags if not isinstance(tag, MemberTag))
    return self._replace_tags((*kept, MemberTag(member)))

as_timber_tag

as_timber_tag(value: Union[TimberTag, str]) -> TimberTag

Convert a tag parameter to a TimberTag, a bare string becoming a GenericTag.

Source code in kumiki/ticket.py
def as_timber_tag(value: Union[TimberTag, str]) -> TimberTag:
    """Convert a tag parameter to a TimberTag, a bare string becoming a GenericTag."""
    if isinstance(value, TimberTag):
        return value
    if isinstance(value, str):
        return GenericTag(value)
    raise TypeError(f"Expected a TimberTag or a str, got {type(value).__name__}")

normalize_timber_tags

normalize_timber_tags(tags: Iterable[Union[TimberTag, str]]) -> tuple[TimberTag, ...]

Coerce, strip, drop empty names, dedupe on (kind, name), and sort for a stable order.

Source code in kumiki/ticket.py
def normalize_timber_tags(tags: Iterable[Union[TimberTag, str]]) -> tuple[TimberTag, ...]:
    """Coerce, strip, drop empty names, dedupe on (kind, name), and sort for a stable order."""
    normalized: list[TimberTag] = []
    seen: set[tuple[type, str]] = set()
    for tag in tags:
        coerced = as_timber_tag(tag)
        stripped = coerced.name.strip()
        if not stripped:
            continue
        if stripped != coerced.name:
            coerced = replace(coerced, name=stripped)
        key = (type(coerced), coerced.name)
        if key in seen:
            continue
        seen.add(key)
        normalized.append(coerced)
    normalized.sort(key=lambda tag: (type(tag).__name__, tag.name))

    # A timber is one member. Two roles at once would leave the drawing system
    # with two sets of default marking instructions and no way to choose; a
    # broader role is a question for Member.is_a, not a second tag.
    members = [tag.name for tag in normalized if isinstance(tag, MemberTag)]
    if len(members) > 1:
        raise ValueError(f"A timber has one member role, got {members}")

    return tuple(normalized)