notionary 0.2.12__py3-none-any.whl → 0.2.14__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- notionary/__init__.py +3 -20
- notionary/{notion_client.py → base_notion_client.py} +92 -98
- notionary/blocks/__init__.py +61 -0
- notionary/{elements → blocks}/audio_element.py +6 -4
- notionary/{elements → blocks}/bookmark_element.py +3 -6
- notionary/{elements → blocks}/bulleted_list_element.py +5 -7
- notionary/{elements → blocks}/callout_element.py +5 -8
- notionary/{elements → blocks}/code_block_element.py +4 -6
- notionary/{elements → blocks}/column_element.py +3 -6
- notionary/{elements → blocks}/divider_element.py +3 -6
- notionary/{elements → blocks}/embed_element.py +4 -6
- notionary/{elements → blocks}/heading_element.py +5 -9
- notionary/{elements → blocks}/image_element.py +4 -6
- notionary/{elements → blocks}/mention_element.py +3 -7
- notionary/blocks/notion_block_client.py +26 -0
- notionary/blocks/notion_block_element.py +34 -0
- notionary/{elements → blocks}/numbered_list_element.py +4 -7
- notionary/{elements → blocks}/paragraph_element.py +4 -7
- notionary/{prompting/element_prompt_content.py → blocks/prompts/element_prompt_builder.py} +1 -40
- notionary/blocks/prompts/element_prompt_content.py +41 -0
- notionary/{elements → blocks}/qoute_element.py +4 -6
- notionary/{elements → blocks}/registry/block_registry.py +4 -26
- notionary/{elements → blocks}/registry/block_registry_builder.py +26 -25
- notionary/{elements → blocks}/table_element.py +6 -8
- notionary/{elements → blocks}/text_inline_formatter.py +1 -4
- notionary/{elements → blocks}/todo_element.py +6 -8
- notionary/{elements → blocks}/toggle_element.py +3 -6
- notionary/{elements → blocks}/toggleable_heading_element.py +5 -8
- notionary/{elements → blocks}/video_element.py +4 -6
- notionary/cli/main.py +245 -53
- notionary/cli/onboarding.py +117 -0
- notionary/database/__init__.py +0 -0
- notionary/database/client.py +132 -0
- notionary/database/database_exceptions.py +13 -0
- notionary/database/factory.py +0 -0
- notionary/database/filter_builder.py +175 -0
- notionary/database/notion_database.py +339 -128
- notionary/database/notion_database_provider.py +230 -0
- notionary/elements/__init__.py +0 -0
- notionary/models/notion_database_response.py +294 -13
- notionary/models/notion_page_response.py +9 -31
- notionary/models/search_response.py +0 -0
- notionary/page/__init__.py +0 -0
- notionary/page/client.py +110 -0
- notionary/page/content/page_content_retriever.py +5 -20
- notionary/page/content/page_content_writer.py +3 -4
- notionary/page/formatting/markdown_to_notion_converter.py +1 -3
- notionary/{prompting → page}/markdown_syntax_prompt_generator.py +1 -2
- notionary/page/notion_page.py +354 -317
- notionary/page/notion_to_markdown_converter.py +1 -4
- notionary/page/properites/property_value_extractor.py +0 -64
- notionary/page/{properites/property_formatter.py → property_formatter.py} +7 -4
- notionary/page/search_filter_builder.py +131 -0
- notionary/page/utils.py +60 -0
- notionary/util/__init__.py +12 -3
- notionary/util/factory_decorator.py +33 -0
- notionary/util/fuzzy_matcher.py +82 -0
- notionary/util/page_id_utils.py +0 -21
- notionary/util/singleton_metaclass.py +22 -0
- notionary/workspace.py +69 -0
- {notionary-0.2.12.dist-info → notionary-0.2.14.dist-info}/METADATA +4 -1
- notionary-0.2.14.dist-info/RECORD +72 -0
- notionary/database/database_discovery.py +0 -142
- notionary/database/notion_database_factory.py +0 -193
- notionary/elements/notion_block_element.py +0 -70
- notionary/exceptions/database_exceptions.py +0 -76
- notionary/exceptions/page_creation_exception.py +0 -9
- notionary/page/metadata/metadata_editor.py +0 -150
- notionary/page/metadata/notion_icon_manager.py +0 -77
- notionary/page/metadata/notion_page_cover_manager.py +0 -56
- notionary/page/notion_page_factory.py +0 -332
- notionary/page/properites/database_property_service.py +0 -302
- notionary/page/properites/page_property_manager.py +0 -152
- notionary/page/relations/notion_page_relation_manager.py +0 -350
- notionary/page/relations/notion_page_title_resolver.py +0 -104
- notionary/page/relations/page_database_relation.py +0 -68
- notionary/telemetry/__init__.py +0 -7
- notionary/telemetry/telemetry.py +0 -226
- notionary/telemetry/track_usage_decorator.py +0 -76
- notionary/util/warn_direct_constructor_usage.py +0 -54
- notionary-0.2.12.dist-info/RECORD +0 -70
- /notionary/util/{singleton.py → singleton_decorator.py} +0 -0
- {notionary-0.2.12.dist-info → notionary-0.2.14.dist-info}/WHEEL +0 -0
- {notionary-0.2.12.dist-info → notionary-0.2.14.dist-info}/entry_points.txt +0 -0
- {notionary-0.2.12.dist-info → notionary-0.2.14.dist-info}/licenses/LICENSE +0 -0
- {notionary-0.2.12.dist-info → notionary-0.2.14.dist-info}/top_level.txt +0 -0
@@ -1,12 +1,19 @@
|
|
1
1
|
from __future__ import annotations
|
2
|
+
import random
|
2
3
|
from typing import Any, AsyncGenerator, Dict, List, Optional
|
3
4
|
|
4
|
-
from notionary.
|
5
|
+
from notionary.database.client import NotionDatabaseClient
|
6
|
+
from notionary.models.notion_database_response import (
|
7
|
+
NotionDatabaseResponse,
|
8
|
+
NotionPageResponse,
|
9
|
+
NotionQueryDatabaseResponse,
|
10
|
+
)
|
5
11
|
from notionary.page.notion_page import NotionPage
|
6
|
-
|
7
|
-
from notionary.
|
8
|
-
|
9
|
-
from notionary.
|
12
|
+
|
13
|
+
from notionary.database.notion_database_provider import NotionDatabaseProvider
|
14
|
+
|
15
|
+
from notionary.database.filter_builder import FilterBuilder
|
16
|
+
from notionary.util import factory_only, LoggingMixin
|
10
17
|
|
11
18
|
|
12
19
|
class NotionDatabase(LoggingMixin):
|
@@ -16,138 +23,312 @@ class NotionDatabase(LoggingMixin):
|
|
16
23
|
for further page operations.
|
17
24
|
"""
|
18
25
|
|
19
|
-
@
|
20
|
-
def __init__(
|
26
|
+
@factory_only("from_database_id", "from_database_name")
|
27
|
+
def __init__(
|
28
|
+
self,
|
29
|
+
database_id: str,
|
30
|
+
title: str,
|
31
|
+
url: str,
|
32
|
+
emoji_icon: Optional[str] = None,
|
33
|
+
properties: Optional[Dict[str, Any]] = None,
|
34
|
+
token: Optional[str] = None,
|
35
|
+
):
|
21
36
|
"""
|
22
37
|
Initialize the minimal database manager.
|
23
|
-
|
24
|
-
Args:
|
25
|
-
database_id: ID of the Notion database
|
26
|
-
token: Optional Notion API token
|
27
38
|
"""
|
28
|
-
self.
|
29
|
-
self.
|
30
|
-
self.
|
39
|
+
self._database_id = database_id
|
40
|
+
self._title = title
|
41
|
+
self._url = url
|
42
|
+
self._emoji_icon = emoji_icon
|
43
|
+
self._properties = properties
|
44
|
+
|
45
|
+
self.client = NotionDatabaseClient(token=token)
|
31
46
|
|
32
47
|
@classmethod
|
33
|
-
def from_database_id(
|
48
|
+
async def from_database_id(
|
34
49
|
cls, database_id: str, token: Optional[str] = None
|
35
50
|
) -> NotionDatabase:
|
36
51
|
"""
|
37
|
-
Create a NotionDatabase from a database ID.
|
38
|
-
Delegates to NotionDatabaseFactory.
|
52
|
+
Create a NotionDatabase from a database ID using NotionDatabaseProvider.
|
39
53
|
"""
|
40
|
-
|
41
|
-
|
42
|
-
return NotionDatabaseFactory.from_database_id(database_id, token)
|
54
|
+
provider = cls.get_database_provider()
|
55
|
+
return await provider.get_database_by_id(database_id, token)
|
43
56
|
|
44
57
|
@classmethod
|
45
58
|
async def from_database_name(
|
46
|
-
cls,
|
59
|
+
cls,
|
60
|
+
database_name: str,
|
61
|
+
token: Optional[str] = None,
|
62
|
+
min_similarity: float = 0.6,
|
47
63
|
) -> NotionDatabase:
|
48
64
|
"""
|
49
|
-
Create a NotionDatabase by finding a database with
|
50
|
-
Delegates to NotionDatabaseFactory.
|
65
|
+
Create a NotionDatabase by finding a database with fuzzy matching on the title using NotionDatabaseProvider.
|
51
66
|
"""
|
52
|
-
|
67
|
+
provider = cls.get_database_provider()
|
68
|
+
return await provider.get_database_by_name(database_name, token, min_similarity)
|
69
|
+
|
70
|
+
@property
|
71
|
+
def database_id(self) -> str:
|
72
|
+
"""Get the database ID (readonly)."""
|
73
|
+
return self._database_id
|
74
|
+
|
75
|
+
@property
|
76
|
+
def title(self) -> str:
|
77
|
+
"""Get the database title (readonly)."""
|
78
|
+
return self._title
|
79
|
+
|
80
|
+
@property
|
81
|
+
def url(self) -> str:
|
82
|
+
"""Get the database URL (readonly)."""
|
83
|
+
return self._url
|
84
|
+
|
85
|
+
@property
|
86
|
+
def emoji(self) -> Optional[str]:
|
87
|
+
"""Get the database emoji (readonly)."""
|
88
|
+
return self._emoji_icon
|
89
|
+
|
90
|
+
@property
|
91
|
+
def properties(self) -> Optional[Dict[str, Any]]:
|
92
|
+
"""Get the database properties (readonly)."""
|
93
|
+
return self._properties
|
94
|
+
|
95
|
+
# Database Provider is a singleton so we can instantiate it here with no worries
|
96
|
+
@property
|
97
|
+
def database_provider(self):
|
98
|
+
"""Return a NotionDatabaseProvider instance for this database."""
|
99
|
+
return NotionDatabaseProvider.get_instance()
|
53
100
|
|
54
|
-
|
101
|
+
@classmethod
|
102
|
+
def get_database_provider(cls):
|
103
|
+
"""Return a NotionDatabaseProvider instance for class-level usage."""
|
104
|
+
return NotionDatabaseProvider.get_instance()
|
55
105
|
|
56
106
|
async def create_blank_page(self) -> Optional[NotionPage]:
|
57
107
|
"""
|
58
108
|
Create a new blank page in the database with minimal properties.
|
109
|
+
"""
|
110
|
+
try:
|
111
|
+
create_page_response: NotionPageResponse = await self.client.create_page(
|
112
|
+
parent_database_id=self.database_id
|
113
|
+
)
|
59
114
|
|
60
|
-
|
61
|
-
|
115
|
+
return await NotionPage.from_page_id(page_id=create_page_response.id)
|
116
|
+
|
117
|
+
except Exception as e:
|
118
|
+
self.logger.error("Error creating blank page: %s", str(e))
|
119
|
+
return None
|
120
|
+
|
121
|
+
async def set_title(self, new_title: str) -> bool:
|
122
|
+
"""
|
123
|
+
Update the database title.
|
62
124
|
"""
|
63
125
|
try:
|
64
|
-
|
65
|
-
|
126
|
+
result = await self.client.update_database_title(
|
127
|
+
database_id=self.database_id, title=new_title
|
66
128
|
)
|
67
129
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
130
|
+
self._title = result.title[0].plain_text
|
131
|
+
self.logger.info(f"Successfully updated database title to: {new_title}")
|
132
|
+
self.database_provider.invalidate_database_cache(
|
133
|
+
database_id=self.database_id
|
134
|
+
)
|
135
|
+
return True
|
136
|
+
|
137
|
+
except Exception as e:
|
138
|
+
self.logger.error(f"Error updating database title: {str(e)}")
|
139
|
+
return False
|
140
|
+
|
141
|
+
async def set_emoji(self, new_emoji: str) -> bool:
|
142
|
+
"""
|
143
|
+
Update the database emoji.
|
144
|
+
"""
|
145
|
+
try:
|
146
|
+
result = await self.client.update_database_emoji(
|
147
|
+
database_id=self.database_id, emoji=new_emoji
|
148
|
+
)
|
73
149
|
|
74
|
-
|
75
|
-
|
150
|
+
self._emoji_icon = result.icon.emoji if result.icon else None
|
151
|
+
self.logger.info(f"Successfully updated database emoji to: {new_emoji}")
|
152
|
+
self.database_provider.invalidate_database_cache(
|
153
|
+
database_id=self.database_id
|
154
|
+
)
|
155
|
+
return True
|
156
|
+
|
157
|
+
except Exception as e:
|
158
|
+
self.logger.error(f"Error updating database emoji: {str(e)}")
|
159
|
+
return False
|
160
|
+
|
161
|
+
async def set_cover_image(self, image_url: str) -> Optional[str]:
|
162
|
+
"""
|
163
|
+
Update the database cover image.
|
164
|
+
"""
|
165
|
+
try:
|
166
|
+
result = await self.client.update_database_cover_image(
|
167
|
+
database_id=self.database_id, image_url=image_url
|
168
|
+
)
|
169
|
+
|
170
|
+
if result.cover and result.cover.external:
|
171
|
+
self.database_provider.invalidate_database_cache(
|
172
|
+
database_id=self.database_id
|
76
173
|
)
|
174
|
+
return result.cover.external.url
|
175
|
+
return None
|
176
|
+
|
177
|
+
except Exception as e:
|
178
|
+
self.logger.error(f"Error updating database cover image: {str(e)}")
|
179
|
+
return None
|
180
|
+
|
181
|
+
async def set_random_gradient_cover(self) -> Optional[str]:
|
182
|
+
"""Sets a random gradient cover from Notion's default gradient covers (always jpg)."""
|
183
|
+
default_notion_covers = [
|
184
|
+
f"https://www.notion.so/images/page-cover/gradients_{i}.png"
|
185
|
+
for i in range(1, 10)
|
186
|
+
]
|
187
|
+
random_cover_url = random.choice(default_notion_covers)
|
188
|
+
return await self.set_cover_image(random_cover_url)
|
189
|
+
|
190
|
+
async def set_external_icon(self, external_icon_url: str) -> Optional[str]:
|
191
|
+
"""
|
192
|
+
Update the database icon with an external image URL.
|
193
|
+
"""
|
194
|
+
try:
|
195
|
+
result = await self.client.update_database_external_icon(
|
196
|
+
database_id=self.database_id, icon_url=external_icon_url
|
197
|
+
)
|
77
198
|
|
78
|
-
|
199
|
+
if result.icon and result.icon.external:
|
200
|
+
self.database_provider.invalidate_database_cache(
|
201
|
+
database_id=self.database_id
|
202
|
+
)
|
203
|
+
return result.icon.external.url
|
79
204
|
return None
|
80
205
|
|
81
206
|
except Exception as e:
|
82
|
-
self.logger.error("Error
|
207
|
+
self.logger.error(f"Error updating database external icon: {str(e)}")
|
83
208
|
return None
|
84
209
|
|
85
|
-
async def
|
86
|
-
self,
|
87
|
-
limit: int = 100,
|
88
|
-
filter_conditions: Optional[Dict[str, Any]] = None,
|
89
|
-
sorts: Optional[List[Dict[str, Any]]] = None,
|
90
|
-
) -> List[NotionPage]:
|
210
|
+
async def get_options_by_property_name(self, property_name: str) -> List[str]:
|
91
211
|
"""
|
92
|
-
|
212
|
+
Retrieve all option names for a select, multi_select, status, or relation property.
|
93
213
|
|
94
214
|
Args:
|
95
|
-
|
96
|
-
filter_conditions: Optional filter to apply to the database query
|
97
|
-
sorts: Optional sort instructions for the database query
|
215
|
+
property_name: The name of the property in the database schema.
|
98
216
|
|
99
217
|
Returns:
|
100
|
-
|
218
|
+
A list of option names for the given property. For select, multi_select, or status,
|
219
|
+
returns the option names directly. For relation properties, returns the titles of related pages.
|
101
220
|
"""
|
102
|
-
self.
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
221
|
+
property_schema = self.properties.get(property_name)
|
222
|
+
|
223
|
+
property_type = property_schema.get("type")
|
224
|
+
|
225
|
+
if property_type in ["select", "multi_select", "status"]:
|
226
|
+
options = property_schema.get(property_type, {}).get("options", [])
|
227
|
+
return [option.get("name", "") for option in options]
|
228
|
+
|
229
|
+
if property_type == "relation":
|
230
|
+
return await self._get_relation_options(property_name)
|
231
|
+
|
232
|
+
return []
|
233
|
+
|
234
|
+
def get_property_type(self, property_name: str) -> Optional[str]:
|
235
|
+
"""
|
236
|
+
Get the type of a property by its name.
|
237
|
+
"""
|
238
|
+
property_schema = self.properties.get(property_name)
|
239
|
+
return property_schema.get("type") if property_schema else None
|
240
|
+
|
241
|
+
async def query_database_by_title(self, page_title: str) -> List[NotionPage]:
|
242
|
+
"""
|
243
|
+
Query the database for pages with a specific title.
|
244
|
+
"""
|
245
|
+
search_results: NotionQueryDatabaseResponse = (
|
246
|
+
await self.client.query_database_by_title(
|
247
|
+
database_id=self.database_id, page_title=page_title
|
248
|
+
)
|
107
249
|
)
|
108
250
|
|
109
|
-
|
110
|
-
|
251
|
+
page_results: List[NotionPage] = []
|
252
|
+
|
253
|
+
for page in search_results.results:
|
254
|
+
page = NotionPage.from_page_id(page_id=page.id, token=self.client.token)
|
255
|
+
page_results.append(page)
|
256
|
+
|
257
|
+
return page_results
|
258
|
+
|
259
|
+
async def iter_pages_updated_within(
|
260
|
+
self, hours: int = 24, page_size: int = 100
|
261
|
+
) -> AsyncGenerator[NotionPage, None]:
|
262
|
+
"""
|
263
|
+
Iterate through pages edited in the last N hours using FilterBuilder.
|
264
|
+
"""
|
111
265
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
266
|
+
filter_builder = FilterBuilder()
|
267
|
+
filter_builder.with_updated_last_n_hours(hours).with_page_size(page_size)
|
268
|
+
filter_conditions = filter_builder.build()
|
269
|
+
|
270
|
+
async for page in self._iter_pages(
|
271
|
+
page_size=page_size, filter_conditions=filter_conditions
|
116
272
|
):
|
273
|
+
yield page
|
274
|
+
|
275
|
+
async def get_all_pages(self) -> List[NotionPage]:
|
276
|
+
"""
|
277
|
+
Get all pages in the database (use with caution for large databases).
|
278
|
+
"""
|
279
|
+
pages = []
|
280
|
+
async for page in self._iter_pages():
|
117
281
|
pages.append(page)
|
118
|
-
|
282
|
+
return pages
|
119
283
|
|
120
|
-
|
121
|
-
|
284
|
+
async def get_last_edited_time(self) -> Optional[str]:
|
285
|
+
"""
|
286
|
+
Retrieve the last edited time of the database.
|
122
287
|
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
288
|
+
Returns:
|
289
|
+
ISO 8601 timestamp string of the last database edit, or None if request fails.
|
290
|
+
"""
|
291
|
+
try:
|
292
|
+
db = await self.client.get_database(self.database_id)
|
293
|
+
|
294
|
+
return db.last_edited_time
|
295
|
+
|
296
|
+
except Exception as e:
|
297
|
+
self.logger.error(
|
298
|
+
"Error fetching last_edited_time for database %s: %s",
|
299
|
+
self.database_id,
|
300
|
+
str(e),
|
301
|
+
)
|
302
|
+
return None
|
303
|
+
|
304
|
+
def create_filter(self) -> FilterBuilder:
|
305
|
+
"""Create a new filter builder for this database."""
|
306
|
+
return FilterBuilder()
|
307
|
+
|
308
|
+
async def iter_pages_with_filter(
|
309
|
+
self, filter_builder: FilterBuilder, page_size: int = 100
|
310
|
+
):
|
311
|
+
"""Iterate pages using a filter builder."""
|
312
|
+
filter_config = filter_builder.build()
|
313
|
+
self.logger.debug("Using filter: %s", filter_config)
|
314
|
+
async for page in self._iter_pages(
|
315
|
+
page_size=page_size, filter_conditions=filter_config
|
316
|
+
):
|
317
|
+
yield page
|
127
318
|
|
128
|
-
async def
|
319
|
+
async def _iter_pages(
|
129
320
|
self,
|
130
321
|
page_size: int = 100,
|
131
322
|
filter_conditions: Optional[Dict[str, Any]] = None,
|
132
|
-
sorts: Optional[List[Dict[str, Any]]] = None,
|
133
323
|
) -> AsyncGenerator[NotionPage, None]:
|
134
324
|
"""
|
135
325
|
Asynchronous generator that yields pages from the database.
|
136
326
|
Directly queries the Notion API without using the schema.
|
137
|
-
|
138
|
-
Args:
|
139
|
-
page_size: Number of pages to fetch per request
|
140
|
-
filter_conditions: Optional filter to apply to the database query
|
141
|
-
sorts: Optional sort instructions for the database query
|
142
|
-
|
143
|
-
Yields:
|
144
|
-
NotionPage instances for each page
|
145
327
|
"""
|
146
328
|
self.logger.debug(
|
147
|
-
"Iterating pages with page_size: %d, filter: %s
|
329
|
+
"Iterating pages with page_size: %d, filter: %s",
|
148
330
|
page_size,
|
149
331
|
filter_conditions,
|
150
|
-
sorts,
|
151
332
|
)
|
152
333
|
|
153
334
|
start_cursor: Optional[str] = None
|
@@ -158,79 +339,109 @@ class NotionDatabase(LoggingMixin):
|
|
158
339
|
if filter_conditions:
|
159
340
|
body["filter"] = filter_conditions
|
160
341
|
|
161
|
-
if sorts:
|
162
|
-
body["sorts"] = sorts
|
163
|
-
|
164
342
|
while has_more:
|
165
343
|
current_body = body.copy()
|
166
344
|
if start_cursor:
|
167
345
|
current_body["start_cursor"] = start_cursor
|
168
346
|
|
169
|
-
result = await self.
|
170
|
-
|
347
|
+
result = await self.client.query_database(
|
348
|
+
database_id=self.database_id, query_data=current_body
|
171
349
|
)
|
172
350
|
|
173
|
-
if not result or
|
351
|
+
if not result or not result.results:
|
174
352
|
return
|
175
353
|
|
176
|
-
for page in result
|
177
|
-
|
178
|
-
|
179
|
-
yield NotionPage.from_page_id(page_id=page_id, token=self._client.token)
|
354
|
+
for page in result.results:
|
355
|
+
yield await NotionPage.from_page_id(page_id=page.id, token=self.client.token)
|
180
356
|
|
181
|
-
has_more = result.
|
182
|
-
start_cursor = result.
|
357
|
+
has_more = result.has_more
|
358
|
+
start_cursor = result.next_cursor if has_more else None
|
183
359
|
|
184
|
-
|
360
|
+
@classmethod
|
361
|
+
def _create_from_response(
|
362
|
+
cls, db_response: NotionDatabaseResponse, token: Optional[str]
|
363
|
+
) -> NotionDatabase:
|
185
364
|
"""
|
186
|
-
|
365
|
+
Create NotionDatabase instance from API response.
|
366
|
+
"""
|
367
|
+
title = cls._extract_title(db_response)
|
368
|
+
emoji_icon = cls._extract_emoji_icon(db_response)
|
369
|
+
|
370
|
+
instance = cls(
|
371
|
+
database_id=db_response.id,
|
372
|
+
title=title,
|
373
|
+
url=db_response.url,
|
374
|
+
emoji_icon=emoji_icon,
|
375
|
+
properties=db_response.properties,
|
376
|
+
token=token,
|
377
|
+
)
|
187
378
|
|
188
|
-
|
189
|
-
|
379
|
+
cls.logger.info(
|
380
|
+
"Created database manager: '%s' (ID: %s)", title, db_response.id
|
381
|
+
)
|
190
382
|
|
191
|
-
|
192
|
-
bool: True if successful, False otherwise
|
193
|
-
"""
|
194
|
-
try:
|
195
|
-
formatted_page_id = format_uuid(page_id)
|
383
|
+
return instance
|
196
384
|
|
197
|
-
|
385
|
+
@staticmethod
|
386
|
+
def _extract_title(db_response: NotionDatabaseResponse) -> str:
|
387
|
+
"""Extract title from database response."""
|
388
|
+
if db_response.title and len(db_response.title) > 0:
|
389
|
+
return db_response.title[0].plain_text
|
390
|
+
return "Untitled Database"
|
198
391
|
|
199
|
-
|
392
|
+
@staticmethod
|
393
|
+
def _extract_emoji_icon(db_response: NotionDatabaseResponse) -> Optional[str]:
|
394
|
+
"""Extract emoji from database response."""
|
395
|
+
if not db_response.icon:
|
396
|
+
return None
|
200
397
|
|
201
|
-
|
202
|
-
|
203
|
-
return False
|
398
|
+
if db_response.icon.type == "emoji":
|
399
|
+
return db_response.icon.emoji
|
204
400
|
|
205
|
-
|
206
|
-
"Page %s successfully deleted (archived)", formatted_page_id
|
207
|
-
)
|
208
|
-
return True
|
401
|
+
return None
|
209
402
|
|
210
|
-
|
211
|
-
|
212
|
-
|
403
|
+
def _extract_title_from_page(self, page: NotionPageResponse) -> Optional[str]:
|
404
|
+
"""
|
405
|
+
Extracts the title from a NotionPageResponse object.
|
406
|
+
"""
|
407
|
+
if not page.properties:
|
408
|
+
return None
|
213
409
|
|
214
|
-
|
410
|
+
title_property = next(
|
411
|
+
(
|
412
|
+
prop
|
413
|
+
for prop in page.properties.values()
|
414
|
+
if isinstance(prop, dict) and prop.get("type") == "title"
|
415
|
+
),
|
416
|
+
None,
|
417
|
+
)
|
418
|
+
|
419
|
+
if not title_property or "title" not in title_property:
|
420
|
+
return None
|
421
|
+
|
422
|
+
try:
|
423
|
+
title_parts = title_property["title"]
|
424
|
+
return "".join(part.get("plain_text", "") for part in title_parts)
|
425
|
+
|
426
|
+
except (KeyError, TypeError, AttributeError):
|
427
|
+
return None
|
428
|
+
|
429
|
+
async def _get_relation_options(self, property_name: str) -> List[Dict[str, Any]]:
|
215
430
|
"""
|
216
|
-
Retrieve the
|
431
|
+
Retrieve the titles of all pages related to a relation property.
|
432
|
+
|
433
|
+
Args:
|
434
|
+
property_name: The name of the relation property in the database schema.
|
217
435
|
|
218
436
|
Returns:
|
219
|
-
|
437
|
+
A list of titles for all related pages. Returns an empty list if no related pages are found.
|
220
438
|
"""
|
221
|
-
|
222
|
-
db = await self._client.get_database(self.database_id)
|
439
|
+
property_schema = self.properties.get(property_name)
|
223
440
|
|
224
|
-
|
441
|
+
relation_database_id = property_schema.get("relation", {}).get("database_id")
|
225
442
|
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
self.database_id,
|
230
|
-
str(e),
|
231
|
-
)
|
232
|
-
return None
|
443
|
+
search_results = await self.client.query_database(
|
444
|
+
database_id=relation_database_id
|
445
|
+
)
|
233
446
|
|
234
|
-
|
235
|
-
"""Close the client connection."""
|
236
|
-
await self._client.close()
|
447
|
+
return [self._extract_title_from_page(page) for page in search_results.results]
|