Skip to content

Commit 066e712

Browse files
authored
Merge pull request #1792 from elementary-data/ele-3997-message-body-infra
Add message body blocks and structures
2 parents fb3e32d + 2d37747 commit 066e712

File tree

5 files changed

+390
-0
lines changed

5 files changed

+390
-0
lines changed

elementary/messages/README.md

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# Elementary Messages
2+
3+
This package provides a flexible system for building structured messages that can be rendered across different platforms (like Slack, Teams, etc.).
4+
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.
5+
6+
## Core Components
7+
8+
- `MessageBody`: The root container for a message
9+
- `blocks`: Building blocks for message content (headers, code blocks, text, etc.)
10+
- `block_builders`: Helper functions to easily construct common block patterns
11+
12+
## Message Structure
13+
14+
A `MessageBody` can contain the following blocks directly:
15+
16+
- `HeaderBlock`
17+
- `CodeBlock`
18+
- `DividerBlock`
19+
- `LinesBlock`
20+
- `FactListBlock`
21+
- `ExpandableBlock`
22+
23+
## Basic Usage
24+
25+
```python
26+
from elementary.messages.message_body import MessageBody, Color
27+
from elementary.messages.blocks import HeaderBlock, LinesBlock, TextBlock
28+
from elementary.messages.block_builders import TextLineBlock, BoldTextLineBlock
29+
30+
# Create a simple message
31+
message = MessageBody(
32+
blocks=[
33+
HeaderBlock(text="My Message Title"),
34+
TextLineBlock(text="This is a simple text line"),
35+
BoldTextLineBlock(text="This is bold text"),
36+
],
37+
color=Color.GREEN
38+
)
39+
```
40+
41+
## Available Blocks
42+
43+
### Text and Layout
44+
45+
- `HeaderBlock`: Section headers
46+
- `TextBlock`: Basic text content with optional style (BOLD/ITALIC)
47+
- `LineBlock`: A line that can contain multiple inline elements:
48+
- `TextBlock`: Text with optional styling
49+
- `LinkBlock`: Clickable URL with display text
50+
- `IconBlock`: Visual indicator (like ✓, ⚠️, etc.)
51+
- `LinesBlock`: Multiple lines of text
52+
- `DividerBlock`: Visual separator
53+
- `CodeBlock`: Code snippets or formatted text
54+
55+
### Interactive Elements
56+
57+
- `ExpandableBlock`: Collapsible sections
58+
- `LinkBlock`: Clickable URLs
59+
60+
### Data Display
61+
62+
- `FactBlock`: Key-value pairs
63+
- `FactListBlock`: Collection of facts
64+
65+
### Styling
66+
67+
- `TextStyle`: Text formatting (BOLD, ITALIC)
68+
- `Icon`: Various icons for visual indicators
69+
- `Color`: Message theme colors (RED, YELLOW, GREEN)
70+
71+
## Block Builders
72+
73+
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:
74+
75+
```python
76+
from elementary.messages.block_builders import (
77+
BulletListBlock,
78+
FactsBlock,
79+
JsonCodeBlock,
80+
TitledParagraphBlock
81+
)
82+
from elementary.messages.blocks import Icon
83+
84+
# Create a bullet list
85+
bullet_list = BulletListBlock(
86+
icon=Icon.CHECK,
87+
lines=[TextLineBlock(text="Item 1"), TextLineBlock(text="Item 2")]
88+
)
89+
90+
# Create facts list
91+
facts = FactsBlock(
92+
facts=[
93+
("Status", "Passed"),
94+
("Duration", "2.5s"),
95+
]
96+
)
97+
98+
# Create JSON code block
99+
json_block = JsonCodeBlock(
100+
content={"key": "value"}
101+
)
102+
103+
# Create titled paragraph
104+
paragraph = TitledParagraphBlock(
105+
title="Section Title",
106+
lines=[TextLineBlock(text="Paragraph content")]
107+
)
108+
109+
# Example of a line with multiple inline elements
110+
from elementary.messages.blocks import LineBlock, TextBlock, LinkBlock, IconBlock, Icon, TextStyle
111+
112+
complex_line = LineBlock(
113+
inlines=[
114+
IconBlock(icon=Icon.CHECK),
115+
TextBlock(text="Test passed - ", style=TextStyle.BOLD),
116+
TextBlock(text="View details at "),
117+
LinkBlock(text="dashboard", url="https://example.com"),
118+
]
119+
)
120+
```
121+
122+
## Message Formatting
123+
124+
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:
125+
126+
- Core blocks: `HeaderBlock`, `CodeBlock`, `DividerBlock`, `LinesBlock`, `FactListBlock`, `ExpandableBlock`
127+
- Inline blocks: `TextBlock`, `LinkBlock`, `IconBlock`
128+
- Styling: `TextStyle`, `Color`
129+
130+
The block builders are convenience wrappers that ultimately create these basic blocks, so formatters only need to handle the core block types.

