GithubHelp home page GithubHelp logo

notion-py's Introduction

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

notion-py's People

Contributors

ag0n1k avatar arsenal591 avatar c0j0s avatar chrisjune avatar doreapp avatar egordm avatar evertheylen avatar gabrocheleau avatar hmartiro avatar hoahovan avatar indirectlylit avatar itayc0hen avatar ivanistheone avatar jamalex avatar jean avatar jessemzhang avatar jheddings avatar kevinjalbert avatar mappyliam avatar miraclexyz avatar natikgadzhi avatar nielsbom avatar nwittwer avatar ruucm avatar samtgarson avatar shunyooo avatar snoop2head avatar tanoinc avatar wai-chuen avatar wsykala avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

notion-py's Issues

collection.get_rows() returns nothing

Thanks in advance for a great package. I am having a challenge where collection.get_rows() is returning nothing when the table has rows. The schema shows up, but the data does not. It seems to only be an issue for recently made tables (less than a day old). Older tables I get the rows ~70% of the time.

RecursionError on polling and operations

Traceback (most recent call last):
  File "C:\Users\Bence\.virtualenvs\wickie-Qv0iMXBj\lib\site-packages\notion\monitor.py", line 241, in poll_forever
    self.poll()
  File "C:\Users\Bence\.virtualenvs\wickie-Qv0iMXBj\lib\site-packages\notion\monitor.py", line 162, in poll
    self.poll(retries=retries - 1)
  File "C:\Users\Bence\.virtualenvs\wickie-Qv0iMXBj\lib\site-packages\notion\monitor.py", line 162, in poll
    self.poll(retries=retries - 1)
  File "C:\Users\Bence\.virtualenvs\wickie-Qv0iMXBj\lib\site-packages\notion\monitor.py", line 162, in poll
    self.poll(retries=retries - 1)
  [Previous line repeated 3 more times]
  File "C:\Users\Bence\.virtualenvs\wickie-Qv0iMXBj\lib\site-packages\notion\monitor.py", line 165, in poll
    self._decode_numbered_json_thing(response.content)
  File "C:\Users\Bence\.virtualenvs\wickie-Qv0iMXBj\lib\site-packages\notion\monitor.py", line 218, in _refresh_updated_records
    self.client.refresh_collection_rows(collection_id)
  File "C:\Users\Bence\.virtualenvs\wickie-Qv0iMXBj\lib\site-packages\notion\client.py", line 141, in refresh_collection_rows
    row_ids = self.search_pages_with_parent(collection_id)
  File "C:\Users\Bence\.virtualenvs\wickie-Qv0iMXBj\lib\site-packages\notion\client.py", line 213, in search_pages_with_parent
    self._store.store_recordmap(response["recordMap"])
  File "C:\Users\Bence\.virtualenvs\wickie-Qv0iMXBj\lib\site-packages\notion\store.py", line 292, in store_recordmap
    table, id, value=record.get("value"), role=record.get("role")
  File "C:\Users\Bence\.virtualenvs\wickie-Qv0iMXBj\lib\site-packages\notion\store.py", line 207, in _update_record
    expand=True,
  File "C:\Users\Bence\.virtualenvs\wickie-Qv0iMXBj\lib\site-packages\dictdiffer\__init__.py", line 217, in _diff_recursive
    for diffed in recurred:
  File "C:\Users\Bence\.virtualenvs\wickie-Qv0iMXBj\lib\site-packages\dictdiffer\__init__.py", line 217, in _diff_recursive
    for diffed in recurred:
  File "C:\Users\Bence\.virtualenvs\wickie-Qv0iMXBj\lib\site-packages\dictdiffer\__init__.py", line 217, in _diff_recursive
    for diffed in recurred:
  File "C:\Users\Bence\.virtualenvs\wickie-Qv0iMXBj\lib\site-packages\dictdiffer\__init__.py", line 264, in _diff_recursive
    (key, deepcopy(_first[key]))]
  File "C:\Users\Bence\.virtualenvs\wickie-Qv0iMXBj\lib\copy.py", line 150, in deepcopy
    y = copier(x, memo)
  File "C:\Users\Bence\.virtualenvs\wickie-Qv0iMXBj\lib\copy.py", line 240, in _deepcopy_dict
    y[deepcopy(key, memo)] = deepcopy(value, memo)
  File "C:\Users\Bence\.virtualenvs\wickie-Qv0iMXBj\lib\copy.py", line 180, in deepcopy
    y = _reconstruct(x, memo, *rv)
  File "C:\Users\Bence\.virtualenvs\wickie-Qv0iMXBj\lib\copy.py", line 280, in _reconstruct
    state = deepcopy(state, memo)
  File "C:\Users\Bence\.virtualenvs\wickie-Qv0iMXBj\lib\copy.py", line 150, in deepcopy
    y = copier(x, memo)
  File "C:\Users\Bence\.virtualenvs\wickie-Qv0iMXBj\lib\copy.py", line 240, in _deepcopy_dict
    y[deepcopy(key, memo)] = deepcopy(value, memo)
  File "C:\Users\Bence\.virtualenvs\wickie-Qv0iMXBj\lib\copy.py", line 180, in deepcopy
    y = _reconstruct(x, memo, *rv)
  File "C:\Users\Bence\.virtualenvs\wickie-Qv0iMXBj\lib\copy.py", line 280, in _reconstruct
    state = deepcopy(state, memo)
  File "C:\Users\Bence\.virtualenvs\wickie-Qv0iMXBj\lib\copy.py", line 150, in deepcopy
    y = copier(x, memo)
  File "C:\Users\Bence\.virtualenvs\wickie-Qv0iMXBj\lib\copy.py", line 240, in _deepcopy_dict
    y[deepcopy(key, memo)] = deepcopy(value, memo)
  File "C:\Users\Bence\.virtualenvs\wickie-Qv0iMXBj\lib\copy.py", line 180, in deepcopy
    y = _reconstruct(x, memo, *rv)
  File "C:\Users\Bence\.virtualenvs\wickie-Qv0iMXBj\lib\copy.py", line 280, in _reconstruct
    state = deepcopy(state, memo)
  File "C:\Users\Bence\.virtualenvs\wickie-Qv0iMXBj\lib\copy.py", line 150, in deepcopy
    y = copier(x, memo)
  File "C:\Users\Bence\.virtualenvs\wickie-Qv0iMXBj\lib\copy.py", line 240, in _deepcopy_dict
    y[deepcopy(key, memo)] = deepcopy(value, memo)
  File "C:\Users\Bence\.virtualenvs\wickie-Qv0iMXBj\lib\copy.py", line 180, in deepcopy
    y = _reconstruct(x, memo, *rv)
  File "C:\Users\Bence\.virtualenvs\wickie-Qv0iMXBj\lib\copy.py", line 280, in _reconstruct
    state = deepcopy(state, memo)
  File "C:\Users\Bence\.virtualenvs\wickie-Qv0iMXBj\lib\copy.py", line 150, in deepcopy
    y = copier(x, memo)
  File "C:\Users\Bence\.virtualenvs\wickie-Qv0iMXBj\lib\copy.py", line 240, in _deepcopy_dict
    y[deepcopy(key, memo)] = deepcopy(value, memo)
  File "C:\Users\Bence\.virtualenvs\wickie-Qv0iMXBj\lib\copy.py", line 141, in deepcopy
    d = id(x)
