Skip to content

Commit f5e66f2

Browse files
committed
Added "NotionDate" object, some caching, quick fixes, and version bump
1 parent 1a39b92 commit f5e66f2

File tree

5 files changed

+114
-14
lines changed

5 files changed

+114
-14
lines changed

notion/block.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import time
77
import uuid
88

9+
from cached_property import cached_property
910
from copy import deepcopy
1011

1112
from .logger import logger
@@ -608,7 +609,7 @@ class CollectionViewBlock(MediaBlock):
608609

609610
_type = "collection_view"
610611

611-
@property
612+
@cached_property
612613
def collection(self):
613614
collection_id = self.get("collection_id")
614615
if not collection_id:

notion/collection.py

+103-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
from cached_property import cached_property
12
from copy import deepcopy
2-
from datetime import datetime
3+
from datetime import datetime, date
4+
from tzlocal import get_localzone
35

46
from .block import Block, PageBlock
57
from .logger import logger
@@ -10,6 +12,82 @@
1012
from .utils import add_signed_prefix_as_needed, remove_signed_prefix_as_needed, slugify
1113

1214

15+
class NotionDate(object):
16+
17+
start = None
18+
end = None
19+
timezone = None
20+
21+
def __init__(self, start, end=None, timezone=None):
22+
self.start = start
23+
self.end = end
24+
self.timezone = timezone
25+
26+
@classmethod
27+
def from_notion(cls, obj):
28+
if isinstance(obj, dict):
29+
data = obj
30+
elif isinstance(obj, list):
31+
data = obj[0][1][0][1]
32+
else:
33+
return None
34+
start = cls._parse_datetime(data.get("start_date"), data.get("start_time"))
35+
end = cls._parse_datetime(data.get("end_date"), data.get("end_time"))
36+
timezone = data.get("timezone")
37+
return cls(start, end=end, timezone=timezone)
38+
39+
@classmethod
40+
def _parse_datetime(cls, date_str, time_str):
41+
if not date_str:
42+
return None
43+
if time_str:
44+
return datetime.strptime(date_str + " " + time_str, '%Y-%m-%d %H:%M')
45+
else:
46+
return date.strptime(date_str, '%Y-%m-%d')
47+
48+
def _format_datetime(self, date_or_datetime):
49+
if not date_or_datetime:
50+
return None, None
51+
if isinstance(date_or_datetime, datetime):
52+
return date_or_datetime.strftime("%Y-%m-%d"), date_or_datetime.strftime("%H:%M")
53+
else:
54+
return date_or_datetime.strftime("%Y-%m-%d"), None
55+
56+
def type(self):
57+
name = "date"
58+
if isinstance(self.start, datetime):
59+
name += "time"
60+
if self.end:
61+
name += "range"
62+
return name
63+
64+
def to_notion(self):
65+
66+
if self.end:
67+
self.start, self.end = sorted([self.start, self.end])
68+
69+
start_date, start_time = self._format_datetime(self.start)
70+
end_date, end_time = self._format_datetime(self.end)
71+
72+
if not start_date:
73+
return []
74+
75+
data = {
76+
"type": self.type(),
77+
"start_date": start_date,
78+
}
79+
80+
if end_date:
81+
data["end_date"] = end_date
82+
83+
if "time" in data["type"]:
84+
data["time_zone"] = str(self.timezone or get_localzone())
85+
data["start_time"] = start_time or "00:00"
86+
if end_date:
87+
data["end_time"] = end_time or "00:00"
88+
89+
return [["‣", [["d", data]]]]
90+
1391
class Collection(Record):
1492
"""
1593
A "collection" corresponds to what's sometimes called a "database" in the Notion UI.
@@ -21,6 +99,10 @@ class Collection(Record):
2199
description = field_map("description", api_to_python=notion_to_markdown, python_to_api=markdown_to_notion)
22100
cover = field_map("cover")
23101

102+
def __init__(self, *args, **kwargs):
103+
super().__init__(*args, **kwargs)
104+
self._client.refresh_collection_rows(self.id)
105+
24106
def get_schema_properties(self):
25107
"""
26108
Fetch a flattened list of all properties in the collection's schema.
@@ -43,17 +125,21 @@ def get_schema_property(self, identifier):
43125
return prop
44126
return None
45127

