notionary 0.2.21__py3-none-any.whl → 0.2.23__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 (100) hide show
  1. notionary/blocks/_bootstrap.py +9 -1
  2. notionary/blocks/audio/audio_element.py +53 -28
  3. notionary/blocks/audio/audio_markdown_node.py +10 -4
  4. notionary/blocks/base_block_element.py +15 -3
  5. notionary/blocks/bookmark/bookmark_element.py +39 -36
  6. notionary/blocks/bookmark/bookmark_markdown_node.py +16 -17
  7. notionary/blocks/breadcrumbs/breadcrumb_element.py +2 -2
  8. notionary/blocks/bulleted_list/bulleted_list_element.py +21 -4
  9. notionary/blocks/callout/callout_element.py +20 -4
  10. notionary/blocks/child_database/__init__.py +11 -4
  11. notionary/blocks/child_database/child_database_element.py +59 -0
  12. notionary/blocks/child_database/child_database_models.py +7 -14
  13. notionary/blocks/child_page/child_page_element.py +94 -0
  14. notionary/blocks/client.py +0 -1
  15. notionary/blocks/code/code_element.py +51 -2
  16. notionary/blocks/code/code_markdown_node.py +52 -1
  17. notionary/blocks/column/column_element.py +9 -3
  18. notionary/blocks/column/column_list_element.py +18 -3
  19. notionary/blocks/divider/divider_element.py +3 -11
  20. notionary/blocks/embed/embed_element.py +27 -6
  21. notionary/blocks/equation/equation_element.py +94 -41
  22. notionary/blocks/equation/equation_element_markdown_node.py +8 -9
  23. notionary/blocks/file/file_element.py +56 -37
  24. notionary/blocks/file/file_element_markdown_node.py +9 -7
  25. notionary/blocks/guards.py +22 -0
  26. notionary/blocks/heading/heading_element.py +23 -4
  27. notionary/blocks/image_block/image_element.py +43 -38
  28. notionary/blocks/image_block/image_markdown_node.py +10 -5
  29. notionary/blocks/mixins/captions/__init__.py +4 -0
  30. notionary/blocks/mixins/captions/caption_markdown_node_mixin.py +31 -0
  31. notionary/blocks/mixins/captions/caption_mixin.py +92 -0
  32. notionary/blocks/models.py +3 -1
  33. notionary/blocks/numbered_list/numbered_list_element.py +21 -4
  34. notionary/blocks/paragraph/paragraph_element.py +21 -5
  35. notionary/blocks/pdf/pdf_element.py +47 -41
  36. notionary/blocks/pdf/pdf_markdown_node.py +9 -7
  37. notionary/blocks/quote/quote_element.py +26 -9
  38. notionary/blocks/quote/quote_markdown_node.py +2 -2
  39. notionary/blocks/registry/block_registry.py +1 -46
  40. notionary/blocks/registry/block_registry_builder.py +8 -0
  41. notionary/blocks/rich_text/rich_text_models.py +62 -29
  42. notionary/blocks/rich_text/text_inline_formatter.py +432 -101
  43. notionary/blocks/syntax_prompt_builder.py +137 -0
  44. notionary/blocks/table/table_element.py +110 -9
  45. notionary/blocks/table_of_contents/table_of_contents_element.py +19 -2
  46. notionary/blocks/todo/todo_element.py +21 -4
  47. notionary/blocks/toggle/toggle_element.py +19 -3
  48. notionary/blocks/toggle/toggle_markdown_node.py +1 -1
  49. notionary/blocks/toggleable_heading/toggleable_heading_element.py +19 -4
  50. notionary/blocks/types.py +69 -0
  51. notionary/blocks/video/video_element.py +44 -39
  52. notionary/blocks/video/video_markdown_node.py +10 -5
  53. notionary/comments/__init__.py +26 -0
  54. notionary/comments/client.py +211 -0
  55. notionary/comments/models.py +129 -0
  56. notionary/database/client.py +23 -0
  57. notionary/file_upload/models.py +2 -2
  58. notionary/markdown/markdown_builder.py +34 -27
  59. notionary/page/client.py +21 -6
  60. notionary/page/notion_page.py +77 -2
  61. notionary/page/page_content_deleting_service.py +117 -0
  62. notionary/page/page_content_writer.py +89 -113
  63. notionary/page/page_context.py +64 -0
  64. notionary/page/reader/handler/__init__.py +2 -0
  65. notionary/page/reader/handler/base_block_renderer.py +4 -4
  66. notionary/page/reader/handler/block_rendering_context.py +5 -0
  67. notionary/page/reader/handler/line_renderer.py +16 -3
  68. notionary/page/reader/handler/numbered_list_renderer.py +85 -0
  69. notionary/page/reader/page_content_retriever.py +17 -5
  70. notionary/page/writer/handler/__init__.py +2 -0
  71. notionary/page/writer/handler/code_handler.py +12 -40
  72. notionary/page/writer/handler/column_handler.py +12 -12
  73. notionary/page/writer/handler/column_list_handler.py +13 -13
  74. notionary/page/writer/handler/equation_handler.py +74 -0
  75. notionary/page/writer/handler/line_handler.py +4 -4
  76. notionary/page/writer/handler/regular_line_handler.py +31 -37
  77. notionary/page/writer/handler/table_handler.py +8 -72
  78. notionary/page/writer/handler/toggle_handler.py +14 -12
  79. notionary/page/writer/handler/toggleable_heading_handler.py +22 -16
  80. notionary/page/writer/markdown_to_notion_converter.py +28 -9
  81. notionary/page/writer/markdown_to_notion_converter_context.py +30 -0
  82. notionary/page/writer/markdown_to_notion_formatting_post_processor.py +73 -0
  83. notionary/page/writer/markdown_to_notion_post_processor.py +0 -0
  84. notionary/page/writer/markdown_to_notion_text_length_post_processor.py +0 -0
  85. notionary/page/writer/notion_text_length_processor.py +150 -0
  86. notionary/shared/__init__.py +5 -0
  87. notionary/shared/name_to_id_resolver.py +203 -0
  88. notionary/telemetry/service.py +0 -1
  89. notionary/user/notion_user_manager.py +22 -95
  90. notionary/util/concurrency_limiter.py +0 -0
  91. notionary/workspace.py +4 -4
  92. notionary-0.2.23.dist-info/METADATA +235 -0
  93. {notionary-0.2.21.dist-info → notionary-0.2.23.dist-info}/RECORD +96 -77
  94. notionary/page/markdown_whitespace_processor.py +0 -80
  95. notionary/page/notion_text_length_utils.py +0 -119
  96. notionary/user/notion_user_provider.py +0 -1
  97. notionary-0.2.21.dist-info/METADATA +0 -229
  98. /notionary/page/reader/handler/{context.py → equation_renderer.py} +0 -0
  99. {notionary-0.2.21.dist-info → notionary-0.2.23.dist-info}/LICENSE +0 -0
  100. {notionary-0.2.21.dist-info → notionary-0.2.23.dist-info}/WHEEL +0 -0
