Skip to content

Commit 01185de

Browse files
author
Mario Hayashi
committed
Merge branch 'jamalex-master'
2 parents 3d76592 + 8fdc178 commit 01185de

11 files changed

+384
-66
lines changed

README.md

+39-3
Original file line numberDiff line numberDiff line change
@@ -201,22 +201,58 @@ result = cv.build_query(sort=sort_params).execute()
201201
print("Sorted results, showing most valuable first:", result)
202202
```
203203

204-
Note: You can combine `filter`, `aggregate`, and `sort`. See more examples of queries by setting up complex views in Notion, and then inspecting `cv.get("query")`
204+
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")`.
205205

206206
You can also see [more examples in action in the smoke test runner](https://github.com/jamalex/notion-py/blob/master/notion/smoke_test.py). Run it using:
207207

208208
```sh
209209
python run_smoke_test.py --page [YOUR_NOTION_PAGE_URL] --token [YOUR_NOTION_TOKEN_V2]
210210
```
211211

212-
# _Quick plug: Learning Equality is hiring!_
212+
## Example: Lock/Unlock A Page
213+
```Python
214+
from notion.client import NotionClient
215+
216+
# Obtain the `token_v2` value by inspecting your browser cookies on a logged-in session on Notion.so
217+
client = NotionClient(token_v2="<token_v2>")
218+
219+
# Replace this URL with the URL of the page or database you want to edit
220+
page = client.get_block("https://www.notion.so/myorg/Test-c0d20a71c0944985ae96e661ccc99821")
221+
222+
# The "locked" property is available on PageBlock and CollectionViewBlock objects
223+
# Set it to True to lock the page/database
224+
page.locked = True
225+
# and False to unlock it again
226+
page.locked = False
227+
```
228+
229+
## Example: Set the current user for multi-account user
230+
231+
```python
232+
from notion.client import NotionClient
233+
client = NotionClient(token_v2="<token_v2>")
234+
235+
# The initial current_user of a multi-account user may be an unwanted user
236+
print(client.current_user.email) #[email protected]
237+
238+
# Set current_user to the desired user
239+
client.set_user_by_email('[email protected]')
240+
print(client.current_user.email) #[email protected]
241+
242+
# You can also set the current_user by uid.
243+
client.set_user_by_uid('<uid>')
244+
print(client.current_user.email) #[email protected]
245+
```
246+
247+
# _Quick plug: Learning Equality needs your support!_
213248

214-
We're a [small nonprofit](https://learningequality.org/) with [global impact](https://learningequality.org/ka-lite/map/), building [exciting tech](https://learningequality.org/kolibri/)! We're currently [hiring](https://grnh.se/6epyi21) -- come join us!
249+
If you'd like to support notion-py development, please consider [donating to my open-source nonprofit, Learning Equality](https://learningequality.org/donate/), 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](https://github.com/sponsors/learningequality)!
215250

216251
# Related Projects
217252

218253
- [md2notion](https://github.com/Cobertos/md2notion): import Markdown files to Notion
219254
- [notion-export-ics](https://github.com/evertheylen/notion-export-ics): Export Notion Databases to ICS calendar files
255+
- [notion-tqdm](https://github.com/shunyooo/notion-tqdm): Progress Bar displayed in Notion like tqdm
220256

221257
# TODO
222258

notion/block.py

+20
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,24 @@ class PageBlock(BasicBlock):
540540
python_to_api=remove_signed_prefix_as_needed,
541541
)
542542

543+
locked = field_map("format.block_locked")
544+
545+
def get_backlinks(self):
546+
"""
547+
Returns a list of blocks that referencing the current PageBlock. Note that only PageBlocks support backlinks.
548+
"""
549+
data = self._client.post("getBacklinksForBlock", {"blockId": self.id}).json()
550+
backlinks = []
551+
for block in data.get("backlinks") or []:
552+
mention = block.get("mentioned_from")
553+
if not mention:
554+
continue
555+
block_id = mention.get("block_id") or mention.get("parent_block_id")
556+
if block_id:
557+
backlinks.append(self._client.get_block(block_id))
558+
return backlinks
559+
560+
543561
class BulletedListBlock(BasicBlock):
544562

545563
_type = "bulleted_list"
@@ -731,6 +749,8 @@ def description(self):
731749
def description(self, val):
732750
self.collection.description = val
733751

752+
locked = field_map("format.block_locked")
753+
734754
def _str_fields(self):
735755
return super()._str_fields() + ["title", "collection"]
736756

notion/client.py

+128-19
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from urllib.parse import urljoin
99
from requests.adapters import HTTPAdapter
1010
from requests.packages.urllib3.util.retry import Retry
11+
from getpass import getpass
1112

1213
from .block import Block, BLOCK_TYPES
1314
from .collection import (
@@ -27,18 +28,29 @@
2728
from .utils import extract_id, now
2829

2930

30-
def create_session():
31+
def create_session(client_specified_retry=None):
3132
"""
3233
retry on 502
3334
"""
3435
session = Session()
35-
retry = Retry(
36-
5,
37-
backoff_factor=0.3,
38-
status_forcelist=(502,),
39-
# CAUTION: adding 'POST' to this list which is not technically idempotent
40-
method_whitelist=("POST", "HEAD", "TRACE", "GET", "PUT", "OPTIONS", "DELETE"),
41-
)
36+
if client_specified_retry:
37+
retry = client_specified_retry
38+
else:
39+
retry = Retry(
40+
5,
41+
backoff_factor=0.3,
42+
status_forcelist=(502, 503, 504),
43+
# CAUTION: adding 'POST' to this list which is not technically idempotent
44+
method_whitelist=(
45+
"POST",
46+
"HEAD",
47+
"TRACE",
48+
"GET",
49+
"PUT",
50+
"OPTIONS",
51+
"DELETE",
52+
),
53+
)
4254
adapter = HTTPAdapter(max_retries=retry)
4355
session.mount("https://", adapter)
4456
return session
@@ -51,9 +63,23 @@ class NotionClient(object):
5163
for internal use -- the main one you'll likely want to use is `get_block`.
5264
"""
5365

54-
def __init__(self, token_v2=None, monitor=False, start_monitoring=False, enable_caching=False, cache_key=None):
55-
self.session = create_session()
56-
self.session.cookies = cookiejar_from_dict({"token_v2": token_v2})
66+
def __init__(
67+
self,
68+
token_v2=None,
69+
monitor=False,
70+
start_monitoring=False,
71+
enable_caching=False,
72+
cache_key=None,
73+
email=None,
74+
password=None,
75+
client_specified_retry=None,
76+
):
77+
self.session = create_session(client_specified_retry)
78+
if token_v2:
79+
self.session.cookies = cookiejar_from_dict({"token_v2": token_v2})
80+
else:
81+
self._set_token(email=email, password=password)
82+
5783
if enable_caching:
5884
cache_key = cache_key or hashlib.sha256(token_v2.encode()).hexdigest()
5985
self._store = RecordStore(self, cache_key=cache_key)
@@ -65,19 +91,69 @@ def __init__(self, token_v2=None, monitor=False, start_monitoring=False, enable_
6591
self.start_monitoring()
6692
else:
6793
self._monitor = None
68-
if token_v2:
69-
self._update_user_info()
94+
95+
self._update_user_info()
7096

7197
def start_monitoring(self):
7298
self._monitor.poll_async()
99+
100+
def _fetch_guest_space_data(self, records):
101+
"""
102+
guest users have an empty `space` dict, so get the space_id from the `space_view` dict instead,
103+
and fetch the space data from the getPublicSpaceData endpoint.
104+
105+
Note: This mutates the records dict
106+
"""
107+
space_id = list(records["space_view"].values())[0]["value"]["space_id"]
108+
109+
space_data = self.post(
110+
"getPublicSpaceData", {"type": "space-ids", "spaceIds": [space_id]}
111+
).json()
112+
113+
records["space"] = {
114+
space["id"]: {"value": space} for space in space_data["results"]
115+
}
116+
117+
118+
def _set_token(self, email=None, password=None):
119+
if not email:
120+
email = input("Enter your Notion email address:\n")
121+
if not password:
122+
password = getpass("Enter your Notion password:\n")
123+
self.post("loginWithEmail", {"email": email, "password": password}).json()
73124

74125
def _update_user_info(self):
75126
records = self.post("loadUserContent", {}).json()["recordMap"]
127+
if not records["space"]:
128+
self._fetch_guest_space_data(records)
129+
76130
self._store.store_recordmap(records)
77131
self.current_user = self.get_user(list(records["notion_user"].keys())[0])
78132
self.current_space = self.get_space(list(records["space"].keys())[0])
79133
return records
80134

135+
def get_email_uid(self):
136+
response = self.post("getSpaces", {}).json()
137+
return {
138+
response[uid]["notion_user"][uid]["value"]["email"]: uid
139+
for uid in response.keys()
140+
}
141+
142+
def set_user_by_uid(self, user_id):
143+
self.session.headers.update({"x-notion-active-user-header": user_id})
144+
self._update_user_info()
145+
146+
def set_user_by_email(self, email):
147+
email_uid_dict = self.get_email_uid()
148+
uid = email_uid_dict.get(email)
149+
if not uid:
150+
raise Exception(
151+
"Requested email address {email} not found; available addresses: {available}".format(
152+
email=email, available=list(email_uid_dict)
153+
)
154+
)
155+
self.set_user_by_uid(uid)
156+
81157
def get_top_level_pages(self):
82158
records = self._update_user_info()
83159
return [self.get_block(bid) for bid in records["block"].keys()]
@@ -242,15 +318,47 @@ def search_pages_with_parent(self, parent_id, search=""):
242318
return response["results"]
243319

244320
def search_blocks(self, search, limit=25):
321+
return self.search(query=search, limit=limit)
322+
323+
def search(
324+
self,
325+
query="",
326+
search_type="BlocksInSpace",
327+
limit=100,
328+
sort="Relevance",
329+
source="quick_find",
330+
isDeletedOnly=False,
331+
excludeTemplates=False,
332+
isNavigableOnly=False,
333+
requireEditPermissions=False,
334+
ancestors=[],
335+
createdBy=[],
336+
editedBy=[],
337+
lastEditedTime={},
338+
createdTime={},
339+
):
245340
data = {
246-
"query": search,
247-
"table": "space",
248-
"id": self.current_space.id,
341+
"type": search_type,
342+
"query": query,
343+
"spaceId": self.current_space.id,
249344
"limit": limit,
345+
"filters": {
346+
"isDeletedOnly": isDeletedOnly,
347+
"excludeTemplates": excludeTemplates,
348+
"isNavigableOnly": isNavigableOnly,
349+
"requireEditPermissions": requireEditPermissions,
350+
"ancestors": ancestors,
351+
"createdBy": createdBy,
352+
"editedBy": editedBy,
353+
"lastEditedTime": lastEditedTime,
354+
"createdTime": createdTime,
355+
},
356+
"sort": sort,
357+
"source": source,
250358
}
251-
response = self.post("searchBlocks", data).json()
359+
response = self.post("search", data).json()
252360
self._store.store_recordmap(response["recordMap"])
253-
return [self.get_block(block_id) for block_id in response["results"]]
361+
return [self.get_block(result["id"]) for result in response["results"]]
254362

255363
def create_record(self, table, parent, **kwargs):
256364

@@ -263,7 +371,8 @@ def create_record(self, table, parent, **kwargs):
263371
"id": record_id,
264372
"version": 1,
265373
"alive": True,
266-
"created_by": self.current_user.id,
374+
"created_by_id": self.current_user.id,
375+
"created_by_table": "notion_user",
267376
"created_time": now(),
268377
"parent_id": parent.id,
269378
"parent_table": parent._table,

0 commit comments

Comments
 (0)