notionary 0.1.1__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.
Files changed (51) hide show
  1. notionary/__init__.py +9 -0
  2. notionary/core/__init__.py +0 -0
  3. notionary/core/converters/__init__.py +50 -0
  4. notionary/core/converters/elements/__init__.py +0 -0
  5. notionary/core/converters/elements/bookmark_element.py +224 -0
  6. notionary/core/converters/elements/callout_element.py +179 -0
  7. notionary/core/converters/elements/code_block_element.py +153 -0
  8. notionary/core/converters/elements/column_element.py +294 -0
  9. notionary/core/converters/elements/divider_element.py +73 -0
  10. notionary/core/converters/elements/heading_element.py +84 -0
  11. notionary/core/converters/elements/image_element.py +130 -0
  12. notionary/core/converters/elements/list_element.py +130 -0
  13. notionary/core/converters/elements/notion_block_element.py +51 -0
  14. notionary/core/converters/elements/paragraph_element.py +73 -0
  15. notionary/core/converters/elements/qoute_element.py +242 -0
  16. notionary/core/converters/elements/table_element.py +306 -0
  17. notionary/core/converters/elements/text_inline_formatter.py +294 -0
  18. notionary/core/converters/elements/todo_lists.py +114 -0
  19. notionary/core/converters/elements/toggle_element.py +205 -0
  20. notionary/core/converters/elements/video_element.py +159 -0
  21. notionary/core/converters/markdown_to_notion_converter.py +482 -0
  22. notionary/core/converters/notion_to_markdown_converter.py +45 -0
  23. notionary/core/converters/registry/__init__.py +0 -0
  24. notionary/core/converters/registry/block_element_registry.py +234 -0
  25. notionary/core/converters/registry/block_element_registry_builder.py +280 -0
  26. notionary/core/database/database_info_service.py +43 -0
  27. notionary/core/database/database_query_service.py +73 -0
  28. notionary/core/database/database_schema_service.py +57 -0
  29. notionary/core/database/models/page_result.py +10 -0
  30. notionary/core/database/notion_database_manager.py +332 -0
  31. notionary/core/database/notion_database_manager_factory.py +233 -0
  32. notionary/core/database/notion_database_schema.py +415 -0
  33. notionary/core/database/notion_database_writer.py +390 -0
  34. notionary/core/database/page_service.py +161 -0
  35. notionary/core/notion_client.py +134 -0
  36. notionary/core/page/meta_data/metadata_editor.py +37 -0
  37. notionary/core/page/notion_page_manager.py +110 -0
  38. notionary/core/page/page_content_manager.py +85 -0
  39. notionary/core/page/property_formatter.py +97 -0
  40. notionary/exceptions/database_exceptions.py +76 -0
  41. notionary/exceptions/page_creation_exception.py +9 -0
  42. notionary/util/logging_mixin.py +47 -0
  43. notionary/util/singleton_decorator.py +20 -0
  44. notionary/util/uuid_utils.py +24 -0
  45. {notionary-0.1.1.dist-info → notionary-0.1.3.dist-info}/METADATA +1 -1
  46. notionary-0.1.3.dist-info/RECORD +49 -0
  47. notionary-0.1.3.dist-info/top_level.txt +1 -0
  48. notionary-0.1.1.dist-info/RECORD +0 -5
  49. notionary-0.1.1.dist-info/top_level.txt +0 -1
  50. {notionary-0.1.1.dist-info → notionary-0.1.3.dist-info}/WHEEL +0 -0
  51. {notionary-0.1.1.dist-info → notionary-0.1.3.dist-info}/licenses/LICENSE +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 ""