notionary 0.2.28__py3-none-any.whl → 0.3.1__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 (149) hide show
  1. notionary/__init__.py +9 -2
  2. notionary/blocks/__init__.py +5 -0
  3. notionary/blocks/client.py +6 -4
  4. notionary/blocks/enums.py +28 -1
  5. notionary/blocks/rich_text/markdown_rich_text_converter.py +14 -0
  6. notionary/blocks/rich_text/models.py +14 -0
  7. notionary/blocks/rich_text/name_id_resolver/__init__.py +2 -0
  8. notionary/blocks/rich_text/name_id_resolver/data_source.py +32 -0
  9. notionary/blocks/rich_text/rich_text_markdown_converter.py +12 -0
  10. notionary/blocks/rich_text/rich_text_patterns.py +3 -0
  11. notionary/blocks/schemas.py +42 -10
  12. notionary/comments/__init__.py +5 -0
  13. notionary/comments/client.py +7 -10
  14. notionary/comments/factory.py +4 -6
  15. notionary/data_source/http/data_source_instance_client.py +14 -4
  16. notionary/data_source/properties/{models.py → schemas.py} +4 -8
  17. notionary/data_source/query/__init__.py +9 -0
  18. notionary/data_source/query/builder.py +38 -10
  19. notionary/data_source/query/schema.py +13 -10
  20. notionary/data_source/query/validator.py +11 -11
  21. notionary/data_source/schema/registry.py +104 -0
  22. notionary/data_source/schema/service.py +136 -0
  23. notionary/data_source/schemas.py +1 -1
  24. notionary/data_source/service.py +29 -103
  25. notionary/database/service.py +17 -60
  26. notionary/exceptions/__init__.py +5 -1
  27. notionary/exceptions/block_parsing.py +21 -0
  28. notionary/exceptions/search.py +24 -0
  29. notionary/http/client.py +9 -10
  30. notionary/http/models.py +5 -4
  31. notionary/page/content/factory.py +10 -3
  32. notionary/page/content/markdown/builder.py +76 -154
  33. notionary/page/content/markdown/nodes/__init__.py +0 -2
  34. notionary/page/content/markdown/nodes/audio.py +1 -1
  35. notionary/page/content/markdown/nodes/base.py +1 -1
  36. notionary/page/content/markdown/nodes/bookmark.py +1 -1
  37. notionary/page/content/markdown/nodes/breadcrumb.py +1 -1
  38. notionary/page/content/markdown/nodes/bulleted_list.py +31 -8
  39. notionary/page/content/markdown/nodes/callout.py +12 -10
  40. notionary/page/content/markdown/nodes/code.py +3 -5
  41. notionary/page/content/markdown/nodes/columns.py +39 -21
  42. notionary/page/content/markdown/nodes/container.py +64 -0
  43. notionary/page/content/markdown/nodes/divider.py +1 -1
  44. notionary/page/content/markdown/nodes/embed.py +1 -1
  45. notionary/page/content/markdown/nodes/equation.py +1 -1
  46. notionary/page/content/markdown/nodes/file.py +1 -1
  47. notionary/page/content/markdown/nodes/heading.py +26 -6
  48. notionary/page/content/markdown/nodes/image.py +1 -1
  49. notionary/page/content/markdown/nodes/mixins/__init__.py +5 -0
  50. notionary/page/content/markdown/nodes/mixins/caption.py +1 -1
  51. notionary/page/content/markdown/nodes/numbered_list.py +28 -5
  52. notionary/page/content/markdown/nodes/paragraph.py +1 -1
  53. notionary/page/content/markdown/nodes/pdf.py +1 -1
  54. notionary/page/content/markdown/nodes/quote.py +17 -5
  55. notionary/page/content/markdown/nodes/space.py +1 -1
  56. notionary/page/content/markdown/nodes/table.py +1 -1
  57. notionary/page/content/markdown/nodes/table_of_contents.py +1 -1
  58. notionary/page/content/markdown/nodes/todo.py +23 -7
  59. notionary/page/content/markdown/nodes/toggle.py +13 -14
  60. notionary/page/content/markdown/nodes/video.py +1 -1
  61. notionary/page/content/parser/context.py +98 -21
  62. notionary/page/content/parser/factory.py +1 -10
  63. notionary/page/content/parser/parsers/__init__.py +0 -2
  64. notionary/page/content/parser/parsers/audio.py +1 -1
  65. notionary/page/content/parser/parsers/base.py +1 -1
  66. notionary/page/content/parser/parsers/bookmark.py +1 -1
  67. notionary/page/content/parser/parsers/breadcrumb.py +1 -1
  68. notionary/page/content/parser/parsers/bulleted_list.py +52 -8
  69. notionary/page/content/parser/parsers/callout.py +55 -84
  70. notionary/page/content/parser/parsers/caption.py +1 -1
  71. notionary/page/content/parser/parsers/code.py +5 -5
  72. notionary/page/content/parser/parsers/column.py +23 -64
  73. notionary/page/content/parser/parsers/column_list.py +45 -45
  74. notionary/page/content/parser/parsers/divider.py +1 -1
  75. notionary/page/content/parser/parsers/embed.py +1 -1
  76. notionary/page/content/parser/parsers/equation.py +1 -1
  77. notionary/page/content/parser/parsers/file.py +1 -1
  78. notionary/page/content/parser/parsers/heading.py +65 -8
  79. notionary/page/content/parser/parsers/image.py +1 -1
  80. notionary/page/content/parser/parsers/numbered_list.py +52 -8
  81. notionary/page/content/parser/parsers/paragraph.py +3 -2
  82. notionary/page/content/parser/parsers/pdf.py +1 -1
  83. notionary/page/content/parser/parsers/quote.py +75 -15
  84. notionary/page/content/parser/parsers/space.py +14 -8
  85. notionary/page/content/parser/parsers/table.py +1 -1
  86. notionary/page/content/parser/parsers/table_of_contents.py +1 -1
  87. notionary/page/content/parser/parsers/todo.py +57 -19
  88. notionary/page/content/parser/parsers/toggle.py +17 -74
  89. notionary/page/content/parser/parsers/video.py +1 -1
  90. notionary/page/content/parser/post_processing/handlers/rich_text_length.py +6 -4
  91. notionary/page/content/parser/post_processing/handlers/rich_text_length_truncation.py +43 -22
  92. notionary/page/content/parser/pre_processsing/handlers/__init__.py +4 -0
  93. notionary/page/content/parser/pre_processsing/handlers/column_syntax.py +108 -54
  94. notionary/page/content/parser/pre_processsing/handlers/indentation.py +86 -0
  95. notionary/page/content/parser/pre_processsing/handlers/video_syntax.py +66 -0
  96. notionary/page/content/parser/pre_processsing/handlers/whitespace.py +14 -7
  97. notionary/page/content/parser/service.py +9 -0
  98. notionary/page/content/renderer/context.py +5 -2
  99. notionary/page/content/renderer/factory.py +2 -11
  100. notionary/page/content/renderer/post_processing/handlers/__init__.py +2 -2
  101. notionary/page/content/renderer/post_processing/handlers/numbered_list.py +156 -0
  102. notionary/page/content/renderer/renderers/__init__.py +0 -2
  103. notionary/page/content/renderer/renderers/base.py +1 -1
  104. notionary/page/content/renderer/renderers/bulleted_list.py +1 -1
  105. notionary/page/content/renderer/renderers/callout.py +6 -21
  106. notionary/page/content/renderer/renderers/captioned_block.py +1 -1
  107. notionary/page/content/renderer/renderers/column.py +28 -19
  108. notionary/page/content/renderer/renderers/column_list.py +24 -11
  109. notionary/page/content/renderer/renderers/heading.py +53 -27
  110. notionary/page/content/renderer/renderers/numbered_list.py +6 -5
  111. notionary/page/content/renderer/renderers/quote.py +1 -1
  112. notionary/page/content/renderer/renderers/todo.py +1 -1
  113. notionary/page/content/renderer/renderers/toggle.py +6 -7
  114. notionary/page/content/service.py +4 -1
  115. notionary/page/content/syntax/__init__.py +4 -0
  116. notionary/page/content/syntax/grammar.py +10 -0
  117. notionary/page/content/syntax/models.py +0 -2
  118. notionary/page/content/syntax/{service.py → registry.py} +31 -91
  119. notionary/page/properties/client.py +3 -3
  120. notionary/page/properties/models.py +3 -2
  121. notionary/page/properties/service.py +18 -3
  122. notionary/page/service.py +22 -80
  123. notionary/shared/entity/service.py +94 -36
  124. notionary/shared/models/cover.py +1 -1
  125. notionary/shared/typings.py +3 -0
  126. notionary/user/base.py +60 -11
  127. notionary/user/factory.py +0 -0
  128. notionary/utils/decorators.py +122 -0
  129. notionary/utils/fuzzy.py +18 -6
  130. notionary/utils/mixins/logging.py +38 -27
  131. notionary/utils/pagination.py +70 -16
  132. notionary/workspace/__init__.py +2 -1
  133. notionary/workspace/client.py +4 -2
  134. notionary/workspace/query/__init__.py +3 -0
  135. notionary/workspace/query/builder.py +25 -1
  136. notionary/workspace/query/models.py +12 -3
  137. notionary/workspace/query/service.py +57 -32
  138. notionary/workspace/service.py +31 -21
  139. {notionary-0.2.28.dist-info → notionary-0.3.1.dist-info}/METADATA +35 -105
  140. notionary-0.3.1.dist-info/RECORD +211 -0
  141. notionary/page/content/markdown/nodes/toggleable_heading.py +0 -35
  142. notionary/page/content/parser/parsers/toggleable_heading.py +0 -150
  143. notionary/page/content/renderer/post_processing/handlers/numbered_list_placeholdere.py +0 -62
  144. notionary/page/content/renderer/renderers/toggleable_heading.py +0 -78
  145. notionary/utils/async_retry.py +0 -39
  146. notionary/utils/singleton.py +0 -13
  147. notionary-0.2.28.dist-info/RECORD +0 -200
  148. {notionary-0.2.28.dist-info → notionary-0.3.1.dist-info}/WHEEL +0 -0
  149. {notionary-0.2.28.dist-info → notionary-0.3.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,9 +1,9 @@
1
1
  from collections.abc import AsyncGenerator
2
- from typing import Any
3
2
 
4
3
  from notionary.data_source.schemas import DataSourceDto
5
4
  from notionary.http.client import NotionHttpClient
6
5
  from notionary.page.schemas import NotionPageDto
6
+ from notionary.shared.typings import JsonDict
7
7
  from notionary.utils.pagination import paginate_notion_api_generator
8
8
  from notionary.workspace.query.models import WorkspaceQueryConfig
9
9
  from notionary.workspace.schemas import DataSourceSearchResponse, PageSearchResponse
@@ -22,6 +22,7 @@ class WorkspaceClient:
22
22
  async for page in paginate_notion_api_generator(
23
23
  self._query_pages,
24
24
  search_config=search_config,
25
+ total_results_limit=search_config.total_results_limit,
25
26
  ):
26
27
  yield page
27
28
 
@@ -32,6 +33,7 @@ class WorkspaceClient:
32
33
  async for data_source in paginate_notion_api_generator(
33
34
  self._query_data_sources,
34
35
  search_config=search_config,
36
+ total_results_limit=search_config.total_results_limit,
35
37
  ):
36
38
  yield data_source
37
39
 
@@ -57,6 +59,6 @@ class WorkspaceClient:
57
59
  response = await self._execute_search(search_config)
58
60
  return DataSourceSearchResponse.model_validate(response)
59
61
 
60
- async def _execute_search(self, config: WorkspaceQueryConfig) -> dict[str, Any]:
62
+ async def _execute_search(self, config: WorkspaceQueryConfig) -> JsonDict:
61
63
  serialized_config = config.model_dump(exclude_none=True, by_alias=True)
62
64
  return await self._http_client.post("search", serialized_config)
@@ -0,0 +1,3 @@
1
+ from .builder import NotionWorkspaceQueryConfigBuilder
2
+
3
+ __all__ = ["NotionWorkspaceQueryConfigBuilder"]
@@ -8,7 +8,7 @@ from notionary.workspace.query.models import (
8
8
  )
9
9
 
10
10
 
11
- class WorkspaceQueryConfigBuilder:
11
+ class NotionWorkspaceQueryConfigBuilder:
12
12
  def __init__(self, config: WorkspaceQueryConfig = None) -> None:
13
13
  self.config = config or WorkspaceQueryConfig()
14
14
 
@@ -16,6 +16,10 @@ class WorkspaceQueryConfigBuilder:
16
16
  self.config.query = query
17
17
  return self
18
18
 
19
+ def with_total_results_limit(self, limit: int) -> Self:
20
+ self.config.total_results_limit = limit
21
+ return self
22
+
19
23
  def with_pages_only(self) -> Self:
20
24
  self.config.object_type = WorkspaceQueryObjectType.PAGE
21
25
  return self
@@ -44,6 +48,26 @@ class WorkspaceQueryConfigBuilder:
44
48
  def with_sort_by_last_edited(self) -> Self:
45
49
  return self.with_sort_timestamp(SortTimestamp.LAST_EDITED_TIME)
46
50
 
51
+ def with_sort_by_created_time_ascending(self) -> Self:
52
+ self.config.sort_timestamp = SortTimestamp.CREATED_TIME
53
+ self.config.sort_direction = SortDirection.ASCENDING
54
+ return self
55
+
56
+ def with_sort_by_created_time_descending(self) -> Self:
57
+ self.config.sort_timestamp = SortTimestamp.CREATED_TIME
58
+ self.config.sort_direction = SortDirection.DESCENDING
59
+ return self
60
+
61
+ def with_sort_by_last_edited_ascending(self) -> Self:
62
+ self.config.sort_timestamp = SortTimestamp.LAST_EDITED_TIME
63
+ self.config.sort_direction = SortDirection.ASCENDING
64
+ return self
65
+
66
+ def with_sort_by_last_edited_descending(self) -> Self:
67
+ self.config.sort_timestamp = SortTimestamp.LAST_EDITED_TIME
68
+ self.config.sort_direction = SortDirection.DESCENDING
69
+ return self
70
+
47
71
  def with_page_size(self, size: int) -> Self:
48
72
  self.config.page_size = min(size, 100)
49
73
  return self
@@ -1,8 +1,14 @@
1
1
  from enum import StrEnum
2
- from typing import Any
2
+ from typing import Protocol
3
3
 
4
4
  from pydantic import BaseModel, Field, field_validator, model_serializer
5
5
 
6
+ from notionary.shared.typings import JsonDict
7
+
8
+
9
+ class SearchableEntity(Protocol):
10
+ title: str
11
+
6
12
 
7
13
  class SortDirection(StrEnum):
8
14
  ASCENDING = "ascending"
@@ -24,9 +30,12 @@ class WorkspaceQueryConfig(BaseModel):
24
30
  object_type: WorkspaceQueryObjectType | None = None
25
31
  sort_direction: SortDirection = SortDirection.DESCENDING
26
32
  sort_timestamp: SortTimestamp = SortTimestamp.LAST_EDITED_TIME
33
+
27
34
  page_size: int = Field(default=100, ge=1, le=100)
28
35
  start_cursor: str | None = None
29
36
 
37
+ total_results_limit: int | None = None
38
+
30
39
  @field_validator("query")
31
40
  @classmethod
32
41
  def replace_empty_query_with_none(cls, value: str | None) -> str | None:
@@ -35,8 +44,8 @@ class WorkspaceQueryConfig(BaseModel):
35
44
  return value
36
45
 
37
46
  @model_serializer
38
- def serialize_model(self) -> dict[str, Any]:
39
- search_dict: dict[str, Any] = {}
47
+ def to_api_params(self) -> JsonDict:
48
+ search_dict: JsonDict = {}
40
49
 
41
50
  if self.query:
42
51
  search_dict["query"] = self.query
@@ -2,22 +2,18 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  from collections.abc import AsyncIterator
5
- from typing import TYPE_CHECKING, Protocol
5
+ from typing import TYPE_CHECKING
6
6
 
7
7
  from notionary.exceptions.search import DatabaseNotFound, DataSourceNotFound, PageNotFound
8
- from notionary.utils.fuzzy import find_best_match
8
+ from notionary.utils.fuzzy import find_all_matches
9
9
  from notionary.workspace.client import WorkspaceClient
10
- from notionary.workspace.query.builder import WorkspaceQueryConfigBuilder
11
- from notionary.workspace.query.models import WorkspaceQueryConfig
10
+ from notionary.workspace.query.builder import NotionWorkspaceQueryConfigBuilder
11
+ from notionary.workspace.query.models import SearchableEntity, WorkspaceQueryConfig
12
12
 
13
13
  if TYPE_CHECKING:
14
14
  from notionary import NotionDatabase, NotionDataSource, NotionPage
15
15
 
16
16
 
17
- class SearchableEntity(Protocol):
18
- title: str
19
-
20
-
21
17
  class WorkspaceQueryService:
22
18
  def __init__(self, client: WorkspaceClient | None = None) -> None:
23
19
  self._client = client or WorkspaceClient()
@@ -48,46 +44,75 @@ class WorkspaceQueryService:
48
44
  data_source_tasks = [NotionDataSource.from_id(dto.id) for dto in data_source_dtos]
49
45
  return await asyncio.gather(*data_source_tasks)
50
46
 
51
- async def find_data_source(self, query: str, min_similarity: float = 0.6) -> NotionDataSource:
52
- config = WorkspaceQueryConfigBuilder().with_query(query).with_data_sources_only().with_page_size(5).build()
47
+ async def find_data_source(self, query: str) -> NotionDataSource:
48
+ config = (
49
+ NotionWorkspaceQueryConfigBuilder()
50
+ .with_query(query)
51
+ .with_data_sources_only()
52
+ .with_page_size(100)
53
+ .build()
54
+ )
53
55
  data_sources = await self.get_data_sources(config)
54
-
55
- return self._get_best_match(
56
- data_sources, query, exception_class=DataSourceNotFound, min_similarity=min_similarity
56
+ return self._find_exact_match(data_sources, query, DataSourceNotFound)
57
+
58
+ async def find_page(self, query: str) -> NotionPage:
59
+ config = (
60
+ NotionWorkspaceQueryConfigBuilder()
61
+ .with_query(query)
62
+ .with_pages_only()
63
+ .with_page_size(100)
64
+ .build()
57
65
  )
58
-
59
- async def find_page(self, query: str, min_similarity: float = 0.6) -> NotionPage:
60
- config = WorkspaceQueryConfigBuilder().with_query(query).with_pages_only().with_page_size(5).build()
61
66
  pages = await self.get_pages(config)
62
-
63
- return self._get_best_match(pages, query, exception_class=PageNotFound, min_similarity=min_similarity)
64
-
65
- async def find_database(self, query: str = "") -> NotionDatabase:
66
- config = WorkspaceQueryConfigBuilder().with_query(query).with_data_sources_only().with_page_size(100).build()
67
+ return self._find_exact_match(pages, query, PageNotFound)
68
+
69
+ async def find_database(self, query: str) -> NotionDatabase:
70
+ config = (
71
+ NotionWorkspaceQueryConfigBuilder()
72
+ .with_query(query)
73
+ .with_data_sources_only()
74
+ .with_page_size(100)
75
+ .build()
76
+ )
67
77
  data_sources = await self.get_data_sources(config)
68
78
 
69
- parent_database_tasks = [data_source.get_parent_database() for data_source in data_sources]
79
+ parent_database_ids = [data_sources.get_parent_database_id_if_present() for data_sources in data_sources]
80
+ # filter none values which should not happen but for safety
81
+ parent_database_ids = [id for id in parent_database_ids if id is not None]
82
+
83
+ parent_database_tasks = [NotionDatabase.from_id(db_id) for db_id in parent_database_ids]
70
84
  parent_databases = await asyncio.gather(*parent_database_tasks)
71
85
  potential_databases = [database for database in parent_databases if database is not None]
72
86
 
73
- return self._get_best_match(potential_databases, query, exception_class=DatabaseNotFound)
87
+ return self._find_exact_match(potential_databases, query, DatabaseNotFound)
74
88
 
75
- def _get_best_match(
89
+ def _find_exact_match(
76
90
  self,
77
91
  search_results: list[SearchableEntity],
78
92
  query: str,
79
93
  exception_class: type[Exception],
80
- min_similarity: float | None = None,
81
94
  ) -> SearchableEntity:
82
- best_match = find_best_match(
95
+ if not search_results:
96
+ raise exception_class(query, [])
97
+
98
+ query_lower = query.lower()
99
+ exact_matches = [result for result in search_results if result.title.lower() == query_lower]
100
+
101
+ if exact_matches:
102
+ return exact_matches[0]
103
+
104
+ suggestions = self._get_fuzzy_suggestions(search_results, query)
105
+ raise exception_class(query, suggestions)
106
+
107
+ def _get_fuzzy_suggestions(self, search_results: list[SearchableEntity], query: str) -> list[str]:
108
+ sorted_by_similarity = find_all_matches(
83
109
  query=query,
84
110
  items=search_results,
85
- text_extractor=lambda searchable_entity: searchable_entity.title,
86
- min_similarity=min_similarity,
111
+ text_extractor=lambda entity: entity.title,
112
+ min_similarity=0.6,
87
113
  )
88
114
 
89
- if not best_match:
90
- available_titles = [result.title for result in search_results[:5]]
91
- raise exception_class(query, available_titles)
115
+ if sorted_by_similarity:
116
+ return [result.title for result in sorted_by_similarity[:5]]
92
117
 
93
- return best_match
118
+ return [result.title for result in search_results[:5]]
@@ -4,7 +4,7 @@ from collections.abc import AsyncIterator, Callable
4
4
  from typing import TYPE_CHECKING, Self
5
5
 
6
6
  from notionary.user.service import UserService
7
- from notionary.workspace.query.builder import WorkspaceQueryConfigBuilder
7
+ from notionary.workspace.query.builder import NotionWorkspaceQueryConfigBuilder
8
8
  from notionary.workspace.query.models import WorkspaceQueryConfig, WorkspaceQueryObjectType
9
9
  from notionary.workspace.query.service import WorkspaceQueryService
10
10
 
@@ -13,7 +13,9 @@ if TYPE_CHECKING:
13
13
  from notionary.page.service import NotionPage
14
14
  from notionary.user import BotUser, PersonUser
15
15
 
16
- type _QueryConfigInput = WorkspaceQueryConfig | Callable[[WorkspaceQueryConfigBuilder], WorkspaceQueryConfigBuilder]
16
+ type _QueryConfigInput = (
17
+ WorkspaceQueryConfig | Callable[[NotionWorkspaceQueryConfigBuilder], NotionWorkspaceQueryConfigBuilder]
18
+ )
17
19
 
18
20
 
19
21
  class NotionWorkspace:
@@ -43,14 +45,14 @@ class NotionWorkspace:
43
45
  self,
44
46
  config: _QueryConfigInput | None = None,
45
47
  ) -> list[NotionPage]:
46
- query_config = self._resolve_config(config, default_object_type_to_query=WorkspaceQueryObjectType.PAGE)
48
+ query_config = self._resolve_config(config, WorkspaceQueryObjectType.PAGE)
47
49
  return await self._query_service.get_pages(query_config)
48
50
 
49
51
  async def get_pages_stream(
50
52
  self,
51
53
  config: _QueryConfigInput | None = None,
52
54
  ) -> AsyncIterator[NotionPage]:
53
- query_config = self._resolve_config(config, default_object_type_to_query=WorkspaceQueryObjectType.PAGE)
55
+ query_config = self._resolve_config(config, WorkspaceQueryObjectType.PAGE)
54
56
  async for page in self._query_service.get_pages_stream(query_config):
55
57
  yield page
56
58
 
@@ -58,41 +60,49 @@ class NotionWorkspace:
58
60
  self,
59
61
  config: _QueryConfigInput | None = None,
60
62
  ) -> list[NotionDataSource]:
61
- query_config = self._resolve_config(config, default_object_type_to_query=WorkspaceQueryObjectType.DATA_SOURCE)
63
+ query_config = self._resolve_config(config, WorkspaceQueryObjectType.DATA_SOURCE)
62
64
  return await self._query_service.get_data_sources(query_config)
63
65
 
64
66
  async def get_data_sources_stream(
65
67
  self,
66
68
  config: _QueryConfigInput | None = None,
67
69
  ) -> AsyncIterator[NotionDataSource]:
68
- query_config = self._resolve_config(config, default_object_type_to_query=WorkspaceQueryObjectType.DATA_SOURCE)
70
+ query_config = self._resolve_config(config, WorkspaceQueryObjectType.DATA_SOURCE)
69
71
  async for data_source in self._query_service.get_data_sources_stream(query_config):
70
72
  yield data_source
71
73
 
72
74
  def _resolve_config(
73
75
  self,
74
76
  config: _QueryConfigInput | None,
75
- default_object_type_to_query: WorkspaceQueryObjectType,
77
+ expected_object_type: WorkspaceQueryObjectType,
76
78
  ) -> WorkspaceQueryConfig:
77
- if isinstance(config, WorkspaceQueryConfig):
78
- return config
79
-
80
- builder = self._create_builder_with_defaults(default_object_type_to_query)
79
+ if config is None:
80
+ return self._create_default_config(expected_object_type)
81
81
 
82
- if callable(config):
83
- config(builder)
82
+ if isinstance(config, WorkspaceQueryConfig):
83
+ return self._ensure_correct_object_type(config, expected_object_type)
84
84
 
85
- return builder.build()
85
+ return self._build_config_from_callable(config, expected_object_type)
86
86
 
87
- def _create_builder_with_defaults(self, object_type: WorkspaceQueryObjectType) -> WorkspaceQueryConfigBuilder:
88
- builder = WorkspaceQueryConfigBuilder()
87
+ def _create_default_config(self, object_type: WorkspaceQueryObjectType) -> WorkspaceQueryConfig:
88
+ return WorkspaceQueryConfig(object_type=object_type)
89
89
 
90
- if object_type == WorkspaceQueryObjectType.PAGE:
91
- builder.with_pages_only()
92
- else:
93
- builder.with_data_sources_only()
90
+ def _build_config_from_callable(
91
+ self,
92
+ config_callable: Callable[[NotionWorkspaceQueryConfigBuilder], NotionWorkspaceQueryConfigBuilder],
93
+ expected_object_type: WorkspaceQueryObjectType,
94
+ ) -> WorkspaceQueryConfig:
95
+ builder = NotionWorkspaceQueryConfigBuilder()
96
+ config_callable(builder)
97
+ return self._ensure_correct_object_type(builder.build(), expected_object_type)
94
98
 
95
- return builder
99
+ def _ensure_correct_object_type(
100
+ self,
101
+ config: WorkspaceQueryConfig,
102
+ expected_object_type: WorkspaceQueryObjectType,
103
+ ) -> WorkspaceQueryConfig:
104
+ config.object_type = expected_object_type
105
+ return config
96
106
 
97
107
  async def get_users(self) -> list[PersonUser]:
98
108
  return [user async for user in self._user_service.list_users_stream()]
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: notionary
3
- Version: 0.2.28
3
+ Version: 0.3.1
4
4
  Summary: Python library for programmatic Notion workspace management - databases, pages, and content with advanced Markdown support
5
5
  Project-URL: Homepage, https://github.com/mathisarends/notionary
6
6
  Author-email: Mathis Arends <mathisarends27@gmail.com>
7
7
  License: MIT
8
8
  License-File: LICENSE
9
- Requires-Python: >=3.11
9
+ Requires-Python: >=3.12
10
10
  Requires-Dist: aiofiles<25.0.0,>=24.1.0
11
11
  Requires-Dist: httpx>=0.28.0
12
12
  Requires-Dist: pydantic>=2.11.4
@@ -23,9 +23,13 @@ Description-Content-Type: text/markdown
23
23
 
24
24
  <div align="center">
25
25
 
26
- [![Python Version](https://img.shields.io/badge/python-3.8%2B-blue.svg)](https://www.python.org/downloads/)
27
- [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
28
- [![Documentation](https://img.shields.io/badge/docs-mathisarends.github.io-blue.svg)](https://mathisarends.github.io/notionary/)
26
+ [![PyPI version](https://badge.fury.io/py/notionary.svg)](https://badge.fury.io/py/notionary)
27
+ [![Python Version](https://img.shields.io/badge/python-3.12%2B-3776AB?logo=python&logoColor=white)](https://www.python.org/downloads/)
28
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
29
+ [![Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/mathisarends/fc0568b66a20fbbaa5018205861c0da9/raw/notionary-coverage.json)](https://github.com/mathisarends/notionary)
30
+ [![Downloads](https://img.shields.io/pypi/dm/notionary?color=blue)](https://pypi.org/project/notionary/)
31
+ [![Documentation](https://img.shields.io/badge/docs-notionary-blue?style=flat&logo=readthedocs)](https://mathisarends.github.io/notionary/)
32
+ [![Notion API](https://img.shields.io/badge/Notion%20API-Official-000000?logo=notion&logoColor=white)](https://developers.notion.com/)
29
33
 
30
34
  **Transform complex Notion API interactions into simple, Pythonic code.**
31
35
  Perfect for developers building AI agents, automation workflows, and dynamic content systems.
@@ -61,17 +65,9 @@ export NOTION_SECRET=your_integration_key
61
65
 
62
66
  ## See It in Action
63
67
 
64
- ### Creating Rich Database Entries
65
-
66
68
  https://github.com/user-attachments/assets/da8b4691-bee4-4b0f-801e-dccacb630398
67
69
 
68
- _Create styled project pages with properties, content, and rich formatting_
69
-
70
- ### Local File Uploads (Videos & Images)
71
-
72
- https://github.com/user-attachments/assets/a079ec01-bb56-4c65-8260-7b1fca42ac68
73
-
74
- _Upload videos and images using simple markdown syntax - files are automatically uploaded to Notion_
70
+ _Create rich database entries with properties, content, and beautiful formatting_
75
71
 
76
72
  ---
77
73
 
@@ -80,50 +76,30 @@ _Upload videos and images using simple markdown syntax - files are automatically
80
76
  ### Find → Create → Update Flow
81
77
 
82
78
  ```python
83
- import asyncio
84
- from notionary import NotionPage, NotionDatabase
85
-
86
- async def main():
87
- # Find pages by name - fuzzy matching included!
88
- page = await NotionPage.from_title("Meeting Notes")
89
-
90
- # Option 1: Direct Extended Markdown
91
- await page.append_markdown("""
92
- ## Action Items
93
- - [x] Review project proposal
94
- - [ ] Schedule team meeting
95
- - [ ] Update documentation
96
-
97
- [callout](Meeting decisions require follow-up "💡")
98
-
99
- +++ Details
100
- Additional context and next steps...
101
- +++
102
- """)
103
-
104
- # Option 2: Type-Safe Builder (maps to same markdown internally)
105
- await page.append_markdown(lambda builder: (
106
- builder
107
- .h2("Project Status")
108
- .callout("Milestone reached!", "🎉")
109
- .columns(
110
- lambda col: col.h3("Completed").bulleted_list([
111
- "API design", "Database setup", "Authentication"
112
- ]),
113
- lambda col: col.h3("In Progress").bulleted_list([
114
- "Frontend UI", "Testing", "Documentation"
115
- ]),
116
- width_ratios=[0.6, 0.4]
117
- )
118
- .toggle("Budget Details", lambda t: t
119
- .table(["Item", "Cost", "Status"], [
120
- ["Development", "$15,000", "Paid"],
121
- ["Design", "$8,000", "Pending"]
122
- ])
123
- )
124
- ))
125
-
126
- asyncio.run(main())
79
+ from notionary import NotionPage
80
+
81
+ # Find pages by name with fuzzy matching
82
+ page = await NotionPage.from_title("Meeting Notes")
83
+
84
+ # Define rich content with extended markdown
85
+ content = """
86
+ ## Action Items
87
+ - [x] Review proposal
88
+ - [ ] Schedule meeting
89
+
90
+ [callout](Key decision made! "💡")
91
+
92
+ | Task | Owner | Deadline |
93
+ |------|-------|----------|
94
+ | Design Review | Alice | 2024-03-15 |
95
+ | Implementation | Bob | 2024-03-22 |
96
+
97
+ +++ Budget Details
98
+ See attached spreadsheet...
99
+ +++
100
+ """
101
+
102
+ await page.append_markdown(content)
127
103
  ```
128
104
 
129
105
  ### Complete Block Support
@@ -132,36 +108,13 @@ Every Notion block type with extended syntax:
132
108
 
133
109
  | Block Type | Markdown Syntax | Use Case |
134
110
  | ------------- | -------------------------------------------- | ---------------------------- |
135
- | **Callouts** | `[callout](Text "🔥")` | Highlighting key information |
136
111
  | **Toggles** | `+++ Title\nContent\n+++` | Collapsible sections |
137
112
  | **Columns** | `::: columns\n::: column\nContent\n:::\n:::` | Side-by-side layouts |
138
113
  | **Tables** | Standard markdown tables | Structured data |
139
114
  | **Media** | `[video](./file.mp4)(caption:Description)` | Auto-uploading files |
140
115
  | **Code** | Standard code fences with captions | Code snippets |
141
116
  | **Equations** | `$LaTeX$` | Mathematical expressions |
142
- | **TOC** | `[toc](blue_background)` | Auto-generated navigation |
143
-
144
- ---
145
-
146
- ## What You Can Build 💡
147
-
148
- ### **AI Content Systems**
149
-
150
- - **Report Generation**: AI agents that create structured reports, documentation, and analysis
151
- - **Content Pipelines**: Automated workflows that process data and generate Notion pages
152
- - **Knowledge Management**: AI-powered documentation systems with smart categorization
153
-
154
- ### **Workflow Automation**
155
-
156
- - **Project Management**: Sync project status, update timelines, generate progress reports
157
- - **Data Integration**: Connect external APIs and databases to Notion workspaces
158
- - **Template Systems**: Dynamic page generation from templates and data sources
159
-
160
- ### **Content Management**
161
-
162
- - **Bulk Operations**: Mass page updates, content migration, and database management
163
- - **Media Handling**: Automated image/video uploads with proper organization
164
- - **Cross-Platform**: Sync content between Notion and other platforms
117
+ | **TOC** | `[toc]` | Auto-generated navigation |
165
118
 
166
119
  ---
167
120
 
@@ -222,29 +175,6 @@ Every Notion block type with extended syntax:
222
175
 
223
176
  [**mathisarends.github.io/notionary**](https://mathisarends.github.io/notionary/) - Complete API reference, guides, and tutorials
224
177
 
225
- ### Quick Links
226
-
227
- - [**Getting Started**](https://mathisarends.github.io/notionary/get-started/) - Setup and first steps
228
- - [**Page Management**](https://mathisarends.github.io/notionary/page/) - Content and properties
229
- - [**Database Operations**](https://mathisarends.github.io/notionary/database/) - Queries and management
230
- - [**Block Types Reference**](https://mathisarends.github.io/notionary/blocks/) - Complete syntax guide
231
-
232
- ### Hands-On Examples
233
-
234
- **Core Functionality:**
235
-
236
- - [Page Management](examples/page_example.py) - Create, update, and manage pages
237
- - [Database Operations](examples/database.py) - Connect and query databases
238
- - [Workspace Discovery](examples/workspace_discovery.py) - Explore your workspace
239
-
240
- **Extended Markdown:**
241
-
242
- - [Basic Formatting](examples/markdown/basic.py) - Text, lists, and links
243
- - [Callouts & Highlights](examples/markdown/callout.py) - Information boxes
244
- - [Toggle Sections](examples/markdown/toggle.py) - Collapsible content
245
- - [Multi-Column Layouts](examples/markdown/columns.py) - Side-by-side design
246
- - [Tables & Data](examples/markdown/table.py) - Structured presentations
247
-
248
178
  ---
249
179
 
250
180
  ## Contributing