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
@@ -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
- """Klasse zur Formatierung von Notion-Eigenschaften nach Typ."""
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
- """Formatiert einen Titel-Wert."""
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
- """Formatiert einen Rich-Text-Wert."""
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
- """Formatiert eine URL."""
34
+ """Formats a URL value."""
36
35
  return {"url": value}
37
36
 
38
37
  def format_email(self, value: str) -> Dict[str, Any]:
39
- """Formatiert eine E-Mail-Adresse."""
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
- """Formatiert eine Telefonnummer."""
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
- """Formatiert eine Zahl."""
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
- """Formatiert einen Checkbox-Wert."""
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
- """Formatiert einen Select-Wert."""
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
- """Formatiert einen Multi-Select-Wert."""
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
- """Formatiert ein Datum."""
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
- """Formatiert einen Status-Wert."""
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
- """Formatiert einen Relations-Wert."""
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
- Formatiert einen Wert entsprechend des angegebenen Eigenschaftstyps.
81
+ Formats a value according to the given Notion property type.
83
82
 
84
83
  Args:
85
- property_type: Notion-Eigenschaftstyp (z.B. "title", "rich_text", "status")
86
- value: Der zu formatierende Wert
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
- Formatierter Wert als Dictionary oder None bei unbekanntem Typ
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("Unbekannter Eigenschaftstyp: %s", property_type)
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