notionary 0.1.6__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.
- 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} +0 -1
- 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/{uuid_utils.py → page_id_utils.py} +15 -0
- {notionary-0.1.6.dist-info → notionary-0.1.7.dist-info}/METADATA +1 -1
- {notionary-0.1.6.dist-info → notionary-0.1.7.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-0.1.6.dist-info → notionary-0.1.7.dist-info}/WHEEL +0 -0
- {notionary-0.1.6.dist-info → notionary-0.1.7.dist-info}/licenses/LICENSE +0 -0
- {notionary-0.1.6.dist-info → notionary-0.1.7.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,146 @@
|
|
1
|
+
from typing import Dict, Any, List, Optional
|
2
|
+
from notionary.core.notion_client import NotionClient
|
3
|
+
from notionary.core.page.metadata.metadata_editor import MetadataEditor
|
4
|
+
from notionary.core.page.properites.property_operation_result import PropertyOperationResult
|
5
|
+
from notionary.core.page.relations.notion_page_title_resolver import NotionPageTitleResolver
|
6
|
+
from notionary.core.page.properites.database_property_service import DatabasePropertyService
|
7
|
+
from notionary.core.page.relations.page_database_relation import PageDatabaseRelation
|
8
|
+
from notionary.core.page.properites.property_value_extractor import PropertyValueExtractor
|
9
|
+
from notionary.util.logging_mixin import LoggingMixin
|
10
|
+
|
11
|
+
class PagePropertyManager(LoggingMixin):
|
12
|
+
"""Verwaltet den Zugriff auf und die Änderung von Seiteneigenschaften."""
|
13
|
+
|
14
|
+
def __init__(self, page_id: str, client: NotionClient,
|
15
|
+
metadata_editor: MetadataEditor,
|
16
|
+
db_relation: PageDatabaseRelation):
|
17
|
+
self._page_id = page_id
|
18
|
+
self._client = client
|
19
|
+
self._page_data = None
|
20
|
+
self._metadata_editor = metadata_editor
|
21
|
+
self._db_relation = db_relation
|
22
|
+
self._db_property_service = None
|
23
|
+
|
24
|
+
self._extractor = PropertyValueExtractor(self.logger)
|
25
|
+
self._title_resolver = NotionPageTitleResolver(client)
|
26
|
+
|
27
|
+
async def get_properties(self) -> Dict[str, Any]:
|
28
|
+
"""Retrieves all properties of the page."""
|
29
|
+
page_data = await self._get_page_data()
|
30
|
+
if page_data and "properties" in page_data:
|
31
|
+
return page_data["properties"]
|
32
|
+
return {}
|
33
|
+
|
34
|
+
async def get_property_value(self, property_name: str, relation_getter=None) -> Any:
|
35
|
+
"""
|
36
|
+
Get the value of a specific property.
|
37
|
+
|
38
|
+
Args:
|
39
|
+
property_name: Name of the property to get
|
40
|
+
relation_getter: Optional callback function to get relation values
|
41
|
+
"""
|
42
|
+
properties = await self.get_properties()
|
43
|
+
if property_name not in properties:
|
44
|
+
return None
|
45
|
+
|
46
|
+
prop_data = properties[property_name]
|
47
|
+
return await self._extractor.extract(property_name, prop_data, relation_getter)
|
48
|
+
|
49
|
+
|
50
|
+
async def set_property_by_name(self, property_name: str, value: Any) -> PropertyOperationResult:
|
51
|
+
"""
|
52
|
+
Set a property value by name, automatically detecting the property type.
|
53
|
+
|
54
|
+
Args:
|
55
|
+
property_name: Name of the property
|
56
|
+
value: Value to set
|
57
|
+
|
58
|
+
Returns:
|
59
|
+
PropertyOperationResult: Result of the operation with status, error messages,
|
60
|
+
and available options if applicable
|
61
|
+
"""
|
62
|
+
property_type = await self.get_property_type(property_name)
|
63
|
+
|
64
|
+
if property_type == "relation":
|
65
|
+
result = PropertyOperationResult.from_relation_type_error(property_name, value)
|
66
|
+
self.logger.warning(result.error)
|
67
|
+
return result
|
68
|
+
|
69
|
+
if not await self._db_relation.is_database_page():
|
70
|
+
api_response = await self._metadata_editor.set_property_by_name(property_name, value)
|
71
|
+
if api_response:
|
72
|
+
await self.invalidate_cache()
|
73
|
+
return PropertyOperationResult.from_success(property_name, value, api_response)
|
74
|
+
return PropertyOperationResult.from_no_api_response(property_name, value)
|
75
|
+
|
76
|
+
db_service = await self._init_db_property_service()
|
77
|
+
|
78
|
+
if not db_service:
|
79
|
+
api_response = await self._metadata_editor.set_property_by_name(property_name, value)
|
80
|
+
if api_response:
|
81
|
+
await self.invalidate_cache()
|
82
|
+
return PropertyOperationResult.from_success(property_name, value, api_response)
|
83
|
+
return PropertyOperationResult.from_no_api_response(property_name, value)
|
84
|
+
|
85
|
+
is_valid, error_message, available_options = await db_service.validate_property_value(
|
86
|
+
property_name, value
|
87
|
+
)
|
88
|
+
|
89
|
+
if not is_valid:
|
90
|
+
if available_options:
|
91
|
+
options_str = "', '".join(available_options)
|
92
|
+
detailed_error = f"{error_message}\nAvailable options for '{property_name}': '{options_str}'"
|
93
|
+
self.logger.warning(detailed_error)
|
94
|
+
else:
|
95
|
+
self.logger.warning("%s\nNo valid options available for '%s'", error_message, property_name)
|
96
|
+
|
97
|
+
return PropertyOperationResult.from_error(
|
98
|
+
property_name,
|
99
|
+
error_message,
|
100
|
+
value,
|
101
|
+
available_options
|
102
|
+
)
|
103
|
+
|
104
|
+
api_response = await self._metadata_editor.set_property_by_name(property_name, value)
|
105
|
+
if api_response:
|
106
|
+
await self.invalidate_cache()
|
107
|
+
return PropertyOperationResult.from_success(property_name, value, api_response)
|
108
|
+
|
109
|
+
return PropertyOperationResult.from_no_api_response(property_name, value)
|
110
|
+
|
111
|
+
async def get_property_type(self, property_name: str) -> Optional[str]:
|
112
|
+
"""Gets the type of a specific property."""
|
113
|
+
db_service = await self._init_db_property_service()
|
114
|
+
if db_service:
|
115
|
+
return await db_service.get_property_type(property_name)
|
116
|
+
return None
|
117
|
+
|
118
|
+
async def get_available_options_for_property(self, property_name: str) -> List[str]:
|
119
|
+
"""Gets the available option names for a property."""
|
120
|
+
db_service = await self._init_db_property_service()
|
121
|
+
if db_service:
|
122
|
+
return await db_service.get_option_names(property_name)
|
123
|
+
return []
|
124
|
+
|
125
|
+
async def _get_page_data(self, force_refresh=False) -> Dict[str, Any]:
|
126
|
+
"""Gets the page data and caches it for future use."""
|
127
|
+
if self._page_data is None or force_refresh:
|
128
|
+
self._page_data = await self._client.get_page(self._page_id)
|
129
|
+
return self._page_data
|
130
|
+
|
131
|
+
async def invalidate_cache(self) -> None:
|
132
|
+
"""Forces a refresh of the cached page data on next access."""
|
133
|
+
self._page_data = None
|
134
|
+
|
135
|
+
async def _init_db_property_service(self) -> Optional[DatabasePropertyService]:
|
136
|
+
"""Lazily initializes the database property service if needed."""
|
137
|
+
if self._db_property_service is not None:
|
138
|
+
return self._db_property_service
|
139
|
+
|
140
|
+
database_id = await self._db_relation.get_parent_database_id()
|
141
|
+
if not database_id:
|
142
|
+
return None
|
143
|
+
|
144
|
+
self._db_property_service = DatabasePropertyService(database_id, self._client)
|
145
|
+
await self._db_property_service.load_schema()
|
146
|
+
return self._db_property_service
|
@@ -4,10 +4,9 @@ from notionary.util.logging_mixin import LoggingMixin
|
|
4
4
|
|
5
5
|
|
6
6
|
class NotionPropertyFormatter(LoggingMixin):
|
7
|
-
"""
|
7
|
+
"""Class for formatting Notion properties based on their type."""
|
8
8
|
|
9
9
|
def __init__(self):
|
10
|
-
# Mapping von Typen zu Formatierungsmethoden
|
11
10
|
self._formatters = {
|
12
11
|
"title": self.format_title,
|
13
12
|
"rich_text": self.format_rich_text,
|
@@ -24,74 +23,74 @@ class NotionPropertyFormatter(LoggingMixin):
|
|
24
23
|
}
|
25
24
|
|
26
25
|
def format_title(self, value: Any) -> Dict[str, Any]:
|
27
|
-
"""
|
26
|
+
"""Formats a title value."""
|
28
27
|
return {"title": [{"type": "text", "text": {"content": str(value)}}]}
|
29
28
|
|
30
29
|
def format_rich_text(self, value: Any) -> Dict[str, Any]:
|
31
|
-
"""
|
30
|
+
"""Formats a rich text value."""
|
32
31
|
return {"rich_text": [{"type": "text", "text": {"content": str(value)}}]}
|
33
32
|
|
34
33
|
def format_url(self, value: str) -> Dict[str, Any]:
|
35
|
-
"""
|
34
|
+
"""Formats a URL value."""
|
36
35
|
return {"url": value}
|
37
36
|
|
38
37
|
def format_email(self, value: str) -> Dict[str, Any]:
|
39
|
-
"""
|
38
|
+
"""Formats an email address."""
|
40
39
|
return {"email": value}
|
41
40
|
|
42
41
|
def format_phone_number(self, value: str) -> Dict[str, Any]:
|
43
|
-
"""
|
42
|
+
"""Formats a phone number."""
|
44
43
|
return {"phone_number": value}
|
45
44
|
|
46
45
|
def format_number(self, value: Any) -> Dict[str, Any]:
|
47
|
-
"""
|
46
|
+
"""Formats a numeric value."""
|
48
47
|
return {"number": float(value)}
|
49
48
|
|
50
49
|
def format_checkbox(self, value: Any) -> Dict[str, Any]:
|
51
|
-
"""
|
50
|
+
"""Formats a checkbox value."""
|
52
51
|
return {"checkbox": bool(value)}
|
53
52
|
|
54
53
|
def format_select(self, value: str) -> Dict[str, Any]:
|
55
|
-
"""
|
54
|
+
"""Formats a select value."""
|
56
55
|
return {"select": {"name": str(value)}}
|
57
56
|
|
58
57
|
def format_multi_select(self, value: Any) -> Dict[str, Any]:
|
59
|
-
"""
|
58
|
+
"""Formats a multi-select value."""
|
60
59
|
if isinstance(value, list):
|
61
60
|
return {"multi_select": [{"name": item} for item in value]}
|
62
61
|
return {"multi_select": [{"name": str(value)}]}
|
63
62
|
|
64
63
|
def format_date(self, value: Any) -> Dict[str, Any]:
|
65
|
-
"""
|
64
|
+
"""Formats a date value."""
|
66
65
|
if isinstance(value, dict) and "start" in value:
|
67
66
|
return {"date": value}
|
68
67
|
return {"date": {"start": str(value)}}
|
69
68
|
|
70
69
|
def format_status(self, value: str) -> Dict[str, Any]:
|
71
|
-
"""
|
70
|
+
"""Formats a status value."""
|
72
71
|
return {"status": {"name": str(value)}}
|
73
72
|
|
74
73
|
def format_relation(self, value: Any) -> Dict[str, Any]:
|
75
|
-
"""
|
74
|
+
"""Formats a relation value."""
|
76
75
|
if isinstance(value, list):
|
77
76
|
return {"relation": [{"id": item} for item in value]}
|
78
77
|
return {"relation": [{"id": str(value)}]}
|
79
78
|
|
80
79
|
def format_value(self, property_type: str, value: Any) -> Optional[Dict[str, Any]]:
|
81
80
|
"""
|
82
|
-
|
81
|
+
Formats a value according to the given Notion property type.
|
83
82
|
|
84
83
|
Args:
|
85
|
-
property_type: Notion
|
86
|
-
value:
|
84
|
+
property_type: Notion property type (e.g., "title", "rich_text", "status")
|
85
|
+
value: The value to be formatted
|
87
86
|
|
88
87
|
Returns:
|
89
|
-
|
88
|
+
A dictionary with the formatted value, or None if the type is unknown.
|
90
89
|
"""
|
91
90
|
formatter = self._formatters.get(property_type)
|
92
91
|
if not formatter:
|
93
92
|
if self.logger:
|
94
|
-
self.logger.warning("
|
93
|
+
self.logger.warning("Unknown property type: %s", property_type)
|
95
94
|
return None
|
96
95
|
|
97
|
-
return formatter(value)
|
96
|
+
return formatter(value)
|
@@ -0,0 +1,103 @@
|
|
1
|
+
from typing import Any, Dict, List, Optional
|
2
|
+
from dataclasses import dataclass
|
3
|
+
|
4
|
+
@dataclass
|
5
|
+
class PropertyOperationResult:
|
6
|
+
"""
|
7
|
+
Result of a property operation in Notion.
|
8
|
+
|
9
|
+
Attributes:
|
10
|
+
success: Whether the operation was successful
|
11
|
+
property_name: Name of the affected property
|
12
|
+
value: The value that was set or retrieved
|
13
|
+
error: Error message, if any
|
14
|
+
available_options: Available options for select-like properties
|
15
|
+
api_response: The original API response
|
16
|
+
"""
|
17
|
+
success: bool
|
18
|
+
property_name: str
|
19
|
+
value: Optional[Any] = None
|
20
|
+
error: Optional[str] = None
|
21
|
+
available_options: Optional[List[str]] = None
|
22
|
+
api_response: Optional[Dict[str, Any]] = None
|
23
|
+
|
24
|
+
# Common error messages
|
25
|
+
NO_API_RESPONSE = "Failed to set property (no API response)"
|
26
|
+
RELATION_TYPE_ERROR = "Property '{}' is of type 'relation'. Relations must be set using the RelationManager."
|
27
|
+
|
28
|
+
@classmethod
|
29
|
+
def from_success(cls, property_name: str, value: Any, api_response: Dict[str, Any]) -> "PropertyOperationResult":
|
30
|
+
"""Creates a success result."""
|
31
|
+
return cls(
|
32
|
+
success=True,
|
33
|
+
property_name=property_name,
|
34
|
+
value=value,
|
35
|
+
api_response=api_response
|
36
|
+
)
|
37
|
+
|
38
|
+
@classmethod
|
39
|
+
def from_error(cls, property_name: str,
|
40
|
+
error: str,
|
41
|
+
value: Optional[Any] = None,
|
42
|
+
available_options: Optional[List[str]] = None) -> "PropertyOperationResult":
|
43
|
+
"""Creates an error result."""
|
44
|
+
return cls(
|
45
|
+
success=False,
|
46
|
+
property_name=property_name,
|
47
|
+
value=value,
|
48
|
+
error=error,
|
49
|
+
available_options=available_options or []
|
50
|
+
)
|
51
|
+
|
52
|
+
@classmethod
|
53
|
+
def from_api_error(cls, property_name: str, api_response: Dict[str, Any]) -> "PropertyOperationResult":
|
54
|
+
"""Creates a result from an API error response."""
|
55
|
+
return cls(
|
56
|
+
success=False,
|
57
|
+
property_name=property_name,
|
58
|
+
error=api_response.get("message", "Unknown API error"),
|
59
|
+
api_response=api_response
|
60
|
+
)
|
61
|
+
|
62
|
+
@classmethod
|
63
|
+
def from_no_api_response(cls, property_name: str, value: Optional[Any] = None) -> "PropertyOperationResult":
|
64
|
+
"""Creates a standardized result for missing API responses."""
|
65
|
+
return cls.from_error(property_name, cls.NO_API_RESPONSE, value)
|
66
|
+
|
67
|
+
@classmethod
|
68
|
+
def from_relation_type_error(cls, property_name: str, value: Optional[Any] = None) -> "PropertyOperationResult":
|
69
|
+
"""Creates a standardized error result for relation type properties."""
|
70
|
+
error_msg = cls.RELATION_TYPE_ERROR.format(property_name)
|
71
|
+
return cls.from_error(property_name, error_msg, value)
|
72
|
+
|
73
|
+
def to_dict(self) -> Dict[str, Any]:
|
74
|
+
"""Converts the result to a dictionary."""
|
75
|
+
result = {
|
76
|
+
"success": self.success,
|
77
|
+
"property": self.property_name,
|
78
|
+
}
|
79
|
+
|
80
|
+
if self.value is not None:
|
81
|
+
result["value"] = self.value
|
82
|
+
|
83
|
+
if not self.success:
|
84
|
+
result["error"] = self.error
|
85
|
+
|
86
|
+
if self.available_options:
|
87
|
+
result["available_options"] = self.available_options
|
88
|
+
|
89
|
+
if self.api_response:
|
90
|
+
result["api_response"] = self.api_response
|
91
|
+
|
92
|
+
return result
|
93
|
+
|
94
|
+
def __str__(self) -> str:
|
95
|
+
"""String representation of the result."""
|
96
|
+
if self.success:
|
97
|
+
return f"Success: Property '{self.property_name}' set to '{self.value}'"
|
98
|
+
|
99
|
+
if self.available_options:
|
100
|
+
options = "', '".join(self.available_options)
|
101
|
+
return f"Error: {self.error}\nAvailable options for '{self.property_name}': '{options}'"
|
102
|
+
|
103
|
+
return f"Error: {self.error}"
|
@@ -0,0 +1,46 @@
|
|
1
|
+
import asyncio
|
2
|
+
from typing import Any, Awaitable, Callable
|
3
|
+
|
4
|
+
|
5
|
+
class PropertyValueExtractor:
|
6
|
+
def __init__(self, logger=None):
|
7
|
+
self.logger = logger
|
8
|
+
|
9
|
+
async def extract(
|
10
|
+
self,
|
11
|
+
property_name: str,
|
12
|
+
prop_data: dict,
|
13
|
+
relation_resolver: Callable[[str], Awaitable[Any]]
|
14
|
+
) -> Any:
|
15
|
+
prop_type = prop_data.get("type")
|
16
|
+
if not prop_type:
|
17
|
+
return None
|
18
|
+
|
19
|
+
handlers: dict[str, Callable[[], Awaitable[Any] | Any]] = {
|
20
|
+
"title": lambda: "".join(t.get("plain_text", "") for t in prop_data.get("title", [])),
|
21
|
+
"rich_text": lambda: "".join(t.get("plain_text", "") for t in prop_data.get("rich_text", [])),
|
22
|
+
"number": lambda: prop_data.get("number"),
|
23
|
+
"select": lambda: prop_data.get("select", {}).get("name") if prop_data.get("select") else None,
|
24
|
+
"multi_select": lambda: [o.get("name") for o in prop_data.get("multi_select", [])],
|
25
|
+
"status": lambda: prop_data.get("status", {}).get("name") if prop_data.get("status") else None,
|
26
|
+
"date": lambda: prop_data.get("date"),
|
27
|
+
"checkbox": lambda: prop_data.get("checkbox"),
|
28
|
+
"url": lambda: prop_data.get("url"),
|
29
|
+
"email": lambda: prop_data.get("email"),
|
30
|
+
"phone_number": lambda: prop_data.get("phone_number"),
|
31
|
+
"relation": lambda: relation_resolver(property_name),
|
32
|
+
"people": lambda: [p.get("id") for p in prop_data.get("people", [])],
|
33
|
+
"files": lambda: [
|
34
|
+
f.get("external", {}).get("url") if f.get("type") == "external" else f.get("name")
|
35
|
+
for f in prop_data.get("files", [])
|
36
|
+
]
|
37
|
+
}
|
38
|
+
|
39
|
+
handler = handlers.get(prop_type)
|
40
|
+
if handler is None:
|
41
|
+
if self.logger:
|
42
|
+
self.logger.warning(f"Unsupported property type: {prop_type}")
|
43
|
+
return None
|
44
|
+
|
45
|
+
result = handler()
|
46
|
+
return await result if asyncio.iscoroutine(result) else result
|