Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .github/scripts/monitor_notion_changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,7 @@ async def main():
logger.info("Found 1 new entry. Opening issue for it...")
else:
logger.info(
f"Found {len(new_entries_keys)} new entries. "
f"Opening issues for them..."
f"Found {len(new_entries_keys)} new entries. Opening issues for them..."
)
for entry in new_entries:
title = f"New Notion API Changelog Entry: {new_entries[entry]['title']}"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:

strategy:
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]

steps:

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ at the end of the session.

This package supports the following minimum versions:

* Python >= 3.7
* Python >= 3.8
* httpx >= 0.23.0

Earlier versions may still work, but we encourage people building new applications
Expand Down
117 changes: 75 additions & 42 deletions notion_client/api_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,52 +112,41 @@ def delete(self, block_id: str, **kwargs: Any) -> SyncAsync[Any]:


class DatabasesEndpoint(Endpoint):
def list(self, **kwargs: Any) -> SyncAsync[Any]: # pragma: no cover
"""List all [Databases](https://developers.notion.com/reference/database) shared with the authenticated integration.

> ⚠️ **Deprecated endpoint**
def retrieve(self, database_id: str, **kwargs: Any) -> SyncAsync[Any]:
"""Retrieves a [database object](https://developers.notion.com/reference/database) for a provided database ID.

*[🔗 Endpoint documentation](https://developers.notion.com/reference/get-databases)*
*[🔗 Endpoint documentation](https://developers.notion.com/reference/database-retrieve)*
""" # noqa: E501
return self.parent.request(
path="databases",
path=f"databases/{database_id}",
method="GET",
query=pick(kwargs, "start_cursor", "page_size"),
auth=kwargs.get("auth"),
)

def query(self, database_id: str, **kwargs: Any) -> SyncAsync[Any]:
"""Get a list of [Pages](https://developers.notion.com/reference/page) contained in the database.
def update(self, database_id: str, **kwargs: Any) -> SyncAsync[Any]:
"""Update the title or properties of an existing database.

*[🔗 Endpoint documentation](https://developers.notion.com/reference/post-database-query)*
*[🔗 Endpoint documentation](https://developers.notion.com/reference/update-a-database)*
""" # noqa: E501
return self.parent.request(
path=f"databases/{database_id}/query",
method="POST",
query=pick(kwargs, "filter_properties"),
path=f"databases/{database_id}",
method="PATCH",
body=pick(
kwargs,
"filter",
"sorts",
"start_cursor",
"page_size",
"archived",
"parent",
"title",
"description",
"is_inline",
"icon",
"cover",
"in_trash",
"is_locked",
),
auth=kwargs.get("auth"),
)

def retrieve(self, database_id: str, **kwargs: Any) -> SyncAsync[Any]:
"""Retrieve a [Database object](https://developers.notion.com/reference/database) using the ID specified.

*[🔗 Endpoint documentation](https://developers.notion.com/reference/retrieve-a-database)*
""" # noqa: E501
return self.parent.request(
path=f"databases/{database_id}", method="GET", auth=kwargs.get("auth")
)

def create(self, **kwargs: Any) -> SyncAsync[Any]:
"""Create a database as a subpage in the specified parent page.
"""Create a new database.

*[🔗 Endpoint documentation](https://developers.notion.com/reference/create-a-database)*
""" # noqa: E501
Expand All @@ -169,36 +158,72 @@ def create(self, **kwargs: Any) -> SyncAsync[Any]:
"parent",
"title",
"description",
"properties",
"is_inline",
"initial_data_source",
"icon",
"cover",
"is_inline",
),
auth=kwargs.get("auth"),
)

def update(self, database_id: str, **kwargs: Any) -> SyncAsync[Any]:
"""Update an existing database as specified by the parameters.

*[🔗 Endpoint documentation](https://developers.notion.com/reference/update-a-database)*
class DataSourcesEndpoint(Endpoint):
def retrieve(self, data_source_id: str, **kwargs: Any) -> SyncAsync[Any]:
"""Retrieve a [data source](https://developers.notion.com/reference/data-source) object for a provided data source ID.

*[🔗 Endpoint documentation](https://developers.notion.com/reference/retrieve-a-data-source)*
""" # noqa: E501
return self.parent.request(
path=f"databases/{database_id}",
method="PATCH",
path=f"data_sources/{data_source_id}", method="GET", auth=kwargs.get("auth")
)

def query(self, data_source_id: str, **kwargs: Any) -> SyncAsync[Any]:
"""Get a list of [Pages](https://developers.notion.com/reference/page) and/or [Data Sources](https://developers.notion.com/reference/data-source) contained in the data source.

*[🔗 Endpoint documentation](https://developers.notion.com/reference/query-a-data-source)*
""" # noqa: E501
return self.parent.request(
path=f"data_sources/{data_source_id}/query",
method="POST",
query=pick(kwargs, "filter_properties"),
body=pick(
kwargs,
"properties",
"title",
"description",
"icon",
"cover",
"is_inline",
"sorts",
"filter",
"start_cursor",
"page_size",
"archived",
"in_trash",
),
auth=kwargs.get("auth"),
)

def create(self, **kwargs: Any) -> SyncAsync[Any]:
"""Add an additional [data source](https://developers.notion.com/reference/data-source) to an existing [database](https://developers.notion.com/reference/database).

*[🔗 Endpoint documentation](https://developers.notion.com/reference/create-a-data-source)*
""" # noqa: E501
return self.parent.request(
path="data_sources",
method="POST",
body=pick(kwargs, "parent", "properties", "title", "icon"),
auth=kwargs.get("auth"),
)

def update(self, data_source_id: str, **kwargs: Any) -> SyncAsync[Any]:
"""Updates the [data source](https://developers.notion.com/reference/data-source) object of a specified data source under a database.

*[🔗 Endpoint documentation](https://developers.notion.com/reference/update-a-data-source)*
""" # noqa: E501
return self.parent.request(
path=f"data_sources/{data_source_id}",
method="PATCH",
body=pick(
kwargs, "title", "icon", "properties", "in_trash", "archived", "parent"
),
auth=kwargs.get("auth"),
)


class PagesPropertiesEndpoint(Endpoint):
def retrieve(self, page_id: str, property_id: str, **kwargs: Any) -> SyncAsync[Any]:
Expand Down Expand Up @@ -251,7 +276,15 @@ def update(self, page_id: str, **kwargs: Any) -> SyncAsync[Any]:
return self.parent.request(
path=f"pages/{page_id}",
method="PATCH",
body=pick(kwargs, "in_trash", "archived", "properties", "icon", "cover"),
body=pick(
kwargs,
"properties",
"icon",
"cover",
"is_locked",
"in_trash",
"archived",
),
auth=kwargs.get("auth"),
)

Expand Down
8 changes: 5 additions & 3 deletions notion_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
BlocksEndpoint,
CommentsEndpoint,
DatabasesEndpoint,
DataSourcesEndpoint,
PagesEndpoint,
SearchEndpoint,
UsersEndpoint,
Expand All @@ -38,8 +39,8 @@ class ClientOptions:
should be set on each request.
timeout_ms: Number of milliseconds to wait before emitting a
`RequestTimeoutError`.
base_url: The root URL for sending API requests. This can be changed to test with
a mock server.
base_url: The root URL for sending API requests. This can be changed to test
with a mock server.
log_level: Verbosity of logs the instance will produce. By default, logs are
written to `stdout`.
logger: A custom logger.
Expand All @@ -51,7 +52,7 @@ class ClientOptions:
base_url: str = "https://api.notion.com"
log_level: int = logging.WARNING
logger: Optional[logging.Logger] = None
notion_version: str = "2022-06-28"
notion_version: str = "2025-09-03"


class BaseClient:
Expand All @@ -75,6 +76,7 @@ def __init__(

self.blocks = BlocksEndpoint(self)
self.databases = DatabasesEndpoint(self)
self.data_sources = DataSourcesEndpoint(self)
self.users = UsersEndpoint(self)
self.pages = PagesEndpoint(self)
self.search = SearchEndpoint(self)
Expand Down
6 changes: 5 additions & 1 deletion notion_client/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ class HTTPResponseError(Exception):
headers: httpx.Headers
body: str

def __init__(self, response: httpx.Response, message: Optional[str] = None) -> None:
def __init__(
self,
response: httpx.Response,
message: Optional[str] = None,
) -> None:
if message is None:
message = (
f"Request to Notion API failed with status: {response.status_code}"
Expand Down
4 changes: 1 addition & 3 deletions requirements/tests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,4 @@ pytest-asyncio
pytest-cov
pytest-timeout
pytest-vcr

# I'm unable to use vcrpy 6.x cassettes (UnicodeDecodeError on JSON body), let's try again later
vcrpy<6
vcrpy==6.0.2
3 changes: 1 addition & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,11 @@ def get_description():
long_description=get_description(),
long_description_content_type="text/markdown",
packages=["notion_client"],
python_requires=">=3.7, <4",
python_requires=">=3.8, <4",
install_requires=[
"httpx >= 0.23.0",
],
classifiers=[
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
Expand Down
16 changes: 11 additions & 5 deletions tests/cassettes/test_api_async_request_bad_request_error.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,18 @@ interactions:
host:
- mock.httpstatus.io
notion-version:
- '2022-06-28'
- '2025-09-03'
method: GET
uri: https://mock.httpstatus.io/400
response:
content: 400 Bad Request
headers: {}
http_version: HTTP/1.1
status_code: 400
body:
string: 400 Bad Request
headers:
content-length:
- '15'
content-type:
- text/plain
status:
code: 400
message: Bad Request
version: 1
30 changes: 30 additions & 0 deletions tests/cassettes/test_api_http_response_error.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
interactions:
- request:
body: ''
headers:
accept:
- '*/*'
accept-encoding:
- gzip, deflate
authorization:
- ntn_...
connection:
- keep-alive
host:
- mock.httpstatus.io
notion-version:
- '2025-09-03'
method: GET
uri: https://mock.httpstatus.io/400
response:
body:
string: 400 Bad Request
headers:
content-length:
- '15'
content-type:
- text/plain
status:
code: 400
message: Bad Request
version: 1
36 changes: 24 additions & 12 deletions tests/cassettes/test_api_response_error.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,21 @@ interactions:
host:
- api.notion.com
notion-version:
- '2022-06-28'
- '2025-09-03'
method: GET
uri: https://api.notion.com/v1/invalid
response:
content: '{"object":"error","status":400,"code":"invalid_request_url","message":"Invalid
request URL."}'
headers: {}
http_version: HTTP/1.1
status_code: 400
body:
string: '{"object":"error","status":400,"code":"invalid_request_url","message":"Invalid
request URL.","request_id":"8960d1de-371c-42ae-b80a-a0cd522ba425"}'
headers:
Content-Length:
- '145'
Content-Type:
- application/json; charset=utf-8
status:
code: 400
message: Bad Request
- request:
body: ''
headers:
Expand All @@ -36,13 +42,19 @@ interactions:
host:
- api.notion.com
notion-version:
- '2022-06-28'
- '2025-09-03'
method: GET
uri: https://api.notion.com/v1/users
response:
content: '{"object":"error","status":401,"code":"unauthorized","message":"API
token is invalid.","request_id":"15ca8906-fcb1-4d90-b25a-ad03ba508350"}'
headers: {}
http_version: HTTP/1.1
status_code: 401
body:
string: '{"object":"error","status":401,"code":"unauthorized","message":"API
token is invalid.","request_id":"069e7b33-687e-4b20-b798-878437aae90c"}'
headers:
Content-Length:
- '139'
Content-Type:
- application/json; charset=utf-8
status:
code: 401
message: Unauthorized
version: 1
Loading