slackblocks 2.4.0__tar.gz → 2.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. {slackblocks-2.4.0 → slackblocks-2.6.0}/.gitignore +4 -0
  2. {slackblocks-2.4.0 → slackblocks-2.6.0}/CHANGELOG.md +63 -0
  3. {slackblocks-2.4.0 → slackblocks-2.6.0}/PKG-INFO +1 -1
  4. slackblocks-2.6.0/generator/generate_limits.py +59 -0
  5. {slackblocks-2.4.0 → slackblocks-2.6.0}/pyproject.toml +2 -2
  6. {slackblocks-2.4.0 → slackblocks-2.6.0}/slackblocks/__init__.py +1 -1
  7. slackblocks-2.6.0/slackblocks/_limits.py +287 -0
  8. {slackblocks-2.4.0 → slackblocks-2.6.0}/slackblocks/_surfaces.py +18 -0
  9. {slackblocks-2.4.0 → slackblocks-2.6.0}/slackblocks/attachments.py +9 -5
  10. {slackblocks-2.4.0 → slackblocks-2.6.0}/slackblocks/blocks.py +199 -90
  11. {slackblocks-2.4.0 → slackblocks-2.6.0}/slackblocks/elements.py +274 -98
  12. {slackblocks-2.4.0 → slackblocks-2.6.0}/slackblocks/messages.py +46 -7
  13. {slackblocks-2.4.0 → slackblocks-2.6.0}/slackblocks/objects.py +141 -32
  14. {slackblocks-2.4.0 → slackblocks-2.6.0}/slackblocks/rich_text/objects.py +41 -10
  15. {slackblocks-2.4.0 → slackblocks-2.6.0}/slackblocks/utils.py +2 -1
  16. {slackblocks-2.4.0 → slackblocks-2.6.0}/slackblocks/views.py +33 -10
  17. {slackblocks-2.4.0 → slackblocks-2.6.0}/uv.lock +412 -362
  18. {slackblocks-2.4.0 → slackblocks-2.6.0}/LICENSE +0 -0
  19. {slackblocks-2.4.0 → slackblocks-2.6.0}/LICENSE.BSD-3-Clause +0 -0
  20. {slackblocks-2.4.0 → slackblocks-2.6.0}/README.md +0 -0
  21. {slackblocks-2.4.0 → slackblocks-2.6.0}/conformance/skiplist.txt +0 -0
  22. {slackblocks-2.4.0 → slackblocks-2.6.0}/slackblocks/_core.py +0 -0
  23. {slackblocks-2.4.0 → slackblocks-2.6.0}/slackblocks/builder.py +0 -0
  24. {slackblocks-2.4.0 → slackblocks-2.6.0}/slackblocks/errors.py +0 -0
  25. {slackblocks-2.4.0 → slackblocks-2.6.0}/slackblocks/modals.py +0 -0
  26. {slackblocks-2.4.0 → slackblocks-2.6.0}/slackblocks/py.typed +0 -0
  27. {slackblocks-2.4.0 → slackblocks-2.6.0}/slackblocks/rich_text/__init__.py +0 -0
  28. {slackblocks-2.4.0 → slackblocks-2.6.0}/slackblocks/rich_text/elements.py +0 -0
@@ -117,3 +117,7 @@ node_modules/
117
117
  /docs/docs/reference/csharp/
118
118
  /docs/scripts/csharp-reference/bin/
119
119
  /docs/scripts/csharp-reference/obj/