46-
def add_row(self):
128+
def add_row(self, **kwargs):
47129
"""
48130
Create a new empty CollectionRowBlock under this collection, and return the instance.
49131
"""
50132

51133
row_id = self._client.create_record("block", self, type="page")
134+
row = CollectionRowBlock(self._client, row_id)
52135

53-
return CollectionRowBlock(self._client, row_id)
136+
with self._client.as_atomic_transaction():
137+
for key, val in kwargs.items():
138+
setattr(key, val)
54139

55-
def get_rows(self):
140+
return row
56141

142+
def get_rows(self):
57143
return [self._client.get_block(row_id) for row_id in self._client._store.get_collection_rows(self.id)]
58144

59145
def _convert_diff_to_changelist(self, difference, old_val, new_val):
@@ -164,7 +250,7 @@ def execute(self):
164250

165251
class CollectionRowBlock(PageBlock):
166252

167-
@property
253+
@cached_property
168254
def collection(self):
169255
return self._client.get_collection(self.get("parent_id"))
170256

@@ -191,11 +277,11 @@ def __dir__(self):
191277
return self._get_property_slugs() + super().__dir__()
192278

193279
def get_property(self, identifier):
194-
280+
195281
prop = self.collection.get_schema_property(identifier)
196282
if prop is None:
197283
raise AttributeError("Object does not have property '{}'".format(identifier))
198-
284+
199285
val = self.get(["properties", prop["id"]])
200286

201287
return self._convert_notion_to_python(val, prop)
@@ -246,7 +332,7 @@ def _convert_notion_to_python(self, val, prop):
246332
if prop["type"] in ["email", "phone_number", "url"]:
247333
val = val[0][0] if val else ""
248334
if prop["type"] in ["date"]:
249-
val = val[0][1][0][1] if val else None
335+
val = NotionDate.from_notion(val)
250336
if prop["type"] in ["file"]:
251337
val = [add_signed_prefix_as_needed(item[1][0][1]) for item in val if item[0] != ","] if val else []
252338
if prop["type"] in ["checkbox"]:
@@ -274,7 +360,7 @@ def set_property(self, identifier, val):
274360
prop = self.collection.get_schema_property(identifier)
275361
if prop is None:
276362
raise AttributeError("Object does not have property '{}'".format(identifier))
277-
363+
278364
path, val = self._convert_python_to_notion(val, prop)
279365

280366
self.set(path, val)
@@ -316,7 +402,12 @@ def _convert_python_to_notion(self, val, prop):
316402
if prop["type"] in ["email", "phone_number", "url"]:
317403
val = [[val, [["a", val]]]]
318404
if prop["type"] in ["date"]:
319-
val = [['‣', [['d', val]]]]
405+
if isinstance(val, date) or isinstance(val, datetime):
406+
val = NotionDate(val)
407+
if isinstance(val, NotionDate):
408+
val = val.to_notion()
409+
else:
410+
val = []
320411
if prop["type"] in ["file"]:
321412
filelist = []
322413
if not isinstance(val, list):
@@ -332,6 +423,8 @@ def _convert_python_to_notion(self, val, prop):
332423
val = [["Yes" if val else "No"]]
333424
if prop["type"] in ["relation"]:
334425
pagelist = []
426+
if not isinstance(val, list):
427+
val = [val]
335428
for page in val:
336429
if isinstance(page, str):
337430
page = self._client.get_block(page)

notion/monitor.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -191,4 +191,9 @@ def poll_async(self):
191191

192192
def poll_forever(self):
193193
while True:
194-
self.poll()
194+
try:
195+
self.poll()
196+
except Exception as e:
197+
logger.error("Encountered error during polling!")
198+
logger.error(e, exc_info=True)
199+
time.sleep(1)

requirements.txt

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ commonmark
33
bs4
44
tzlocal
55
python-slugify
6-
dictdiffer
6+
dictdiffer
7+
cached-property

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
setuptools.setup(
1515
name="notion",
16-
version="0.0.15",
16+
version="0.0.17",
1717
author="Jamie Alexandre",
1818
author_email="jamalex+python@gmail.com",
1919
description="Unofficial Python API client for Notion.so",

0 commit comments

Comments
 (0)