@@ -0,0 +1,203 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+ from notionary.user.notion_user_manager import NotionUserManager
6
+ from notionary.util import format_uuid
7
+ from notionary.util.fuzzy import find_best_match
8
+
9
+
10
+ class NameIdResolver:
11
+ """
12
+ Bidirectional resolver for Notion page and database names and IDs.
13
+ """
14
+
15
+ def __init__(
16
+ self,
17
+ *,
18
+ token: Optional[str] = None,
19
+ search_limit: int = 10,
20
+ ):
21
+ """
22
+ Initialize the resolver with a Notion workspace.
23
+ """
24
+ from notionary import NotionWorkspace
25
+
26
+ self.workspace = NotionWorkspace(token=token)
27
+ self.notion_user_manager = NotionUserManager(token=token)
28
+ self.search_limit = search_limit
29
+
30
+ async def resolve_page_id(self, name: str) -> Optional[str]:
31
+ """
32
+ Convert a page name to its Notion page ID.
33
+ Specifically searches only pages, not databases.
34
+ """
35
+ if not name:
36
+ return None
37
+
38
+ cleaned_name = name.strip()
39
+
40
+ # Return if already a valid Notion ID
41
+ formatted_uuid = format_uuid(cleaned_name)
42
+ if formatted_uuid:
43
+ return formatted_uuid
44
+
45
+ # Search for page by name
46
+ return await self._resolve_page_id(cleaned_name)
47
+
48
+ async def resolve_database_id(self, name: str) -> Optional[str]:
49
+ """
50
+ Convert a database name to its Notion database ID.
51
+ Specifically searches only databases, not pages.
52
+ """
53
+ if not name:
54
+ return None
55
+
56
+ cleaned_name = name.strip()
57
+
58
+ formatted_uuid = format_uuid(cleaned_name)
59
+ if formatted_uuid:
60
+ return formatted_uuid
61
+
62
+ return await self._resolve_database_id(cleaned_name)
63
+
64
+ async def resolve_page_name(self, page_id: str) -> Optional[str]:
65
+ """
66
+ Convert a Notion page ID to its human-readable title.
67
+ """
68
+ if not page_id:
69
+ return None
70
+
71
+ formatted_id = format_uuid(page_id)
72
+ if not formatted_id:
73
+ return None
74
+
75
+ try:
76
+ from notionary import NotionPage
77
+
78
+ page = await NotionPage.from_page_id(formatted_id)
79
+ return page.title if page else None
80
+ except Exception:
81
+ return None
82
+
83
+ async def resolve_database_name(self, database_id: str) -> Optional[str]:
84
+ """
85
+ Convert a Notion database ID to its human-readable title.
86
+ """
87
+ if not database_id:
88
+ return None
89
+
90
+ # Validate and format UUID
91
+ formatted_id = format_uuid(database_id)
92
+ if not formatted_id:
93
+ return None
94
+
95
+ try:
96
+ from notionary.database import NotionDatabase
97
+
98
+ database = await NotionDatabase.from_database_id(formatted_id)
99
+ return database.title if database else None
100
+ except Exception:
101
+ return None
102
+
103
+ async def resolve_user_id(self, name: str) -> Optional[str]:
104
+ """
105
+ Convert a user name to its Notion user ID.
106
+ Specifically searches only users.
107
+ """
108
+ if not name:
109
+ return None
110
+
111
+ cleaned_name = name.strip()
112
+
113
+ # Return if already a valid Notion ID
114
+ formatted_uuid = format_uuid(cleaned_name)
115
+ if formatted_uuid:
116
+ return formatted_uuid
117
+
118
+ # Search for user by name
119
+ return await self._resolve_user_id(cleaned_name)
120
+
121
+ async def resolve_user_name(self, user_id: str) -> Optional[str]:
122
+ """
123
+ Convert a Notion user ID to its human-readable name.
124
+
125
+ Args:
126
+ user_id: Notion user ID to resolve
127
+
128
+ Returns:
129
+ User name if found, None if not found or inaccessible
130
+ """
131
+ if not user_id:
132
+ return None
133
+
134
+ # Validate and format UUID
135
+ formatted_id = format_uuid(user_id)
136
+ if not formatted_id:
137
+ return None
138
+
139
+ try:
140
+ user = await self.notion_user_manager.get_user_by_id(formatted_id)
141
+ return user.name if user else None
142
+ except Exception:
143
+ return None
144
+
145
+ async def _resolve_user_id(self, name: str) -> Optional[str]:
146
+ """Search for users matching the name."""
147
+ try:
148
+ users = await self.notion_user_manager.find_users_by_name(name)
149
+
150
+ if not users:
151
+ return None
152
+
153
+ # Use fuzzy matching to find best match
154
+ best_match = find_best_match(
155
+ query=name,
156
+ items=users,
157
+ text_extractor=lambda user: user.name or "",
158
+ )
159
+
160
+ return best_match.item.id if best_match else None
161
+ except Exception:
162
+ return None
163
+
164
+ async def _resolve_page_id(self, name: str) -> Optional[str]:
165
+ """Search for pages matching the name."""
166
+ search_results = await self.workspace.search_pages(
167
+ query=name, limit=self.search_limit
168
+ )
169
+
170
+ return self._find_best_fuzzy_match(query=name, candidate_objects=search_results)
171
+
172
+ async def _resolve_database_id(self, name: str) -> Optional[str]:
173
+ """Search for databases matching the name."""
174
+ search_results = await self.workspace.search_databases(
175
+ query=name, limit=self.search_limit
176
+ )
177
+
178
+ return self._find_best_fuzzy_match(query=name, candidate_objects=search_results)
179
+
180
+ def _find_best_fuzzy_match(
181
+ self, query: str, candidate_objects: list
182
+ ) -> Optional[str]:
183
+ """
184
+ Find the best fuzzy match among candidate objects using existing fuzzy matching logic.
185
+
186
+ Args:
187
+ query: The search query to match against
188
+ candidate_objects: Objects (pages or databases) with .id and .title attributes
189
+
190
+ Returns:
191
+ ID of best match, or None if no match meets threshold
192
+ """
193
+ if not candidate_objects:
194
+ return None
195
+
196
+ # Use existing fuzzy matching logic
197
+ best_match = find_best_match(
198
+ query=query,
199
+ items=candidate_objects,
200
+ text_extractor=lambda obj: obj.title,
201
+ )
202
+
203
+ return best_match.item.id if best_match else None
@@ -1,7 +1,6 @@
1
1
  import os