120
+
121
+ /docs/docs/reference/rust/
122
+
123
+ /rust/dist/
@@ -5,6 +5,69 @@ All notable changes to `slackblocks` are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ## [2.6.0] — 2026-10-09
11
+
12
+ - Release alongside the new native Rust implementation; supported language versions are unchanged.
13
+ - Update development type checking to mypy 2.4 and refresh its locked dependencies.
14
+
15
+ ## [2.5.0] — 2026-09-28
16
+
17
+ ### Changed
18
+
19
+ - `WorkflowButton` now rejects an `accessibility_label` longer than 75 characters,
20
+ matching Slack's limit and the shared conformance contract.
21
+ - `DispatchActionConfiguration` without `trigger_actions_on` now raises
22
+ `MissingRequiredError` instead of `LengthError`.
23
+ - `VideoBlock` now accepts `alt_text` of up to 2000 characters, including an
24
+ empty string, matching Slack's validator (previously 1 to 200 characters).
25
+ - `StaticMultiSelectMenu` and `ExternalMultiSelectMenu` no longer cap
26
+ `initial_options` at 100; Slack documents and enforces no such limit.
27
+ - Validation now follows Slack's `blocks.validate`. Newly rejected:
28
+ - out-of-range `RichTextList` `indent` (0-8), `offset` (0 or more) and
29
+ `border` (0-1), and `RichTextQuote` / `RichTextCodeBlock` `border` (0-1);
30
+ - `PlainTextInput` `min_length` outside 0-3000 or `max_length` below 1;
31
+ `max_selected_items` below 1 on all five multi-select menus; a negative
32
+ `DataTableBlock` `row_header_column_index`;
33
+ - an `ImageBlock` `title` over 2000 characters, an `Image` element
34
+ `image_url` over 3000 or `alt_text` over 2000, `VideoBlock`
35
+ `thumbnail_url` / `video_url` over 3000 and `title_url` /
36
+ `provider_icon_url` over 2000, and a view `external_id` over 255;
37
+ - more than 20 `TableBlock` `column_settings`, a `ContainerBlock` with no
38
+ child blocks, and a `PlanBlock` with more than 50 tasks or duplicate
39
+ `task_id`s;
40
+ - a `ConversationFilter` whose `include` is empty or names anything other
41
+ than `im`, `mpim`, `private` or `public`;
42
+ - a `SlackFile` `id` that does not match `^F[A-Z0-9]{8,}$`;
43
+ - messages (`Message`, `MessageResponse`, `WebhookMessage`) whose markdown
44
+ blocks total more than 12,000 characters or whose data tables total more
45
+ than 20,000 characters of cell text, counting attachment blocks;
46
+ - a standalone `TaskCardBlock` with `pending` status in a message or
47
+ attachment (plan tasks may still be `pending`).
48
+ - Newly required, raising `MissingRequiredError` when omitted (signatures are
49
+ unchanged): `PlanBlock` `tasks`, `TaskCardBlock` `status`, `SlackIcon`
50
+ `name`, `NumberInput` `is_decimal_allowed`, `Attachment` `blocks`, and
51
+ `WorkflowButton` `action_id`. `WorkflowButton` gains the `action_id`
52
+ keyword argument, which it now renders.
53
+ - `ImageBlock` accepts a `slack_file` keyword argument (and parses it in
54
+ `from_dict`); exactly one of `image_url` and `slack_file` is required, so
55
+ `image_url` is now optional.
56
+ - `RichTextInput` `initial_value` now takes a `RichTextBlock` instead of a
57
+ rich text element, and `RichTextInput` gains `min_lines` and `max_lines`
58
+ (1-100).
59
+ - `TableBlock` no longer requires one `column_settings` entry per column.
60
+ - Messages now accept `InputBlock`, and `HomeTabView` accepts
61
+ `DataVisualizationBlock`.
62
+ - `action_id` is now optional (and omitted from the payload when unset) on
63
+ every interactive element except `RichTextInput` and `WorkflowButton`; it
64
+ defaults to `None`, except on `CheckboxGroup`, `OverflowMenu`,
65
+ `RadioButtonGroup` and `StaticMultiSelectMenu`, where a required `options`
66
+ follows it, so pass `action_id=None` there.
67
+ - Newly accepted: a `URLSource` `url` of any length, an empty `MarkdownBlock`
68
+ `text`, a view with no blocks, and `TableBlock` rows with different numbers
69
+ of cells.
70
+
8
71
  ## [2.4.0] — 2026-09-17
9
72
 
10
73
  This coordinated release introduces the C# package, published to NuGet as
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: slackblocks
3
- Version: 2.4.0
3
+ Version: 2.6.0
4
4
  Summary: Python wrapper for the Slack Blocks API
5
5
  Project-URL: Homepage, https://github.com/nicklambourne/slackblocks
