An open API service indexing awesome lists of open source software.

https://github.com/ramnes/notion-sdk-py

Notion API client SDK, rewritten in Python! (sync + async)
https://github.com/ramnes/notion-sdk-py

api-client async dataclasses httpx notion notion-api python python-client

Last synced: 16 days ago
JSON representation

Notion API client SDK, rewritten in Python! (sync + async)

Awesome Lists containing this project

README

          

![notion-sdk-py](https://socialify.git.ci/ramnes/notion-sdk-py/image?font=Bitter&language=1&logo=https%3A%2F%2Fupload.wikimedia.org%2Fwikipedia%2Fcommons%2F4%2F45%2FNotion_app_logo.png&owner=1&pattern=Circuit%20Board&theme=Light)



PyPI
Supported Python Versions


License
Code style
Coverage
Package downloads


Code Quality
Tests
Docs


**_notion-sdk-py_ is a simple and easy to use client library for the official
[Notion API](https://developers.notion.com/).**

It is meant to be a Python version of the reference [JavaScript SDK](https://github.com/makenotion/notion-sdk-js),
so usage should be very similar between both. 😊 (If not, please open an issue
or PR!)

## Installation

```shell
pip install notion-client
```

## Usage

> Use Notion's [Getting Started Guide](https://developers.notion.com/docs/getting-started)
> to get set up to use Notion's API.

Import and initialize a client using an **integration token** or an
OAuth **access token**.

```python
import os
from notion_client import Client

notion = Client(auth=os.environ["NOTION_TOKEN"])
```

In an asyncio environment, use the asynchronous client instead:

```python
from notion_client import AsyncClient

notion = AsyncClient(auth=os.environ["NOTION_TOKEN"])
```

Make a request to any Notion API endpoint.

```python
from pprint import pprint

list_users_response = notion.users.list()
pprint(list_users_response)
```

> [!NOTE]
> See the complete list of endpoints in the [API reference](https://developers.notion.com/reference).

or with the asynchronous client:

```python
list_users_response = await notion.users.list()
pprint(list_users_response)
```

This would output something like:

```text
{'results': [{'avatar_url': 'https://secure.notion-static.com/e6a352a8-8381-44d0-a1dc-9ed80e62b53d.jpg',
'id': 'd40e767c-d7af-4b18-a86d-55c61f1e39a4',
'name': 'Avocado Lovelace',
'object': 'user',
'person': {'email': 'avo@example.org'},
'type': 'person'},
...]}
```

All API endpoints are available in both the synchronous and asynchronous clients.

Endpoint parameters are grouped into a single object. You don't need to remember
which parameters go in the path, query, or body.

```python
my_page = notion.data_sources.query(
**{
"data_source_id": "897e5a76-ae52-4b48-9fdf-e71f5945d1af",
"filter": {
"property": "Landmark",
"rich_text": {
"contains": "Bridge",
},
},
}
)
```

### Handling errors

If the API returns an unsuccessful response, an `APIResponseError` will be raised.

The error contains properties from the response, and the most helpful is `code`.
You can compare `code` to the values in the `APIErrorCode` object to avoid
misspelling error codes.

```python
import logging
from notion_client import APIErrorCode, APIResponseError, Client

try:
notion = Client(auth=os.environ["NOTION_TOKEN"])
my_page = notion.data_sources.query(
**{
"data_source_id": "897e5a76-ae52-4b48-9fdf-e71f5945d1af",
"filter": {
"property": "Landmark",
"rich_text": {
"contains": "Bridge",
},
},
}
)
except APIResponseError as error:
if error.code == APIErrorCode.ObjectNotFound:
#
# For example: handle by asking the user to select a different data source
#
...
else:
# Other error handling code
print(error)
```

### Logging

The client emits useful information to a logger. By default, it only emits warnings
and errors.

If you're debugging an application, and would like the client to log request & response
bodies, set the `log_level` option to `logging.DEBUG`.

```python
import logging
from notion_client import Client

notion = Client(
auth=os.environ["NOTION_TOKEN"],
log_level=logging.DEBUG,
)
```

You may also set a custom `logger` to emit logs to a destination other than `stdout`.
Have a look at [Python's logging cookbook](https://docs.python.org/3/howto/logging-cookbook.html)
if you want to create your own logger.

### Client options

`Client` and `AsyncClient` both support the following options on initialization.
These options are all keys in the single constructor parameter.

| Option | Default value | Type | Description |
| ------------ | -------------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `auth` | `None` | `string` | Bearer token for authentication. If left undefined, the `auth` parameter should be set on each request. |
| `log_level` | `logging.WARNING` | `int` | Verbosity of logs the instance will produce. By default, logs are written to `stdout`. |
| `timeout_ms` | `DEFAULT_TIMEOUT_MS` | `int` | Number of milliseconds to wait before emitting a `RequestTimeoutError` |
| `base_url` | `DEFAULT_BASE_URL` | `string` | The root URL for sending API requests. This can be changed to test with a mock server. |
| `logger` | Log to console | `logging.Logger` | A custom logger. |
| `retry` | See [constants](#constants) | `RetryOptions` | Configuration for automatic retries on rate limits (429), service overloads (529), and server errors (500, 503). See [Automatic retries](#automatic-retries) below. |

### Automatic retries

The client automatically retries requests that fail due to rate limiting or
transient server errors. By default, it will retry up to 2 times using
exponential back-off with jitter.

**Retryable errors:**

- `rate_limited` (HTTP 429) - Too many requests; retried for all HTTP methods
- `service_overload` (HTTP 529) - Service overloaded; retried for all HTTP methods
- `internal_server_error` (HTTP 500) - Server error; retried only for GET and DELETE
- `service_unavailable` (HTTP 503) - Service temporarily unavailable;
retried only for GET and DELETE

Server errors (500, 503) are only retried for idempotent HTTP methods
(GET, DELETE) to avoid duplicate side effects. Rate limits (429) and
service overloads (529) are retried for all methods since the server
explicitly asks clients to retry.

**Configuration:**

```python
from notion_client import Client, RetryOptions

notion = Client(
auth="secret_...",
retry=RetryOptions(
max_retries=5, # Maximum retry attempts (default: 2)
initial_retry_delay_ms=500, # Initial delay in ms (default: 1000)
max_retry_delay_ms=60000, # Maximum delay in ms (default: 60000)
),
)
```

To disable automatic retries:

```python
notion = Client(auth="secret_...", retry=False)
```

### Constants

The SDK exports named constants for all default values used by the client, as well
as useful Notion-specific values. You can import them directly:

```python
from notion_client import (
DEFAULT_BASE_URL, # "https://api.notion.com"
DEFAULT_TIMEOUT_MS, # 60_000
DEFAULT_MAX_RETRIES, # 2
DEFAULT_INITIAL_RETRY_DELAY_MS, # 1_000
DEFAULT_MAX_RETRY_DELAY_MS, # 60_000
MIN_VIEW_COLUMN_WIDTH, # 32
)
```

`MIN_VIEW_COLUMN_WIDTH` is the minimum width (in pixels) that a table column can
have in the Notion UI. Set a property's `width` to this value when creating or
updating a view to make a column appear collapsed -- useful for checkbox or
status-as-checkbox columns:

```python
await notion.views.create(
database_id=database_id,
name="My view",
type="table",
configuration={
"table": {
"properties": [
{
"property_id": checkbox_prop_id,
"visible": True,
"width": MIN_VIEW_COLUMN_WIDTH,
},
],
},
},
)
```

### Full API responses

The following functions can distinguish between full and partial API responses.

| Function | Purpose |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `is_full_page` | Determine whether an object is a full [Page object](https://developers.notion.com/reference/page) |
| `is_full_block` | Determine whether an object is a full [Block object](https://developers.notion.com/reference/block) |
| `is_full_data_source` | Determine whether an object is a full [Data source object](https://developers.notion.com/reference/data-source) |
| `is_full_page_or_data_source` | Determine whether an object is a full [Page object](https://developers.notion.com/reference/page) or [Data source object](https://developers.notion.com/reference/data-source) |
| `is_full_user` | Determine whether an object is a full [User object](https://developers.notion.com/reference/user) |
| `is_full_comment` | Determine whether an object is a full [Comment object](https://developers.notion.com/reference/comment-object) |

```python
from notion_client.helpers import is_full_page

full_or_partial_pages = notion.data_sources.query(
data_source_id="897e5a76-ae52-4b48-9fdf-e71f5945d1af"
)

for page in full_or_partial_pages["results"]:
if not is_full_page_or_data_source(page):
continue
print(f"Created at: {page['created_time']}")
```

### Utility functions

This package also exports a few utility functions that are helpful for dealing
with any of the paginated APIs.

#### `iterate_paginated_api(function, **kwargs)`

This utility turns any paginated API into a generator.

**Parameters:**

- `function`: Any function on the Notion client that represents a paginated API
(i.e. accepts `start_cursor`.) Example: `notion.blocks.children.list`.
- `**kwargs`: Arguments that should be passed to the API on the first and
subsequent calls to the API, for example a `block_id`.

**Returns:**

A generator over results from the API.

**Example:**

```python
from notion_client.helpers import iterate_paginated_api

for block in iterate_paginated_api(
notion.blocks.children.list, block_id=parent_block_id
):
# Do something with block.
...
```

#### `collect_paginated_api(function, **kwargs)`

This utility accepts the same arguments as `iterate_paginated_api`, but collects
the results into an in-memory array.

Before using this utility, check that the data you are dealing with is small
enough to fit in memory.

**Parameters:**

- `function`: Any function on the Notion client that represents a paginated API
(i.e. accepts `start_cursor`.) Example: `notion.blocks.children.list`.
- `**kwargs`: Arguments that should be passed to the API on the first and
subsequent calls to the API, for example a `block_id`.

**Returns:**

An array with results from the API.

**Example:**

```python
from notion_client.helpers import collect_paginated_api

blocks = collect_paginated_api(
notion.blocks.children.list, block_id=parent_block_id
)
# Do something with blocks.
```

Both utilities also have async versions: `async_iterate_paginated_api` and
`async_collect_paginated_api`.

### Custom requests

To make requests directly to a Notion API endpoint instead of using the
pre-built families of methods, call the `request()` method. For example:

```python
import json

# POST /v1/comments
response = notion.request(
path="comments",
method="post",
body={
"parent": {"page_id": "5c6a28216bb14a7eb6e1c50111515c3d"},
"rich_text": [{"text": {"content": "Hello, world!"}}],
},
# No `query` params in this example, only `body`.
)

print(json.dumps(response, indent=2))
```

> [!TIP]
> Usually, making custom requests with `notion.request()` isn't necessary, but
> can be helpful in some cases, e.g. when upgrading your [Notion API version](https://developers.notion.com/reference/versioning)
> incrementally before upgrading your SDK version. For example, if there's a new
> or renamed endpoint in the new API version that isn't yet available to call
> via a dedicated method on `Client`.
>
> In the above example, the simpler approach is to use
> `notion.comments.create()`.

Another customization you can make is to pass your own `httpx.Client` or
`httpx.AsyncClient` to the `Client` or `AsyncClient` constructor. This might be
helpful for some execution environments where the default HTTPX client isn't
suitable.

### Verifying webhook signatures

If your integration receives [Notion webhook deliveries](https://developers.notion.com/reference/webhooks),
use `verify_webhook_signature` to confirm each request was sent by Notion and
was not tampered with in transit. Notion signs every delivery with HMAC-SHA256
over the raw request body using the subscription's verification token, and
places the result in the `X-Notion-Signature` header as `sha256=`.

```python
import json
import os

from fastapi import FastAPI, HTTPException, Request
from notion_client import verify_webhook_signature

app = FastAPI()

# The body must be read as the raw bytes that arrived over the wire —
# JSON-parsed and re-serialized bodies will not verify, because
# re-serialization changes whitespace and key order.
@app.post("/notion-webhook")
async def notion_webhook(request: Request):
body = await request.body()
if not verify_webhook_signature(
body=body,
signature=request.headers.get("X-Notion-Signature"),
verification_token=os.environ["NOTION_WEBHOOK_VERIFICATION_TOKEN"],
):
raise HTTPException(status_code=401, detail="invalid signature")

event = json.loads(body)
# ...handle the event
return "ok"
```

The same helper handles the initial verification handshake that Notion sends
when you first register a webhook URL — the handshake body
(`{"verification_token": "..."}`) is signed with the same token, so a single
code path covers both cases.

`sign_webhook_payload(body, verification_token)` produces the same
`sha256=` header value, which is useful for unit-testing your webhook
handler without standing up a real subscription.

## Testing

Run the tests with the `pytest` command. If you want to test against all Python
versions, you can run `tox` instead.

The tests are using `pytest-vcr`'s cassettes for simulating requests to the
Notion API. To create new tests or run them without cassettes, you need to set
up the environment variables `NOTION_TOKEN` and `NOTION_TEST_PAGE_ID` (a page
where your integration has all the capabilities enabled).

The code will use the page at `NOTION_TEST_PAGE_ID` to generate a temporary
environment with the Notion objects to be tested, which will be deleted
at the end of the session.

## Requirements and compatibility

This package supports the following minimum versions:

- Runtime: Python >= 3.8
- httpx >= 0.23.0

Earlier versions may still work, but we encourage people building new applications
to upgrade to the current stable.

## Getting help

If you want to submit a feature request for Notion's API, or are experiencing
any issues with the API platform, please email `developers@makenotion.com`.

If you found a bug with the library, please [submit an issue](https://github.com/ramnes/notion-sdk-py/issues).