RecursionError: maximum recursion depth exceeded while calling a Python object

I get this error every time the monitor tries to poll, or I set a variable. Setting a variable eventually raises a RecursionError inside run_local_operation.

Status during upload

Hello,

Is there a way to print the current state in % during the upload of file ? Usecase upload of a large video or image file.

Color only particular words

It would be cool to introduce something in the markdown syntax to make it possible to color particular words - instead of a whole block.

How to upload a file?

HI, it's me again! thanks for the awesome work you've put in this project, it's been really useful to me!

i'm trying to upload an image:

img_block = page.add_new(ImageBlock)
img_block.upload_file(fp.name)

but i get a an error:

requests.exceptions.HTTPError: 501 Server Error: Not Implemented for url: https://s3.us-west-2.amazonaws.com/secure.notion-static.com/

Am i doing it wrong, or is there any other problem?

Android Termux: "ImportError: cannot import name 'SemLock' from '_multiprocessing'"

Not so much an issue as a request.
I have it running perfectly on raspberry pi, it's awesome!

I'm a huge fan of Tasker on Android, and there's a terminal emulator for Android that works well with Tasker that can run python scripts called Termux, however, when I attempt to run notion-py I get this error:

>>> from notion.client import NotionClient              
>>> client = NotionClient(token_v2="AlmostForgotToTakeOutMyTokenHeh")
Traceback (most recent call last):
  File "/data/data/com.termux/files/usr/lib/python3.7/multiprocessing/synchronize.py", line 28, in <module>
    from _multiprocessing import SemLock, sem_unlink
