python-substack 0.3.0__tar.gz → 0.4.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-substack
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Write and safely manage Substack drafts from Markdown with Python, CLI, and MCP.
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -263,6 +263,7 @@ See [MCP server](docs/mcp.md) for the tool list and safety notes.
263
263
  - [Low-level Python API](docs/low-level-api.md)
264
264
  - [YAML drafts](docs/yaml.md)
265
265
  - [MCP server](docs/mcp.md)
266
+ - [Gemini CLI with Vertex AI](docs/gemini-vertex-ai.md)
266
267
  - [Safety and publishing behavior](docs/safety.md)
267
268
  - [Troubleshooting](docs/troubleshooting.md)
268
269
  - [Compatibility policy](docs/compatibility.md)
@@ -227,6 +227,7 @@ See [MCP server](docs/mcp.md) for the tool list and safety notes.
227
227
  - [Low-level Python API](docs/low-level-api.md)
228
228
  - [YAML drafts](docs/yaml.md)
229
229
  - [MCP server](docs/mcp.md)
230
+ - [Gemini CLI with Vertex AI](docs/gemini-vertex-ai.md)
230
231
  - [Safety and publishing behavior](docs/safety.md)
231
232
  - [Troubleshooting](docs/troubleshooting.md)
232
233
  - [Compatibility policy](docs/compatibility.md)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "python-substack"
3
- version = "0.3.0"
3
+ version = "0.4.0"
4
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"
@@ -3,7 +3,7 @@
3
3
  __author__ = "Paolo Mazza"
4
4
  __email__ = "mazzapaolo2019@gmail.com"
5
5
  __license__ = "MIT License"
6
- __version__ = "0.3.0"
6
+ __version__ = "0.4.0"
7
7
  __url__ = "https://github.com/ma2za/python-substack"
8
8
  __download_url__ = "https://pypi.python.org/pypi/python-substack"
