Unofficial Python API client for Notion.so

Overview

notion-py

Unofficial Python 3 client for Notion.so API v3.

  • Object-oriented interface (mapping database tables to Python classes/attributes)
  • Automatic conversion between internal Notion formats and appropriate Python objects
  • Local cache of data in a unified data store (Note: disk cache now disabled by default; to enable, add enable_caching=True when initializing NotionClient)
  • Real-time reactive two-way data binding (changing Python object -> live updating of Notion UI, and vice-versa) (Note: Notion->Python automatic updating is currently broken and hence disabled by default; call my_block.refresh() to update, in the meantime, while monitoring is being fixed)
  • Callback system for responding to changes in Notion (e.g. for triggering actions, updating another API, etc)

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="<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

  • We map tables in the Notion database into Python classes (subclassing Record), with each instance of a class representing a particular record. Some fields from the records (like title in the example above) have been mapped to model properties, allowing for easy, instantaneous read/write of the record. Other fields can be read with the get method, and written with the set method, but then you'll need to make sure to match the internal structures exactly.
  • The tables we currently support are block (via Block class and its subclasses, corresponding to different type of blocks), space (via Space class), collection (via Collection class), collection_view (via CollectionView and subclasses), and notion_user (via User class).
  • Data for all tables are stored in a central RecordStore, with the Record instances not storing state internally, but always referring to the data in the central RecordStore. Many API operations return updating versions of a large number of associated records, which we use to update the store, so the data in Record instances may sometimes update without being explicitly requested. You can also call the refresh method on a Record to trigger an update, or pass force_update to methods like get.
  • The API doesn't have strong validation of most data, so be careful to maintain the structures Notion is expecting. You can view the full internal structure of a record by calling myrecord.get() with no arguments.
  • When you call client.get_block, you can pass in either an ID, or the URL of a page. Note that pages themselves are just blocks, as are all the chunks of content on the page. You can get the URL for a block within a page by clicking "Copy Link" in the context menu for the block, and pass that URL into get_block as well.

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 my_block.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:

  • Main container block: CollectionViewBlock (inline) / CollectionViewPageBlock (full-page)
    • Collection (holds the schema, and is parent to the database rows themselves)
      • CollectionRowBlock
      • CollectionRowBlock
      • ... (more database records)
    • CollectionView (holds filters/sort/etc about each specific view)

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="<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="<token_v2>")

# The initial current_user of a multi-account user may be an unwanted user
print(client.current_user.email) # โ†’ [email protected]

# Set current_user to the desired user
client.set_user_by_email('[email protected]')
print(client.current_user.email) # โ†’ [email protected]

# You can also set the current_user by uid.
client.set_user_by_uid('<uid>')
print(client.current_user.email) # โ†’ [email protected]

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!

Related Projects

TODO

  • Cloning pages hierarchically
  • Debounce cache-saving?
  • Support inline "user" and "page" links, and reminders, in markdown conversion
  • Utilities to support updating/creating collection schemas
  • Utilities to support updating/creating collection_view queries
  • Support for easily managing page permissions
  • Websocket support for live block cache updating
  • "Render full page to markdown" mode
  • "Import page from html" mode
Owner
Jamie Alexandre
Jamie Alexandre
GroupMenter : New Telegram Group Manager Bot๐Ÿ”ธFast ๐Ÿ”ธPython๐Ÿ”ธPyrogram ๐Ÿ”ธ

GroupMenter An PowerFull Group Manager Bot. Written In Pytelethon. Info โ€ข A modular Telegram Python bot running on python3. โ€ข Can be found on telegram

Group Menter 24 Jun 28, 2022
A Python Client for News API

newsapi-python A Python client for the News API. License Provided under MIT License by Matt Lisivick. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRAN

Matt Lisivick 281 Dec 29, 2022
Programmeertheorie 2022 - Team Trainspotters - RailNL

Trainspotters Vak: Programmeertheorie 2022 Gekozen case: RailNL Teamnaam: Trainspotters Studenten: Mijntje Meijer, Sam Bijhouwer, Maik Larooij To-do's

Maik Larooij 1 Jan 25, 2022
Senexia - A powerful telegram bot to manage your groups as effectively as possible

โšก Kenechi bot โšก A Powerful, Smart And Simple Group Manager ... Written with AioG

Akhi 2 Jan 11, 2022
A script to automate the process of downloading Markdown and CSV backups of Notion

