notionary 0.2.13__py3-none-any.whl → 0.2.15__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 (83) hide show
  1. notionary/__init__.py +3 -16
  2. notionary/{notion_client.py → base_notion_client.py} +92 -98
  3. notionary/blocks/__init__.py +61 -0
  4. notionary/{elements → blocks}/audio_element.py +6 -3
  5. notionary/{elements → blocks}/bookmark_element.py +3 -5
  6. notionary/{elements → blocks}/bulleted_list_element.py +5 -6
  7. notionary/{elements → blocks}/callout_element.py +4 -6
  8. notionary/{elements → blocks}/code_block_element.py +4 -5
  9. notionary/{elements → blocks}/column_element.py +3 -5
  10. notionary/{elements → blocks}/divider_element.py +3 -5
  11. notionary/{elements → blocks}/embed_element.py +4 -5
  12. notionary/{elements → blocks}/heading_element.py +4 -7
  13. notionary/{elements → blocks}/image_element.py +4 -5
  14. notionary/{elements → blocks}/mention_element.py +3 -6
  15. notionary/blocks/notion_block_client.py +26 -0
  16. notionary/{elements → blocks}/notion_block_element.py +2 -3
  17. notionary/{elements → blocks}/numbered_list_element.py +4 -6
  18. notionary/{elements → blocks}/paragraph_element.py +4 -6
  19. notionary/{prompting/element_prompt_content.py → blocks/prompts/element_prompt_builder.py} +1 -40
  20. notionary/blocks/prompts/element_prompt_content.py +41 -0
  21. notionary/{elements → blocks}/qoute_element.py +4 -5
  22. notionary/{elements → blocks}/registry/block_registry.py +4 -26
  23. notionary/{elements → blocks}/registry/block_registry_builder.py +26 -25
  24. notionary/{elements → blocks}/table_element.py +5 -6
  25. notionary/{elements → blocks}/text_inline_formatter.py +1 -4
  26. notionary/{elements → blocks}/todo_element.py +5 -6
  27. notionary/{elements → blocks}/toggle_element.py +3 -5
  28. notionary/{elements → blocks}/toggleable_heading_element.py +4 -6
  29. notionary/{elements → blocks}/video_element.py +4 -5
  30. notionary/database/__init__.py +0 -0
  31. notionary/database/client.py +132 -0
  32. notionary/database/database_exceptions.py +13 -0
  33. notionary/database/factory.py +0 -0
  34. notionary/database/filter_builder.py +175 -0
  35. notionary/database/notion_database.py +340 -127
  36. notionary/database/notion_database_provider.py +230 -0
  37. notionary/elements/__init__.py +0 -0
  38. notionary/models/notion_database_response.py +294 -13
  39. notionary/models/notion_page_response.py +9 -31
  40. notionary/models/search_response.py +0 -0
  41. notionary/page/__init__.py +0 -0
  42. notionary/page/client.py +110 -0
  43. notionary/page/content/page_content_retriever.py +5 -20
  44. notionary/page/content/page_content_writer.py +3 -4
  45. notionary/page/formatting/markdown_to_notion_converter.py +1 -3
  46. notionary/{prompting → page}/markdown_syntax_prompt_generator.py +1 -2
  47. notionary/page/notion_page.py +354 -317
  48. notionary/page/notion_to_markdown_converter.py +1 -4
  49. notionary/page/properites/property_value_extractor.py +0 -64
  50. notionary/page/{properites/property_formatter.py → property_formatter.py} +7 -4
  51. notionary/page/search_filter_builder.py +131 -0
  52. notionary/page/utils.py +60 -0
  53. notionary/util/__init__.py +12 -3
  54. notionary/util/factory_decorator.py +33 -0
  55. notionary/util/fuzzy_matcher.py +82 -0
  56. notionary/util/page_id_utils.py +0 -21
  57. notionary/util/singleton_metaclass.py +22 -0
  58. notionary/workspace.py +69 -0
  59. notionary-0.2.15.dist-info/METADATA +223 -0
  60. notionary-0.2.15.dist-info/RECORD +68 -0
  61. {notionary-0.2.13.dist-info → notionary-0.2.15.dist-info}/WHEEL +1 -2
  62. notionary/cli/main.py +0 -347
  63. notionary/cli/onboarding.py +0 -116
  64. notionary/database/database_discovery.py +0 -142
  65. notionary/database/notion_database_factory.py +0 -190
  66. notionary/exceptions/database_exceptions.py +0 -76
  67. notionary/exceptions/page_creation_exception.py +0 -9
  68. notionary/page/metadata/metadata_editor.py +0 -150
  69. notionary/page/metadata/notion_icon_manager.py +0 -77
  70. notionary/page/metadata/notion_page_cover_manager.py +0 -56
  71. notionary/page/notion_page_factory.py +0 -328
  72. notionary/page/properites/database_property_service.py +0 -302
  73. notionary/page/properites/page_property_manager.py +0 -152
  74. notionary/page/relations/notion_page_relation_manager.py +0 -350
  75. notionary/page/relations/notion_page_title_resolver.py +0 -104
  76. notionary/page/relations/page_database_relation.py +0 -68
  77. notionary/util/warn_direct_constructor_usage.py +0 -54
  78. notionary-0.2.13.dist-info/METADATA +0 -273
  79. notionary-0.2.13.dist-info/RECORD +0 -67
  80. notionary-0.2.13.dist-info/entry_points.txt +0 -2
  81. notionary-0.2.13.dist-info/top_level.txt +0 -1
  82. /notionary/util/{singleton.py → singleton_decorator.py} +0 -0
  83. {notionary-0.2.13.dist-info/licenses → notionary-0.2.15.dist-info}/LICENSE +0 -0
