python-substack 0.1.26__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.26"
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",
@@ -39,7 +39,7 @@ requests = "^2.32.0"
39
39
  python-dotenv = "^1.2.1"
40
40
  PyYAML = "^6.0"
41
41
  markdown-it-py = "^3.0"
42
- mdit-py-plugins = "^0.4"
42
+ mdit-py-plugins = ">=0.5,<0.7"
43
43
  fastmcp = { version = "^3.1.1", optional = true }
44
44
 
45
45
  [tool.poetry.extras]
@@ -47,6 +47,12 @@ mcp = ["fastmcp"]
47
47
 
48
48
  [tool.poetry.group.dev.dependencies]
49
49
  pytest = "^9.1.1"
50
+ pre-commit = "^4.6.1"
51
+
52
+ [tool.pytest.ini_options]
53
+ markers = [
54
+ "live: opt-in tests that call the live Substack service",
55
+ ]
50
56
 
51
57
  [tool.poetry.scripts]
52
58
  substack = "substack.cli:main"
@@ -3,9 +3,11 @@
3
3
  __author__ = "Paolo Mazza"
4
4
  __email__ = "mazzapaolo2019@gmail.com"
5
5
  __license__ = "MIT License"
6
- __version__ = "0.1.26"
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
@@ -12,6 +12,7 @@ from datetime import datetime
12
12
  from urllib.parse import unquote, urljoin
13
13
 
14
14
  import requests
15
+ from requests.adapters import HTTPAdapter, Retry
15
16
 
16
17
  from substack.exceptions import SubstackAPIException, SubstackRequestException
17
18
 
@@ -65,6 +66,18 @@ class Api:
65
66
  logging.getLogger().setLevel(logging.DEBUG)
66
67
 
67
68
  self._session = requests.Session()
69
+ retry = Retry(
70
+ total=4,
71
+ status=4,
72
+ backoff_factor=1,
73
+ status_forcelist=(429,),
74
+ allowed_methods=frozenset({"GET", "DELETE"}),
75
+ respect_retry_after_header=True,
76
+ raise_on_status=False,
77
+ )
78
+ adapter = HTTPAdapter(max_retries=retry)
79
+ self._session.mount("http://", adapter)
80
+ self._session.mount("https://", adapter)
68
81
 
69
82
  # Load cookies from file if provided
70
83
  # Helps with Captcha errors by reusing cookies from "local" auth, then switching to running code in the cloud
@@ -335,7 +348,6 @@ class Api:
335
348
  return Api._handle_response(response=response)
336
349
 
337
350
  def get_publication_subscriber_count(self):
338
-
339
351
  """
340
352
  Get subscriber count.
341
353
 
@@ -346,7 +358,10 @@ class Api:
346
358
  f"{self.publication_url}/publication_launch_checklist"
347
359
  )
348
360
 
349
- return Api._handle_response(response=response)["subscriberCount"]
361
+ data = Api._handle_response(response=response)
362
+ if "subscriberCount" in data:
363
+ return data["subscriberCount"]
364
+ return len(data["subscribers"])
350
365
 
351
366
  def get_published_posts(
352
367
  self, offset=0, limit=25, order_by="post_date", order_direction="desc"
@@ -553,8 +568,8 @@ class Api:
553
568
 
554
569
  """
555
570
  response = self._session.post(
556
- f"{self.publication_url}/drafts/{draft}/schedule",
557
- json={"post_date": draft_datetime.isoformat()},
571
+ f"{self.publication_url}/drafts/{draft}/scheduled_release",
572
+ json={"trigger_at": draft_datetime.isoformat()},
558
573
  )
559
574
  return Api._handle_response(response=response)
560
575
 
@@ -567,8 +582,8 @@ class Api:
567
582
  Returns:
568
583
 
569
584
  """
570
- response = self._session.post(
571
- f"{self.publication_url}/drafts/{draft}/schedule", json={"post_date": None}
585
+ response = self._session.delete(
586
+ f"{self.publication_url}/drafts/{draft}/scheduled_release"
572
587
  )
573
588
  return Api._handle_response(response=response)
574
589
 
@@ -276,6 +276,37 @@ def _drafts_get(api, args):
276
276
  print(f"{label}: {_display(value)}")
277
277
 
278
278
 
279
+ def _drafts_create(api, args):
280
+ markdown_path = Path(args.markdown_file)
281
+ markdown = markdown_path.read_text(encoding="utf-8")
282
+ title = args.title or _title_from_markdown(markdown, markdown_path.stem)
283
+ result = api.create_draft_from_markdown(
284
+ title=title,
285
+ markdown=markdown,
286
+ subtitle=args.subtitle,
287
+ audience=args.audience,
288
+ write_comment_permissions=args.write_comment_permissions,
289
+ search_engine_title=args.search_engine_title,
290
+ search_engine_description=args.search_engine_description,
291
+ slug=args.slug,
292
+ draft_section_id=args.draft_section_id,
293
+ tags=args.tags,
294
+ prepublish=False,
295
+ publish=False,
296
+ )
297
+ draft = result["draft"]
298
+ payload = {
299
+ "action": "create",
300
+ "draft_id": draft.get("id"),
301
+ "draft": draft,
302
+ "tags": result.get("tags"),
303
+ }
304
+ if args.json_output:
305
+ _print_json(payload)
306
+ else:
307
+ print(f"Created draft {draft.get('id')}: {title}")
308
+
309
+
279
310
  def _drafts_schedule(api, args):
280
311
  scheduled_at = _parse_schedule(args.at)
281
312
  result = api.schedule_draft(args.draft_id, scheduled_at)
@@ -370,6 +401,21 @@ def _build_parser():
370
401
  drafts_get.add_argument("draft_id", type=int)
371
402
  drafts_get.set_defaults(handler=_drafts_get)
372
403
 
404
+ drafts_create = draft_commands.add_parser(
405
+ "create", help="Create a draft from a Markdown file."
406
+ )
407
+ drafts_create.add_argument("markdown_file", metavar="MARKDOWN_FILE")
408
+ drafts_create.add_argument("--title")
409
+ drafts_create.add_argument("--subtitle", default="")
410
+ drafts_create.add_argument("--audience", default="everyone")
411
+ drafts_create.add_argument("--write-comment-permissions", default="everyone")
412
+ drafts_create.add_argument("--search-engine-title")
413
+ drafts_create.add_argument("--search-engine-description")
414
+ drafts_create.add_argument("--slug")
415
+ drafts_create.add_argument("--draft-section-id", type=int)
416
+ drafts_create.add_argument("--tag", action="append", dest="tags", metavar="TAG")
417
+ drafts_create.set_defaults(handler=_drafts_create)
418
+
373
419
  drafts_schedule = draft_commands.add_parser("schedule", help="Schedule a draft.")
374
420
  drafts_schedule.add_argument("draft_id", type=int)
375
421
  drafts_schedule.add_argument("--at", required=True)