2
2
  import uuid
3
3
  from pathlib import Path
4
- from typing import Any, Dict, Optional
5
4
 
6
5
  from dotenv import load_dotenv
7
6
  from posthog import Posthog
@@ -1,7 +1,6 @@
1
- from typing import Any, Dict, List, Optional
1
+ from typing import Optional
2
2
 
3
3
  from notionary.user.client import NotionUserClient
4
- from notionary.user.models import NotionUsersListResponse
5
4
  from notionary.user.notion_user import NotionUser
6
5
  from notionary.util import LoggingMixin
7
6
 
@@ -18,72 +17,52 @@ class NotionUserManager(LoggingMixin):
18
17
  """Initialize the user manager."""
19
18
  self.client = NotionUserClient(token=token)
20
19
 
21
- async def get_current_bot_user(self) -> Optional[NotionUser]:
22
- """
23
- Get the current bot user from the API token.
24
- """
25
- return await NotionUser.current_bot_user(token=self.client.token)
26
-
27
20
  async def get_user_by_id(self, user_id: str) -> Optional[NotionUser]:
28
21
  """
29
22
  Get a specific user by their ID.
30
23
  """
31
24
  return await NotionUser.from_user_id(user_id, token=self.client.token)
32
25
 
33
- async def list_users(
34
- self, page_size: int = 100, start_cursor: Optional[str] = None
35
- ) -> Optional[NotionUsersListResponse]:
36
- """
37
- List users in the workspace (paginated).
38
-
39
- Note: Guests are not included in the response.
40
- """
41
- try:
42
- response = await self.client.list_users(page_size, start_cursor)
43
- if response is None:
44
- self.logger.error("Failed to list users")
45
- return None
46
-
47
- self.logger.info(
48
- "Retrieved %d users (has_more: %s)",
49
- len(response.results),
50
- response.has_more,
51
- )
52
- return response
53
-
54
- except Exception as e:
55
- self.logger.error("Error listing users: %s", str(e))
56
- return None
57
-
58
- async def get_all_users(self) -> List[NotionUser]:
26
+ async def get_all_users(self) -> list[NotionUser]:
59
27
  """
60
28
  Get all users in the workspace as NotionUser objects.
61
29
  Automatically handles pagination and converts responses to NotionUser instances.
30
+ Only returns person users, excludes bots and integrations.
62
31
  """