@@ -1,350 +0,0 @@
1
- import asyncio
2
- from typing import Any, Dict, List, Optional
3
- from notionary.models.notion_page_response import DatabaseParent, NotionPageResponse
4
- from notionary.notion_client import NotionClient
5
- from notionary.page.relations.notion_page_title_resolver import (
6
- NotionPageTitleResolver,
7
- )
8
- from notionary.util import LoggingMixin
9
-
10
-
11
- class NotionPageRelationManager(LoggingMixin):
12
- """
13
- Manager for relation properties of a Notion page.
14
- Manages links between pages and loads available relation options.
15
- """
16
-
17
- def __init__(
18
- self, page_id: str, client: NotionClient, database_id: Optional[str] = None
19
- ):
20
- """
21
- Initializes the relation manager.
22
- """
23
- self._page_id = page_id
24
- self._client = client
25
- self._database_id = database_id
26
- self._page_properties = None
27
-
28
- self._page_title_resolver = NotionPageTitleResolver(client=client)
29
-
30
- async def get_relation_property_ids(self) -> List[str]:
31
- """
32
- Returns a list of all relation property names.
33
-
34
- Returns:
35
- List[str]: Names of all relation properties
36
- """
37
- properties = await self._get_page_properties()
38
-
39
- return [
40
- prop_name
41
- for prop_name, prop_data in properties.items()
42
- if prop_data.get("type") == "relation"
43
- ]
44
-
45
- async def get_relation_values(self, property_name: str) -> List[str]:
46
- """
47
- Returns the titles of the pages linked via a relation property.
48
-
49
- Args:
50
- property_name: Name of the relation property
51
-
52
- Returns:
53
- List[str]: List of linked page titles
54
- """
55
- properties = await self._get_page_properties()
56
-
57
- if property_name not in properties:
58
- return []
59
-
60
- prop_data = properties[property_name]
61
-
62
- if prop_data.get("type") != "relation" or "relation" not in prop_data:
63
- return []
64
-
65
- resolver = NotionPageTitleResolver(self._client)
66
- titles = []
67
-
68
- for rel in prop_data["relation"]:
69
- page_id = rel.get("id")
70
- if not page_id:
71
- continue
72
-
73
- title = await resolver.get_title_by_page_id(page_id)
74
- if not title:
75
- continue
76
-
77
- titles.append(title)
78
-
79
- return titles
80
-
81
- async def get_relation_database_id(self, property_name: str) -> Optional[str]:
82
- """
83
- Returns the ID of the linked database for a relation property.
84
-
85
- Args:
86
- property_name: Name of the relation property
87
-
88
- Returns:
89
- Optional[str]: ID of the linked database or None
90
- """
91
- relation_details = await self._get_relation_details(property_name)
92
-
93
- if not relation_details:
94
- return None
95
-
96
- return relation_details.get("database_id")
97
-
98
- async def get_relation_options(
99
- self, property_name: str, limit: int = 100
100
- ) -> List[str]:
101
- """
102
- Returns available title options for a relation property.
103
-
104
- Args:
105
- property_name: Name of the relation property
106
- limit: Maximum number of options to return
107
-
108
- Returns:
109
- List[str]: List of page titles that can be used for this relation
110
- """
111
- related_db_id = await self.get_relation_database_id(property_name)
112
-
113
- if not related_db_id:
114
- return []
115
-
116
- try:
117
- query_result = await self._client.post(
118
- f"databases/{related_db_id}/query",
119
- {
120
- "page_size": limit,
121
- },
122
- )
123
-
124
- if not query_result or "results" not in query_result:
125
- return []
126
-
127
- titles = []
128
- for page in query_result["results"]:
129
- title = self._extract_title_from_page(page)
130
- if title:
131
- titles.append(title)
132
-
133
- return titles
134
- except Exception as e:
135
- self.logger.error("Error retrieving relation options: %s", str(e))
136
- return []
137
-
138
- async def set_relation_values_by_page_titles(
139
- self, property_name: str, page_titles: List[str]
140
- ) -> List[str]:
141
- """
142
- Sets relation values based on page titles, replacing any existing relations.
143
-
144
- Args:
145
- property_name: Name of the relation property
146
- page_titles: List of page titles to set as relations
147
-
148
- Returns:
149
- List[str]: List of page titles that were successfully set as relations
150
- """
151
- self.logger.info(
152
- "Setting %d relation(s) for property '%s'",
153
- len(page_titles),
154
- property_name,
155
- )
156
-
157
- resolution_results = await asyncio.gather(
158
- *(
159
- self._page_title_resolver.get_page_id_by_title(title)
160
- for title in page_titles
161
- )
162
- )
163
-
164
- found_pages = []
165
- page_ids = []
166
- not_found_pages = []
167
-
168
- for title, page_id in zip(page_titles, resolution_results):
169
- if page_id:
170
- found_pages.append(title)
171
- page_ids.append(page_id)
172
- self.logger.debug("Found page ID %s for title '%s'", page_id, title)
173
- else:
174
- not_found_pages.append(title)
175
- self.logger.warning("No page found with title '%s'", title)
176
-
177
- self.logger.debug("Page IDs being sent to API: %s", page_ids)
178
-
179
- if not page_ids:
180
- self.logger.warning(
181
- "No valid page IDs found for any of the titles, no changes applied"
182
- )
183
- return []
184
-
185
- api_response = await self._set_relations_by_page_ids(property_name, page_ids)
186
-
187
- if not api_response:
188
- self.logger.error(
189
- "Failed to set relations for '%s' (API error)", property_name
190
- )
191
- return []
192
-
193
- if not_found_pages:
194
- not_found_str = "', '".join(not_found_pages)
195
- self.logger.info(
196
- "Set %d relation(s) for '%s', but couldn't find pages: '%s'",
197
- len(page_ids),
198
- property_name,
199
- not_found_str,
200
- )
201
- else:
202
- self.logger.info(
203
- "Successfully set all %d relation(s) for '%s'",
204
- len(page_ids),
205
- property_name,
206
- )
207
-
208
- return found_pages
209
-
210
- async def get_all_relations(self) -> Dict[str, List[str]]:
211
- """
212
- Returns all relation properties and their values.
213
-
214
- Returns:
215
- Dict[str, List[str]]: Dictionary of property names and their values
216
- """
217
- relation_properties = await self.get_relation_property_ids()
218
-
219
- if not relation_properties:
220
- return {}
221
-
222
- result = {}
223
- for prop_name in relation_properties:
224
- result[prop_name] = await self.get_relation_values(prop_name)
225
-
226
- return result
227
-
228
- async def _get_relation_details(
229
- self, property_name: str
230
- ) -> Optional[Dict[str, Any]]:
231
- """
232
- Returns details about the relation property, including the linked database.
233
-
234
- Args:
235
- property_name: Name of the relation property
236
-
237
- Returns:
238
- The "relation" field of the property, or None if not found or not of type "relation".
239
- """
240
- database_id = await self._ensure_database_id()
241
- if not database_id:
242
- return None
243
-
244
- try:
245
- database = await self._client.get_database(database_id)
246
-
247
- prop_data = database.properties.get(property_name)
248
- if not prop_data:
249
- return None
250
-
251
- if prop_data.get("type") != "relation":
252
- return None
253
-
254
- return prop_data.get("relation")
255
-
256
- except Exception as e:
257
- self.logger.error("Error retrieving relation details: %s", str(e))
258
- return None
259
-
260
- async def _get_page_properties(self, force_refresh: bool = False) -> Dict[str, Any]:
261
- """
262
- Loads the properties of the page.
263
-
264
- Args:
265
- force_refresh: If True, a new API call will be made.
266
-
267
- Returns:
268
- Dict[str, Any]: The properties of the page.
269
- """
270
- if self._page_properties is None or force_refresh:
271
- page_data = await self._client.get_page(self._page_id)
272
- if page_data:
273
- self._page_properties = page_data.properties or {}
274
- else:
275
- self._page_properties = {}
276
-
277
- return self._page_properties
278
-
279
- async def _ensure_database_id(self) -> Optional[str]:
280
- """
281
- Ensures the database_id is available. Loads it if necessary.
282
-
283
- Returns:
284
- Optional[str]: The database ID or None
285
- """
286
- if self._database_id:
287
- return self._database_id
288
-
289
- page_data = await self._client.get_page(self._page_id)
290
-
291
- if not page_data or not page_data.parent:
292
- return None
293
-
294
- if isinstance(page_data.parent, DatabaseParent):
295
- self._database_id = page_data.parent.database_id
296
- return self._database_id
297
-
298
- return None
299
-
300
- def _extract_title_from_page(self, page: Dict[str, Any]) -> Optional[str]:
301
- """
302
- Extracts the title from a page object.
303
-
304
- Args:
305
- page: The page object from the Notion API
306
-
307
- Returns:
308
- Optional[str]: The page title or None
309
- """
310
- if "properties" not in page:
311
- return None
312
-
313
- properties = page["properties"]
314
-
315
- for prop_data in properties.values():
316
- if prop_data.get("type") == "title" and "title" in prop_data:
317
- title_parts = prop_data["title"]
318
- return "".join(
319
- [text_obj.get("plain_text", "") for text_obj in title_parts]
320
- )
321
-
322
- return None
323
-
324
- async def _set_relations_by_page_ids(
325
- self, property_name: str, page_ids: List[str]
326
- ) -> Optional[NotionPageResponse]:
327
- """
328
- Adds one or more relations.
329
-
330
- Args:
331
- property_name: Name of the relation property
332
- page_ids: List of page IDs to add
333
-
334
- Returns:
335
- Optional[NotionPageResponse]: API response or None on error
336
- """
337
- relation_payload = {"relation": [{"id": page_id} for page_id in page_ids]}
338
-
339
- try:
340
- page_response: NotionPageResponse = await self._client.patch_page(
341
- self._page_id,
342
- {"properties": {property_name: relation_payload}},
343
- )
344
-
345
- self._page_properties = None
346
-
347
- return page_response
348
- except Exception as e:
349
- self.logger.error("Error adding relation: %s", str(e))
350
- return None
@@ -1,104 +0,0 @@
1
- from typing import Optional, Dict, Any, List
2
- from notionary.notion_client import NotionClient
3
- from notionary.util import LoggingMixin
4
-
5
-
6
- class NotionPageTitleResolver(LoggingMixin):
7
- def __init__(self, client: NotionClient):
8
- self._client = client
9
-
10
- async def get_page_id_by_title(self, title: str) -> Optional[str]:
11
- """
12
- Searches for a Notion page by its title and returns the corresponding page ID if found.
13
- """
14
- try:
15
- search_results = await self._client.post(
16
- "search",
17
- {"query": title, "filter": {"value": "page", "property": "object"}},
18
- )
19
-
20
- results = search_results.get("results", [])
21
-
22
- if not results:
23
- self.logger.debug(f"No page found with title '{title}'")
24
- return None
25
-
26
- # Durchsuche die Ergebnisse nach dem passenden Titel
27
- for result in results:
28
- properties = result.get("properties", {})
29
- page_title = self._extract_page_title_from_properties(properties)
30
-
31
- if page_title == title:
32
- return result.get("id")
33
-
34
- self.logger.debug(f"No matching page found with title '{title}'")
35
- return None
36
-
37
- except Exception as e:
38
- self.logger.error(f"Error while searching for page '{title}': {e}")
39
- return None
40
-
41
- async def get_title_by_page_id(self, page_id: str) -> Optional[str]:
42
- """
43
- Retrieves the title of a Notion page by its page ID.
44
-
45
- Args:
46
- page_id: The ID of the Notion page.
47
-
48
- Returns:
49
- The title of the page, or None if not found.
50
- """
51
- try:
52
- page = await self._client.get_page(page_id)
53
- return self._extract_page_title_from_properties(page.properties)
54
-
55
- except Exception as e:
56
- self.logger.error(f"Error retrieving title for page ID '{page_id}': {e}")
57
- return None
58
-
59
- async def get_page_titles_by_ids(self, page_ids: List[str]) -> Dict[str, str]:
60
- """
61
- Retrieves titles for multiple page IDs at once.
62
-
63
- Args:
64
- page_ids: List of page IDs to get titles for
65
-
66
- Returns:
67
- Dictionary mapping page IDs to their titles
68
- """
69
- result = {}
70
- for page_id in page_ids:
71
- title = await self.get_title_by_page_id(page_id)
72
- if title:
73
- result[page_id] = title
74
- return result
75
-
76
- def _extract_page_title_from_properties(self, properties: Dict[str, Any]) -> str:
77
- """
78
- Extract title from properties dictionary.
79
-
80
- Args:
81
- properties: The properties dictionary from a Notion page
82
-
83
- Returns:
84
- str: The extracted title or "Untitled" if not found
85
- """
86
- try:
87
- for prop_value in properties.values():
88
- if not isinstance(prop_value, dict):
89
- continue
90
-
91
- if prop_value.get("type") != "title":
92
- continue
93
-
94
- title_array = prop_value.get("title", [])
95
- if not title_array:
96
- continue
97
-
98
- for text_obj in title_array:
99
- if "plain_text" in text_obj:
100
- return text_obj["plain_text"]
101
- except Exception as e:
102
- self.logger.error(f"Error extracting page title from properties: {e}")
103
-
104
- return "Untitled"
@@ -1,68 +0,0 @@
1
- from typing import Dict, Optional, Any
2
- from notionary.models.notion_page_response import DatabaseParent, NotionPageResponse
3
- from notionary.notion_client import NotionClient
4
- from notionary.util import LoggingMixin
5
-
6
-
7
- class PageDatabaseRelation(LoggingMixin):
8
- """
9
- Manages the relationship between a Notion page and its parent database.
10
- Provides methods to access database schema and property options.
11
- """
12
-
13
- def __init__(self, page_id: str, client: NotionClient):
14
- """
15
- Initialize the page-database relationship handler.
16
-
17
- Args:
18
- page_id: ID of the Notion page
19
- client: Instance of NotionClient
20
- """
21
- self._page_id = page_id
22
- self._client = client
23
- self._parent_database_id = None
24
- self._database_schema = None
25
- self._page_data = None
26
-
27
- async def _get_page_data(self, force_refresh=False) -> NotionPageResponse:
28
- """
29
- Gets the page data and caches it for future use.
30
-
31
- Args:
32
- force_refresh: Whether to force a refresh of the page data
33
-
34
- Returns:
35
- Dict[str, Any]: The page data
36
- """
37
- if self._page_data is None or force_refresh:
38
- self._page_data = await self._client.get_page(self._page_id)
39
- return self._page_data
40
-
41
- async def get_parent_database_id(self) -> Optional[str]:
42
- """
43
- Returns the ID of the database this page belongs to, if any.
44
- """
45
- if self._parent_database_id is not None:
46
- return self._parent_database_id
47
-
48
- page_data = await self._get_page_data()
49
-
50
- if not page_data:
51
- return None
52
-
53
- parent = page_data.parent
54
- if isinstance(parent, DatabaseParent):
55
- self._parent_database_id = parent.database_id
56
- return self._parent_database_id
57
-
58
- return None
59
-
60
- async def is_database_page(self) -> bool:
61
- """
62
- Checks if this page belongs to a database.
63
-
64
- Returns:
65
- bool: True if the page belongs to a database, False otherwise
66
- """
67
- database_id = await self.get_parent_database_id()
68
- return database_id is not None
@@ -1,54 +0,0 @@
1
- import functools
2
- import inspect
3
- from typing import Callable, Any, TypeVar, cast
4
-
5
- F = TypeVar("F", bound=Callable[..., Any])
6
-
7
-
8
- def warn_direct_constructor_usage(func: F) -> F:
9
- """
10
- Method decorator that logs a warning when the constructor is called directly
11
- instead of through a factory method.
12
-
13
- This is an advisory decorator - it only logs a warning and doesn't
14
- prevent direct constructor usage.
15
- """
16
-
17
- @functools.wraps(func)
18
- def wrapper(self, *args, **kwargs):
19
- # Get the call stack
20
- stack = inspect.stack()
21
-
22
- self._from_factory = False
23
-
24
- search_depth = min(6, len(stack))
25
-
26
- for i in range(1, search_depth):
27
- if i >= len(stack):
28
- break
29
-
30
- caller_frame = stack[i]
31
- caller_name = caller_frame.function
32
-
33
- # Debug logging might be helpful during development
34
- # print(f"Frame {i}: {caller_name}")
35
-
36
- # If called from a factory method, mark it and break
37
- if caller_name.startswith("create_from_") or caller_name.startswith(
38
- "from_"
39
- ):
40
- self._from_factory = True
41
- break
42
-
43
- # If not from factory, log warning
44
- if not self._from_factory and hasattr(self, "logger"):
45
- self.logger.warning(
46
- "Advisory: Direct constructor usage is discouraged. "
47
- "Consider using factory methods like create_from_page_id(), "
48
- "create_from_url(), or create_from_page_name() instead."
49
- )
50
-
51
- # Call the original __init__
52
- return func(self, *args, **kwargs)
53
-
54
- return cast(F, wrapper)