notionary 0.2.16__py3-none-any.whl → 0.2.18__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/__init__.py +10 -5
- notionary/base_notion_client.py +18 -7
- notionary/blocks/__init__.py +55 -24
- notionary/blocks/audio/__init__.py +7 -0
- notionary/blocks/audio/audio_element.py +152 -0
- notionary/blocks/audio/audio_markdown_node.py +29 -0
- notionary/blocks/audio/audio_models.py +59 -0
- notionary/blocks/bookmark/__init__.py +7 -0
- notionary/blocks/{bookmark_element.py → bookmark/bookmark_element.py} +20 -65
- notionary/blocks/bookmark/bookmark_markdown_node.py +43 -0
- notionary/blocks/bulleted_list/__init__.py +7 -0
- notionary/blocks/{bulleted_list_element.py → bulleted_list/bulleted_list_element.py} +7 -3
- notionary/blocks/bulleted_list/bulleted_list_markdown_node.py +33 -0
- notionary/blocks/bulleted_list/bulleted_list_models.py +0 -0
- notionary/blocks/callout/__init__.py +7 -0
- notionary/blocks/callout/callout_element.py +132 -0
- notionary/blocks/callout/callout_markdown_node.py +31 -0
- notionary/blocks/callout/callout_models.py +0 -0
- notionary/blocks/code/__init__.py +7 -0
- notionary/blocks/{code_block_element.py → code/code_element.py} +72 -40
- notionary/blocks/code/code_markdown_node.py +43 -0
- notionary/blocks/code/code_models.py +0 -0
- notionary/blocks/column/__init__.py +5 -0
- notionary/blocks/{column_element.py → column/column_element.py} +24 -55
- notionary/blocks/column/column_models.py +0 -0
- notionary/blocks/divider/__init__.py +7 -0
- notionary/blocks/{divider_element.py → divider/divider_element.py} +11 -3
- notionary/blocks/divider/divider_markdown_node.py +24 -0
- notionary/blocks/divider/divider_models.py +0 -0
- notionary/blocks/document/__init__.py +7 -0
- notionary/blocks/document/document_element.py +102 -0
- notionary/blocks/document/document_markdown_node.py +31 -0
- notionary/blocks/document/document_models.py +0 -0
- notionary/blocks/embed/__init__.py +7 -0
- notionary/blocks/{embed_element.py → embed/embed_element.py} +50 -32
- notionary/blocks/embed/embed_markdown_node.py +30 -0
- notionary/blocks/embed/embed_models.py +0 -0
- notionary/blocks/heading/__init__.py +7 -0
- notionary/blocks/{heading_element.py → heading/heading_element.py} +25 -17
- notionary/blocks/heading/heading_markdown_node.py +29 -0
- notionary/blocks/heading/heading_models.py +0 -0
- notionary/blocks/image/__init__.py +7 -0
- notionary/blocks/{image_element.py → image/image_element.py} +62 -42
- notionary/blocks/image/image_markdown_node.py +33 -0
- notionary/blocks/image/image_models.py +0 -0
- notionary/blocks/markdown_builder.py +356 -0
- notionary/blocks/markdown_node.py +29 -0
- notionary/blocks/mention/__init__.py +7 -0
- notionary/blocks/{mention_element.py → mention/mention_element.py} +6 -2
- notionary/blocks/mention/mention_markdown_node.py +38 -0
- notionary/blocks/mention/mention_models.py +0 -0
- notionary/blocks/numbered_list/__init__.py +7 -0
- notionary/blocks/{numbered_list_element.py → numbered_list/numbered_list_element.py} +10 -6
- notionary/blocks/numbered_list/numbered_list_markdown_node.py +29 -0
- notionary/blocks/numbered_list/numbered_list_models.py +0 -0
- notionary/blocks/paragraph/__init__.py +7 -0
- notionary/blocks/{paragraph_element.py → paragraph/paragraph_element.py} +7 -3
- notionary/blocks/paragraph/paragraph_markdown_node.py +25 -0
- notionary/blocks/paragraph/paragraph_models.py +0 -0
- notionary/blocks/quote/__init__.py +7 -0
- notionary/blocks/quote/quote_element.py +92 -0
- notionary/blocks/quote/quote_markdown_node.py +23 -0
- notionary/blocks/quote/quote_models.py +0 -0
- notionary/blocks/registry/block_registry.py +17 -3
- notionary/blocks/registry/block_registry_builder.py +90 -178
- notionary/blocks/shared/__init__.py +0 -0
- notionary/blocks/shared/block_client.py +256 -0
- notionary/blocks/shared/models.py +710 -0
- notionary/blocks/{notion_block_element.py → shared/notion_block_element.py} +8 -5
- notionary/blocks/{text_inline_formatter.py → shared/text_inline_formatter.py} +14 -14
- notionary/blocks/shared/text_inline_formatter_new.py +139 -0
- notionary/blocks/table/__init__.py +7 -0
- notionary/blocks/{table_element.py → table/table_element.py} +23 -11
- notionary/blocks/table/table_markdown_node.py +40 -0
- notionary/blocks/table/table_models.py +0 -0
- notionary/blocks/todo/__init__.py +7 -0
- notionary/blocks/{todo_element.py → todo/todo_element.py} +8 -4
- notionary/blocks/todo/todo_markdown_node.py +31 -0
- notionary/blocks/todo/todo_models.py +0 -0
- notionary/blocks/toggle/__init__.py +4 -0
- notionary/blocks/{toggle_element.py → toggle/toggle_element.py} +7 -3
- notionary/blocks/toggle/toggle_markdown_node.py +35 -0
- notionary/blocks/toggle/toggle_models.py +0 -0
- notionary/blocks/toggleable_heading/__init__.py +9 -0
- notionary/blocks/{toggleable_heading_element.py → toggleable_heading/toggleable_heading_element.py} +8 -4
- notionary/blocks/toggleable_heading/toggleable_heading_markdown_node.py +43 -0
- notionary/blocks/toggleable_heading/toggleable_heading_models.py +0 -0
- notionary/blocks/video/__init__.py +7 -0
- notionary/blocks/{video_element.py → video/video_element.py} +82 -57
- notionary/blocks/video/video_markdown_node.py +30 -0
- notionary/database/__init__.py +4 -0
- notionary/database/database.py +481 -0
- notionary/database/{filter_builder.py → database_filter_builder.py} +27 -29
- notionary/database/{notion_database_provider.py → database_provider.py} +4 -4
- notionary/database/notion_database.py +45 -18
- notionary/file_upload/__init__.py +7 -0
- notionary/file_upload/client.py +254 -0
- notionary/file_upload/models.py +60 -0
- notionary/file_upload/notion_file_upload.py +387 -0
- notionary/page/content/markdown_whitespace_processor.py +80 -0
- notionary/page/content/notion_text_length_utils.py +87 -0
- notionary/page/content/page_content_retriever.py +2 -2
- notionary/page/content/page_content_writer.py +97 -148
- notionary/page/formatting/line_processor.py +153 -0
- notionary/page/formatting/markdown_to_notion_converter.py +103 -424
- notionary/page/notion_page.py +13 -14
- notionary/page/notion_to_markdown_converter.py +9 -13
- notionary/telemetry/views.py +15 -6
- notionary/user/__init__.py +11 -0
- notionary/user/base_notion_user.py +52 -0
- notionary/user/client.py +129 -0
- notionary/user/models.py +83 -0
- notionary/user/notion_bot_user.py +227 -0
- notionary/user/notion_user.py +256 -0
- notionary/user/notion_user_manager.py +173 -0
- notionary/user/notion_user_provider.py +1 -0
- notionary/util/__init__.py +3 -5
- notionary/util/factory_decorator.py +0 -33
- notionary/util/factory_only.py +37 -0
- notionary/util/fuzzy.py +74 -0
- notionary/util/logging_mixin.py +12 -12
- notionary/workspace.py +38 -3
- {notionary-0.2.16.dist-info → notionary-0.2.18.dist-info}/METADATA +2 -1
- notionary-0.2.18.dist-info/RECORD +149 -0
- notionary/blocks/audio_element.py +0 -144
- notionary/blocks/callout_element.py +0 -122
- notionary/blocks/notion_block_client.py +0 -26
- notionary/blocks/qoute_element.py +0 -169
- notionary/page/content/notion_page_content_chunker.py +0 -84
- notionary/page/formatting/spacer_rules.py +0 -483
- notionary/util/fuzzy_matcher.py +0 -82
- notionary-0.2.16.dist-info/RECORD +0 -71
- /notionary/{elements/__init__.py → blocks/bookmark/bookmark_models.py} +0 -0
- /notionary/database/{database_exceptions.py → exceptions.py} +0 -0
- /notionary/util/{singleton_decorator.py → singleton.py} +0 -0
- {notionary-0.2.16.dist-info → notionary-0.2.18.dist-info}/LICENSE +0 -0
- {notionary-0.2.16.dist-info → notionary-0.2.18.dist-info}/WHEEL +0 -0
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
from typing import Dict, Any
|
|
2
|
-
|
|
3
|
-
from notionary.blocks import BlockRegistry, BlockRegistryBuilder
|
|
1
|
+
from typing import Dict, Any
|
|
4
2
|
|
|
5
3
|
|
|
6
4
|
class NotionToMarkdownConverter:
|
|
@@ -9,15 +7,13 @@ class NotionToMarkdownConverter:
|
|
|
9
7
|
TOGGLE_ELEMENT_TYPES = ["toggle", "toggleable_heading"]
|
|
10
8
|
LIST_ITEM_TYPES = ["numbered_list_item", "bulleted_list_item"]
|
|
11
9
|
|
|
12
|
-
def __init__(self, block_registry
|
|
10
|
+
def __init__(self, block_registry):
|
|
13
11
|
"""
|
|
14
12
|
Initialize the NotionToMarkdownConverter.
|
|
15
13
|
"""
|
|
16
|
-
self._block_registry =
|
|
17
|
-
block_registry or BlockRegistryBuilder().create_full_registry()
|
|
18
|
-
)
|
|
14
|
+
self._block_registry = block_registry
|
|
19
15
|
|
|
20
|
-
def convert(self, blocks:
|
|
16
|
+
def convert(self, blocks: list[Dict[str, Any]]) -> str:
|
|
21
17
|
"""
|
|
22
18
|
Convert Notion blocks to Markdown text, handling nested structures.
|
|
23
19
|
"""
|
|
@@ -102,7 +98,7 @@ class NotionToMarkdownConverter:
|
|
|
102
98
|
indent = " " * spaces
|
|
103
99
|
return "\n".join([f"{indent}{line}" for line in text.split("\n")])
|
|
104
100
|
|
|
105
|
-
def extract_toggle_content(self, blocks:
|
|
101
|
+
def extract_toggle_content(self, blocks: list[Dict[str, Any]]) -> str:
|
|
106
102
|
"""
|
|
107
103
|
Extract only the content of toggles from blocks.
|
|
108
104
|
"""
|
|
@@ -117,7 +113,7 @@ class NotionToMarkdownConverter:
|
|
|
117
113
|
return "\n".join(toggle_contents)
|
|
118
114
|
|
|
119
115
|
def _extract_toggle_content_recursive(
|
|
120
|
-
self, block: Dict[str, Any], result:
|
|
116
|
+
self, block: Dict[str, Any], result: list[str]
|
|
121
117
|
) -> None:
|
|
122
118
|
"""
|
|
123
119
|
Recursively extract toggle content from a block and its children.
|
|
@@ -137,7 +133,7 @@ class NotionToMarkdownConverter:
|
|
|
137
133
|
return block.get("type") in self.TOGGLE_ELEMENT_TYPES and "children" in block
|
|
138
134
|
|
|
139
135
|
def _add_toggle_header_to_result(
|
|
140
|
-
self, block: Dict[str, Any], result:
|
|
136
|
+
self, block: Dict[str, Any], result: list[str]
|
|
141
137
|
) -> None:
|
|
142
138
|
"""
|
|
143
139
|
Add toggle header text to result list.
|
|
@@ -156,7 +152,7 @@ class NotionToMarkdownConverter:
|
|
|
156
152
|
result.append(f"### {toggle_text}")
|
|
157
153
|
|
|
158
154
|
def _add_toggle_children_to_result(
|
|
159
|
-
self, block: Dict[str, Any], result:
|
|
155
|
+
self, block: Dict[str, Any], result: list[str]
|
|
160
156
|
) -> None:
|
|
161
157
|
"""
|
|
162
158
|
Add formatted toggle children to result list.
|
|
@@ -173,7 +169,7 @@ class NotionToMarkdownConverter:
|
|
|
173
169
|
if child_text:
|
|
174
170
|
result.append(f"- {child_text}")
|
|
175
171
|
|
|
176
|
-
def _extract_text_from_rich_text(self, rich_text:
|
|
172
|
+
def _extract_text_from_rich_text(self, rich_text: list[Dict[str, Any]]) -> str:
|
|
177
173
|
"""
|
|
178
174
|
Extract plain text from Notion's rich text array.
|
|
179
175
|
"""
|
notionary/telemetry/views.py
CHANGED
|
@@ -25,6 +25,7 @@ class DatabaseFactoryUsedEvent(BaseTelemetryEvent):
|
|
|
25
25
|
def name(self) -> str:
|
|
26
26
|
return "database_factory_used"
|
|
27
27
|
|
|
28
|
+
|
|
28
29
|
@dataclass
|
|
29
30
|
class QueryOperationEvent(BaseTelemetryEvent):
|
|
30
31
|
"""Event fired when a query operation is performed"""
|
|
@@ -34,7 +35,8 @@ class QueryOperationEvent(BaseTelemetryEvent):
|
|
|
34
35
|
@property
|
|
35
36
|
def name(self) -> str:
|
|
36
37
|
return "query_operation"
|
|
37
|
-
|
|
38
|
+
|
|
39
|
+
|
|
38
40
|
@dataclass
|
|
39
41
|
class NotionMarkdownSyntaxPromptEvent(BaseTelemetryEvent):
|
|
40
42
|
"""Event fired when Notion Markdown syntax is used"""
|
|
@@ -43,12 +45,16 @@ class NotionMarkdownSyntaxPromptEvent(BaseTelemetryEvent):
|
|
|
43
45
|
def name(self) -> str:
|
|
44
46
|
return "notion_markdown_syntax_used"
|
|
45
47
|
|
|
48
|
+
|
|
46
49
|
# Tracks markdown conversion
|
|
47
50
|
@dataclass
|
|
48
51
|
class MarkdownToNotionConversionEvent(BaseTelemetryEvent):
|
|
49
52
|
"""Event fired when markdown is converted to Notion blocks"""
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
|
|
54
|
+
handler_element_name: Optional[str] = (
|
|
55
|
+
None # e.g. "HeadingElement", "ParagraphElement"
|
|
56
|
+
)
|
|
57
|
+
|
|
52
58
|
@property
|
|
53
59
|
def name(self) -> str:
|
|
54
60
|
return "markdown_to_notion_conversion"
|
|
@@ -57,8 +63,11 @@ class MarkdownToNotionConversionEvent(BaseTelemetryEvent):
|
|
|
57
63
|
@dataclass
|
|
58
64
|
class NotionToMarkdownConversionEvent(BaseTelemetryEvent):
|
|
59
65
|
"""Event fired when Notion blocks are converted to markdown"""
|
|
60
|
-
|
|
61
|
-
|
|
66
|
+
|
|
67
|
+
handler_element_name: Optional[str] = (
|
|
68
|
+
None # e.g. "HeadingElement", "ParagraphElement"
|
|
69
|
+
)
|
|
70
|
+
|
|
62
71
|
@property
|
|
63
72
|
def name(self) -> str:
|
|
64
|
-
return "notion_to_markdown_conversion"
|
|
73
|
+
return "notion_to_markdown_conversion"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from .notion_user import NotionUser
|
|
2
|
+
from .notion_user_manager import NotionUserManager
|
|
3
|
+
from .client import NotionUserClient
|
|
4
|
+
from .notion_bot_user import NotionBotUser
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"NotionUser",
|
|
8
|
+
"NotionUserManager",
|
|
9
|
+
"NotionUserClient",
|
|
10
|
+
"NotionBotUser",
|
|
11
|
+
]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from abc import ABC
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from notionary.user.client import NotionUserClient
|
|
5
|
+
|
|
6
|
+
from notionary.util import LoggingMixin
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BaseNotionUser(LoggingMixin, ABC):
|
|
10
|
+
"""
|
|
11
|
+
Base class for all Notion user types with common functionality.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
user_id: str,
|
|
17
|
+
name: Optional[str] = None,
|
|
18
|
+
avatar_url: Optional[str] = None,
|
|
19
|
+
token: Optional[str] = None,
|
|
20
|
+
):
|
|
21
|
+
"""Initialize base user properties."""
|
|
22
|
+
self._user_id = user_id
|
|
23
|
+
self._name = name
|
|
24
|
+
self._avatar_url = avatar_url
|
|
25
|
+
self.client = NotionUserClient(token=token)
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def id(self) -> str:
|
|
29
|
+
"""Get the user ID."""
|
|
30
|
+
return self._user_id
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def name(self) -> Optional[str]:
|
|
34
|
+
"""Get the user name."""
|
|
35
|
+
return self._name
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def avatar_url(self) -> Optional[str]:
|
|
39
|
+
"""Get the avatar URL."""
|
|
40
|
+
return self._avatar_url
|
|
41
|
+
|
|
42
|
+
def get_display_name(self) -> str:
|
|
43
|
+
"""Get a display name for the user."""
|
|
44
|
+
return self._name or f"User {self._user_id[:8]}"
|
|
45
|
+
|
|
46
|
+
def __str__(self) -> str:
|
|
47
|
+
"""String representation of the user."""
|
|
48
|
+
return f"{self.__class__.__name__}(name='{self.get_display_name()}', id='{self._user_id[:8]}...')"
|
|
49
|
+
|
|
50
|
+
def __repr__(self) -> str:
|
|
51
|
+
"""Detailed string representation."""
|
|
52
|
+
return self.__str__()
|
notionary/user/client.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
from typing import Optional, List
|
|
2
|
+
from notionary.base_notion_client import BaseNotionClient
|
|
3
|
+
from notionary.user.models import (
|
|
4
|
+
NotionBotUserResponse,
|
|
5
|
+
NotionUserResponse,
|
|
6
|
+
NotionUsersListResponse,
|
|
7
|
+
)
|
|
8
|
+
from notionary.util import singleton
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@singleton
|
|
12
|
+
class NotionUserClient(BaseNotionClient):
|
|
13
|
+
"""
|
|
14
|
+
Client for Notion user-specific operations.
|
|
15
|
+
Inherits base HTTP functionality from BaseNotionClient.
|
|
16
|
+
|
|
17
|
+
Note: The Notion API only supports individual user queries and bot user info.
|
|
18
|
+
List users endpoint is available but only returns workspace members (no guests).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
async def get_user(self, user_id: str) -> Optional[NotionUserResponse]:
|
|
22
|
+
"""
|
|
23
|
+
Retrieve a user by their ID.
|
|
24
|
+
"""
|
|
25
|
+
response = await self.get(f"users/{user_id}")
|
|
26
|
+
if response is None:
|
|
27
|
+
self.logger.error("Failed to fetch user %s - API returned None", user_id)
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
return NotionUserResponse.model_validate(response)
|
|
32
|
+
except Exception as e:
|
|
33
|
+
self.logger.error("Failed to validate user response for %s: %s", user_id, e)
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
async def get_bot_user(self) -> Optional[NotionBotUserResponse]:
|
|
37
|
+
"""
|
|
38
|
+
Retrieve your token's bot user information.
|
|
39
|
+
"""
|
|
40
|
+
response = await self.get("users/me")
|
|
41
|
+
if response is None:
|
|
42
|
+
self.logger.error("Failed to fetch bot user - API returned None")
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
return NotionBotUserResponse.model_validate(response)
|
|
47
|
+
except Exception as e:
|
|
48
|
+
self.logger.error("Failed to validate bot user response: %s", e)
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
async def list_users(
|
|
52
|
+
self, page_size: int = 100, start_cursor: Optional[str] = None
|
|
53
|
+
) -> Optional[NotionUsersListResponse]:
|
|
54
|
+
"""
|
|
55
|
+
List all users in the workspace (paginated).
|
|
56
|
+
|
|
57
|
+
Note: Guests are not included in the response.
|
|
58
|
+
"""
|
|
59
|
+
params = {"page_size": min(page_size, 100)} # API max is 100
|
|
60
|
+
if start_cursor:
|
|
61
|
+
params["start_cursor"] = start_cursor
|
|
62
|
+
|
|
63
|
+
response = await self.get("users", params=params)
|
|
64
|
+
if response is None:
|
|
65
|
+
self.logger.error("Failed to fetch users list - API returned None")
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
return NotionUsersListResponse.model_validate(response)
|
|
70
|
+
except Exception as e:
|
|
71
|
+
self.logger.error("Failed to validate users list response: %s", e)
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
async def get_all_users(self) -> List[NotionUserResponse]:
|
|
75
|
+
"""
|
|
76
|
+
Get all users in the workspace by handling pagination automatically.
|
|
77
|
+
"""
|
|
78
|
+
all_users = []
|
|
79
|
+
start_cursor = None
|
|
80
|
+
|
|
81
|
+
while True:
|
|
82
|
+
try:
|
|
83
|
+
response = await self.list_users(
|
|
84
|
+
page_size=100, start_cursor=start_cursor
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if not response or not response.results:
|
|
88
|
+
break
|
|
89
|
+
|
|
90
|
+
all_users.extend(response.results)
|
|
91
|
+
|
|
92
|
+
# Check if there are more pages
|
|
93
|
+
if not response.has_more or not response.next_cursor:
|
|
94
|
+
break
|
|
95
|
+
|
|
96
|
+
start_cursor = response.next_cursor
|
|
97
|
+
|
|
98
|
+
except Exception as e:
|
|
99
|
+
self.logger.error("Error fetching all users: %s", str(e))
|
|
100
|
+
break
|
|
101
|
+
|
|
102
|
+
self.logger.info("Retrieved %d total users from workspace", len(all_users))
|
|
103
|
+
return all_users
|
|
104
|
+
|
|
105
|
+
async def get_workspace_name(self) -> Optional[str]:
|
|
106
|
+
"""
|
|
107
|
+
Get the workspace name from the bot user.
|
|
108
|
+
"""
|
|
109
|
+
try:
|
|
110
|
+
bot_user = await self.get_bot_user()
|
|
111
|
+
if bot_user and bot_user.bot and bot_user.bot.workspace_name:
|
|
112
|
+
return bot_user.bot.workspace_name
|
|
113
|
+
return None
|
|
114
|
+
except Exception as e:
|
|
115
|
+
self.logger.error("Error fetching workspace name: %s", str(e))
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
async def get_workspace_limits(self) -> Optional[dict]:
|
|
119
|
+
"""
|
|
120
|
+
Get workspace limits from the bot user.
|
|
121
|
+
"""
|
|
122
|
+
try:
|
|
123
|
+
bot_user = await self.get_bot_user()
|
|
124
|
+
if bot_user and bot_user.bot and bot_user.bot.workspace_limits:
|
|
125
|
+
return bot_user.bot.workspace_limits.model_dump()
|
|
126
|
+
return None
|
|
127
|
+
except Exception as e:
|
|
128
|
+
self.logger.error("Error fetching workspace limits: %s", str(e))
|
|
129
|
+
return None
|
notionary/user/models.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Literal, Optional
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PersonUser(BaseModel):
|
|
7
|
+
"""Person user details"""
|
|
8
|
+
|
|
9
|
+
email: Optional[str] = None
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BotOwner(BaseModel):
|
|
13
|
+
"""Bot owner information - simplified structure"""
|
|
14
|
+
|
|
15
|
+
type: Literal["workspace", "user"]
|
|
16
|
+
workspace: Optional[bool] = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class WorkspaceLimits(BaseModel):
|
|
20
|
+
"""Workspace limits for bot users"""
|
|
21
|
+
|
|
22
|
+
max_file_upload_size_in_bytes: int
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class BotUser(BaseModel):
|
|
26
|
+
"""Bot user details"""
|
|
27
|
+
|
|
28
|
+
owner: Optional[BotOwner] = None
|
|
29
|
+
workspace_name: Optional[str] = None
|
|
30
|
+
workspace_limits: Optional[WorkspaceLimits] = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class NotionUserResponse(BaseModel):
|
|
34
|
+
"""
|
|
35
|
+
Represents a Notion user object as returned by the Users API.
|
|
36
|
+
Can represent both person and bot users.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
object: Literal["user"]
|
|
40
|
+
id: str
|
|
41
|
+
type: Optional[Literal["person", "bot"]] = None
|
|
42
|
+
name: Optional[str] = None
|
|
43
|
+
avatar_url: Optional[str] = None
|
|
44
|
+
|
|
45
|
+
# Person-specific fields
|
|
46
|
+
person: Optional[PersonUser] = None
|
|
47
|
+
|
|
48
|
+
# Bot-specific fields
|
|
49
|
+
bot: Optional[BotUser] = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class NotionBotUserResponse(NotionUserResponse):
|
|
53
|
+
"""
|
|
54
|
+
Specialized response for bot user (from /users/me endpoint)
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
# Bot users should have these fields, but they can still be None
|
|
58
|
+
type: Literal["bot"]
|
|
59
|
+
bot: Optional[BotUser] = None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class NotionUsersListResponse(BaseModel):
|
|
63
|
+
"""
|
|
64
|
+
Response model for paginated users list from /v1/users endpoint.
|
|
65
|
+
Follows Notion's standard pagination pattern.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
object: Literal["list"]
|
|
69
|
+
results: list[NotionUserResponse]
|
|
70
|
+
next_cursor: Optional[str] = None
|
|
71
|
+
has_more: bool
|
|
72
|
+
type: Literal["user"]
|
|
73
|
+
user: dict = {}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class WorkspaceInfo:
|
|
78
|
+
"""Dataclass to hold workspace information for bot users."""
|
|
79
|
+
|
|
80
|
+
name: Optional[str] = None
|
|
81
|
+
limits: Optional[WorkspaceLimits] = None
|
|
82
|
+
owner_type: Optional[str] = None
|
|
83
|
+
is_workspace_owned: bool = False
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Optional, List
|
|
3
|
+
from notionary.user.base_notion_user import BaseNotionUser
|
|
4
|
+
from notionary.user.client import NotionUserClient
|
|
5
|
+
from notionary.user.models import (
|
|
6
|
+
NotionBotUserResponse,
|
|
7
|
+
WorkspaceLimits,
|
|
8
|
+
)
|
|
9
|
+
from notionary.util import factory_only
|
|
10
|
+
from notionary.util.fuzzy import find_best_match
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NotionBotUser(BaseNotionUser):
|
|
14
|
+
"""
|
|
15
|
+
Manager for Notion bot users.
|
|
16
|
+
Handles bot-specific operations and workspace information.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
NO_USERS_FOUND_MSG = "No users found in workspace"
|
|
20
|
+
NO_BOT_USERS_FOUND_MSG = "No bot users found in workspace"
|
|
21
|
+
|
|
22
|
+
@factory_only("from_current_integration", "from_bot_response", "from_name")
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
user_id: str,
|
|
26
|
+
name: Optional[str] = None,
|
|
27
|
+
avatar_url: Optional[str] = None,
|
|
28
|
+
workspace_name: Optional[str] = None,
|
|
29
|
+
workspace_limits: Optional[WorkspaceLimits] = None,
|
|
30
|
+
owner_type: Optional[str] = None,
|
|
31
|
+
token: Optional[str] = None,
|
|
32
|
+
):
|
|
33
|
+
"""Initialize bot user with bot-specific properties."""
|
|
34
|
+
super().__init__(user_id, name, avatar_url, token)
|
|
35
|
+
self._workspace_name = workspace_name
|
|
36
|
+
self._workspace_limits = workspace_limits
|
|
37
|
+
self._owner_type = owner_type
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
async def from_current_integration(
|
|
41
|
+
cls, token: Optional[str] = None
|
|
42
|
+
) -> Optional[NotionBotUser]:
|
|
43
|
+
"""
|
|
44
|
+
Get the current bot user (from the API token).
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
token: Optional Notion API token
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Optional[NotionBotUser]: Bot user instance or None if failed
|
|
51
|
+
"""
|
|
52
|
+
client = NotionUserClient(token=token)
|
|
53
|
+
bot_response = await client.get_bot_user()
|
|
54
|
+
|
|
55
|
+
if bot_response is None:
|
|
56
|
+
cls.logger.error("Failed to load bot user data")
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
return cls._create_from_response(bot_response, token)
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
async def from_name(
|
|
63
|
+
cls, name: str, token: Optional[str] = None, min_similarity: float = 0.6
|
|
64
|
+
) -> Optional[NotionBotUser]:
|
|
65
|
+
"""
|
|
66
|
+
Create a NotionBotUser by finding a bot user with fuzzy matching on the name.
|
|
67
|
+
Uses Notion's list users API and fuzzy matching to find the best result.
|
|
68
|
+
"""
|
|
69
|
+
client = NotionUserClient(token=token)
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
# Get all users from workspace
|
|
73
|
+
all_users_response = await client.get_all_users()
|
|
74
|
+
|
|
75
|
+
if not all_users_response:
|
|
76
|
+
cls.logger.warning(cls.NO_USERS_FOUND_MSG)
|
|
77
|
+
raise ValueError(cls.NO_USERS_FOUND_MSG)
|
|
78
|
+
|
|
79
|
+
# Filter to only bot users
|
|
80
|
+
bot_users = [
|
|
81
|
+
user for user in all_users_response if user.type == "bot" and user.name
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
if not bot_users:
|
|
85
|
+
cls.logger.warning(cls.NO_BOT_USERS_FOUND_MSG)
|
|
86
|
+
raise ValueError(cls.NO_BOT_USERS_FOUND_MSG)
|
|
87
|
+
|
|
88
|
+
cls.logger.debug(
|
|
89
|
+
"Found %d bot users for fuzzy matching: %s",
|
|
90
|
+
len(bot_users),
|
|
91
|
+
[user.name for user in bot_users[:5]], # Log first 5 names
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Use fuzzy matching to find best match
|
|
95
|
+
best_match = find_best_match(
|
|
96
|
+
query=name,
|
|
97
|
+
items=bot_users,
|
|
98
|
+
text_extractor=lambda user: user.name or "",
|
|
99
|
+
min_similarity=min_similarity,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if not best_match:
|
|
103
|
+
available_names = [user.name for user in bot_users[:5]]
|
|
104
|
+
cls.logger.warning(
|
|
105
|
+
"No sufficiently similar bot user found for '%s' (min: %.3f). Available: %s",
|
|
106
|
+
name,
|
|
107
|
+
min_similarity,
|
|
108
|
+
available_names,
|
|
109
|
+
)
|
|
110
|
+
raise ValueError(f"No sufficiently similar bot user found for '{name}'")
|
|
111
|
+
|
|
112
|
+
cls.logger.info(
|
|
113
|
+
"Found best match: '%s' with similarity %.3f for query '%s'",
|
|
114
|
+
best_match.matched_text,
|
|
115
|
+
best_match.similarity,
|
|
116
|
+
name,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Create NotionBotUser from the matched user response
|
|
120
|
+
return cls._create_from_response(best_match.item, token)
|
|
121
|
+
|
|
122
|
+
except Exception as e:
|
|
123
|
+
cls.logger.error("Error finding bot user by name '%s': %s", name, str(e))
|
|
124
|
+
raise
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
def from_bot_response(
|
|
128
|
+
cls, bot_response: NotionBotUserResponse, token: Optional[str] = None
|
|
129
|
+
) -> NotionBotUser:
|
|
130
|
+
"""
|
|
131
|
+
Create a NotionBotUser from an existing bot API response.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
bot_response: Bot user response from Notion API
|
|
135
|
+
token: Optional Notion API token
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
NotionBotUser: Bot user instance
|
|
139
|
+
"""
|
|
140
|
+
return cls._create_from_response(bot_response, token)
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def workspace_name(self) -> Optional[str]:
|
|
144
|
+
"""Get the workspace name."""
|
|
145
|
+
return self._workspace_name
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def workspace_limits(self) -> Optional[WorkspaceLimits]:
|
|
149
|
+
"""Get the workspace limits."""
|
|
150
|
+
return self._workspace_limits
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def owner_type(self) -> Optional[str]:
|
|
154
|
+
"""Get the owner type ('workspace' or 'user')."""
|
|
155
|
+
return self._owner_type
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def user_type(self) -> str:
|
|
159
|
+
"""Get the user type."""
|
|
160
|
+
return "bot"
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def is_person(self) -> bool:
|
|
164
|
+
"""Check if this is a person user."""
|
|
165
|
+
return False
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def is_bot(self) -> bool:
|
|
169
|
+
"""Check if this is a bot user."""
|
|
170
|
+
return True
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def is_workspace_integration(self) -> bool:
|
|
174
|
+
"""Check if this is a workspace-owned integration."""
|
|
175
|
+
return self._owner_type == "workspace"
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def is_user_integration(self) -> bool:
|
|
179
|
+
"""Check if this is a user-owned integration."""
|
|
180
|
+
return self._owner_type == "user"
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def max_file_upload_size(self) -> Optional[int]:
|
|
184
|
+
"""The maximum file upload size in bytes."""
|
|
185
|
+
return (
|
|
186
|
+
self._workspace_limits.max_file_upload_size_in_bytes
|
|
187
|
+
if self._workspace_limits
|
|
188
|
+
else None
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def __str__(self) -> str:
|
|
192
|
+
"""String representation of the bot user."""
|
|
193
|
+
workspace = self._workspace_name or "Unknown Workspace"
|
|
194
|
+
return f"NotionBotUser(name='{self.get_display_name()}', workspace='{workspace}', id='{self._user_id[:8]}...')"
|
|
195
|
+
|
|
196
|
+
@classmethod
|
|
197
|
+
def _create_from_response(
|
|
198
|
+
cls, bot_response: NotionBotUserResponse, token: Optional[str]
|
|
199
|
+
) -> NotionBotUser:
|
|
200
|
+
"""Create NotionBotUser instance from API response."""
|
|
201
|
+
workspace_name = None
|
|
202
|
+
workspace_limits = None
|
|
203
|
+
owner_type = None
|
|
204
|
+
|
|
205
|
+
if bot_response.bot:
|
|
206
|
+
workspace_name = bot_response.bot.workspace_name
|
|
207
|
+
workspace_limits = bot_response.bot.workspace_limits
|
|
208
|
+
owner_type = bot_response.bot.owner.type if bot_response.bot.owner else None
|
|
209
|
+
|
|
210
|
+
instance = cls(
|
|
211
|
+
user_id=bot_response.id,
|
|
212
|
+
name=bot_response.name,
|
|
213
|
+
avatar_url=bot_response.avatar_url,
|
|
214
|
+
workspace_name=workspace_name,
|
|
215
|
+
workspace_limits=workspace_limits,
|
|
216
|
+
owner_type=owner_type,
|
|
217
|
+
token=token,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
cls.logger.info(
|
|
221
|
+
"Created bot user: '%s' (ID: %s, Workspace: %s)",
|
|
222
|
+
bot_response.name or "Unknown Bot",
|
|
223
|
+
bot_response.id,
|
|
224
|
+
workspace_name or "Unknown",
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
return instance
|