slackblocks 3.2.0__tar.gz → 4.0.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 (29) hide show
  1. {slackblocks-3.2.0 → slackblocks-4.0.0}/CHANGELOG.md +11 -0
  2. {slackblocks-3.2.0 → slackblocks-4.0.0}/PKG-INFO +2 -2
  3. {slackblocks-3.2.0 → slackblocks-4.0.0}/README.md +1 -1
  4. {slackblocks-3.2.0 → slackblocks-4.0.0}/pyproject.toml +2 -2
  5. {slackblocks-3.2.0 → slackblocks-4.0.0}/slackblocks/__init__.py +1 -1
  6. {slackblocks-3.2.0 → slackblocks-4.0.0}/slackblocks/_limits.py +4 -0
  7. {slackblocks-3.2.0 → slackblocks-4.0.0}/slackblocks/blocks.py +40 -18
  8. {slackblocks-3.2.0 → slackblocks-4.0.0}/slackblocks/messages.py +12 -1
  9. {slackblocks-3.2.0 → slackblocks-4.0.0}/slackblocks/modals.py +2 -0
  10. {slackblocks-3.2.0 → slackblocks-4.0.0}/slackblocks/objects.py +9 -9
  11. {slackblocks-3.2.0 → slackblocks-4.0.0}/slackblocks/views.py +1 -0
  12. {slackblocks-3.2.0 → slackblocks-4.0.0}/uv.lock +1 -1
  13. {slackblocks-3.2.0 → slackblocks-4.0.0}/.gitignore +0 -0
  14. {slackblocks-3.2.0 → slackblocks-4.0.0}/LICENSE +0 -0
  15. {slackblocks-3.2.0 → slackblocks-4.0.0}/LICENSE.BSD-3-Clause +0 -0
  16. {slackblocks-3.2.0 → slackblocks-4.0.0}/conformance/skiplist.txt +0 -0
  17. {slackblocks-3.2.0 → slackblocks-4.0.0}/generator/generate_limits.py +0 -0
  18. {slackblocks-3.2.0 → slackblocks-4.0.0}/slackblocks/_core.py +0 -0
  19. {slackblocks-3.2.0 → slackblocks-4.0.0}/slackblocks/_layout.py +0 -0
  20. {slackblocks-3.2.0 → slackblocks-4.0.0}/slackblocks/_surfaces.py +0 -0
  21. {slackblocks-3.2.0 → slackblocks-4.0.0}/slackblocks/attachments.py +0 -0
  22. {slackblocks-3.2.0 → slackblocks-4.0.0}/slackblocks/builder.py +0 -0
  23. {slackblocks-3.2.0 → slackblocks-4.0.0}/slackblocks/elements.py +0 -0
  24. {slackblocks-3.2.0 → slackblocks-4.0.0}/slackblocks/errors.py +0 -0
  25. {slackblocks-3.2.0 → slackblocks-4.0.0}/slackblocks/py.typed +0 -0
  26. {slackblocks-3.2.0 → slackblocks-4.0.0}/slackblocks/rich_text/__init__.py +0 -0
  27. {slackblocks-3.2.0 → slackblocks-4.0.0}/slackblocks/rich_text/elements.py +0 -0
  28. {slackblocks-3.2.0 → slackblocks-4.0.0}/slackblocks/rich_text/objects.py +0 -0
  29. {slackblocks-3.2.0 → slackblocks-4.0.0}/slackblocks/utils.py +0 -0
@@ -5,6 +5,17 @@ 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
+ ## [4.0.0] — 2026-11-14
9
+
10
+ - Coordinate the ten-language 4.0.0 release with shared contract 1.6.0. Supported runtime floors are unchanged. See the 4.0.0 migration guide for Go imports, sparse-setting types and stricter validation.
11
+
12
+ - Require non-empty numeric display labels in both plain and data tables, matching the isolated Slack validation probe.
13
+
14
+ - Preserve skipped columns in sparse table column settings through native APIs and supported parsers. Null entries count toward the existing 20-column-settings limit.
15
+ - Reject non-finite numeric cells while preserving arbitrary-size Python integers.
16
+
17
+ - Add numeric plain-table cells and enforce Slack’s documented 10,000-character per-table and message-wide limits (spec 1.6.0). Count displayed cell text as Unicode code points, including composed blocks and attachments; ragged rows and partial column settings remain valid. Previously accepted over-limit tables now fail validation. Supported runtime floors are unchanged.
18
+
8
19
  ## [3.2.0] — 2026-10-31
9
20
 
10
21
  - Add native layout blocks, rows and items with typed widths/alignment, closed child and fallback roles, checked nesting and receiving-surface validation (spec 1.5.0).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: slackblocks