9
9
  __description__ = (
@@ -163,55 +163,24 @@ async def post_draft_from_markdown(
163
163
  This docstring example is meant to mirror the YAML-driven workflow and show how to decompose the same operations into explicit tool calls.
164
164
  """
165
165
  client = get_api()
166
- user_id = client.get_user_id()
167
166
 
168
- post = Post(
167
+ return client.create_draft_from_markdown(
169
168
  title=title,
170
- subtitle=subtitle or "",
171
- user_id=user_id,
169
+ markdown=markdown,
170
+ subtitle=subtitle,
172
171
  audience=audience,
173
172
  write_comment_permissions=write_comment_permissions,
173
+ search_engine_title=search_engine_title,
174
+ search_engine_description=search_engine_description,
175
+ slug=slug,
176
+ draft_section_id=draft_section_id,
177
+ tags=tags,
178
+ prepublish=prepublish,
179
+ publish=publish,
180
+ send=send,
181
+ share_automatically=share_automatically,
174
182
  )
175
183
 
176
- post.from_markdown(markdown, api=client)
177
-
178
- draft = client.post_draft(post.get_draft())
179
-
180
- update_payload: Dict[str, Any] = {}
181
- if search_engine_title:
182
- update_payload["search_engine_title"] = search_engine_title
183
- if search_engine_description:
184
- update_payload["search_engine_description"] = search_engine_description
185
- if slug:
186
- update_payload["slug"] = slug
187
- if draft_section_id is not None:
188
- update_payload["draft_section_id"] = draft_section_id
189
-
190
- if update_payload:
191
- draft = client.put_draft(draft.get("id"), **update_payload)
192
-
193
- tags_list = _normalize_tags(tags)
194
- tags_result = None
195
- if tags_list:
196
- tags_result = client.add_tags_to_post(draft.get("id"), tags_list)
197
-
198
- prepublish_result = None
199
- if prepublish:
200
- prepublish_result = client.prepublish_draft(draft.get("id"))
201
-
202
- publish_result = None
203
- if publish:
204
- publish_result = client.publish_draft(
205
- draft.get("id"), send=send, share_automatically=share_automatically
206
- )
207
-
208
- return {
209
- "draft": draft,
210
- "tags": tags_result,
211
- "prepublish": prepublish_result,
212
- "publish": publish_result,
213
- }
214
-
215
184
 
216
185
  @mcp.tool()
217
186
  async def put_draft(
@@ -269,7 +238,11 @@ async def publish_draft(
269
238
  send: bool = True,
270
239
  share_automatically: bool = False,
271
240
  ) -> Dict[str, Any]:
272
- """Publish a draft to live post state.
241
+ """Publish a draft to live post state. (Legacy compatibility interface).
242
+
243
+ This tool remains for backward compatibility. It is recommended to use
244
+ `publish_draft_checked` instead, which provides a safer publishing path
245
+ with explicit confirmation and prepublish validation.
273
246
 
274
247
  Args:
275
248
  draft_id: target draft identifier.
@@ -285,6 +258,126 @@ async def publish_draft(
285
258
  )
286
259
 
287
260
 
261
+ @mcp.tool()
262
+ async def publish_draft_checked(
263
+ draft_id: int,
264
+ confirm: bool = False,
265
+ send: bool = False,
266
+ share_automatically: bool = False,
267
+ ) -> Dict[str, Any]:
268
+ """A safer publishing path that requires confirmation and runs prepublish checks.
269
+
270
+ Args:
271
+ draft_id: target draft identifier.
272
+ confirm: Must be True to proceed with publication.
273
+ send: if False then do not send email to subscribers. Defaults to False.
274
+ share_automatically: whether to auto-share.
275
+
276
+ Returns:
277
+ Response from Substack `publish_draft`.
278
+ """
279
+ if not confirm:
280
+ raise ValueError("Publishing rejected: confirm parameter must be True.")
281
+
282
+ client = get_api()
283
+ client.prepublish_draft(draft_id)
284
+ return client.publish_draft(
285
+ draft_id, send=send, share_automatically=share_automatically
286
+ )
287
+
288
+
289
+ @mcp.tool()
290
+ async def get_status() -> Dict[str, Any]:
291
+ """Get the authentication status and basic user information.
292
+
293
+ Returns:
294
+ A dictionary containing user profile and primary publication details.
295
+ """
296
+ client = get_api()
297
+ profile = client.get_user_profile()
298
+ primary_pub = client.get_user_primary_publication()
299
+ return {"profile": profile, "primary_publication": primary_pub}
300
+
301
+
302
+ @mcp.tool()
303
+ async def list_publications() -> List[Dict[str, Any]]:
304
+ """List all publications available to the authenticated user.
305
+
306
+ Returns:
307
+ A list of publications.
308
+ """
309
+ client = get_api()
310
+ return client.get_user_publications()
311
+
312
+
313
+ @mcp.tool()
314
+ async def list_drafts(
315
+ filter: str = "draft", offset: int = 0, limit: int = 25
316
+ ) -> List[Dict[str, Any]]:
317
+ """List drafts for the current publication.
318
+
319
+ Args:
320
+ filter: Filter string, defaults to "draft".
321
+ offset: Pagination offset.
322
+ limit: Max number of drafts to return.
323
+
324
+ Returns:
325
+ A list of drafts.
326
+ """
327
+ client = get_api()
328
+ return client.get_drafts(filter=filter, offset=offset, limit=limit)
329
+
330
+
331
+ @mcp.tool()
332
+ async def get_draft(draft_id: int) -> Dict[str, Any]:
333
+ """Get a specific draft by its ID.
334
+
335
+ Args:
336
+ draft_id: The identifier of the draft.
337
+
338
+ Returns:
339
+ The draft details.
340
+ """
341
+ client = get_api()
342
+ return client.get_draft(draft_id)
343
+
344
+
345
+ @mcp.tool()
346
+ async def schedule_draft(draft_id: int, at: str) -> Dict[str, Any]:
347
+ """Schedule a draft for release.
348
+
349
+ Args:
350
+ draft_id: target draft identifier.
351
+ at: ISO 8601 formatted datetime string (e.g., "2024-01-01T12:00:00Z").
352
+
353
+ Returns:
354
+ API response dict for the scheduled draft.
355
+ """
356
+ from datetime import datetime
357
+
358
+ try:
359
+ draft_datetime = datetime.fromisoformat(at.replace("Z", "+00:00"))
360
+ except ValueError as e:
361
+ raise ValueError(f"Invalid ISO datetime string for 'at': {e}")
362
+
363
+ client = get_api()
364
+ return client.schedule_draft(draft_id, draft_datetime)
365
+
366
+
367
+ @mcp.tool()
368
+ async def unschedule_draft(draft_id: int) -> Dict[str, Any]:
369
+ """Unschedule a previously scheduled draft.
370
+
371
+ Args:
372
+ draft_id: target draft identifier.
373
+
374
+ Returns:
375
+ API response dict for unscheduling.
376
+ """
377
+ client = get_api()
378
+ return client.unschedule_draft(draft_id)
379
+
380
+
288
381
  def main() -> None:
289
382
  mcp.run(transport="stdio")
290
383
 
File without changes