Skip to content

Commit e4b9b5e

Browse files
committed
Add database support + refactor as necessary
1 parent 36c81a5 commit e4b9b5e

15 files changed

Lines changed: 635 additions & 96 deletions

File tree

notion/client.py

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
from typing import Any, Dict, Optional
1+
from typing import Any, Dict, List, Optional, Union
22

33
import requests
44

5-
from notion.model import Page
5+
from notion.model.common.utils import UUIDv4
6+
from notion.model.databases.database import Database
7+
from notion.model.filters import Filter
8+
from notion.model.page import Page
69

710
API_BASE_URL = "https://api.notion.com/v1/"
811
API_VERSION = "2022-02-22"
@@ -41,7 +44,7 @@ def _paginate(
4144
entity: str,
4245
payload: Optional[Dict[str, Any]] = None,
4346
limit: Optional[int] = None,
44-
) -> list:
47+
) -> List[dict]:
4548
if payload is None:
4649
payload = {}
4750

@@ -59,21 +62,65 @@ def _paginate(
5962
return results[:limit]
6063

6164
# ---------------------------------------------------------------------------
62-
# Pages
65+
# Databases
6366
# ---------------------------------------------------------------------------
6467

65-
def get_pages(self, database_id, filter_: Optional[dict] = None):
66-
return self._paginate("post", f"databases/{database_id}/query", filter_)
68+
def get_database(self, database_id) -> Database:
69+
"Get a single Notion database by its ID."
70+
data = self._make_request("get", f"databases/{database_id}")
71+
return Database.from_json(data).with_client(self)
72+
73+
def query_database(
74+
self,
75+
database_id,
76+
filter_: Optional[Union[Filter, dict]] = None,
77+
sort: Optional[dict] = None,
78+
) -> List[Page]:
79+
"Query a Notion database for pages given some filter(s)."
80+
filter_ = {
81+
"filter": filter_.to_json() if isinstance(filter_, Filter) else filter_
82+
}
83+
data = self._paginate(
84+
"post", f"databases/{database_id}/query", {**filter_, **(sort or {})}
85+
)
86+
return [Page.from_json(page_data).with_client(self) for page_data in data]
87+
88+
def create_database(self, database: Database, parent_id: Optional[UUIDv4] = None):
89+
"Create a new Notion database."
90+
if parent_id:
91+
database._data["parent"] = {"type": "page_id", "page_id": parent_id}
92+
response = self._make_request("post", "databases", database._data)
93+
database._data = response
94+
database._client = self
95+
96+
def update_database(self, database_id, payload: dict) -> dict:
97+
"Update properties of an existing Notion database."
98+
return self._make_request("patch", f"databases/{database_id}", payload)
99+
100+
def delete_database(self, page_id):
101+
"""Deletes the Notion Page with the given ID.
102+
103+
The Notion API does not offer a DELETE method but insteads works by setting the `archived` field.
104+
"""
105+
return self.update_database(page_id, {"archived": True})
106+
107+
# ---------------------------------------------------------------------------
108+
# Pages
109+
# ---------------------------------------------------------------------------
67110

68111
def get_page(self, page_id):
112+
"Get a single Notion page by its ID."
69113
data = self._make_request("get", f"pages/{page_id}")
70114
return Page(client=self, data=data)
71115

72-
def create_page(self, page) -> Page:
73-
response = self._make_request("post", "pages", page)
116+
def create_page(self, page: Union[Page, dict]) -> Page:
117+
"Create a new Notion page."
118+
page_data = page.to_json() if isinstance(page, Page) else page
119+
response = self._make_request("post", "pages", page_data)
74120
return response
75121

76122
def update_page(self, page_id, payload: dict):
123+
"Update properties of an existing Notion page."
77124
return self._make_request("patch", f"pages/{page_id}", payload)
78125

79126
def delete_page(self, page_id):
@@ -88,12 +135,15 @@ def delete_page(self, page_id):
88135
# ---------------------------------------------------------------------------
89136

90137
def update_block(self, block_id, payload: dict):
138+
"Update properties of an existing Notion page."
91139
return self._make_request("patch", f"blocks/{block_id}", payload)
92140

93141
def retrieve_block_children(self, block_id: str, limit: Optional[int] = None):
142+
"Retrieve children of a given block."
94143
return self._make_request("get", f"blocks/{block_id}/children")
95144

96145
def append_block_children(self, block_id: str, children: str):
146+
"Append children blocks to an existing block"
97147
return self._make_request(
98148
"patch", f"blocks/{block_id}/children", {"children": children}
99149
)
@@ -105,6 +155,10 @@ def delete_block(self, block_id: str):
105155
"""
106156
return self.update_block(block_id, {"archived": True})
107157

158+
# ---------------------------------------------------------------------------
159+
# Search
160+
# ---------------------------------------------------------------------------
161+
108162
def search(
109163
self,
110164
query: str,
@@ -120,4 +174,4 @@ def search(
120174
payload["filter"] = filter
121175

122176
results = self._paginate("post", "search", payload, limit)
123-
return [Page(data=p, client=self) for p in results]
177+
return [Page.from_json(page_data).with_client(self) for page_data in results]

notion/model/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
from notion.model.block import *
2-
from notion.model.page import *
1+
from notion.model.databases import Database
2+
from notion.model.page import Page

notion/model/block.py

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from typing import List, Optional, Union
22

3-
from notion.model.common import NotionObjectBase, UUIDv4
4-
3+
from notion.model.common.notion_object_base import NotionObjectBase
4+
from notion.model.common.utils import UUIDv4
55

66
# ---------------------------------------------------------------------------
77
# Base Class
@@ -20,6 +20,14 @@ def has_children(self) -> bool:
2020
def delete(self):
2121
self._data = self._client.delete_block(self.id)
2222

23+
def to_json(self) -> dict:
24+
res = self._data.copy()
25+
if "children" in res[self.type]:
26+
res[self.type]["children"] = [
27+
child.to_json() for child in res[self.type]["children"]
28+
]
29+
return res
30+
2331

2432
# ---------------------------------------------------------------------------
2533
# Utils
@@ -29,6 +37,7 @@ def delete(self):
2937
def type_name_from_object(object) -> str:
3038
type_name = {
3139
ChildPage: "child_page",
40+
ChildDatabase: "child_database",
3241
Paragraph: "paragraph",
3342
HeadingOne: "heading_1",
3443
HeadingTwo: "heading_2",
@@ -67,6 +76,7 @@ def type_name_from_object(object) -> str:
6776
def block_class_from_type_name(type_name: str) -> Block:
6877
type_class = {
6978
"child_page": ChildPage,
79+
"child_database": ChildDatabase,
7080
"paragraph": Paragraph,
7181
"heading_1": HeadingOne,
7282
"heading_2": HeadingTwo,
@@ -129,7 +139,7 @@ def append_children(self, children: Union[dict, List[dict]]) -> List[dict]:
129139
object_name = child._data["object"]
130140
if object_name == "block":
131141
append_results = self._client.append_block_children(
132-
self.id, [child.to_dict()]
142+
self.id, [child.to_json()]
133143
)
134144
new_block = append_results["results"][0]
135145
child._data = new_block
@@ -244,17 +254,11 @@ def url(self, new_url: str) -> str:
244254
# ---------------------------------------------------------------------------
245255

246256

247-
class ChildPage(Block):
248-
"""A page contained in another page.
249-
250-
From the Notion docs (https://developers.notion.com/docs/working-with-page-content#modeling-content-as-blocks):
251-
When a child page appears inside another page, it's represented as a `child_page` block, which does not have children.
252-
You should think of this as a reference to the page block.
253-
"""
257+
class Child(Block):
258+
"A block contained in another page."
254259

255260
@property
256261
def title(self) -> str:
257-
# return self._data["child_page"]["title"]
258262
return self._data[self.type]["title"]
259263

260264
@property
@@ -266,15 +270,36 @@ def parent(self):
266270
full_page = self._client.get_page(self.id)
267271
return full_page.parent
268272

273+
274+
class ChildPage(Child):
275+
"""A page contained in another page.
276+
277+
From the Notion docs (https://developers.notion.com/docs/working-with-page-content#modeling-content-as-blocks):
278+
When a child page appears inside another page, it's represented as a `child_page` block, which does not have children.
279+
You should think of this as a reference to the page block.
280+
"""
281+
269282
def delete(self):
270-
"""Delete the ChildPage.
283+
"""Delete the `ChildPage` in Notion.
271284
272285
Needs to be overwritten to use the `delete_page` endpoint instead of `delete_block`.
273286
"""
274287
deletion_result = self._client.delete_page(self.id)
275288
self._data["archived"] = deletion_result["archived"]
276289

277290

291+
class ChildDatabase(Child):
292+
"A database contained in another page."
293+
294+
def delete(self):
295+
"""Delete the `ChildDatabase` in Notion.
296+
297+
Needs to be overwritten to use the `delete_database` endpoint instead of `delete_block`.
298+
"""
299+
deletion_result = self._client.delete_database(self.id)
300+
self._data["archived"] = deletion_result["archived"]
301+
302+
278303
class RichText(Block, RichTextMixin):
279304
def __init__(self, text: str = None, data=None, client=None) -> None:
280305
if not data:

notion/model/common/__init__.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
from .notion_object_base import NotionObjectBase
2-
from .types import UUIDv4
3-
from .utils import is_valid_notion_id, parse_notion_datetime
2+
from .rich_text import RichText
3+
from .utils import UUIDv4, is_valid_notion_id, parse_notion_datetime
44

5-
__all__ = ["is_valid_notion_id", "NotionObjectBase", "parse_notion_datetime", "UUIDv4"]
5+
__all__ = [
6+
"is_valid_notion_id",
7+
"NotionObjectBase",
8+
"parse_notion_datetime",
9+
"RichText",
10+
"UUIDv4",
11+
]

notion/model/common/emoji.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class Emoji:
2+
def __init__(self, emoji: str):
3+
self.emoji = emoji
4+
5+
def to_json(self) -> dict:
6+
return {"type": "emoji", "emoji": self.emoji}

notion/model/common/file.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class File:
2+
def __init__(self, url: str):
3+
self.url = url
4+
5+
def to_json(self) -> dict:
6+
return {"type": "external", "external": {"url": self.url}}

notion/model/common/notion_object_base.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from datetime import datetime
2+
from typing import Union
23

4+
from notion.model.common.parent import Parent, ParentPage
35
from notion.model.common.utils import parse_notion_datetime
46

57

@@ -8,6 +10,10 @@ def __init__(self, data=None, client=None):
810
self._data = data
911
self._client = client
1012

13+
def with_client(self, client) -> "NotionObjectBase":
14+
self._client = client
15+
return self
16+
1117
@property
1218
def object(self) -> str:
1319
"""Get the Notion object type of the page as a string.
@@ -20,6 +26,17 @@ def object(self) -> str:
2026
def id(self) -> str:
2127
return self._data["id"]
2228

29+
@property
30+
def parent(self) -> Parent:
31+
return Parent.from_json(self._data)
32+
33+
@parent.setter
34+
def parent(self, new_parent: Union[ParentPage, str]):
35+
if isinstance(new_parent, str):
36+
new_parent = ParentPage(new_parent)
37+
38+
self._data["parent"] = new_parent.to_json()
39+
2340
@property
2441
def created_time(self) -> datetime:
2542
return parse_notion_datetime(self._data["created_time"])
@@ -43,11 +60,3 @@ def last_edited_by(self) -> dict:
4360
@property
4461
def archived(self) -> bool:
4562
return self._data["archived"]
46-
47-
def to_dict(self) -> dict:
48-
res = self._data.copy()
49-
if "children" in res[self.type]:
50-
res[self.type]["children"] = [
51-
child.to_dict() for child in res[self.type]["children"]
52-
]
53-
return res

notion/model/common/parent.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from typing import Union
2+
3+
from notion.model.common.utils import UUIDv4
4+
5+
6+
class Parent:
7+
"""The parent Notion object of another given Notion object.
8+
9+
Docs: https://developers.notion.com/reference/parent-object
10+
"""
11+
12+
def __init__(self, type_: str, id_: Union[UUIDv4, str, bool]):
13+
if isinstance(id_, str) and type_ in ("page_id", "database_id"):
14+
id_ = UUIDv4(id_)
15+
elif isinstance(id_, bool) and id_ and type_ == "workspace":
16+
pass
17+
else:
18+
raise ValueError("Invalid combination of `id_` and `type_` arguments.")
19+
20+
self.type = type_
21+
self.id = id_
22+
23+
def from_json(
24+
data: dict,
25+
) -> Union["ParentWorkspace", "ParentPage", "ParentDatabase", None]:
26+
if "parent" not in data:
27+
return None
28+
29+
type_ = data["parent"]["type"]
30+
id_ = data["parent"][type_]
31+
32+
if type_ == "workspace":
33+
return ParentWorkspace()
34+
elif type_ == "page_id":
35+
return ParentPage(id_)
36+
elif type_ == "database_id":
37+
return ParentDatabase(id_)
38+
else:
39+
# FIXME: This can never be reached because id_=... line will not find key.
40+
raise ValueError(f"Parent type {type_!r} is not supported.")
41+
42+
def to_json(self):
43+
return {"type": self.type, self.type: self.id}
44+
45+
46+
class ParentWorkspace(Parent):
47+
def __init__(self):
48+
super().__init__("workspace", True)
49+
50+
51+
class ParentPage(Parent):
52+
def __init__(self, id_):
53+
super().__init__("page_id", id_)
54+
55+
56+
class ParentDatabase(Parent):
57+
def __init__(self, id_):
58+
super().__init__("database_id", id_)

notion/model/common/rich_text.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
class RichText:
2+
def __init__(self, text: str):
3+
self.text = text
4+
5+
@staticmethod
6+
def from_json(data: dict) -> "RichText":
7+
return RichText(data)
8+
9+
def to_json(self) -> dict:
10+
return {
11+
"type": "text",
12+
"text": {
13+
"content": self.text,
14+
},
15+
}
16+
17+
def __str__(self) -> str:
18+
return self.text
19+
20+
def __repr__(self) -> str:
21+
return str(self)

0 commit comments

Comments
 (0)