6
6
  Project-URL: Repository, https://github.com/nicklambourne/slackblocks
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env python3
2
+ """Generate python/slackblocks/_limits.py from spec/limits.json.
3
+
4
+ The wheel and sdist are built from python/ and cannot read spec/ at runtime, so every
5
+ limit is written out as a module-level constant named after its dotted path, e.g.
6
+ button.url.max_length becomes BUTTON_URL_MAX_LENGTH.
7
+
8
+ Usage:
9
+ python3 python/generator/generate_limits.py
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ from pathlib import Path
16
+ from typing import TYPE_CHECKING, Any
17
+
18
+ if TYPE_CHECKING:
19
+ from collections.abc import Iterator
20
+
21
+ ROOT = Path(__file__).resolve().parents[2]
22
+ LIMITS = ROOT / "spec/limits.json"
23
+ OUTPUT = ROOT / "python/slackblocks/_limits.py"
24
+ HEADER = '''\
25
+ # Generated by python/generator/generate_limits.py from spec/limits.json. Do not edit.
26
+ """Slack Block Kit limits from spec/limits.json, the normative limit registry."""
27
+ '''
28
+
29
+
30
+ def leaves(node: dict[str, Any], prefix: str = "") -> Iterator[tuple[str, int | float]]:
31
+ for key, value in node.items():
32
+ path = f"{prefix}.{key}" if prefix else key
33
+ if isinstance(value, dict):
34
+ yield from leaves(value, path)
35
+ elif isinstance(value, int | float) and not isinstance(value, bool):
36
+ yield path, value
37
+ else:
38
+ raise TypeError(f"{path}: expected a number, got {value!r}")
39
+
40
+
41
+ def render(limits: dict[str, Any]) -> str:
42
+ lines = [HEADER]
43
+ paths: dict[str, str] = {}
44
+ for path, value in leaves(limits):
45
+ name = path.replace(".", "_").upper()
46
+ if name in paths:
47
+ raise ValueError(f"{path} and {paths[name]} both map to {name}")
48
+ paths[name] = path
49
+ lines.append(f"# {path}\n{name} = {value!r}")
50
+ return "\n".join(lines) + "\n"
51
+
52
+
53
+ def main() -> None:
54
+ limits = json.loads(LIMITS.read_text(encoding="utf-8"))
55
+ OUTPUT.write_text(render(limits), encoding="utf-8", newline="\n")
56
+
57
+
58
+ if __name__ == "__main__":
59
+ main()
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "slackblocks"
3
- version = "2.4.0"
3
+ version = "2.6.0"
4
4
  description = "Python wrapper for the Slack Blocks API"
5
5
  authors = [
6
6
  { name = "Nicholas Lambourne", email = "dev@ndl.im" },
@@ -45,7 +45,7 @@ exclude = ["test/**", "docs/**"]
45
45
  dev = [
46
46
  "aiohttp>=3.10.11,<4.0.0",
47
47
  "hypothesis>=6.100.0,<7.0.0",
48
- "mypy>=1.14.1,<2.4.0",
48
+ "mypy>=1.14.1,<2.5.0",
49
49
  "pytest>=8.3.4,<10.0.0",
50
50
  "pytest-cov>=6.0.0,<8.0.0",
51
51
  "ruff>=0.8.0,<1.0.0",
@@ -123,4 +123,4 @@ name = "slackblocks"
123
123
 
124
124
  # The version of the shared conformance spec (spec/SPEC.md) this
125
125
  # implementation conforms to.
126
- SPEC_VERSION: str = "1.1.0"
126
+ SPEC_VERSION: str = "1.2.0"
@@ -0,0 +1,287 @@
1
+ # Generated by python/generator/generate_limits.py from spec/limits.json. Do not edit.
2
+ """Slack Block Kit limits from spec/limits.json, the normative limit registry."""
3
+
4
+ # text.min_length
5
+ TEXT_MIN_LENGTH = 1
6
+ # text.max_length
7
+ TEXT_MAX_LENGTH = 3000
8
+ # action_id.max_length
9
+ ACTION_ID_MAX_LENGTH = 255
10
+ # block_id.max_length
11
+ BLOCK_ID_MAX_LENGTH = 255
12
+ # button.text.max_length
13
+ BUTTON_TEXT_MAX_LENGTH = 75
14
+ # button.url.max_length
15
+ BUTTON_URL_MAX_LENGTH = 3000
16
+ # button.value.max_length
17
+ BUTTON_VALUE_MAX_LENGTH = 2000
18
+ # button.accessibility_label.max_length
19
+ BUTTON_ACCESSIBILITY_LABEL_MAX_LENGTH = 75
20
+ # confirmation.title.max_length
21
+ CONFIRMATION_TITLE_MAX_LENGTH = 100
22
+ # confirmation.text.max_length
23
+ CONFIRMATION_TEXT_MAX_LENGTH = 300
24
+ # confirmation.confirm.max_length
25
+ CONFIRMATION_CONFIRM_MAX_LENGTH = 30
26
+ # confirmation.deny.max_length
27
+ CONFIRMATION_DENY_MAX_LENGTH = 30
28
+ # option.text.max_length
29
+ OPTION_TEXT_MAX_LENGTH = 75
30
+ # option.value.max_length
31
+ OPTION_VALUE_MAX_LENGTH = 150
32
+ # option.description.max_length
33
+ OPTION_DESCRIPTION_MAX_LENGTH = 75
34
+ # option.url.max_length
35
+ OPTION_URL_MAX_LENGTH = 3000
36
+ # option_group.label.max_length
37
+ OPTION_GROUP_LABEL_MAX_LENGTH = 75
38
+ # option_group.options.min_items
39
+ OPTION_GROUP_OPTIONS_MIN_ITEMS = 1
40
+ # option_group.options.max_items
41
+ OPTION_GROUP_OPTIONS_MAX_ITEMS = 100
42
+ # select.placeholder.max_length
43
+ SELECT_PLACEHOLDER_MAX_LENGTH = 150
44
+ # select.options.max_items
45
+ SELECT_OPTIONS_MAX_ITEMS = 100
46
+ # select.option_groups.max_items
47
+ SELECT_OPTION_GROUPS_MAX_ITEMS = 100
48
+ # overflow.options.min_items
49
+ OVERFLOW_OPTIONS_MIN_ITEMS = 1
50
+ # overflow.options.max_items
51
+ OVERFLOW_OPTIONS_MAX_ITEMS = 5
52
+ # checkboxes.options.min_items
53
+ CHECKBOXES_OPTIONS_MIN_ITEMS = 1
54
+ # checkboxes.options.max_items
55
+ CHECKBOXES_OPTIONS_MAX_ITEMS = 10
56
+ # radio_buttons.options.min_items
57
+ RADIO_BUTTONS_OPTIONS_MIN_ITEMS = 1
58
+ # radio_buttons.options.max_items
59
+ RADIO_BUTTONS_OPTIONS_MAX_ITEMS = 10
60
+ # file_input.max_files.min
61
+ FILE_INPUT_MAX_FILES_MIN = 1
62
+ # file_input.max_files.max
63
+ FILE_INPUT_MAX_FILES_MAX = 10
64
+ # plain_text_input.min_length.min
65
+ PLAIN_TEXT_INPUT_MIN_LENGTH_MIN = 0
66
+ # plain_text_input.min_length.max
67
+ PLAIN_TEXT_INPUT_MIN_LENGTH_MAX = 3000
68
+ # plain_text_input.max_length.min
69
+ PLAIN_TEXT_INPUT_MAX_LENGTH_MIN = 1
70
+ # plain_text_input.max_length.max
71
+ PLAIN_TEXT_INPUT_MAX_LENGTH_MAX = 3000
72
+ # plain_text_input.placeholder.max_length
73
+ PLAIN_TEXT_INPUT_PLACEHOLDER_MAX_LENGTH = 150
74
+ # email_input.placeholder.max_length
75
+ EMAIL_INPUT_PLACEHOLDER_MAX_LENGTH = 150
76
+ # url_input.placeholder.max_length
77
+ URL_INPUT_PLACEHOLDER_MAX_LENGTH = 150
78
+ # number_input.placeholder.max_length
79
+ NUMBER_INPUT_PLACEHOLDER_MAX_LENGTH = 150
80
+ # date_picker.placeholder.max_length
81
+ DATE_PICKER_PLACEHOLDER_MAX_LENGTH = 150
82
+ # time_picker.placeholder.max_length
83
+ TIME_PICKER_PLACEHOLDER_MAX_LENGTH = 150
84
+ # rich_text_input.placeholder.max_length
85
+ RICH_TEXT_INPUT_PLACEHOLDER_MAX_LENGTH = 150
86
+ # rich_text_input.min_lines.min
87
+ RICH_TEXT_INPUT_MIN_LINES_MIN = 1
88
+ # rich_text_input.min_lines.max
89
+ RICH_TEXT_INPUT_MIN_LINES_MAX = 100
90
+ # rich_text_input.max_lines.min
91
+ RICH_TEXT_INPUT_MAX_LINES_MIN = 1
92
+ # rich_text_input.max_lines.max
93
+ RICH_TEXT_INPUT_MAX_LINES_MAX = 100
94
+ # multi_select.max_selected_items.min
95
+ MULTI_SELECT_MAX_SELECTED_ITEMS_MIN = 1
96
+ # conversation_filter.include.min_items
97
+ CONVERSATION_FILTER_INCLUDE_MIN_ITEMS = 1
98
+ # dispatch_action_configuration.trigger_actions_on.min_items
99
+ DISPATCH_ACTION_CONFIGURATION_TRIGGER_ACTIONS_ON_MIN_ITEMS = 1
100
+ # dispatch_action_configuration.trigger_actions_on.max_items
101
+ DISPATCH_ACTION_CONFIGURATION_TRIGGER_ACTIONS_ON_MAX_ITEMS = 2
102
+ # actions.elements.max_items
103
+ ACTIONS_ELEMENTS_MAX_ITEMS = 25
104
+ # alert.text.max_length
105
+ ALERT_TEXT_MAX_LENGTH = 200
106
+ # card.title.max_length
107
+ CARD_TITLE_MAX_LENGTH = 150
108
+ # card.subtitle.max_length
109
+ CARD_SUBTITLE_MAX_LENGTH = 150
110
+ # card.body.max_length
111
+ CARD_BODY_MAX_LENGTH = 200
112
+ # card.actions.max_items
113
+ CARD_ACTIONS_MAX_ITEMS = 3
114
+ # card.subtext.max_length
115
+ CARD_SUBTEXT_MAX_LENGTH = 200
116
+ # carousel.elements.min_items
117
+ CAROUSEL_ELEMENTS_MIN_ITEMS = 1
118
+ # carousel.elements.max_items
119
+ CAROUSEL_ELEMENTS_MAX_ITEMS = 10
120
+ # container.title.max_length
121
+ CONTAINER_TITLE_MAX_LENGTH = 150
122
+ # container.subtitle.max_length
123
+ CONTAINER_SUBTITLE_MAX_LENGTH = 150
124
+ # container.child_blocks.min_items
125
+ CONTAINER_CHILD_BLOCKS_MIN_ITEMS = 1
126
+ # container.child_blocks.max_items
127
+ CONTAINER_CHILD_BLOCKS_MAX_ITEMS = 10
128
+ # context.elements.max_items
129
+ CONTEXT_ELEMENTS_MAX_ITEMS = 10
130
+ # context_actions.elements.max_items
131
+ CONTEXT_ACTIONS_ELEMENTS_MAX_ITEMS = 5
132
+ # feedback_button.text.max_length
133
+ FEEDBACK_BUTTON_TEXT_MAX_LENGTH = 75
134
+ # feedback_button.value.max_length
135
+ FEEDBACK_BUTTON_VALUE_MAX_LENGTH = 2000
136
+ # feedback_button.accessibility_label.max_length
137
+ FEEDBACK_BUTTON_ACCESSIBILITY_LABEL_MAX_LENGTH = 75
138
+ # icon_button.value.max_length
139
+ ICON_BUTTON_VALUE_MAX_LENGTH = 2000
140
+ # icon_button.accessibility_label.max_length
141
+ ICON_BUTTON_ACCESSIBILITY_LABEL_MAX_LENGTH = 75
142
+ # icon_button.visible_to_user_ids.max_items
143
+ ICON_BUTTON_VISIBLE_TO_USER_IDS_MAX_ITEMS = 10
144
+ # workflow_button.text.max_length
145
+ WORKFLOW_BUTTON_TEXT_MAX_LENGTH = 75
146
+ # workflow_button.accessibility_label.max_length
147
+ WORKFLOW_BUTTON_ACCESSIBILITY_LABEL_MAX_LENGTH = 75
148
+ # table.rows.max_items
149
+ TABLE_ROWS_MAX_ITEMS = 100
150
+ # table.columns.max_items
151
+ TABLE_COLUMNS_MAX_ITEMS = 20
152
+ # table.column_settings.max_items
153
+ TABLE_COLUMN_SETTINGS_MAX_ITEMS = 20
154
+ # data_table.rows.min_items
155
+ DATA_TABLE_ROWS_MIN_ITEMS = 2
156
+ # data_table.rows.max_items
157
+ DATA_TABLE_ROWS_MAX_ITEMS = 201
158
+ # data_table.columns.min_items
159
+ DATA_TABLE_COLUMNS_MIN_ITEMS = 1
160
+ # data_table.columns.max_items
161
+ DATA_TABLE_COLUMNS_MAX_ITEMS = 20
162
+ # data_table.page_size.min
163
+ DATA_TABLE_PAGE_SIZE_MIN = 1
164
+ # data_table.page_size.max
165
+ DATA_TABLE_PAGE_SIZE_MAX = 100
166
+ # data_table.cell_text.min_length
167
+ DATA_TABLE_CELL_TEXT_MIN_LENGTH = 1
168
+ # data_table.content.max_length
169
+ DATA_TABLE_CONTENT_MAX_LENGTH = 20000
170
+ # data_table.total_content.max_length
171
+ DATA_TABLE_TOTAL_CONTENT_MAX_LENGTH = 20000
172
+ # data_table.row_header_column_index.min
173
+ DATA_TABLE_ROW_HEADER_COLUMN_INDEX_MIN = 0
174
+ # data_visualization.title.max_length
175
+ DATA_VISUALIZATION_TITLE_MAX_LENGTH = 50
176
+ # data_visualization.segments.min_items
177
+ DATA_VISUALIZATION_SEGMENTS_MIN_ITEMS = 1
178
+ # data_visualization.segments.max_items
179
+ DATA_VISUALIZATION_SEGMENTS_MAX_ITEMS = 12
180
+ # data_visualization.segment.label.max_length
181
+ DATA_VISUALIZATION_SEGMENT_LABEL_MAX_LENGTH = 20
182
+ # data_visualization.segment.value.exclusive_min
183
+ DATA_VISUALIZATION_SEGMENT_VALUE_EXCLUSIVE_MIN = 0
184
+ # data_visualization.series.min_items
185
+ DATA_VISUALIZATION_SERIES_MIN_ITEMS = 1
186
+ # data_visualization.series.max_items
187
+ DATA_VISUALIZATION_SERIES_MAX_ITEMS = 12
188
+ # data_visualization.series_name.max_length
189
+ DATA_VISUALIZATION_SERIES_NAME_MAX_LENGTH = 20
190
+ # data_visualization.data.min_items
191
+ DATA_VISUALIZATION_DATA_MIN_ITEMS = 1
192
+ # data_visualization.data.max_items
193
+ DATA_VISUALIZATION_DATA_MAX_ITEMS = 20
194
+ # data_visualization.point_label.max_length
195
+ DATA_VISUALIZATION_POINT_LABEL_MAX_LENGTH = 20
196
+ # data_visualization.categories.min_items
197
+ DATA_VISUALIZATION_CATEGORIES_MIN_ITEMS = 1
198
+ # data_visualization.categories.max_items
199
+ DATA_VISUALIZATION_CATEGORIES_MAX_ITEMS = 20
200
+ # data_visualization.category_label.max_length
201
+ DATA_VISUALIZATION_CATEGORY_LABEL_MAX_LENGTH = 20
202
+ # data_visualization.axis_label.max_length
203
+ DATA_VISUALIZATION_AXIS_LABEL_MAX_LENGTH = 50
204
+ # header.text.max_length
205
+ HEADER_TEXT_MAX_LENGTH = 150
206
+ # image.image_url.max_length
207
+ IMAGE_IMAGE_URL_MAX_LENGTH = 3000
208
+ # image.alt_text.max_length
209
+ IMAGE_ALT_TEXT_MAX_LENGTH = 2000
210
+ # image.title.max_length
211
+ IMAGE_TITLE_MAX_LENGTH = 2000
212
+ # image_element.image_url.max_length
213
+ IMAGE_ELEMENT_IMAGE_URL_MAX_LENGTH = 3000
214
+ # image_element.alt_text.max_length
215
+ IMAGE_ELEMENT_ALT_TEXT_MAX_LENGTH = 2000
216
+ # input.label.max_length
217
+ INPUT_LABEL_MAX_LENGTH = 2000
218
+ # input.hint.max_length
219
+ INPUT_HINT_MAX_LENGTH = 2000
220
+ # markdown.text.max_length
221
+ MARKDOWN_TEXT_MAX_LENGTH = 12000
222
+ # markdown.total_text.max_length
223
+ MARKDOWN_TOTAL_TEXT_MAX_LENGTH = 12000
224
+ # plan.tasks.max_items
225
+ PLAN_TASKS_MAX_ITEMS = 50
226
+ # rich_text_list.indent.min
227
+ RICH_TEXT_LIST_INDENT_MIN = 0
228
+ # rich_text_list.indent.max
229
+ RICH_TEXT_LIST_INDENT_MAX = 8
230
+ # rich_text_list.offset.min
231
+ RICH_TEXT_LIST_OFFSET_MIN = 0
232
+ # rich_text_list.border.min
233
+ RICH_TEXT_LIST_BORDER_MIN = 0
234
+ # rich_text_list.border.max
235
+ RICH_TEXT_LIST_BORDER_MAX = 1
236
+ # rich_text_quote.border.min
237
+ RICH_TEXT_QUOTE_BORDER_MIN = 0
238
+ # rich_text_quote.border.max
239
+ RICH_TEXT_QUOTE_BORDER_MAX = 1
240
+ # rich_text_preformatted.border.min
241
+ RICH_TEXT_PREFORMATTED_BORDER_MIN = 0
242
+ # rich_text_preformatted.border.max
243
+ RICH_TEXT_PREFORMATTED_BORDER_MAX = 1
244
+ # section.text.max_length
245
+ SECTION_TEXT_MAX_LENGTH = 3000
246
+ # section.fields.max_items
247
+ SECTION_FIELDS_MAX_ITEMS = 10
248
+ # section.fields.item_max_length
249
+ SECTION_FIELDS_ITEM_MAX_LENGTH = 2000
250
+ # video.alt_text.max_length
251
+ VIDEO_ALT_TEXT_MAX_LENGTH = 2000
252
+ # video.title.max_length
253
+ VIDEO_TITLE_MAX_LENGTH = 200
254
+ # video.author_name.max_length
255
+ VIDEO_AUTHOR_NAME_MAX_LENGTH = 50
256
+ # video.description.max_length
257
+ VIDEO_DESCRIPTION_MAX_LENGTH = 200
258
+ # video.provider_name.max_length
259
+ VIDEO_PROVIDER_NAME_MAX_LENGTH = 50
260
+ # video.thumbnail_url.max_length
261
+ VIDEO_THUMBNAIL_URL_MAX_LENGTH = 3000
262
+ # video.video_url.max_length
263
+ VIDEO_VIDEO_URL_MAX_LENGTH = 3000
264
+ # video.title_url.max_length
265
+ VIDEO_TITLE_URL_MAX_LENGTH = 2000
266
+ # video.provider_icon_url.max_length
267
+ VIDEO_PROVIDER_ICON_URL_MAX_LENGTH = 2000
268
+ # message.channel.min_length
269
+ MESSAGE_CHANNEL_MIN_LENGTH = 1
270
+ # message.blocks.max_items
271
+ MESSAGE_BLOCKS_MAX_ITEMS = 50
272
+ # message.attachments.max_items
273
+ MESSAGE_ATTACHMENTS_MAX_ITEMS = 100
274
+ # view.blocks.max_items
275
+ VIEW_BLOCKS_MAX_ITEMS = 100
276
+ # view.private_metadata.max_length
277
+ VIEW_PRIVATE_METADATA_MAX_LENGTH = 3000
278
+ # view.callback_id.max_length
279
+ VIEW_CALLBACK_ID_MAX_LENGTH = 255
280
+ # view.external_id.max_length
281
+ VIEW_EXTERNAL_ID_MAX_LENGTH = 255
282
+ # view.title.max_length
283
+ VIEW_TITLE_MAX_LENGTH = 24
284
+ # view.close.max_length
285
+ VIEW_CLOSE_MAX_LENGTH = 24
286
+ # view.submit.max_length
287
+ VIEW_SUBMIT_MAX_LENGTH = 24
@@ -24,6 +24,7 @@ _BLOCK_TYPES_BY_SURFACE: dict[BlockSurface, frozenset[str]] = {
24
24
  "file",
25
25
  "header",
26
26
  "image",
27
+ "input",
27
28
  "markdown",
28
29
  "plan",
29
30
  "rich_text",
@@ -56,6 +57,7 @@ _BLOCK_TYPES_BY_SURFACE: dict[BlockSurface, frozenset[str]] = {
56
57
  "container",
57
58
  "context",
58
59
  "data_table",
60
+ "data_visualization",
59
61
  "divider",
60
62
  "header",
61
63
  "image",
@@ -87,3 +89,19 @@ def validate_surface_blocks(blocks: list[Any], surface: BlockSurface) -> None:
87
89
  raise TypeMismatchError(
88
90
  f"blocks[{index}] type {type_!r} is not supported on {surface} surfaces"
89
91
  )
92
+ validate_standalone_task_cards(blocks)
93
+
94
+
95
+ def validate_standalone_task_cards(blocks: list[Any]) -> None:
96
+ """Reject standalone task cards with ``pending`` status; only plan tasks may be pending."""
97
+ for index, block in enumerate(blocks):
98
+ rendered = resolve(block)
99
+ if (
100
+ isinstance(rendered, dict)
101
+ and rendered.get("type") == "task_card"
102
+ and rendered.get("status") == "pending"
103
+ ):
104
+ raise TypeMismatchError(
105
+ f"blocks[{index}] is a standalone task card with `pending` status; "
106
+ "only plan tasks can be pending"
107
+ )
@@ -12,9 +12,10 @@ from json import dumps
12
12
  from typing import Any
13
13
 
14
14
  from slackblocks._core import resolve
15
+ from slackblocks._surfaces import validate_standalone_task_cards
15
16
  from slackblocks.blocks import Block
16
- from slackblocks.errors import TypeMismatchError
17
- from slackblocks.utils import coerce_to_list, is_hex
17
+ from slackblocks.errors import MissingRequiredError, TypeMismatchError
18
+ from slackblocks.utils import coerce_to_list, coerce_to_list_nonnull, is_hex
18
19
 
19
20
 
20
21
  class Color(Enum):
@@ -99,7 +100,7 @@ class Attachment:
99
100
  `slackblocks`.
100
101
 
101
102
  Args:
102
- blocks: an array of Blocks that define the content of the attachment.
103
+ blocks: an array of Blocks that define the content of the attachment (required).
103
104
  color: the color (in hex format, e.g. #ffffff) of the vertical bar to the left of the
104
105
  attachment content. Consider using the `Color` enum from this module.
105
106
  fields: a list of `Field` objects to be included in what's rendered in the attachment.
@@ -107,7 +108,7 @@ class Attachment:
107
108
  formatted text (eg. IRC, mobile notifications).
108
109
 
109
110
  Throws:
110
- InvalidUsageError: if the `color` code provided is invalid.
111
+ InvalidUsageError: if `blocks` is missing or the `color` code provided is invalid.
111
112
  """
112
113
 
113
114
  def __init__(
@@ -117,7 +118,10 @@ class Attachment:
117
118
  fields: Field | list[Field] | None = None,
118
119
  fallback: str | None = None,
119
120
  ):
120
- self.blocks = coerce_to_list(blocks, Block, allow_none=True)
121
+ if blocks is None:
122
+ raise MissingRequiredError("Attachment requires `blocks`.")
123
+ self.blocks: list[Block] = coerce_to_list_nonnull(blocks, Block)
124
+ validate_standalone_task_cards(self.blocks)
121
125
  self.fields = coerce_to_list(fields, Field, allow_none=True)
122
126
  self.fallback = fallback
123
127
  self.color: str | None