Package: notion-py
notion-py(Fork of) Unofficial Python 3 client for Notion.so API v3.Documentation|Package on PyPINOTE:This is a fork of theoriginal repositorycreated byJamie Alexandre.You can try out this package - it's callednotion-pyon PyPI. The original package created by Jamie is  still online
under the namenotionon PyPI,
so please watch out for any confusion.imports are still working as before, the-pyin
name is there only to differentiate between these two.These libraries as of now arenotfully compatible.(I'm working on sending PRs to the upstream)List of major differences:imports were changed, especially for blocks and collections.General rule is:notion.block.py       ->  notion.block.*.pynotion.collection.py  ->  notion.block.collection.*.pysome block names were changed to align them with notion.soOne of such examples isTodoBlock -> ToDoBlock(because it's type isto_do)some function definitions also changedI did that to simplify the API and make it more uniform.FeaturesAutomatic conversion between Notion blocks and Python objectswe covered pretty much every block type there is!Callback system for responding to changes in Notionuseful for triggering actions, updating another block, etc.Object-oriented interfaceseamless mapping of API response parameters to Python classes/attributes.Local cache of data in a unified data storenote: this is disabled by default; addenable_caching=Truewhen initializingNotionClientto change it.Real-time reactive two-way data bindingfancy way of saying that changing Python object will update the Notion UI, and vice-versa.(Example of the two-way data binding in action)Read more about Notion and the original notion-py package on Jamie's blog.UsageQuickstartNOTE:The latest version ofnotion-pyrequires Python 3.6 or greater.pip install notion-pyfromnotion.clientimportNotionClient# Obtain the `token_v2` value by inspecting your browser# cookies on a logged-in (non-guest) session on Notion.soclient=NotionClient(token_v2="123123...")# Replace this URL with the URL of the page you want to editpage=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_v2Opennotion.soin your browser and log in.Open up developer console (quick tutorial the most common browsers).Find a list of cookies (Firefox:Storage->Cookies, Chrome:Application->Cookies).Find the one namedtoken_v2and copy its value (lengthy, 160ish characters hex string).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 recordsWe keep a local cache of all data that passes through.When you reference an attribute on aRecord(basically
anyBlock) 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 aRecordby calling therefresh()method on it.By default (unless we instantiateNotionClientwithmonitor=False), we also subscribe to long-polling
updates for any instantiatedRecord, so the local cache
data for theseRecordsshould be automatically
live-updated shortly after any data changes on the server.The long-polling happens in a background daemon thread.Concepts and notesThe tables we currently support areblock,space,collection,collection_view, andnotion_user.We map tables in the Notion database into Python classesby subclassingRecord, with each instance of a class
representing a particular record. Some fields from the
records (liketitlein the example above) have been
mapped to model properties, allowing for easy,
instantaneous read/write of the record.
Other fields can be read with thegetmethod,
and written with thesetmethod, but then you'll
need to make sure to match the internal structures exactly.Data for all tables are stored in a central RecordStorewith theRecordinstances not storing state internally,
but always referring to the data in the
centralRecordStore.
Many API operations return updating versions of a large
number of associated records, which we use to update
the store, so the data inRecordinstances may sometimes
update without being explicitly requested.
You can also call therefresh()method on aRecordto trigger an update, or passforce_update=Trueto
methods likeget().The API doesn't have strong validation of most dataso be careful to maintain the structures Notion is expecting.
You can view the full internal structure of a record by
callingmyrecord.get()with no arguments.When you callclient.get_block(), you can pass in
block ID, or the URL of a blockNote that pages themselves are justblocks, 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
intoget_block()as well.Working on a Pull RequestYou'll needgitandpython3withvenvmodule.Best way to start is to clone the repo and prepare the.envfile.
This step is optional but nice to have to create healthy python venv.githttps://github.com/arturtamborski/notion-pycdnotion-py

cp.env.example.env
vim.envYou should modify the variables as following:# see above for info on how to get itNOTION_TOKEN_V2="insert your token_v2 here"# used in smoke testsNOTION_PAGE_URL="insert URL from some notion page here"# set it to any level from python logging libraryNOTION_LOG_LEVEL="DEBUG"# the location for cache, defaults to current directoryNOTION_DATA_DIR=".notion-py"And then load that file (which will also create local venv):source.envOn top of that there's a handy toolbox provided to you viaMakefile.
Everything related to the development of the project relies heavily on
the interface it provides.You can display all commands by runningmakehelpWhich 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 tomake try-smoke-testlike so:maketry-smoke-testsmoke_tests/test_workflow.py::test_workflow_1That'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 2Notice thatERROR 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!maketry-smoke-testsmoke_tests/block/test_basic.py::test_blockExamplesClick here to show or hideExample: Traversing the block treeforchildinpage.children:print(child.title)print(f"Parent of{page.id}is{page.parent.id}")Example: Adding a new nodefromnotion.block.basicimportToDoBlocktodo=page.children.add_new(ToDoBlock,title="Something to get done")todo.checked=TrueExample: Deleting nodes# soft-deletepage.remove()# hard-deletepage.remove(permanently=True)Example: Create an embedded content type (iframe, video, etc)fromnotion.block.uploadimportVideoBlockvideo=page.children.add_new(VideoBlock,width=200)# sets "property.source" to the URL# and "format.display_source" to the embedly-converted URLvideo.set_source_url("https://www.youtube.com/watch?v=oHg5SJYRHA0")Example: Create a new embedded collection view blockfromnotion.block.collection.basicimportCollectionViewBlockcollection=client.get_collection("<some collection ID>")# get an existing collectioncvb=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 videomy_block.move_to(video,"after")# move my block to the end of otherblock's childrenmy_block.move_to(otherblock,"last-child")# Note: you can also use "before" and "first-child" :)Example: Subscribing to updatesNOTE:Notion -> Python automatic updating is
currently broken and hence disabled by default.callmy_block.refresh()to update, in the meantime,
while monitoring is being fixed.We can "watch" aRecordso 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 (all arguments are optional, just include the ones you care about)defmy_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 videomy_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)CollectionBlockCollectionBlock... (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 theCollection, into getter/setter attributes
on theCollectionBlockinstances.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 viamyrowblock.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 aNotionUserinstance,
or a list of them, and for a "Relation" we expect a singular/list
of instances of a subclass ofBlock.# Access a database using the URL of the database page or the inline blockcv=client.get_collection_view("https://www.notion.so/myorg/b9076...8b832?v=8de...8e1")# List all the records with "Bob" in themforrowincv.collection.get_rows(search="Bob"):print("We estimate the value of '{}' at{}".format(row.name,row.estimated_value))# Add a new recordrow=cv.collection.add_row()row.name="Just some data"row.is_confirmed=Truerow.estimated_value=399row.files=["https://www.birdlife.org/sites/default/files/styles/1600/public/slide.jpg"]row.person=client.current_userrow.tags=["A","C"]row.where_to="https://learningequality.org"# Run a filtered/sorted query using a view's default parametersresult=cv.default_query().execute()forrowinresult:print(row)# Run an "aggregation" queryaggregations=[{"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" querysorters=[{"direction":"descending","property":"estimated_value",}]result=cv.build_query(sort=sorters).execute()print("Sorted results, showing most valuable first:",result)NOTE:: You can combinefilter,aggregate, andsort.
See more examples of queries by setting up complex views in Notion,
and then inspectingcv.get("query").Example: Lock/Unlock A Pagefromnotion.clientimportNotionClientclient=NotionClient(token_v2="123123...")# Replace this URL with the URL of the page you want to editpage=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)