{"id":13658457,"url":"https://github.com/arturtamborski/notion-py","last_synced_at":"2025-04-24T11:31:42.713Z","repository":{"id":48230061,"uuid":"245887110","full_name":"arturtamborski/notion-py","owner":"arturtamborski","description":"(Fork of) Unofficial Python API client for Notion.so","archived":false,"fork":true,"pushed_at":"2023-10-14T18:47:24.000Z","size":809,"stargazers_count":63,"open_issues_count":13,"forks_count":9,"subscribers_count":6,"default_branch":"master","last_synced_at":"2024-09-07T08:37:07.274Z","etag":null,"topics":["api-client","notion","python3"],"latest_commit_sha":null,"homepage":"https://pypi.org/project/notion-py/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":"jamalex/notion-py","license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/arturtamborski.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null},"funding":{"github":["arturtamborski"],"ko_fi":"arturtamborski"}},"created_at":"2020-03-08T20:58:26.000Z","updated_at":"2024-05-15T02:33:38.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/arturtamborski/notion-py","commit_stats":null,"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arturtamborski%2Fnotion-py","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arturtamborski%2Fnotion-py/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arturtamborski%2Fnotion-py/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arturtamborski%2Fnotion-py/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/arturtamborski","download_url":"https://codeload.github.com/arturtamborski/notion-py/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":223952509,"owners_count":17230900,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api-client","notion","python3"],"created_at":"2024-08-02T05:00:59.720Z","updated_at":"2024-11-10T12:30:16.980Z","avatar_url":"https://github.com/arturtamborski.png","language":"Python","funding_links":["https://github.com/sponsors/arturtamborski","https://ko-fi.com/arturtamborski"],"categories":["Python","HarmonyOS"],"sub_categories":["Windows Manager"],"readme":"\u003c!-- markdownlint-disable no-inline-html first-line-h1 --\u003e\n\n\u003cdiv align=\"center\"\u003e\n  \u003ch1\u003enotion-py\u003c/h1\u003e\n  \u003ch4\u003e(Fork of) Unofficial Python 3 client for Notion.so API v3.\u003c/h4\u003e\n\n  [Documentation][documentation-url]\n  | [Package on PyPI][package-url]\n  \u003cbr\u003e\n  \u003cbr\u003e\n  ![check formatting][check-formatting-url]\n  ![run unit tests][run-unit-tests-url]  \n  ![upload-python-package][upload-python-package-url]\n  ![run-smoke-tests][run-smoke-tests-url]\n  ![documentation-status][documentation-status-url]  \n  ![code-style][code-style-url]\n  ![license][license-url]\n  ![code-size][code-size-url]\n  ![downloads-rate][downloads-rate-url]\n\u003c/div\u003e\n\u003cbr\u003e\n\n \n\n\n---\n\n\u003e **_NOTE:_**  This is a fork of the \n[original repository](https://github.com/jamalex/notion-py)\ncreated by [Jamie Alexandre](https://github.com/jamalex).\n\nYou can try out this package - it's called \n[notion-py](https://pypi.org/project/notion-py/)\non PyPI. The original package created by Jamie is  still online\nunder the name [notion](https://pypi.org/project/notion/) on PyPI,\nso please watch out for any confusion.\n\nimports are still working as before, the `-py` in \nname is there only to differentiate between these two.\n\n---\n \nThese libraries as of now are _not_ fully compatible.  \n(I'm working on sending PRs to the upstream)\n\nList of major differences:\n- imports were changed, especially for blocks and collections.  \n  General rule is:\n  - `notion.block.py       -\u003e  notion.block.*.py`\n  - `notion.collection.py  -\u003e  notion.block.collection.*.py`\n- some block names were changed to align them with notion.so  \n  One of such examples is `TodoBlock -\u003e ToDoBlock` (because it's type is `to_do`)\n- some function definitions also changed  \n  I did that to simplify the API and make it more uniform.\n\n\u003cbr\u003e\n\u003cbr\u003e\n\n\n\n## Features\n- **Automatic conversion between Notion blocks and Python objects**  \n  we covered pretty much every block type there is!\n\n- **Callback system for responding to changes in Notion**  \n  useful for triggering actions, updating another block, etc.\n\n- **Object-oriented interface**  \n  seamless mapping of API response parameters to Python classes/attributes.\n  \n- **Local cache of data in a unified data store**  \n  note: this is disabled by default; add `enable_caching=True` when initializing `NotionClient` to change it.\n  \n- **Real-time reactive two-way data binding**  \n  fancy way of saying that changing Python object will update the Notion UI, and vice-versa.\n\n---\n\n![data binding example][data-binding-url]  \n\u003csup\u003e*(Example of the two-way data binding in action)*\u003c/sup\u003e\n\u003cbr\u003e\n\n\n[Read more about Notion and the original notion-py package on Jamie's blog][introduction-url].\n\n\n## Usage\n\n### Quickstart\n\n\n\u003e **_NOTE:_** The latest version of **notion-py** requires Python 3.6 or greater.\n\n\n`pip install notion-py`\n\n```Python\nfrom notion.client import NotionClient\n\n# Obtain the `token_v2` value by inspecting your browser \n# cookies on a logged-in (non-guest) session on Notion.so\nclient = NotionClient(token_v2=\"123123...\")\n\n# Replace this URL with the URL of the page you want to edit\npage = client.get_block(\"https://www.notion.so/myorg/Test-c0d20a71c0944985ae96e661ccc99821\")\n\nprint(\"The old title is:\", page.title)\n\n# You can use Markdown! We convert on-the-fly \n# to Notion's internal formatted text data structure.\npage.title = \"The title has now changed, and has *live-updated* in the browser!\"\n```\n\n## Getting the token_v2\n\n1. Open [notion.so](https://notion.so) in your browser and log in.\n2. Open up developer console ([quick tutorial the most common browsers][dev-tools-url]).\n3. Find a list of cookies (Firefox: `Storage` -\u003e `Cookies`, Chrome: `Application` -\u003e `Cookies`).\n4. Find the one named `token_v2` and copy its value (lengthy, 160ish characters hex string).\n5. Save it somewhere safe and use it with notion-py!\n\n\u003e **_NOTE:_** Keep the token in secure place and out of your repository!  \n\u003e This token when leaked can let anyone do anything on your notion account!\n\n\n## Updating records\n\nWe keep a local cache of all data that passes through.  \nWhen you reference an attribute on a `Record` (basically\nany `Block`) we first look to that cache to retrieve the value.\nIf it doesn't find it, it retrieves it from the server.\nYou can also manually refresh the data for a `Record`\nby calling the `refresh()` method on it.\n\nBy default (unless we instantiate `NotionClient` \nwith `monitor=False`), we also subscribe to long-polling \nupdates for any instantiated `Record`, so the local cache \ndata for these `Records` should be automatically \nlive-updated shortly after any data changes on the server.  \nThe long-polling happens in a background daemon thread.\n\n\n## Concepts and notes\n  \n- **The tables we currently support are `block`, `space`,\n  `collection`, `collection_view`, and `notion_user`.**\n\n- **We map tables in the Notion database into Python classes**  \n  by subclassing `Record`, with each instance of a class\n  representing a particular record. Some fields from the\n  records (like `title` in the example above) have been\n  mapped to model properties, allowing for easy,\n  instantaneous read/write of the record.\n  Other fields can be read with the `get` method,\n  and written with the `set` method, but then you'll \n  need to make sure to match the internal structures exactly.\n  \n- **Data for all tables are stored in a central RecordStore**  \n  with the `Record` instances not storing state internally,\n  but always referring to the data in the \n  central `RecordStore`.\n  Many API operations return updating versions of a large \n  number of associated records, which we use to update \n  the store, so the data in `Record` instances may sometimes \n  update without being explicitly requested.\n  You can also call the `refresh()` method on a `Record` \n  to trigger an update, or pass `force_update=True` to \n  methods like `get()`.\n  \n- **The API doesn't have strong validation of most data**  \n  so be careful to maintain the structures Notion is expecting.\n  You can view the full internal structure of a record by \n  calling `myrecord.get()` with no arguments.\n  \n- **When you call `client.get_block()`, you can pass in \n  block ID, or the URL of a block**  \n  Note that pages themselves are just `blocks`, as are all \n  the chunks of content on the page. You can get the URL \n  for a block within a page by clicking \"Copy Link\" in the \n  context menu for the block, and pass that URL \n  into `get_block()` as well.\n\n\n\n## Working on a Pull Request\n\nYou'll need `git` and `python3` with `venv` module.  \n\n\nBest way to start is to clone the repo and prepare the `.env` file.\nThis step is optional but nice to have to create healthy python venv.\n\n```bash\ngit https://github.com/arturtamborski/notion-py\n\ncd notion-py\n\ncp .env.example .env\nvim .env\n```\n\nYou should modify the variables as following:\n```bash\n# see above for info on how to get it\nNOTION_TOKEN_V2=\"insert your token_v2 here\"\n\n# used in smoke tests\nNOTION_PAGE_URL=\"insert URL from some notion page here\"\n\n# set it to any level from python logging library\nNOTION_LOG_LEVEL=\"DEBUG\" \n\n# the location for cache, defaults to current directory\nNOTION_DATA_DIR=\".notion-py\"\n```\n\nAnd then load that file (which will also create local venv):\n```bash\nsource .env\n```\n\nOn top of that there's a handy toolbox provided to you via `Makefile`.\nEverything related to the development of the project relies heavily on\nthe interface it provides.\n\nYou can display all commands by running\n```bash\nmake help\n```\n\nWhich should print a nice list of commands avaiable to you.\nThese are compatible with the Github Actions (CI system),\nin fact the actions are using Makefile directly for formatting\nand other steps so everything that Github might show you\nunder your Pull Request can be reproduced locally via Makefile.\n\n\nAlso, there's one very handy shortcut that I'm using all the\ntime when testing the library with smoke tests.\n\nThis command will run a single test unit that you point at\nby passing an argument to `make try-smoke-test` like so:\n\n```bash\nmake try-smoke-test smoke_tests/test_workflow.py::test_workflow_1\n```\n\nThat's super handy when you run some smoke tests and see the failed output:\n```\n============================= short test summary info =============================\nERROR smoke_tests/block/test_basic.py::test_block - KeyboardInterrupt\n!!!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!!\n!!!!!!!!!!!!!!!!!!!! _pytest.outcomes.Exit: Quitting debugger !!!!!!!!!!!!!!!!!!!!!\n================================ 1 error in 32.90s ================================\nmake: *** [Makefile:84: try-smoke-test] Error 2\n```\n\nNotice that `ERROR smoke_tests/...test_basic.py::test_block` - just copy it over\nas a command argument and run it again - you'll run this and only this one test!\n\n```bash\nmake try-smoke-test smoke_tests/block/test_basic.py::test_block\n```\n\n\n\n\n## Examples\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cem\u003eClick here to show or hide\u003c/em\u003e\u003c/summary\u003e  \n\n\n### Example: Traversing the block tree\n\n```Python\nfor child in page.children:\n    print(child.title)\n\nprint(f\"Parent of {page.id} is {page.parent.id}\")\n```\n\n\n### Example: Adding a new node\n\n```Python\nfrom notion.block.basic import ToDoBlock\n\ntodo = page.children.add_new(ToDoBlock, title=\"Something to get done\")\ntodo.checked = True\n```\n\n\n### Example: Deleting nodes\n\n```Python\n# soft-delete\npage.remove()\n\n# hard-delete\npage.remove(permanently=True)\n```\n\n\n### Example: Create an embedded content type (iframe, video, etc)\n\n```Python\nfrom notion.block.upload import VideoBlock\n\nvideo = page.children.add_new(VideoBlock, width=200)\n\n# sets \"property.source\" to the URL\n# and \"format.display_source\" to the embedly-converted URL\nvideo.set_source_url(\"https://www.youtube.com/watch?v=oHg5SJYRHA0\")\n```\n\n\n### Example: Create a new embedded collection view block\n\n```Python\nfrom notion.block.collection.basic import CollectionViewBlock\n\ncollection = client.get_collection(\"\u003csome collection ID\u003e\") # get an existing collection\ncvb = page.children.add_new(CollectionViewBlock, collection=collection)\nview = cvb.views.add_new(view_type=\"table\")\n\n# Before the view can be browsed in Notion, \n# the filters and format options on the view should be set as desired.\n# \n# for example:\n#   view.set(\"query\", ...)\n#   view.set(\"format.board_groups\", ...)\n#   view.set(\"format.board_properties\", ...)\n```\n\n\n### Example: Moving blocks around\n\n```Python\n# move my block to after the video\nmy_block.move_to(video, \"after\")\n\n# move my block to the end of otherblock's children\nmy_block.move_to(otherblock, \"last-child\")\n\n# Note: you can also use \"before\" and \"first-child\" :)\n```\n\n\n### Example: Subscribing to updates\n\n\u003e **_NOTE:_** Notion -\u003e Python automatic updating is \n\u003e currently broken and hence disabled by default.  \n\u003e call `my_block.refresh()` to update, in the meantime,\n\u003e while monitoring is being fixed.\n\nWe can \"watch\" a `Record` so that we get a callback whenever \nit changes. Combined with the live-updating of records based \non long-polling, this allows for a \"reactive\" design, where \nactions in our local application can be triggered in response \nto interactions with the Notion interface.\n\n```Python\n# define a callback (all arguments are optional, just include the ones you care about)\ndef my_callback(record, difference):\n    print(\"The record's title is now:\", record.title)\n    print(\"Here's what was changed:\\n\", difference)\n\n# move my block to after the video\nmy_block.add_callback(my_callback)\n```\n\n\n### Example: Working with databases, aka \"collections\" (tables, boards, etc)\n\nHere's how things fit together:\n- Main container block: `CollectionViewBlock` (inline) / `CollectionViewPageBlock` (full-page)\n    - `Collection` (holds the schema, and is parent to the database rows themselves)\n        - `CollectionBlock`\n        - `CollectionBlock`\n        - ... (more database records)\n    - `CollectionView` (holds filters/sort/etc about each specific view)\n\nFor convenience, we automatically map the database\n\"columns\" (aka properties), based on the schema defined\nin the `Collection`, into getter/setter attributes \non the `CollectionBlock` instances.\n\nThe attribute name is a \"slugified\" version of the name of \nthe column. So if you have a column named \"Estimated value\", \nyou can read and write it via `myrowblock.estimated_value`.\n\nSome basic validation may be conducted, and it will be \nconverted into the appropriate internal format.\n\nFor columns of type \"Person\", we expect a `NotionUser` instance, \nor a list of them, and for a \"Relation\" we expect a singular/list \nof instances of a subclass of `Block`.\n\n```Python\n# Access a database using the URL of the database page or the inline block\ncv = client.get_collection_view(\"https://www.notion.so/myorg/b9076...8b832?v=8de...8e1\")\n\n# List all the records with \"Bob\" in them\nfor row in cv.collection.get_rows(search=\"Bob\"):\n    print(\"We estimate the value of '{}' at {}\".format(row.name, row.estimated_value))\n\n# Add a new record\nrow = cv.collection.add_row()\nrow.name = \"Just some data\"\nrow.is_confirmed = True\nrow.estimated_value = 399\nrow.files = [\"https://www.birdlife.org/sites/default/files/styles/1600/public/slide.jpg\"]\nrow.person = client.current_user\nrow.tags = [\"A\", \"C\"]\nrow.where_to = \"https://learningequality.org\"\n\n# Run a filtered/sorted query using a view's default parameters\nresult = cv.default_query().execute()\nfor row in result:\n    print(row)\n\n# Run an \"aggregation\" query\naggregations = [{\n    \"property\": \"estimated_value\",\n    \"aggregator\": \"sum\",\n    \"id\": \"total_value\",\n}]\nresult = cv.build_query(aggregate=aggregations).execute()\nprint(\"Total estimated value:\", result.get_aggregate(\"total_value\"))\n\n# Run a \"filtered\" query (inspect network tab in browser for examples, on queryCollection calls)\nfilters = {\n    \"filters\": [{\n        \"filter\": {\n            \"value\": {\n                \"type\": \"exact\",\n                \"value\": {\"table\": \"notion_user\", \"id\": client.current_user.id}\n            },\n            \"operator\": \"person_contains\"\n        },\n        \"property\": \"assigned_to\"\n    }],\n    \"operator\": \"and\"\n}\nresult = cv.build_query(filter=filters).execute()\nprint(\"Things assigned to me:\", result)\n\n# Run a \"sorted\" query\nsorters = [{\n    \"direction\": \"descending\",\n    \"property\": \"estimated_value\",\n}]\nresult = cv.build_query(sort=sorters).execute()\nprint(\"Sorted results, showing most valuable first:\", result)\n```\n\n\u003e **_NOTE:_**: You can combine `filter`, `aggregate`, and `sort`.\n\u003e See more examples of queries by setting up complex views in Notion,\n\u003e and then inspecting `cv.get(\"query\")`.\n\n\n### Example: Lock/Unlock A Page\n\n```python\nfrom notion.client import NotionClient\n\nclient = NotionClient(token_v2=\"123123...\")\n\n# Replace this URL with the URL of the page you want to edit\npage = client.get_block(\"https://www.notion.so/myorg/Test-c0d20a71c0944985ae96e661ccc99821\")\n\n# change_lock is a method accessible to every Block/Page in notion.\n# Pass True to lock a page and False to unlock it. \npage.change_lock(True)\npage.change_lock(False)\n```\n\n\n\u003c/details\u003e\n\u003cbr\u003e\n\n\n[documentation-url]: https://notion-py.readthedocs.io\n[package-url]: https://pypi.org/project/notion-py/\n[check-formatting-url]: https://github.com/arturtamborski/notion-py/workflows/Check%20Code%20Formatting/badge.svg\n[run-unit-tests-url]: https://github.com/arturtamborski/notion-py/workflows/Run%20Unit%20Tests/badge.svg\n[upload-python-package-url]: https://github.com/arturtamborski/notion-py/workflows/Upload%20Python%20Package/badge.svg\n[run-smoke-tests-url]: https://github.com/arturtamborski/notion-py/workflows/Run%20Smoke%20Tests/badge.svg\n[code-style-url]: https://img.shields.io/badge/code%20style-black-000000\n[documentation-status-url]: https://readthedocs.org/projects/notion-py/badge/?version=latest\n[license-url]: https://img.shields.io/github/license/arturtamborski/notion-py\n[code-size-url]: https://img.shields.io/github/languages/code-size/arturtamborski/notion-py\n[downloads-rate-url]: https://img.shields.io/pypi/dm/notion-py.svg\n\n[introduction-url]: https://medium.com/@jamiealexandre/introducing-notion-py-an-unofficial-python-api-wrapper-for-notion-so-603700f92369\n[data-binding-url]: https://raw.githubusercontent.com/jamalex/notion-py/master/ezgif-3-a935fdcb7415.gif\n[dev-tools-url]: https://support.airtable.com/hc/en-us/articles/232313848-How-to-open-the-developer-console\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farturtamborski%2Fnotion-py","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Farturtamborski%2Fnotion-py","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farturtamborski%2Fnotion-py/lists"}