notionary 0.2.13__py3-none-any.whl → 0.2.14__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 +3 -16
- notionary/{notion_client.py → base_notion_client.py} +92 -98
- notionary/blocks/__init__.py +61 -0
- notionary/{elements → blocks}/audio_element.py +6 -3
- notionary/{elements → blocks}/bookmark_element.py +3 -5
- notionary/{elements → blocks}/bulleted_list_element.py +5 -6
- notionary/{elements → blocks}/callout_element.py +4 -6
- notionary/{elements → blocks}/code_block_element.py +4 -5
- notionary/{elements → blocks}/column_element.py +3 -5
- notionary/{elements → blocks}/divider_element.py +3 -5
- notionary/{elements → blocks}/embed_element.py +4 -5
- notionary/{elements → blocks}/heading_element.py +4 -7
- notionary/{elements → blocks}/image_element.py +4 -5
- notionary/{elements → blocks}/mention_element.py +3 -6
- notionary/blocks/notion_block_client.py +26 -0
- notionary/{elements → blocks}/notion_block_element.py +2 -3
- notionary/{elements → blocks}/numbered_list_element.py +4 -6
- notionary/{elements → blocks}/paragraph_element.py +4 -6
- notionary/{prompting/element_prompt_content.py → blocks/prompts/element_prompt_builder.py} +1 -40
- notionary/blocks/prompts/element_prompt_content.py +41 -0
- notionary/{elements → blocks}/qoute_element.py +4 -5
- notionary/{elements → blocks}/registry/block_registry.py +4 -26
- notionary/{elements → blocks}/registry/block_registry_builder.py +26 -25
- notionary/{elements → blocks}/table_element.py +5 -6
- notionary/{elements → blocks}/text_inline_formatter.py +1 -4
- notionary/{elements → blocks}/todo_element.py +5 -6
- notionary/{elements → blocks}/toggle_element.py +3 -5
- notionary/{elements → blocks}/toggleable_heading_element.py +4 -6
- notionary/{elements → blocks}/video_element.py +4 -5
- notionary/cli/main.py +157 -128
- notionary/cli/onboarding.py +10 -9
- notionary/database/__init__.py +0 -0
- notionary/database/client.py +132 -0
- notionary/database/database_exceptions.py +13 -0
- notionary/database/factory.py +0 -0
- notionary/database/filter_builder.py +175 -0
- notionary/database/notion_database.py +339 -126
- notionary/database/notion_database_provider.py +230 -0
- notionary/elements/__init__.py +0 -0
- notionary/models/notion_database_response.py +294 -13
- notionary/models/notion_page_response.py +9 -31
- notionary/models/search_response.py +0 -0
- notionary/page/__init__.py +0 -0
- notionary/page/client.py +110 -0
- notionary/page/content/page_content_retriever.py +5 -20
- notionary/page/content/page_content_writer.py +3 -4
- notionary/page/formatting/markdown_to_notion_converter.py +1 -3
- notionary/{prompting → page}/markdown_syntax_prompt_generator.py +1 -2
- notionary/page/notion_page.py +354 -317
- notionary/page/notion_to_markdown_converter.py +1 -4
- notionary/page/properites/property_value_extractor.py +0 -64
- notionary/page/{properites/property_formatter.py → property_formatter.py} +7 -4
- notionary/page/search_filter_builder.py +131 -0
- notionary/page/utils.py +60 -0
- notionary/util/__init__.py +12 -3
- notionary/util/factory_decorator.py +33 -0
- notionary/util/fuzzy_matcher.py +82 -0
- notionary/util/page_id_utils.py +0 -21
- notionary/util/singleton_metaclass.py +22 -0
- notionary/workspace.py +69 -0
- {notionary-0.2.13.dist-info → notionary-0.2.14.dist-info}/METADATA +4 -1
- notionary-0.2.14.dist-info/RECORD +72 -0
- notionary/database/database_discovery.py +0 -142
- notionary/database/notion_database_factory.py +0 -190
- notionary/exceptions/database_exceptions.py +0 -76
- notionary/exceptions/page_creation_exception.py +0 -9
- notionary/page/metadata/metadata_editor.py +0 -150
- notionary/page/metadata/notion_icon_manager.py +0 -77
- notionary/page/metadata/notion_page_cover_manager.py +0 -56
- notionary/page/notion_page_factory.py +0 -328
- notionary/page/properites/database_property_service.py +0 -302
- notionary/page/properites/page_property_manager.py +0 -152
- notionary/page/relations/notion_page_relation_manager.py +0 -350
- notionary/page/relations/notion_page_title_resolver.py +0 -104
- notionary/page/relations/page_database_relation.py +0 -68
- notionary/util/warn_direct_constructor_usage.py +0 -54
- notionary-0.2.13.dist-info/RECORD +0 -67
- /notionary/util/{singleton.py → singleton_decorator.py} +0 -0
- {notionary-0.2.13.dist-info → notionary-0.2.14.dist-info}/WHEEL +0 -0
- {notionary-0.2.13.dist-info → notionary-0.2.14.dist-info}/entry_points.txt +0 -0
- {notionary-0.2.13.dist-info → notionary-0.2.14.dist-info}/licenses/LICENSE +0 -0
- {notionary-0.2.13.dist-info → notionary-0.2.14.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,230 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import Dict, Optional, TYPE_CHECKING
|
4
|
+
from notionary.database.client import NotionDatabaseClient
|
5
|
+
from notionary.database.database_exceptions import DatabaseNotFoundException
|
6
|
+
from notionary.models.notion_database_response import NotionDatabaseResponse
|
7
|
+
from notionary.util import LoggingMixin, FuzzyMatcher, format_uuid
|
8
|
+
from notionary.util.singleton_metaclass import SingletonMetaClass
|
9
|
+
|
10
|
+
if TYPE_CHECKING:
|
11
|
+
from notionary import NotionDatabase
|
12
|
+
|
13
|
+
|
14
|
+
class NotionDatabaseProvider(LoggingMixin, metaclass=SingletonMetaClass):
|
15
|
+
"""
|
16
|
+
Provider class for creating and caching Notion database instances.
|
17
|
+
|
18
|
+
Prevents duplicate database creation when working with multiple pages from the same database.
|
19
|
+
Each Notion page references its parent database to determine selectable properties and options.
|
20
|
+
By caching database instances, this provider avoids excessive network requests when reading options,
|
21
|
+
significantly improving performance for repeated property lookups across many pages.
|
22
|
+
"""
|
23
|
+
|
24
|
+
def __init__(self):
|
25
|
+
self._database_cache: Dict[str, NotionDatabase] = {}
|
26
|
+
|
27
|
+
async def get_database_by_id(
|
28
|
+
self, database_id: str, token: Optional[str] = None, force_refresh: bool = False
|
29
|
+
) -> NotionDatabase:
|
30
|
+
"""Get a NotionDatabase by ID with caching."""
|
31
|
+
cache_key = self._create_id_cache_key(database_id)
|
32
|
+
|
33
|
+
if self._should_use_cache(cache_key, force_refresh):
|
34
|
+
self.logger.debug(f"Using cached database for ID: {database_id}")
|
35
|
+
return self._database_cache[cache_key]
|
36
|
+
|
37
|
+
database = await self._create_from_database_id(database_id, token)
|
38
|
+
self._cache_database(database, token)
|
39
|
+
return database
|
40
|
+
|
41
|
+
async def get_database_by_name(
|
42
|
+
self,
|
43
|
+
database_name: str,
|
44
|
+
token: Optional[str] = None,
|
45
|
+
min_similarity: float = 0.6,
|
46
|
+
force_refresh: bool = False,
|
47
|
+
) -> NotionDatabase:
|
48
|
+
"""Get a NotionDatabase by name with caching."""
|
49
|
+
name_cache_key = self._create_name_cache_key(database_name, token)
|
50
|
+
|
51
|
+
if self._should_use_cache(name_cache_key, force_refresh):
|
52
|
+
return self._database_cache[name_cache_key]
|
53
|
+
|
54
|
+
database = await self._create_from_database_name(
|
55
|
+
database_name, token, min_similarity
|
56
|
+
)
|
57
|
+
|
58
|
+
id_cache_key = self._create_id_cache_key(database.database_id)
|
59
|
+
if not force_refresh and id_cache_key in self._database_cache:
|
60
|
+
self.logger.debug(
|
61
|
+
f"Found existing cached database by ID: {database.database_id}"
|
62
|
+
)
|
63
|
+
existing_database = self._database_cache[id_cache_key]
|
64
|
+
|
65
|
+
self._database_cache[name_cache_key] = existing_database
|
66
|
+
return existing_database
|
67
|
+
|
68
|
+
self._cache_database(database, token, database_name)
|
69
|
+
self.logger.debug(
|
70
|
+
f"Cached database: {database.title} (ID: {database.database_id})"
|
71
|
+
)
|
72
|
+
|
73
|
+
return database
|
74
|
+
|
75
|
+
def invalidate_database_cache(self, database_id: str) -> bool:
|
76
|
+
"""
|
77
|
+
Simply invalidate (remove) cache entries for a database without reloading.
|
78
|
+
|
79
|
+
Args:
|
80
|
+
database_id: The database ID to invalidate
|
81
|
+
|
82
|
+
Returns:
|
83
|
+
True if cache entries were found and removed, False otherwise
|
84
|
+
"""
|
85
|
+
|
86
|
+
id_cache_key = self._create_id_cache_key(database_id)
|
87
|
+
was_cached = id_cache_key in self._database_cache
|
88
|
+
|
89
|
+
if not was_cached:
|
90
|
+
self.logger.debug(f"No cache entry found for database ID: {database_id}")
|
91
|
+
return False
|
92
|
+
|
93
|
+
removed_database = self._database_cache.pop(id_cache_key)
|
94
|
+
self.logger.debug(f"Invalidated cached database: {removed_database.title}")
|
95
|
+
|
96
|
+
name_keys_to_remove = [
|
97
|
+
cache_key
|
98
|
+
for cache_key, cached_db in self._database_cache.items()
|
99
|
+
if (cache_key.startswith("name:") and cached_db.database_id == database_id)
|
100
|
+
]
|
101
|
+
|
102
|
+
for name_key in name_keys_to_remove:
|
103
|
+
self._database_cache.pop(name_key)
|
104
|
+
self.logger.debug(f"Invalidated name-based cache: {name_key}")
|
105
|
+
|
106
|
+
return was_cached
|
107
|
+
|
108
|
+
async def _create_from_database_id(
|
109
|
+
self, database_id: str, token: Optional[str]
|
110
|
+
) -> NotionDatabase:
|
111
|
+
"""Create a NotionDatabase from database ID via API."""
|
112
|
+
formatted_id = format_uuid(database_id) or database_id
|
113
|
+
|
114
|
+
async with NotionDatabaseClient(token=token) as client:
|
115
|
+
db_response = await client.get_database(formatted_id)
|
116
|
+
return self._create_from_response(db_response, token)
|
117
|
+
|
118
|
+
async def _create_from_database_name(
|
119
|
+
self,
|
120
|
+
database_name: str,
|
121
|
+
token: Optional[str] = None,
|
122
|
+
min_similarity: float = 0.6,
|
123
|
+
) -> NotionDatabase:
|
124
|
+
"""Create a NotionDatabase by finding it via name with fuzzy matching."""
|
125
|
+
async with NotionDatabaseClient(token=token) as client:
|
126
|
+
search_result = await client.search_databases(database_name, limit=10)
|
127
|
+
|
128
|
+
if not search_result.results:
|
129
|
+
self.logger.warning("No databases found for name: %s", database_name)
|
130
|
+
raise DatabaseNotFoundException(database_name)
|
131
|
+
|
132
|
+
best_match = FuzzyMatcher.find_best_match(
|
133
|
+
query=database_name,
|
134
|
+
items=search_result.results,
|
135
|
+
text_extractor=lambda db: self._extract_title(db),
|
136
|
+
min_similarity=min_similarity,
|
137
|
+
)
|
138
|
+
|
139
|
+
if not best_match:
|
140
|
+
available_titles = [
|
141
|
+
self._extract_title(db) for db in search_result.results[:5]
|
142
|
+
]
|
143
|
+
self.logger.warning(
|
144
|
+
"No sufficiently similar database found for '%s' (min: %.3f). Available: %s",
|
145
|
+
database_name,
|
146
|
+
min_similarity,
|
147
|
+
available_titles,
|
148
|
+
)
|
149
|
+
raise DatabaseNotFoundException(database_name)
|
150
|
+
|
151
|
+
database_id = best_match.item.id
|
152
|
+
db_response = await client.get_database(database_id=database_id)
|
153
|
+
instance = self._create_from_response(db_response, token)
|
154
|
+
|
155
|
+
self.logger.info(
|
156
|
+
"Created database: '%s' (ID: %s, similarity: %.3f)",
|
157
|
+
instance.title,
|
158
|
+
database_id,
|
159
|
+
best_match.similarity,
|
160
|
+
)
|
161
|
+
|
162
|
+
return instance
|
163
|
+
|
164
|
+
def _should_use_cache(self, cache_key: str, force_refresh: bool) -> bool:
|
165
|
+
"""Returns True if the cache should be used for the given cache_key."""
|
166
|
+
return not force_refresh and cache_key in self._database_cache
|
167
|
+
|
168
|
+
def _cache_database(
|
169
|
+
self,
|
170
|
+
database: NotionDatabase,
|
171
|
+
token: Optional[str],
|
172
|
+
original_name: Optional[str] = None,
|
173
|
+
) -> None:
|
174
|
+
"""Cache a database by both ID and name (if provided)."""
|
175
|
+
# Always cache by ID
|
176
|
+
id_cache_key = self._create_id_cache_key(database.database_id)
|
177
|
+
self._database_cache[id_cache_key] = database
|
178
|
+
|
179
|
+
if original_name:
|
180
|
+
name_cache_key = self._create_name_cache_key(original_name, token)
|
181
|
+
self._database_cache[name_cache_key] = database
|
182
|
+
|
183
|
+
def _create_id_cache_key(self, database_id: str) -> str:
|
184
|
+
"""Create cache key for database ID."""
|
185
|
+
return f"id:{database_id}"
|
186
|
+
|
187
|
+
def _create_name_cache_key(self, database_name: str, token: Optional[str]) -> str:
|
188
|
+
"""Create cache key for database name."""
|
189
|
+
token_suffix = f":{hash(token)}" if token else ":default"
|
190
|
+
return f"name:{database_name.lower().strip()}{token_suffix}"
|
191
|
+
|
192
|
+
def _create_from_response(
|
193
|
+
self, db_response: NotionDatabaseResponse, token: Optional[str]
|
194
|
+
) -> NotionDatabase:
|
195
|
+
"""Create NotionDatabase instance from API response."""
|
196
|
+
from notionary import NotionDatabase
|
197
|
+
|
198
|
+
title = self._extract_title(db_response)
|
199
|
+
emoji_icon = self._extract_emoji_icon(db_response)
|
200
|
+
|
201
|
+
instance = NotionDatabase(
|
202
|
+
database_id=db_response.id,
|
203
|
+
title=title,
|
204
|
+
url=db_response.url,
|
205
|
+
emoji_icon=emoji_icon,
|
206
|
+
properties=db_response.properties,
|
207
|
+
token=token,
|
208
|
+
)
|
209
|
+
|
210
|
+
self.logger.info(
|
211
|
+
"Created database manager: '%s' (ID: %s)", title, db_response.id
|
212
|
+
)
|
213
|
+
|
214
|
+
return instance
|
215
|
+
|
216
|
+
def _extract_title(self, db_response: NotionDatabaseResponse) -> str:
|
217
|
+
"""Extract title from database response."""
|
218
|
+
if db_response.title and len(db_response.title) > 0:
|
219
|
+
return db_response.title[0].plain_text
|
220
|
+
return "Untitled Database"
|
221
|
+
|
222
|
+
def _extract_emoji_icon(self, db_response: NotionDatabaseResponse) -> Optional[str]:
|
223
|
+
"""Extract emoji from database response."""
|
224
|
+
if not db_response.icon:
|
225
|
+
return None
|
226
|
+
|
227
|
+
if db_response.icon.type == "emoji":
|
228
|
+
return db_response.icon.emoji
|
229
|
+
|
230
|
+
return None
|
File without changes
|
@@ -1,10 +1,11 @@
|
|
1
1
|
from pydantic import BaseModel
|
2
2
|
from dataclasses import dataclass
|
3
|
-
from typing import Optional, List, Dict, Any, Literal
|
3
|
+
from typing import Optional, List, Dict, Any, Literal, Union
|
4
4
|
|
5
|
-
from notionary.models.notion_page_response import Icon
|
5
|
+
from notionary.models.notion_page_response import Cover, Icon
|
6
6
|
|
7
7
|
|
8
|
+
# Reusing existing types from your codebase
|
8
9
|
@dataclass
|
9
10
|
class TextContent:
|
10
11
|
content: str
|
@@ -27,11 +28,214 @@ class User:
|
|
27
28
|
|
28
29
|
@dataclass
|
29
30
|
class Parent:
|
30
|
-
type: Literal["page_id", "workspace", "block_id"]
|
31
|
+
type: Literal["page_id", "workspace", "block_id", "database_id"]
|
31
32
|
page_id: Optional[str] = None
|
32
|
-
block_id: Optional[str] = None
|
33
|
+
block_id: Optional[str] = None
|
34
|
+
database_id: Optional[str] = None
|
33
35
|
|
34
36
|
|
37
|
+
# Rich text types for Pydantic models
|
38
|
+
class TextContentPydantic(BaseModel):
|
39
|
+
content: str
|
40
|
+
link: Optional[Dict[str, str]] = None
|
41
|
+
|
42
|
+
|
43
|
+
class Annotations(BaseModel):
|
44
|
+
bold: bool
|
45
|
+
italic: bool
|
46
|
+
strikethrough: bool
|
47
|
+
underline: bool
|
48
|
+
code: bool
|
49
|
+
color: str
|
50
|
+
|
51
|
+
|
52
|
+
class RichTextItemPydantic(BaseModel):
|
53
|
+
type: str # 'text', 'mention', 'equation'
|
54
|
+
text: Optional[TextContentPydantic] = None
|
55
|
+
annotations: Annotations
|
56
|
+
plain_text: str
|
57
|
+
href: Optional[str] = None
|
58
|
+
|
59
|
+
|
60
|
+
# Database property schema types (these are schema definitions, not values)
|
61
|
+
class StatusOption(BaseModel):
|
62
|
+
id: str
|
63
|
+
name: str
|
64
|
+
color: str
|
65
|
+
description: Optional[str] = None
|
66
|
+
|
67
|
+
|
68
|
+
class StatusGroup(BaseModel):
|
69
|
+
id: str
|
70
|
+
name: str
|
71
|
+
color: str
|
72
|
+
option_ids: List[str]
|
73
|
+
|
74
|
+
|
75
|
+
class StatusPropertySchema(BaseModel):
|
76
|
+
options: List[StatusOption]
|
77
|
+
groups: List[StatusGroup]
|
78
|
+
|
79
|
+
|
80
|
+
class DatabaseStatusProperty(BaseModel):
|
81
|
+
id: str
|
82
|
+
name: str
|
83
|
+
type: Literal["status"]
|
84
|
+
status: StatusPropertySchema
|
85
|
+
|
86
|
+
|
87
|
+
class RelationPropertySchema(BaseModel):
|
88
|
+
database_id: str
|
89
|
+
type: str # "single_property"
|
90
|
+
single_property: Dict[str, Any]
|
91
|
+
|
92
|
+
|
93
|
+
class DatabaseRelationProperty(BaseModel):
|
94
|
+
id: str
|
95
|
+
name: str
|
96
|
+
type: Literal["relation"]
|
97
|
+
relation: RelationPropertySchema
|
98
|
+
|
99
|
+
|
100
|
+
class DatabaseUrlProperty(BaseModel):
|
101
|
+
id: str
|
102
|
+
name: str
|
103
|
+
type: Literal["url"]
|
104
|
+
url: Dict[str, Any] # Usually empty dict
|
105
|
+
|
106
|
+
|
107
|
+
class DatabaseRichTextProperty(BaseModel):
|
108
|
+
id: str
|
109
|
+
name: str
|
110
|
+
type: Literal["rich_text"]
|
111
|
+
rich_text: Dict[str, Any] # Usually empty dict
|
112
|
+
|
113
|
+
|
114
|
+
class MultiSelectOption(BaseModel):
|
115
|
+
id: str
|
116
|
+
name: str
|
117
|
+
color: str
|
118
|
+
description: Optional[str] = None
|
119
|
+
|
120
|
+
|
121
|
+
class MultiSelectPropertySchema(BaseModel):
|
122
|
+
options: List[MultiSelectOption]
|
123
|
+
|
124
|
+
|
125
|
+
class DatabaseMultiSelectProperty(BaseModel):
|
126
|
+
id: str
|
127
|
+
name: str
|
128
|
+
type: Literal["multi_select"]
|
129
|
+
multi_select: MultiSelectPropertySchema
|
130
|
+
|
131
|
+
|
132
|
+
class DatabaseTitleProperty(BaseModel):
|
133
|
+
id: str
|
134
|
+
name: str
|
135
|
+
type: Literal["title"]
|
136
|
+
title: Dict[str, Any] # Usually empty dict
|
137
|
+
|
138
|
+
|
139
|
+
# Generic database property for unknown types
|
140
|
+
class GenericDatabaseProperty(BaseModel):
|
141
|
+
id: str
|
142
|
+
name: str
|
143
|
+
type: str
|
144
|
+
|
145
|
+
class Config:
|
146
|
+
extra = "allow"
|
147
|
+
|
148
|
+
|
149
|
+
# Union of all database property types
|
150
|
+
DatabaseProperty = Union[
|
151
|
+
DatabaseStatusProperty,
|
152
|
+
DatabaseRelationProperty,
|
153
|
+
DatabaseUrlProperty,
|
154
|
+
DatabaseRichTextProperty,
|
155
|
+
DatabaseMultiSelectProperty,
|
156
|
+
DatabaseTitleProperty,
|
157
|
+
GenericDatabaseProperty,
|
158
|
+
]
|
159
|
+
|
160
|
+
|
161
|
+
# Page property value types (these are actual values, not schemas)
|
162
|
+
class StatusValue(BaseModel):
|
163
|
+
id: str
|
164
|
+
name: str
|
165
|
+
color: str
|
166
|
+
|
167
|
+
|
168
|
+
class StatusProperty(BaseModel):
|
169
|
+
id: str
|
170
|
+
type: str # 'status'
|
171
|
+
status: Optional[StatusValue] = None
|
172
|
+
|
173
|
+
|
174
|
+
class RelationItem(BaseModel):
|
175
|
+
id: str
|
176
|
+
|
177
|
+
|
178
|
+
class RelationProperty(BaseModel):
|
179
|
+
id: str
|
180
|
+
type: str # 'relation'
|
181
|
+
relation: List[RelationItem]
|
182
|
+
has_more: bool
|
183
|
+
|
184
|
+
|
185
|
+
class UrlProperty(BaseModel):
|
186
|
+
id: str
|
187
|
+
type: str # 'url'
|
188
|
+
url: Optional[str] = None
|
189
|
+
|
190
|
+
|
191
|
+
class RichTextProperty(BaseModel):
|
192
|
+
id: str
|
193
|
+
type: str # 'rich_text'
|
194
|
+
rich_text: List[RichTextItemPydantic]
|
195
|
+
|
196
|
+
|
197
|
+
class MultiSelectItem(BaseModel):
|
198
|
+
id: str
|
199
|
+
name: str
|
200
|
+
color: str
|
201
|
+
|
202
|
+
|
203
|
+
class MultiSelectProperty(BaseModel):
|
204
|
+
id: str
|
205
|
+
type: str # 'multi_select'
|
206
|
+
multi_select: List[MultiSelectItem]
|
207
|
+
|
208
|
+
|
209
|
+
class TitleProperty(BaseModel):
|
210
|
+
id: str
|
211
|
+
type: str # 'title'
|
212
|
+
title: List[RichTextItemPydantic]
|
213
|
+
|
214
|
+
|
215
|
+
# Cover types
|
216
|
+
class ExternalCover(BaseModel):
|
217
|
+
url: str
|
218
|
+
|
219
|
+
|
220
|
+
class NotionCover(BaseModel):
|
221
|
+
type: str # 'external', 'file'
|
222
|
+
external: Optional[ExternalCover] = None
|
223
|
+
|
224
|
+
|
225
|
+
# Parent types for Pydantic
|
226
|
+
class NotionParent(BaseModel):
|
227
|
+
type: str # 'database_id', 'page_id', 'workspace'
|
228
|
+
database_id: Optional[str] = None
|
229
|
+
page_id: Optional[str] = None
|
230
|
+
|
231
|
+
|
232
|
+
# User type for Pydantic
|
233
|
+
class NotionUser(BaseModel):
|
234
|
+
object: str # 'user'
|
235
|
+
id: str
|
236
|
+
|
237
|
+
|
238
|
+
# Database object
|
35
239
|
class NotionDatabaseResponse(BaseModel):
|
36
240
|
"""
|
37
241
|
Represents the response from the Notion API when retrieving a database.
|
@@ -39,19 +243,96 @@ class NotionDatabaseResponse(BaseModel):
|
|
39
243
|
|
40
244
|
object: Literal["database"]
|
41
245
|
id: str
|
42
|
-
cover: Optional[Any]
|
43
|
-
icon: Optional[Icon]
|
246
|
+
cover: Optional[Any] = None
|
247
|
+
icon: Optional[Icon] = None
|
248
|
+
cover: Optional[Cover]
|
44
249
|
created_time: str
|
45
250
|
last_edited_time: str
|
46
|
-
created_by:
|
47
|
-
last_edited_by:
|
48
|
-
title: List[
|
251
|
+
created_by: NotionUser
|
252
|
+
last_edited_by: NotionUser
|
253
|
+
title: List[RichTextItemPydantic]
|
49
254
|
description: List[Any]
|
50
255
|
is_inline: bool
|
51
|
-
properties: Dict[
|
52
|
-
|
256
|
+
properties: Dict[
|
257
|
+
str, Any
|
258
|
+
] # Using Any for flexibility with different property schemas
|
259
|
+
parent: NotionParent
|
53
260
|
url: str
|
54
|
-
public_url: Optional[str]
|
261
|
+
public_url: Optional[str] = None
|
55
262
|
archived: bool
|
56
263
|
in_trash: bool
|
57
|
-
|
264
|
+
|
265
|
+
|
266
|
+
class NotionPageResponse(BaseModel):
|
267
|
+
object: Literal["page"]
|
268
|
+
id: str
|
269
|
+
created_time: str
|
270
|
+
last_edited_time: str
|
271
|
+
created_by: NotionUser
|
272
|
+
last_edited_by: NotionUser
|
273
|
+
cover: Optional[NotionCover] = None
|
274
|
+
icon: Optional[Icon] = None
|
275
|
+
parent: NotionParent
|
276
|
+
archived: bool
|
277
|
+
in_trash: bool
|
278
|
+
properties: Dict[str, Any]
|
279
|
+
url: str
|
280
|
+
public_url: Optional[str] = None
|
281
|
+
|
282
|
+
|
283
|
+
class NotionQueryResponse(BaseModel):
|
284
|
+
"""
|
285
|
+
Complete Notion search/query response model that can contain both pages and databases.
|
286
|
+
"""
|
287
|
+
|
288
|
+
object: Literal["list"]
|
289
|
+
results: List[Union[NotionPageResponse, NotionDatabaseResponse]]
|
290
|
+
next_cursor: Optional[str] = None
|
291
|
+
has_more: bool
|
292
|
+
type: Literal["page_or_database"]
|
293
|
+
page_or_database: Dict[str, Any]
|
294
|
+
request_id: str
|
295
|
+
|
296
|
+
|
297
|
+
# Specific response type for database queries (pages only)
|
298
|
+
class NotionQueryDatabaseResponse(BaseModel):
|
299
|
+
"""
|
300
|
+
Notion database query response model for querying pages within a database.
|
301
|
+
"""
|
302
|
+
|
303
|
+
object: Literal["list"]
|
304
|
+
results: List[NotionPageResponse]
|
305
|
+
next_cursor: Optional[str] = None
|
306
|
+
has_more: bool
|
307
|
+
type: Literal["page_or_database"]
|
308
|
+
page_or_database: Dict[str, Any]
|
309
|
+
request_id: str
|
310
|
+
|
311
|
+
|
312
|
+
# Specific response type for search results (can be mixed)
|
313
|
+
class NotionSearchResponse(BaseModel):
|
314
|
+
"""
|
315
|
+
Notion search response model that can return both pages and databases.
|
316
|
+
"""
|
317
|
+
|
318
|
+
object: Literal["list"]
|
319
|
+
results: List[Union[NotionPageResponse, NotionDatabaseResponse]]
|
320
|
+
next_cursor: Optional[str] = None
|
321
|
+
has_more: bool
|
322
|
+
type: Literal["page_or_database"]
|
323
|
+
page_or_database: Dict[str, Any]
|
324
|
+
request_id: str
|
325
|
+
|
326
|
+
|
327
|
+
class NotionDatabaseSearchResponse(BaseModel):
|
328
|
+
"""
|
329
|
+
Notion search response model for database-only searches.
|
330
|
+
"""
|
331
|
+
|
332
|
+
object: Literal["list"]
|
333
|
+
results: List[NotionDatabaseResponse]
|
334
|
+
next_cursor: Optional[str] = None
|
335
|
+
has_more: bool
|
336
|
+
type: Literal["page_or_database"]
|
337
|
+
page_or_database: Dict[str, Any]
|
338
|
+
request_id: str
|
@@ -1,73 +1,52 @@
|
|
1
|
-
from dataclasses import dataclass
|
2
1
|
from typing import Literal, Optional, Dict, Any, Union
|
3
2
|
|
4
3
|
from pydantic import BaseModel
|
5
4
|
|
6
5
|
|
7
|
-
|
8
|
-
class User:
|
6
|
+
class User(BaseModel):
|
9
7
|
"""Represents a Notion user object."""
|
10
8
|
|
11
9
|
object: str
|
12
10
|
id: str
|
13
11
|
|
14
12
|
|
15
|
-
|
16
|
-
class ExternalFile:
|
13
|
+
class ExternalFile(BaseModel):
|
17
14
|
"""Represents an external file, e.g., for cover images."""
|
18
15
|
|
19
16
|
url: str
|
20
17
|
|
21
18
|
|
22
|
-
|
23
|
-
class Cover:
|
19
|
+
class Cover(BaseModel):
|
24
20
|
"""Cover image for a Notion page."""
|
25
21
|
|
26
22
|
type: str
|
27
23
|
external: ExternalFile
|
28
24
|
|
29
25
|
|
30
|
-
|
31
|
-
class EmojiIcon:
|
26
|
+
class EmojiIcon(BaseModel):
|
32
27
|
type: Literal["emoji"]
|
33
28
|
emoji: str
|
34
29
|
|
35
30
|
|
36
|
-
|
37
|
-
class ExternalIcon:
|
31
|
+
class ExternalIcon(BaseModel):
|
38
32
|
type: Literal["external"]
|
39
33
|
external: ExternalFile
|
40
34
|
|
41
35
|
|
42
|
-
|
43
|
-
class FileObject:
|
44
|
-
url: str
|
45
|
-
expiry_time: str
|
46
|
-
|
47
|
-
|
48
|
-
@dataclass
|
49
|
-
class FileIcon:
|
50
|
-
type: Literal["file"]
|
51
|
-
file: FileObject
|
52
|
-
|
53
|
-
|
54
|
-
Icon = Union[EmojiIcon, ExternalIcon, FileIcon]
|
36
|
+
Icon = Union[EmojiIcon, ExternalIcon]
|
55
37
|
|
56
38
|
|
57
|
-
|
58
|
-
class DatabaseParent:
|
39
|
+
class DatabaseParent(BaseModel):
|
59
40
|
type: Literal["database_id"]
|
60
41
|
database_id: str
|
61
42
|
|
62
43
|
|
63
|
-
|
64
|
-
class PageParent:
|
44
|
+
class PageParent(BaseModel):
|
65
45
|
type: Literal["page_id"]
|
66
46
|
page_id: str
|
67
47
|
|
68
48
|
|
69
|
-
|
70
|
-
class WorkspaceParent:
|
49
|
+
class WorkspaceParent(BaseModel):
|
71
50
|
type: Literal["workspace"]
|
72
51
|
workspace: bool = True
|
73
52
|
|
@@ -75,7 +54,6 @@ class WorkspaceParent:
|
|
75
54
|
Parent = Union[DatabaseParent, PageParent, WorkspaceParent]
|
76
55
|
|
77
56
|
|
78
|
-
@dataclass
|
79
57
|
class NotionPageResponse(BaseModel):
|
80
58
|
"""
|
81
59
|
Represents a full Notion page object as returned by the Notion API.
|
File without changes
|
File without changes
|