63
32
  try:
64
33
  # Get raw user responses
65
34
  user_responses = await self.client.get_all_users()
66
35
 
67
- # Convert to NotionUser objects
36
+ # Filter for person users only and convert to NotionUser objects
68
37
  notion_users = []
69
38
  for user_response in user_responses:
39
+ # Skip bot users and integrations
40
+ if user_response.type != "person":
41
+ self.logger.debug(
42
+ "Skipping non-person user %s (type: %s)",
43
+ user_response.id,
44
+ user_response.type,
45
+ )
46
+ continue
47
+
70
48
  try:
71
49
  # Use the internal creation method to convert response to NotionUser
72
- notion_user = NotionUser.from_notion_user_response(
50
+ notion_user = NotionUser.from_user_response(
73
51
  user_response, self.client.token
74
52
  )
75
53
  notion_users.append(notion_user)
76
54
  except Exception as e:
77
55
  self.logger.warning(
78
- "Failed to convert user %s to NotionUser: %s",
56
+ "Failed to convert person user %s to NotionUser: %s",
79
57
  user_response.id,
80
58
  str(e),
81
59
  )
82
60
  continue
83
61
 
84
62
  self.logger.info(
85
- "Successfully converted %d users to NotionUser objects",
63
+ "Successfully converted %d person users to NotionUser objects (skipped %d non-person users)",
86
64
  len(notion_users),
65
+ len(user_responses) - len(notion_users),
87
66
  )
88
67
  return notion_users
89
68
 
@@ -91,68 +70,16 @@ class NotionUserManager(LoggingMixin):
91
70
  self.logger.error("Error getting all users: %s", str(e))
92
71
  return []
93
72
 
94
- async def get_users_by_type(self, user_type: str = "person") -> List[NotionUser]:
95
- """
96
- Get all users of a specific type (person or bot).
97
- """
98
- try:
99
- all_users = await self.get_all_users()
100
- filtered_users = [user for user in all_users if user.user_type == user_type]
101
-
102
- self.logger.info(
103
- "Found %d users of type '%s' out of %d total users",
104
- len(filtered_users),
105
- user_type,
106
- len(all_users),
107
- )
108
- return filtered_users
109
-
110
- except Exception as e:
111
- self.logger.error("Error filtering users by type: %s", str(e))
112
- return []
113
-
114
- # TODO: Type this
115
- async def get_workspace_info(self) -> Optional[Dict[str, Any]]:
116
- """
117
- Get available workspace information from the bot user.
118
- """
119
- bot_user = await self.get_current_bot_user()
120
- if bot_user is None:
121
- self.logger.error("Failed to get bot user for workspace info")
122
- return None
123
-
124
- workspace_info = {
125
- "workspace_name": bot_user.workspace_name,
126
- "bot_user_id": bot_user.id,
127
- "bot_user_name": bot_user.name,
128
- "bot_user_type": bot_user.user_type,
129
- }
130
-
131
- # Add workspace limits if available
132
- if bot_user.is_bot:
133
- limits = await bot_user.get_workspace_limits()
134
- if limits:
135
- workspace_info["workspace_limits"] = limits
136
-
137
- # Add user count statistics
138
- try:
139
- all_users = await self.get_all_users()
140
- workspace_info["total_users"] = len(all_users)
141
- workspace_info["person_users"] = len([u for u in all_users if u.is_person])
142
- workspace_info["bot_users"] = len([u for u in all_users if u.is_bot])
143
- except Exception as e:
144
- self.logger.warning("Could not get user statistics: %s", str(e))
145
-
146
- return workspace_info
147
-
148
- async def find_users_by_name(self, name_pattern: str) -> List[NotionUser]:
73
+ async def find_users_by_name(self, name_pattern: str) -> list[NotionUser]:
149
74
  """
150
- Find users by name pattern (case-insensitive partial match).
75
+ Find person users by name pattern (case-insensitive partial match).
76
+ Only returns person users, excludes bots and integrations.
151
77
 
152
78
  Note: The API doesn't support server-side filtering, so this fetches all users
153
79
  and filters client-side.
154
80
  """
155
81
  try:
82
+ # get_all_users() already filters for person users only
156
83
  all_users = await self.get_all_users()
157
84
  pattern_lower = name_pattern.lower()
158
85
 
@@ -163,7 +90,7 @@ class NotionUserManager(LoggingMixin):
163
90
  ]
