GitHub - jamalex/notion-py: Unofficial Python API client for Notion.so (original) (raw)

notion-py

Unofficial Python 3 client for Notion.so API v3.

Read more about Notion and Notion-py on Jamie's blog

Usage

Quickstart

Note: the latest version of notion-py requires Python 3.5 or greater.

pip install notion

from notion.client import NotionClient

Obtain the token_v2 value by inspecting your browser cookies on a logged-in (non-guest) session on Notion.so

client = NotionClient(token_v2="")

Replace this URL with the URL of the page you want to edit

page = client.get_block("https://www.notion.so/myorg/Test-c0d20a71c0944985ae96e661ccc99821")

print("The old title is:", page.title)

Note: You can use Markdown! We convert on-the-fly to Notion's internal formatted text data structure.

page.title = "The title has now changed, and has live-updated in the browser!"

Concepts and notes

Updating records

We keep a local cache of all data that passes through. When you reference an attribute on a Record, we first look to that cache to retrieve the value. If it doesn't find it, it retrieves it from the server. You can also manually refresh the data for a Record by calling the refresh method on it. By default (unless we instantiate NotionClient with monitor=False), we also subscribe to long-polling updates for any instantiated Record, so the local cache data for these Records should be automatically live-updated shortly after any data changes on the server. The long-polling happens in a background daemon thread.

Example: Traversing the block tree

for child in page.children: print(child.title)

print("Parent of {} is {}".format(page.id, page.parent.id))

Example: Adding a new node

from notion.block import TodoBlock

newchild = page.children.add_new(TodoBlock, title="Something to get done") newchild.checked = True

Example: Deleting nodes

soft-delete

page.remove()

hard-delete

page.remove(permanently=True)

Example: Create an embedded content type (iframe, video, etc)

from notion.block import VideoBlock

video = page.children.add_new(VideoBlock, width=200)

sets "property.source" to the URL, and "format.display_source" to the embedly-converted URL

video.set_source_url("https://www.youtube.com/watch?v=oHg5SJYRHA0")

Example: Create a new embedded collection view block

collection = client.get_collection(COLLECTION_ID) # get an existing collection cvb = page.children.add_new(CollectionViewBlock, collection=collection) view = cvb.views.add_new(view_type="table")

Before the view can be browsed in Notion,

the filters and format options on the view should be set as desired.

for example:

view.set("query", ...)

view.set("format.board_groups", ...)

view.set("format.board_properties", ...)

Example: Moving blocks around

move my block to after the video

my_block.move_to(video, "after")

move my block to the end of otherblock's children

my_block.move_to(otherblock, "last-child")

(you can also use "before" and "first-child")

Example: Subscribing to updates

(Note: Notion->Python automatic updating is currently broken and hence disabled by default; call myblock.refresh() to update, in the meantime, while monitoring is being fixed)

We can "watch" a Record so that we get a callback whenever it changes. Combined with the live-updating of records based on long-polling, this allows for a "reactive" design, where actions in our local application can be triggered in response to interactions with the Notion interface.

define a callback (note: all arguments are optional, just include the ones you care about)

def my_callback(record, difference): print("The record's title is now:" record.title) print("Here's what was changed:") print(difference)

move my block to after the video

my_block.add_callback(my_callback)

Example: Working with databases, aka "collections" (tables, boards, etc)

Here's how things fit together:

Note: For convenience, we automatically map the database "columns" (aka properties), based on the schema defined in the Collection, into getter/setter attributes on the CollectionRowBlock instances. The attribute name is a "slugified" version of the name of the column. So if you have a column named "Estimated value", you can read and write it via myrowblock.estimated_value. Some basic validation may be conducted, and it will be converted into the appropriate internal format. For columns of type "Person", we expect a User instance, or a list of them, and for a "Relation" we expect a singular/list of instances of a subclass of Block.

Access a database using the URL of the database page or the inline block

cv = client.get_collection_view("https://www.notion.so/myorg/8511b9fc522249f79b90768b832599cc?v=8dee2a54f6b64cb296c83328adba78e1")

List all the records with "Bob" in them

for row in cv.collection.get_rows(search="Bob"): print("We estimate the value of '{}' at {}".format(row.name, row.estimated_value))

Add a new record

row = cv.collection.add_row() row.name = "Just some data" row.is_confirmed = True row.estimated_value = 399 row.files = ["https://www.birdlife.org/sites/default/files/styles/1600/public/slide.jpg"] row.person = client.current_user row.tags = ["A", "C"] row.where_to = "https://learningequality.org"

Run a filtered/sorted query using a view's default parameters

result = cv.default_query().execute() for row in result: print(row)

Run an "aggregation" query

aggregations = [{ "property": "estimated_value", "aggregator": "sum", "id": "total_value", }] result = cv.build_query(aggregate=aggregate_params).execute() print("Total estimated value:", result.get_aggregate("total_value"))

Run a "filtered" query (inspect network tab in browser for examples, on queryCollection calls)

filter_params = { "filters": [{ "filter": { "value": { "type": "exact", "value": {"table": "notion_user", "id": client.current_user.id} }, "operator": "person_contains" }, "property": "assigned_to" }], "operator": "and" } result = cv.build_query(filter=filter_params).execute() print("Things assigned to me:", result)

Run a "sorted" query

sort_params = [{ "direction": "descending", "property": "estimated_value", }] result = cv.build_query(sort=sort_params).execute() print("Sorted results, showing most valuable first:", result)

Note: You can combine filter, aggregate, and sort. See more examples of queries by setting up complex views in Notion, and then inspecting the full query: cv.get("query2").

You can also see more examples in action in the smoke test runner. Run it using:

python run_smoke_test.py --page [YOUR_NOTION_PAGE_URL] --token [YOUR_NOTION_TOKEN_V2]

Example: Lock/Unlock A Page

from notion.client import NotionClient

Obtain the token_v2 value by inspecting your browser cookies on a logged-in session on Notion.so

client = NotionClient(token_v2="")

Replace this URL with the URL of the page or database you want to edit

page = client.get_block("https://www.notion.so/myorg/Test-c0d20a71c0944985ae96e661ccc99821")

The "locked" property is available on PageBlock and CollectionViewBlock objects

Set it to True to lock the page/database

page.locked = True

and False to unlock it again

page.locked = False

Example: Set the current user for multi-account user

from notion.client import NotionClient client = NotionClient(token_v2="")

The initial current_user of a multi-account user may be an unwanted user

print(client.current_user.email) # → not_the_desired@email.co.jp

Set current_user to the desired user

client.set_user_by_email('desired@email.com') print(client.current_user.email) # → desired@email.com

You can also set the current_user by uid.

client.set_user_by_uid('') print(client.current_user.email) # → desired@email.com

Quick plug: Learning Equality needs your support!

If you'd like to support notion-py development, please consider donating to my open-source nonprofit, Learning Equality, since when I'm not working on notion-py, it probably means I'm heads-down fundraising for our global education work (bringing resources like Khan Academy to communities with no Internet). COVID has further amplified needs, with over a billion kids stuck at home, and over half of them without the connectivity they need for distance learning. You can now also support our work via GitHub Sponsors!

TODO