arturtamborski / notion-py

(Fork of) Unofficial Python API client for Notion.so
https://pypi.org/project/notion-py/
MIT License
63 stars 9 forks source link
api-client notion python3

notion-py

(Fork of) Unofficial Python 3 client for Notion.so API v3.

[Documentation][documentation-url] | [Package on PyPI][package-url]

![check formatting][check-formatting-url] ![run unit tests][run-unit-tests-url] ![upload-python-package][upload-python-package-url] ![run-smoke-tests][run-smoke-tests-url] ![documentation-status][documentation-status-url] ![code-style][code-style-url] ![license][license-url] ![code-size][code-size-url] ![downloads-rate][downloads-rate-url]



NOTE: This is a fork of the original repository created by Jamie Alexandre.

You can try out this package - it's called notion-py on PyPI. The original package created by Jamie is still online under the name notion on PyPI, so please watch out for any confusion.

imports are still working as before, the -py in name is there only to differentiate between these two.


These libraries as of now are not fully compatible.
(I'm working on sending PRs to the upstream)

List of major differences:



Features


data binding example
(Example of the two-way data binding in action)

Read more about Notion and the original notion-py package on Jamie's blog.

Usage

Quickstart

NOTE: The latest version of notion-py requires Python 3.6 or greater.

pip install notion-py

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="123123...")

# 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)

# 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!"

Getting the token_v2

  1. Open notion.so in your browser and log in.
  2. Open up developer console (quick tutorial the most common browsers).
  3. Find a list of cookies (Firefox: Storage -> Cookies, Chrome: Application -> Cookies).
  4. Find the one named token_v2 and copy its value (lengthy, 160ish characters hex string).
  5. Save it somewhere safe and use it with notion-py!

NOTE: Keep the token in secure place and out of your repository!
This token when leaked can let anyone do anything on your notion account!

Updating records

We keep a local cache of all data that passes through.
When you reference an attribute on a Record (basically any Block) 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.

Concepts and notes

Working on a Pull Request

You'll need git and python3 with venv module.

Best way to start is to clone the repo and prepare the .env file. This step is optional but nice to have to create healthy python venv.

git https://github.com/arturtamborski/notion-py

cd notion-py

cp .env.example .env
vim .env

You should modify the variables as following:

# see above for info on how to get it
NOTION_TOKEN_V2="insert your token_v2 here"

# used in smoke tests
NOTION_PAGE_URL="insert URL from some notion page here"

# set it to any level from python logging library
NOTION_LOG_LEVEL="DEBUG" 

# the location for cache, defaults to current directory
NOTION_DATA_DIR=".notion-py"

And then load that file (which will also create local venv):

source .env

On top of that there's a handy toolbox provided to you via Makefile. Everything related to the development of the project relies heavily on the interface it provides.

You can display all commands by running

make help

Which should print a nice list of commands avaiable to you. These are compatible with the Github Actions (CI system), in fact the actions are using Makefile directly for formatting and other steps so everything that Github might show you under your Pull Request can be reproduced locally via Makefile.

Also, there's one very handy shortcut that I'm using all the time when testing the library with smoke tests.

This command will run a single test unit that you point at by passing an argument to make try-smoke-test like so:

make try-smoke-test smoke_tests/test_workflow.py::test_workflow_1

That's super handy when you run some smoke tests and see the failed output:

============================= short test summary info =============================
ERROR smoke_tests/block/test_basic.py::test_block - KeyboardInterrupt
!!!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!!
!!!!!!!!!!!!!!!!!!!! _pytest.outcomes.Exit: Quitting debugger !!!!!!!!!!!!!!!!!!!!!
================================ 1 error in 32.90s ================================
make: *** [Makefile:84: try-smoke-test] Error 2

Notice that ERROR smoke_tests/...test_basic.py::test_block - just copy it over as a command argument and run it again - you'll run this and only this one test!

make try-smoke-test smoke_tests/block/test_basic.py::test_block

Examples

Click here to show or hide ### Example: Traversing the block tree ```Python for child in page.children: print(child.title) print(f"Parent of {page.id} is {page.parent.id}") ``` ### Example: Adding a new node ```Python from notion.block.basic import ToDoBlock todo = page.children.add_new(ToDoBlock, title="Something to get done") todo.checked = True ``` ### Example: Deleting nodes ```Python # soft-delete page.remove() # hard-delete page.remove(permanently=True) ``` ### Example: Create an embedded content type (iframe, video, etc) ```Python from notion.block.upload 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 ```Python from notion.block.collection.basic import CollectionViewBlock collection = client.get_collection("") # 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 ```Python # 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") # Note: 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. ```Python # define a callback (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:\n", 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) - `CollectionBlock` - `CollectionBlock` - ... (more database records) - `CollectionView` (holds filters/sort/etc about each specific view) For convenience, we automatically map the database "columns" (aka properties), based on the schema defined in the `Collection`, into getter/setter attributes on the `CollectionBlock` 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 `NotionUser` instance, or a list of them, and for a "Relation" we expect a singular/list of instances of a subclass of `Block`. ```Python # Access a database using the URL of the database page or the inline block cv = client.get_collection_view("https://www.notion.so/myorg/b9076...8b832?v=8de...8e1") # 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=aggregations).execute() print("Total estimated value:", result.get_aggregate("total_value")) # Run a "filtered" query (inspect network tab in browser for examples, on queryCollection calls) filters = { "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=filters).execute() print("Things assigned to me:", result) # Run a "sorted" query sorters = [{ "direction": "descending", "property": "estimated_value", }] result = cv.build_query(sort=sorters).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 `cv.get("query")`. ### Example: Lock/Unlock A Page ```python from notion.client import NotionClient client = NotionClient(token_v2="123123...") # Replace this URL with the URL of the page you want to edit page = client.get_block("https://www.notion.so/myorg/Test-c0d20a71c0944985ae96e661ccc99821") # change_lock is a method accessible to every Block/Page in notion. # Pass True to lock a page and False to unlock it. page.change_lock(True) page.change_lock(False) ```