164
91
 
165
92
  self.logger.info(
166
- "Found %d users matching pattern '%s'",
93
+ "Found %d person users matching pattern '%s'",
167
94
  len(matching_users),
168
95
  name_pattern,
169
96
  )
File without changes
notionary/workspace.py CHANGED
@@ -1,5 +1,5 @@
1
1
  import asyncio
2
- from typing import List, Optional
2
+ from typing import Optional
3
3
 
4
4
  from notionary import NotionDatabase, NotionPage
5
5
  from notionary.database.client import NotionDatabaseClient
@@ -24,7 +24,7 @@ class NotionWorkspace(LoggingMixin):
24
24
  self.page_client = NotionPageClient(token=token)
25
25
  self.user_manager = NotionUserManager(token=token)
26
26
 
27
- async def search_pages(self, query: str, limit=100) -> List[NotionPage]:
27
+ async def search_pages(self, query: str, limit=100) -> list[NotionPage]:
28
28
  """
29
29
  Search for pages globally across Notion workspace.
30
30
  """
@@ -35,7 +35,7 @@ class NotionWorkspace(LoggingMixin):
35
35
 
36
36
  async def search_databases(
37
37
  self, query: str, limit: int = 100
38
- ) -> List[NotionDatabase]:
38
+ ) -> list[NotionDatabase]:
39
39
  """
40
40
  Search for databases globally across the Notion workspace.
41
41
  """
