Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions elementary/messages/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# Elementary Messages

This package provides a flexible system for building structured messages that can be rendered across different platforms (like Slack, Teams, etc.).
The system defines a minimal and simple format that can be formatted into different output formats.However, some blocks, styles, or icons might not be supported across all platforms.

## Core Components

- `MessageBody`: The root container for a message
- `blocks`: Building blocks for message content (headers, code blocks, text, etc.)
- `block_builders`: Helper functions to easily construct common block patterns

## Message Structure

A `MessageBody` can contain the following blocks directly:

- `HeaderBlock`
- `CodeBlock`
- `DividerBlock`
- `LinesBlock`
- `FactListBlock`
- `ExpandableBlock`

## Basic Usage

```python
from elementary.messages.message_body import MessageBody, Color
from elementary.messages.blocks import HeaderBlock, LinesBlock, TextBlock
from elementary.messages.block_builders import TextLineBlock, BoldTextLineBlock

# Create a simple message
message = MessageBody(
blocks=[
HeaderBlock(text="My Message Title"),
TextLineBlock(text="This is a simple text line"),
BoldTextLineBlock(text="This is bold text"),
],
color=Color.GREEN
)
```

## Available Blocks

### Text and Layout

- `HeaderBlock`: Section headers
- `TextBlock`: Basic text content with optional style (BOLD/ITALIC)
- `LineBlock`: A line that can contain multiple inline elements:
- `TextBlock`: Text with optional styling
- `LinkBlock`: Clickable URL with display text
- `IconBlock`: Visual indicator (like ✓, ⚠️, etc.)
- `LinesBlock`: Multiple lines of text
- `DividerBlock`: Visual separator
- `CodeBlock`: Code snippets or formatted text

### Interactive Elements

- `ExpandableBlock`: Collapsible sections
- `LinkBlock`: Clickable URLs

### Data Display

- `FactBlock`: Key-value pairs
- `FactListBlock`: Collection of facts

### Styling

- `TextStyle`: Text formatting (BOLD, ITALIC)
- `Icon`: Various icons for visual indicators
- `Color`: Message theme colors (RED, YELLOW, GREEN)

## Block Builders

The `block_builders` module provides convenient functions for creating common block patterns. These builders are wrappers around the basic blocks defined in the `blocks` module and help with constructing commonly used block combinations:

```python
from elementary.messages.block_builders import (
BulletListBlock,
FactsBlock,
JsonCodeBlock,
TitledParagraphBlock
)
from elementary.messages.blocks import Icon

# Create a bullet list
bullet_list = BulletListBlock(
icon=Icon.CHECK,
lines=[TextLineBlock(text="Item 1"), TextLineBlock(text="Item 2")]
)

# Create facts list
facts = FactsBlock(
facts=[
("Status", "Passed"),
("Duration", "2.5s"),
]
)

# Create JSON code block
json_block = JsonCodeBlock(
content={"key": "value"}
)

# Create titled paragraph
paragraph = TitledParagraphBlock(
title="Section Title",
lines=[TextLineBlock(text="Paragraph content")]
)

# Example of a line with multiple inline elements
from elementary.messages.blocks import LineBlock, TextBlock, LinkBlock, IconBlock, Icon, TextStyle

complex_line = LineBlock(
inlines=[
IconBlock(icon=Icon.CHECK),
TextBlock(text="Test passed - ", style=TextStyle.BOLD),
TextBlock(text="View details at "),
LinkBlock(text="dashboard", url="https://example.com"),
]
)
```

## Message Formatting

To format messages into different output formats (like Slack or Teams), a formatter needs to support all the basic blocks defined in the `blocks` module:

- Core blocks: `HeaderBlock`, `CodeBlock`, `DividerBlock`, `LinesBlock`, `FactListBlock`, `ExpandableBlock`
- Inline blocks: `TextBlock`, `LinkBlock`, `IconBlock`
- Styling: `TextStyle`, `Color`

The block builders are convenience wrappers that ultimately create these basic blocks, so formatters only need to handle the core block types.
Empty file added elementary/messages/__init__.py
Empty file.
122 changes: 122 additions & 0 deletions elementary/messages/block_builders.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import json
from typing import List, Optional, Tuple, Union

from .blocks import (
CodeBlock,
FactBlock,
FactListBlock,
Icon,
IconBlock,
InlineBlock,
LineBlock,
LinesBlock,
LinkBlock,
TextBlock,
TextStyle,
)

SimpleInlineBlock = Union[str, Icon]
SimpleLineBlock = Union[str, Icon, List[SimpleInlineBlock]]


def _build_inline_block(
content: SimpleInlineBlock, style: Optional[TextStyle] = None
) -> InlineBlock:
return IconBlock(icon=content) if isinstance(content, Icon) else TextBlock(text=content, style=style) # type: ignore


def _build_inlines(
content: SimpleLineBlock, style: Optional[TextStyle] = None
) -> List[InlineBlock]:
if isinstance(content, list):
return [_build_inline_block(line, style) for line in content]
return [_build_inline_block(content)]


