notionary 0.1.11__py3-none-any.whl → 0.1.13__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 (55) hide show
  1. notionary/__init__.py +21 -6
  2. notionary/{core/converters → converters}/elements/audio_element.py +7 -5
  3. notionary/{core/converters → converters}/elements/bookmark_element.py +1 -1
  4. notionary/{core/converters → converters}/elements/callout_element.py +2 -2
  5. notionary/{core/converters → converters}/elements/code_block_element.py +1 -1
  6. notionary/{core/converters → converters}/elements/column_element.py +1 -1
  7. notionary/{core/converters → converters}/elements/divider_element.py +1 -1
  8. notionary/{core/converters → converters}/elements/embed_element.py +3 -5
  9. notionary/{core/converters → converters}/elements/heading_element.py +2 -2
  10. notionary/{core/converters → converters}/elements/image_element.py +1 -1
  11. notionary/{core/converters → converters}/elements/list_element.py +2 -2
  12. notionary/{core/converters → converters}/elements/paragraph_element.py +2 -2
  13. notionary/{core/converters → converters}/elements/qoute_element.py +1 -1
  14. notionary/{core/converters → converters}/elements/table_element.py +2 -2
  15. notionary/{core/converters → converters}/elements/todo_lists.py +2 -2
  16. notionary/{core/converters → converters}/elements/toggle_element.py +24 -21
  17. notionary/{core/converters → converters}/elements/video_element.py +1 -1
  18. notionary/{core/converters → converters}/markdown_to_notion_converter.py +72 -111
  19. notionary/{core/converters → converters}/notion_to_markdown_converter.py +2 -2
  20. notionary/{core/converters → converters}/registry/block_element_registry.py +5 -5
  21. notionary/{core/converters → converters}/registry/block_element_registry_builder.py +18 -18
  22. notionary/database/database_discovery.py +142 -0
  23. notionary/{core/database → database}/database_info_service.py +1 -1
  24. notionary/{core/database/notion_database_manager.py → database/notion_database.py} +33 -57
  25. notionary/{core/database/notion_database_manager_factory.py → database/notion_database_factory.py} +18 -16
  26. notionary/{core/notion_client.py → notion_client.py} +4 -2
  27. notionary/page/content/notion_page_content_chunker.py +84 -0
  28. notionary/{core/page → page}/content/page_content_manager.py +29 -13
  29. notionary/{core/page → page}/metadata/metadata_editor.py +59 -46
  30. notionary/{core/page → page}/metadata/notion_icon_manager.py +10 -12
  31. notionary/{core/page → page}/metadata/notion_page_cover_manager.py +16 -21
  32. notionary/page/notion_page.py +504 -0
  33. notionary/page/notion_page_factory.py +256 -0
  34. notionary/{core/page → page}/properites/database_property_service.py +115 -99
  35. notionary/{core/page → page}/properites/page_property_manager.py +81 -52
  36. notionary/{core/page → page}/properites/property_formatter.py +1 -1
  37. notionary/{core/page → page}/properites/property_operation_result.py +43 -30
  38. notionary/{core/page → page}/properites/property_value_extractor.py +26 -8
  39. notionary/{core/page → page}/relations/notion_page_relation_manager.py +72 -53
  40. notionary/{core/page → page}/relations/notion_page_title_resolver.py +12 -12
  41. notionary/{core/page → page}/relations/page_database_relation.py +15 -15
  42. notionary/{core/page → page}/relations/relation_operation_result.py +50 -41
  43. notionary/util/page_id_utils.py +14 -8
  44. {notionary-0.1.11.dist-info → notionary-0.1.13.dist-info}/METADATA +1 -1
  45. notionary-0.1.13.dist-info/RECORD +56 -0
  46. notionary/core/database/notion_database_schema.py +0 -104
  47. notionary/core/page/notion_page_manager.py +0 -322
  48. notionary-0.1.11.dist-info/RECORD +0 -54
  49. /notionary/{core/converters → converters}/__init__.py +0 -0
  50. /notionary/{core/converters → converters}/elements/notion_block_element.py +0 -0
  51. /notionary/{core/converters → converters}/elements/text_inline_formatter.py +0 -0
  52. /notionary/{core/database → database}/models/page_result.py +0 -0
  53. {notionary-0.1.11.dist-info → notionary-0.1.13.dist-info}/WHEEL +0 -0
  54. {notionary-0.1.11.dist-info → notionary-0.1.13.dist-info}/licenses/LICENSE +0 -0
  55. {notionary-0.1.11.dist-info → notionary-0.1.13.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,256 @@
1
+ import logging
2
+ from typing import List, Optional, Dict, Any
3
+ from difflib import SequenceMatcher
4
+
5
+ from notionary import NotionPage, NotionClient
6
+ from notionary.util.logging_mixin import LoggingMixin
7
+ from notionary.util.page_id_utils import format_uuid, extract_and_validate_page_id
8
+
9
+
10
+ class NotionPageFactory(LoggingMixin):
11
+ """
12
+ Factory class for creating NotionPage instances.
13
+ Provides methods for creating page instances by page ID, URL, or name.
14
+ """
15
+
16
+ @classmethod
17
+ def class_logger(cls):
18
+ """Class logger - for class methods"""
19
+ return logging.getLogger(cls.__name__)
20
+
21
+ @classmethod
22
+ async def from_page_id(
23
+ cls, page_id: str, token: Optional[str] = None
24
+ ) -> NotionPage:
25
+ """
26
+ Create a NotionPage from a page ID.
27
+
28
+ Args:
29
+ page_id: The ID of the Notion page
30
+ token: Optional Notion API token (uses environment variable if not provided)
31
+
32
+ Returns:
33
+ An initialized NotionPage instance
34
+
35
+ Raises:
36
+ NotionError: If there is any error during page creation or connection
37
+ """
38
+ logger = cls.class_logger()
39
+
40
+ try:
41
+ formatted_id = format_uuid(page_id) or page_id
42
+
43
+ page = NotionPage(page_id=formatted_id, token=token)
44
+
45
+ logger.info("Successfully created page instance for ID: %s", formatted_id)
46
+ return page
47
+
48
+ except Exception as e:
49
+ error_msg = f"Error connecting to page {page_id}: {str(e)}"
50
+ logger.error(error_msg)
51
+
52
+ @classmethod
53
+ async def from_url(cls, url: str, token: Optional[str] = None) -> NotionPage:
54
+ """
55
+ Create a NotionPage from a Notion URL.
56
+
57
+ Args:
58
+ url: The URL of the Notion page
59
+ token: Optional Notion API token (uses environment variable if not provided)
60
+
61
+ Returns:
62
+ An initialized NotionPage instance
63
+
64
+ Raises:
65
+ NotionError: If there is any error during page creation or connection
66
+ """
67
+ logger = cls.class_logger()
68
+
69
+ try:
70
+ page_id = extract_and_validate_page_id(url=url)
71
+ if not page_id:
72
+ error_msg = f"Could not extract valid page ID from URL: {url}"
73
+ logger.error(error_msg)
74
+
75
+ page = NotionPage(page_id=page_id, url=url, token=token)
76
+
77
+ logger.info(
78
+ "Successfully created page instance from URL for ID: %s", page_id
79
+ )
80
+ return page
81
+
82
+ except Exception as e:
83
+ error_msg = f"Error connecting to page with URL {url}: {str(e)}"
84
+ logger.error(error_msg)
85
+
86
+ @classmethod
87
+ async def from_page_name(
88
+ cls, page_name: str, token: Optional[str] = None
89
+ ) -> NotionPage:
90
+ """
91
+ Create a NotionPage by finding a page with a matching name.
92
+ Uses fuzzy matching to find the closest match to the given name.
93
+ If no good match is found, suggests closest alternatives ("Did you mean?").
94
+
95
+ Args:
96
+ page_name: The name of the Notion page to search for
97
+ token: Optional Notion API token (uses environment variable if not provided)
98
+
99
+ Returns:
100
+ An initialized NotionPage instance
101
+
102
+ Raises:
103
+ NotionError: If there is any error during page search or connection
104
+ NotionPageNotFoundError: If no matching page found, includes suggestions
105
+ """
106
+ logger = cls.class_logger()
107
+ logger.debug("Searching for page with name: %s", page_name)
108
+
109
+ client = NotionClient(token=token)
110
+
111
+ try:
112
+ logger.debug("Using search endpoint to find pages")
113
+
114
+ search_payload = {
115
+ "filter": {"property": "object", "value": "page"},
116
+ "page_size": 100,
117
+ }
118
+
119
+ response = await client.post("search", search_payload)
120
+
121
+ if not response or "results" not in response:
122
+ error_msg = "Failed to fetch pages using search endpoint"
123
+ logger.error(error_msg)
124
+
125
+ pages = response.get("results", [])
126
+
127
+ if not pages:
128
+ error_msg = f"No pages found matching '{page_name}'"
129
+ logger.warning(error_msg)
130
+
131
+ logger.debug("Found %d pages, searching for best match", len(pages))
132
+
133
+ # Store all matches with their scores for potential suggestions
134
+ matches = []
135
+ best_match = None
136
+ best_score = 0
137
+
138
+ for page in pages:
139
+ title = cls._extract_title_from_page(page)
140
+ score = SequenceMatcher(None, page_name.lower(), title.lower()).ratio()
141
+
142
+ matches.append((page, title, score))
143
+
144
+ if score > best_score:
145
+ best_score = score
146
+ best_match = page
147
+
148
+ if best_score < 0.6 or not best_match:
149
+ # Sort matches by score in descending order
150
+ matches.sort(key=lambda x: x[2], reverse=True)
151
+
152
+ # Take top N suggestions (adjust as needed)
153
+ suggestions = [title for _, title, _ in matches[:5]]
154
+
155
+ error_msg = f"No good match found for '{page_name}'. Did you mean one of these?\n"
156
+ error_msg += "\n".join(f"- {suggestion}" for suggestion in suggestions)
157
+
158
+ logger.warning(
159
+ "No good match found for '%s'. Best score: %.2f",
160
+ page_name,
161
+ best_score,
162
+ )
163
+
164
+ page_id = best_match.get("id")
165
+
166
+ if not page_id:
167
+ error_msg = "Best match page has no ID"
168
+ logger.error(error_msg)
169
+
170
+ matched_name = cls._extract_title_from_page(best_match)
171
+
172
+ logger.info(
173
+ "Found matching page: '%s' (ID: %s) with score: %.2f",
174
+ matched_name,
175
+ page_id,
176
+ best_score,
177
+ )
178
+
179
+ page = NotionPage(
180
+ page_id=page_id, title=matched_name, token=token
181
+ )
182
+
183
+ logger.info("Successfully created page instance for '%s'", matched_name)
184
+ await client.close()
185
+ return page
186
+
187
+ except Exception as e:
188
+ error_msg = f"Error finding page by name: {str(e)}"
189
+ logger.error(error_msg)
190
+
191
+ @classmethod
192
+ def _extract_title_from_page(cls, page: Dict[str, Any]) -> str:
193
+ """
194
+ Extract the title from a page object.
195
+
196
+ Args:
197
+ page: The page object returned from the Notion API
198
+
199
+ Returns:
200
+ The title of the page
201
+
202
+ Raises:
203
+ NotionError: If the title cannot be extracted
204
+ """
205
+ try:
206
+ if "properties" in page:
207
+ for prop_value in page["properties"].values():
208
+ if prop_value.get("type") != "title":
209
+ continue
210
+ title_array = prop_value.get("title", [])
211
+ if not title_array:
212
+ continue
213
+ return cls._extract_text_from_rich_text(title_array)
214
+
215
+ if "child_page" in page:
216
+ return page.get("child_page", {}).get("title", "Untitled")
217
+
218
+ return "Untitled"
219
+
220
+ except Exception as e:
221
+ error_msg = f"Error extracting page title: {str(e)}"
222
+ cls.class_logger().warning(error_msg)
223
+ return "Untitled"
224
+
225
+ @classmethod
226
+ def _extract_text_from_rich_text(cls, rich_text: List[Dict[str, Any]]) -> str:
227
+ """
228
+ Extract plain text from a rich text array.
229
+
230
+ Args:
231
+ rich_text: A list of rich text objects from the Notion API
232
+
233
+ Returns:
234
+ The combined plain text content
235
+ """
236
+ if not rich_text:
237
+ return ""
238
+
239
+ text_parts = []
240
+ for text_obj in rich_text:
241
+ if "plain_text" in text_obj:
242
+ text_parts.append(text_obj["plain_text"])
243
+
244
+ return "".join(text_parts)
245
+
246
+
247
+ async def demo():
248
+ clipboard = await NotionPageFactory.from_page_name("Jarvis Clipboard")
249
+ icon = await clipboard.get_icon()
250
+ print(f"Icon: {icon}")
251
+
252
+
253
+ if __name__ == "__main__":
254
+ import asyncio
255
+
256
+ asyncio.run(demo())
@@ -1,5 +1,5 @@
1
1
  from typing import Dict, List, Optional, Any, Tuple
2
- from notionary.core.notion_client import NotionClient
2
+ from notionary.notion_client import NotionClient
3
3
  from notionary.util.logging_mixin import LoggingMixin
4
4
 
5
5
 
@@ -12,7 +12,7 @@ class DatabasePropertyService(LoggingMixin):
12
12
  def __init__(self, database_id: str, client: NotionClient):
13
13
  """
14
14
  Initialize the database property service.
15
-
15
+
16
16
  Args:
17
17
  database_id: ID of the Notion database
18
18
  client: Instance of NotionClient
@@ -20,20 +20,20 @@ class DatabasePropertyService(LoggingMixin):
20
20
  self._database_id = database_id
21
21
  self._client = client
22
22
  self._schema = None
23
-
23
+
24
24
  async def load_schema(self, force_refresh=False) -> bool:
25
25
  """
26
26
  Loads the database schema.
27
-
27
+
28
28
  Args:
29
29
  force_refresh: Whether to force a refresh of the schema
30
-
30
+
31
31
  Returns:
32
32
  bool: True if schema loaded successfully, False otherwise
33
33
  """
34
34
  if self._schema is not None and not force_refresh:
35
35
  return True
36
-
36
+
37
37
  try:
38
38
  database = await self._client.get(f"databases/{self._database_id}")
39
39
  if database and "properties" in database:
@@ -41,290 +41,306 @@ class DatabasePropertyService(LoggingMixin):
41
41
  self.logger.debug("Loaded schema for database %s", self._database_id)
42
42
  return True
43
43
  else:
44
- self.logger.error("Failed to load schema: missing 'properties' in response")
44
+ self.logger.error(
45
+ "Failed to load schema: missing 'properties' in response"
46
+ )
45
47
  return False
46
48
  except Exception as e:
47
49
  self.logger.error("Error loading database schema: %s", str(e))
48
50
  return False
49
-
51
+
50
52
  async def _ensure_schema_loaded(self) -> None:
51
53
  """
52
54
  Ensures the schema is loaded before accessing it.
53
55
  """
54
56
  if self._schema is None:
55
57
  await self.load_schema()
56
-
58
+
57
59
  async def get_schema(self) -> Dict[str, Any]:
58
60
  """
59
61
  Gets the database schema.
60
-
62
+
61
63
  Returns:
62
64
  Dict[str, Any]: The database schema
63
65
  """
64
66
  await self._ensure_schema_loaded()
65
67
  return self._schema or {}
66
-
68
+
67
69
  async def get_property_types(self) -> Dict[str, str]:
68
70
  """
69
71
  Gets all property types for the database.
70
-
72
+
71
73
  Returns:
72
74
  Dict[str, str]: Dictionary mapping property names to their types
73
75
  """
74
76
  await self._ensure_schema_loaded()
75
-
77
+
76
78
  if not self._schema:
77
79
  return {}
78
-
80
+
79
81
  return {
80
82
  prop_name: prop_data.get("type", "unknown")
81
83
  for prop_name, prop_data in self._schema.items()
82
84
  }
83
-
85
+
84
86
  async def get_property_schema(self, property_name: str) -> Optional[Dict[str, Any]]:
85
87
  """
86
88
  Gets the schema for a specific property.
87
-
89
+
88
90
  Args:
89
91
  property_name: The name of the property
90
-
92
+
91
93
  Returns:
92
94
  Optional[Dict[str, Any]]: The property schema or None if not found
93
95
  """
94
96
  await self._ensure_schema_loaded()
95
-
97
+
96
98
  if not self._schema or property_name not in self._schema:
97
99
  return None
98
-
100
+
99
101
  return self._schema[property_name]
100
-
102
+
101
103
  async def get_property_type(self, property_name: str) -> Optional[str]:
102
104
  """
103
105
  Gets the type of a specific property.
104
-
106
+
105
107
  Args:
106
108
  property_name: The name of the property
107
-
109
+
108
110
  Returns:
109
111
  Optional[str]: The property type or None if not found
110
112
  """
111
113
  property_schema = await self.get_property_schema(property_name)
112
-
114
+
113
115
  if not property_schema:
114
116
  return None
115
-
117
+
116
118
  return property_schema.get("type")
117
-
119
+
118
120
  async def property_exists(self, property_name: str) -> bool:
119
121
  """
120
122
  Checks if a property exists in the database.
121
-
123
+
122
124
  Args:
123
125
  property_name: The name of the property
124
-
126
+
125
127
  Returns:
126
128
  bool: True if the property exists, False otherwise
127
129
  """
128
130
  property_schema = await self.get_property_schema(property_name)
129
131
  return property_schema is not None
130
-
132
+
131
133
  async def get_property_options(self, property_name: str) -> List[Dict[str, Any]]:
132
134
  """
133
135
  Gets the available options for a property (select, multi_select, status).
134
-
136
+
135
137
  Args:
136
138
  property_name: The name of the property
137
-
139
+
138
140
  Returns:
139
141
  List[Dict[str, Any]]: List of available options with their metadata
140
142
  """
141
143
  property_schema = await self.get_property_schema(property_name)
142
-
144
+
143
145
  if not property_schema:
144
146
  return []
145
-
147
+
146
148
  property_type = property_schema.get("type")
147
-
149
+
148
150
  if property_type in ["select", "multi_select", "status"]:
149
151
  return property_schema.get(property_type, {}).get("options", [])
150
-
152
+
151
153
  return []
152
-
154
+
153
155
  async def get_option_names(self, property_name: str) -> List[str]:
154
156
  """
155
157
  Gets the available option names for a property (select, multi_select, status).
156
-
158
+
157
159
  Args:
158
160
  property_name: The name of the property
159
-
161
+
160
162
  Returns:
161
163
  List[str]: List of available option names
162
164
  """
163
165
  options = await self.get_property_options(property_name)
164
166
  return [option.get("name", "") for option in options]
165
-
166
- async def get_relation_details(self, property_name: str) -> Optional[Dict[str, Any]]:
167
+
168
+ async def get_relation_details(
169
+ self, property_name: str
170
+ ) -> Optional[Dict[str, Any]]:
167
171
  """
168
172
  Gets details about a relation property, including the related database.
169
-
173
+
170
174
  Args:
171
175
  property_name: The name of the property
172
-
176
+
173
177
  Returns:
174
178
  Optional[Dict[str, Any]]: The relation details or None if not a relation
175
179
  """
176
180
  property_schema = await self.get_property_schema(property_name)
177
-
181
+
178
182
  if not property_schema or property_schema.get("type") != "relation":
179
183
  return None
180
-
184
+
181
185
  return property_schema.get("relation", {})
182
-
183
- async def get_relation_options(self, property_name: str, limit: int = 100) -> List[Dict[str, Any]]:
186
+
187
+ async def get_relation_options(
188
+ self, property_name: str, limit: int = 100
189
+ ) -> List[Dict[str, Any]]:
184
190
  """
185
191
  Gets available options for a relation property by querying the related database.
186
-
192
+
187
193
  Args:
188
194
  property_name: The name of the relation property
189
195
  limit: Maximum number of options to retrieve
190
-
196
+
191
197
  Returns:
192
198
  List[Dict[str, Any]]: List of pages from the related database
193
199
  """
194
200
  relation_details = await self.get_relation_details(property_name)
195
-
201
+
196
202
  if not relation_details or "database_id" not in relation_details:
197
203
  return []
198
-
204
+
199
205
  related_db_id = relation_details["database_id"]
200
-
206
+
201
207
  try:
202
208
  # Query the related database to get options
203
209
  query_result = await self._client.post(
204
210
  f"databases/{related_db_id}/query",
205
211
  {
206
212
  "page_size": limit,
207
- }
213
+ },
208
214
  )
209
-
215
+
210
216
  if not query_result or "results" not in query_result:
211
217
  return []
212
-
218
+
213
219
  # Extract relevant information from each page
214
220
  options = []
215
221
  for page in query_result["results"]:
216
222
  page_id = page.get("id")
217
223
  title = self._extract_title_from_page(page)
218
-
224
+
219
225
  if page_id and title:
220
- options.append({
221
- "id": page_id,
222
- "name": title
223
- })
224
-
226
+ options.append({"id": page_id, "name": title})
227
+
225
228
  return options
226
229
  except Exception as e:
227
230
  self.logger.error(f"Error getting relation options: {str(e)}")
228
231
  return []
229
-
232
+
230
233
  def _extract_title_from_page(self, page: Dict[str, Any]) -> Optional[str]:
231
234
  """
232
235
  Extracts the title from a page object.
233
-
236
+
234
237
  Args:
235
238
  page: The page object from Notion API
236
-
239
+
237
240
  Returns:
238
241
  Optional[str]: The page title or None if not found
239
242
  """
240
243
  if "properties" not in page:
241
244
  return None
242
-
245
+
243
246
  properties = page["properties"]
244
-
247
+
245
248
  # Look for a title property
246
249
  for prop_data in properties.values():
247
250
  if prop_data.get("type") == "title" and "title" in prop_data:
248
251
  title_parts = prop_data["title"]
249
- return "".join([text_obj.get("plain_text", "") for text_obj in title_parts])
250
-
252
+ return "".join(
253
+ [text_obj.get("plain_text", "") for text_obj in title_parts]
254
+ )
255
+
251
256
  return None
252
-
253
- async def validate_property_value(self, property_name: str, value: Any) -> Tuple[bool, Optional[str], Optional[List[str]]]:
257
+
258
+ async def validate_property_value(
259
+ self, property_name: str, value: Any
260
+ ) -> Tuple[bool, Optional[str], Optional[List[str]]]:
254
261
  """
255
262
  Validates a value for a property.
256
-
263
+
257
264
  Args:
258
265
  property_name: The name of the property
259
266
  value: The value to validate
260
-
267
+
261
268
  Returns:
262
- Tuple[bool, Optional[str], Optional[List[str]]]:
269
+ Tuple[bool, Optional[str], Optional[List[str]]]:
263
270
  - Boolean indicating if valid
264
271
  - Error message if invalid
265
272
  - Available options if applicable
266
273
  """
267
274
  property_schema = await self.get_property_schema(property_name)
268
-
275
+
269
276
  if not property_schema:
270
277
  return False, f"Property '{property_name}' does not exist", None
271
-
278
+
272
279
  property_type = property_schema.get("type")
273
-
280
+
274
281
  # Validate select, multi_select, status properties
275
282
  if property_type in ["select", "status"]:
276
283
  options = await self.get_option_names(property_name)
277
-
284
+
278
285
  if isinstance(value, str) and value not in options:
279
- return False, f"Invalid {property_type} option. Value '{value}' is not in the available options.", options
280
-
286
+ return (
287
+ False,
288
+ f"Invalid {property_type} option. Value '{value}' is not in the available options.",
289
+ options,
290
+ )
291
+
281
292
  elif property_type == "multi_select":
282
293
  options = await self.get_option_names(property_name)
283
-
294
+
284
295
  if isinstance(value, list):
285
296
  invalid_values = [val for val in value if val not in options]
286
297
  if invalid_values:
287
- return False, f"Invalid multi_select options: {', '.join(invalid_values)}", options
288
-
298
+ return (
299
+ False,
300
+ f"Invalid multi_select options: {', '.join(invalid_values)}",
301
+ options,
302
+ )
303
+
289
304
  return True, None, None
290
-
291
- async def get_database_metadata(self, include_types: Optional[List[str]] = None) -> Dict[str, Any]:
305
+
306
+ async def get_database_metadata(
307
+ self, include_types: Optional[List[str]] = None
308
+ ) -> Dict[str, Any]:
292
309
  """
293
310
  Gets the complete metadata of the database, including property options.
294
-
311
+
295
312
  Args:
296
313
  include_types: List of property types to include (if None, include all)
297
-
314
+
298
315
  Returns:
299
316
  Dict[str, Any]: The database metadata
300
317
  """
301
318
  await self._ensure_schema_loaded()
302
-
319
+
303
320
  if not self._schema:
304
321
  return {"properties": {}}
305
-
322
+
306
323
  metadata = {"properties": {}}
307
-
324
+
308
325
  for prop_name, prop_data in self._schema.items():
309
326
  prop_type = prop_data.get("type")
310
-
327
+
311
328
  # Skip if we're filtering and this type isn't included
312
329
  if include_types and prop_type not in include_types:
313
330
  continue
314
-
315
- prop_metadata = {
316
- "type": prop_type,
317
- "options": []
318
- }
319
-
331
+
332
+ prop_metadata = {"type": prop_type, "options": []}
333
+
320
334
  # Include options for select, multi_select, status
321
335
  if prop_type in ["select", "multi_select", "status"]:
322
- prop_metadata["options"] = prop_data.get(prop_type, {}).get("options", [])
323
-
336
+ prop_metadata["options"] = prop_data.get(prop_type, {}).get(
337
+ "options", []
338
+ )
339
+
324
340
  # For relation properties, we might want to include related database info
325
341
  elif prop_type == "relation":
326
342
  prop_metadata["relation_details"] = prop_data.get("relation", {})
327
-
343
+
328
344
  metadata["properties"][prop_name] = prop_metadata
329
-
330
- return metadata
345
+
346
+ return metadata