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,75 +1,12 @@
|
|
1
1
|
from typing import (
|
2
2
|
AsyncGenerator,
|
3
3
|
Dict,
|
4
|
-
List,
|
5
4
|
Optional,
|
6
5
|
Any,
|
7
|
-
TypedDict,
|
8
|
-
Union,
|
9
|
-
cast,
|
10
|
-
Literal,
|
11
6
|
)
|
12
7
|
from notionary.core.notion_client import NotionClient
|
13
|
-
from notionary.core.page.notion_page_manager import NotionPageManager
|
14
8
|
from notionary.util.logging_mixin import LoggingMixin
|
15
9
|
|
16
|
-
|
17
|
-
class NotionTextContent(TypedDict):
|
18
|
-
plain_text: str
|
19
|
-
|
20
|
-
|
21
|
-
class NotionTitleProperty(TypedDict):
|
22
|
-
type: Literal["title"]
|
23
|
-
title: List[NotionTextContent]
|
24
|
-
|
25
|
-
|
26
|
-
class NotionSelectOption(TypedDict):
|
27
|
-
name: str
|
28
|
-
id: Optional[str]
|
29
|
-
color: Optional[str]
|
30
|
-
|
31
|
-
|
32
|
-
class NotionSelectProperty(TypedDict):
|
33
|
-
type: Literal["select"]
|
34
|
-
select: Dict[str, List[NotionSelectOption]]
|
35
|
-
|
36
|
-
|
37
|
-
class NotionMultiSelectProperty(TypedDict):
|
38
|
-
type: Literal["multi_select"]
|
39
|
-
multi_select: Dict[str, List[NotionSelectOption]]
|
40
|
-
|
41
|
-
|
42
|
-
class NotionStatusProperty(TypedDict):
|
43
|
-
type: Literal["status"]
|
44
|
-
status: Dict[str, List[NotionSelectOption]]
|
45
|
-
|
46
|
-
|
47
|
-
class NotionRelationProperty(TypedDict):
|
48
|
-
type: Literal["relation"]
|
49
|
-
relation: Dict[str, str]
|
50
|
-
|
51
|
-
|
52
|
-
class NotionNumberProperty(TypedDict):
|
53
|
-
type: Literal["number"]
|
54
|
-
number: Dict[str, Any]
|
55
|
-
|
56
|
-
|
57
|
-
NotionPropertyType = Union[
|
58
|
-
NotionTitleProperty,
|
59
|
-
NotionSelectProperty,
|
60
|
-
NotionMultiSelectProperty,
|
61
|
-
NotionStatusProperty,
|
62
|
-
NotionRelationProperty,
|
63
|
-
NotionNumberProperty,
|
64
|
-
Dict[str, Any], # Fallback
|
65
|
-
]
|
66
|
-
|
67
|
-
|
68
|
-
class RelationOption(TypedDict):
|
69
|
-
id: str
|
70
|
-
title: str
|
71
|
-
|
72
|
-
|
73
10
|
class NotionDatabaseAccessor(LoggingMixin):
|
74
11
|
"""
|
75
12
|
A utility class that provides methods to access Notion databases.
|
@@ -164,254 +101,4 @@ class NotionDatabaseAccessor(LoggingMixin):
|
|
164
101
|
if title_parts:
|
165
102
|
title = "".join(title_parts)
|
166
103
|
|
167
|
-
return title
|
168
|
-
|
169
|
-
|
170
|
-
class NotionDatabaseSchema:
|
171
|
-
"""
|
172
|
-
Represents the schema of a specific Notion database.
|
173
|
-
Manages property information, options, and relations for a single database.
|
174
|
-
"""
|
175
|
-
|
176
|
-
def __init__(self, database_id: str, client: NotionClient) -> None:
|
177
|
-
"""
|
178
|
-
Initialize a database schema handler for a specific database.
|
179
|
-
|
180
|
-
Args:
|
181
|
-
database_id: The ID of the database
|
182
|
-
client: An instance of NotionClient for API requests
|
183
|
-
"""
|
184
|
-
self.database_id: str = database_id
|
185
|
-
self._client: NotionClient = client
|
186
|
-
self._properties: Dict[str, NotionPropertyType] = {}
|
187
|
-
self._loaded: bool = False
|
188
|
-
|
189
|
-
async def load(self) -> bool:
|
190
|
-
"""
|
191
|
-
Load the database schema from the Notion API.
|
192
|
-
|
193
|
-
Returns:
|
194
|
-
True if the schema was loaded successfully, False otherwise
|
195
|
-
"""
|
196
|
-
if self._loaded:
|
197
|
-
return True
|
198
|
-
|
199
|
-
db_details = await self._client.get(f"databases/{self.database_id}")
|
200
|
-
if not db_details or "properties" not in db_details:
|
201
|
-
return False
|
202
|
-
|
203
|
-
self._properties = db_details["properties"]
|
204
|
-
self._loaded = True
|
205
|
-
return True
|
206
|
-
|
207
|
-
async def get_property_types(self) -> Dict[str, str]:
|
208
|
-
"""
|
209
|
-
Get a mapping of property names to their types.
|
210
|
-
|
211
|
-
Returns:
|
212
|
-
A dictionary mapping property names to types
|
213
|
-
"""
|
214
|
-
if not self._loaded:
|
215
|
-
await self.load()
|
216
|
-
|
217
|
-
return {name: prop.get("type", "") for name, prop in self._properties.items()}
|
218
|
-
|
219
|
-
async def get_select_options(self, property_name: str) -> List[NotionSelectOption]:
|
220
|
-
"""
|
221
|
-
Get the options for a select, multi_select, or status property.
|
222
|
-
|
223
|
-
Args:
|
224
|
-
property_name: The name of the property
|
225
|
-
|
226
|
-
Returns:
|
227
|
-
A list of option objects
|
228
|
-
"""
|
229
|
-
if not self._loaded:
|
230
|
-
await self.load()
|
231
|
-
|
232
|
-
if property_name not in self._properties:
|
233
|
-
return []
|
234
|
-
|
235
|
-
prop = self._properties[property_name]
|
236
|
-
prop_type = prop.get("type", "")
|
237
|
-
|
238
|
-
if prop_type not in ["select", "multi_select", "status"]:
|
239
|
-
return []
|
240
|
-
|
241
|
-
if prop_type in prop and "options" in prop[prop_type]:
|
242
|
-
return cast(List[NotionSelectOption], prop[prop_type]["options"])
|
243
|
-
|
244
|
-
return []
|
245
|
-
|
246
|
-
async def get_relation_options(
|
247
|
-
self, property_name: str, limit: int = 100
|
248
|
-
) -> List[RelationOption]:
|
249
|
-
"""
|
250
|
-
Get available options for a relation property (pages in the related database).
|
251
|
-
|
252
|
-
Args:
|
253
|
-
property_name: The name of the relation property
|
254
|
-
limit: Maximum number of options to retrieve
|
255
|
-
|
256
|
-
Returns:
|
257
|
-
List of options with id and title
|
258
|
-
"""
|
259
|
-
related_db_id = await self.get_relation_database_id(property_name)
|
260
|
-
if not related_db_id:
|
261
|
-
return []
|
262
|
-
|
263
|
-
pages = await self._query_database_pages(related_db_id, limit)
|
264
|
-
return self._extract_page_titles_and_ids(pages)
|
265
|
-
|
266
|
-
async def get_relation_database_id(self, property_name: str) -> Optional[str]:
|
267
|
-
"""
|
268
|
-
Get the ID of the related database for a relation property.
|
269
|
-
|
270
|
-
Args:
|
271
|
-
property_name: The name of the property
|
272
|
-
|
273
|
-
Returns:
|
274
|
-
The ID of the related database or None
|
275
|
-
"""
|
276
|
-
if not self._loaded:
|
277
|
-
await self.load()
|
278
|
-
|
279
|
-
if property_name not in self._properties:
|
280
|
-
return None
|
281
|
-
|
282
|
-
prop = self._properties[property_name]
|
283
|
-
prop_type = prop.get("type", "")
|
284
|
-
|
285
|
-
if prop_type != "relation" or "relation" not in prop:
|
286
|
-
return None
|
287
|
-
|
288
|
-
relation_prop = cast(NotionRelationProperty, prop)
|
289
|
-
return relation_prop["relation"].get("database_id")
|
290
|
-
|
291
|
-
def _extract_page_titles_and_ids(
|
292
|
-
self, pages: List[NotionPageManager]
|
293
|
-
) -> List[RelationOption]:
|
294
|
-
"""
|
295
|
-
Extract titles and IDs from page objects.
|
296
|
-
|
297
|
-
Args:
|
298
|
-
pages: List of page objects from the Notion API
|
299
|
-
|
300
|
-
Returns:
|
301
|
-
List of dictionaries with id and title for each page
|
302
|
-
"""
|
303
|
-
options: List[RelationOption] = []
|
304
|
-
|
305
|
-
for page_manager in pages:
|
306
|
-
page_title = page_manager.title or "Untitled"
|
307
|
-
options.append({"id": page_manager.page_id, "title": page_title})
|
308
|
-
|
309
|
-
return options
|
310
|
-
|
311
|
-
async def _query_database_pages(
|
312
|
-
self, database_id: str, limit: int = 100
|
313
|
-
) -> List[Dict[str, Any]]:
|
314
|
-
"""
|
315
|
-
Returns:
|
316
|
-
List of page objects from the Notion API
|
317
|
-
"""
|
318
|
-
pages: List[Dict[str, Any]] = []
|
319
|
-
count = 0
|
320
|
-
|
321
|
-
async for page in self.iter_database_pages(
|
322
|
-
database_id=database_id, page_size=min(limit, 100)
|
323
|
-
):
|
324
|
-
pages.append(page)
|
325
|
-
count += 1
|
326
|
-
|
327
|
-
if count >= limit:
|
328
|
-
break
|
329
|
-
|
330
|
-
return pages
|
331
|
-
|
332
|
-
async def iter_database_pages(
|
333
|
-
self,
|
334
|
-
database_id: Optional[str] = None,
|
335
|
-
page_size: int = 100,
|
336
|
-
filter_conditions: Optional[Dict[str, Any]] = None,
|
337
|
-
sorts: Optional[List[Dict[str, Any]]] = None,
|
338
|
-
) -> AsyncGenerator[NotionPageManager, None]:
|
339
|
-
"""
|
340
|
-
Asynchronous generator that yields pages from a Notion database one by one.
|
341
|
-
|
342
|
-
Uses the Notion API to provide paginated access to all pages in a database
|
343
|
-
without loading all of them into memory at once.
|
344
|
-
|
345
|
-
Args:
|
346
|
-
database_id: The ID of the database to query (uses self.database_id if None)
|
347
|
-
page_size: The number of pages to fetch per request
|
348
|
-
filter_conditions: Optional filter to apply to the database query
|
349
|
-
sorts: Optional sort instructions for the database query
|
350
|
-
|
351
|
-
Yields:
|
352
|
-
Individual page objects from the Notion API
|
353
|
-
"""
|
354
|
-
db_id = database_id or self.database_id
|
355
|
-
if not db_id:
|
356
|
-
raise ValueError("No database ID provided")
|
357
|
-
|
358
|
-
start_cursor: Optional[str] = None
|
359
|
-
has_more = True
|
360
|
-
|
361
|
-
body: Dict[str, Any] = {"page_size": page_size}
|
362
|
-
|
363
|
-
if filter_conditions:
|
364
|
-
body["filter"] = filter_conditions
|
365
|
-
|
366
|
-
if sorts:
|
367
|
-
body["sorts"] = sorts
|
368
|
-
|
369
|
-
while has_more:
|
370
|
-
current_body = body.copy()
|
371
|
-
if start_cursor:
|
372
|
-
current_body["start_cursor"] = start_cursor
|
373
|
-
|
374
|
-
result = await self._client.post(
|
375
|
-
f"databases/{db_id}/query", data=current_body
|
376
|
-
)
|
377
|
-
|
378
|
-
if not result or "results" not in result:
|
379
|
-
return
|
380
|
-
|
381
|
-
for page in result["results"]:
|
382
|
-
page_id: str = page.get("id", "")
|
383
|
-
title = self._extract_page_title(page)
|
384
|
-
|
385
|
-
page_url = f"https://notion.so/{page_id.replace('-', '')}"
|
386
|
-
|
387
|
-
notion_page_manager = NotionPageManager(page_id=page_id, title=title, url=page_url)
|
388
|
-
yield notion_page_manager
|
389
|
-
|
390
|
-
has_more = result.get("has_more", False)
|
391
|
-
start_cursor = result.get("next_cursor") if has_more else None
|
392
|
-
|
393
|
-
def _extract_page_title(self, page: Dict[str, Any]) -> str:
|
394
|
-
"""
|
395
|
-
Extracts the title from a Notion page object.
|
396
|
-
|
397
|
-
Args:
|
398
|
-
page: The Notion page object
|
399
|
-
|
400
|
-
Returns:
|
401
|
-
The extracted title as a string, or an empty string if no title found
|
402
|
-
"""
|
403
|
-
properties = page.get("properties", {})
|
404
|
-
if not properties:
|
405
|
-
return ""
|
406
|
-
|
407
|
-
for prop_value in properties.values():
|
408
|
-
if prop_value.get("type") != "title":
|
409
|
-
continue
|
410
|
-
|
411
|
-
title_array = prop_value.get("title", [])
|
412
|
-
if not title_array:
|
413
|
-
continue
|
414
|
-
|
415
|
-
return title_array[0].get("plain_text", "")
|
416
|
-
|
417
|
-
return ""
|
104
|
+
return title
|
notionary/core/notion_client.py
CHANGED
@@ -1,12 +1,11 @@
|
|
1
1
|
import asyncio
|
2
2
|
import os
|
3
|
+
import weakref
|
3
4
|
from enum import Enum
|
4
5
|
from typing import Dict, Any, Optional, Union
|
5
6
|
import httpx
|
6
7
|
from dotenv import load_dotenv
|
7
8
|
from notionary.util.logging_mixin import LoggingMixin
|
8
|
-
import weakref
|
9
|
-
|
10
9
|
|
11
10
|
class HttpMethod(Enum):
|
12
11
|
"""Enum für HTTP-Methoden."""
|
@@ -16,7 +15,6 @@ class HttpMethod(Enum):
|
|
16
15
|
PATCH = "patch"
|
17
16
|
DELETE = "delete"
|
18
17
|
|
19
|
-
|
20
18
|
class NotionClient(LoggingMixin):
|
21
19
|
"""Verbesserter Notion-Client mit automatischer Ressourcenverwaltung."""
|
22
20
|
|
@@ -113,12 +111,6 @@ class NotionClient(LoggingMixin):
|
|
113
111
|
return None
|
114
112
|
|
115
113
|
def __del__(self):
|
116
|
-
"""
|
117
|
-
Destruktor, der beim Garbage Collecting aufgerufen wird.
|
118
|
-
|
119
|
-
Hinweis: Dies ist nur ein Fallback, da __del__ nicht garantiert für async Cleanup funktioniert.
|
120
|
-
Die bessere Praxis ist, close() explizit zu rufen, wenn möglich.
|
121
|
-
"""
|
122
114
|
if not hasattr(self, "client") or not self.client:
|
123
115
|
return
|
124
116
|
|
@@ -134,4 +126,4 @@ class NotionClient(LoggingMixin):
|
|
134
126
|
loop.create_task(self.close())
|
135
127
|
self.logger.debug("Created cleanup task for NotionClient")
|
136
128
|
except RuntimeError:
|
137
|
-
self.logger.warning("No event loop available for auto-closing NotionClient")
|
129
|
+
self.logger.warning("No event loop available for auto-closing NotionClient")
|
@@ -48,26 +48,30 @@ class PageContentManager(LoggingMixin):
|
|
48
48
|
return "No content to delete."
|
49
49
|
|
50
50
|
deleted = 0
|
51
|
-
|
52
|
-
|
51
|
+
skipped = 0
|
52
|
+
for block in results:
|
53
|
+
if block.get("type") in ["child_database", "database", "linked_database"]:
|
54
|
+
skipped += 1
|
55
|
+
continue
|
56
|
+
|
57
|
+
if await self._client.delete(f"blocks/{block['id']}"):
|
53
58
|
deleted += 1
|
54
59
|
|
55
|
-
return f"Deleted {deleted}/{len(results)} blocks."
|
60
|
+
return f"Deleted {deleted}/{len(results)} blocks. Skipped {skipped} database blocks."
|
56
61
|
|
57
|
-
# Methods from PageContentReader
|
58
62
|
async def get_blocks(self) -> List[Dict[str, Any]]:
|
59
63
|
result = await self._client.get(f"blocks/{self.page_id}/children")
|
60
64
|
if not result:
|
61
65
|
self.logger.error("Error retrieving page content: %s", result.error)
|
62
66
|
return []
|
63
|
-
return result.
|
67
|
+
return result.get("results", [])
|
64
68
|
|
65
69
|
async def get_block_children(self, block_id: str) -> List[Dict[str, Any]]:
|
66
70
|
result = await self._client.get(f"blocks/{block_id}/children")
|
67
71
|
if not result:
|
68
72
|
self.logger.error("Error retrieving block children: %s", result.error)
|
69
73
|
return []
|
70
|
-
return result.
|
74
|
+
return result.get("results", [])
|
71
75
|
|
72
76
|
async def get_page_blocks_with_children(self) -> List[Dict[str, Any]]:
|
73
77
|
blocks = await self.get_blocks()
|
@@ -0,0 +1,109 @@
|
|
1
|
+
from typing import Any, Dict, Optional
|
2
|
+
from notionary.core.notion_client import NotionClient
|
3
|
+
from notionary.core.page.properites.property_formatter import NotionPropertyFormatter
|
4
|
+
from notionary.util.logging_mixin import LoggingMixin
|
5
|
+
|
6
|
+
|
7
|
+
class MetadataEditor(LoggingMixin):
|
8
|
+
def __init__(self, page_id: str, client: NotionClient):
|
9
|
+
self.page_id = page_id
|
10
|
+
self._client = client
|
11
|
+
self._property_formatter = NotionPropertyFormatter()
|
12
|
+
|
13
|
+
async def set_title(self, title: str) -> Optional[Dict[str, Any]]:
|
14
|
+
return await self._client.patch(
|
15
|
+
f"pages/{self.page_id}",
|
16
|
+
{
|
17
|
+
"properties": {
|
18
|
+
"title": {"title": [{"type": "text", "text": {"content": title}}]}
|
19
|
+
}
|
20
|
+
},
|
21
|
+
)
|
22
|
+
|
23
|
+
async def set_property(self, property_name: str, property_value: Any, property_type: str) -> Optional[Dict[str, Any]]:
|
24
|
+
"""
|
25
|
+
Generic method to set any property on a Notion page.
|
26
|
+
|
27
|
+
Args:
|
28
|
+
property_name: The name of the property in Notion
|
29
|
+
property_value: The value to set
|
30
|
+
property_type: The type of property ('select', 'multi_select', 'status', 'relation', etc.)
|
31
|
+
|
32
|
+
Returns:
|
33
|
+
Optional[Dict[str, Any]]: The API response or None if the operation fails
|
34
|
+
"""
|
35
|
+
property_payload = self._property_formatter.format_value(property_type, property_value)
|
36
|
+
|
37
|
+
if not property_payload:
|
38
|
+
self.logger.warning("Could not create payload for property type: %s", property_type)
|
39
|
+
return None
|
40
|
+
|
41
|
+
return await self._client.patch(
|
42
|
+
f"pages/{self.page_id}",
|
43
|
+
{
|
44
|
+
"properties": {
|
45
|
+
property_name: property_payload
|
46
|
+
}
|
47
|
+
},
|
48
|
+
)
|
49
|
+
|
50
|
+
|
51
|
+
async def get_property_schema(self) -> Dict[str, Dict[str, Any]]:
|
52
|
+
"""
|
53
|
+
Retrieves the schema for all properties of the page.
|
54
|
+
|
55
|
+
Returns:
|
56
|
+
Dict[str, Dict[str, Any]]: A dictionary mapping property names to their schema
|
57
|
+
"""
|
58
|
+
page_data = await self._client.get_page(self.page_id)
|
59
|
+
property_schema = {}
|
60
|
+
|
61
|
+
if not page_data or "properties" not in page_data:
|
62
|
+
return property_schema
|
63
|
+
|
64
|
+
for prop_name, prop_data in page_data["properties"].items():
|
65
|
+
prop_type = prop_data.get("type")
|
66
|
+
property_schema[prop_name] = {
|
67
|
+
"id": prop_data.get("id"),
|
68
|
+
"type": prop_type,
|
69
|
+
"name": prop_name
|
70
|
+
}
|
71
|
+
|
72
|
+
try:
|
73
|
+
if prop_type == "select" and "select" in prop_data:
|
74
|
+
# Make sure prop_data["select"] is a dictionary before calling .get()
|
75
|
+
if isinstance(prop_data["select"], dict):
|
76
|
+
property_schema[prop_name]["options"] = prop_data["select"].get("options", [])
|
77
|
+
elif prop_type == "multi_select" and "multi_select" in prop_data:
|
78
|
+
# Make sure prop_data["multi_select"] is a dictionary before calling .get()
|
79
|
+
if isinstance(prop_data["multi_select"], dict):
|
80
|
+
property_schema[prop_name]["options"] = prop_data["multi_select"].get("options", [])
|
81
|
+
elif prop_type == "status" and "status" in prop_data:
|
82
|
+
# Make sure prop_data["status"] is a dictionary before calling .get()
|
83
|
+
if isinstance(prop_data["status"], dict):
|
84
|
+
property_schema[prop_name]["options"] = prop_data["status"].get("options", [])
|
85
|
+
except Exception as e:
|
86
|
+
if hasattr(self, 'logger') and self.logger:
|
87
|
+
self.logger.warning("Error processing property schema for '%s': %s", prop_name, e)
|
88
|
+
|
89
|
+
return property_schema
|
90
|
+
|
91
|
+
async def set_property_by_name(self, property_name: str, value: Any) -> Optional[Dict[str, Any]]:
|
92
|
+
"""
|
93
|
+
Sets a property value based on the property name, automatically detecting the property type.
|
94
|
+
|
95
|
+
Args:
|
96
|
+
property_name: The name of the property in Notion
|
97
|
+
value: The value to set
|
98
|
+
|
99
|
+
Returns:
|
100
|
+
Optional[Dict[str, Any]]: The API response or None if the operation fails
|
101
|
+
"""
|
102
|
+
property_schema = await self.get_property_schema()
|
103
|
+
|
104
|
+
if property_name not in property_schema:
|
105
|
+
self.logger.warning("Property '%s' not found in database schema", property_name)
|
106
|
+
return None
|
107
|
+
|
108
|
+
property_type = property_schema[property_name]["type"]
|
109
|
+
return await self.set_property(property_name, value, property_type)
|
@@ -0,0 +1,46 @@
|
|
1
|
+
from typing import Any, Dict, Optional
|
2
|
+
|
3
|
+
from notionary.core.notion_client import NotionClient
|
4
|
+
from notionary.util.logging_mixin import LoggingMixin
|
5
|
+
|
6
|
+
class NotionPageIconManager(LoggingMixin):
|
7
|
+
def __init__(self, page_id: str, client: NotionClient):
|
8
|
+
self.page_id = page_id
|
9
|
+
self._client = client
|
10
|
+
|
11
|
+
async def set_icon(
|
12
|
+
self, emoji: Optional[str] = None, external_url: Optional[str] = None
|
13
|
+
) -> Optional[Dict[str, Any]]:
|
14
|
+
if emoji:
|
15
|
+
icon = {"type": "emoji", "emoji": emoji}
|
16
|
+
elif external_url:
|
17
|
+
icon = {"type": "external", "external": {"url": external_url}}
|
18
|
+
else:
|
19
|
+
return None
|
20
|
+
|
21
|
+
return await self._client.patch(f"pages/{self.page_id}", {"icon": icon})
|
22
|
+
|
23
|
+
|
24
|
+
async def get_icon(self) -> Optional[str]:
|
25
|
+
"""
|
26
|
+
Retrieves the page icon - either emoji or external URL.
|
27
|
+
|
28
|
+
Returns:
|
29
|
+
str: Emoji character or URL if set, None if no icon
|
30
|
+
"""
|
31
|
+
page_data = await self._client.get_page(self.page_id)
|
32
|
+
|
33
|
+
if not page_data or "icon" not in page_data:
|
34
|
+
return None
|
35
|
+
|
36
|
+
icon_data = page_data.get("icon", {})
|
37
|
+
icon_type = icon_data.get("type")
|
38
|
+
|
39
|
+
if icon_type == "emoji":
|
40
|
+
return icon_data.get("emoji")
|
41
|
+
elif icon_type == "external":
|
42
|
+
return icon_data.get("external", {}).get("url")
|
43
|
+
|
44
|
+
return None
|
45
|
+
|
46
|
+
|
@@ -1,48 +1,25 @@
|
|
1
|
+
|
1
2
|
import random
|
2
3
|
from typing import Any, Dict, Optional
|
3
4
|
from notionary.core.notion_client import NotionClient
|
4
5
|
from notionary.util.logging_mixin import LoggingMixin
|
5
6
|
|
6
|
-
|
7
|
-
class MetadataEditor(LoggingMixin):
|
7
|
+
class NotionPageCoverManager(LoggingMixin):
|
8
8
|
def __init__(self, page_id: str, client: NotionClient):
|
9
9
|
self.page_id = page_id
|
10
10
|
self._client = client
|
11
|
-
|
12
|
-
async def set_title(self, title: str) -> Optional[Dict[str, Any]]:
|
13
|
-
return await self._client.patch(
|
14
|
-
f"pages/{self.page_id}",
|
15
|
-
{
|
16
|
-
"properties": {
|
17
|
-
"title": {"title": [{"type": "text", "text": {"content": title}}]}
|
18
|
-
}
|
19
|
-
},
|
20
|
-
)
|
21
|
-
|
22
|
-
async def set_icon(
|
23
|
-
self, emoji: Optional[str] = None, external_url: Optional[str] = None
|
24
|
-
) -> Optional[Dict[str, Any]]:
|
25
|
-
if emoji:
|
26
|
-
icon = {"type": "emoji", "emoji": emoji}
|
27
|
-
elif external_url:
|
28
|
-
icon = {"type": "external", "external": {"url": external_url}}
|
29
|
-
else:
|
30
|
-
return None
|
31
|
-
|
32
|
-
return await self._client.patch(f"pages/{self.page_id}", {"icon": icon})
|
33
|
-
|
11
|
+
|
34
12
|
async def set_cover(self, external_url: str) -> Optional[Dict[str, Any]]:
|
13
|
+
"""Sets a cover image from an external URL.
|
14
|
+
"""
|
15
|
+
|
35
16
|
return await self._client.patch(
|
36
17
|
f"pages/{self.page_id}",
|
37
18
|
{"cover": {"type": "external", "external": {"url": external_url}}},
|
38
19
|
)
|
39
20
|
|
40
21
|
async def set_random_gradient_cover(self) -> Optional[Dict[str, Any]]:
|
41
|
-
"""
|
42
|
-
Sets a random gradient cover from Notion's default gradient covers.
|
43
|
-
|
44
|
-
Returns:
|
45
|
-
Optional[Dict[str, Any]]: The API response or None if the operation fails
|
22
|
+
""" Sets a random gradient cover from Notion's default gradient covers.
|
46
23
|
"""
|
47
24
|
default_notion_covers = [
|
48
25
|
"https://www.notion.so/images/page-cover/gradients_8.png",
|
@@ -56,3 +33,16 @@ class MetadataEditor(LoggingMixin):
|
|
56
33
|
random_cover_url = random.choice(default_notion_covers)
|
57
34
|
|
58
35
|
return await self.set_cover(random_cover_url)
|
36
|
+
|
37
|
+
|
38
|
+
async def get_cover_url(self) -> str:
|
39
|
+
"""Retrieves the current cover image URL of the page.
|
40
|
+
"""
|
41
|
+
|
42
|
+
page_data = await self._client.get_page(self.page_id)
|
43
|
+
|
44
|
+
if not page_data:
|
45
|
+
return ""
|
46
|
+
|
47
|
+
return page_data.get("cover", {}).get("external", {}).get("url", "")
|
48
|
+
|