elementary/messages/__init__.py

Whitespace-only changes.
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import json
2+
from typing import List, Optional, Tuple, Union
3+
4+
from .blocks import (
5+
CodeBlock,
6+
FactBlock,
7+
FactListBlock,
8+
Icon,
9+
IconBlock,
10+
InlineBlock,
11+
LineBlock,
12+
LinesBlock,
13+
LinkBlock,
14+
TextBlock,
15+
TextStyle,
16+
)
17+
18+
SimpleInlineBlock = Union[str, Icon]
19+
SimpleLineBlock = Union[str, Icon, List[SimpleInlineBlock]]
20+
21+
22+
def _build_inline_block(
23+
content: SimpleInlineBlock, style: Optional[TextStyle] = None
24+
) -> InlineBlock:
25+
return IconBlock(icon=content) if isinstance(content, Icon) else TextBlock(text=content, style=style) # type: ignore
26+
27+
28+
def _build_inlines(
29+
content: SimpleLineBlock, style: Optional[TextStyle] = None
30+
) -> List[InlineBlock]:
31+
if isinstance(content, list):
32+
return [_build_inline_block(line, style) for line in content]
33+
return [_build_inline_block(content)]
34+
35+
36+
def BulletListBlock(
37+
*,
38+
icon: Union[Icon, str],
39+
lines: List[LineBlock] = [],
40+
) -> LinesBlock:
41+
icon_inline: InlineBlock = (
42+
IconBlock(icon=icon) if isinstance(icon, Icon) else TextBlock(text=icon)
43+
)
44+
lines = [LineBlock(inlines=[icon_inline] + line.inlines) for line in lines]
45+
return LinesBlock(lines=lines)
46+
47+
48+
def BoldTextBlock(*, text: str) -> TextBlock:
49+
return TextBlock(text=text, style=TextStyle.BOLD)
50+
51+
52+
def ItalicTextBlock(*, text: str) -> TextBlock:
53+
return TextBlock(text=text, style=TextStyle.ITALIC)
54+
55+
56+
def TextLineBlock(
57+
*, text: SimpleLineBlock, style: Optional[TextStyle] = None
58+
) -> LineBlock:
59+
return LineBlock(inlines=_build_inlines(text, style))
60+
61+
62+
def BoldTextLineBlock(*, text: SimpleLineBlock) -> LineBlock:
63+
return TextLineBlock(text=text, style=TextStyle.BOLD)
64+
65+
66+
def ItalicTextLineBlock(*, text: SimpleLineBlock) -> LineBlock:
67+
return TextLineBlock(text=text, style=TextStyle.ITALIC)
68+
69+
70+
def LinkLineBlock(*, text: str, url: str) -> LineBlock:
71+
return LineBlock(inlines=[LinkBlock(text=text, url=url)])
72+
73+
74+
def SummaryLineBlock(
75+
*,
76+
summary: List[Tuple[SimpleLineBlock, SimpleLineBlock]],
77+
include_empty_values: bool = False,
78+
) -> LineBlock:
79+
text_blocks: List[InlineBlock] = []
80+
for title, value in summary:
81+
if not value and not include_empty_values:
82+
continue
83+
text_blocks.extend(_build_inlines(title, TextStyle.BOLD))
84+
text_blocks.extend(_build_inlines(value))
85+
text_blocks.append(TextBlock(text="|"))
86+
text_blocks = text_blocks[:-1]
87+
return LineBlock(inlines=text_blocks)
88+
89+
90+
def FactsBlock(
91+
*,
92+
facts: List[
93+
Tuple[
94+
SimpleLineBlock,
95+
SimpleLineBlock,
96+
]
97+
],
98+
include_empty_values: bool = False,
99+
) -> FactListBlock:
100+
return FactListBlock(
101+
facts=[
102+
FactBlock(
103+
title=LineBlock(inlines=_build_inlines(title)),
104+
value=LineBlock(inlines=_build_inlines(value)),
105+
)
106+
for title, value in facts
107+
if value or include_empty_values
108+
]
109+
)
110+
111+
112+
def TitledParagraphBlock(
113+
*,
114+
title: SimpleLineBlock,
115+
lines: List[LineBlock],
116+
) -> LinesBlock:
117+
title_line = LineBlock(inlines=_build_inlines(title, TextStyle.BOLD))
118+
return LinesBlock(lines=[title_line] + lines)
119+
120+
121+
def JsonCodeBlock(*, content: Union[str, dict, list], indent: int = 2) -> CodeBlock:
122+
return CodeBlock(text=json.dumps(content, indent=indent))

