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.
- {slackblocks-2.4.0 → slackblocks-2.6.0}/.gitignore +4 -0
- {slackblocks-2.4.0 → slackblocks-2.6.0}/CHANGELOG.md +63 -0
- {slackblocks-2.4.0 → slackblocks-2.6.0}/PKG-INFO +1 -1
- slackblocks-2.6.0/generator/generate_limits.py +59 -0
- {slackblocks-2.4.0 → slackblocks-2.6.0}/pyproject.toml +2 -2
- {slackblocks-2.4.0 → slackblocks-2.6.0}/slackblocks/__init__.py +1 -1
- slackblocks-2.6.0/slackblocks/_limits.py +287 -0
- {slackblocks-2.4.0 → slackblocks-2.6.0}/slackblocks/_surfaces.py +18 -0
- {slackblocks-2.4.0 → slackblocks-2.6.0}/slackblocks/attachments.py +9 -5
- {slackblocks-2.4.0 → slackblocks-2.6.0}/slackblocks/blocks.py +199 -90
- {slackblocks-2.4.0 → slackblocks-2.6.0}/slackblocks/elements.py +274 -98
- {slackblocks-2.4.0 → slackblocks-2.6.0}/slackblocks/messages.py +46 -7
- {slackblocks-2.4.0 → slackblocks-2.6.0}/slackblocks/objects.py +141 -32
- {slackblocks-2.4.0 → slackblocks-2.6.0}/slackblocks/rich_text/objects.py +41 -10
- {slackblocks-2.4.0 → slackblocks-2.6.0}/slackblocks/utils.py +2 -1
- {slackblocks-2.4.0 → slackblocks-2.6.0}/slackblocks/views.py +33 -10
- {slackblocks-2.4.0 → slackblocks-2.6.0}/uv.lock +412 -362
- {slackblocks-2.4.0 → slackblocks-2.6.0}/LICENSE +0 -0
- {slackblocks-2.4.0 → slackblocks-2.6.0}/LICENSE.BSD-3-Clause +0 -0
- {slackblocks-2.4.0 → slackblocks-2.6.0}/README.md +0 -0
- {slackblocks-2.4.0 → slackblocks-2.6.0}/conformance/skiplist.txt +0 -0
- {slackblocks-2.4.0 → slackblocks-2.6.0}/slackblocks/_core.py +0 -0
- {slackblocks-2.4.0 → slackblocks-2.6.0}/slackblocks/builder.py +0 -0
- {slackblocks-2.4.0 → slackblocks-2.6.0}/slackblocks/errors.py +0 -0
- {slackblocks-2.4.0 → slackblocks-2.6.0}/slackblocks/modals.py +0 -0
- {slackblocks-2.4.0 → slackblocks-2.6.0}/slackblocks/py.typed +0 -0
- {slackblocks-2.4.0 → slackblocks-2.6.0}/slackblocks/rich_text/__init__.py +0 -0
- {slackblocks-2.4.0 → slackblocks-2.6.0}/slackblocks/rich_text/elements.py +0 -0
|
@@ -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.
|
|
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.
|
|
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.
|
|
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",
|
|
@@ -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
|
-
|
|
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
|