python-substack 0.1.27__tar.gz → 0.2.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.
@@ -0,0 +1,273 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-substack
3
+ Version: 0.2.0
4
+ Summary: Write and safely manage Substack drafts from Markdown with Python, CLI, and MCP.
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Keywords: substack,substack-api,cli,newsletter,publishing,automation,mcp
8
+ Author: Paolo Mazza
9
+ Author-email: mazzapaolo2019@gmail.com
10
+ Requires-Python: >=3.10,<4.0
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Communications :: Email
21
+ Classifier: Topic :: Internet :: WWW/HTTP
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Provides-Extra: mcp
24
+ Requires-Dist: PyYAML (>=6.0,<7.0)
25
+ Requires-Dist: fastmcp (>=3.1.1,<4.0.0) ; extra == "mcp"
26
+ Requires-Dist: markdown-it-py (>=3.0,<4.0)
27
+ Requires-Dist: mdit-py-plugins (>=0.5,<0.7)
28
+ Requires-Dist: python-dotenv (>=1.2.1,<2.0.0)
29
+ Requires-Dist: requests (>=2.32.0,<3.0.0)
30
+ Project-URL: Changelog, https://github.com/ma2za/python-substack/blob/main/CHANGELOG.md
31
+ Project-URL: Homepage, https://github.com/ma2za/python-substack
32
+ Project-URL: Issues, https://github.com/ma2za/python-substack/issues
33
+ Project-URL: Repository, https://github.com/ma2za/python-substack
34
+ Description-Content-Type: text/markdown
35
+
36
+ # Python Substack
37
+
38
+ Write Substack posts in Markdown and safely create, inspect, schedule, and
39
+ publish them through Python, a command-line interface, or MCP.
40
+
41
+ [![PyPI](https://img.shields.io/pypi/v/python-substack)](https://pypi.org/project/python-substack/)
42
+ [![Python](https://img.shields.io/pypi/pyversions/python-substack)](https://pypi.org/project/python-substack/)
43
+ [![Tests](https://github.com/ma2za/python-substack/actions/workflows/ci.yml/badge.svg)](https://github.com/ma2za/python-substack/actions/workflows/ci.yml)
44
+ [![Release](https://github.com/ma2za/python-substack/actions/workflows/ci_publish.yml/badge.svg)](https://github.com/ma2za/python-substack/actions/workflows/ci_publish.yml)
45
+ [![License](https://img.shields.io/pypi/l/python-substack)](LICENSE)
46
+ [![Downloads](https://static.pepy.tech/badge/python-substack/month)](https://pepy.tech/project/python-substack)
47
+
48
+ > [!IMPORTANT]
49
+ > Creating and publishing are separate operations. `substack drafts create`
50
+ > always creates an unpublished draft. It never schedules, sends, publishes,
51
+ > or deletes content.
52
+
53
+ ## From Markdown to a Substack draft
54
+
55
+ Install the package:
56
+
57
+ ```bash
58
+ pip install python-substack
59
+ ```
60
+
61
+ Check the selected account and publication:
62
+
63
+ ```bash
64
+ substack status
65
+ ```
66
+
67
+ Create a safe unpublished draft, then publish only when it is ready:
68
+
69
+ ```bash
70
+ substack drafts create post.md
71
+ substack drafts publish 12345 --no-send
72
+ ```
73
+
74
+ Publishing and deletion require confirmation. Noninteractive and JSON
75
+ workflows must pass `--yes` explicitly.
76
+
77
+ Markdown source:
78
+
79
+ ![Markdown before conversion](docs/before.png)
80
+
81
+ Substack result:
82
+
83
+ ![Substack after conversion](docs/after.png)
84
+
85
+ ## What it supports
86
+
87
+ - Create rich Substack drafts from Markdown.
88
+ - Upload local images referenced by Markdown.
89
+ - Set audience, comment permissions, SEO metadata, slug, sections, and tags.
90
+ - List and inspect publications and drafts.
91
+ - Schedule, unschedule, publish, and delete drafts with explicit safeguards.
92
+ - Use stable JSON envelopes in scripts and automation.
93
+ - Authenticate with browser cookies or email and password.
94
+ - Use the same publishing workflow from Python or an optional MCP server.
95
+
96
+ ## Setup
97
+
98
+ Copy `.env.example` to `.env` and configure one authentication method:
99
+
100
+ ```env
101
+ EMAIL=
102
+ PASSWORD=
103
+ PUBLICATION_URL=
104
+ COOKIES_PATH=
105
+ COOKIES_STRING=
106
+ ```
107
+
108
+ Cookie authentication is usually more reliable when Substack requires captcha
109
+ or magic-link sign-in. See
110
+ [Authentication](docs/authentication.md) for cookie export instructions and
111
+ account-selection details.
112
+
113
+ Verify the installation without authenticating:
114
+
115
+ ```bash
116
+ substack --version
117
+ substack --help
118
+ ```
119
+
120
+ ## CLI
121
+
122
+ Create a draft with metadata:
123
+
124
+ ```bash
125
+ substack --json drafts create post.md \
126
+ --title "My Post" \
127
+ --subtitle "Optional subtitle" \
128
+ --tag python \
129
+ --tag substack \
130
+ --slug my-post \
131
+ --search-engine-title "SEO title" \
132
+ --search-engine-description "SEO description"
133
+ ```
134
+
135
+ Inspect publications and drafts:
136
+
137
+ ```bash
138
+ substack publications list
139
+ substack drafts list --limit 10
140
+ substack drafts get 12345
141
+ substack --publication-url https://example.substack.com drafts list
142
+ ```
143
+
144
+ Manage scheduling:
145
+
146
+ ```bash
147
+ substack drafts schedule 12345 --at 2026-08-01T09:00:00+03:00
148
+ substack drafts unschedule 12345
149
+ ```
150
+
151
+ Publish or delete intentionally:
152
+
153
+ ```bash
154
+ substack drafts publish 12345 --no-send
155
+ substack drafts delete 12345 --yes
156
+ ```
157
+
158
+ Global options such as `--json`, `--cookies`, and `--publication-url` must
159
+ appear before the command:
160
+
161
+ ```bash
162
+ substack --json drafts list
163
+ substack --cookies cookies.json --json status
164
+ ```
165
+
166
+ The original standalone commands remain supported. See
167
+ [Legacy CLI commands](docs/legacy-cli.md).
168
+
169
+ ## Python
170
+
171
+ ```python
172
+ import os
173
+
174
+ from dotenv import load_dotenv
175
+ from substack import Api
176
+
177
+ load_dotenv()
178
+
179
+ api = Api(
180
+ email=os.getenv("EMAIL"),
181
+ password=os.getenv("PASSWORD"),
182
+ publication_url=os.getenv("PUBLICATION_URL"),
183
+ )
184
+
185
+ result = api.create_draft_from_markdown(
186
+ title="Shipping with Python",
187
+ subtitle="A short note from a script",
188
+ markdown="""
189
+ # Hello
190
+
191
+ This draft was created from **Markdown**.
192
+
193
+ ![Alt text](https://example.com/image.png "Image caption")
194
+ """,
195
+ tags=["python", "automation"],
196
+ slug="shipping-with-python",
197
+ )
198
+
199
+ print(result["draft"]["id"])
200
+ ```
201
+
202
+ `create_draft_from_markdown` creates a draft by default. It publishes only when
203
+ `publish=True` is passed.
204
+
205
+ For direct ProseMirror node construction, see the
206
+ [low-level Python API](docs/low-level-api.md). YAML workflows are documented in
207
+ [YAML drafts](docs/yaml.md).
208
+
209
+ ## Markdown
210
+
211
+ Supported Markdown includes headings, paragraphs, bold, italic, inline code,
212
+ strikethrough, superscript, subscript, links, images, linked images, image
213
+ captions, code blocks, blockquotes, ordered and unordered lists, horizontal
214
+ rules, footnotes, LaTeX math, pull quotes, and callouts.
215
+
216
+ ```python
217
+ from substack.post import Post
218
+
219
+ post = Post("Title", "Subtitle", user_id=1)
220
+ post.from_markdown(
221
+ """
222
+ # Heading
223
+
224
+ Paragraph with **bold**, *italic*, `code`, and [links](https://example.com).
225
+ """
226
+ )
227
+ ```
228
+
229
+ Pass `api=` to upload local images while rendering:
230
+
231
+ ```python
232
+ post.from_markdown(markdown_content, api=api)
233
+ ```
234
+
235
+ See the complete [Markdown reference](docs/markdown.md).
236
+
237
+ ## MCP
238
+
239
+ Install and run the optional MCP server:
240
+
241
+ ```bash
242
+ pip install "python-substack[mcp]"
243
+ substack-mcp
244
+ ```
245
+
246
+ The MCP tools use the same environment variables and SDK behavior as the CLI.
247
+ See [MCP server](docs/mcp.md) for the tool list and safety notes.
248
+
249
+ ## Project documentation
250
+
251
+ - [Authentication](docs/authentication.md)
252
+ - [Markdown reference](docs/markdown.md)
253
+ - [Legacy CLI commands](docs/legacy-cli.md)
254
+ - [Low-level Python API](docs/low-level-api.md)
255
+ - [YAML drafts](docs/yaml.md)
256
+ - [MCP server](docs/mcp.md)
257
+ - [Compatibility policy](docs/compatibility.md)
258
+ - [Contributing](CONTRIBUTING.md)
259
+ - [Security policy](SECURITY.md)
260
+ - [Changelog](CHANGELOG.md)
261
+
262
+ ## Compatibility
263
+
264
+ The project preserves existing Python APIs, console commands, CLI behavior,
265
+ environment variables, JSON keys, and MCP tool signatures through the 1.x
266
+ series. Additive capabilities may be introduced. See the
267
+ [compatibility policy](docs/compatibility.md).
268
+
269
+ ## Disclaimer
270
+
271
+ This project is not affiliated with Substack. It uses undocumented Substack
272
+ interfaces that may change without notice.
273
+
@@ -0,0 +1,237 @@
1
+ # Python Substack
2
+
3
+ Write Substack posts in Markdown and safely create, inspect, schedule, and
4
+ publish them through Python, a command-line interface, or MCP.
5
+
6
+ [![PyPI](https://img.shields.io/pypi/v/python-substack)](https://pypi.org/project/python-substack/)
7
+ [![Python](https://img.shields.io/pypi/pyversions/python-substack)](https://pypi.org/project/python-substack/)
8
+ [![Tests](https://github.com/ma2za/python-substack/actions/workflows/ci.yml/badge.svg)](https://github.com/ma2za/python-substack/actions/workflows/ci.yml)
9
+ [![Release](https://github.com/ma2za/python-substack/actions/workflows/ci_publish.yml/badge.svg)](https://github.com/ma2za/python-substack/actions/workflows/ci_publish.yml)
10
+ [![License](https://img.shields.io/pypi/l/python-substack)](LICENSE)
11
+ [![Downloads](https://static.pepy.tech/badge/python-substack/month)](https://pepy.tech/project/python-substack)
12
+
13
+ > [!IMPORTANT]
14
+ > Creating and publishing are separate operations. `substack drafts create`
15
+ > always creates an unpublished draft. It never schedules, sends, publishes,
16
+ > or deletes content.
17
+
18
+ ## From Markdown to a Substack draft
19
+
20
+ Install the package:
21
+
22
+ ```bash
23
+ pip install python-substack
24
+ ```
25
+
26
+ Check the selected account and publication:
27
+
28
+ ```bash
29
+ substack status
30
+ ```
31
+
32
+ Create a safe unpublished draft, then publish only when it is ready:
33
+
34
+ ```bash
35
+ substack drafts create post.md
36
+ substack drafts publish 12345 --no-send
37
+ ```
38
+
39
+ Publishing and deletion require confirmation. Noninteractive and JSON
40
+ workflows must pass `--yes` explicitly.
41
+
42
+ Markdown source:
43
+
44
+ ![Markdown before conversion](docs/before.png)
45
+
46
+ Substack result:
47
+
48
+ ![Substack after conversion](docs/after.png)
49
+
50
+ ## What it supports
51
+
52
+ - Create rich Substack drafts from Markdown.
53
+ - Upload local images referenced by Markdown.
54
+ - Set audience, comment permissions, SEO metadata, slug, sections, and tags.
55
+ - List and inspect publications and drafts.
56
+ - Schedule, unschedule, publish, and delete drafts with explicit safeguards.
57
+ - Use stable JSON envelopes in scripts and automation.
58
+ - Authenticate with browser cookies or email and password.
59
+ - Use the same publishing workflow from Python or an optional MCP server.
60
+
61
+ ## Setup
62
+
63
+ Copy `.env.example` to `.env` and configure one authentication method:
64
+
65
+ ```env
66
+ EMAIL=
67
+ PASSWORD=
68
+ PUBLICATION_URL=
69
+ COOKIES_PATH=
70
+ COOKIES_STRING=
71
+ ```
72
+
73
+ Cookie authentication is usually more reliable when Substack requires captcha
74
+ or magic-link sign-in. See
75
+ [Authentication](docs/authentication.md) for cookie export instructions and
76
+ account-selection details.
77
+
78
+ Verify the installation without authenticating:
79
+
80
+ ```bash
81
+ substack --version
82
+ substack --help
83
+ ```
84
+
85
+ ## CLI
86
+
87
+ Create a draft with metadata:
88
+
89
+ ```bash
90
+ substack --json drafts create post.md \
91
+ --title "My Post" \
92
+ --subtitle "Optional subtitle" \
93
+ --tag python \
94
+ --tag substack \
95
+ --slug my-post \
96
+ --search-engine-title "SEO title" \
97
+ --search-engine-description "SEO description"
98
+ ```
99
+
100
+ Inspect publications and drafts:
101
+
102
+ ```bash
103
+ substack publications list
104
+ substack drafts list --limit 10
105
+ substack drafts get 12345
106
+ substack --publication-url https://example.substack.com drafts list
107
+ ```
108
+
109
+ Manage scheduling:
110
+
111
+ ```bash
112
+ substack drafts schedule 12345 --at 2026-08-01T09:00:00+03:00
113
+ substack drafts unschedule 12345
114
+ ```
115
+
116
+ Publish or delete intentionally:
117
+
118
+ ```bash
119
+ substack drafts publish 12345 --no-send
120
+ substack drafts delete 12345 --yes
121
+ ```
122
+
123
+ Global options such as `--json`, `--cookies`, and `--publication-url` must
124
+ appear before the command:
125
+
126
+ ```bash
127
+ substack --json drafts list
128
+ substack --cookies cookies.json --json status
129
+ ```
130
+
131
+ The original standalone commands remain supported. See
132
+ [Legacy CLI commands](docs/legacy-cli.md).
133
+
134
+ ## Python
135
+
136
+ ```python
137
+ import os
138
+
139
+ from dotenv import load_dotenv
140
+ from substack import Api
141
+
142
+ load_dotenv()
143
+
144
+ api = Api(
145
+ email=os.getenv("EMAIL"),
146
+ password=os.getenv("PASSWORD"),
147
+ publication_url=os.getenv("PUBLICATION_URL"),
148
+ )
149
+
150
+ result = api.create_draft_from_markdown(
151
+ title="Shipping with Python",
152
+ subtitle="A short note from a script",
153
+ markdown="""
154
+ # Hello
155
+
156
+ This draft was created from **Markdown**.
157
+
158
+ ![Alt text](https://example.com/image.png "Image caption")
159
+ """,
160
+ tags=["python", "automation"],
161
+ slug="shipping-with-python",
162
+ )
163
+
164
+ print(result["draft"]["id"])
165
+ ```
166
+
167
+ `create_draft_from_markdown` creates a draft by default. It publishes only when
168
+ `publish=True` is passed.
169
+
170
+ For direct ProseMirror node construction, see the
171
+ [low-level Python API](docs/low-level-api.md). YAML workflows are documented in
172
+ [YAML drafts](docs/yaml.md).
173
+
174
+ ## Markdown
175
+
176
+ Supported Markdown includes headings, paragraphs, bold, italic, inline code,
177
+ strikethrough, superscript, subscript, links, images, linked images, image
178
+ captions, code blocks, blockquotes, ordered and unordered lists, horizontal
179
+ rules, footnotes, LaTeX math, pull quotes, and callouts.
180
+
181
+ ```python
182
+ from substack.post import Post
183
+
184
+ post = Post("Title", "Subtitle", user_id=1)
185
+ post.from_markdown(
186
+ """
187
+ # Heading
188
+
189
+ Paragraph with **bold**, *italic*, `code`, and [links](https://example.com).
190
+ """
191
+ )
192
+ ```
193
+
194
+ Pass `api=` to upload local images while rendering:
195
+
196
+ ```python
197
+ post.from_markdown(markdown_content, api=api)
198
+ ```
199
+
200
+ See the complete [Markdown reference](docs/markdown.md).
201
+
202
+ ## MCP
203
+
204
+ Install and run the optional MCP server:
205
+
206
+ ```bash
207
+ pip install "python-substack[mcp]"
208
+ substack-mcp
209
+ ```
210
+
211
+ The MCP tools use the same environment variables and SDK behavior as the CLI.
212
+ See [MCP server](docs/mcp.md) for the tool list and safety notes.
213
+
214
+ ## Project documentation
215
+
216
+ - [Authentication](docs/authentication.md)
217
+ - [Markdown reference](docs/markdown.md)
218
+ - [Legacy CLI commands](docs/legacy-cli.md)
219
+ - [Low-level Python API](docs/low-level-api.md)
220
+ - [YAML drafts](docs/yaml.md)
221
+ - [MCP server](docs/mcp.md)
222
+ - [Compatibility policy](docs/compatibility.md)
223
+ - [Contributing](CONTRIBUTING.md)
224
+ - [Security policy](SECURITY.md)
225
+ - [Changelog](CHANGELOG.md)
226
+
227
+ ## Compatibility
228
+
229
+ The project preserves existing Python APIs, console commands, CLI behavior,
230
+ environment variables, JSON keys, and MCP tool signatures through the 1.x
231
+ series. Additive capabilities may be introduced. See the
232
+ [compatibility policy](docs/compatibility.md).
233
+
234
+ ## Disclaimer
235
+
236
+ This project is not affiliated with Substack. It uses undocumented Substack
237
+ interfaces that may change without notice.
@@ -1,7 +1,7 @@
1
1
  [tool.poetry]
2
2
  name = "python-substack"
3
- version = "0.1.27"
4
- description = "A Python SDK and CLI for managing Substack publications and drafts."
3
+ version = "0.2.0"
4
+ description = "Write and safely manage Substack drafts from Markdown with Python, CLI, and MCP."
5
5
  authors = ["Paolo Mazza <mazzapaolo2019@gmail.com>"]
6
6
  license = "MIT"
7
7
  packages = [
@@ -16,7 +16,7 @@ homepage = "https://github.com/ma2za/python-substack"
16
16
 
17
17
  keywords = ["substack", "substack-api", "cli", "newsletter", "publishing", "automation", "mcp"]
18
18
  classifiers = [
19
- "Development Status :: 3 - Alpha",
19
+ "Development Status :: 4 - Beta",
20
20
  "Intended Audience :: Developers",
21
21
  "Programming Language :: Python :: 3",
22
22
  "Programming Language :: Python :: 3.10",
@@ -47,6 +47,7 @@ mcp = ["fastmcp"]
47
47
 
48
48
  [tool.poetry.group.dev.dependencies]
49
49
  pytest = "^9.1.1"
50
+ pre-commit = "^4.6.1"
50
51
 
51
52
  [tool.pytest.ini_options]
52
53
  markers = [
@@ -3,9 +3,11 @@
3
3
  __author__ = "Paolo Mazza"
4
4
  __email__ = "mazzapaolo2019@gmail.com"
5
5
  __license__ = "MIT License"
6
- __version__ = "0.1.27"
6
+ __version__ = "0.2.0"
7
7
  __url__ = "https://github.com/ma2za/python-substack"
8
8
  __download_url__ = "https://pypi.python.org/pypi/python-substack"
9
- __description__ = "A Python SDK and CLI for managing Substack publications and drafts"
9
+ __description__ = (
10
+ "Write and safely manage Substack drafts from Markdown with Python, CLI, and MCP."
11
+ )
10
12
 
11
13
  from .api import Api
@@ -348,7 +348,6 @@ class Api:
348
348
  return Api._handle_response(response=response)
349
349
 
350
350
  def get_publication_subscriber_count(self):
351
-
352
351
  """
353
352
  Get subscriber count.
354
353
 
@@ -10,8 +10,8 @@ from typing import Dict, List
10
10
 
11
11
  __all__ = ["Post", "parse_inline", "tokens_to_text_nodes"]
12
12
 
13
- from substack.exceptions import SectionNotExistsException
14
13
  from substack import nodes
14
+ from substack.exceptions import SectionNotExistsException
15
15
 
16
16
 
17
17
  def tokens_to_text_nodes(tokens: List[Dict]) -> List[Dict]:
@@ -61,12 +61,12 @@ def parse_inline(text: str) -> List[Dict]:
61
61
  tokens = []
62
62
 
63
63
  # Pattern order matters: code > links > bold+italic > bold > italic > strikethrough
64
- code_pattern = r'`([^`]+)`'
65
- link_pattern = r'\[([^\]]+)\]\(([^)]+)\)'
66
- bold_italic_pattern = r'\*\*\*([^*]+)\*\*\*'
67
- bold_pattern = r'\*\*([^*]+)\*\*'
68
- italic_pattern = r'(?<!\*)\*([^*]+)\*(?!\*)' # Not preceded or followed by *
69
- strikethrough_pattern = r'~~([^~]+)~~'
64
+ code_pattern = r"`([^`]+)`"
65
+ link_pattern = r"\[([^\]]+)\]\(([^)]+)\)"
66
+ bold_italic_pattern = r"\*\*\*([^*]+)\*\*\*"
67
+ bold_pattern = r"\*\*([^*]+)\*\*"
68
+ italic_pattern = r"(?<!\*)\*([^*]+)\*(?!\*)" # Not preceded or followed by *
69
+ strikethrough_pattern = r"~~([^~]+)~~"
70
70
 
71
71
  # Find all matches with their positions
72
72
  matches = []
@@ -79,14 +79,18 @@ def parse_inline(text: str) -> List[Dict]:
79
79
  for match in re.finditer(link_pattern, text):
80
80
  # Skip if it's an image link (starts with ![)
81
81
  # But do NOT skip normal links at position 0.
82
- if match.start() == 0 or text[match.start()-1:match.start()+1] != "![":
82
+ if match.start() == 0 or text[match.start() - 1 : match.start() + 1] != "![":
83
83
  if not any(start <= match.start() < end for start, end, _, _, _ in matches):
84
- matches.append((match.start(), match.end(), "link", match.group(1), match.group(2)))
84
+ matches.append(
85
+ (match.start(), match.end(), "link", match.group(1), match.group(2))
86
+ )
85
87
 
86
88
  # Bold+italic combo
87
89
  for match in re.finditer(bold_italic_pattern, text):
88
90
  if not any(start <= match.start() < end for start, end, _, _, _ in matches):
89
- matches.append((match.start(), match.end(), "bold_italic", match.group(1), None))
91
+ matches.append(
92
+ (match.start(), match.end(), "bold_italic", match.group(1), None)
93
+ )
90
94
 
91
95
  # Bold
92
96
  for match in re.finditer(bold_pattern, text):
@@ -101,7 +105,9 @@ def parse_inline(text: str) -> List[Dict]:
101
105
  # Strikethrough
102
106
  for match in re.finditer(strikethrough_pattern, text):
103
107
  if not any(start <= match.start() < end for start, end, _, _, _ in matches):
104
- matches.append((match.start(), match.end(), "strikethrough", match.group(1), None))
108
+ matches.append(
109
+ (match.start(), match.end(), "strikethrough", match.group(1), None)
110
+ )
105
111
 
106
112
  # Sort matches by position
107
113
  matches.sort(key=lambda x: x[0])
@@ -115,35 +121,24 @@ def parse_inline(text: str) -> List[Dict]:
115
121
 
116
122
  # Add the formatted content
117
123
  if match_type == "code":
118
- tokens.append({
119
- "content": content,
120
- "marks": [{"type": "code"}]
121
- })
124
+ tokens.append({"content": content, "marks": [{"type": "code"}]})
122
125
  elif match_type == "link":
123
- tokens.append({
124
- "content": content,
125
- "marks": [{"type": "link", "attrs": {"href": url}}]
126
- })
126
+ tokens.append(
127
+ {
128
+ "content": content,
129
+ "marks": [{"type": "link", "attrs": {"href": url}}],
130
+ }
131
+ )
127
132
  elif match_type == "bold_italic":
128
- tokens.append({
129
- "content": content,
130
- "marks": [{"type": "strong"}, {"type": "em"}]
131
- })
133
+ tokens.append(
134
+ {"content": content, "marks": [{"type": "strong"}, {"type": "em"}]}
135
+ )
132
136
  elif match_type == "bold":
133
- tokens.append({
134
- "content": content,
135
- "marks": [{"type": "strong"}]
136
- })
137
+ tokens.append({"content": content, "marks": [{"type": "strong"}]})
137
138
  elif match_type == "italic":
138
- tokens.append({
139
- "content": content,
140
- "marks": [{"type": "em"}]
141
- })
139
+ tokens.append({"content": content, "marks": [{"type": "em"}]})
142
140
  elif match_type == "strikethrough":
143
- tokens.append({
144
- "content": content,
145
- "marks": [{"type": "strikethrough"}]
146
- })
141
+ tokens.append({"content": content, "marks": [{"type": "strikethrough"}]})
147
142
 
148
143
  last_pos = end
149
144
 
@@ -583,7 +578,9 @@ class Post:
583
578
  for chunk in re.split(r"\n\s*\n", content):
584
579
  chunk = chunk.strip()
585
580
  if chunk:
586
- paragraphs.append(nodes.paragraph(tokens_to_text_nodes(parse_inline(chunk))))
581
+ paragraphs.append(
582
+ nodes.paragraph(tokens_to_text_nodes(parse_inline(chunk)))
583
+ )
587
584
  elif isinstance(content, list):
588
585
  # Accept either parse_inline tokens ({"content": ...}) or text nodes.
589
586
  if content and content[0].get("type") == "text":