3
- Version: 3.2.0
3
+ Version: 4.0.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
@@ -55,7 +55,7 @@ This release conforms to the shared cross-language slackblocks specification.
55
55
  pip install slackblocks
56
56
  ```
57
57
 
58
- `slackblocks 2.x` requires Python 3.10 or newer. Users on Python 3.8 / 3.9 should pin to the `1.x` line — see the [Compatibility](https://nicklambourne.github.io/slackblocks/usage/compatibility) page.
58
+ `slackblocks 4.x` requires Python 3.10 or newer. Users on Python 3.8 / 3.9 should pin to the `1.x` line — see the [Compatibility](https://nicklambourne.github.io/slackblocks/usage/compatibility) page.
59
59
 
60
60
  ## Quickstart
61
61
 
@@ -28,7 +28,7 @@ This release conforms to the shared cross-language slackblocks specification.
28
28
  pip install slackblocks
29
29
  ```
30
30
 
31
- `slackblocks 2.x` requires Python 3.10 or newer. Users on Python 3.8 / 3.9 should pin to the `1.x` line — see the [Compatibility](https://nicklambourne.github.io/slackblocks/usage/compatibility) page.
31
+ `slackblocks 4.x` requires Python 3.10 or newer. Users on Python 3.8 / 3.9 should pin to the `1.x` line — see the [Compatibility](https://nicklambourne.github.io/slackblocks/usage/compatibility) page.
32
32
 
33
33
  ## Quickstart
34
34
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "slackblocks"
3
- version = "3.2.0"
3
+ version = "4.0.0"
4
4
  description = "Python wrapper for the Slack Blocks API"
5
5
  authors = [
6
6
  { name = "Nicholas Lambourne", email = "dev@ndl.im" },
@@ -58,7 +58,7 @@ docs = [
58
58
  ]
59
59
 
60
60
  [build-system]
61
- requires = ["hatchling<1.33"]
61
+ requires = ["hatchling<1.34"]
62
62
  build-backend = "hatchling.build"
63
63
 
64
64
  [tool.ruff]
@@ -131,7 +131,7 @@ name = "slackblocks"
131
131
 
132
132
  # The version of the shared conformance spec (spec/SPEC.md) this
133
133
  # implementation conforms to.
134
- SPEC_VERSION: str = "1.5.0"
134
+ SPEC_VERSION: str = "1.6.0"
135
135
 
136
136
  # Literal exports are visible to strict external type checkers.
137
137
  __all__ = [
@@ -159,6 +159,10 @@ TABLE_ROWS_MAX_ITEMS = 100
159
159
  TABLE_COLUMNS_MAX_ITEMS = 20
160
160
  # table.column_settings.max_items
161
161
  TABLE_COLUMN_SETTINGS_MAX_ITEMS = 20
162
+ # table.content.max_length
163
+ TABLE_CONTENT_MAX_LENGTH = 10000
164
+ # table.total_content.max_length
165
+ TABLE_TOTAL_CONTENT_MAX_LENGTH = 10000
162
166
  # data_table.rows.min_items
163
167
  DATA_TABLE_ROWS_MIN_ITEMS = 2
164
168
  # data_table.rows.max_items
@@ -59,6 +59,7 @@ from slackblocks._limits import (
59
59
  SECTION_TEXT_MAX_LENGTH,
60
60
  TABLE_COLUMN_SETTINGS_MAX_ITEMS,
61
61
  TABLE_COLUMNS_MAX_ITEMS,
62
+ TABLE_CONTENT_MAX_LENGTH,
62
63
  TABLE_ROWS_MAX_ITEMS,
63
64
  VIDEO_ALT_TEXT_MAX_LENGTH,
64
65
  VIDEO_AUTHOR_NAME_MAX_LENGTH,
@@ -163,6 +164,7 @@ ALLOWED_INPUT_ELEMENTS = (
163
164
 
164
165
  ALLOWED_TABLE_CELL_ELEMENTS = (
165
166
  RawText,
167
+ RawNumber,
166
168
  RichTextList,
167
169
  RichTextCodeBlock,
168
170
  RichTextQuote,
@@ -416,7 +418,8 @@ class DividerBlock(Block):
416
418
 
417
419
  class FileBlock(Block):
418
420
  """
419
- Displays a remote file (e.g. a PDF).
421
+ A retrieved remote-file representation (e.g. a PDF), not an outgoing message block.
422
+ Register and share remote files with the Files API, then read back this representation.
420
423
 
421
424
  For details on how remote files are exposed to Slack, see
422
425
  <https://api.slack.com/messaging/files#adding>.
@@ -818,21 +821,22 @@ class TableBlock(Block):
818
821
  A `TableBlock` displays data in a table format.
819
822
 
820
823
  Args:
821
- rows: a list of lists of `RawText` or `RichTextObject` objects.
822
- column_settings: a list of `ColumnSettings` objects.
824
+ rows: a list of lists of `RawText`, `RawNumber` or `RichTextObject` objects.
825
+ column_settings: settings for leading columns; omitted trailing columns use Slack defaults.
823
826
  block_id: you can use this field to provide a deterministic identifier for the block.
824
827
 
825
828
  Throws:
826
- InvalidUsageError: when items in `rows` are not `RawText` or `RichTextObject` objects.
829
+ InvalidUsageError: when items in `rows` are not `RawText`, `RawNumber` or `RichTextObject` objects.
827
830
  InvalidUsageError: when the number of rows is greater than 100.
828
831
  InvalidUsageError: when the number of columns in a row is greater than 20.
829
832
  InvalidUsageError: when the number of column_settings is greater than 20.
833
+ LengthError: when displayed cell text exceeds 10,000 Unicode code points.
830
834
  """
831
835
 
832
836
  def __init__(
833
837
  self,
834
- rows: list[list[RawText | RichTextObject]],
835
- column_settings: list[ColumnSettings] | None = None,
838
+ rows: list[list[RawText | RawNumber | RichTextObject]],
839
+ column_settings: list[ColumnSettings | None] | None = None,
836
840
  block_id: str | None = None,
837
841
  ) -> None:
838
842
  super().__init__(type_=BlockType.TABLE, block_id=block_id)
@@ -858,7 +862,13 @@ class TableBlock(Block):
858
862
  self.rows.append(validated_row)
859
863
  if column_settings and len(column_settings) > TABLE_COLUMN_SETTINGS_MAX_ITEMS:
860
864
  raise LengthError("`column_settings` can have a maximum of 20 items.")
865
+ if column_settings is not None:
866
+ for setting in column_settings:
867
+ if setting is not None and not isinstance(setting, ColumnSettings):
868
+ raise TypeMismatchError("Column settings must be ColumnSettings or None.")
861
869
  self.column_settings = column_settings
870
+ if _text_character_count(self._resolve()["rows"]) > TABLE_CONTENT_MAX_LENGTH:
871
+ raise LengthError("Table cell text exceeds the 10,000-character limit.")
862
872
 
863
873
  def _resolve(self) -> dict[str, Any]:
864
874
  return resolve(
@@ -869,14 +879,14 @@ class TableBlock(Block):
869
879
  }
870
880
  )
871
881
 
872
- def _resolve_cell(self, cell: RawText | RichTextObject) -> dict[str, Any]:
882
+ def _resolve_cell(self, cell: RawText | RawNumber | RichTextObject) -> dict[str, Any]:
873
883
  """
874
884
  Resolve a table cell to its JSON representation.
875
885
 
876
- RawText cells are resolved directly.
886
+ RawText and RawNumber cells are resolved directly.
877
887
  RichTextObject cells are wrapped in a rich_text structure.
878
888
  """
879
- if isinstance(cell, RawText):
889
+ if isinstance(cell, (RawText, RawNumber)):
880
890
  return cell._resolve()
881
891
  else:
882
892
  # Wrap RichTextObject in rich_text structure
@@ -1098,7 +1108,10 @@ class CardBlock(Block):
1098
1108
  title: the card's title (max 150 chars). Can be a string or `Text` object.
1099
1109
  subtitle: the card's subtitle (max 150 chars). Can be a string or `Text` object.
1100
1110
  body: the card's body text (max 200 chars). Can be a string or `Text` object.
1101
- actions: up to three `Button` elements presented as card actions.
1111
+ actions: up to three `Button` elements, serialized in the supplied order.
1112
+ Slack right-aligns buttons with primary last. With two buttons,
1113
+ danger precedes the other button in that group; with three,
1114
+ danger sits at the card's far left.
1102
1115
  slack_icon: a `SlackIcon` naming a Slack-provided icon. Cannot be
1103
1116
  combined with `icon`.
1104
1117
  subtext: additional text displayed at the bottom of the card
@@ -1406,8 +1419,11 @@ class DataTableBlock(Block):
1406
1419
  for cell in row:
1407
1420
  if not isinstance(cell, RawText | RawNumber | RichTextBlock):
1408
1421
  raise TypeMismatchError("Unsupported data table cell type.")
1409
- if isinstance(cell, RawText) and len(cell.text) < DATA_TABLE_CELL_TEXT_MIN_LENGTH:
1410
- raise LengthError("Data table raw text cells cannot be empty.")
1422
+ if (
1423
+ isinstance(cell, RawText | RawNumber)
1424
+ and len(cell.text) < DATA_TABLE_CELL_TEXT_MIN_LENGTH
1425
+ ):
1426
+ raise LengthError("Data table raw text and numeric labels cannot be empty.")
1411
1427
  if any(isinstance(cell, RichTextBlock) for cell in rows[0]):
1412
1428
  raise TypeMismatchError("Data table header cells cannot contain rich text.")
1413
1429
  self.page_size = validate_int(
@@ -1443,12 +1459,18 @@ class DataTableBlock(Block):
1443
1459
 
1444
1460
  def _text_character_count(value: Any) -> int:
1445
1461
  if isinstance(value, dict):
1446
- return sum(
1447
- len(nested)
1448
- if key == "text" and isinstance(nested, str)
1449
- else _text_character_count(nested)
1450
- for key, nested in value.items()
1451
- )
1462
+ if value.get("type") in {"raw_text", "raw_number", "text", "link"}:
1463
+ text = value.get("text")
1464
+ return len(text) if isinstance(text, str) else 0
1465
+ if value.get("type") in {
1466
+ "rich_text",
1467
+ "rich_text_section",
1468
+ "rich_text_list",
1469
+ "rich_text_quote",
1470
+ "rich_text_preformatted",
1471
+ }:
1472
+ return _text_character_count(value.get("elements", []))
1473
+ return 0
1452
1474
  if isinstance(value, list):
1453
1475
  return sum(_text_character_count(item) for item in value)
1454
1476
  return 0
@@ -21,6 +21,7 @@ from slackblocks._limits import (
21
21
  MESSAGE_BLOCKS_MAX_ITEMS,
22
22
  MESSAGE_CHANNEL_MIN_LENGTH,
23
23
  MESSAGE_DATA_VISUALIZATIONS_MAX_ITEMS,
24
+ TABLE_TOTAL_CONTENT_MAX_LENGTH,
24
25
  )
25
26
  from slackblocks._surfaces import validate_surface_blocks
26
27
  from slackblocks.utils import coerce_to_list, validate_string_nonnull
@@ -74,6 +75,13 @@ def _validate_message_totals(
74
75
  f"Markdown block text ({markdown} characters) exceeds the message-wide limit of "
75
76
  f"{MARKDOWN_TOTAL_TEXT_MAX_LENGTH} characters."
76
77
  )
78
+ table = sum(
79
+ _text_character_count(block["rows"])
80
+ for block in rendered_blocks
81
+ if block.get("type") == "table"
82
+ )
83
+ if table > TABLE_TOTAL_CONTENT_MAX_LENGTH:
84
+ raise LengthError("Table cell text exceeds the message-wide 10,000-character limit.")
77
85
  data_table = sum(
78
86
  _text_character_count(block["rows"])
79
87
  for block in rendered_blocks
@@ -99,9 +107,11 @@ class _MessagePayloadMixin:
99
107
  raise NotImplementedError
100
108
 
101
109
  def to_dict(self) -> dict[str, Any]:
110
+ """Return a fresh Slack-shaped dictionary, validating the resolved payload."""
102
111
  return self._resolve()
103
112
 
104
113
  def json(self) -> str:
114
+ """Return the validated Slack-shaped payload as indented JSON."""
105
115
  return dumps(self._resolve(), indent=4)
106
116
 
107
117
  def __repr__(self) -> str:
@@ -111,6 +121,7 @@ class _MessagePayloadMixin:
111
121
  return self._resolve()[item]
112
122
 
113
123
  def keys(self) -> list[str]:
124
+ """Return the serialized field names for mapping-style SDK argument unpacking."""
114
125
  return list(self._resolve().keys())
115
126
 
116
127
 
@@ -184,7 +195,7 @@ class Message(BaseMessage):
184
195
  automatically unfurl.
185
196
  metadata: optional message metadata, including `event_type` and `event_payload`.
186
197
  Throws:
187
- InvalidUsageException: in the event that the items passed to `blocks`
198
+ InvalidUsageError: in the event that the items passed to `blocks`
188
199
  are not valid [`Blocks`](/slackblocks/latest/reference/blocks).
189
200
  """
190
201
 
@@ -26,7 +26,9 @@ class Modal(ModalView):
26
26
  return dumps(self._resolve(), indent=4)
27
27
 
28
28
  def to_dict(self) -> dict[str, Any]:
29
+ """Return a fresh Slack-shaped dictionary, validating the resolved payload."""
29
30
  return self._resolve()
30
31
 
31
32
  def json(self) -> str:
33
+ """Return the validated Slack-shaped payload as indented JSON."""
32
34
  return dumps(self._resolve(), indent=4)
@@ -6,6 +6,7 @@ See: <https://api.slack.com/reference/block-kit/composition-objects>.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ import math
9
10
  import re
10
11
  from abc import ABC, abstractmethod
11
12
  from enum import Enum
@@ -19,7 +20,6 @@ from slackblocks._limits import (
19
20
  CONFIRMATION_TEXT_MAX_LENGTH,
20
21
  CONFIRMATION_TITLE_MAX_LENGTH,
21
22
  CONVERSATION_FILTER_INCLUDE_MIN_ITEMS,
22
- DATA_TABLE_CELL_TEXT_MIN_LENGTH,
23
23
  DATA_VISUALIZATION_AXIS_LABEL_MAX_LENGTH,
24
24
  DATA_VISUALIZATION_CATEGORIES_MAX_ITEMS,
25
25
  DATA_VISUALIZATION_CATEGORIES_MIN_ITEMS,
@@ -130,7 +130,7 @@ class Text(CompositionObject):
130
130
  names, user names will not automatically be rendered as links.
131
131
 
132
132
  Throws:
133
- InvalidUsageException: if the provided `text` fails validation.
133
+ InvalidUsageError: if the provided `text` fails validation.
134
134
  """
135
135
 
136
136
  def __init__(
@@ -641,7 +641,7 @@ class ConversationFilter(CompositionObject):
641
641
  exclude_bot_users: whether to remove bot users from the list of conversations.
642
642
 
643
643
  Throws:
644
- InvalidUsageException: in the event that the user provides none of `include`,
644
+ InvalidUsageError: in the event that the user provides none of `include`,
645
645
  `exclude_external_shared_channels`, or `exclude_bot_users` arguments.
646
646
  """
647
647
 
@@ -907,7 +907,7 @@ class RawText:
907
907
  escaped into text format (e.g. `:smile:`).
908
908
 
909
909
  Throws:
910
- InvalidUsageException: if the provided `text` fails validation.
910
+ InvalidUsageError: if the provided `text` fails validation.
911
911
  """
912
912
 
913
913
  def __init__(
@@ -931,14 +931,14 @@ class RawText:
931
931
 
932
932
  class RawNumber(RenderableMixin):
933
933
  """
934
- A numeric cell for a
934
+ A numeric cell for a table or
935
935
  [`DataTableBlock`](/slackblocks/latest/reference/blocks/#blocks.DataTableBlock).
936
936
 
937
937
  See: <https://docs.slack.dev/reference/block-kit/blocks/data-table-block>.
938
938
 
939
939
  Args:
940
940
  value: the numeric value Slack uses for sorting and display.
941
- text: the non-empty text Slack displays in the cell.
941
+ text: the non-empty text Slack displays.
942
942
 
943
943
  Throws:
944
944
  InvalidUsageError: if any of the provided arguments fail validation.
@@ -947,11 +947,11 @@ class RawNumber(RenderableMixin):
947
947
  def __init__(self, value: int | float, text: str) -> None:
948
948
  if isinstance(value, bool) or not isinstance(value, int | float):
949
949
  raise TypeMismatchError("`value` must be a number.")
950
+ if isinstance(value, float) and not math.isfinite(value):
951
+ raise InvalidUsageError("`value` must be finite.")
950
952
  self.type = "raw_number"
951
953
  self.value = value
952
- self.text = validate_string_nonnull(
953
- text, field_name="text", min_length=DATA_TABLE_CELL_TEXT_MIN_LENGTH
954
- )
954
+ self.text = validate_string_nonnull(text, field_name="text", min_length=1)
955
955
 
956
956
  def _resolve(self) -> dict[str, Any]:
957
957
  return {"type": self.type, "value": self.value, "text": self.text}
@@ -106,6 +106,7 @@ class View:
106
106
  )
107
107
 
108
108
  def to_dict(self) -> dict[str, Any]:
109
+ """Return a fresh Slack-shaped dictionary, validating the resolved payload."""
109
110
  return self._resolve()
110
111
 
111
112
  def __repr__(self) -> str:
@@ -1695,7 +1695,7 @@ wheels = [
1695
1695
 
1696
1696
  [[package]]
1697
1697
  name = "slackblocks"
1698
- version = "3.2.0"
1698
+ version = "4.0.0"
1699
1699
  source = { editable = "." }
1700
1700
 
1701
1701
  [package.dev-dependencies]
File without changes
File without changes