python-substack 0.1.18__tar.gz → 0.1.19__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.
- {python_substack-0.1.18 → python_substack-0.1.19}/PKG-INFO +2 -2
- {python_substack-0.1.18 → python_substack-0.1.19}/pyproject.toml +2 -2
- {python_substack-0.1.18 → python_substack-0.1.19}/substack/api.py +61 -0
- {python_substack-0.1.18 → python_substack-0.1.19}/substack/post.py +85 -5
- {python_substack-0.1.18 → python_substack-0.1.19}/LICENSE +0 -0
- {python_substack-0.1.18 → python_substack-0.1.19}/README.md +0 -0
- {python_substack-0.1.18 → python_substack-0.1.19}/substack/__init__.py +0 -0
- {python_substack-0.1.18 → python_substack-0.1.19}/substack/exceptions.py +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-substack
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.19
|
|
4
4
|
Summary: A Python wrapper around the Substack API.
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
7
7
|
Keywords: substack
|
|
8
8
|
Author: Paolo Mazza
|
|
9
9
|
Author-email: mazzapaolo2019@gmail.com
|
|
10
|
-
Requires-Python: >=3.9
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
11
|
Classifier: License :: OSI Approved :: MIT License
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.9
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "python-substack"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.19"
|
|
4
4
|
description = "A Python wrapper around the Substack API."
|
|
5
5
|
authors = ["Paolo Mazza <mazzapaolo2019@gmail.com>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -16,7 +16,7 @@ homepage = "https://github.com/ma2za/python-substack"
|
|
|
16
16
|
keywords = ["substack"]
|
|
17
17
|
|
|
18
18
|
[tool.poetry.dependencies]
|
|
19
|
-
python = "
|
|
19
|
+
python = ">=3.9"
|
|
20
20
|
|
|
21
21
|
requests = "^2.31.0"
|
|
22
22
|
python-dotenv = "^0.21.0"
|
|
@@ -514,6 +514,67 @@ class Api:
|
|
|
514
514
|
data={"image": image},
|
|
515
515
|
)
|
|
516
516
|
return Api._handle_response(response=response)
|
|
517
|
+
|
|
518
|
+
def add_tags_to_post(self, post_id: int, tag_names: list) -> dict:
|
|
519
|
+
"""
|
|
520
|
+
Add multiple tags to a post.
|
|
521
|
+
|
|
522
|
+
Args:
|
|
523
|
+
post_id: The ID of the post to tag.
|
|
524
|
+
tag_names: A list of tag names to add.
|
|
525
|
+
|
|
526
|
+
Returns:
|
|
527
|
+
A dictionary with the results of applying all tags.
|
|
528
|
+
"""
|
|
529
|
+
results = []
|
|
530
|
+
for tag_name in tag_names:
|
|
531
|
+
result = self.add_tag_to_post(post_id, tag_name)
|
|
532
|
+
results.append(result)
|
|
533
|
+
return {"tags_added": results}
|
|
534
|
+
|
|
535
|
+
def get_publication_post_tags(self) -> list:
|
|
536
|
+
"""
|
|
537
|
+
Retrieve all post tags for the current publication.
|
|
538
|
+
|
|
539
|
+
Returns:
|
|
540
|
+
List of tag dicts as returned by Substack API.
|
|
541
|
+
"""
|
|
542
|
+
response = self._session.get(f"{self.publication_url}/publication/post-tag")
|
|
543
|
+
return Api._handle_response(response=response)
|
|
544
|
+
|
|
545
|
+
def add_tag_to_post(self, post_id: int, tag_name: str) -> dict:
|
|
546
|
+
"""
|
|
547
|
+
Add a tag to a post by first checking published tags and creating only if needed.
|
|
548
|
+
|
|
549
|
+
Args:
|
|
550
|
+
post_id: The ID of the post to tag.
|
|
551
|
+
tag_name: The name of the tag to add.
|
|
552
|
+
|
|
553
|
+
Returns:
|
|
554
|
+
The response from applying the tag to the post.
|
|
555
|
+
"""
|
|
556
|
+
# Fetch existing publication tags first (avoid re-creating an already existing tag)
|
|
557
|
+
existing_tags = self.get_publication_post_tags() or []
|
|
558
|
+
existing_tag = next(
|
|
559
|
+
(tag for tag in existing_tags if tag.get("name") == tag_name),
|
|
560
|
+
None,
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
if existing_tag is not None:
|
|
564
|
+
tag_id = existing_tag["id"]
|
|
565
|
+
else:
|
|
566
|
+
create_tag_response = self._session.post(
|
|
567
|
+
f"{self.publication_url}/publication/post-tag",
|
|
568
|
+
json={"name": tag_name},
|
|
569
|
+
)
|
|
570
|
+
tag_data = Api._handle_response(create_tag_response)
|
|
571
|
+
tag_id = tag_data["id"]
|
|
572
|
+
|
|
573
|
+
apply_tag_response = self._session.post(
|
|
574
|
+
f"{self.publication_url}/post/{post_id}/tag/{tag_id}",
|
|
575
|
+
)
|
|
576
|
+
return Api._handle_response(apply_tag_response)
|
|
577
|
+
|
|
517
578
|
|
|
518
579
|
def get_categories(self):
|
|
519
580
|
"""
|
|
@@ -227,6 +227,42 @@ class Post:
|
|
|
227
227
|
item["level"] = level
|
|
228
228
|
return self.add(item)
|
|
229
229
|
|
|
230
|
+
def blockquote(self, content=None):
|
|
231
|
+
"""
|
|
232
|
+
Add a blockquote to the post.
|
|
233
|
+
|
|
234
|
+
The blockquote wraps one or more paragraph nodes.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
content: Text string or list of inline token dicts. When a plain
|
|
238
|
+
string is provided it is wrapped in a single paragraph node.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
Self for method chaining.
|
|
242
|
+
"""
|
|
243
|
+
paragraphs: List[Dict] = []
|
|
244
|
+
if content is not None:
|
|
245
|
+
if isinstance(content, str):
|
|
246
|
+
tokens = parse_inline(content)
|
|
247
|
+
text_nodes = [
|
|
248
|
+
{"type": "text", "text": t["content"]} for t in tokens if t
|
|
249
|
+
]
|
|
250
|
+
if text_nodes:
|
|
251
|
+
paragraphs.append({"type": "paragraph", "content": text_nodes})
|
|
252
|
+
elif isinstance(content, list):
|
|
253
|
+
for item in content:
|
|
254
|
+
if isinstance(item, dict) and item.get("type") == "paragraph":
|
|
255
|
+
paragraphs.append(item)
|
|
256
|
+
elif isinstance(item, dict):
|
|
257
|
+
text_nodes = [{"type": "text", "text": item.get("content", "")}]
|
|
258
|
+
paragraphs.append({"type": "paragraph", "content": text_nodes})
|
|
259
|
+
|
|
260
|
+
node: Dict = {"type": "blockquote"}
|
|
261
|
+
if paragraphs:
|
|
262
|
+
node["content"] = paragraphs
|
|
263
|
+
self.draft_body["content"] = self.draft_body.get("content", []) + [node]
|
|
264
|
+
return self
|
|
265
|
+
|
|
230
266
|
def horizontal_rule(self):
|
|
231
267
|
"""
|
|
232
268
|
|
|
@@ -464,6 +500,7 @@ class Post:
|
|
|
464
500
|
- Linked images: [](link_url) - images that are also links
|
|
465
501
|
- Links: [text](url) - inline links in paragraphs
|
|
466
502
|
- Code blocks: Fenced code blocks with ```language or ```
|
|
503
|
+
- Blockquotes: Lines starting with '>' (consecutive lines grouped)
|
|
467
504
|
- Paragraphs: Regular text blocks
|
|
468
505
|
- Bullet lists: Lines starting with '*' or '-'
|
|
469
506
|
- Inline formatting: **bold** and *italic* within paragraphs
|
|
@@ -611,12 +648,14 @@ class Post:
|
|
|
611
648
|
|
|
612
649
|
self.add({"type": "captionedImage", "src": image_url})
|
|
613
650
|
|
|
614
|
-
# Process paragraphs
|
|
651
|
+
# Process paragraphs, bullet lists, or blockquotes
|
|
615
652
|
else:
|
|
616
653
|
if "\n" in text_content:
|
|
617
654
|
# Process each line, grouping consecutive bullets
|
|
618
|
-
# into a single bullet_list node
|
|
655
|
+
# into a single bullet_list node and consecutive
|
|
656
|
+
# blockquote lines into a single blockquote node.
|
|
619
657
|
pending_bullets: List[List[Dict]] = []
|
|
658
|
+
pending_quotes: List[str] = []
|
|
620
659
|
|
|
621
660
|
def flush_bullets():
|
|
622
661
|
if not pending_bullets:
|
|
@@ -632,10 +671,36 @@ class Post:
|
|
|
632
671
|
)
|
|
633
672
|
pending_bullets.clear()
|
|
634
673
|
|
|
674
|
+
def flush_quotes():
|
|
675
|
+
if not pending_quotes:
|
|
676
|
+
return
|
|
677
|
+
paragraphs: List[Dict] = []
|
|
678
|
+
for quote_line in pending_quotes:
|
|
679
|
+
tokens = parse_inline(quote_line)
|
|
680
|
+
text_nodes = [
|
|
681
|
+
{"type": "text", "text": t["content"]}
|
|
682
|
+
for t in tokens if t
|
|
683
|
+
]
|
|
684
|
+
if text_nodes:
|
|
685
|
+
paragraphs.append({"type": "paragraph", "content": text_nodes})
|
|
686
|
+
node: Dict = {"type": "blockquote"}
|
|
687
|
+
if paragraphs:
|
|
688
|
+
node["content"] = paragraphs
|
|
689
|
+
self.draft_body["content"].append(node)
|
|
690
|
+
pending_quotes.clear()
|
|
691
|
+
|
|
635
692
|
for line in text_content.split("\n"):
|
|
636
693
|
line = line.strip()
|
|
637
694
|
if not line:
|
|
638
695
|
flush_bullets()
|
|
696
|
+
flush_quotes()
|
|
697
|
+
continue
|
|
698
|
+
|
|
699
|
+
# Check for blockquote marker
|
|
700
|
+
if line.startswith("> ") or line == ">":
|
|
701
|
+
flush_bullets()
|
|
702
|
+
quote_text = line[2:] if line.startswith("> ") else ""
|
|
703
|
+
pending_quotes.append(quote_text)
|
|
639
704
|
continue
|
|
640
705
|
|
|
641
706
|
# Check for bullet marker
|
|
@@ -648,18 +713,33 @@ class Post:
|
|
|
648
713
|
bullet_text = line[1:].strip()
|
|
649
714
|
|
|
650
715
|
if bullet_text is not None:
|
|
716
|
+
flush_quotes()
|
|
651
717
|
tokens = parse_inline(bullet_text)
|
|
652
718
|
if tokens:
|
|
653
719
|
pending_bullets.append(tokens)
|
|
654
720
|
else:
|
|
655
721
|
flush_bullets()
|
|
722
|
+
flush_quotes()
|
|
656
723
|
tokens = parse_inline(line)
|
|
657
724
|
self.add({"type": "paragraph", "content": tokens})
|
|
658
725
|
|
|
659
726
|
flush_bullets()
|
|
727
|
+
flush_quotes()
|
|
660
728
|
else:
|
|
661
|
-
# Single paragraph
|
|
662
|
-
|
|
663
|
-
|
|
729
|
+
# Single line — could be a blockquote or paragraph
|
|
730
|
+
if text_content.startswith("> ") or text_content == ">":
|
|
731
|
+
quote_text = text_content[2:] if text_content.startswith("> ") else ""
|
|
732
|
+
tokens = parse_inline(quote_text)
|
|
733
|
+
text_nodes = [
|
|
734
|
+
{"type": "text", "text": t["content"]}
|
|
735
|
+
for t in tokens if t
|
|
736
|
+
]
|
|
737
|
+
para = {"type": "paragraph", "content": text_nodes} if text_nodes else {"type": "paragraph"}
|
|
738
|
+
self.draft_body["content"] = self.draft_body.get("content", []) + [
|
|
739
|
+
{"type": "blockquote", "content": [para]}
|
|
740
|
+
]
|
|
741
|
+
else:
|
|
742
|
+
tokens = parse_inline(text_content)
|
|
743
|
+
self.add({"type": "paragraph", "content": tokens})
|
|
664
744
|
|
|
665
745
|
return self
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|