ImportError: cannot import name 'SemLock' from '_multiprocessing' (/data/data/com.termux/files/usr/lib/python3.7/lib-dynload/_multiprocessing.cpython-37m.so)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/data/data/com.termux/files/usr/lib/python3.7/site-packages/notion/client.py", line 39, in __init__
    self._store = RecordStore(self, cache_key=cache_key)
  File "/data/data/com.termux/files/usr/lib/python3.7/site-packages/notion/store.py", line 78, in __init__
    self._mutex = Lock()
  File "/data/data/com.termux/files/usr/lib/python3.7/multiprocessing/context.py", line 66, in Lock
    from .synchronize import Lock
  File "/data/data/com.termux/files/usr/lib/python3.7/multiprocessing/synchronize.py", line 32, in <module>
    " synchronization primitives needed will not" +
ImportError: This platform lacks a functioning sem_open implementation, therefore, the required synchronization primitives needed will not function, see issue 3770.

I've found a similar issue on a different project that may have a solution, but it requires changing the code. I fear I'm not adept enough with Python to make these changes to your script...
Here's the link to the other issue
asciinema/asciinema#271

I'll keep digging, but I appreciate any help. It would be phenomenal to have this script running on Android.

De-activable Markdown conversion in CodeBlock

While using markdown_to_notion() for the title property makes lot's of sense for most blocks, it might be an issue for CodeBlock.

I saw that property_map has a flag for markdown conversion but it's not accessible from the block initialisation.

def property_map(name, python_to_api=lambda x: x, api_to_python=lambda x: x, markdown=True):

Create new tags in collection

How can we create a new possible tags in a select or multi-selct collection property ?
I see the schema involves unique ID for each tags ...

cv.collection.get_schema_property('tags')
{'id': '.$<t',
 'slug': 'tags',
 'name': 'Tags',
 'type': 'multi_select',
 'options': [{'id': '0f1e3982-3d0f-4821-a26e-7674635fc6c0',
   'color': 'yellow',
   'value': 'tagA'}]}

So, how can we add a new tag as the naive approche collection.tags = newtag raises a ValueError ?

ValueError: Value 'tagB' not acceptable for property 'tags' (valid options: ['tagB'])

Adding "Discussion" and "Comment" capabilities

First of all, I wanted to say thank you for this implementation of the Notion API 🙌
It is really really useful, it is crazy all the automation opportunities it brings and shows the real potential of Notion.

I was wondering if you had explored the possibility to handle Discussion and Comment? Allowing to open a new Discussion on a block, to add a Comment to a Discussion – and even to listen to new Comments – like you manage to do it with live-update of Blocks?

Once again, thank you for this repo – it is already really amazing 👍

Is there a quick way to extract all rows from table with column data?

Now I use something like that:

client.get_collection_view(collection_view_url)
all_rows = list(collection_view.build_query().execute())
all_rows_data = [row.get_all_properties() for row in all_rows]

But it's extremely slow (~1m for each row). Are there any other ways to do it?

Performance of local store cache doesn't scale well

As noted in #24, performance degrades with large databases or when many blocks have been loaded due to the local cache we maintain of all data. Right now, these are saved into large JSON files, which adds a lot of overhead when reading/writing/updating.

This cache is important both for minimizing requests to the server and for ensuring that when we have monitoring enabled we don't spuriously fire off callbacks when nothing has actually changed (after an app restart).

Better performance could be had by switching to an approach such as:

  • Separate JSON files per record
  • SQLite database backend with a row per record
  • ?

Add global search capabilities

