notionary 0.2.17__py3-none-any.whl → 0.2.19__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 (113) hide show
  1. notionary/__init__.py +3 -2
  2. notionary/blocks/__init__.py +54 -25
  3. notionary/blocks/audio/__init__.py +7 -0
  4. notionary/blocks/audio/audio_element.py +152 -0
  5. notionary/blocks/audio/audio_markdown_node.py +29 -0
  6. notionary/blocks/audio/audio_models.py +59 -0
  7. notionary/blocks/bookmark/__init__.py +7 -0
  8. notionary/blocks/{bookmark_element.py → bookmark/bookmark_element.py} +20 -65
  9. notionary/blocks/bookmark/bookmark_markdown_node.py +43 -0
  10. notionary/blocks/bookmark/bookmark_models.py +0 -0
  11. notionary/blocks/bulleted_list/__init__.py +7 -0
  12. notionary/blocks/{bulleted_list_element.py → bulleted_list/bulleted_list_element.py} +7 -3
  13. notionary/blocks/bulleted_list/bulleted_list_markdown_node.py +33 -0
  14. notionary/blocks/bulleted_list/bulleted_list_models.py +0 -0
  15. notionary/blocks/callout/__init__.py +7 -0
  16. notionary/blocks/callout/callout_element.py +132 -0
  17. notionary/blocks/callout/callout_markdown_node.py +31 -0
  18. notionary/blocks/callout/callout_models.py +0 -0
  19. notionary/blocks/code/__init__.py +7 -0
  20. notionary/blocks/{code_block_element.py → code/code_element.py} +72 -40
  21. notionary/blocks/code/code_markdown_node.py +43 -0
  22. notionary/blocks/code/code_models.py +0 -0
  23. notionary/blocks/column/__init__.py +5 -0
  24. notionary/blocks/{column_element.py → column/column_element.py} +24 -55
  25. notionary/blocks/column/column_models.py +0 -0
  26. notionary/blocks/divider/__init__.py +7 -0
  27. notionary/blocks/{divider_element.py → divider/divider_element.py} +11 -3
  28. notionary/blocks/divider/divider_markdown_node.py +24 -0
  29. notionary/blocks/divider/divider_models.py +0 -0
  30. notionary/blocks/document/__init__.py +7 -0
  31. notionary/blocks/document/document_element.py +102 -0
  32. notionary/blocks/document/document_markdown_node.py +31 -0
  33. notionary/blocks/document/document_models.py +0 -0
  34. notionary/blocks/embed/__init__.py +7 -0
  35. notionary/blocks/{embed_element.py → embed/embed_element.py} +50 -32
  36. notionary/blocks/embed/embed_markdown_node.py +30 -0
  37. notionary/blocks/embed/embed_models.py +0 -0
  38. notionary/blocks/heading/__init__.py +7 -0
  39. notionary/blocks/{heading_element.py → heading/heading_element.py} +25 -17
  40. notionary/blocks/heading/heading_markdown_node.py +29 -0
  41. notionary/blocks/heading/heading_models.py +0 -0
  42. notionary/blocks/image/__init__.py +7 -0
  43. notionary/blocks/{image_element.py → image/image_element.py} +62 -42
  44. notionary/blocks/image/image_markdown_node.py +33 -0
  45. notionary/blocks/image/image_models.py +0 -0
  46. notionary/blocks/markdown_builder.py +356 -0
  47. notionary/blocks/markdown_node.py +29 -0
  48. notionary/blocks/mention/__init__.py +7 -0
  49. notionary/blocks/{mention_element.py → mention/mention_element.py} +6 -2
  50. notionary/blocks/mention/mention_markdown_node.py +38 -0
  51. notionary/blocks/mention/mention_models.py +0 -0
  52. notionary/blocks/numbered_list/__init__.py +7 -0
  53. notionary/blocks/{numbered_list_element.py → numbered_list/numbered_list_element.py} +10 -6
  54. notionary/blocks/numbered_list/numbered_list_markdown_node.py +29 -0
  55. notionary/blocks/numbered_list/numbered_list_models.py +0 -0
  56. notionary/blocks/paragraph/__init__.py +7 -0
  57. notionary/blocks/{paragraph_element.py → paragraph/paragraph_element.py} +7 -3
  58. notionary/blocks/paragraph/paragraph_markdown_node.py +25 -0
  59. notionary/blocks/paragraph/paragraph_models.py +0 -0
  60. notionary/blocks/quote/__init__.py +7 -0
  61. notionary/blocks/quote/quote_element.py +92 -0
  62. notionary/blocks/quote/quote_markdown_node.py +23 -0
  63. notionary/blocks/quote/quote_models.py +0 -0
  64. notionary/blocks/registry/block_registry.py +17 -3
  65. notionary/blocks/registry/block_registry_builder.py +90 -178
  66. notionary/blocks/shared/__init__.py +0 -0
  67. notionary/blocks/shared/block_client.py +256 -0
  68. notionary/blocks/shared/models.py +713 -0
  69. notionary/blocks/{notion_block_element.py → shared/notion_block_element.py} +8 -5
  70. notionary/blocks/{text_inline_formatter.py → shared/text_inline_formatter.py} +14 -14
  71. notionary/blocks/shared/text_inline_formatter_new.py +139 -0
  72. notionary/blocks/table/__init__.py +7 -0
  73. notionary/blocks/{table_element.py → table/table_element.py} +23 -11
  74. notionary/blocks/table/table_markdown_node.py +40 -0
  75. notionary/blocks/table/table_models.py +0 -0
  76. notionary/blocks/todo/__init__.py +7 -0
  77. notionary/blocks/{todo_element.py → todo/todo_element.py} +8 -4
  78. notionary/blocks/todo/todo_markdown_node.py +31 -0
  79. notionary/blocks/todo/todo_models.py +0 -0
  80. notionary/blocks/toggle/__init__.py +4 -0
  81. notionary/blocks/{toggle_element.py → toggle/toggle_element.py} +7 -3
  82. notionary/blocks/toggle/toggle_markdown_node.py +35 -0
  83. notionary/blocks/toggle/toggle_models.py +0 -0
  84. notionary/blocks/toggleable_heading/__init__.py +9 -0
  85. notionary/blocks/{toggleable_heading_element.py → toggleable_heading/toggleable_heading_element.py} +8 -4
  86. notionary/blocks/toggleable_heading/toggleable_heading_markdown_node.py +43 -0
  87. notionary/blocks/toggleable_heading/toggleable_heading_models.py +0 -0
  88. notionary/blocks/video/__init__.py +7 -0
  89. notionary/blocks/{video_element.py → video/video_element.py} +82 -57
  90. notionary/blocks/video/video_markdown_node.py +30 -0
  91. notionary/file_upload/notion_file_upload.py +1 -1
  92. notionary/page/content/markdown_whitespace_processor.py +80 -0
  93. notionary/page/content/notion_text_length_utils.py +87 -0
  94. notionary/page/content/page_content_retriever.py +18 -10
  95. notionary/page/content/page_content_writer.py +97 -148
  96. notionary/page/formatting/line_processor.py +153 -0
  97. notionary/page/formatting/markdown_to_notion_converter.py +104 -425
  98. notionary/page/notion_page.py +9 -11
  99. notionary/page/notion_to_markdown_converter.py +9 -13
  100. notionary/util/factory_decorator.py +0 -0
  101. notionary/workspace.py +0 -1
  102. {notionary-0.2.17.dist-info → notionary-0.2.19.dist-info}/METADATA +1 -1
  103. notionary-0.2.19.dist-info/RECORD +150 -0
  104. notionary/blocks/audio_element.py +0 -144
  105. notionary/blocks/callout_element.py +0 -122
  106. notionary/blocks/document_element.py +0 -194
  107. notionary/blocks/notion_block_client.py +0 -26
  108. notionary/blocks/qoute_element.py +0 -169
  109. notionary/page/content/notion_page_content_chunker.py +0 -84
  110. notionary/page/formatting/spacer_rules.py +0 -483
  111. notionary-0.2.17.dist-info/RECORD +0 -85
  112. {notionary-0.2.17.dist-info → notionary-0.2.19.dist-info}/LICENSE +0 -0
  113. {notionary-0.2.17.dist-info → notionary-0.2.19.dist-info}/WHEEL +0 -0
