notionary 0.1.6__py3-none-any.whl → 0.1.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/core/database/notion_database_manager.py +146 -232
- notionary/core/database/notion_database_manager_factory.py +9 -52
- notionary/core/database/notion_database_schema.py +1 -314
- notionary/core/notion_client.py +2 -10
- notionary/core/page/{page_content_manager.py → content/page_content_manager.py} +10 -6
- notionary/core/page/metadata/metadata_editor.py +109 -0
- notionary/core/page/metadata/notion_icon_manager.py +46 -0
- notionary/core/page/{meta_data/metadata_editor.py → metadata/notion_page_cover_manager.py} +20 -30
- notionary/core/page/notion_page_manager.py +218 -59
- notionary/core/page/properites/database_property_service.py +330 -0
- notionary/core/page/properites/page_property_manager.py +146 -0
- notionary/core/page/{property_formatter.py → properites/property_formatter.py} +19 -20
- notionary/core/page/properites/property_operation_result.py +103 -0
- notionary/core/page/properites/property_value_extractor.py +46 -0
- notionary/core/page/relations/notion_page_relation_manager.py +364 -0
- notionary/core/page/relations/notion_page_title_resolver.py +43 -0
- notionary/core/page/relations/page_database_relation.py +70 -0
- notionary/core/page/relations/relation_operation_result.py +135 -0
- notionary/util/page_id_utils.py +44 -0
- {notionary-0.1.6.dist-info → notionary-0.1.8.dist-info}/METADATA +1 -1
- {notionary-0.1.6.dist-info → notionary-0.1.8.dist-info}/RECORD +24 -18
- notionary/core/database/database_query_service.py +0 -73
- notionary/core/database/database_schema_service.py +0 -57
- notionary/core/database/notion_database_writer.py +0 -390
- notionary/core/database/page_service.py +0 -161
- notionary/util/uuid_utils.py +0 -24
- {notionary-0.1.6.dist-info → notionary-0.1.8.dist-info}/WHEEL +0 -0
- {notionary-0.1.6.dist-info → notionary-0.1.8.dist-info}/licenses/LICENSE +0 -0
- {notionary-0.1.6.dist-info → notionary-0.1.8.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,5 @@
|
|
1
|
-
|
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.
|
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.
|
12
|
-
from notionary.
|
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
|
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
|
-
|
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=
|
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(
|
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
|
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,66 +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.
|
124
|
+
return await self._page_icon_manager.set_icon(emoji, external_url)
|
108
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
|
+
|
109
138
|
async def get_cover_url(self) -> str:
|
110
|
-
|
111
|
-
|
112
|
-
if not page_data:
|
113
|
-
return ""
|
114
|
-
|
115
|
-
return page_data.get("cover", {}).get("external", {}).get("url", "")
|
139
|
+
return await self._page_cover_manager.get_cover_url()
|
116
140
|
|
117
141
|
async def set_page_cover(self, external_url: str) -> Optional[Dict[str, Any]]:
|
118
|
-
return await self.
|
142
|
+
return await self._page_cover_manager.set_cover(external_url)
|
119
143
|
|
120
144
|
async def set_random_gradient_cover(self) -> Optional[Dict[str, Any]]:
|
121
|
-
return await self.
|
145
|
+
return await self._page_cover_manager.set_random_gradient_cover()
|
122
146
|
|
123
147
|
async def get_properties(self) -> Dict[str, Any]:
|
124
|
-
"""Retrieves all properties of the page"""
|
125
|
-
|
126
|
-
if page_data and "properties" in page_data:
|
127
|
-
return page_data["properties"]
|
128
|
-
return {}
|
148
|
+
"""Retrieves all properties of the page."""
|
149
|
+
return await self._property_manager.get_properties()
|
129
150
|
|
130
|
-
async def
|
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.
|
131
168
|
"""
|
132
|
-
|
169
|
+
return await self._db_relation.is_database_page()
|
133
170
|
|
134
|
-
|
135
|
-
|
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.
|
136
186
|
"""
|
137
|
-
|
138
|
-
if
|
139
|
-
return
|
187
|
+
db_service = await self._get_db_property_service()
|
188
|
+
if db_service:
|
189
|
+
return await db_service.get_property_type(property_name)
|
140
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.
|
194
|
+
"""
|
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")
|
141
230
|
|
142
231
|
|
143
|
-
|
232
|
+
|
233
|
+
async def main():
|
234
|
+
"""
|
235
|
+
Demonstriert die Verwendung des refactorierten NotionPageManager.
|
236
|
+
"""
|
237
|
+
print("=== NotionPageManager Demo ===")
|
238
|
+
|
144
239
|
page_manager = NotionPageManager(page_id="https://notion.so/1d0389d57bd3805cb34ccaf5804b43ce")
|
145
|
-
cover_url = await page_manager.get_cover_url()
|
146
|
-
print(f"Cover URL: {cover_url}")
|
147
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}")
|
148
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
|
+
|
149
308
|
if __name__ == "__main__":
|
150
|
-
import asyncio
|
151
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
|