Last active
December 29, 2024 01:48
-
-
Save POD666/138ade98827e51d4b22523d88613bc0e to your computer and use it in GitHub Desktop.
Notion page duplication requires recursive blocks population and extra tricks to duplicate images. Hope this example will help someone who also struggle with notion API.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import logging | |
import os | |
from io import BytesIO | |
import boto3 | |
import click | |
import requests | |
logger = logging.getLogger(__name__) | |
s3 = boto3.client("s3") | |
# should be created manually and configured for public access | |
S3_BUCKET = "notion-images-for-page-duplication" | |
def request(request_func, *args, **kwargs): | |
"""Simple wrapper for requests with error handling""" | |
try: | |
response = request_func(*args, **kwargs) | |
response.raise_for_status() | |
except requests.exceptions.RequestException as e: | |
click.echo(args) | |
click.echo(kwargs) | |
click.echo(response.content) | |
raise e | |
return response | |
def upload_from_url_to_s3(url): | |
"""Download file to memory and upload to s3 with original filename""" | |
response = request(requests.get, url) | |
file_name = url.split("/")[-1].split("?")[0] | |
s3.upload_fileobj( | |
BytesIO(response.content), | |
S3_BUCKET, | |
file_name, | |
ExtraArgs={"ACL": "public-read"}, | |
) | |
bucket_location = s3.get_bucket_location(Bucket=S3_BUCKET)["LocationConstraint"] | |
return f"https://s3-{bucket_location}.amazonaws.com/{S3_BUCKET}/{file_name}" | |
def recursive_block_creation(original_blocks, new_blocks, headers, depth=0): | |
""" | |
Recursively populate child blocks for a given lists of original blocks and new blocks | |
""" | |
blocks_count = len(original_blocks) | |
for i, (block, new_block) in enumerate(zip(original_blocks, new_blocks)): | |
click.echo(f"{' ' * depth}Block depth={depth} {i}/{blocks_count}") | |
if not block.get("has_children"): | |
continue | |
# Get child blocks | |
block_id = block["id"] | |
new_block_id = new_block["id"] | |
response = request( | |
requests.get, | |
f"https://api.notion.com/v1/blocks/{block_id}/children", | |
headers=headers, | |
) | |
children = response.json()["results"] | |
# Redefine image blocks. | |
# It's not possible to use notion image url for creating new image blocks | |
# https://developers.notion.com/reference/file-object#externally-hosted-files-vs-files-hosted-by-notion | |
# Workaround: use upload_from_url_to_s3 to download images from notion and upload them to s3 instead | |
# so that we can use s3 public link as extarnal for notion block creation | |
children = [ | |
{ | |
"type": "image", | |
"image": { | |
"external": { | |
"url": upload_from_url_to_s3(child["image"]["file"]["url"]) | |
} | |
}, | |
} | |
if child["type"] == "image" | |
else child | |
for child in children | |
] | |
# Append child blocks to a specific block | |
response = request( | |
requests.patch, | |
f"https://api.notion.com/v1/blocks/{new_block_id}/children", | |
json={"children": children}, | |
headers=headers, | |
) | |
new_children = response.json()["results"] | |
recursive_block_creation(children, new_children, headers, depth=depth + 1) | |
@click.command() | |
@click.option( | |
"--new_title", help="New title for a page", required=False, | |
) | |
@click.option( | |
"--from_page_id", help="Specify page id to copy", required=False, | |
) | |
@click.option( | |
"--parent_page_id", help="Specify which page will be used as parent", required=False, | |
) | |
def duplicate_notion_page(new_title, from_page_id, parent_page_id): | |
NOTION_API_KEY = os.environ.get("NOTION_API_KEY") | |
if not NOTION_API_KEY: | |
raise click.UsageError("NOTION_API_KEY env variable not set") | |
headers = { | |
"Authorization": f"Bearer {NOTION_API_KEY}", | |
"Notion-Version": "2021-08-16", | |
"Content-Type": "application/json", | |
} | |
# Get template blocks | |
response = request( | |
requests.get, | |
f"https://api.notion.com/v1/blocks/{from_page_id}/children?page_size=100", | |
headers=headers, | |
) | |
root_blocks = response.json()["results"] | |
# Create a new page from template with root blocks | |
response = request( | |
requests.post, | |
"https://api.notion.com/v1/pages", | |
json={ | |
"parent": {"page_id": parent_page_id}, | |
"properties": { | |
"title": [{"text": {"content": new_title}}] | |
}, | |
"children": root_blocks, | |
}, | |
headers=headers, | |
) | |
response_data = response.json() | |
new_page_url = response_data["url"] | |
new_page_id = response_data["id"] | |
# Retrieve new blocks from the new page | |
response = request( | |
requests.get, | |
f"https://api.notion.com/v1/blocks/{new_page_id}/children?page_size=100", | |
headers=headers, | |
) | |
new_root_blocks = response.json()["results"] | |
# Populate nested blocks | |
recursive_block_creation(root_blocks, new_root_blocks, headers) | |
# Done | |
# Usage example: | |
# `export NEW_PAGE_URL=$(script.py duplicate-notion-page --new_title ... | tail -1 )` | |
click.echo(new_page_url) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment