python-substack 0.1.26__py3-none-any.whl → 0.1.27__py3-none-any.whl
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.
- {python_substack-0.1.26.dist-info → python_substack-0.1.27.dist-info}/METADATA +52 -6
- python_substack-0.1.27.dist-info/RECORD +13 -0
- substack/__init__.py +1 -1
- substack/api.py +21 -5
- substack/cli.py +46 -0
- substack/mdrender.py +39 -1
- substack/nodes.py +35 -0
- python_substack-0.1.26.dist-info/RECORD +0 -13
- {python_substack-0.1.26.dist-info → python_substack-0.1.27.dist-info}/WHEEL +0 -0
- {python_substack-0.1.26.dist-info → python_substack-0.1.27.dist-info}/entry_points.txt +0 -0
- {python_substack-0.1.26.dist-info → python_substack-0.1.27.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-substack
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.27
|
|
4
4
|
Summary: A Python SDK and CLI for managing Substack publications and drafts.
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -24,7 +24,7 @@ Provides-Extra: mcp
|
|
|
24
24
|
Requires-Dist: PyYAML (>=6.0,<7.0)
|
|
25
25
|
Requires-Dist: fastmcp (>=3.1.1,<4.0.0) ; extra == "mcp"
|
|
26
26
|
Requires-Dist: markdown-it-py (>=3.0,<4.0)
|
|
27
|
-
Requires-Dist: mdit-py-plugins (>=0.
|
|
27
|
+
Requires-Dist: mdit-py-plugins (>=0.5,<0.7)
|
|
28
28
|
Requires-Dist: python-dotenv (>=1.2.1,<2.0.0)
|
|
29
29
|
Requires-Dist: requests (>=2.32.0,<3.0.0)
|
|
30
30
|
Project-URL: Changelog, https://github.com/ma2za/python-substack/blob/main/CHANGELOG.md
|
|
@@ -87,6 +87,33 @@ Newer Substack accounts may only have magic-link sign-in enabled. To set a passw
|
|
|
87
87
|
|
|
88
88
|
## CLI Operations
|
|
89
89
|
|
|
90
|
+
Create a draft from Markdown without publishing it:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
substack drafts create post.md
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Set metadata and repeat `--tag` to attach multiple tags:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
substack --json drafts create post.md \
|
|
100
|
+
--title "My Post" \
|
|
101
|
+
--subtitle "Optional subtitle" \
|
|
102
|
+
--tag python \
|
|
103
|
+
--tag substack \
|
|
104
|
+
--slug my-post \
|
|
105
|
+
--search-engine-title "SEO title" \
|
|
106
|
+
--search-engine-description "SEO description"
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Creation and publishing are intentionally separate. Use the returned draft ID
|
|
110
|
+
when the draft is ready:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
substack drafts create post.md
|
|
114
|
+
substack drafts publish 12345 --no-send
|
|
115
|
+
```
|
|
116
|
+
|
|
90
117
|
Check authentication, the selected publication, and subscriber count:
|
|
91
118
|
|
|
92
119
|
```bash
|
|
@@ -163,7 +190,9 @@ print(result["draft"]["id"])
|
|
|
163
190
|
|
|
164
191
|
`create_draft_from_markdown` creates a draft by default. It only publishes when `publish=True` is passed.
|
|
165
192
|
|
|
166
|
-
## Content Publishing CLI
|
|
193
|
+
## Legacy Content Publishing CLI
|
|
194
|
+
|
|
195
|
+
The existing standalone commands remain supported for compatibility.
|
|
167
196
|
|
|
168
197
|
Check authentication without creating a draft:
|
|
169
198
|
|
|
@@ -340,7 +369,7 @@ Paragraph with **bold**, *italic*, `code`, [links](https://example.com), and foo
|
|
|
340
369
|
)
|
|
341
370
|
```
|
|
342
371
|
|
|
343
|
-
Supported Markdown includes headings, paragraphs, bold, italic, inline code, strikethrough, links, images, linked images, image captions, code blocks, blockquotes, ordered lists, unordered lists, horizontal rules, and
|
|
372
|
+
Supported Markdown includes headings, paragraphs, bold, italic, inline code, strikethrough, superscript, subscript, links, images, linked images, image captions, code blocks, blockquotes, ordered lists, unordered lists, horizontal rules, footnotes, LaTeX math, pull quotes, and callouts. See [docs/markdown.md](docs/markdown.md) for the full reference with examples.
|
|
344
373
|
|
|
345
374
|
When an `Api` instance is passed to `from_markdown`, local image paths are uploaded before the draft is created:
|
|
346
375
|
|
|
@@ -421,11 +450,28 @@ pre-commit install
|
|
|
421
450
|
pytest
|
|
422
451
|
```
|
|
423
452
|
|
|
424
|
-
|
|
453
|
+
Run the offline suite with:
|
|
454
|
+
|
|
455
|
+
```bash
|
|
456
|
+
pytest -m "not live"
|
|
457
|
+
```
|
|
425
458
|
|
|
426
|
-
|
|
459
|
+
Live Substack tests are not part of normal CI. They are opt-in and require
|
|
460
|
+
configured credentials:
|
|
461
|
+
|
|
462
|
+
```bash
|
|
463
|
+
RUN_SUBSTACK_E2E=1 pytest -m live
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
The CLI operations smoke tests are separately opt-in. They create, inspect, and
|
|
467
|
+
delete disposable drafts but never publish them:
|
|
468
|
+
|
|
469
|
+
```bash
|
|
470
|
+
RUN_SUBSTACK_CLI_E2E=1 pytest -m live tests/substack/test_cli_end_to_end.py
|
|
471
|
+
```
|
|
427
472
|
|
|
428
473
|
Release changes are tracked in [CHANGELOG.md](CHANGELOG.md).
|
|
474
|
+
The maintainer release process is documented in [docs/releasing.md](docs/releasing.md).
|
|
429
475
|
|
|
430
476
|
## Disclaimer
|
|
431
477
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
substack/__init__.py,sha256=Wd16zqqfA6bYLntSyq_dnRIFxosqgzjbyIVcJigGQ_A,416
|
|
2
|
+
substack/api.py,sha256=tuxefoBuOvBn4Ww2VCbqkWCuX6qPOg7YM6EsLheBpV4,23213
|
|
3
|
+
substack/cli.py,sha256=JBCsCdmthjzTeDpOYBLeVKOdNYjBrPuY1o_CIsAIekk,21712
|
|
4
|
+
substack/exceptions.py,sha256=BbP5W5UpzFcM5SYIxx6snWD_Rmj7F_YjYIYC_r03gZY,911
|
|
5
|
+
substack/mdrender.py,sha256=QGJkdp1isFmhVyTRMogIabAK8OO2xQh9CrjnXZvFTY4,9308
|
|
6
|
+
substack/nodes.py,sha256=fTsGO0-lTztcXiloIjXrHVnJes9EcltGcrX-mEV2ceQ,5004
|
|
7
|
+
substack/post.py,sha256=nXeZMAZ6-lx1Qf4yuAlrEQ0lcWFYF2afai1_TxVEl0Q,19704
|
|
8
|
+
substack_mcp/mcp_server.py,sha256=gmevdc59XTBXXMTvVMpYoq66Umlqku1O_uzmw5tMy7o,8405
|
|
9
|
+
python_substack-0.1.27.dist-info/METADATA,sha256=-yH6Uq4uhgO8tUYONxf3EkMMpvXd7--dxEAMkYsHTMc,11738
|
|
10
|
+
python_substack-0.1.27.dist-info/WHEEL,sha256=kJCRJT_g0adfAJzTx2GUMmS80rTJIVHRCfG0DQgLq3o,88
|
|
11
|
+
python_substack-0.1.27.dist-info/entry_points.txt,sha256=MKPjaBUd-0PtvxsBviStsVq1c0h8JZ_qUoYEsBK1xJc,236
|
|
12
|
+
python_substack-0.1.27.dist-info/licenses/LICENSE,sha256=L6jk148I5HhhVbfUvkO3EO7eAoU5zToLio4-ApkCkxg,1062
|
|
13
|
+
python_substack-0.1.27.dist-info/RECORD,,
|
substack/__init__.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
__author__ = "Paolo Mazza"
|
|
4
4
|
__email__ = "mazzapaolo2019@gmail.com"
|
|
5
5
|
__license__ = "MIT License"
|
|
6
|
-
__version__ = "0.1.
|
|
6
|
+
__version__ = "0.1.27"
|
|
7
7
|
__url__ = "https://github.com/ma2za/python-substack"
|
|
8
8
|
__download_url__ = "https://pypi.python.org/pypi/python-substack"
|
|
9
9
|
__description__ = "A Python SDK and CLI for managing Substack publications and drafts"
|
substack/api.py
CHANGED
|
@@ -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
|
|
@@ -346,7 +359,10 @@ class Api:
|
|
|
346
359
|
f"{self.publication_url}/publication_launch_checklist"
|
|
347
360
|
)
|
|
348
361
|
|
|
349
|
-
|
|
362
|
+
data = Api._handle_response(response=response)
|
|
363
|
+
if "subscriberCount" in data:
|
|
364
|
+
return data["subscriberCount"]
|
|
365
|
+
return len(data["subscribers"])
|
|
350
366
|
|
|
351
367
|
def get_published_posts(
|
|
352
368
|
self, offset=0, limit=25, order_by="post_date", order_direction="desc"
|
|
@@ -553,8 +569,8 @@ class Api:
|
|
|
553
569
|
|
|
554
570
|
"""
|
|
555
571
|
response = self._session.post(
|
|
556
|
-
f"{self.publication_url}/drafts/{draft}/
|
|
557
|
-
json={"
|
|
572
|
+
f"{self.publication_url}/drafts/{draft}/scheduled_release",
|
|
573
|
+
json={"trigger_at": draft_datetime.isoformat()},
|
|
558
574
|
)
|
|
559
575
|
return Api._handle_response(response=response)
|
|
560
576
|
|
|
@@ -567,8 +583,8 @@ class Api:
|
|
|
567
583
|
Returns:
|
|
568
584
|
|
|
569
585
|
"""
|
|
570
|
-
response = self._session.
|
|
571
|
-
f"{self.publication_url}/drafts/{draft}/
|
|
586
|
+
response = self._session.delete(
|
|
587
|
+
f"{self.publication_url}/drafts/{draft}/scheduled_release"
|
|
572
588
|
)
|
|
573
589
|
return Api._handle_response(response=response)
|
|
574
590
|
|
substack/cli.py
CHANGED
|
@@ -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)
|
substack/mdrender.py
CHANGED
|
@@ -22,7 +22,11 @@ from typing import Dict, List, Optional
|
|
|
22
22
|
|
|
23
23
|
from markdown_it import MarkdownIt
|
|
24
24
|
from markdown_it.tree import SyntaxTreeNode
|
|
25
|
+
from mdit_py_plugins.container import container_plugin
|
|
26
|
+
from mdit_py_plugins.dollarmath import dollarmath_plugin
|
|
25
27
|
from mdit_py_plugins.footnote import footnote_plugin
|
|
28
|
+
from mdit_py_plugins.subscript import sub_plugin
|
|
29
|
+
from mdit_py_plugins.superscript import superscript_plugin
|
|
26
30
|
|
|
27
31
|
from substack import nodes
|
|
28
32
|
from substack.nodes import MarkType, NodeType
|
|
@@ -31,11 +35,25 @@ _MARK_FOR = {
|
|
|
31
35
|
"strong": {"type": MarkType.STRONG},
|
|
32
36
|
"em": {"type": MarkType.EM},
|
|
33
37
|
"s": {"type": MarkType.STRIKETHROUGH},
|
|
38
|
+
"sup": {"type": MarkType.SUPERSCRIPT},
|
|
39
|
+
"sub": {"type": MarkType.SUBSCRIPT},
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
|
|
37
43
|
def _make_parser() -> MarkdownIt:
|
|
38
|
-
return
|
|
44
|
+
return (
|
|
45
|
+
MarkdownIt("commonmark")
|
|
46
|
+
.use(footnote_plugin)
|
|
47
|
+
# Pandoc-style delimiters: no whitespace just inside the dollars and no
|
|
48
|
+
# digit just outside them, so paired currency amounts ("$5 ... $10")
|
|
49
|
+
# stay plain text instead of becoming math.
|
|
50
|
+
.use(dollarmath_plugin, allow_space=False, allow_digits=False)
|
|
51
|
+
.use(sub_plugin)
|
|
52
|
+
.use(superscript_plugin)
|
|
53
|
+
.use(container_plugin, name="pullquote")
|
|
54
|
+
.use(container_plugin, name="callout")
|
|
55
|
+
.enable("strikethrough")
|
|
56
|
+
)
|
|
39
57
|
|
|
40
58
|
|
|
41
59
|
def _coalesce(out_nodes: List[Dict]) -> List[Dict]:
|
|
@@ -64,6 +82,8 @@ def _render_inline(node: SyntaxTreeNode, marks: List[Dict], ctx: Dict) -> List[D
|
|
|
64
82
|
out.append(nodes.text(child.content, marks))
|
|
65
83
|
elif t == "code_inline":
|
|
66
84
|
out.append(nodes.text(child.content, marks + [nodes.code_mark()]))
|
|
85
|
+
elif t == "math_inline":
|
|
86
|
+
out.append(nodes.latex_inline(child.content.strip()))
|
|
67
87
|
elif t in _MARK_FOR:
|
|
68
88
|
out.extend(_render_inline(child, marks + [_MARK_FOR[t]], ctx))
|
|
69
89
|
elif t == "link":
|
|
@@ -159,10 +179,28 @@ def _render_block(node: SyntaxTreeNode, api, ctx: Dict) -> List[Dict]:
|
|
|
159
179
|
if t == "ordered_list":
|
|
160
180
|
return [nodes.ordered_list(_render_list_items(node, api, ctx))]
|
|
161
181
|
|
|
182
|
+
# "$$...$$ (label)" tokenizes as math_block_label; Substack has no equation
|
|
183
|
+
# labels, so it renders like an unlabeled block.
|
|
184
|
+
if t in ("math_block", "math_block_label"):
|
|
185
|
+
return [nodes.latex_block(node.content.strip())]
|
|
186
|
+
|
|
187
|
+
if t == "container_pullquote":
|
|
188
|
+
return [nodes.pullquote(_render_container_body(node, api, ctx))]
|
|
189
|
+
|
|
190
|
+
if t == "container_callout":
|
|
191
|
+
return [nodes.callout_block(_render_container_body(node, api, ctx))]
|
|
192
|
+
|
|
162
193
|
# footnote_block is handled separately in markdown_to_doc; ignore it here.
|
|
163
194
|
return []
|
|
164
195
|
|
|
165
196
|
|
|
197
|
+
def _render_container_body(node: SyntaxTreeNode, api, ctx: Dict) -> List[Dict]:
|
|
198
|
+
body: List[Dict] = []
|
|
199
|
+
for child in node.children:
|
|
200
|
+
body.extend(_render_block(child, api, ctx))
|
|
201
|
+
return body
|
|
202
|
+
|
|
203
|
+
|
|
166
204
|
def _render_list_items(list_node: SyntaxTreeNode, api, ctx: Dict) -> List[Dict]:
|
|
167
205
|
items = []
|
|
168
206
|
for li in list_node.children:
|
substack/nodes.py
CHANGED
|
@@ -33,6 +33,10 @@ class NodeType:
|
|
|
33
33
|
FOOTNOTE_ANCHOR = "footnoteAnchor"
|
|
34
34
|
CAPTIONED_IMAGE = "captionedImage"
|
|
35
35
|
CAPTION = "caption"
|
|
36
|
+
LATEX_BLOCK = "latex_block"
|
|
37
|
+
LATEX_INLINE = "latex"
|
|
38
|
+
PULLQUOTE = "pullquote"
|
|
39
|
+
CALLOUT_BLOCK = "calloutBlock"
|
|
36
40
|
|
|
37
41
|
|
|
38
42
|
class MarkType:
|
|
@@ -40,6 +44,8 @@ class MarkType:
|
|
|
40
44
|
EM = "em"
|
|
41
45
|
CODE = "code"
|
|
42
46
|
STRIKETHROUGH = "strikethrough"
|
|
47
|
+
SUPERSCRIPT = "superscript"
|
|
48
|
+
SUBSCRIPT = "subscript"
|
|
43
49
|
LINK = "link"
|
|
44
50
|
|
|
45
51
|
|
|
@@ -146,3 +152,32 @@ def footnote(number: int, paragraphs: List[Dict]) -> Dict:
|
|
|
146
152
|
"attrs": {"number": number},
|
|
147
153
|
"content": paragraphs or [paragraph()],
|
|
148
154
|
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def latex_block(expression: str) -> Dict:
|
|
158
|
+
return {
|
|
159
|
+
"type": NodeType.LATEX_BLOCK,
|
|
160
|
+
"attrs": {"persistentExpression": expression, "dirty": True},
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def latex_inline(expression: str) -> Dict:
|
|
165
|
+
return {
|
|
166
|
+
"type": NodeType.LATEX_INLINE,
|
|
167
|
+
"attrs": {"expression": expression, "persistentExpression": expression},
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def pullquote(paragraphs: List[Dict]) -> Dict:
|
|
172
|
+
return {
|
|
173
|
+
"type": NodeType.PULLQUOTE,
|
|
174
|
+
"attrs": {"align": None, "color": None},
|
|
175
|
+
"content": paragraphs or [paragraph()],
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def callout_block(paragraphs: List[Dict]) -> Dict:
|
|
180
|
+
return {
|
|
181
|
+
"type": NodeType.CALLOUT_BLOCK,
|
|
182
|
+
"content": paragraphs or [paragraph()],
|
|
183
|
+
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
substack/__init__.py,sha256=6wfj_pMsGW4d-y6wyswxGMMQtu0q41tmalHchGkzOyY,416
|
|
2
|
-
substack/api.py,sha256=QX9A_7PanQd_Q_QJwnxXM9auIHCtKd7rHbFTDATcm-U,22640
|
|
3
|
-
substack/cli.py,sha256=F6ba6mgrBzoHXpwNTgUv8JMRmSKgdP7AHpm89keC8EM,19911
|
|
4
|
-
substack/exceptions.py,sha256=BbP5W5UpzFcM5SYIxx6snWD_Rmj7F_YjYIYC_r03gZY,911
|
|
5
|
-
substack/mdrender.py,sha256=cB0fLFzOF7CmWHk-9eRvQormP8StqIgy9hYE6CSKPH0,7770
|
|
6
|
-
substack/nodes.py,sha256=eFVxoVwi684g_DLbz8BRHfU7nyHsJb-m8w17UHocXgY,4106
|
|
7
|
-
substack/post.py,sha256=nXeZMAZ6-lx1Qf4yuAlrEQ0lcWFYF2afai1_TxVEl0Q,19704
|
|
8
|
-
substack_mcp/mcp_server.py,sha256=gmevdc59XTBXXMTvVMpYoq66Umlqku1O_uzmw5tMy7o,8405
|
|
9
|
-
python_substack-0.1.26.dist-info/METADATA,sha256=mw0CiIItyXlZt5jODivLqwcHtMFILPpHZCEJKcsysR4,10702
|
|
10
|
-
python_substack-0.1.26.dist-info/WHEEL,sha256=kJCRJT_g0adfAJzTx2GUMmS80rTJIVHRCfG0DQgLq3o,88
|
|
11
|
-
python_substack-0.1.26.dist-info/entry_points.txt,sha256=MKPjaBUd-0PtvxsBviStsVq1c0h8JZ_qUoYEsBK1xJc,236
|
|
12
|
-
python_substack-0.1.26.dist-info/licenses/LICENSE,sha256=L6jk148I5HhhVbfUvkO3EO7eAoU5zToLio4-ApkCkxg,1062
|
|
13
|
-
python_substack-0.1.26.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|