diff --git a/examples/draft.yaml b/examples/draft.yaml new file mode 100644 index 0000000..16eb7e6 --- /dev/null +++ b/examples/draft.yaml @@ -0,0 +1,23 @@ +title: + "How to publish a Substack post using the Python API" +subtitle: + "This post was published using the Python API" +body: + 0: + type: "heading" + level: 1 + content: "Steps" + 1: + type: "paragraph" + content: "1)" + 2: + type: "paragraph" + content: "Discover your USER ID by inspecting the request body of any publish request." + 3: + type: "horizontal_rule" + 4: + type: "paragraph" + content: "2)" + 5: + type: "paragraph" + content: "Set the EMAIL, PASSWORD, PUBLICATION_URL and USER_ID environment variables." \ No newline at end of file diff --git a/examples/publish_post.py b/examples/publish_post.py index 3afe97d..b1e8002 100644 --- a/examples/publish_post.py +++ b/examples/publish_post.py @@ -1,30 +1,42 @@ +import argparse import os +import yaml from dotenv import load_dotenv from substack import Api +from substack.post import Post load_dotenv() -content = "" -title = "" -subtitle = "" +if __name__ == "__main__": -api = Api( - email=os.getenv("EMAIL"), - password=os.getenv("PASSWORD"), - publication_url=os.getenv("PUBLICATION_URL"), -) + parser = argparse.ArgumentParser() + parser.add_argument("-p", "--post", default="draft.yaml", required=True, + help="YAML file containing the post to publish.", type=str) + parser.add_argument("--publish", help="Publish the draft.", action="store_true") + args = parser.parse_args() -body = f'{{"type":"doc","content": {content}}}' + with open(args.post, "r") as fp: + post_data = yaml.safe_load(fp) -draft = api.post_draft( - [{"id": os.getenv("USER_ID"), "is_guest": False}], - title=title, - subtitle=subtitle, - body=body, -) + title = post_data.get("title", "") + subtitle = post_data.get("subtitle", "") + body = post_data.get("body", {}) -api.prepublish_draft(draft.get("id")) + api = Api( + email=os.getenv("EMAIL"), + password=os.getenv("PASSWORD"), + publication_url=os.getenv("PUBLICATION_URL"), + ) -api.publish_draft(draft.get("id")) + post = Post(title, subtitle, os.getenv("USER_ID")) + for _, item in body.items(): + post.add(item) + + draft = api.post_draft(post.get_draft()) + + if args.publish: + api.prepublish_draft(draft.get("id")) + + api.publish_draft(draft.get("id")) diff --git a/poetry.lock b/poetry.lock index cb2410e..2e9c0ad 100644 --- a/poetry.lock +++ b/poetry.lock @@ -27,15 +27,23 @@ python-versions = ">=3.5" [[package]] name = "python-dotenv" -version = "0.20.0" +version = "0.21.0" description = "Read key-value pairs from a .env file and set them as environment variables" category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.7" [package.extras] cli = ["click (>=5.0)"] +[[package]] +name = "pyyaml" +version = "6.0" +description = "YAML parser and emitter for Python" +category = "main" +optional = false +python-versions = ">=3.6" + [[package]] name = "requests" version = "2.28.1" @@ -70,12 +78,57 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "e71581e2fd13c9806b0c3874153462efb7da0f2a42581922bb869a1280fd3a64" +content-hash = "efbc3a4f68f8764006a665ebdc04818e5b984c7b808cfccc2420a35d8396066a" [metadata.files] certifi = [] charset-normalizer = [] idna = [] -python-dotenv = [] +python-dotenv = [ + {file = "python-dotenv-0.21.0.tar.gz", hash = "sha256:b77d08274639e3d34145dfa6c7008e66df0f04b7be7a75fd0d5292c191d79045"}, + {file = "python_dotenv-0.21.0-py3-none-any.whl", hash = "sha256:1684eb44636dd462b66c3ee016599815514527ad99965de77f43e0944634a7e5"}, +] +pyyaml = [ + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, + {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, + {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, +] requests = [] urllib3 = [] diff --git a/pyproject.toml b/pyproject.toml index 9d0e8e8..686ce14 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,8 +10,8 @@ packages = [ readme = "README.md" -repository = "https://github.com/hogier/python-substack" -homepage = "https://github.com/hogier/python-substack" +repository = "https://github.com/mazza8/python-substack" +homepage = "https://github.com/mazza8/python-substack" keywords = ["substack"] @@ -19,7 +19,8 @@ keywords = ["substack"] python = "^3.8" requests = "^2.28.1" -python-dotenv = "^0.20.0" +python-dotenv = "^0.21.0" +PyYAML = "^6.0" [tool.poetry.dev-dependencies] diff --git a/substack/api.py b/substack/api.py index 764cd90..3ae81ab 100644 --- a/substack/api.py +++ b/substack/api.py @@ -8,245 +8,230 @@ class Api: - """ - - A python interface into the Substack API - - """ - - def __init__( - self, - email: str | None = None, - password: str | None = None, - base_url: str | None = None, - publication_url: str | None = None, - debug: bool = False, - ): - """ - - To create an instance of the substack.Api class: - >>> import substack - >>> api = substack.Api(email="substack email", password="substack password") - - Args: - email: - password: - base_url: - The base URL to use to contact the Substack API. - Defaults to https://substack.com/api/v1. - """ - self.base_url = base_url or "https://substack.com/api/v1" - self.publication_url = publication_url - - if debug: - logging.basicConfig() - logging.getLogger().setLevel(logging.DEBUG) - - self._session = requests.Session() - - if email is not None and password is not None: - self.login(email, password) - - def login(self, email: str, password: str) -> dict: - """ - - Args: - email: substack account email - password: substack account password - """ - - response = self._session.post( - f"{self.base_url}/login", - json={ - "captcha_response": None, - "email": email, - "for_pub": "", - "password": password, - "redirect": "/", - }, - ) - return Api._handle_response(response=response) - - @staticmethod - def _handle_response(response: requests.Response): - """ - - Internal helper for handling API responses from the Substack server. - Raises the appropriate exceptions when necessary; otherwise, returns the - response. - - """ - - if not (200 <= response.status_code < 300): - raise SubstackAPIException(response.status_code, response.text) - try: - return response.json() - except ValueError: - raise SubstackRequestException("Invalid Response: %s" % response.text) - - def get_publication_users(self): - """ - - :return: - """ - response = self._session.get(f"{self.publication_url}/publication/users") - - return Api._handle_response(response=response) - - def get_posts(self) -> dict: - """ - - :return: - """ - response = self._session.get(f"{self.base_url}/reader/posts") - - return Api._handle_response(response=response) - - def get_drafts(self, filter: str = None, offset: int = None, limit: int = None): - response = self._session.get( - f"{self.publication_url}/drafts", - params={"filter": filter, "offset": offset, "limit": limit}, - ) - return Api._handle_response(response=response) - - def post_draft( - self, - draft_bylines: list, - title: str = None, - subtitle: str = None, - body: str = None, - ) -> dict: - """ - - Args: - draft_bylines: - title: - subtitle: - body: - - Returns: - - """ - response = self._session.post( - f"{self.publication_url}/drafts", - json={ - "draft_bylines": draft_bylines, - "draft_title": title, - "draft_subtitle": subtitle, - "draft_body": body, - }, - ) - return Api._handle_response(response=response) - - def put_draft( - self, - draft: str, - title: str = None, - subtitle: str = None, - body: str = None, - cover_image: str = None, - ) -> dict: - """ - - Args: - draft: draft id - title: - subtitle: - body: - cover_image: - - Returns: - - """ - - response = self._session.put( - f"{self.publication_url}/drafts/{draft}", - json={ - "draft_title": title, - "draft_subtitle": subtitle, - "draft_body": body, - "cover_image": cover_image, - }, - ) - return Api._handle_response(response=response) - - def prepublish_draft(self, draft: str) -> dict: - """ - - Args: - draft: draft id - - Returns: - - """ - - response = self._session.get( - f"{self.publication_url}/drafts/{draft}/prepublish" - ) - return Api._handle_response(response=response) - - def publish_draft( - self, draft: str, send: bool = True, share_automatically: bool = False - ) -> dict: - """ - - Args: - draft: draft id - send: - share_automatically: - - Returns: - - """ - response = self._session.post( - f"{self.publication_url}/drafts/{draft}/publish", - json={"send": send, "share_automatically": share_automatically}, - ) - return Api._handle_response(response=response) - - def get_categories(self): - """ - - Retrieve list of all available categories. - - Returns: - - """ - response = self._session.get(f"{self.base_url}/categories") - return Api._handle_response(response=response) - - def get_category(self, category_id: int, category_type: str, page: int): - response = self._session.get(f"{self.base_url}/category/public/{category_id}/{category_type}", - params={"page": page}) - return Api._handle_response(response=response) - - def get_single_category(self, category_id: int, category_type: str, page: int | None = None, - limit: int | None = None): - """ - - Args: - category_id: - category_type: paid or all - page: by default substack retrieves only the first 25 publications in the category. If this is left None, - then all pages will be retrieved. The page size is 25 publications. - limit: - Returns: - - """ - if page is not None: - output = self.get_category(category_id, category_type, page) - else: - publications = [] - page = 0 - while True: - page_output = self.get_category(category_id, category_type, page) - publications.extend(page_output.get("publications", [])) - if (limit is not None and limit <= len(publications)) or not page_output.get("more", False): - publications = publications[:limit] - break - page += 1 - output = { - "publications": publications, - "more": page_output.get("more", False) - } - return output + """ + + A python interface into the Substack API + + """ + + def __init__( + self, + email: str | None = None, + password: str | None = None, + base_url: str | None = None, + publication_url: str | None = None, + debug: bool = False, + ): + """ + + To create an instance of the substack.Api class: + >>> import substack + >>> api = substack.Api(email="substack email", password="substack password") + + Args: + email: + password: + base_url: + The base URL to use to contact the Substack API. + Defaults to https://substack.com/api/v1. + """ + self.base_url = base_url or "https://substack.com/api/v1" + self.publication_url = f"{publication_url}/api/v1" + + if debug: + logging.basicConfig() + logging.getLogger().setLevel(logging.DEBUG) + + self._session = requests.Session() + + if email is not None and password is not None: + self.login(email, password) + + def login(self, email: str, password: str) -> dict: + """ + + Login to the substack account. + + Args: + email: substack account email + password: substack account password + """ + + response = self._session.post( + f"{self.base_url}/login", + json={ + "captcha_response": None, + "email": email, + "for_pub": "", + "password": password, + "redirect": "/", + }, + ) + return Api._handle_response(response=response) + + @staticmethod + def _handle_response(response: requests.Response): + """ + + Internal helper for handling API responses from the Substack server. + Raises the appropriate exceptions when necessary; otherwise, returns the + response. + + """ + + if not (200 <= response.status_code < 300): + raise SubstackAPIException(response.status_code, response.text) + try: + return response.json() + except ValueError: + raise SubstackRequestException("Invalid Response: %s" % response.text) + + def get_publication_users(self): + """ + + :return: + """ + response = self._session.get(f"{self.publication_url}/publication/users") + + return Api._handle_response(response=response) + + def get_posts(self) -> dict: + """ + + :return: + """ + response = self._session.get(f"{self.base_url}/reader/posts") + + return Api._handle_response(response=response) + + def get_drafts(self, filter: str = None, offset: int = None, limit: int = None): + response = self._session.get( + f"{self.publication_url}/drafts", + params={"filter": filter, "offset": offset, "limit": limit}, + ) + return Api._handle_response(response=response) + + def post_draft(self, body) -> dict: + """ + + Args: + body: + + Returns: + + """ + response = self._session.post(f"{self.publication_url}/drafts", json=body) + return Api._handle_response(response=response) + + def put_draft( + self, + draft: str, + title: str = None, + subtitle: str = None, + body: str = None, + cover_image: str = None, + ) -> dict: + """ + + Args: + draft: draft id + title: + subtitle: + body: + cover_image: + + Returns: + + """ + + response = self._session.put( + f"{self.publication_url}/drafts/{draft}", + json={ + "draft_title": title, + "draft_subtitle": subtitle, + "draft_body": body, + "cover_image": cover_image, + }, + ) + return Api._handle_response(response=response) + + def prepublish_draft(self, draft: str) -> dict: + """ + + Args: + draft: draft id + + Returns: + + """ + + response = self._session.get( + f"{self.publication_url}/drafts/{draft}/prepublish" + ) + return Api._handle_response(response=response) + + def publish_draft( + self, draft: str, send: bool = True, share_automatically: bool = False + ) -> dict: + """ + + Args: + draft: draft id + send: + share_automatically: + + Returns: + + """ + response = self._session.post( + f"{self.publication_url}/drafts/{draft}/publish", + json={"send": send, "share_automatically": share_automatically}, + ) + return Api._handle_response(response=response) + + def get_categories(self): + """ + + Retrieve list of all available categories. + + Returns: + + """ + response = self._session.get(f"{self.base_url}/categories") + return Api._handle_response(response=response) + + def get_category(self, category_id: int, category_type: str, page: int): + response = self._session.get(f"{self.base_url}/category/public/{category_id}/{category_type}", + params={"page": page}) + return Api._handle_response(response=response) + + def get_single_category(self, category_id: int, category_type: str, page: int | None = None, + limit: int | None = None): + """ + + Args: + category_id: + category_type: paid or all + page: by default substack retrieves only the first 25 publications in the category. If this is left None, + then all pages will be retrieved. The page size is 25 publications. + limit: + Returns: + + """ + if page is not None: + output = self.get_category(category_id, category_type, page) + else: + publications = [] + page = 0 + while True: + page_output = self.get_category(category_id, category_type, page) + publications.extend(page_output.get("publications", [])) + if (limit is not None and limit <= len(publications)) or not page_output.get("more", False): + publications = publications[:limit] + break + page += 1 + output = { + "publications": publications, + "more": page_output.get("more", False) + } + return output diff --git a/substack/post.py b/substack/post.py new file mode 100644 index 0000000..f78d06c --- /dev/null +++ b/substack/post.py @@ -0,0 +1,74 @@ +import json + + +class Post: + + def __init__(self, title: str, subtitle: str, user_id: str): + self.draft_title = title + self.draft_subtitle = subtitle + self.draft_body = {"type": "doc", "content": []} + self.draft_bylines = [{"id": int(user_id), "is_guest": False}] + + def add(self, item): + self.draft_body["content"] = self.draft_body.get("content", []) + [{"type": item.get("type")}] + content = item.get("content") + if content is not None: + self.text(content) + + if item.get("type") == "heading": + self.attrs(item.get("level", 1)) + return self + + def paragraph(self, content=None): + item = {"type": "paragraph"} + if content is not None: + item["content"] = content + return self.add(item) + + def heading(self, content=None, level=1): + item = {"type": "heading"} + if content is not None: + item["content"] = content + item["level"] = level + return self.add(item) + + def horizontal_rule(self): + return self.add({"type": "horizontal_rule"}) + + def attrs(self, level): + content_attrs = self.draft_body["content"][-1].get("attrs", {}) + content_attrs.update({"level": level}) + self.draft_body["content"][-1]["attrs"] = content_attrs + return self + + def text(self, value: str): + """ + + Add text to the last paragraph. + + Args: + value: Text to add to paragraph. + + Returns: + + """ + content = self.draft_body["content"][-1].get("content", []) + content += [{"type": "text", "text": value}] + self.draft_body["content"][-1]["content"] = content + return self + + def marks(self, marks): + content = self.draft_body["content"][-1].get("content", [])[-1] + content_marks = content.get("marks", []) + for mark in marks: + content_marks.append({"type": mark}) + content["marks"] = content_marks + return self + + def remove_last_paragraph(self): + del self.draft_body.get("content")[-1] + + def get_draft(self): + out = vars(self) + out["draft_body"] = json.dumps(out["draft_body"]) + return out