notionary 0.2.7__py3-none-any.whl → 0.2.8__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/elements/column_element.py +303 -154
- notionary/elements/divider_element.py +1 -1
- notionary/elements/heading_element.py +4 -1
- notionary/elements/registry/block_registry.py +1 -1
- notionary/elements/registry/block_registry_builder.py +3 -2
- notionary/page/content/page_content_writer.py +102 -12
- notionary/page/markdown_to_notion_converter.py +124 -23
- notionary/prompting/markdown_syntax_prompt_generator.py +2 -4
- {notionary-0.2.7.dist-info → notionary-0.2.8.dist-info}/METADATA +28 -2
- {notionary-0.2.7.dist-info → notionary-0.2.8.dist-info}/RECORD +13 -13
- {notionary-0.2.7.dist-info → notionary-0.2.8.dist-info}/WHEEL +1 -1
- {notionary-0.2.7.dist-info → notionary-0.2.8.dist-info}/licenses/LICENSE +0 -0
- {notionary-0.2.7.dist-info → notionary-0.2.8.dist-info}/top_level.txt +0 -0
@@ -1,113 +1,322 @@
|
|
1
1
|
import re
|
2
|
-
from typing import Dict, Any, Optional, List, Tuple
|
2
|
+
from typing import Dict, Any, Optional, List, Tuple, Callable
|
3
|
+
|
3
4
|
from notionary.elements.notion_block_element import NotionBlockElement
|
4
|
-
from notionary.prompting.element_prompt_content import
|
5
|
-
ElementPromptBuilder,
|
6
|
-
ElementPromptContent,
|
7
|
-
)
|
5
|
+
from notionary.prompting.element_prompt_content import ElementPromptContent
|
8
6
|
|
9
7
|
|
10
|
-
|
11
|
-
class ColumnsElement(NotionBlockElement):
|
8
|
+
class ColumnElement(NotionBlockElement):
|
12
9
|
"""
|
13
|
-
Handles conversion between Markdown column syntax and Notion
|
10
|
+
Handles conversion between custom Markdown column syntax and Notion column blocks.
|
11
|
+
|
12
|
+
Markdown column syntax:
|
13
|
+
::: columns
|
14
|
+
::: column
|
15
|
+
Content for first column
|
16
|
+
:::
|
17
|
+
::: column
|
18
|
+
Content for second column
|
19
|
+
:::
|
20
|
+
:::
|
14
21
|
|
15
|
-
|
16
|
-
It returns a column_list block with placeholder content, as the actual columns
|
17
|
-
must be added as children after the column_list is created.
|
22
|
+
This creates a column layout in Notion with the specified content in each column.
|
18
23
|
"""
|
19
24
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
)
|
25
|
+
COLUMNS_START = re.compile(r"^:::\s*columns\s*$")
|
26
|
+
COLUMN_START = re.compile(r"^:::\s*column\s*$")
|
27
|
+
BLOCK_END = re.compile(r"^:::\s*$")
|
24
28
|
|
25
|
-
|
29
|
+
_converter_callback = None
|
26
30
|
|
27
31
|
@classmethod
|
28
|
-
def
|
29
|
-
|
30
|
-
|
32
|
+
def set_converter_callback(
|
33
|
+
cls, callback: Callable[[str], List[Dict[str, Any]]]
|
34
|
+
) -> None:
|
35
|
+
"""
|
36
|
+
Setze die Callback-Funktion, die zum Konvertieren von Markdown zu Notion-Blöcken verwendet wird.
|
31
37
|
|
32
|
-
|
33
|
-
|
34
|
-
"""
|
35
|
-
|
38
|
+
Args:
|
39
|
+
callback: Funktion, die Markdown-Text annimmt und eine Liste von Notion-Blöcken zurückgibt
|
40
|
+
"""
|
41
|
+
cls._converter_callback = callback
|
36
42
|
|
37
|
-
@
|
38
|
-
def
|
39
|
-
"""
|
40
|
-
|
41
|
-
if not match:
|
42
|
-
return None
|
43
|
+
@staticmethod
|
44
|
+
def match_markdown(text: str) -> bool:
|
45
|
+
"""Check if text starts a columns block."""
|
46
|
+
return bool(ColumnElement.COLUMNS_START.match(text.strip()))
|
43
47
|
|
44
|
-
|
45
|
-
|
48
|
+
@staticmethod
|
49
|
+
def match_notion(block: Dict[str, Any]) -> bool:
|
50
|
+
"""Check if block is a Notion column_list."""
|
51
|
+
return block.get("type") == "column_list"
|
52
|
+
|
53
|
+
@staticmethod
|
54
|
+
def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
|
55
|
+
"""
|
56
|
+
Convert markdown column syntax to Notion column blocks.
|
46
57
|
|
47
|
-
|
58
|
+
Note: This only processes the first line (columns start).
|
59
|
+
The full column content needs to be processed separately.
|
60
|
+
"""
|
61
|
+
if not ColumnElement.COLUMNS_START.match(text.strip()):
|
48
62
|
return None
|
49
63
|
|
50
|
-
|
64
|
+
# Create an empty column_list block
|
65
|
+
# Child columns will be added by the column processor
|
66
|
+
return {"type": "column_list", "column_list": {"children": []}}
|
51
67
|
|
52
|
-
@
|
53
|
-
def notion_to_markdown(
|
54
|
-
"""Convert Notion column_list block to markdown
|
68
|
+
@staticmethod
|
69
|
+
def notion_to_markdown(block: Dict[str, Any]) -> Optional[str]:
|
70
|
+
"""Convert Notion column_list block to markdown column syntax."""
|
55
71
|
if block.get("type") != "column_list":
|
56
72
|
return None
|
57
73
|
|
58
|
-
|
59
|
-
|
60
|
-
|
74
|
+
column_children = block.get("column_list", {}).get("children", [])
|
75
|
+
|
76
|
+
# Start the columns block
|
77
|
+
result = ["::: columns"]
|
78
|
+
|
79
|
+
# Process each column
|
80
|
+
for column_block in column_children:
|
81
|
+
if column_block.get("type") == "column":
|
82
|
+
result.append("::: column")
|
83
|
+
|
84
|
+
for _ in column_block.get("column", {}).get("children", []):
|
85
|
+
result.append(" [Column content]") # Placeholder
|
86
|
+
|
87
|
+
result.append(":::")
|
61
88
|
|
62
|
-
#
|
63
|
-
|
64
|
-
markdown += "::: column\nColumn content here\n:::\n"
|
89
|
+
# End the columns block
|
90
|
+
result.append(":::")
|
65
91
|
|
66
|
-
|
67
|
-
|
92
|
+
return "\n".join(result)
|
93
|
+
|
94
|
+
@staticmethod
|
95
|
+
def is_multiline() -> bool:
|
96
|
+
"""Column blocks span multiple lines."""
|
97
|
+
return True
|
68
98
|
|
69
99
|
@classmethod
|
70
|
-
def find_matches(
|
100
|
+
def find_matches(
|
101
|
+
cls, text: str, converter_callback: Optional[Callable] = None
|
102
|
+
) -> List[Tuple[int, int, Dict[str, Any]]]:
|
71
103
|
"""
|
72
|
-
Find all column block matches in the text and return their positions.
|
104
|
+
Find all column block matches in the text and return their positions and blocks.
|
73
105
|
|
74
106
|
Args:
|
75
|
-
text: The
|
107
|
+
text: The input markdown text
|
108
|
+
converter_callback: Optional callback to convert nested content
|
76
109
|
|
77
110
|
Returns:
|
78
|
-
List of tuples
|
111
|
+
List of tuples (start_pos, end_pos, block)
|
79
112
|
"""
|
113
|
+
# Wenn ein Callback übergeben wurde, nutze diesen, sonst die gespeicherte Referenz
|
114
|
+
converter = converter_callback or cls._converter_callback
|
115
|
+
if not converter:
|
116
|
+
raise ValueError(
|
117
|
+
"No converter callback provided for ColumnElement. Call set_converter_callback first or provide converter_callback parameter."
|
118
|
+
)
|
119
|
+
|
80
120
|
matches = []
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
121
|
+
lines = text.split("\n")
|
122
|
+
i = 0
|
123
|
+
|
124
|
+
while i < len(lines):
|
125
|
+
# Skip non-column lines
|
126
|
+
if not ColumnElement.COLUMNS_START.match(lines[i].strip()):
|
127
|
+
i += 1
|
128
|
+
continue
|
129
|
+
|
130
|
+
# Process a column block and add to matches
|
131
|
+
column_block_info = cls._process_column_block(
|
132
|
+
lines=lines, start_index=i, converter_callback=converter
|
133
|
+
)
|
134
|
+
matches.append(column_block_info)
|
135
|
+
|
136
|
+
# Skip to the end of the processed column block
|
137
|
+
i = column_block_info[3] # i is returned as the 4th element in the tuple
|
138
|
+
|
139
|
+
return [(start, end, block) for start, end, block, _ in matches]
|
140
|
+
|
141
|
+
@classmethod
|
142
|
+
def _process_column_block(
|
143
|
+
cls, lines: List[str], start_index: int, converter_callback: Callable
|
144
|
+
) -> Tuple[int, int, Dict[str, Any], int]:
|
145
|
+
"""
|
146
|
+
Process a complete column block structure from the given starting line.
|
147
|
+
|
148
|
+
Args:
|
149
|
+
lines: All lines of the text
|
150
|
+
start_index: Index of the column block start line
|
151
|
+
converter_callback: Callback function to convert markdown to notion blocks
|
152
|
+
|
153
|
+
Returns:
|
154
|
+
Tuple of (start_pos, end_pos, block, next_line_index)
|
155
|
+
"""
|
156
|
+
columns_start = start_index
|
157
|
+
columns_block = cls.markdown_to_notion(lines[start_index].strip())
|
158
|
+
columns_children = []
|
159
|
+
|
160
|
+
next_index = cls._collect_columns(
|
161
|
+
lines, start_index + 1, columns_children, converter_callback
|
162
|
+
)
|
163
|
+
|
164
|
+
# Add columns to the main block
|
165
|
+
if columns_children:
|
166
|
+
columns_block["column_list"]["children"] = columns_children
|
167
|
+
|
168
|
+
# Calculate positions
|
169
|
+
start_pos = sum(len(lines[j]) + 1 for j in range(columns_start))
|
170
|
+
end_pos = sum(len(lines[j]) + 1 for j in range(next_index))
|
171
|
+
|
172
|
+
return (start_pos, end_pos, columns_block, next_index)
|
173
|
+
|
174
|
+
@classmethod
|
175
|
+
def _collect_columns(
|
176
|
+
cls,
|
177
|
+
lines: List[str],
|
178
|
+
start_index: int,
|
179
|
+
columns_children: List[Dict[str, Any]],
|
180
|
+
converter_callback: Callable,
|
181
|
+
) -> int:
|
182
|
+
"""
|
183
|
+
Collect all columns within a column block structure.
|
184
|
+
|
185
|
+
Args:
|
186
|
+
lines: All lines of the text
|
187
|
+
start_index: Index to start collecting from
|
188
|
+
columns_children: List to append collected columns to
|
189
|
+
converter_callback: Callback function to convert column content
|
190
|
+
|
191
|
+
Returns:
|
192
|
+
Next line index after all columns have been processed
|
193
|
+
"""
|
194
|
+
i = start_index
|
195
|
+
in_column = False
|
196
|
+
column_content = []
|
197
|
+
|
198
|
+
while i < len(lines):
|
199
|
+
current_line = lines[i].strip()
|
200
|
+
|
201
|
+
if cls.COLUMNS_START.match(current_line):
|
202
|
+
break
|
203
|
+
|
204
|
+
if cls.COLUMN_START.match(current_line):
|
205
|
+
cls._finalize_column(
|
206
|
+
column_content, columns_children, in_column, converter_callback
|
207
|
+
)
|
208
|
+
column_content = []
|
209
|
+
in_column = True
|
210
|
+
i += 1
|
211
|
+
continue
|
212
|
+
|
213
|
+
if cls.BLOCK_END.match(current_line) and in_column:
|
214
|
+
cls._finalize_column(
|
215
|
+
column_content, columns_children, in_column, converter_callback
|
216
|
+
)
|
217
|
+
column_content = []
|
218
|
+
in_column = False
|
219
|
+
i += 1
|
220
|
+
continue
|
221
|
+
|
222
|
+
if cls.BLOCK_END.match(current_line) and not in_column:
|
223
|
+
i += 1
|
224
|
+
break
|
225
|
+
|
226
|
+
if in_column:
|
227
|
+
column_content.append(lines[i])
|
228
|
+
|
229
|
+
i += 1
|
230
|
+
|
231
|
+
cls._finalize_column(
|
232
|
+
column_content, columns_children, in_column, converter_callback
|
233
|
+
)
|
234
|
+
|
235
|
+
return i
|
236
|
+
|
237
|
+
@staticmethod
|
238
|
+
def _finalize_column(
|
239
|
+
column_content: List[str],
|
240
|
+
columns_children: List[Dict[str, Any]],
|
241
|
+
in_column: bool,
|
242
|
+
converter_callback: Callable,
|
243
|
+
) -> None:
|
244
|
+
"""
|
245
|
+
Finalize a column by processing its content and adding it to the columns_children list.
|
246
|
+
|
247
|
+
Args:
|
248
|
+
column_content: Content lines of the column
|
249
|
+
columns_children: List to append the column block to
|
250
|
+
in_column: Whether we're currently in a column (if False, does nothing)
|
251
|
+
converter_callback: Callback function to convert column content
|
252
|
+
"""
|
253
|
+
if not (in_column and column_content):
|
254
|
+
return
|
255
|
+
|
256
|
+
processed_content = ColumnElement._preprocess_column_content(column_content)
|
257
|
+
|
258
|
+
column_blocks = converter_callback("\n".join(processed_content))
|
85
259
|
|
86
|
-
|
260
|
+
# Create column block
|
261
|
+
column_block = {"type": "column", "column": {"children": column_blocks}}
|
262
|
+
columns_children.append(column_block)
|
87
263
|
|
88
264
|
@classmethod
|
89
265
|
def is_multiline(cls) -> bool:
|
266
|
+
"""Column blocks span multiple lines."""
|
90
267
|
return True
|
268
|
+
|
269
|
+
@staticmethod
|
270
|
+
def _preprocess_column_content(lines: List[str]) -> List[str]:
|
271
|
+
"""
|
272
|
+
Preprocess column content to handle special cases like first headings.
|
273
|
+
|
274
|
+
This removes any spacer markers that might have been added before the first
|
275
|
+
heading in a column, as each column should have its own heading context.
|
276
|
+
|
277
|
+
Args:
|
278
|
+
lines: The lines of content for the column
|
279
|
+
|
280
|
+
Returns:
|
281
|
+
Processed lines ready for conversion
|
282
|
+
"""
|
283
|
+
from notionary.page.markdown_to_notion_converter import MarkdownToNotionConverter
|
284
|
+
|
285
|
+
processed_lines = []
|
286
|
+
found_first_heading = False
|
287
|
+
spacer_marker = MarkdownToNotionConverter.SPACER_MARKER
|
288
|
+
|
289
|
+
i = 0
|
290
|
+
while i < len(lines):
|
291
|
+
line = lines[i]
|
292
|
+
|
293
|
+
# Check if this is a heading line
|
294
|
+
if re.match(r"^(#{1,6})\s+(.+)$", line.strip()):
|
295
|
+
# If it's the first heading, look ahead to check for spacer
|
296
|
+
if not found_first_heading and i > 0 and processed_lines and processed_lines[-1] == spacer_marker:
|
297
|
+
# Remove spacer before first heading in column
|
298
|
+
processed_lines.pop()
|
299
|
+
|
300
|
+
found_first_heading = True
|
301
|
+
|
302
|
+
processed_lines.append(line)
|
303
|
+
i += 1
|
304
|
+
|
305
|
+
return processed_lines
|
91
306
|
|
92
307
|
@classmethod
|
93
308
|
def get_llm_prompt_content(cls) -> ElementPromptContent:
|
94
309
|
"""
|
95
|
-
Returns structured LLM prompt metadata for the
|
310
|
+
Returns structured LLM prompt metadata for the column layout element.
|
96
311
|
"""
|
97
|
-
return
|
98
|
-
|
99
|
-
|
100
|
-
"
|
101
|
-
"
|
102
|
-
"
|
103
|
-
)
|
104
|
-
|
105
|
-
"Use columns when you need to present information side-by-side for comparison, create visual balance "
|
106
|
-
"in your layout, or organize related content in parallel. Great for pros/cons lists, before/after "
|
107
|
-
"comparisons, or displaying multiple related items. Keep column content balanced in length for best "
|
108
|
-
"visual results."
|
109
|
-
)
|
110
|
-
.with_syntax(
|
312
|
+
return {
|
313
|
+
"description": "Creates a multi-column layout that displays content side by side.",
|
314
|
+
"when_to_use": (
|
315
|
+
"Use columns sparingly, only for direct comparisons or when parallel presentation significantly improves readability. "
|
316
|
+
"Best for pros/cons lists, feature comparisons, or pairing images with descriptions. "
|
317
|
+
"Avoid overusing as it can complicate document structure."
|
318
|
+
),
|
319
|
+
"syntax": (
|
111
320
|
"::: columns\n"
|
112
321
|
"::: column\n"
|
113
322
|
"Content for first column\n"
|
@@ -116,89 +325,29 @@ class ColumnsElement(NotionBlockElement):
|
|
116
325
|
"Content for second column\n"
|
117
326
|
":::\n"
|
118
327
|
":::"
|
119
|
-
)
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
"::: column\n"
|
146
|
-
"**JavaScript**\n"
|
147
|
-
"```javascript\n"
|
148
|
-
"console.log('Hello');\n"
|
149
|
-
"```\n"
|
150
|
-
":::\n"
|
151
|
-
"::: column\n"
|
152
|
-
"**Ruby**\n"
|
153
|
-
"```ruby\n"
|
154
|
-
"puts 'Hello'\n"
|
155
|
-
"```\n"
|
156
|
-
":::\n"
|
157
|
-
":::",
|
158
|
-
# Mixed content example
|
159
|
-
"::: columns\n"
|
160
|
-
"::: column\n"
|
161
|
-
"\n"
|
162
|
-
"Product photo\n"
|
163
|
-
":::\n"
|
164
|
-
"::: column\n"
|
165
|
-
"## Product Details\n"
|
166
|
-
"- Price: $99\n"
|
167
|
-
"- Weight: 2kg\n"
|
168
|
-
"- Color: Blue\n"
|
169
|
-
"\n"
|
170
|
-
"[Order Now](link)\n"
|
171
|
-
":::\n"
|
172
|
-
":::",
|
173
|
-
]
|
174
|
-
)
|
175
|
-
.with_avoidance_guidelines(
|
176
|
-
"Avoid nesting column blocks within column blocks - this creates confusing layouts. "
|
177
|
-
"Don't use columns for content that should be read sequentially. Keep the number of columns "
|
178
|
-
"reasonable (2-4 max) for readability. Ensure each ::: marker is on its own line with proper "
|
179
|
-
"nesting. Don't mix column syntax with regular markdown formatting on the same line."
|
180
|
-
)
|
181
|
-
.build()
|
182
|
-
)
|
183
|
-
|
184
|
-
@classmethod
|
185
|
-
def get_column_content(cls, text: str) -> List[str]:
|
186
|
-
"""
|
187
|
-
Extract the content of individual columns from the markdown.
|
188
|
-
This is a helper method that can be used by the implementation
|
189
|
-
to process column content separately.
|
190
|
-
|
191
|
-
Args:
|
192
|
-
text: The complete columns markdown block
|
193
|
-
|
194
|
-
Returns:
|
195
|
-
List of column content strings
|
196
|
-
"""
|
197
|
-
match = cls.PATTERN.search(text)
|
198
|
-
if not match:
|
199
|
-
return []
|
200
|
-
|
201
|
-
columns_content = match.group(1)
|
202
|
-
return [
|
203
|
-
content.strip() for content in cls.COLUMN_PATTERN.findall(columns_content)
|
204
|
-
]
|
328
|
+
),
|
329
|
+
"examples": [
|
330
|
+
"::: columns\n"
|
331
|
+
"::: column\n"
|
332
|
+
"## Features\n"
|
333
|
+
"- Fast response time\n"
|
334
|
+
"- Intuitive interface\n"
|
335
|
+
"- Regular updates\n"
|
336
|
+
":::\n"
|
337
|
+
"::: column\n"
|
338
|
+
"## Benefits\n"
|
339
|
+
"- Increased productivity\n"
|
340
|
+
"- Better collaboration\n"
|
341
|
+
"- Simplified workflows\n"
|
342
|
+
":::\n"
|
343
|
+
":::",
|
344
|
+
"::: columns\n"
|
345
|
+
"::: column\n"
|
346
|
+
"\n"
|
347
|
+
":::\n"
|
348
|
+
"::: column\n"
|
349
|
+
"This text appears next to the image, creating a media-with-caption style layout that's perfect for documentation or articles.\n"
|
350
|
+
":::\n"
|
351
|
+
":::",
|
352
|
+
],
|
353
|
+
}
|
@@ -85,7 +85,10 @@ class HeadingElement(NotionBlockElement):
|
|
85
85
|
.with_usage_guidelines(
|
86
86
|
"Use to group content into sections and define a visual hierarchy."
|
87
87
|
)
|
88
|
+
.with_avoidance_guidelines(
|
89
|
+
"Only H1-H3 syntax is supported. H4 and deeper heading levels are not available."
|
90
|
+
)
|
88
91
|
.with_syntax("## Your Heading Text")
|
89
92
|
.with_standard_markdown()
|
90
93
|
.build()
|
91
|
-
)
|
94
|
+
)
|
@@ -126,7 +126,7 @@ class BlockRegistry:
|
|
126
126
|
|
127
127
|
formatter_names = [e.__name__ for e in element_classes]
|
128
128
|
if "TextInlineFormatter" not in formatter_names:
|
129
|
-
element_classes = element_classes +
|
129
|
+
element_classes = element_classes + [TextInlineFormatter]
|
130
130
|
|
131
131
|
return MarkdownSyntaxPromptGenerator.generate_system_prompt(element_classes)
|
132
132
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
2
2
|
from typing import List, Type
|
3
3
|
from collections import OrderedDict
|
4
4
|
|
5
|
+
from notionary.elements.column_element import ColumnElement
|
5
6
|
from notionary.elements.notion_block_element import NotionBlockElement
|
6
7
|
|
7
8
|
from notionary.elements.audio_element import AudioElement
|
@@ -27,7 +28,6 @@ from notionary.elements.toggleable_heading_element import ToggleableHeadingEleme
|
|
27
28
|
from notionary.elements.video_element import VideoElement
|
28
29
|
from notionary.elements.toggle_element import ToggleElement
|
29
30
|
from notionary.elements.bookmark_element import BookmarkElement
|
30
|
-
from notionary.elements.column_element import ColumnsElement
|
31
31
|
|
32
32
|
|
33
33
|
class BlockRegistryBuilder:
|
@@ -64,6 +64,7 @@ class BlockRegistryBuilder:
|
|
64
64
|
.with_videos()
|
65
65
|
.with_embeds()
|
66
66
|
.with_audio()
|
67
|
+
.with_columns()
|
67
68
|
.with_mention()
|
68
69
|
.with_paragraphs()
|
69
70
|
.with_toggleable_heading_element()
|
@@ -267,7 +268,7 @@ class BlockRegistryBuilder:
|
|
267
268
|
"""
|
268
269
|
Add support for column elements.
|
269
270
|
"""
|
270
|
-
return self.add_element(
|
271
|
+
return self.add_element(ColumnElement)
|
271
272
|
|
272
273
|
def build(self) -> BlockRegistry:
|
273
274
|
"""
|
@@ -1,5 +1,4 @@
|
|
1
1
|
from typing import Any, Dict
|
2
|
-
from textwrap import dedent
|
3
2
|
|
4
3
|
from notionary.elements.divider_element import DividerElement
|
5
4
|
from notionary.elements.registry.block_registry import BlockRegistry
|
@@ -39,16 +38,6 @@ class PageContentWriter(LoggingMixin):
|
|
39
38
|
"""
|
40
39
|
Append markdown text to a Notion page, automatically handling content length limits.
|
41
40
|
"""
|
42
|
-
# Check for leading whitespace in the first three lines and log a warning if found
|
43
|
-
first_three_lines = markdown_text.split('\n')[:3]
|
44
|
-
if any(line.startswith(' ') or line.startswith('\t') for line in first_three_lines):
|
45
|
-
self.logger.warning(
|
46
|
-
"Leading whitespace detected in input markdown. Consider using textwrap.dedent or similar logic: "
|
47
|
-
"this code is indented the wrong way, which could lead to formatting issues."
|
48
|
-
)
|
49
|
-
|
50
|
-
markdown_text = "\n".join(line.lstrip() for line in markdown_text.split("\n"))
|
51
|
-
|
52
41
|
if append_divider and not self.block_registry.contains(DividerElement):
|
53
42
|
self.logger.warning(
|
54
43
|
"DividerElement not registered. Appending divider skipped."
|
@@ -57,7 +46,9 @@ class PageContentWriter(LoggingMixin):
|
|
57
46
|
|
58
47
|
# Append divider in markdown format as it will be converted to a Notion divider block
|
59
48
|
if append_divider:
|
60
|
-
markdown_text = markdown_text + "\n
|
49
|
+
markdown_text = markdown_text + "\n---"
|
50
|
+
|
51
|
+
markdown_text = self._process_markdown_whitespace(markdown_text)
|
61
52
|
|
62
53
|
try:
|
63
54
|
blocks = self._markdown_to_notion_converter.convert(markdown_text)
|
@@ -111,3 +102,102 @@ class PageContentWriter(LoggingMixin):
|
|
111
102
|
except Exception as e:
|
112
103
|
self.logger.error("Failed to delete block: %s", str(e))
|
113
104
|
return False
|
105
|
+
|
106
|
+
def _process_markdown_whitespace(self, markdown_text: str) -> str:
|
107
|
+
"""
|
108
|
+
Process markdown text to preserve code structure while removing unnecessary indentation.
|
109
|
+
Strips all leading whitespace from regular lines, but preserves relative indentation
|
110
|
+
within code blocks.
|
111
|
+
|
112
|
+
Args:
|
113
|
+
markdown_text: Original markdown text with potential leading whitespace
|
114
|
+
|
115
|
+
Returns:
|
116
|
+
Processed markdown text with corrected whitespace
|
117
|
+
"""
|
118
|
+
lines = markdown_text.split("\n")
|
119
|
+
if not lines:
|
120
|
+
return ""
|
121
|
+
|
122
|
+
processed_lines = []
|
123
|
+
in_code_block = False
|
124
|
+
current_code_block = []
|
125
|
+
|
126
|
+
for line in lines:
|
127
|
+
# Handle code block markers
|
128
|
+
if self._is_code_block_marker(line):
|
129
|
+
if not in_code_block:
|
130
|
+
# Starting a new code block
|
131
|
+
in_code_block = True
|
132
|
+
processed_lines.append(self._process_code_block_start(line))
|
133
|
+
current_code_block = []
|
134
|
+
continue
|
135
|
+
|
136
|
+
# Ending a code block
|
137
|
+
processed_lines.extend(
|
138
|
+
self._process_code_block_content(current_code_block)
|
139
|
+
)
|
140
|
+
processed_lines.append("```")
|
141
|
+
in_code_block = False
|
142
|
+
continue
|
143
|
+
|
144
|
+
# Handle code block content
|
145
|
+
if in_code_block:
|
146
|
+
current_code_block.append(line)
|
147
|
+
continue
|
148
|
+
|
149
|
+
# Handle regular text
|
150
|
+
processed_lines.append(line.lstrip())
|
151
|
+
|
152
|
+
# Handle unclosed code block
|
153
|
+
if in_code_block and current_code_block:
|
154
|
+
processed_lines.extend(self._process_code_block_content(current_code_block))
|
155
|
+
processed_lines.append("```")
|
156
|
+
|
157
|
+
return "\n".join(processed_lines)
|
158
|
+
|
159
|
+
def _is_code_block_marker(self, line: str) -> bool:
|
160
|
+
"""Check if a line is a code block marker."""
|
161
|
+
return line.lstrip().startswith("```")
|
162
|
+
|
163
|
+
def _process_code_block_start(self, line: str) -> str:
|
164
|
+
"""Extract and normalize the code block opening marker."""
|
165
|
+
language = line.lstrip().replace("```", "", 1).strip()
|
166
|
+
return "```" + language
|
167
|
+
|
168
|
+
def _process_code_block_content(self, code_lines: list) -> list:
|
169
|
+
"""
|
170
|
+
Normalize code block indentation by removing the minimum common indentation.
|
171
|
+
|
172
|
+
Args:
|
173
|
+
code_lines: List of code block content lines
|
174
|
+
|
175
|
+
Returns:
|
176
|
+
List of processed code lines with normalized indentation
|
177
|
+
"""
|
178
|
+
if not code_lines:
|
179
|
+
return []
|
180
|
+
|
181
|
+
# Find non-empty lines to determine minimum indentation
|
182
|
+
non_empty_code_lines = [line for line in code_lines if line.strip()]
|
183
|
+
if not non_empty_code_lines:
|
184
|
+
return [""] * len(code_lines) # All empty lines stay empty
|
185
|
+
|
186
|
+
# Calculate minimum indentation
|
187
|
+
min_indent = min(
|
188
|
+
len(line) - len(line.lstrip()) for line in non_empty_code_lines
|
189
|
+
)
|
190
|
+
if min_indent == 0:
|
191
|
+
return code_lines # No common indentation to remove
|
192
|
+
|
193
|
+
# Process each line
|
194
|
+
processed_code_lines = []
|
195
|
+
for line in code_lines:
|
196
|
+
if not line.strip():
|
197
|
+
processed_code_lines.append("") # Keep empty lines empty
|
198
|
+
continue
|
199
|
+
|
200
|
+
# Remove exactly the minimum indentation
|
201
|
+
processed_code_lines.append(line[min_indent:])
|
202
|
+
|
203
|
+
return processed_code_lines
|
@@ -1,6 +1,7 @@
|
|
1
1
|
from typing import Dict, Any, List, Optional, Tuple
|
2
2
|
import re
|
3
3
|
|
4
|
+
from notionary.elements.column_element import ColumnElement
|
4
5
|
from notionary.elements.registry.block_registry import BlockRegistry
|
5
6
|
from notionary.elements.registry.block_registry_builder import (
|
6
7
|
BlockRegistryBuilder,
|
@@ -22,6 +23,9 @@ class MarkdownToNotionConverter:
|
|
22
23
|
block_registry or BlockRegistryBuilder().create_full_registry()
|
23
24
|
)
|
24
25
|
|
26
|
+
if self._block_registry.contains(ColumnElement):
|
27
|
+
ColumnElement.set_converter_callback(self.convert)
|
28
|
+
|
25
29
|
def convert(self, markdown_text: str) -> List[Dict[str, Any]]:
|
26
30
|
"""Convert markdown text to Notion API block format."""
|
27
31
|
if not markdown_text:
|
@@ -29,6 +33,7 @@ class MarkdownToNotionConverter:
|
|
29
33
|
|
30
34
|
# Preprocess markdown to add spacers before headings and dividers
|
31
35
|
processed_markdown = self._add_spacers_before_elements(markdown_text)
|
36
|
+
print("Processed Markdown:", processed_markdown)
|
32
37
|
|
33
38
|
# Collect all blocks with their positions in the text
|
34
39
|
all_blocks_with_positions = self._collect_all_blocks_with_positions(
|
@@ -45,37 +50,133 @@ class MarkdownToNotionConverter:
|
|
45
50
|
return self._process_block_spacing(blocks)
|
46
51
|
|
47
52
|
def _add_spacers_before_elements(self, markdown_text: str) -> str:
|
48
|
-
"""Add spacer markers before every heading (except the first one) and before every divider
|
49
|
-
|
53
|
+
"""Add spacer markers before every heading (except the first one) and before every divider,
|
54
|
+
but ignore content inside code blocks and consecutive headings."""
|
55
|
+
lines = markdown_text.split("\n")
|
50
56
|
processed_lines = []
|
51
57
|
found_first_heading = False
|
52
|
-
|
58
|
+
in_code_block = False
|
59
|
+
last_line_was_spacer = False
|
60
|
+
last_non_empty_was_heading = False
|
61
|
+
|
53
62
|
i = 0
|
54
63
|
while i < len(lines):
|
55
64
|
line = lines[i]
|
56
|
-
|
57
|
-
# Check
|
58
|
-
if
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
+
|
66
|
+
# Check for code block boundaries and handle accordingly
|
67
|
+
if self._is_code_block_marker(line):
|
68
|
+
in_code_block = not in_code_block
|
69
|
+
processed_lines.append(line)
|
70
|
+
if line.strip(): # If not empty
|
71
|
+
last_non_empty_was_heading = False
|
72
|
+
last_line_was_spacer = False
|
73
|
+
i += 1
|
74
|
+
continue
|
75
|
+
|
76
|
+
# Skip processing markdown inside code blocks
|
77
|
+
if in_code_block:
|
65
78
|
processed_lines.append(line)
|
66
|
-
|
67
|
-
|
68
|
-
|
79
|
+
if line.strip(): # If not empty
|
80
|
+
last_non_empty_was_heading = False
|
81
|
+
last_line_was_spacer = False
|
82
|
+
i += 1
|
83
|
+
continue
|
84
|
+
|
85
|
+
# Process line with context about consecutive headings
|
86
|
+
result = self._process_line_for_spacers(
|
87
|
+
line,
|
88
|
+
processed_lines,
|
89
|
+
found_first_heading,
|
90
|
+
last_line_was_spacer,
|
91
|
+
last_non_empty_was_heading,
|
92
|
+
)
|
93
|
+
|
94
|
+
last_line_was_spacer = result["added_spacer"]
|
95
|
+
|
96
|
+
# Update tracking of consecutive headings and first heading
|
97
|
+
if line.strip(): # Not empty line
|
98
|
+
is_heading = re.match(self.HEADING_PATTERN, line) is not None
|
99
|
+
if is_heading:
|
100
|
+
if not found_first_heading:
|
101
|
+
found_first_heading = True
|
102
|
+
last_non_empty_was_heading = True
|
103
|
+
elif line.strip() != self.SPACER_MARKER: # Not a spacer or heading
|
104
|
+
last_non_empty_was_heading = False
|
105
|
+
|
106
|
+
i += 1
|
107
|
+
|
108
|
+
return "\n".join(processed_lines)
|
109
|
+
|
110
|
+
def _is_code_block_marker(self, line: str) -> bool:
|
111
|
+
"""Check if a line is a code block marker (start or end)."""
|
112
|
+
return line.strip().startswith("```")
|
113
|
+
|
114
|
+
def _process_line_for_spacers(
|
115
|
+
self,
|
116
|
+
line: str,
|
117
|
+
processed_lines: List[str],
|
118
|
+
found_first_heading: bool,
|
119
|
+
last_line_was_spacer: bool,
|
120
|
+
last_non_empty_was_heading: bool,
|
121
|
+
) -> Dict[str, bool]:
|
122
|
+
"""
|
123
|
+
Process a single line to add spacers before headings and dividers if needed.
|
124
|
+
|
125
|
+
Args:
|
126
|
+
line: The line to process
|
127
|
+
processed_lines: List of already processed lines to append to
|
128
|
+
found_first_heading: Whether the first heading has been found
|
129
|
+
last_line_was_spacer: Whether the last added line was a spacer
|
130
|
+
last_non_empty_was_heading: Whether the last non-empty line was a heading
|
131
|
+
|
132
|
+
Returns:
|
133
|
+
Dictionary with processing results
|
134
|
+
"""
|
135
|
+
added_spacer = False
|
136
|
+
line_stripped = line.strip()
|
137
|
+
is_empty = not line_stripped
|
138
|
+
|
139
|
+
# Skip empty lines
|
140
|
+
if is_empty:
|
141
|
+
processed_lines.append(line)
|
142
|
+
return {"added_spacer": False}
|
143
|
+
|
144
|
+
# Check if line is a heading
|
145
|
+
if re.match(self.HEADING_PATTERN, line):
|
146
|
+
if (
|
147
|
+
found_first_heading
|
148
|
+
and not last_line_was_spacer
|
149
|
+
and not last_non_empty_was_heading
|
150
|
+
):
|
151
|
+
# Add spacer only if:
|
152
|
+
# 1. Not the first heading
|
153
|
+
# 2. Last non-empty line was not a heading
|
154
|
+
# 3. Last line was not already a spacer
|
155
|
+
processed_lines.append(self.SPACER_MARKER)
|
156
|
+
added_spacer = True
|
157
|
+
|
158
|
+
processed_lines.append(line)
|
159
|
+
|
160
|
+
# Check if line is a divider
|
161
|
+
elif re.match(self.DIVIDER_PATTERN, line):
|
162
|
+
if not last_line_was_spacer:
|
69
163
|
# Only add a single spacer line before dividers (no extra line breaks)
|
70
164
|
processed_lines.append(self.SPACER_MARKER)
|
165
|
+
added_spacer = True
|
166
|
+
|
167
|
+
processed_lines.append(line)
|
168
|
+
|
169
|
+
# Check if this line itself is a spacer
|
170
|
+
elif line_stripped == self.SPACER_MARKER:
|
171
|
+
# Never add consecutive spacers
|
172
|
+
if not last_line_was_spacer:
|
71
173
|
processed_lines.append(line)
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
return '\n'.join(processed_lines)
|
174
|
+
added_spacer = True
|
175
|
+
|
176
|
+
else:
|
177
|
+
processed_lines.append(line)
|
178
|
+
|
179
|
+
return {"added_spacer": added_spacer}
|
79
180
|
|
80
181
|
def _collect_all_blocks_with_positions(
|
81
182
|
self, markdown_text: str
|
@@ -464,4 +565,4 @@ class MarkdownToNotionConverter:
|
|
464
565
|
return False
|
465
566
|
|
466
567
|
rich_text = block.get("paragraph", {}).get("rich_text", [])
|
467
|
-
return not rich_text or len(rich_text) == 0
|
568
|
+
return not rich_text or len(rich_text) == 0
|
@@ -11,8 +11,7 @@ class MarkdownSyntaxPromptGenerator:
|
|
11
11
|
and formats them optimally for LLMs.
|
12
12
|
"""
|
13
13
|
|
14
|
-
SYSTEM_PROMPT_TEMPLATE =
|
15
|
-
"""
|
14
|
+
SYSTEM_PROMPT_TEMPLATE = """
|
16
15
|
You create content for Notion pages using Markdown syntax with special Notion extensions.
|
17
16
|
|
18
17
|
# Understanding Notion Blocks
|
@@ -56,7 +55,6 @@ class MarkdownSyntaxPromptGenerator:
|
|
56
55
|
- Format as: ## 🚀 Heading Text (with space after emoji)
|
57
56
|
- Only omit emojis if the user explicitly instructs you not to use them
|
58
57
|
"""
|
59
|
-
)
|
60
58
|
|
61
59
|
@staticmethod
|
62
60
|
def generate_element_doc(element_class: Type[NotionBlockElement]) -> str:
|
@@ -114,4 +112,4 @@ class MarkdownSyntaxPromptGenerator:
|
|
114
112
|
Generates a complete system prompt for LLMs.
|
115
113
|
"""
|
116
114
|
element_docs = cls.generate_element_docs(element_classes)
|
117
|
-
return cls.SYSTEM_PROMPT_TEMPLATE.format(element_docs=element_docs)
|
115
|
+
return cls.SYSTEM_PROMPT_TEMPLATE.format(element_docs=element_docs)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: notionary
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.8
|
4
4
|
Summary: A toolkit to convert between Markdown and Notion blocks
|
5
5
|
Home-page: https://github.com/mathisarends/notionary
|
6
6
|
Author: Mathis Arends
|
@@ -153,6 +153,31 @@ Notionary extends standard Markdown with special syntax to support Notion-specif
|
|
153
153
|
| 3. Add content with replace_content() or append_markdown()
|
154
154
|
```
|
155
155
|
|
156
|
+
### Multi-Column Layout
|
157
|
+
|
158
|
+
```markdown
|
159
|
+
::: columns
|
160
|
+
::: column
|
161
|
+
|
162
|
+
## Left Column
|
163
|
+
|
164
|
+
- Item 1
|
165
|
+
- Item 2
|
166
|
+
- Item 3
|
167
|
+
:::
|
168
|
+
::: column
|
169
|
+
|
170
|
+
## Right Column
|
171
|
+
|
172
|
+
This text appears in the second column. Multi-column layouts are perfect for:
|
173
|
+
|
174
|
+
- Comparing features
|
175
|
+
- Creating side-by-side content
|
176
|
+
- Improving readability of wide content
|
177
|
+
:::
|
178
|
+
:::
|
179
|
+
```
|
180
|
+
|
156
181
|
### Code Blocks
|
157
182
|
|
158
183
|
```python
|
@@ -196,6 +221,7 @@ custom_registry = (
|
|
196
221
|
.with_headings()
|
197
222
|
.with_callouts()
|
198
223
|
.with_toggles()
|
224
|
+
.with_columns() # Include multi-column support
|
199
225
|
.with_code()
|
200
226
|
.with_todos()
|
201
227
|
.with_paragraphs()
|
@@ -205,7 +231,7 @@ custom_registry = (
|
|
205
231
|
# Apply this registry to a page
|
206
232
|
page = NotionPage.from_url("https://www.notion.so/your-page-url")
|
207
233
|
page.block_registry = custom_registry
|
208
|
-
|
234
|
+
|
209
235
|
# Replace content using only supported elements
|
210
236
|
await page.replace_content("# Custom heading with selected elements only")
|
211
237
|
```
|
@@ -9,10 +9,10 @@ notionary/elements/bookmark_element.py,sha256=msCtZvuPkIj1kiShNwE8i1GDYwamFb5mwR
|
|
9
9
|
notionary/elements/bulleted_list_element.py,sha256=obsb3JqUNET3uS5OZM3yzDqxSzJzUuEob-Fzx0UIg9Y,2664
|
10
10
|
notionary/elements/callout_element.py,sha256=ZsRvRtVy9kxdTwgrB5JGjZ4qcCiwcC0WimWJ_cW0aLY,4492
|
11
11
|
notionary/elements/code_block_element.py,sha256=YHOiV2eQIe7gbsOnrsQnztomTZ-eP3HRHsonzrj3Tt8,7529
|
12
|
-
notionary/elements/column_element.py,sha256=
|
13
|
-
notionary/elements/divider_element.py,sha256=
|
12
|
+
notionary/elements/column_element.py,sha256=pUgu0e2vptvti2Mms_jcivSX0VmeBY9B1asCrVnXrxc,12476
|
13
|
+
notionary/elements/divider_element.py,sha256=h6lzk4HITuBwHzxQU967QCWL4F8j2GfEOloWnZ8xs8E,2332
|
14
14
|
notionary/elements/embed_element.py,sha256=Zcc18Kl8SGoG98P2aYE0TkBviRvSz-sYOdjMEs-tvgk,4579
|
15
|
-
notionary/elements/heading_element.py,sha256=
|
15
|
+
notionary/elements/heading_element.py,sha256=4X8LLhUb2oyfErQ5p-r0bq3wSR6TGSTSQjoVAhVnTO4,3166
|
16
16
|
notionary/elements/image_element.py,sha256=cwdovaWK8e4uZJU97l_fJ2etAxAgM2rG2EE34t4eag8,4758
|
17
17
|
notionary/elements/mention_element.py,sha256=L4t6eAY3RcbOqIiwVT_CAqwatDtP4tBs9FaqRhaCbpQ,8227
|
18
18
|
notionary/elements/notion_block_element.py,sha256=BVrZH09vyojuacs3KGReVx3W0Ee6di_5o9E8N5sex28,1258
|
@@ -25,20 +25,20 @@ notionary/elements/todo_element.py,sha256=ND3oOzSnd0l1AUGTcG2NiHW50ZbI4-atjtNorL
|
|
25
25
|
notionary/elements/toggle_element.py,sha256=h9vYkkAIUHzn-0mu31qC6UPdlk_0EFIsU5A4T_A2ZI8,11082
|
26
26
|
notionary/elements/toggleable_heading_element.py,sha256=XdaPsd8anufwAACL8J-Egd_RcqPqZ1gFlzeol1GOyyc,9960
|
27
27
|
notionary/elements/video_element.py,sha256=y0OmOYXdQBc2rSYAHRmA4l4rzNqPnyhuXbEipcgzQgY,5727
|
28
|
-
notionary/elements/registry/block_registry.py,sha256=
|
29
|
-
notionary/elements/registry/block_registry_builder.py,sha256=
|
28
|
+
notionary/elements/registry/block_registry.py,sha256=g0id_Q6guzTyNY6HfnB9AjOBvCR4CvtpnUeFAY8kgY0,5027
|
29
|
+
notionary/elements/registry/block_registry_builder.py,sha256=5zRKnw2102rAeHpANs6Csu4DVufOazf1peEovChWcgs,9572
|
30
30
|
notionary/exceptions/database_exceptions.py,sha256=I-Tx6bYRLpi5pjGPtbT-Mqxvz3BFgYTiuZxknJeLxtI,2638
|
31
31
|
notionary/exceptions/page_creation_exception.py,sha256=4v7IuZD6GsQLrqhDLriGjuG3ML638gAO53zDCrLePuU,281
|
32
32
|
notionary/models/notion_block_response.py,sha256=gzL4C6K9QPcaMS6NbAZaRceSEnMbNwYBVVzxysza5VU,6002
|
33
33
|
notionary/models/notion_database_response.py,sha256=FMAasQP20S12J_KMdMlNpcHHwxFKX2YtbE4Q9xn-ruQ,1213
|
34
34
|
notionary/models/notion_page_response.py,sha256=r4fwMwwDocj92JdbSmyrzIqBKsnEaz4aDUiPabrg9BM,1762
|
35
|
-
notionary/page/markdown_to_notion_converter.py,sha256
|
35
|
+
notionary/page/markdown_to_notion_converter.py,sha256=-IC2yJimrmJ7e5EorV-48V6V12n1VBmsOHKejZxAKCo,20909
|
36
36
|
notionary/page/notion_page.py,sha256=NDxAJaNk4tlKUrenhKBdnuvjlVgnxC0Z6fprf2LyNeE,18046
|
37
37
|
notionary/page/notion_page_factory.py,sha256=2A3M5Ub_kV2-q7PPRqDgfwBjhkGCwtL5i3Kr2RfvvVo,7213
|
38
38
|
notionary/page/notion_to_markdown_converter.py,sha256=vUQss0J7LUFLULGvW27PjaTFuWi8OsRQAUBowSYorkM,6408
|
39
39
|
notionary/page/content/notion_page_content_chunker.py,sha256=xRks74Dqec-De6-AVTxMPnXs-MSJBzSm1HfJfaHiKr8,3330
|
40
40
|
notionary/page/content/page_content_retriever.py,sha256=f8IU1CIfSTTT07m72-vgpUr_VOCsisqqFHQ1JeOhb3g,2222
|
41
|
-
notionary/page/content/page_content_writer.py,sha256=
|
41
|
+
notionary/page/content/page_content_writer.py,sha256=LOn70vFLOzPoCP2vqH922eNEh96B3cNiBuI3eDy8yLA,7439
|
42
42
|
notionary/page/metadata/metadata_editor.py,sha256=HI7m8Zn_Lz6x36rBnW1EnbicVS-4Q8NmCJYKN-OlY-c,5130
|
43
43
|
notionary/page/metadata/notion_icon_manager.py,sha256=6a9GS5sT0trfuAb0hlF2Cw_Wc1oM59a1QA4kO9asvMA,2576
|
44
44
|
notionary/page/metadata/notion_page_cover_manager.py,sha256=gHQSA8EtO4gbkMt_C3nKc0DF44SY_4ycd57cJSihdqk,2215
|
@@ -50,12 +50,12 @@ notionary/page/relations/notion_page_relation_manager.py,sha256=tfkvLHClaYel_uEa
|
|
50
50
|
notionary/page/relations/notion_page_title_resolver.py,sha256=dIjiEeHjjNT-DrIhz1nynkfHkMpUuJJFOEjb25Wy7f4,3575
|
51
51
|
notionary/page/relations/page_database_relation.py,sha256=8lEp8fQjPwjWhA8nZu3k8mW6EEc54ki1Uwf4iUV1DOU,2245
|
52
52
|
notionary/prompting/element_prompt_content.py,sha256=tHref-SKA81Ua_IQD2Km7y7BvFtHl74haSIjHNYE3FE,4403
|
53
|
-
notionary/prompting/markdown_syntax_prompt_generator.py,sha256=
|
53
|
+
notionary/prompting/markdown_syntax_prompt_generator.py,sha256=_1qIYlqSfI6q6Fut10t6gGwTQuS8c3QBcC_5DBme9Mo,5084
|
54
54
|
notionary/util/logging_mixin.py,sha256=b6wHj0IoVSWXbHh0yynfJlwvIR33G2qmaGNzrqyb7Gs,1825
|
55
55
|
notionary/util/page_id_utils.py,sha256=EYNMxgf-7ghzL5K8lKZBZfW7g5CsdY0Xuj4IYmU8RPk,1381
|
56
56
|
notionary/util/warn_direct_constructor_usage.py,sha256=vyJR73F95XVSRWIbyij-82IGOpAne9SBPM25eDpZfSU,1715
|
57
|
-
notionary-0.2.
|
58
|
-
notionary-0.2.
|
59
|
-
notionary-0.2.
|
60
|
-
notionary-0.2.
|
61
|
-
notionary-0.2.
|
57
|
+
notionary-0.2.8.dist-info/licenses/LICENSE,sha256=zOm3cRT1qD49eg7vgw95MI79rpUAZa1kRBFwL2FkAr8,1120
|
58
|
+
notionary-0.2.8.dist-info/METADATA,sha256=RtbtcBYg8U9oqX64Tl2VD79bf199uwLZEqVD9QQ2jio,7521
|
59
|
+
notionary-0.2.8.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
|
60
|
+
notionary-0.2.8.dist-info/top_level.txt,sha256=fhONa6BMHQXqthx5PanWGbPL0b8rdFqhrJKVLf_adSs,10
|
61
|
+
notionary-0.2.8.dist-info/RECORD,,
|
File without changes
|
File without changes
|