@@ -58,7 +58,7 @@ class NotionWorkspace(LoggingMixin):
58
58
 
59
59
  return databases[0] if databases else None
60
60
 
61
- async def list_all_databases(self, limit: int = 100) -> List[NotionDatabase]:
61
+ async def list_all_databases(self, limit: int = 100) -> list[NotionDatabase]:
62
62
  """
63
63
  List all databases in the workspace.
64
64
  Returns a list of NotionDatabase instances.
@@ -0,0 +1,235 @@
1
+ Metadata-Version: 2.3
2
+ Name: notionary
3
+ Version: 0.2.23
4
+ Summary: Python library for programmatic Notion workspace management - databases, pages, and content with advanced Markdown support
5
+ License: MIT
6
+ Author: Mathis Arends
7
+ Author-email: mathisarends27@gmail.com
8
+ Requires-Python: >=3.9
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Requires-Dist: httpx (>=0.28.0)
17
+ Requires-Dist: posthog (>=6.3.1,<7.0.0)
18
+ Requires-Dist: pydantic (>=2.11.4)
19
+ Requires-Dist: python-dotenv (>=1.1.0)
20
+ Project-URL: Homepage, https://github.com/mathisarends/notionary
21
+ Description-Content-Type: text/markdown
22
+
23
+ <picture>
24
+ <source media="(prefers-color-scheme: dark)" srcset="./static/notionary-dark.png">
25
+ <source media="(prefers-color-scheme: light)" srcset="./static/notionary-light.png">
26
+ <img alt="Notionary logo: dark mode shows a white logo, light mode shows a black logo." src="./static/browser-use.png" width="full">
27
+ </picture>
28
+
29
+ <h1 align="center">Notion API simplified for Python developers 🐍</h1>
30
+
31
+ <div align="center">
32
+
33
+ [![Python Version](https://img.shields.io/badge/python-3.8%2B-blue.svg)](https://www.python.org/downloads/)
34
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
35
+ [![Documentation](https://img.shields.io/badge/docs-mathisarends.github.io-blue.svg)](https://mathisarends.github.io/notionary/)
36
+
37
+ Transform complex Notion API interactions into simple, Pythonic code. Build AI agents, automate workflows, and create dynamic content with ease.
38
+
39
+ </div>
40
+
41
+ ---
42
+
43
+ ## Why Notionary?
44
+
45
+ - **Smart Discovery**: Find pages and databases by name—no more hunting for URLs or IDs
46
+ - **Rich Markdown**: Convert extended Markdown (callouts, toggles, columns) directly into beautiful Notion blocks
47
+ - **Async-First**: Built for modern Python with full async/await support and high performance
48
+ - **AI-Ready**: Perfect foundation for AI agents that generate and manage Notion content
49
+ - **Round-Trip**: Read existing content, modify it, and write it back while preserving formatting
50
+
51
+ ---
52
+
53
+ ## Quick Start
54
+
55
+ ```bash
56
+ pip install notionary
57
+ ```
58
+
59
+ Set up your [Notion integration](https://www.notion.so/profile/integrations) and add your token:
60
+
61
+ ```bash
62
+ NOTION_SECRET=your_integration_key
63
+ ```
64
+
65
+ ### Simple Flow: Find → Create → Update
66
+
67
+ ```python
68
+ import asyncio
69
+ from notionary import NotionPage, NotionDatabase
70
+
71
+ async def main():
72
+ # Work with pages - find by name, no exact match needed!
73
+ page = await NotionPage.from_page_name("Meeting Notes")
74
+
75
+ # Direct Markdown - quick & intuitive
76
+ await page.append_markdown("""
77
+ ## Action Items
78
+ - Review project proposal
79
+ - Schedule team meeting
80
+ - Update documentation
81
+
82
+ [callout](Important meeting decisions require follow-up "💡")
83
+ """)
84
+
85
+ # Builder Pattern - type-safe & powerful for complex layouts
86
+ await page.append_markdown(lambda builder: (
87
+ builder
88
+ .h2("Project Status")
89
+ .callout("Project milestone reached!", "🎉")
90
+ .columns(
91
+ lambda col: (col
92
+ .h3("Completed")
93
+ .bulleted_list(["API design", "Database setup", "Authentication"])
94
+ ),
95
+ lambda col: (col
96
+ .h3("In Progress")
97
+ .bulleted_list(["Frontend UI", "Testing", "Documentation"])
98
+ )
99
+ )
100
+ .table(
101
+ headers=["Task", "Owner", "Due Date"],
102
+ rows=[
103
+ ["Launch prep", "Alice", "2024-03-15"],
104
+ ["Marketing", "Bob", "2024-03-20"]
105
+ ]
106
+ )
107
+ ))
108
+
109
+ asyncio.run(main())
110
+ ```
111
+
112
+ ### Create Rich Database Entries
113
+
114
+ ```python
115
+ # Work with databases - connect and create styled entries
116
+ db = await NotionDatabase.from_database_name("Projects")
117
+
118
+ # Create new project with full styling
119
+ project = await db.create_blank_page()
120
+ await project.set_title("New Marketing Campaign")
121
+ await project.set_emoji_icon("🚀")
122
+ await project.set_random_gradient_cover()
123
+
124
+ # Set database properties
125
+ await project.set_property_value_by_name("Status", "Planning")
126
+ await project.set_property_value_by_name("Priority", "High")
127
+ await project.set_property_value_by_name("Team Lead", "sarah@company.com")
128
+
129
+ # Add rich content to the new page
130
+ await project.replace_content(lambda builder: (
131
+ builder
132
+ .h1("Campaign Overview")
133
+ .callout("New marketing initiative targeting Q2 growth", "🎯")
134
+ .h2("Goals & Objectives")
135
+ .numbered_list([
136
+ "Increase brand awareness by 25%",
137
+ "Generate 500 qualified leads",
138
+ "Launch in 3 target markets"
139
+ ])
140
+ .h2("Budget Breakdown")
141
+ .table(
142
+ headers=["Category", "Allocated", "Spent", "Remaining"],
143
+ rows=[
144
+ ["Digital Ads", "$15,000", "$3,200", "$11,800"],
145
+ ["Content Creation", "$8,000", "$1,500", "$6,500"],
146
+ ["Events", "$12,000", "$0", "$12,000"]
147
+ ]
148
+ )
149
+ .divider()
150
+ .toggle("Technical Requirements", lambda toggle: (
151
+ toggle
152
+ .paragraph("Platform specifications and integration details.")
153
+ .bulleted_list([
154
+ "CRM integration with Salesforce",
155
+ "Analytics tracking setup",
156
+ "Landing page development"
157
+ ])
158
+ ))
159
+ ))
160
+
161
+ print(f"✅ Created styled project: {project.url}")
162
+ ```
163
+
164
+ ### Extended Markdown Syntax
165
+
166
+ Notionary supports rich formatting with callouts, toggles, multi-column layouts, tables, media embeds, and more. Use either direct markdown syntax or the type-safe builder pattern.
167
+
168
+ See the complete [Block Types documentation](https://mathisarends.github.io/notionary/blocks/) for all available formatting options and syntax examples.
169
+
170
+ ## What You Can Build
171
+
172
+ - **AI Content Generation** - Perfect for AI agents that create structured reports and documentation
173
+ - **Workflow Automation** - Update project status, sync data between databases, generate reports
174
+ - **Dynamic Documentation** - Auto-generate team docs, API references, and knowledge bases
175
+ - **Content Management** - Bulk page updates, template generation, and content migration
176
+
177
+ ## Core Features
178
+
179
+ | Feature | Description |
180
+ | -------------------- | ------------------------------------------------------ |
181
+ | **Smart Discovery** | Find pages/databases by name with fuzzy matching |
182
+ | **Rich Markdown** | Extended syntax for callouts, toggles, columns, tables |
183
+ | **Async-First** | Modern Python with full async/await support |
184
+ | **Round-Trip** | Read content as markdown, edit, and write back |
185
+ | **AI-Ready** | Generate system prompts for AI content creation |
186
+ | **All Block Types** | Support for every Notion block type |
187
+ | **Type Safety** | Full type hints for better IDE support |
188
+ | **High Performance** | Efficient batch operations and caching |
189
+
190
+ ## Examples & Documentation
191
+
192
+ Explore comprehensive guides and real-world examples:
193
+
194
+ - **[📖 Full Documentation](https://mathisarends.github.io/notionary/)** - Complete API reference and guides
195
+ - **[Getting Started](https://mathisarends.github.io/notionary/get-started/)** - Quick setup and first steps
196
+ - **[Page Management](https://mathisarends.github.io/notionary/page/)** - Work with page content and properties
197
+ - **[Database Operations](https://mathisarends.github.io/notionary/database/)** - Query and manage databases
198
+ - **[Block Types](https://mathisarends.github.io/notionary/blocks/)** - Complete formatting reference
199
+
200
+ Check out the `examples/` directory for hands-on tutorials:
201
+
202
+ ### Core Examples
203
+
204
+ - **[Page Management](examples/page_example.py)** - Create, update, and manage pages
205
+ - **[Database Operations](examples/database.py)** - Connect to and query databases
206
+ - **[Workspace Discovery](examples/workspace_discovery.py)** - Explore your workspace
207
+
208
+ ### Markdown Examples
209
+
210
+ - **[Basic Formatting](examples/markdown/basic.py)** - Text, lists, and links
211
+ - **[Callouts](examples/markdown/callout.py)** - Eye-catching information boxes
212
+ - **[Toggles](examples/markdown/toggle.py)** - Collapsible content sections
213
+ - **[Multi-Column](examples/markdown/columns.py)** - Side-by-side layouts
214
+ - **[Tables](examples/markdown/table.py)** - Structured data presentation
215
+
216
+ ## Contributing
217
+
218
+ We'd love your help making Notionary even better!
219
+
220
+ Whether it's fixing bugs, adding features, improving docs, or sharing examples - all contributions are welcome.
221
+
222
+ See our [Contributing Guide](https://mathisarends.github.io/notionary/contributing/) to get started.
223
+
224
+ ---
225
+
226
+ <div align="center">
227
+
228
+ **Ready to transform your Notion workflow?**
229
+
230
+ [📖 Read the Docs](https://mathisarends.github.io/notionary/) • [Getting Started](https://mathisarends.github.io/notionary/get-started/) • [Examples](examples/)
231
+
232
+ Built with ❤️ for the Python community
233
+
234
+ </div>
235
+