notionary 0.1.2__py3-none-any.whl → 0.1.3__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/core/__init__.py +0 -0
- notionary/core/converters/__init__.py +50 -0
- notionary/core/converters/elements/__init__.py +0 -0
- notionary/core/converters/elements/bookmark_element.py +224 -0
- notionary/core/converters/elements/callout_element.py +179 -0
- notionary/core/converters/elements/code_block_element.py +153 -0
- notionary/core/converters/elements/column_element.py +294 -0
- notionary/core/converters/elements/divider_element.py +73 -0
- notionary/core/converters/elements/heading_element.py +84 -0
- notionary/core/converters/elements/image_element.py +130 -0
- notionary/core/converters/elements/list_element.py +130 -0
- notionary/core/converters/elements/notion_block_element.py +51 -0
- notionary/core/converters/elements/paragraph_element.py +73 -0
- notionary/core/converters/elements/qoute_element.py +242 -0
- notionary/core/converters/elements/table_element.py +306 -0
- notionary/core/converters/elements/text_inline_formatter.py +294 -0
- notionary/core/converters/elements/todo_lists.py +114 -0
- notionary/core/converters/elements/toggle_element.py +205 -0
- notionary/core/converters/elements/video_element.py +159 -0
- notionary/core/converters/markdown_to_notion_converter.py +482 -0
- notionary/core/converters/notion_to_markdown_converter.py +45 -0
- notionary/core/converters/registry/__init__.py +0 -0
- notionary/core/converters/registry/block_element_registry.py +234 -0
- notionary/core/converters/registry/block_element_registry_builder.py +280 -0
- notionary/core/database/database_info_service.py +43 -0
- notionary/core/database/database_query_service.py +73 -0
- notionary/core/database/database_schema_service.py +57 -0
- notionary/core/database/models/page_result.py +10 -0
- notionary/core/database/notion_database_manager.py +332 -0
- notionary/core/database/notion_database_manager_factory.py +233 -0
- notionary/core/database/notion_database_schema.py +415 -0
- notionary/core/database/notion_database_writer.py +390 -0
- notionary/core/database/page_service.py +161 -0
- notionary/core/notion_client.py +134 -0
- notionary/core/page/meta_data/metadata_editor.py +37 -0
- notionary/core/page/notion_page_manager.py +110 -0
- notionary/core/page/page_content_manager.py +85 -0
- notionary/core/page/property_formatter.py +97 -0
- notionary/exceptions/database_exceptions.py +76 -0
- notionary/exceptions/page_creation_exception.py +9 -0
- notionary/util/logging_mixin.py +47 -0
- notionary/util/singleton_decorator.py +20 -0
- notionary/util/uuid_utils.py +24 -0
- {notionary-0.1.2.dist-info → notionary-0.1.3.dist-info}/METADATA +1 -1
- notionary-0.1.3.dist-info/RECORD +49 -0
- notionary-0.1.2.dist-info/RECORD +0 -6
- {notionary-0.1.2.dist-info → notionary-0.1.3.dist-info}/WHEEL +0 -0
- {notionary-0.1.2.dist-info → notionary-0.1.3.dist-info}/licenses/LICENSE +0 -0
- {notionary-0.1.2.dist-info → notionary-0.1.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,415 @@
|
|
1
|
+
from typing import (
|
2
|
+
AsyncGenerator,
|
3
|
+
Dict,
|
4
|
+
List,
|
5
|
+
Optional,
|
6
|
+
Any,
|
7
|
+
TypedDict,
|
8
|
+
Union,
|
9
|
+
cast,
|
10
|
+
Literal,
|
11
|
+
)
|
12
|
+
from notionary.core.notion_client import NotionClient
|
13
|
+
from notionary.core.page.notion_page_manager import NotionPageManager
|
14
|
+
from notionary.util.logging_mixin import LoggingMixin
|
15
|
+
|
16
|
+
|
17
|
+
class NotionTextContent(TypedDict):
|
18
|
+
plain_text: str
|
19
|
+
|
20
|
+
|
21
|
+
class NotionTitleProperty(TypedDict):
|
22
|
+
type: Literal["title"]
|
23
|
+
title: List[NotionTextContent]
|
24
|
+
|
25
|
+
|
26
|
+
class NotionSelectOption(TypedDict):
|
27
|
+
name: str
|
28
|
+
id: Optional[str]
|
29
|
+
color: Optional[str]
|
30
|
+
|
31
|
+
|
32
|
+
class NotionSelectProperty(TypedDict):
|
33
|
+
type: Literal["select"]
|
34
|
+
select: Dict[str, List[NotionSelectOption]]
|
35
|
+
|
36
|
+
|
37
|
+
class NotionMultiSelectProperty(TypedDict):
|
38
|
+
type: Literal["multi_select"]
|
39
|
+
multi_select: Dict[str, List[NotionSelectOption]]
|
40
|
+
|
41
|
+
|
42
|
+
class NotionStatusProperty(TypedDict):
|
43
|
+
type: Literal["status"]
|
44
|
+
status: Dict[str, List[NotionSelectOption]]
|
45
|
+
|
46
|
+
|
47
|
+
class NotionRelationProperty(TypedDict):
|
48
|
+
type: Literal["relation"]
|
49
|
+
relation: Dict[str, str]
|
50
|
+
|
51
|
+
|
52
|
+
class NotionNumberProperty(TypedDict):
|
53
|
+
type: Literal["number"]
|
54
|
+
number: Dict[str, Any]
|
55
|
+
|
56
|
+
|
57
|
+
NotionPropertyType = Union[
|
58
|
+
NotionTitleProperty,
|
59
|
+
NotionSelectProperty,
|
60
|
+
NotionMultiSelectProperty,
|
61
|
+
NotionStatusProperty,
|
62
|
+
NotionRelationProperty,
|
63
|
+
NotionNumberProperty,
|
64
|
+
Dict[str, Any], # Fallback
|
65
|
+
]
|
66
|
+
|
67
|
+
|
68
|
+
class RelationOption(TypedDict):
|
69
|
+
id: str
|
70
|
+
title: str
|
71
|
+
|
72
|
+
|
73
|
+
class NotionDatabaseAccessor(LoggingMixin):
|
74
|
+
"""
|
75
|
+
A utility class that provides methods to access Notion databases.
|
76
|
+
Focused on efficient, paginated access to databases without unnecessary complexity.
|
77
|
+
"""
|
78
|
+
|
79
|
+
def __init__(self, client: Optional[NotionClient] = None) -> None:
|
80
|
+
"""
|
81
|
+
Initialize the accessor with a NotionClient.
|
82
|
+
|
83
|
+
Args:
|
84
|
+
client: NotionClient instance for API communication
|
85
|
+
"""
|
86
|
+
self._client = client if client else NotionClient()
|
87
|
+
self.logger.info("NotionDatabaseAccessor initialized")
|
88
|
+
|
89
|
+
async def iter_databases(
|
90
|
+
self, page_size: int = 100
|
91
|
+
) -> AsyncGenerator[Dict[str, Any], None]:
|
92
|
+
"""
|
93
|
+
Asynchronous generator that yields Notion databases one by one.
|
94
|
+
|
95
|
+
Uses the Notion API to provide paginated access to all databases
|
96
|
+
without loading all of them into memory at once.
|
97
|
+
|
98
|
+
Args:
|
99
|
+
page_size: The number of databases to fetch per request
|
100
|
+
|
101
|
+
Yields:
|
102
|
+
Individual database objects from the Notion API
|
103
|
+
"""
|
104
|
+
start_cursor: Optional[str] = None
|
105
|
+
|
106
|
+
while True:
|
107
|
+
body: Dict[str, Any] = {
|
108
|
+
"filter": {"value": "database", "property": "object"},
|
109
|
+
"page_size": page_size,
|
110
|
+
}
|
111
|
+
|
112
|
+
if start_cursor:
|
113
|
+
body["start_cursor"] = start_cursor
|
114
|
+
|
115
|
+
result = await self._client.post("search", data=body)
|
116
|
+
|
117
|
+
if not result or "results" not in result:
|
118
|
+
self.logger.error("Error fetching databases")
|
119
|
+
break
|
120
|
+
|
121
|
+
for database in result["results"]:
|
122
|
+
yield database
|
123
|
+
|
124
|
+
if "has_more" in result and result["has_more"] and "next_cursor" in result:
|
125
|
+
start_cursor = result["next_cursor"]
|
126
|
+
else:
|
127
|
+
break
|
128
|
+
|
129
|
+
async def get_database(self, database_id: str) -> Optional[Dict[str, Any]]:
|
130
|
+
"""
|
131
|
+
Get the details for a specific database.
|
132
|
+
|
133
|
+
Args:
|
134
|
+
database_id: The ID of the database
|
135
|
+
|
136
|
+
Returns:
|
137
|
+
Database details or None if not found
|
138
|
+
"""
|
139
|
+
db_details = await self._client.get(f"databases/{database_id}")
|
140
|
+
if not db_details:
|
141
|
+
self.logger.error("Failed to retrieve database %s", database_id)
|
142
|
+
return None
|
143
|
+
|
144
|
+
return db_details
|
145
|
+
|
146
|
+
def extract_database_title(self, database: Dict[str, Any]) -> str:
|
147
|
+
"""
|
148
|
+
Extract the database title from a Notion API response.
|
149
|
+
|
150
|
+
Args:
|
151
|
+
database: The database object from the Notion API
|
152
|
+
|
153
|
+
Returns:
|
154
|
+
The extracted title or "Untitled" if no title is found
|
155
|
+
"""
|
156
|
+
title = "Untitled"
|
157
|
+
|
158
|
+
if "title" in database:
|
159
|
+
title_parts = []
|
160
|
+
for text_obj in database["title"]:
|
161
|
+
if "plain_text" in text_obj:
|
162
|
+
title_parts.append(text_obj["plain_text"])
|
163
|
+
|
164
|
+
if title_parts:
|
165
|
+
title = "".join(title_parts)
|
166
|
+
|
167
|
+
return title
|
168
|
+
|
169
|
+
|
170
|
+
class NotionDatabaseSchema:
|
171
|
+
"""
|
172
|
+
Represents the schema of a specific Notion database.
|
173
|
+
Manages property information, options, and relations for a single database.
|
174
|
+
"""
|
175
|
+
|
176
|
+
def __init__(self, database_id: str, client: NotionClient) -> None:
|
177
|
+
"""
|
178
|
+
Initialize a database schema handler for a specific database.
|
179
|
+
|
180
|
+
Args:
|
181
|
+
database_id: The ID of the database
|
182
|
+
client: An instance of NotionClient for API requests
|
183
|
+
"""
|
184
|
+
self.database_id: str = database_id
|
185
|
+
self._client: NotionClient = client
|
186
|
+
self._properties: Dict[str, NotionPropertyType] = {}
|
187
|
+
self._loaded: bool = False
|
188
|
+
|
189
|
+
async def load(self) -> bool:
|
190
|
+
"""
|
191
|
+
Load the database schema from the Notion API.
|
192
|
+
|
193
|
+
Returns:
|
194
|
+
True if the schema was loaded successfully, False otherwise
|
195
|
+
"""
|
196
|
+
if self._loaded:
|
197
|
+
return True
|
198
|
+
|
199
|
+
db_details = await self._client.get(f"databases/{self.database_id}")
|
200
|
+
if not db_details or "properties" not in db_details:
|
201
|
+
return False
|
202
|
+
|
203
|
+
self._properties = db_details["properties"]
|
204
|
+
self._loaded = True
|
205
|
+
return True
|
206
|
+
|
207
|
+
async def get_property_types(self) -> Dict[str, str]:
|
208
|
+
"""
|
209
|
+
Get a mapping of property names to their types.
|
210
|
+
|
211
|
+
Returns:
|
212
|
+
A dictionary mapping property names to types
|
213
|
+
"""
|
214
|
+
if not self._loaded:
|
215
|
+
await self.load()
|
216
|
+
|
217
|
+
return {name: prop.get("type", "") for name, prop in self._properties.items()}
|
218
|
+
|
219
|
+
async def get_select_options(self, property_name: str) -> List[NotionSelectOption]:
|
220
|
+
"""
|
221
|
+
Get the options for a select, multi_select, or status property.
|
222
|
+
|
223
|
+
Args:
|
224
|
+
property_name: The name of the property
|
225
|
+
|
226
|
+
Returns:
|
227
|
+
A list of option objects
|
228
|
+
"""
|
229
|
+
if not self._loaded:
|
230
|
+
await self.load()
|
231
|
+
|
232
|
+
if property_name not in self._properties:
|
233
|
+
return []
|
234
|
+
|
235
|
+
prop = self._properties[property_name]
|
236
|
+
prop_type = prop.get("type", "")
|
237
|
+
|
238
|
+
if prop_type not in ["select", "multi_select", "status"]:
|
239
|
+
return []
|
240
|
+
|
241
|
+
if prop_type in prop and "options" in prop[prop_type]:
|
242
|
+
return cast(List[NotionSelectOption], prop[prop_type]["options"])
|
243
|
+
|
244
|
+
return []
|
245
|
+
|
246
|
+
async def get_relation_options(
|
247
|
+
self, property_name: str, limit: int = 100
|
248
|
+
) -> List[RelationOption]:
|
249
|
+
"""
|
250
|
+
Get available options for a relation property (pages in the related database).
|
251
|
+
|
252
|
+
Args:
|
253
|
+
property_name: The name of the relation property
|
254
|
+
limit: Maximum number of options to retrieve
|
255
|
+
|
256
|
+
Returns:
|
257
|
+
List of options with id and title
|
258
|
+
"""
|
259
|
+
related_db_id = await self.get_relation_database_id(property_name)
|
260
|
+
if not related_db_id:
|
261
|
+
return []
|
262
|
+
|
263
|
+
pages = await self._query_database_pages(related_db_id, limit)
|
264
|
+
return self._extract_page_titles_and_ids(pages)
|
265
|
+
|
266
|
+
async def get_relation_database_id(self, property_name: str) -> Optional[str]:
|
267
|
+
"""
|
268
|
+
Get the ID of the related database for a relation property.
|
269
|
+
|
270
|
+
Args:
|
271
|
+
property_name: The name of the property
|
272
|
+
|
273
|
+
Returns:
|
274
|
+
The ID of the related database or None
|
275
|
+
"""
|
276
|
+
if not self._loaded:
|
277
|
+
await self.load()
|
278
|
+
|
279
|
+
if property_name not in self._properties:
|
280
|
+
return None
|
281
|
+
|
282
|
+
prop = self._properties[property_name]
|
283
|
+
prop_type = prop.get("type", "")
|
284
|
+
|
285
|
+
if prop_type != "relation" or "relation" not in prop:
|
286
|
+
return None
|
287
|
+
|
288
|
+
relation_prop = cast(NotionRelationProperty, prop)
|
289
|
+
return relation_prop["relation"].get("database_id")
|
290
|
+
|
291
|
+
def _extract_page_titles_and_ids(
|
292
|
+
self, pages: List[NotionPageManager]
|
293
|
+
) -> List[RelationOption]:
|
294
|
+
"""
|
295
|
+
Extract titles and IDs from page objects.
|
296
|
+
|
297
|
+
Args:
|
298
|
+
pages: List of page objects from the Notion API
|
299
|
+
|
300
|
+
Returns:
|
301
|
+
List of dictionaries with id and title for each page
|
302
|
+
"""
|
303
|
+
options: List[RelationOption] = []
|
304
|
+
|
305
|
+
for page_manager in pages:
|
306
|
+
page_title = page_manager.title or "Untitled"
|
307
|
+
options.append({"id": page_manager.page_id, "title": page_title})
|
308
|
+
|
309
|
+
return options
|
310
|
+
|
311
|
+
async def _query_database_pages(
|
312
|
+
self, database_id: str, limit: int = 100
|
313
|
+
) -> List[Dict[str, Any]]:
|
314
|
+
"""
|
315
|
+
Returns:
|
316
|
+
List of page objects from the Notion API
|
317
|
+
"""
|
318
|
+
pages: List[Dict[str, Any]] = []
|
319
|
+
count = 0
|
320
|
+
|
321
|
+
async for page in self.iter_database_pages(
|
322
|
+
database_id=database_id, page_size=min(limit, 100)
|
323
|
+
):
|
324
|
+
pages.append(page)
|
325
|
+
count += 1
|
326
|
+
|
327
|
+
if count >= limit:
|
328
|
+
break
|
329
|
+
|
330
|
+
return pages
|
331
|
+
|
332
|
+
async def iter_database_pages(
|
333
|
+
self,
|
334
|
+
database_id: Optional[str] = None,
|
335
|
+
page_size: int = 100,
|
336
|
+
filter_conditions: Optional[Dict[str, Any]] = None,
|
337
|
+
sorts: Optional[List[Dict[str, Any]]] = None,
|
338
|
+
) -> AsyncGenerator[NotionPageManager, None]:
|
339
|
+
"""
|
340
|
+
Asynchronous generator that yields pages from a Notion database one by one.
|
341
|
+
|
342
|
+
Uses the Notion API to provide paginated access to all pages in a database
|
343
|
+
without loading all of them into memory at once.
|
344
|
+
|
345
|
+
Args:
|
346
|
+
database_id: The ID of the database to query (uses self.database_id if None)
|
347
|
+
page_size: The number of pages to fetch per request
|
348
|
+
filter_conditions: Optional filter to apply to the database query
|
349
|
+
sorts: Optional sort instructions for the database query
|
350
|
+
|
351
|
+
Yields:
|
352
|
+
Individual page objects from the Notion API
|
353
|
+
"""
|
354
|
+
db_id = database_id or self.database_id
|
355
|
+
if not db_id:
|
356
|
+
raise ValueError("No database ID provided")
|
357
|
+
|
358
|
+
start_cursor: Optional[str] = None
|
359
|
+
has_more = True
|
360
|
+
|
361
|
+
body: Dict[str, Any] = {"page_size": page_size}
|
362
|
+
|
363
|
+
if filter_conditions:
|
364
|
+
body["filter"] = filter_conditions
|
365
|
+
|
366
|
+
if sorts:
|
367
|
+
body["sorts"] = sorts
|
368
|
+
|
369
|
+
while has_more:
|
370
|
+
current_body = body.copy()
|
371
|
+
if start_cursor:
|
372
|
+
current_body["start_cursor"] = start_cursor
|
373
|
+
|
374
|
+
result = await self._client.post(
|
375
|
+
f"databases/{db_id}/query", data=current_body
|
376
|
+
)
|
377
|
+
|
378
|
+
if not result or "results" not in result:
|
379
|
+
return
|
380
|
+
|
381
|
+
for page in result["results"]:
|
382
|
+
page_id = page.get("id", "")
|
383
|
+
title = self._extract_page_title(page)
|
384
|
+
|
385
|
+
notion_page_manager = NotionPageManager(page_id=page_id, title=title)
|
386
|
+
yield notion_page_manager
|
387
|
+
|
388
|
+
has_more = result.get("has_more", False)
|
389
|
+
start_cursor = result.get("next_cursor") if has_more else None
|
390
|
+
|
391
|
+
def _extract_page_title(self, page: Dict[str, Any]) -> str:
|
392
|
+
"""
|
393
|
+
Extracts the title from a Notion page object.
|
394
|
+
|
395
|
+
Args:
|
396
|
+
page: The Notion page object
|
397
|
+
|
398
|
+
Returns:
|
399
|
+
The extracted title as a string, or an empty string if no title found
|
400
|
+
"""
|
401
|
+
properties = page.get("properties", {})
|
402
|
+
if not properties:
|
403
|
+
return ""
|
404
|
+
|
405
|
+
for prop_value in properties.values():
|
406
|
+
if prop_value.get("type") != "title":
|
407
|
+
continue
|
408
|
+
|
409
|
+
title_array = prop_value.get("title", [])
|
410
|
+
if not title_array:
|
411
|
+
continue
|
412
|
+
|
413
|
+
return title_array[0].get("plain_text", "")
|
414
|
+
|
415
|
+
return ""
|