@@ -0,0 +1,256 @@
1
+ from typing import Optional, Dict, Any
2
+ from notionary.base_notion_client import BaseNotionClient
3
+ from notionary.util import singleton
4
+ from notionary.blocks.shared.models import Block, BlockChildrenResponse
5
+
6
+
7
+ @singleton
8
+ class NotionBlockClient(BaseNotionClient):
9
+ """
10
+ Client for Notion Block API operations.
11
+ """
12
+
13
+ async def get_block(self, block_id: str) -> Optional[Block]:
14
+ """
15
+ Retrieves a single block by its ID.
16
+ """
17
+ self.logger.debug("Retrieving block: %s", block_id)
18
+
19
+ response = await self.get(f"blocks/{block_id}")
20
+ if response:
21
+ try:
22
+ return Block.model_validate(response)
23
+ except Exception as e:
24
+ self.logger.error("Failed to parse block response: %s", str(e))
25
+ return None
26
+ return None
27
+
28
+ async def get_block_children(
29
+ self, block_id: str, start_cursor: Optional[str] = None, page_size: int = 100
30
+ ) -> Optional[BlockChildrenResponse]:
31
+ """
32
+ Retrieves the children of a block with pagination support.
33
+ """
34
+ self.logger.debug("Retrieving children of block: %s", block_id)
35
+
36
+ params = {"page_size": min(page_size, 100)}
37
+ if start_cursor:
38
+ params["start_cursor"] = start_cursor
39
+
40
+ response = await self.get(f"blocks/{block_id}/children", params=params)
41
+ if response:
42
+ try:
43
+ return BlockChildrenResponse.model_validate(response)
44
+ except Exception as e:
45
+ self.logger.error("Failed to parse block children response: %s", str(e))
46
+ return None
47
+ return None
48
+
49
+ async def get_all_block_children(self, block_id: str) -> list[Block]:
50
+ """
51
+ Retrieves ALL children of a block, handling pagination automatically.
52
+ """
53
+ all_blocks = []
54
+ cursor = None
55
+
56
+ while True:
57
+ response = await self.get_block_children(
58
+ block_id=block_id, start_cursor=cursor, page_size=100
59
+ )
60
+
61
+ if not response:
62
+ break
63
+
64
+ all_blocks.extend(response.results)
65
+
66
+ if not response.has_more:
67
+ break
68
+
69
+ cursor = response.next_cursor
70
+
71
+ self.logger.debug(
72
+ "Retrieved %d total children for block %s", len(all_blocks), block_id
73
+ )
74
+ return all_blocks
75
+
76
+ async def append_block_children(
77
+ self, block_id: str, children: list[Dict[str, Any]], after: Optional[str] = None
78
+ ) -> Optional[BlockChildrenResponse]:
79
+ """
80
+ Appends new child blocks to a parent block.
81
+ Automatically handles batching for more than 100 blocks.
82
+ """
83
+ if not children:
84
+ self.logger.warning("No children provided to append")
85
+ return None
86
+
87
+ self.logger.debug("Appending %d children to block: %s", len(children), block_id)
88
+
89
+ # If 100 or fewer blocks, use single request
90
+ if len(children) <= 100:
91
+ return await self._append_single_batch(block_id, children, after)
92
+
93
+ # For more than 100 blocks, use batch processing
94
+ return await self._append_multiple_batches(block_id, children, after)
95
+
96
+ async def _append_single_batch(
97
+ self, block_id: str, children: list[Dict[str, Any]], after: Optional[str] = None
98
+ ) -> Optional[BlockChildrenResponse]:
99
+ """
100
+ Appends a single batch of blocks (≤100).
101
+ """
102
+ data = {"children": children}
103
+ if after:
104
+ data["after"] = after
105
+
106
+ response = await self.patch(f"blocks/{block_id}/children", data)
107
+ if response:
108
+ try:
109
+ return BlockChildrenResponse.model_validate(response)
110
+ except Exception as e:
111
+ self.logger.error("Failed to parse append response: %s", str(e))
112
+ return None
113
+ return None
114
+
115
+ async def _append_multiple_batches(
116
+ self, block_id: str, children: list[Dict[str, Any]], after: Optional[str] = None
117
+ ) -> Optional[BlockChildrenResponse]:
118
+ """
119
+ Appends multiple batches of blocks, handling pagination.
120
+ """
121
+ all_results = []
122
+ current_after = after
123
+ batch_size = 100
124
+
125
+ self.logger.info(
126
+ "Processing %d blocks in batches of %d", len(children), batch_size
127
+ )
128
+
129
+ # Process blocks in chunks of 100
130
+ for i in range(0, len(children), batch_size):
131
+ batch = children[i : i + batch_size]
132
+ batch_num = (i // batch_size) + 1
133
+ total_batches = (len(children) + batch_size - 1) // batch_size
134
+
135
+ self.logger.debug(
136
+ "Processing batch %d/%d (%d blocks)",
137
+ batch_num,
138
+ total_batches,
139
+ len(batch),
140
+ )
141
+
142
+ # Append current batch
143
+ response = await self._append_single_batch(block_id, batch, current_after)
144
+
145
+ if not response:
146
+ self.logger.error(
147
+ "Failed to append batch %d/%d", batch_num, total_batches
148
+ )
149
+ # Return partial results if we have any
150
+ if all_results:
151
+ return self._combine_batch_responses(all_results)
152
+ return None
153
+
154
+ all_results.append(response)
155
+
156
+ # Update 'after' to the last block ID from this batch for next iteration
157
+ if response.results:
158
+ current_after = response.results[-1].id
159
+
160
+ self.logger.debug(
161
+ "Successfully appended batch %d/%d", batch_num, total_batches
162
+ )
163
+
164
+ self.logger.info(
165
+ "Successfully appended all %d blocks in %d batches",
166
+ len(children),
167
+ len(all_results),
168
+ )
169
+
170
+ # Combine all batch responses into a single response
171
+ return self._combine_batch_responses(all_results)
172
+
173
+ def _combine_batch_responses(
174
+ self, responses: list[BlockChildrenResponse]
175
+ ) -> BlockChildrenResponse:
176
+ """
177
+ Combines multiple batch responses into a single response.
178
+ """
179
+ if not responses:
180
+ # Return empty response structure
181
+ return BlockChildrenResponse(
182
+ object="list",
183
+ results=[],
184
+ next_cursor=None,
185
+ has_more=False,
186
+ type="block",
187
+ block={},
188
+ request_id="",
189
+ )
190
+
191
+ # Use the first response as template and combine all results
192
+ combined = responses[0]
193
+ all_blocks = []
194
+
195
+ for response in responses:
196
+ all_blocks.extend(response.results)
197
+
198
+ # Create new combined response
199
+ return BlockChildrenResponse(
200
+ object=combined.object,
201
+ results=all_blocks,
202
+ next_cursor=None, # No pagination in combined result
203
+ has_more=False, # All blocks are included
204
+ type=combined.type,
205
+ block=combined.block,
206
+ request_id=responses[-1].request_id, # Use last request ID
207
+ )
208
+
209
+ async def update_block(
210
+ self, block_id: str, block_data: Dict[str, Any], archived: Optional[bool] = None
211
+ ) -> Optional[Block]:
212
+ """
213
+ Updates an existing block.
214
+ """
215
+ self.logger.debug("Updating block: %s", block_id)
216
+
217
+ data = block_data.copy()
218
+ if archived is not None:
219
+ data["archived"] = archived
220
+
221
+ response = await self.patch(f"blocks/{block_id}", data)
222
+ if response:
223
+ try:
224
+ return Block.model_validate(response)
225
+ except Exception as e:
226
+ self.logger.error("Failed to parse update response: %s", str(e))
227
+ return None
228
+ return None
229
+
230
+ async def delete_block(self, block_id: str) -> Optional[Block]:
231
+ """
232
+ Deletes (archives) a block.
233
+ """
234
+ self.logger.debug("Deleting block: %s", block_id)
235
+
236
+ success = await self.delete(f"blocks/{block_id}")
237
+ if success:
238
+ # After deletion, retrieve the block to return the updated state
239
+ return await self.get_block(block_id)
240
+ return None
241
+
242
+ async def archive_block(self, block_id: str) -> Optional[Block]:
243
+ """
244
+ Archives a block by setting archived=True.
245
+ """
246
+ self.logger.debug("Archiving block: %s", block_id)
247
+
248
+ return await self.update_block(block_id=block_id, block_data={}, archived=True)
249
+
250
+ async def unarchive_block(self, block_id: str) -> Optional[Block]:
251
+ """
252
+ Unarchives a block by setting archived=False.
253
+ """
254
+ self.logger.debug("Unarchiving block: %s", block_id)
255
+
256
+ return await self.update_block(block_id=block_id, block_data={}, archived=False)