I am interested if we can add the ability to programatically search (like cmd/ctrl + p) through all block. I checked and there is a specific API call (https://www.notion.so/api/v3/searchBlocks) that is used, although the output is a little more puzzling on how it narrows down to the shown content.

It would be neat to some how surface the block's title/type and the content (much like how the search modal works now).

Collection column/property order and hide status

How can I get the display order of a table properties/columns ? And hide status ?
When accessing a View

  • get_all_properties() is not ordered
  • schema doesn't have any index or order

Am I missing something ?

PS: Great work. Thanks very much.

Polling is broken

Hi.First, thank you for the library, very helpful!
Second, I was experiencing lines in a log like
2019-06-24 01:43:26,076 - ERROR - Persistent error submitting polling request: b'{"code":1,"message":"Session ID unknown"}' / 400 Client Error: Bad Request for url: https://msgstore.www.notion.so/primus/?sessionId=SESSION_ID&EIO=3&transport=polling&sid=SID (will retry 5 more times)

Also, CPU was always 100% busy and I saw that caching was working in a very strange manner. Looked like the data was constantly re-written: I monitored the sizes of cache files, and they were growing to some point, then reset back to zero and then the growth repeated with the reset in the end and so on. My system monitoring shown that for about an hour the amount of data the process written to disk was about 15 gigs, but no such network activity was seen for this process at the same time.

When I restarted the shell and created a client with
monitor=False,
this stopped and everything went fine.

400 error when getting collection view

Tracceback is below. Quick start instructions worked fine on a page, but when trying to pick up a database/collection I get the following. Any suggestions, and thanks!

File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python3.7/site-packages/notion/client.py", line 98, in get_collection_view
    collection = self.get_block(block_id, force_refresh=force_refresh).collection
  File "/usr/local/lib/python3.7/site-packages/cached_property.py", line 35, in __get__
    value = obj.__dict__[self.func.__name__] = self.func(obj)
  File "/usr/local/lib/python3.7/site-packages/notion/block.py", line 618, in collection
    self._collection = self._client.get_collection(collection_id)
  File "/usr/local/lib/python3.7/site-packages/notion/client.py", line 70, in get_collection
    return Collection(self, collection_id) if coll else None
  File "/usr/local/lib/python3.7/site-packages/notion/collection.py", line 104, in __init__
    self._client.refresh_collection_rows(self.id)
  File "/usr/local/lib/python3.7/site-packages/notion/client.py", line 115, in refresh_collection_rows
    row_ids = self.search_pages_with_parent(collection_id)
  File "/usr/local/lib/python3.7/site-packages/notion/client.py", line 172, in search_pages_with_parent
    response = self.post("searchPagesWithParent", data).json()
  File "/usr/local/lib/python3.7/site-packages/notion/client.py", line 126, in post
    raise HTTPError(response.json().get("message", "There was an error (400) submitting the request."))
requests.exceptions.HTTPError: Invalid input.

Callback issue (how to properly use them

Trying to add callbacks and watch for changes in all row blocks of a collection view, but nothing is happening; the script just displays all my rows regardless and then ends. I was expecting to change a title on a record and then see the printed line below. See code below:

Note: I also tried just calling add_callback(my_callback) instead of calling the my_callback function but then it simply doesn't return anything and ends.

def watch_completed_milestones():

	cv = client.get_collection_view(
		"https://www.notion.so/jayray/c064538161ae4a9312353688ebe5f8ce?v=cc43d90823954c61beced9804e1e1fc1"
	)

	def my_callback(record):
		print("The record's title is now:", record.title)

	for block_row in cv.collection.get_rows() : 
		block_row.add_callback(my_callback(block_row))

enable_debugging()
watch_completed_milestones()

os.makedirs not callable on aws lambda

on write-only containers (with temp dirs) like lambda instances, DATADIR and the likes should be created in tmp/<path>. I don't see much downside in doing this in all cases. will be forking for my own purposes, lmk if I should open a PR

Notion Webclipper functionality

The webclipper is convenient because it also saves the content of the page.

I'd love to be able to programmatically request Notion to parse and save the contents of links into a Notion table.

Background: I want any link I store into my Pocket account to also be available in my Notion list (especially the content).

delete rows

First, thank you for creating such a good library.

Is there a way to clear all the rows in Collection(notion table)?

I've confirmed using collection.add_row () to create a row, but I do not know how to delete some or all rows.

Thanks

Normalised option for get_all_properties()

Would be fairly useful to allow .get_all_properties() the possibility of passing parameters in regards how certain default fields to be handled: such as User class to be always interpreted and converted to User.full_name().

Is it possible under the current format ?

Kind Regards

API to get the pages hierarchy

I am looking for a way to get the list of all the pages in my notion space as well as their place in the hierarchy. I started by calling the client.get_space and then using the resulting pages field:

# pip install notion
from notion.client import NotionClient

client = NotionClient(token_v2="some_token")

space = client.get_space('my_space_id')

for page_id in space.pages:
    page = client.get_block(page_id)
    print(page.title)

Now that I get all the childs of my root/space, is there a way to continue the recursion and get the child sub pages?

Notion Client error

When running notion for the first time it throws several errors and is unable to connect to notion.so

requests.exceptions.SSLError: HTTPSConnectionPool(host='msgstore.www.notion.so', port=443): Max retries exceeded with url: /primus/?sessionId=aa6704e9-e9e7-45bc-a5bb-e59121633ca8&EIO=3&transport=polling (Caused by SSLError(SSLError(1, '[SSL: TLSV1_ALERT_PROTOCOL_VERSION] tlsv1 alert protocol version (_ssl.c:645)'),))

I'm using the Quickstart tutorial in the Readme so I'm not sure what's going on here

Get all pages

Is there a way to pass a token and get all pages for a user?
most of the methods require you to pass a url to get the ID of the blocks and then interact with them but was wondering if there could be a higher function to get everything in a user workspace.

been trying to understand whats the real endpoint the api hits and from what i can see its something like notion.so/api/v3 but then what else is passed down to it? could that be something on the wiki page for people to test out things?

CollectionView `default_query` fails on keyword argument 'group_by'

While trying this example in the README,

from notion.client import NotionClient
client = NotionClient(token_v2='<redacted>')
cv_url = 'https://www.notion.so/learningequality/fc389a273bd2459cb72eeba3ff67ff3e?v=8bb74160eefd45bcb89cf3905dbf026c'
cv = client.get_collection_view(cv_url)
cv.default_query()

I got this error:

TypeError                                 Traceback (most recent call last)
<ipython-input-2-c4d34340d2a7> in <module>()
----> 1 cv.default_query()

~/Projects/FLECode/cloud-chef/venv/lib/python3.6/site-packages/notion/collection.py in default_query(self)
     88 
     89     def default_query(self):
---> 90         return self.build_query(**self.get("query", {}))
     91 
     92 

~/Projects/FLECode/cloud-chef/venv/lib/python3.6/site-packages/notion/collection.py in build_query(self, **kwargs)
     85 
     86     def build_query(self, **kwargs):
---> 87         return CollectionQuery(collection=self.collection, collection_view=self, **kwargs)
     88 
     89     def default_query(self):

TypeError: __init__() got an unexpected keyword argument 'group_by'

View has no filters, no sort orders, and the following Group By field:

screen shot 2018-12-18 at 10 46 58 pm

Errors in set_value internals reference identifier that does not exist in scope

https://github.com/jamalex/notion-py/blob/master/notion/collection.py#L372

throws

  File "/home/adjective/.local/share/virtualenvs/notion-strip-7FiIVxN3/src/notion/notion/collection.py", line 373, in set_property
    path, val = self._convert_python_to_notion(val, prop)
  File "/home/adjective/.local/share/virtualenvs/notion-strip-7FiIVxN3/src/notion/notion/collection.py", line 392, in _convert_python_to_notion
    .format(val, identifier, valid_options))
NameError: name 'identifier' is not defined

API to create new select item

I want to add some new select item/tag to the select/multi-select property of a page. Is there anyway to do that ?

Unresponsive and repeat file changes at running code

If you attempt to fetch information using get_block, get_collection_view on a Specific full page, the code will not work and will remain stuck.
And ****_values.json, ****_role.json in the ~/.notion-py/cache folder are changed from time to time.

PS. The specific page type is Database and Full-Page, and more than 5000 internal items

How can I fix it?

Using get_block(page_url)

get_block

Using get_collection_view(page_url)

get_collection

But collection_view is return None

You can not see that ****_role.json changes in the picture, but sometimes this file is changed like the ****_values.json file.

Add the information below.

pip list


$ pip list
notion          0.0.21
pip             19.1.1
python-slugify  3.0.2
pytz            2019.1
requests        2.21.0
setuptools      41.0.1
soupsieve       1.9.1
text-unidecode  1.2
tzlocal         2.0.0b1
urllib3         1.24.3
wheel           0.33.1

Code


from notion.client import NotionClient

client = NotionClient(token_v2="blank")
page = client.get_collection("blank") # return None
# page = client.get_collection_view("blank") not response
# page = client.get_block("blank") not response

print(page)

If force shutdown while stopped, the following error appears.

Running at get_block


^CTraceback (most recent call last):
  File "main.py", line 7, in <module>
    print(page)
  File "/Users/ted/Workspace/tmp/notion-py/venv/lib/python3.7/site-packages/notion-0.0.21-py3.7.egg/notion/records.py", line 35, in __str__
    return ", ".join(["{}={}".format(field, repr(getattr(self, field))) for field in self._str_fields() if getattr(self, field, "")])
  File "/Users/ted/Workspace/tmp/notion-py/venv/lib/python3.7/site-packages/notion-0.0.21-py3.7.egg/notion/records.py", line 35, in <listcomp>
    return ", ".join(["{}={}".format(field, repr(getattr(self, field))) for field in self._str_fields() if getattr(self, field, "")])
  File "/Users/ted/Workspace/tmp/notion-py/venv/lib/python3.7/site-packages/notion-0.0.21-py3.7.egg/notion/block.py", line 634, in title
    return self.collection.name
  File "/Users/ted/Workspace/tmp/notion-py/venv/lib/python3.7/site-packages/cached_property-1.5.1-py3.7.egg/cached_property.py", line 35, in __get__
    value = obj.__dict__[self.func.__name__] = self.func(obj)
  File "/Users/ted/Workspace/tmp/notion-py/venv/lib/python3.7/site-packages/notion-0.0.21-py3.7.egg/notion/block.py", line 623, in collection
    self._collection = self._client.get_collection(collection_id)
  File "/Users/ted/Workspace/tmp/notion-py/venv/lib/python3.7/site-packages/notion-0.0.21-py3.7.egg/notion/client.py", line 73, in get_collection
    return Collection(self, collection_id) if coll else None
  File "/Users/ted/Workspace/tmp/notion-py/venv/lib/python3.7/site-packages/notion-0.0.21-py3.7.egg/notion/collection.py", line 104, in __init__
    self._client.refresh_collection_rows(self.id)
  File "/Users/ted/Workspace/tmp/notion-py/venv/lib/python3.7/site-packages/notion-0.0.21-py3.7.egg/notion/client.py", line 118, in refresh_collection_rows
    row_ids = self.search_pages_with_parent(collection_id)
  File "/Users/ted/Workspace/tmp/notion-py/venv/lib/python3.7/site-packages/notion-0.0.21-py3.7.egg/notion/client.py", line 174, in search_pages_with_parent
    self._store.store_recordmap(response["recordMap"])
  File "/Users/ted/Workspace/tmp/notion-py/venv/lib/python3.7/site-packages/notion-0.0.21-py3.7.egg/notion/store.py", line 237, in store_recordmap
    self._update_record(table, id, value=record.get("value"), role=record.get("role"))
  File "/Users/ted/Workspace/tmp/notion-py/venv/lib/python3.7/site-packages/notion-0.0.21-py3.7.egg/notion/store.py", line 176, in _update_record
    self._save_cache("_values")
  File "/Users/ted/Workspace/tmp/notion-py/venv/lib/python3.7/site-packages/notion-0.0.21-py3.7.egg/notion/store.py", line 139, in _save_cache
    json.dump(getattr(self, attribute), f)
  File "/usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7/lib/python3.7/json/__init__.py", line 180, in dump
    fp.write(chunk)
KeyboardInterrupt

Running at get_collection_view


^CTraceback (most recent call last):
  File "main.py", line 4, in <module>
    page = client.get_collection_view("blank")
  File "/Users/ted/Workspace/tmp/notion-py/venv/lib/python3.7/site-packages/notion-0.0.21-py3.7.egg/notion/client.py", line 101, in get_collection_view
    collection = self.get_block(block_id, force_refresh=force_refresh).collection
  File "/Users/ted/Workspace/tmp/notion-py/venv/lib/python3.7/site-packages/cached_property-1.5.1-py3.7.egg/cached_property.py", line 35, in __get__
    value = obj.__dict__[self.func.__name__] = self.func(obj)
  File "/Users/ted/Workspace/tmp/notion-py/venv/lib/python3.7/site-packages/notion-0.0.21-py3.7.egg/notion/block.py", line 623, in collection
    self._collection = self._client.get_collection(collection_id)
  File "/Users/ted/Workspace/tmp/notion-py/venv/lib/python3.7/site-packages/notion-0.0.21-py3.7.egg/notion/client.py", line 73, in get_collection
    return Collection(self, collection_id) if coll else None
  File "/Users/ted/Workspace/tmp/notion-py/venv/lib/python3.7/site-packages/notion-0.0.21-py3.7.egg/notion/collection.py", line 104, in __init__
    self._client.refresh_collection_rows(self.id)
  File "/Users/ted/Workspace/tmp/notion-py/venv/lib/python3.7/site-packages/notion-0.0.21-py3.7.egg/notion/client.py", line 118, in refresh_collection_rows
    row_ids = self.search_pages_with_parent(collection_id)
  File "/Users/ted/Workspace/tmp/notion-py/venv/lib/python3.7/site-packages/notion-0.0.21-py3.7.egg/notion/client.py", line 174, in search_pages_with_parent
    self._store.store_recordmap(response["recordMap"])
  File "/Users/ted/Workspace/tmp/notion-py/venv/lib/python3.7/site-packages/notion-0.0.21-py3.7.egg/notion/store.py", line 237, in store_recordmap
    self._update_record(table, id, value=record.get("value"), role=record.get("role"))
  File "/Users/ted/Workspace/tmp/notion-py/venv/lib/python3.7/site-packages/notion-0.0.21-py3.7.egg/notion/store.py", line 166, in _update_record
    with self._mutex:
  File "/usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7/lib/python3.7/multiprocessing/synchronize.py", line 95, in __enter__
    return self._semlock.__enter__()
KeyboardInterrupt

get_rows() returns empty result, but build_query().execute() is not

I have a simple notion database (in table view) and try to retrieve all rows.
collection_view.collection.get_rows returns an empty list, but if I try to retrieve row with collection_view.build_query().execute() it working. I have this issue on a few tables, but other works well with get_rows.

Do you have any ideas about why it happens?

Can programmatically disable logging using FileHandler

Use Case: I tried to run notion-py on Lambda and there is an error. The error is caused by the notion-py logger trying to write to disk. I resolved the error by replacing logging.FileHandler with logging.NullHandler. It would be nice if we can configure log setting programatically.

add_new doesn't handle markdown for nested blocks

Description

The readme mentions that the add_new method handles markdown formatting (and it does for some markdown elements like bold and italics) but it doesn't handle markdown for other "nested" blocks

page.children.add_new(TextBlock, title='Testing 1, 2, 3...\n - Testing \n - Another \n * [ ] Heres one more')

Results in 1 block of text:

Testing 1, 2, 3...

Testing

Another
[ ] Heres one more

Ideally it should result in 4 blocks like this:

Testing 1, 2, 3...

  • Testing
  • Another
  • Heres one more

I've done some testing and these are the supported/unsupported markdown lists that I can gather:

Supported Markdown

  • Bold
  • Italics
  • Links
  • Inline code (code)

Unsupported Markdown

  • Todos
  • Lists (bullets, ordered lists, etc.)
  • Headers
  • Multi-line code blocks
  • Horizontal rules

Since TextBlock is explicitly text, the current behavior makes sense, however maybe a new Block class is a better approach to handle the unique markdown parsing. Something like:

page.children.add_new(MarkdownBlock, title='Testing 1, 2, 3...\n - Testing \n - Another \n * [ ] Heres one more')

What does the keys of the collection schema mean?

I'm trying to understand the roles of the keys in the collection schema:

"OBcJ": {"name": "Where to?", "type": "url"},
"dV$q": {"name": "Files", "type": "file"},
"title": {"name": "Name", "type": "title"}

i've tried my best to decipher those characters, but i'm out of ideas!

update rows

Thanks for your library

How can i update row's value in colleciton?

Please let me know how to do it.

How to embed image in DriveBlock?

I have an image on Google Drive I'd like to embed into a DriveBlock. On the UI client, I can simply set the URL on the block pointing to the Drive page containing the image.

I can create the DriveBlock, but when I try to use set_source_url, manually assign source, nothing happens.

block sometimes needs an initial refresh to get most recent information

Sometimes edits to blocks made in the Notion web UI are not immediately available through the API.

For example, create a sample script test.py like:

client = NotionClient("[TOKEN]", monitor=False) 
page = client.get_block("[PAGE URL]")
print(page.title)
page.refresh()
print(page.title)

Create new page with title "A" and run the script on it. It will output

A
A

Edit the title to "B" in the web UI and re-run. It will output

A
B

I would expect it to output

B
B

Instantiation of NotionClient fails

After trying to follow the basic tutorial in the README, I encountered an error.

Traceback (most recent call last):
  File "C:\Python34\lib\site-packages\notion\store.py", line 111, in _load_cache
    with open(self._get_cache_path(attr)) as f:
FileNotFoundError: [Errno 2] No such file or directory: 'C:\\Users\\username\\.notion-py\\cache\\edf90714b95fc442ac97965a8e2fb37795a647ed342bd73df657bcd3c6a016ea_values.json'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File ".\index.py", line 3, in <module>
    client = NotionClient(token_v2='xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
  File "C:\Python34\lib\site-packages\notion\client.py", line 33, in __init__
    self._store = RecordStore(self, cache_key=cache_key)
  File "C:\Python34\lib\site-packages\notion\store.py", line 82, in __init__
    self._load_cache()
  File "C:\Python34\lib\site-packages\notion\store.py", line 117, in _load_cache
    except (FileNotFoundError, json.JSONDecodeError):
AttributeError: 'module' object has no attribute 'JSONDecodeError'

Any ideas as to what could be causing this?

Files not accessible

When I try reading a file field from a collection, it returns a URL.

When I visit this url, I get this error:
image

Color issue

So I've been messing around with changing color.

page = client.get_block("https://www.notion.so/598d04a5d54a40d3b93637f158420039") page.set('format.block_color', 'green_background')

I haven't tried every color, but it seems that most of them are working except 'green' and 'green_background'

Any advice?

CollectionRowBlock property name inconsistency

I'm working with a collection, and I'd like to know the key names (properties) for the rows in that collection. Consider the following helper methods:

def _print_keys(coll):
    """
    One way to get properties keys for a collection.
    """
    first_row = coll.get_rows()[0]
    return list(first_row.get_all_properties().keys())

def _print_keys_alt(coll):
    """
    Another way to get properties keys for a collection.
    """
    props = pipeline_coll.get_schema_properties()
    return [prop['slug'] for prop in props]

When running on the Chef Pipeline board collection, I get the following outputs:

>>> pipeline_coll = client.get_collection('c70d925f-fd8f-4134-a26b-0b71a14bd6ff')

>>> _print_keys(pipeline_coll)
['license(s)',
 'source_url',
 ...

>>> _print_keys_alt(pipeline_coll)
['license_s',
 'source_url',
  ...

Expected

first_row.get_all_properties().keys() to be the same as slug or name of the property.

Observed

The key license(s) seems like lowercase version of the property name

Context

I was trying to find the property "accessor" method for getting properties and tried all of these options:

    row.get('license(s)'),  # None
    row.get('License(s)'),  # None
    row.get('license_s'),  # None
    row.get(['license(s)']),  # None
    row.get(['License(s)']), # None
    row.get(['license_s']),  # None
    row['license(s)'],  # raises since no __getitem__
    row['License(s)'],  # raises since no __getitem__
    row['license_s'],  # raises since no __getitem__

before learning about the row.get_property method which works with all three identifiers:

    row.get_property('license(s)'),  # GOOD
    row.get_property('License(s)'),  # GOOD
    row.get_property('license_s'),   # GOOD

@jamalex Would it make sense to define a get and/or __getitem__ as alias for get_property for CollectionRowBlock objects?

AttributeError: object 'datetime.date' has no attribute 'strptime'

traceback

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-12-f7fc1e88a7f7> in <module>()
      4     # 'version', 'is_public', 'demoserver_url', 'languages', 'has_stage_tree',
      5     # 'original_channel', 'column_1', 'name']
----> 6     ch = channel.get_all_properties()
      7     if ch['derivative_channels']:
      8         continue  # skip original channels (use derivative channel instead)

~/Projects/FLECode/cloud-chef/venv/lib/python3.6/site-packages/notion/collection.py in get_all_properties(self)
    353         for prop in self.schema:
    354             propid = prop["name"].lower().replace(" ", "_")
--> 355             allprops[propid] = self.get_property(propid)
    356         return allprops
    357 

~/Projects/FLECode/cloud-chef/venv/lib/python3.6/site-packages/notion/collection.py in get_property(self, identifier)
    285         val = self.get(["properties", prop["id"]])
    286 
--> 287         return self._convert_notion_to_python(val, prop)
    288 
    289     def _convert_diff_to_changelist(self, difference, old_val, new_val):

~/Projects/FLECode/cloud-chef/venv/lib/python3.6/site-packages/notion/collection.py in _convert_notion_to_python(self, val, prop)
    333             val = val[0][0] if val else ""
    334         if prop["type"] in ["date"]:
--> 335             val = NotionDate.from_notion(val)
    336         if prop["type"] in ["file"]:
    337             val = [add_signed_prefix_as_needed(item[1][0][1], client=self._client) for item in val if item[0] != ","] if val else []

~/Projects/FLECode/cloud-chef/venv/lib/python3.6/site-packages/notion/collection.py in from_notion(cls, obj)
     32         else:
     33             return None
---> 34         start = cls._parse_datetime(data.get("start_date"), data.get("start_time"))
     35         end = cls._parse_datetime(data.get("end_date"), data.get("end_time"))
     36         timezone = data.get("timezone")

~/Projects/FLECode/cloud-chef/venv/lib/python3.6/site-packages/notion/collection.py in _parse_datetime(cls, date_str, time_str)
     44             return datetime.strptime(date_str + " " + time_str, '%Y-%m-%d %H:%M')
     45         else:
---> 46             return date.strptime(date_str, '%Y-%m-%d')
     47 
     48     def _format_datetime(self, date_or_datetime):

AttributeError: type object 'datetime.date' has no attribute 'strptime'

just saw this is fixed in efe3ab3#diff-ff227cb3ca5f6b2999b69d70551f639a

so @jamalex maybe it's time for a new release on pypi ?

Add new row to collection with Date property value

I'm trying to create a new row in a collection. The collection has a column called 'last_contact' which is a date field. I'm not having any luck passing in a value for the row.last_contact. I've gone through the smoke_test.py, but haven't seen any examples in there. Likewise, I've looked at the collection.py file, at the NotionDate class in particular, but still haven't seen anything.

What's the best way to add a date field to a new row?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.