elementary/messages/blocks.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
from enum import Enum
2+
from typing import List, Optional, Union
3+
4+
from pydantic import BaseModel
5+
6+
7+
class Icon(Enum):
8+
RED_TRIANGLE = "red_triangle"
9+
X = "x"
10+
WARNING = "warning"
11+
EXCLAMATION = "exclamation"
12+
CHECK = "check"
13+
MAGNIFYING_GLASS = "magnifying_glass"
14+
HAMMER_AND_WRENCH = "hammer_and_wrench"
15+
POLICE_LIGHT = "police_light"
16+
INFO = "info"
17+
EYE = "eye"
18+
GEAR = "gear"
19+
BELL = "bell"
20+
21+
22+
class TextStyle(Enum):
23+
BOLD = "bold"
24+
ITALIC = "italic"
25+
26+
27+
class BaseBlock(BaseModel):
28+
pass
29+
30+
31+
class BaseInlineTextBlock(BaseBlock):
32+
pass
33+
34+
35+
class TextBlock(BaseInlineTextBlock):
36+
text: str
37+
style: Optional[TextStyle] = None
38+
39+
40+
class LinkBlock(BaseInlineTextBlock):
41+
text: str
42+
url: str
43+
44+
45+
class IconBlock(BaseInlineTextBlock):
46+
icon: Icon
47+
48+
49+
InlineBlock = Union[TextBlock, LinkBlock, IconBlock]
50+
51+
52+
class HeaderBlock(BaseBlock):
53+
text: str
54+
55+
56+
class CodeBlock(BaseBlock):
57+
text: str
58+
59+
60+
class DividerBlock(BaseBlock):
61+
pass
62+
63+
64+
class LineBlock(BaseBlock):
65+
inlines: List[InlineBlock]
66+
sep: str = " "
67+
68+
69+
class BaseLinesBlock(BaseBlock):
70+
lines: List[LineBlock]
71+
72+
73+
class LinesBlock(BaseLinesBlock):
74+
pass
75+
76+
77+
class FactBlock(BaseBlock):
78+
title: LineBlock
79+
value: LineBlock
80+
81+
82+
class FactListBlock(BaseBlock):
83+
facts: List[FactBlock]
84+
85+
86+
class ExpandableBlock(BaseBlock):
87+
title: str
88+
body: List["InExpandableBlock"]
89+
expanded: bool = False
90+
91+
92+
InExpandableBlock = Union[
93+
HeaderBlock,
94+
CodeBlock,
95+
DividerBlock,
96+
LinesBlock,
97+
FactListBlock,
98+
"ExpandableBlock",
99+
]
100+
101+
ExpandableBlock.model_rebuild()
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from enum import Enum
2+
from typing import List, Optional, Union
3+
4+
from pydantic import BaseModel
5+
6+
from elementary.messages.blocks import (
7+
CodeBlock,
8+
DividerBlock,
9+
ExpandableBlock,
10+
FactListBlock,
11+
HeaderBlock,
12+
LinesBlock,
13+
)
14+
15+
16+
class Color(Enum):
17+
RED = "red"
18+
YELLOW = "yellow"
19+
GREEN = "green"
20+
21+
22+
MessageBlock = Union[
23+
HeaderBlock,
24+
CodeBlock,
25+
DividerBlock,
26+
LinesBlock,
27+
FactListBlock,
28+
ExpandableBlock,
29+
]
30+
31+
32+
class MessageBody(BaseModel):
33+
blocks: List[MessageBlock]
34+
color: Optional[Color] = None
35+
36+
37+
MessageBody.model_rebuild()

0 commit comments

Comments
 (0)