Automatic-Notion-Backup A script to automate the process of downloading Markdown and CSV backups of Notion. In addition, the data is processed to remo

Jorge Manuel Lozano Gรณmez 2 Nov 02, 2022
Rbx-mass-send - mass sends trades to item owners

mass sends trades to item owners proxies should be in ip:port format itemsToSend

0 Feb 20, 2022
API para realizar parser de frases

NLP API Simple api to parse and apply some preprocessing steps in portuguses phrases (pt_BR) This api uses the great FastAPI and spaCy packages! Usage

โŸ  Rodolfo De Nadai 1 Dec 28, 2021
Match-making API for OpenSanctions

OpenSanctions Match-making API This directory contains code and a Docker image for running an API to match data against OpenSanctions. It is intended

OpenSanctions.org 26 Dec 15, 2022
All in one Search Engine Scrapper for used by API or Python Module. It's Free!

All in one Search Engine Scrapper for used by API or Python Module. How to use: Video Documentation Senginta is All in one Search Engine Scrapper. Wit

33 Nov 21, 2022
PyMusic Player is a music player written in python3.

PyMusic Player is a music player written in python3. It harvests r

PythonSerious 2 Jan 30, 2022
Bypass Hcaptcha Purely based on http requests, Creates unlocked discord accounts if used correctly

hcaptcha-bypass-discord Bypass HCAPTCHA purely based on http requests Works for discord dosen't create locked accounts :)) HOW TO USE โ—‰ add the hcapby

Avenger 80 Dec 22, 2022
Widevine CDM API

wvproxy Widevine CDM API Setup Install Python 3.6 or newer and Poetry Install Python package dependencies using poetry install Activate the virtual en

24 Dec 09, 2022
โœจ ๐Ÿ Python SDK for StarkNet.

โœจ ๐Ÿ starknet.py StarkNet SDK for Python ๐Ÿ“˜ Documentation Installation Quickstart Guide API Installation To install this package run pip install stark

Software Mansion 158 Jan 04, 2023
Want to get your driver's license? Can't get a appointment because of COVID? Well I got a solution for you.

NJDMV-appoitment-alert Want to get your driver's license? Can't get a appointment because of COVID? Well I got a solution for you. We'll get you one i

Harris Spahic 3 Feb 04, 2022
๐€ ๐ฆ๐จ๐๐ฎ๐ฅ๐š๐ซ ๐“๐ž๐ฅ๐ž๐ ๐ซ๐š๐ฆ ๐†๐ซ๐จ๐ฎ๐ฉ ๐ฆ๐š๐ง๐š๐ ๐ž๐ฆ๐ž๐ง๐ญ ๐›๐จ๐ญ ๐ฐ๐ข๐ญ๐ก ๐ฎ๐ฅ๐ญ๐ข๐ฆ๐š๐ญ๐ž ๐Ÿ๐ž๐š๐ญ๐ฎ๐ซ๐ž๐ฌ !!

๐‡๐จ๐ฐ ๐“๐จ ๐ƒ๐ž๐ฉ๐ฅ๐จ๐ฒ For easiest way to deploy this Bot click on the below button ๐Œ๐š๐๐ž ๐๐ฒ ๐’๐ฎ๐ฉ๐ฉ๐จ๐ซ๐ญ ๐†๐ซ๐จ๐ฎ๐ฉ ๐’๐จ๐ฎ๐ซ๐œ๐ž๐ฌ ๐†๐ž๐ง๐ž?

Mukesh Solanki 4 Oct 18, 2021
Dashboard to monitor the performance of your Binance Futures account

futuresboard A python based scraper and dashboard to monitor the performance of your Binance Futures account. Note: A local sqlite3 database config/fu

86 Dec 29, 2022
We have made you a wrapper you can't refuse

We have made you a wrapper you can't refuse We have a vibrant community of developers helping each other in our Telegram group. Join us! Stay tuned fo

20.6k Jan 04, 2023
Diablo II Resurrected Diablo Clone Running Room Mgr

d2rdc Diablo II Resurrected Diablo Clone Running Room Mgr Install Dependencies pip install fastapi pip install uvicorn Running uvicorn init:app INFO:

1 Dec 03, 2021
A simple and modular Discord bot with various functionalities.

All-In-Bot for Discord A simple and modular Discord bot with various functionalities. How to use the bot? Simple! Just invite the bot to your server u

Th3J0nny 3 Jan 29, 2022
Mass Instagram Checker

Mass Instagram Checker

X - MrG3P5 5 Nov 09, 2022