def BulletListBlock(
*,
icon: Union[Icon, str],
lines: List[LineBlock] = [],
) -> LinesBlock:
icon_inline: InlineBlock = (
IconBlock(icon=icon) if isinstance(icon, Icon) else TextBlock(text=icon)
)
lines = [LineBlock(inlines=[icon_inline] + line.inlines) for line in lines]
return LinesBlock(lines=lines)


def BoldTextBlock(*, text: str) -> TextBlock:
return TextBlock(text=text, style=TextStyle.BOLD)


def ItalicTextBlock(*, text: str) -> TextBlock:
return TextBlock(text=text, style=TextStyle.ITALIC)


def TextLineBlock(
*, text: SimpleLineBlock, style: Optional[TextStyle] = None
) -> LineBlock:
return LineBlock(inlines=_build_inlines(text, style))


def BoldTextLineBlock(*, text: SimpleLineBlock) -> LineBlock:
return TextLineBlock(text=text, style=TextStyle.BOLD)


def ItalicTextLineBlock(*, text: SimpleLineBlock) -> LineBlock:
return TextLineBlock(text=text, style=TextStyle.ITALIC)


def LinkLineBlock(*, text: str, url: str) -> LineBlock:
return LineBlock(inlines=[LinkBlock(text=text, url=url)])


def SummaryLineBlock(
*,
summary: List[Tuple[SimpleLineBlock, SimpleLineBlock]],
include_empty_values: bool = False,
) -> LineBlock:
text_blocks: List[InlineBlock] = []
for title, value in summary:
if not value and not include_empty_values:
continue
text_blocks.extend(_build_inlines(title, TextStyle.BOLD))
text_blocks.extend(_build_inlines(value))
text_blocks.append(TextBlock(text="|"))
text_blocks = text_blocks[:-1]
return LineBlock(inlines=text_blocks)


def FactsBlock(
*,
facts: List[
Tuple[
SimpleLineBlock,
SimpleLineBlock,
]
],
include_empty_values: bool = False,
) -> FactListBlock:
return FactListBlock(
facts=[
FactBlock(
title=LineBlock(inlines=_build_inlines(title)),
value=LineBlock(inlines=_build_inlines(value)),
)
for title, value in facts
if value or include_empty_values
]
)


def TitledParagraphBlock(
*,
title: SimpleLineBlock,
lines: List[LineBlock],
) -> LinesBlock:
title_line = LineBlock(inlines=_build_inlines(title, TextStyle.BOLD))
return LinesBlock(lines=[title_line] + lines)


def JsonCodeBlock(*, content: Union[str, dict, list], indent: int = 2) -> CodeBlock:
return CodeBlock(text=json.dumps(content, indent=indent))
101 changes: 101 additions & 0 deletions elementary/messages/blocks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
from enum import Enum
from typing import List, Optional, Union

from pydantic import BaseModel


class Icon(Enum):
RED_TRIANGLE = "red_triangle"
X = "x"
WARNING = "warning"
EXCLAMATION = "exclamation"
CHECK = "check"
MAGNIFYING_GLASS = "magnifying_glass"
HAMMER_AND_WRENCH = "hammer_and_wrench"
POLICE_LIGHT = "police_light"
INFO = "info"
EYE = "eye"
GEAR = "gear"
BELL = "bell"


class TextStyle(Enum):
BOLD = "bold"
ITALIC = "italic"


class BaseBlock(BaseModel):
pass


class BaseInlineTextBlock(BaseBlock):
pass


class TextBlock(BaseInlineTextBlock):
text: str
style: Optional[TextStyle] = None


class LinkBlock(BaseInlineTextBlock):
text: str
url: str


class IconBlock(BaseInlineTextBlock):
icon: Icon


InlineBlock = Union[TextBlock, LinkBlock, IconBlock]


class HeaderBlock(BaseBlock):
text: str


class CodeBlock(BaseBlock):
text: str


class DividerBlock(BaseBlock):
pass


class LineBlock(BaseBlock):
inlines: List[InlineBlock]
sep: str = " "


class BaseLinesBlock(BaseBlock):
lines: List[LineBlock]


class LinesBlock(BaseLinesBlock):
pass


class FactBlock(BaseBlock):
title: LineBlock
value: LineBlock


class FactListBlock(BaseBlock):
facts: List[FactBlock]


class ExpandableBlock(BaseBlock):
title: str
body: List["InExpandableBlock"]
expanded: bool = False


InExpandableBlock = Union[
HeaderBlock,
CodeBlock,
DividerBlock,
LinesBlock,
FactListBlock,
"ExpandableBlock",
]

ExpandableBlock.model_rebuild()
37 changes: 37 additions & 0 deletions elementary/messages/message_body.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from enum import Enum
from typing import List, Optional, Union

from pydantic import BaseModel

from elementary.messages.blocks import (
CodeBlock,
DividerBlock,
ExpandableBlock,
FactListBlock,
HeaderBlock,
LinesBlock,
)


class Color(Enum):
RED = "red"
YELLOW = "yellow"
GREEN = "green"


MessageBlock = Union[
HeaderBlock,
CodeBlock,
DividerBlock,
LinesBlock,
FactListBlock,
ExpandableBlock,
]


class MessageBody(BaseModel):
blocks: List[MessageBlock]
color: Optional[Color] = None


MessageBody.model_rebuild()
Loading