python-substack 0.1.25__py3-none-any.whl → 0.1.26__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.
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-substack
3
- Version: 0.1.25
4
- Summary: A Python wrapper around the Substack API.
3
+ Version: 0.1.26
4
+ Summary: A Python SDK and CLI for managing Substack publications and drafts.
5
5
  License: MIT
6
6
  License-File: LICENSE
7
- Keywords: substack
7
+ Keywords: substack,substack-api,cli,newsletter,publishing,automation,mcp
8
8
  Author: Paolo Mazza
9
9
  Author-email: mazzapaolo2019@gmail.com
10
10
  Requires-Python: >=3.10,<4.0
@@ -35,13 +35,20 @@ Description-Content-Type: text/markdown
35
35
 
36
36
  # Python Substack
37
37
 
38
- Unofficial Python tools for publishing to [Substack](https://substack.com/).
38
+ An unofficial Python SDK and CLI for managing [Substack](https://substack.com/) publications and drafts.
39
39
 
40
+ [![PyPI](https://img.shields.io/pypi/v/python-substack)](https://pypi.org/project/python-substack/)
41
+ [![Python](https://img.shields.io/pypi/pyversions/python-substack)](https://pypi.org/project/python-substack/)
42
+ [![Tests](https://github.com/ma2za/python-substack/actions/workflows/ci.yml/badge.svg)](https://github.com/ma2za/python-substack/actions/workflows/ci.yml)
43
+ [![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)
44
+ [![License](https://img.shields.io/pypi/l/python-substack)](LICENSE)
40
45
  [![Downloads](https://static.pepy.tech/badge/python-substack/month)](https://pepy.tech/project/python-substack)
41
- ![Release Build](https://github.com/ma2za/python-substack/actions/workflows/ci_publish.yml/badge.svg)
42
46
 
43
47
  ## Features
44
48
 
49
+ - Inspect authentication and publication status from the terminal.
50
+ - List publications and inspect, schedule, publish, or delete drafts.
51
+ - Use stable JSON output in scripts and automation.
45
52
  - Create drafts and publish posts from Python.
46
53
  - Convert Markdown into Substack's editor document format.
47
54
  - Upload local images while rendering Markdown.
@@ -78,6 +85,49 @@ Use either `EMAIL` and `PASSWORD`, or cookie-based authentication with `COOKIES_
78
85
 
79
86
  Newer Substack accounts may only have magic-link sign-in enabled. To set a password, sign out of Substack, choose "Sign in with password", then choose "Set a new password".
80
87
 
88
+ ## CLI Operations
89
+
90
+ Check authentication, the selected publication, and subscriber count:
91
+
92
+ ```bash
93
+ substack status
94
+ ```
95
+
96
+ List available publications or target one without changing `.env`:
97
+
98
+ ```bash
99
+ substack publications list
100
+ substack --publication-url https://example.substack.com drafts list
101
+ ```
102
+
103
+ List and inspect drafts:
104
+
105
+ ```bash
106
+ substack drafts list --limit 10
107
+ substack drafts get 12345
108
+ ```
109
+
110
+ Schedule with a timezone-aware ISO 8601 timestamp, or remove a schedule:
111
+
112
+ ```bash
113
+ substack drafts schedule 12345 --at 2026-08-01T09:00:00+03:00
114
+ substack drafts unschedule 12345
115
+ ```
116
+
117
+ Publishing and deletion prompt for confirmation. Use `--yes` for intentional non-interactive execution:
118
+
119
+ ```bash
120
+ substack drafts publish 12345 --no-send
121
+ substack drafts delete 12345 --yes
122
+ ```
123
+
124
+ Global options must appear before the command. `--json` returns stable envelopes containing the raw Substack responses:
125
+
126
+ ```bash
127
+ substack --json drafts list
128
+ substack --cookies cookies.json --json status
129
+ ```
130
+
81
131
  ## Quickstart
82
132
 
83
133
  ```python
@@ -113,7 +163,7 @@ print(result["draft"]["id"])
113
163
 
114
164
  `create_draft_from_markdown` creates a draft by default. It only publishes when `publish=True` is passed.
115
165
 
116
- ## CLI
166
+ ## Content Publishing CLI
117
167
 
118
168
  Check authentication without creating a draft:
119
169
 
@@ -373,6 +423,8 @@ pytest
373
423
 
374
424
  Live Substack tests are opt-in. Set `RUN_SUBSTACK_E2E=1` and configure credentials before running them.
375
425
 
426
+ The CLI operations smoke test is separately opt-in. Set `RUN_SUBSTACK_CLI_E2E=1` to create, schedule, unschedule, inspect, and delete a disposable draft. It never publishes the draft.
427
+
376
428
  Release changes are tracked in [CHANGELOG.md](CHANGELOG.md).
377
429
 
378
430
  ## Disclaimer
@@ -0,0 +1,13 @@
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.4.0
2
+ Generator: poetry-core 2.3.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,4 +1,5 @@
1
1
  [console_scripts]
2
+ substack=substack.cli:main
2
3
  substack-auth-check=substack.cli:auth_check
3
4
  substack-mcp=substack_mcp.mcp_server:main
4
5
  substack-publish-markdown=substack.cli:publish_markdown
substack/__init__.py CHANGED
@@ -3,9 +3,9 @@
3
3
  __author__ = "Paolo Mazza"
4
4
  __email__ = "mazzapaolo2019@gmail.com"
5
5
  __license__ = "MIT License"
6
- __version__ = "0.1.25"
6
+ __version__ = "0.1.26"
7
7
  __url__ = "https://github.com/ma2za/python-substack"
8
8
  __download_url__ = "https://pypi.python.org/pypi/python-substack"
9
- __description__ = "A Python wrapper around the Substack API"
9
+ __description__ = "A Python SDK and CLI for managing Substack publications and drafts"
10
10
 
11
11
  from .api import Api
substack/cli.py CHANGED
@@ -1,32 +1,41 @@
1
1
  import argparse
2
2
  import json
3
3
  import os
4
+ import sys
5
+ from datetime import datetime
4
6
  from pathlib import Path
7
+ from urllib.parse import urljoin
5
8
 
6
9
  import yaml
7
10
  from dotenv import load_dotenv
8
11
 
9
- from substack import Api
12
+ from substack import Api, __version__
13
+ from substack.exceptions import SubstackAPIException, SubstackRequestException
10
14
  from substack.post import Post
11
15
 
12
16
 
13
- def _api_from_env(cookies_path=None):
17
+ class CLIUsageError(Exception):
18
+ pass
19
+
20
+
21
+ def _api_from_env(cookies_path=None, publication_url=None):
14
22
  load_dotenv()
15
23
 
16
24
  cookies_path = cookies_path or os.getenv("COOKIES_PATH")
17
25
  cookies_string = os.getenv("COOKIES_STRING")
26
+ publication_url = publication_url or os.getenv("PUBLICATION_URL")
18
27
 
19
28
  if cookies_path or cookies_string:
20
29
  return Api(
21
30
  cookies_path=cookies_path,
22
31
  cookies_string=cookies_string,
23
- publication_url=os.getenv("PUBLICATION_URL"),
32
+ publication_url=publication_url,
24
33
  )
25
34
 
26
35
  return Api(
27
36
  email=os.getenv("EMAIL"),
28
37
  password=os.getenv("PASSWORD"),
29
- publication_url=os.getenv("PUBLICATION_URL"),
38
+ publication_url=publication_url,
30
39
  )
31
40
 
32
41
 
@@ -56,6 +65,356 @@ def _print_result(result):
56
65
  print(json.dumps({"draft_id": draft.get("id"), "draft": draft}, indent=2))
57
66
 
58
67
 
68
+ def _print_json(value, stream=None):
69
+ print(json.dumps(value, indent=2, default=str), file=stream)
70
+
71
+
72
+ def _display(value):
73
+ if value is None or value == "":
74
+ return "-"
75
+ return str(value)
76
+
77
+
78
+ def _print_rows(headers, rows):
79
+ rows = [[_display(value) for value in row] for row in rows]
80
+ widths = [len(header) for header in headers]
81
+ for row in rows:
82
+ for index, value in enumerate(row):
83
+ widths[index] = max(widths[index], len(value))
84
+
85
+ print(
86
+ " ".join(header.ljust(widths[index]) for index, header in enumerate(headers))
87
+ )
88
+ print(" ".join("-" * width for width in widths))
89
+ for row in rows:
90
+ print(" ".join(value.ljust(widths[index]) for index, value in enumerate(row)))
91
+
92
+
93
+ def _identity(profile):
94
+ return (
95
+ profile.get("email")
96
+ or profile.get("name")
97
+ or profile.get("handle")
98
+ or str(profile.get("id", "unknown"))
99
+ )
100
+
101
+
102
+ def _selected_publication(api, publications):
103
+ for publication in publications:
104
+ publication_url = publication.get("publication_url")
105
+ if (
106
+ publication_url
107
+ and urljoin(publication_url, "api/v1") == api.publication_url
108
+ ):
109
+ return publication
110
+ return {"publication_url": api.publication_url.removesuffix("/api/v1")}
111
+
112
+
113
+ def _parse_schedule(value):
114
+ normalized = value[:-1] + "+00:00" if value.endswith(("Z", "z")) else value
115
+ try:
116
+ scheduled_at = datetime.fromisoformat(normalized)
117
+ except ValueError as exc:
118
+ raise CLIUsageError("--at must be a valid ISO 8601 timestamp") from exc
119
+ if scheduled_at.utcoffset() is None:
120
+ raise CLIUsageError("--at must include a timezone offset or Z")
121
+ return scheduled_at
122
+
123
+
124
+ def _confirm(action, target, yes, json_output):
125
+ if yes:
126
+ return
127
+ if json_output or not sys.stdin.isatty():
128
+ raise CLIUsageError(f"{action} requires --yes in non-interactive or JSON mode")
129
+ try:
130
+ confirmed = input(f"{action} draft {target}? [y/N] ").strip().lower()
131
+ except EOFError as exc:
132
+ raise CLIUsageError(f"{action} requires confirmation or --yes") from exc
133
+ if confirmed not in {"y", "yes"}:
134
+ raise CLIUsageError("Cancelled")
135
+
136
+
137
+ def _redact(message):
138
+ redacted = str(message)
139
+ for name in ("EMAIL", "PASSWORD", "COOKIES_STRING"):
140
+ secret = os.getenv(name)
141
+ if secret:
142
+ redacted = redacted.replace(secret, "[redacted]")
143
+ return redacted
144
+
145
+
146
+ def _error_payload(exc):
147
+ if isinstance(exc, SubstackAPIException):
148
+ return {
149
+ "error": {
150
+ "type": "api_error",
151
+ "message": _redact(exc.message),
152
+ "status_code": exc.status_code,
153
+ }
154
+ }
155
+ if isinstance(exc, SubstackRequestException):
156
+ return {
157
+ "error": {
158
+ "type": "request_error",
159
+ "message": _redact(exc.message),
160
+ }
161
+ }
162
+ if isinstance(exc, OSError):
163
+ return {"error": {"type": "io_error", "message": _redact(exc)}}
164
+ return {"error": {"type": "configuration_error", "message": _redact(exc)}}
165
+
166
+
167
+ def _print_error(exc, json_output):
168
+ payload = _error_payload(exc)
169
+ if json_output:
170
+ _print_json(payload, stream=sys.stderr)
171
+ else:
172
+ print(f"Error: {payload['error']['message']}", file=sys.stderr)
173
+
174
+
175
+ def _print_usage_error(exc, json_output):
176
+ if json_output:
177
+ _print_json(
178
+ {"error": {"type": "usage_error", "message": str(exc)}},
179
+ stream=sys.stderr,
180
+ )
181
+ else:
182
+ print(f"Error: {exc}", file=sys.stderr)
183
+
184
+
185
+ def _status(api, args):
186
+ profile = api.get_user_profile()
187
+ publications = api.get_user_publications()
188
+ selected = _selected_publication(api, publications)
189
+ subscriber_count = api.get_publication_subscriber_count()
190
+ result = {
191
+ "status": {
192
+ "identity": _identity(profile),
193
+ "auth_method": _auth_method(args.cookies),
194
+ "subscriber_count": subscriber_count,
195
+ },
196
+ "profile": profile,
197
+ "publication": selected,
198
+ "publications": publications,
199
+ }
200
+ if args.json_output:
201
+ _print_json(result)
202
+ else:
203
+ print(f"Authenticated as: {result['status']['identity']}")
204
+ print(f"Auth method: {result['status']['auth_method']}")
205
+ print(f"Publication: {_display(selected.get('name'))}")
206
+ print(f"Publication URL: {_display(selected.get('publication_url'))}")
207
+ print(f"Subscribers: {subscriber_count}")
208
+ print(f"Available publications: {len(publications)}")
209
+
210
+
211
+ def _publications_list(api, args):
212
+ publications = api.get_user_publications()
213
+ if args.json_output:
214
+ _print_json({"publications": publications, "count": len(publications)})
215
+ else:
216
+ _print_rows(
217
+ ["ID", "NAME", "SUBDOMAIN", "URL"],
218
+ [
219
+ [
220
+ publication.get("id"),
221
+ publication.get("name"),
222
+ publication.get("subdomain"),
223
+ publication.get("publication_url"),
224
+ ]
225
+ for publication in publications
226
+ ],
227
+ )
228
+
229
+
230
+ def _drafts_list(api, args):
231
+ if args.offset < 0:
232
+ raise CLIUsageError("--offset must be zero or greater")
233
+ if args.limit < 1:
234
+ raise CLIUsageError("--limit must be greater than zero")
235
+ drafts = api.get_drafts(filter=args.filter, offset=args.offset, limit=args.limit)
236
+ if args.json_output:
237
+ _print_json(
238
+ {
239
+ "drafts": drafts,
240
+ "count": len(drafts),
241
+ "filter": args.filter,
242
+ "offset": args.offset,
243
+ "limit": args.limit,
244
+ }
245
+ )
246
+ else:
247
+ _print_rows(
248
+ ["ID", "TITLE", "STATUS", "SCHEDULED"],
249
+ [
250
+ [
251
+ draft.get("id"),
252
+ draft.get("draft_title") or draft.get("title"),
253
+ draft.get("type") or draft.get("status") or "draft",
254
+ draft.get("post_date"),
255
+ ]
256
+ for draft in drafts
257
+ ],
258
+ )
259
+
260
+
261
+ def _drafts_get(api, args):
262
+ draft = api.get_draft(args.draft_id)
263
+ if args.json_output:
264
+ _print_json({"draft": draft})
265
+ else:
266
+ fields = [
267
+ ("ID", draft.get("id")),
268
+ ("Title", draft.get("draft_title") or draft.get("title")),
269
+ ("Subtitle", draft.get("draft_subtitle") or draft.get("subtitle")),
270
+ ("Status", draft.get("type") or draft.get("status") or "draft"),
271
+ ("Slug", draft.get("slug")),
272
+ ("Scheduled", draft.get("post_date")),
273
+ ("Audience", draft.get("audience")),
274
+ ]
275
+ for label, value in fields:
276
+ print(f"{label}: {_display(value)}")
277
+
278
+
279
+ def _drafts_schedule(api, args):
280
+ scheduled_at = _parse_schedule(args.at)
281
+ result = api.schedule_draft(args.draft_id, scheduled_at)
282
+ payload = {
283
+ "action": "schedule",
284
+ "draft_id": args.draft_id,
285
+ "scheduled_at": scheduled_at.isoformat(),
286
+ "result": result,
287
+ }
288
+ if args.json_output:
289
+ _print_json(payload)
290
+ else:
291
+ print(f"Scheduled draft {args.draft_id} for {scheduled_at.isoformat()}")
292
+
293
+
294
+ def _drafts_unschedule(api, args):
295
+ result = api.unschedule_draft(args.draft_id)
296
+ payload = {"action": "unschedule", "draft_id": args.draft_id, "result": result}
297
+ if args.json_output:
298
+ _print_json(payload)
299
+ else:
300
+ print(f"Unscheduled draft {args.draft_id}")
301
+
302
+
303
+ def _drafts_publish(api, args):
304
+ _confirm("Publish", args.draft_id, args.yes, args.json_output)
305
+ prepublish = api.prepublish_draft(args.draft_id)
306
+ result = api.publish_draft(
307
+ args.draft_id,
308
+ send=args.send,
309
+ share_automatically=args.share_automatically,
310
+ )
311
+ payload = {
312
+ "action": "publish",
313
+ "draft_id": args.draft_id,
314
+ "prepublish": prepublish,
315
+ "result": result,
316
+ }
317
+ if args.json_output:
318
+ _print_json(payload)
319
+ else:
320
+ delivery = "without email" if not args.send else "with email"
321
+ print(f"Published draft {args.draft_id} {delivery}")
322
+
323
+
324
+ def _drafts_delete(api, args):
325
+ _confirm("Delete", args.draft_id, args.yes, args.json_output)
326
+ result = api.delete_draft(args.draft_id)
327
+ payload = {"action": "delete", "draft_id": args.draft_id, "result": result}
328
+ if args.json_output:
329
+ _print_json(payload)
330
+ else:
331
+ print(f"Deleted draft {args.draft_id}")
332
+
333
+
334
+ def _build_parser():
335
+ parser = argparse.ArgumentParser(
336
+ prog="substack", description="Manage Substack publications and drafts."
337
+ )
338
+ parser.add_argument("--cookies", help="Path to a cookies JSON file.")
339
+ parser.add_argument(
340
+ "--publication-url", help="Override PUBLICATION_URL for this command."
341
+ )
342
+ parser.add_argument("--json", action="store_true", dest="json_output")
343
+ parser.add_argument("--version", action="version", version=__version__)
344
+
345
+ commands = parser.add_subparsers(dest="command", required=True)
346
+ status = commands.add_parser(
347
+ "status", help="Show authentication and publication status."
348
+ )
349
+ status.set_defaults(handler=_status)
350
+
351
+ publications = commands.add_parser("publications", help="Manage publications.")
352
+ publication_commands = publications.add_subparsers(
353
+ dest="publication_command", required=True
354
+ )
355
+ publications_list = publication_commands.add_parser(
356
+ "list", help="List available publications."
357
+ )
358
+ publications_list.set_defaults(handler=_publications_list)
359
+
360
+ drafts = commands.add_parser("drafts", help="Manage drafts.")
361
+ draft_commands = drafts.add_subparsers(dest="draft_command", required=True)
362
+
363
+ drafts_list = draft_commands.add_parser("list", help="List drafts.")
364
+ drafts_list.add_argument("--filter", default="draft")
365
+ drafts_list.add_argument("--offset", type=int, default=0)
366
+ drafts_list.add_argument("--limit", type=int, default=25)
367
+ drafts_list.set_defaults(handler=_drafts_list)
368
+
369
+ drafts_get = draft_commands.add_parser("get", help="Inspect a draft.")
370
+ drafts_get.add_argument("draft_id", type=int)
371
+ drafts_get.set_defaults(handler=_drafts_get)
372
+
373
+ drafts_schedule = draft_commands.add_parser("schedule", help="Schedule a draft.")
374
+ drafts_schedule.add_argument("draft_id", type=int)
375
+ drafts_schedule.add_argument("--at", required=True)
376
+ drafts_schedule.set_defaults(handler=_drafts_schedule)
377
+
378
+ drafts_unschedule = draft_commands.add_parser(
379
+ "unschedule", help="Remove a draft schedule."
380
+ )
381
+ drafts_unschedule.add_argument("draft_id", type=int)
382
+ drafts_unschedule.set_defaults(handler=_drafts_unschedule)
383
+
384
+ drafts_publish = draft_commands.add_parser("publish", help="Publish a draft.")
385
+ drafts_publish.add_argument("draft_id", type=int)
386
+ drafts_publish.add_argument(
387
+ "--no-send", action="store_false", dest="send", default=True
388
+ )
389
+ drafts_publish.add_argument("--share-automatically", action="store_true")
390
+ drafts_publish.add_argument("--yes", action="store_true")
391
+ drafts_publish.set_defaults(handler=_drafts_publish)
392
+
393
+ drafts_delete = draft_commands.add_parser("delete", help="Delete a draft.")
394
+ drafts_delete.add_argument("draft_id", type=int)
395
+ drafts_delete.add_argument("--yes", action="store_true")
396
+ drafts_delete.set_defaults(handler=_drafts_delete)
397
+ return parser
398
+
399
+
400
+ def main(argv=None):
401
+ parser = _build_parser()
402
+ args = parser.parse_args(argv)
403
+ try:
404
+ api = _api_from_env(
405
+ cookies_path=args.cookies,
406
+ publication_url=args.publication_url,
407
+ )
408
+ args.handler(api, args)
409
+ except CLIUsageError as exc:
410
+ _print_usage_error(exc, args.json_output)
411
+ return 2
412
+ except (SubstackAPIException, SubstackRequestException, OSError, ValueError) as exc:
413
+ _print_error(exc, args.json_output)
414
+ return 1
415
+ return 0
416
+
417
+
59
418
  def publish_markdown(argv=None):
60
419
  parser = argparse.ArgumentParser()
61
420
  parser.add_argument("markdown", nargs="?", default="README.md")
@@ -1,13 +0,0 @@
1
- substack/__init__.py,sha256=sm5s0r53o_oBjA_3yUob0L4VwXeT8bNuA20PnAjTflI,390
2
- substack/api.py,sha256=QX9A_7PanQd_Q_QJwnxXM9auIHCtKd7rHbFTDATcm-U,22640
3
- substack/cli.py,sha256=Hu9tBjyIW9zl7IE92VuZ-MOThS2cLpwXXAOR8Ixsu6o,7708
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.25.dist-info/METADATA,sha256=Ero48_tB_HIHbkccKxfKK24rvOJiqlO80SlXItRgpC4,8720
10
- python_substack-0.1.25.dist-info/WHEEL,sha256=EGEvSphFYqXKs23-kQBeyNoJP1nrT8ZJKQoi5p5DYL8,88
11
- python_substack-0.1.25.dist-info/entry_points.txt,sha256=la9TUtzyaVksmdSkdKZsg55XIRXx4KjkKW7J3pvBegc,209
12
- python_substack-0.1.25.dist-info/licenses/LICENSE,sha256=L6jk148I5HhhVbfUvkO3EO7eAoU5zToLio4-ApkCkxg,1062
13
- python_substack-0.1.25.dist-info/RECORD,,