notionary 0.1.5__py3-none-any.whl → 0.1.7__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 (29) hide show
  1. notionary/core/database/notion_database_manager.py +146 -232
  2. notionary/core/database/notion_database_manager_factory.py +9 -52
  3. notionary/core/database/notion_database_schema.py +1 -314
  4. notionary/core/notion_client.py +2 -10
  5. notionary/core/page/{page_content_manager.py → content/page_content_manager.py} +0 -1
  6. notionary/core/page/metadata/metadata_editor.py +109 -0
  7. notionary/core/page/metadata/notion_icon_manager.py +46 -0
  8. notionary/core/page/metadata/notion_page_cover_manager.py +48 -0
  9. notionary/core/page/notion_page_manager.py +230 -49
  10. notionary/core/page/properites/database_property_service.py +330 -0
  11. notionary/core/page/properites/page_property_manager.py +146 -0
  12. notionary/core/page/{property_formatter.py → properites/property_formatter.py} +19 -20
  13. notionary/core/page/properites/property_operation_result.py +103 -0
  14. notionary/core/page/properites/property_value_extractor.py +46 -0
  15. notionary/core/page/relations/notion_page_relation_manager.py +364 -0
  16. notionary/core/page/relations/notion_page_title_resolver.py +43 -0
  17. notionary/core/page/relations/page_database_relation.py +70 -0
  18. notionary/core/page/relations/relation_operation_result.py +135 -0
  19. notionary/util/{uuid_utils.py → page_id_utils.py} +15 -0
  20. {notionary-0.1.5.dist-info → notionary-0.1.7.dist-info}/METADATA +1 -1
  21. {notionary-0.1.5.dist-info → notionary-0.1.7.dist-info}/RECORD +24 -18
  22. notionary/core/database/database_query_service.py +0 -73
  23. notionary/core/database/database_schema_service.py +0 -57
  24. notionary/core/database/notion_database_writer.py +0 -390
  25. notionary/core/database/page_service.py +0 -161
  26. notionary/core/page/meta_data/metadata_editor.py +0 -37
  27. {notionary-0.1.5.dist-info → notionary-0.1.7.dist-info}/WHEEL +0 -0
  28. {notionary-0.1.5.dist-info → notionary-0.1.7.dist-info}/licenses/LICENSE +0 -0
  29. {notionary-0.1.5.dist-info → notionary-0.1.7.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,5 @@
1
- from typing import Any, Dict, List, Optional
1
+ import asyncio
2
+ from typing import Any, Dict, List, Optional, Union
2
3
  from notionary.core.converters.registry.block_element_registry import (
3
4
  BlockElementRegistry,
4
5
  )
@@ -6,15 +7,20 @@ from notionary.core.converters.registry.block_element_registry_builder import (
6
7
  BlockElementRegistryBuilder,
7
8
  )
8
9
  from notionary.core.notion_client import NotionClient
9
- from notionary.core.page.page_content_manager import PageContentManager
10
+ from notionary.core.page.metadata.metadata_editor import MetadataEditor
11
+ from notionary.core.page.metadata.notion_icon_manager import NotionPageIconManager
12
+ from notionary.core.page.metadata.notion_page_cover_manager import NotionPageCoverManager
13
+ from notionary.core.page.properites.database_property_service import DatabasePropertyService
14
+ from notionary.core.page.relations.notion_page_relation_manager import NotionRelationManager
15
+ from notionary.core.page.content.page_content_manager import PageContentManager
16
+ from notionary.core.page.properites.page_property_manager import PagePropertyManager
10
17
  from notionary.util.logging_mixin import LoggingMixin
11
- from notionary.core.page.meta_data.metadata_editor import MetadataEditor
12
- from notionary.util.uuid_utils import extract_uuid, format_uuid, is_valid_uuid
13
-
18
+ from notionary.util.page_id_utils import extract_and_validate_page_id
19
+ from notionary.core.page.relations.page_database_relation import PageDatabaseRelation
14
20
 
15
21
  class NotionPageManager(LoggingMixin):
16
22
  """
17
- High-Level Fassade zur Verwaltung von Inhalten und Metadaten einer Notion-Seite.
23
+ High-Level Facade for managing content and metadata of a Notion page.
18
24
  """
19
25
 
20
26
  def __init__(
@@ -24,38 +30,60 @@ class NotionPageManager(LoggingMixin):
24
30
  url: Optional[str] = None,
25
31
  token: Optional[str] = None,
26
32
  ):
27
- if not page_id and not url:
28
- raise ValueError("Either page_id or url must be provided")
29
-
30
- if not page_id and url:
31
- page_id = extract_uuid(url)
32
- if not page_id:
33
- raise ValueError(f"Could not extract a valid UUID from the URL: {url}")
34
-
35
- page_id = format_uuid(page_id)
36
- if not page_id or not is_valid_uuid(page_id):
37
- raise ValueError(f"Invalid UUID format: {page_id}")
33
+ self._page_id = extract_and_validate_page_id(page_id=page_id, url=url)
38
34
 
39
- self._page_id = page_id
40
35
  self.url = url
41
36
  self._title = title
42
-
43
37
  self._client = NotionClient(token=token)
38
+ self._page_data = None
44
39
 
45
40
  self._block_element_registry = (
46
41
  BlockElementRegistryBuilder.create_standard_registry()
47
42
  )
48
43
 
49
44
  self._page_content_manager = PageContentManager(
50
- page_id=page_id,
45
+ page_id=self._page_id,
51
46
  client=self._client,
52
47
  block_registry=self._block_element_registry,
53
48
  )
54
- self._metadata = MetadataEditor(page_id, self._client)
49
+ self._metadata = MetadataEditor(self._page_id, self._client)
50
+ self._page_cover_manager = NotionPageCoverManager(page_id=self._page_id, client=self._client)
51
+ self._page_icon_manager = NotionPageIconManager(page_id=self._page_id, client=self._client)
52
+
53
+ self._db_relation = PageDatabaseRelation(page_id=self._page_id, client=self._client)
54
+ self._db_property_service = None
55
+
56
+ self._relation_manager = NotionRelationManager(page_id=self._page_id, client=self._client)
57
+
58
+ self._property_manager = PagePropertyManager(
59
+ self._page_id,
60
+ self._client,
61
+ self._metadata,
62
+ self._db_relation
63
+ )
64
+
65
+ async def _get_db_property_service(self) -> Optional[DatabasePropertyService]:
66
+ """
67
+ Gets the database property service, initializing it if necessary.
68
+ This is a more intuitive way to work with the instance variable.
69
+
70
+ Returns:
71
+ Optional[DatabasePropertyService]: The database property service or None if not applicable
72
+ """
73
+ if self._db_property_service is not None:
74
+ return self._db_property_service
75
+
76
+ database_id = await self._db_relation.get_parent_database_id()
77
+ if not database_id:
78
+ return None
79
+
80
+ self._db_property_service = DatabasePropertyService(database_id, self._client)
81
+ await self._db_property_service.load_schema()
82
+ return self._db_property_service
55
83
 
56
84
  @property
57
85
  def page_id(self) -> Optional[str]:
58
- """Get the title of the page."""
86
+ """Get the ID of the page."""
59
87
  return self._page_id
60
88
 
61
89
  @property
@@ -69,9 +97,7 @@ class NotionPageManager(LoggingMixin):
69
97
  @block_registry.setter
70
98
  def block_registry(self, block_registry: BlockElementRegistry) -> None:
71
99
  """Set the block element registry for the page content manager."""
72
-
73
100
  self._block_element_registry = block_registry
74
-
75
101
  self._page_content_manager = PageContentManager(
76
102
  page_id=self._page_id, client=self._client, block_registry=block_registry
77
103
  )
@@ -86,44 +112,199 @@ class NotionPageManager(LoggingMixin):
86
112
  await self._page_content_manager.clear()
87
113
  return await self._page_content_manager.append_markdown(markdown)
88
114
 
89
- async def get_blocks(self) -> List[Dict[str, Any]]:
90
- return await self._page_content_manager.get_blocks()
91
-
92
- async def get_block_children(self, block_id: str) -> List[Dict[str, Any]]:
93
- return await self._page_content_manager.get_block_children(block_id)
94
-
95
- async def get_page_blocks_with_children(self) -> List[Dict[str, Any]]:
96
- return await self._page_content_manager.get_page_blocks_with_children()
97
-
98
115
  async def get_text(self) -> str:
99
116
  return await self._page_content_manager.get_text()
100
-
117
+
101
118
  async def set_title(self, title: str) -> Optional[Dict[str, Any]]:
102
119
  return await self._metadata.set_title(title)
103
120
 
104
121
  async def set_page_icon(
105
122
  self, emoji: Optional[str] = None, external_url: Optional[str] = None
106
123
  ) -> Optional[Dict[str, Any]]:
107
- return await self._metadata.set_icon(emoji, external_url)
124
+ return await self._page_icon_manager.set_icon(emoji, external_url)
125
+
126
+ async def _get_page_data(self, force_refresh=False) -> Dict[str, Any]:
127
+ """ Gets the page data and caches it for future use.
128
+ """
129
+ if self._page_data is None or force_refresh:
130
+ self._page_data = await self._client.get_page(self._page_id)
131
+ return self._page_data
132
+
133
+ async def get_icon(self) -> Optional[str]:
134
+ """Retrieves the page icon - either emoji or external URL.
135
+ """
136
+ return await self._page_icon_manager.get_icon()
137
+
138
+ async def get_cover_url(self) -> str:
139
+ return await self._page_cover_manager.get_cover_url()
108
140
 
109
141
  async def set_page_cover(self, external_url: str) -> Optional[Dict[str, Any]]:
110
- return await self._metadata.set_cover(external_url)
142
+ return await self._page_cover_manager.set_cover(external_url)
143
+
144
+ async def set_random_gradient_cover(self) -> Optional[Dict[str, Any]]:
145
+ return await self._page_cover_manager.set_random_gradient_cover()
111
146
 
112
147
  async def get_properties(self) -> Dict[str, Any]:
113
- """Retrieves all properties of the page"""
114
- page_data = await self._client.get_page(self._page_id)
115
- if page_data and "properties" in page_data:
116
- return page_data["properties"]
117
- return {}
148
+ """Retrieves all properties of the page."""
149
+ return await self._property_manager.get_properties()
118
150
 
119
- async def get_status(self) -> Optional[str]:
151
+ async def get_property_value(self, property_name: str) -> Any:
152
+ """Get the value of a specific property."""
153
+ return await self._property_manager.get_property_value(
154
+ property_name,
155
+ self._relation_manager.get_relation_values
156
+ )
157
+
158
+ async def set_property_by_name(self, property_name: str, value: Any) -> Optional[Dict[str, Any]]:
159
+ """ Sets the value of a specific property by its name.
160
+ """
161
+ return await self._property_manager.set_property_by_name(
162
+ property_name=property_name,
163
+ value=value,
164
+ )
165
+
166
+ async def is_database_page(self) -> bool:
167
+ """ Checks if this page belongs to a database.
120
168
  """
121
- Determines the status of the page (e.g., 'Draft', 'Completed', etc.)
169
+ return await self._db_relation.is_database_page()
122
170
 
123
- Returns:
124
- Optional[str]: The status as a string or None if not available
171
+ async def get_parent_database_id(self) -> Optional[str]:
172
+ """ Gets the ID of the database this page belongs to, if any
173
+ """
174
+ return await self._db_relation.get_parent_database_id()
175
+
176
+ async def get_available_options_for_property(self, property_name: str) -> List[str]:
177
+ """ Gets the available option names for a property (select, multi_select, status).
178
+ """
179
+ db_service = await self._get_db_property_service()
180
+ if db_service:
181
+ return await db_service.get_option_names(property_name)
182
+ return []
183
+
184
+ async def get_property_type(self, property_name: str) -> Optional[str]:
185
+ """ Gets the type of a specific property.
186
+ """
187
+ db_service = await self._get_db_property_service()
188
+ if db_service:
189
+ return await db_service.get_property_type(property_name)
190
+ return None
191
+
192
+ async def get_database_metadata(self, include_types: Optional[List[str]] = None) -> Dict[str, Any]:
193
+ """ Gets complete metadata about the database this page belongs to.
125
194
  """
126
- properties = await self.get_properties()
127
- if "Status" in properties and properties["Status"].get("status"):
128
- return properties["Status"]["status"]["name"]
129
- return None
195
+ db_service = await self._get_db_property_service()
196
+ if db_service:
197
+ return await db_service.get_database_metadata(include_types)
198
+ return {"properties": {}}
199
+
200
+ async def get_relation_options(self, property_name: str, limit: int = 100) -> List[Dict[str, Any]]:
201
+ """ Returns available options for a relation property.
202
+ """
203
+ return await self._relation_manager.get_relation_options(property_name, limit)
204
+
205
+ async def add_relations_by_name(self, relation_property_name: str, page_titles: Union[str, List[str]]) -> Optional[Dict[str, Any]]:
206
+ """ Adds one or more relations.
207
+ """
208
+ return await self._relation_manager.add_relation_by_name(property_name=relation_property_name, page_titles=page_titles)
209
+
210
+ async def get_relation_values(self, property_name: str) -> List[str]:
211
+ """
212
+ Returns the current relation values for a property.
213
+ """
214
+ return await self._relation_manager.get_relation_values(property_name)
215
+
216
+ async def get_relation_property_ids(self) -> List[str]:
217
+ """ Returns a list of all relation property names.
218
+ """
219
+ return await self._relation_manager.get_relation_property_ids()
220
+
221
+ async def get_all_relations(self) -> Dict[str, List[str]]:
222
+ """ Returns all relation properties and their values.
223
+ """
224
+ return await self._relation_manager.get_all_relations()
225
+
226
+ async def get_status(self) -> Optional[str]:
227
+ """ Determines the status of the page (e.g., 'Draft', 'Completed', etc.)
228
+ """
229
+ return await self.get_property_value("Status")
230
+
231
+
232
+
233
+ async def main():
234
+ """
235
+ Demonstriert die Verwendung des refactorierten NotionPageManager.
236
+ """
237
+ print("=== NotionPageManager Demo ===")
238
+
239
+ page_manager = NotionPageManager(page_id="https://notion.so/1d0389d57bd3805cb34ccaf5804b43ce")
240
+
241
+ await page_manager.add_relations_by_name("Projekte", ["Fetzen mit Stine"])
242
+
243
+
244
+ input("Drücke Enter, um fortzufahren...")
245
+
246
+
247
+ is_database_page = await page_manager.is_database_page()
248
+
249
+ if not is_database_page:
250
+ print("Diese Seite gehört zu keiner Datenbank. Demo wird beendet.")
251
+ return
252
+
253
+ db_id = await page_manager.get_parent_database_id()
254
+ print(f"\n2. Datenbank-ID: {db_id}")
255
+
256
+ properties = await page_manager.get_properties()
257
+ print("\n3. Aktuelle Eigenschaften der Seite:")
258
+ for prop_name, prop_data in properties.items():
259
+ prop_type = prop_data.get("type", "unbekannt")
260
+
261
+ value = await page_manager.get_property_value(prop_name)
262
+ print(f" - {prop_name} ({prop_type}): {value}")
263
+
264
+ status_options = await page_manager.get_available_options_for_property("Status")
265
+ print(f"\n4. Verfügbare Status-Optionen: {status_options}")
266
+
267
+ tags_options = await page_manager.get_available_options_for_property("Tags")
268
+ print(f"\n5. Verfügbare Tags-Optionen: {tags_options}")
269
+
270
+ print("\n6. Relation-Eigenschaften und deren Optionen:")
271
+ for prop_name, prop_data in properties.items():
272
+ if prop_data.get("type") == "relation":
273
+ relation_options = await page_manager.get_relation_options(prop_name, limit=5)
274
+ option_names = [option.get("name", "Unbenannt") for option in relation_options]
275
+ print(f" - {prop_name} Relation-Optionen (max. 5): {option_names}")
276
+
277
+ print("\n7. Typen aller Eigenschaften:")
278
+ for prop_name in properties.keys():
279
+ prop_type = await page_manager.get_property_type(prop_name)
280
+ print(f" - {prop_name}: {prop_type}")
281
+
282
+ if status_options:
283
+ valid_status = status_options[0]
284
+ print(f"\n8. Setze Status auf '{valid_status}'...")
285
+ result = await page_manager.set_property_by_name("Status", valid_status)
286
+ print(f" Ergebnis: {'Erfolgreich' if result else 'Fehlgeschlagen'}")
287
+
288
+ current_status = await page_manager.get_status()
289
+ print(f" Aktueller Status: {current_status}")
290
+
291
+ # 9. Versuch, einen ungültigen Status zu setzen
292
+ invalid_status = "Bin King"
293
+ print(f"\n9. Versuche ungültigen Status '{invalid_status}' zu setzen...")
294
+ await page_manager.set_property_by_name("Status", invalid_status)
295
+
296
+ # 10. Komplette Datenbank-Metadaten für select-ähnliche Properties abrufen
297
+ print("\n10. Datenbank-Metadaten für select, multi_select und status Properties:")
298
+ metadata = await page_manager.get_database_metadata(
299
+ include_types=["select", "multi_select", "status"]
300
+ )
301
+
302
+ for prop_name, prop_info in metadata.get("properties", {}).items():
303
+ option_names = [opt.get("name", "") for opt in prop_info.get("options", [])]
304
+ print(f" - {prop_name} ({prop_info.get('type')}): {option_names}")
305
+
306
+ print("\nDemonstration abgeschlossen.")
307
+
308
+ if __name__ == "__main__":
309
+ asyncio.run(main())
310
+ print("\nDemonstration completed.")
@@ -0,0 +1,330 @@
1
+ from typing import Dict, List, Optional, Any, Tuple
2
+ from notionary.core.notion_client import NotionClient
3
+ from notionary.util.logging_mixin import LoggingMixin
4
+
5
+
6
+ class DatabasePropertyService(LoggingMixin):
7
+ """
8
+ Service for working with Notion database properties and options.
9
+ Provides specialized methods for retrieving property information and validating values.
10
+ """
11
+
12
+ def __init__(self, database_id: str, client: NotionClient):
13
+ """
14
+ Initialize the database property service.
15
+
16
+ Args:
17
+ database_id: ID of the Notion database
18
+ client: Instance of NotionClient
19
+ """
20
+ self._database_id = database_id
21
+ self._client = client
22
+ self._schema = None
23
+
24
+ async def load_schema(self, force_refresh=False) -> bool:
25
+ """
26
+ Loads the database schema.
27
+
28
+ Args:
29
+ force_refresh: Whether to force a refresh of the schema
30
+
31
+ Returns:
32
+ bool: True if schema loaded successfully, False otherwise
33
+ """
34
+ if self._schema is not None and not force_refresh:
35
+ return True
36
+
37
+ try:
38
+ database = await self._client.get(f"databases/{self._database_id}")
39
+ if database and "properties" in database:
40
+ self._schema = database["properties"]
41
+ self.logger.debug("Loaded schema for database %s", self._database_id)
42
+ return True
43
+ else:
44
+ self.logger.error("Failed to load schema: missing 'properties' in response")
45
+ return False
46
+ except Exception as e:
47
+ self.logger.error("Error loading database schema: %s", str(e))
48
+ return False
49
+
50
+ async def _ensure_schema_loaded(self) -> None:
51
+ """
52
+ Ensures the schema is loaded before accessing it.
53
+ """
54
+ if self._schema is None:
55
+ await self.load_schema()
56
+
57
+ async def get_schema(self) -> Dict[str, Any]:
58
+ """
59
+ Gets the database schema.
60
+
61
+ Returns:
62
+ Dict[str, Any]: The database schema
63
+ """
64
+ await self._ensure_schema_loaded()
65
+ return self._schema or {}
66
+
67
+ async def get_property_types(self) -> Dict[str, str]:
68
+ """
69
+ Gets all property types for the database.
70
+
71
+ Returns:
72
+ Dict[str, str]: Dictionary mapping property names to their types
73
+ """
74
+ await self._ensure_schema_loaded()
75
+
76
+ if not self._schema:
77
+ return {}
78
+
79
+ return {
80
+ prop_name: prop_data.get("type", "unknown")
81
+ for prop_name, prop_data in self._schema.items()
82
+ }
83
+
84
+ async def get_property_schema(self, property_name: str) -> Optional[Dict[str, Any]]:
85
+ """
86
+ Gets the schema for a specific property.
87
+
88
+ Args:
89
+ property_name: The name of the property
90
+
91
+ Returns:
92
+ Optional[Dict[str, Any]]: The property schema or None if not found
93
+ """
94
+ await self._ensure_schema_loaded()
95
+
96
+ if not self._schema or property_name not in self._schema:
97
+ return None
98
+
99
+ return self._schema[property_name]
100
+
101
+ async def get_property_type(self, property_name: str) -> Optional[str]:
102
+ """
103
+ Gets the type of a specific property.
104
+
105
+ Args:
106
+ property_name: The name of the property
107
+
108
+ Returns:
109
+ Optional[str]: The property type or None if not found
110
+ """
111
+ property_schema = await self.get_property_schema(property_name)
112
+
113
+ if not property_schema:
114
+ return None
115
+
116
+ return property_schema.get("type")
117
+
118
+ async def property_exists(self, property_name: str) -> bool:
119
+ """
120
+ Checks if a property exists in the database.
121
+
122
+ Args:
123
+ property_name: The name of the property
124
+
125
+ Returns:
126
+ bool: True if the property exists, False otherwise
127
+ """
128
+ property_schema = await self.get_property_schema(property_name)
129
+ return property_schema is not None
130
+
131
+ async def get_property_options(self, property_name: str) -> List[Dict[str, Any]]:
132
+ """
133
+ Gets the available options for a property (select, multi_select, status).
134
+
135
+ Args:
136
+ property_name: The name of the property
137
+
138
+ Returns:
139
+ List[Dict[str, Any]]: List of available options with their metadata
140
+ """
141
+ property_schema = await self.get_property_schema(property_name)
142
+
143
+ if not property_schema:
144
+ return []
145
+
146
+ property_type = property_schema.get("type")
147
+
148
+ if property_type in ["select", "multi_select", "status"]:
149
+ return property_schema.get(property_type, {}).get("options", [])
150
+
151
+ return []
152
+
153
+ async def get_option_names(self, property_name: str) -> List[str]:
154
+ """
155
+ Gets the available option names for a property (select, multi_select, status).
156
+
157
+ Args:
158
+ property_name: The name of the property
159
+
160
+ Returns:
161
+ List[str]: List of available option names
162
+ """
163
+ options = await self.get_property_options(property_name)
164
+ return [option.get("name", "") for option in options]
165
+
166
+ async def get_relation_details(self, property_name: str) -> Optional[Dict[str, Any]]:
167
+ """
168
+ Gets details about a relation property, including the related database.
169
+
170
+ Args:
171
+ property_name: The name of the property
172
+
173
+ Returns:
174
+ Optional[Dict[str, Any]]: The relation details or None if not a relation
175
+ """
176
+ property_schema = await self.get_property_schema(property_name)
177
+
178
+ if not property_schema or property_schema.get("type") != "relation":
179
+ return None
180
+
181
+ return property_schema.get("relation", {})
182
+
183
+ async def get_relation_options(self, property_name: str, limit: int = 100) -> List[Dict[str, Any]]:
184
+ """
185
+ Gets available options for a relation property by querying the related database.
186
+
187
+ Args:
188
+ property_name: The name of the relation property
189
+ limit: Maximum number of options to retrieve
190
+
191
+ Returns:
192
+ List[Dict[str, Any]]: List of pages from the related database
193
+ """
194
+ relation_details = await self.get_relation_details(property_name)
195
+
196
+ if not relation_details or "database_id" not in relation_details:
197
+ return []
198
+
199
+ related_db_id = relation_details["database_id"]
200
+
201
+ try:
202
+ # Query the related database to get options
203
+ query_result = await self._client.post(
204
+ f"databases/{related_db_id}/query",
205
+ {
206
+ "page_size": limit,
207
+ }
208
+ )
209
+
210
+ if not query_result or "results" not in query_result:
211
+ return []
212
+
213
+ # Extract relevant information from each page
214
+ options = []
215
+ for page in query_result["results"]:
216
+ page_id = page.get("id")
217
+ title = self._extract_title_from_page(page)
218
+
219
+ if page_id and title:
220
+ options.append({
221
+ "id": page_id,
222
+ "name": title
223
+ })
224
+
225
+ return options
226
+ except Exception as e:
227
+ self.logger.error(f"Error getting relation options: {str(e)}")
228
+ return []
229
+
230
+ def _extract_title_from_page(self, page: Dict[str, Any]) -> Optional[str]:
231
+ """
232
+ Extracts the title from a page object.
233
+
234
+ Args:
235
+ page: The page object from Notion API
236
+
237
+ Returns:
238
+ Optional[str]: The page title or None if not found
239
+ """
240
+ if "properties" not in page:
241
+ return None
242
+
243
+ properties = page["properties"]
244
+
245
+ # Look for a title property
246
+ for prop_data in properties.values():
247
+ if prop_data.get("type") == "title" and "title" in prop_data:
248
+ title_parts = prop_data["title"]
249
+ return "".join([text_obj.get("plain_text", "") for text_obj in title_parts])
250
+
251
+ return None
252
+
253
+ async def validate_property_value(self, property_name: str, value: Any) -> Tuple[bool, Optional[str], Optional[List[str]]]:
254
+ """
255
+ Validates a value for a property.
256
+
257
+ Args:
258
+ property_name: The name of the property
259
+ value: The value to validate
260
+
261
+ Returns:
262
+ Tuple[bool, Optional[str], Optional[List[str]]]:
263
+ - Boolean indicating if valid
264
+ - Error message if invalid
265
+ - Available options if applicable
266
+ """
267
+ property_schema = await self.get_property_schema(property_name)
268
+
269
+ if not property_schema:
270
+ return False, f"Property '{property_name}' does not exist", None
271
+
272
+ property_type = property_schema.get("type")
273
+
274
+ # Validate select, multi_select, status properties
275
+ if property_type in ["select", "status"]:
276
+ options = await self.get_option_names(property_name)
277
+
278
+ if isinstance(value, str) and value not in options:
279
+ return False, f"Invalid {property_type} option. Value '{value}' is not in the available options.", options
280
+
281
+ elif property_type == "multi_select":
282
+ options = await self.get_option_names(property_name)
283
+
284
+ if isinstance(value, list):
285
+ invalid_values = [val for val in value if val not in options]
286
+ if invalid_values:
287
+ return False, f"Invalid multi_select options: {', '.join(invalid_values)}", options
288
+
289
+ return True, None, None
290
+
291
+ async def get_database_metadata(self, include_types: Optional[List[str]] = None) -> Dict[str, Any]:
292
+ """
293
+ Gets the complete metadata of the database, including property options.
294
+
295
+ Args:
296
+ include_types: List of property types to include (if None, include all)
297
+
298
+ Returns:
299
+ Dict[str, Any]: The database metadata
300
+ """
301
+ await self._ensure_schema_loaded()
302
+
303
+ if not self._schema:
304
+ return {"properties": {}}
305
+
306
+ metadata = {"properties": {}}
307
+
308
+ for prop_name, prop_data in self._schema.items():
309
+ prop_type = prop_data.get("type")
310
+
311
+ # Skip if we're filtering and this type isn't included
312
+ if include_types and prop_type not in include_types:
313
+ continue
314
+
315
+ prop_metadata = {
316
+ "type": prop_type,
317
+ "options": []
318
+ }
319
+
320
+ # Include options for select, multi_select, status
321
+ if prop_type in ["select", "multi_select", "status"]:
322
+ prop_metadata["options"] = prop_data.get(prop_type, {}).get("options", [])
323
+
324
+ # For relation properties, we might want to include related database info
325
+ elif prop_type == "relation":
326
+ prop_metadata["relation_details"] = prop_data.get("relation", {})
327
+
328
+ metadata["properties"][prop_name] = prop_metadata
329
+
330
+ return metadata