notionary 0.2.27__py3-none-any.whl → 0.3.0__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 (395) hide show
  1. notionary/__init__.py +5 -20
  2. notionary/blocks/__init__.py +4 -4
  3. notionary/blocks/client.py +90 -216
  4. notionary/blocks/enums.py +167 -0
  5. notionary/blocks/rich_text/markdown_rich_text_converter.py +280 -0
  6. notionary/blocks/rich_text/models.py +178 -0
  7. notionary/blocks/rich_text/name_id_resolver/__init__.py +13 -0
  8. notionary/blocks/rich_text/name_id_resolver/data_source.py +32 -0
  9. notionary/blocks/rich_text/name_id_resolver/database.py +31 -0
  10. notionary/blocks/rich_text/name_id_resolver/page.py +34 -0
  11. notionary/blocks/rich_text/name_id_resolver/person.py +37 -0
  12. notionary/blocks/rich_text/name_id_resolver/port.py +11 -0
  13. notionary/blocks/rich_text/rich_text_markdown_converter.py +144 -0
  14. notionary/blocks/rich_text/rich_text_patterns.py +42 -0
  15. notionary/blocks/schemas.py +778 -0
  16. notionary/comments/__init__.py +1 -22
  17. notionary/comments/client.py +52 -187
  18. notionary/comments/factory.py +38 -0
  19. notionary/comments/models.py +5 -127
  20. notionary/comments/schemas.py +240 -0
  21. notionary/comments/service.py +34 -0
  22. notionary/data_source/http/client.py +11 -0
  23. notionary/data_source/http/data_source_instance_client.py +104 -0
  24. notionary/data_source/properties/schemas.py +402 -0
  25. notionary/data_source/query/builder.py +448 -0
  26. notionary/data_source/query/resolver.py +114 -0
  27. notionary/data_source/query/schema.py +302 -0
  28. notionary/data_source/query/validator.py +73 -0
  29. notionary/data_source/schema/registry.py +104 -0
  30. notionary/data_source/schema/service.py +136 -0
  31. notionary/data_source/schemas.py +27 -0
  32. notionary/data_source/service.py +377 -0
  33. notionary/database/client.py +30 -135
  34. notionary/database/database_metadata_update_client.py +19 -0
  35. notionary/database/schemas.py +29 -0
  36. notionary/database/service.py +168 -0
  37. notionary/exceptions/__init__.py +33 -0
  38. notionary/exceptions/api.py +41 -0
  39. notionary/exceptions/base.py +2 -0
  40. notionary/exceptions/block_parsing.py +16 -0
  41. notionary/exceptions/data_source/__init__.py +6 -0
  42. notionary/exceptions/data_source/builder.py +182 -0
  43. notionary/exceptions/data_source/properties.py +34 -0
  44. notionary/exceptions/properties.py +58 -0
  45. notionary/exceptions/search.py +57 -0
  46. notionary/file_upload/client.py +18 -30
  47. notionary/file_upload/models.py +7 -8
  48. notionary/file_upload/{notion_file_upload.py → service.py} +29 -64
  49. notionary/http/client.py +204 -0
  50. notionary/http/models.py +50 -0
  51. notionary/page/blocks/client.py +1 -0
  52. notionary/page/content/factory.py +73 -0
  53. notionary/page/content/markdown/__init__.py +5 -0
  54. notionary/page/content/markdown/builder.py +226 -0
  55. notionary/page/content/markdown/nodes/__init__.py +52 -0
  56. notionary/page/content/markdown/nodes/audio.py +23 -0
  57. notionary/page/content/markdown/nodes/base.py +12 -0
  58. notionary/page/content/markdown/nodes/bookmark.py +25 -0
  59. notionary/page/content/markdown/nodes/breadcrumb.py +14 -0
  60. notionary/page/content/markdown/nodes/bulleted_list.py +41 -0
  61. notionary/page/content/markdown/nodes/callout.py +34 -0
  62. notionary/page/content/markdown/nodes/code.py +28 -0
  63. notionary/page/content/markdown/nodes/columns.py +69 -0
  64. notionary/page/content/markdown/nodes/container.py +64 -0
  65. notionary/page/content/markdown/nodes/divider.py +14 -0
  66. notionary/page/content/markdown/nodes/embed.py +23 -0
  67. notionary/page/content/markdown/nodes/equation.py +19 -0
  68. notionary/page/content/markdown/nodes/file.py +23 -0
  69. notionary/page/content/markdown/nodes/heading.py +36 -0
  70. notionary/page/content/markdown/nodes/image.py +23 -0
  71. notionary/page/content/markdown/nodes/mixins/__init__.py +5 -0
  72. notionary/page/content/markdown/nodes/mixins/caption.py +12 -0
  73. notionary/page/content/markdown/nodes/numbered_list.py +38 -0
  74. notionary/page/content/markdown/nodes/paragraph.py +14 -0
  75. notionary/page/content/markdown/nodes/pdf.py +23 -0
  76. notionary/page/content/markdown/nodes/quote.py +27 -0
  77. notionary/page/content/markdown/nodes/space.py +14 -0
  78. notionary/page/content/markdown/nodes/table.py +45 -0
  79. notionary/page/content/markdown/nodes/table_of_contents.py +14 -0
  80. notionary/page/content/markdown/nodes/todo.py +38 -0
  81. notionary/page/content/markdown/nodes/toggle.py +27 -0
  82. notionary/page/content/markdown/nodes/video.py +23 -0
  83. notionary/page/content/parser/context.py +126 -0
  84. notionary/page/content/parser/factory.py +210 -0
  85. notionary/page/content/parser/parsers/__init__.py +58 -0
  86. notionary/page/content/parser/parsers/audio.py +40 -0
  87. notionary/page/content/parser/parsers/base.py +30 -0
  88. notionary/page/content/parser/parsers/bookmark.py +33 -0
  89. notionary/page/content/parser/parsers/breadcrumb.py +33 -0
  90. notionary/page/content/parser/parsers/bulleted_list.py +85 -0
  91. notionary/page/content/parser/parsers/callout.py +100 -0
  92. notionary/page/content/parser/parsers/caption.py +55 -0
  93. notionary/page/content/parser/parsers/code.py +81 -0
  94. notionary/page/content/parser/parsers/column.py +76 -0
  95. notionary/page/content/parser/parsers/column_list.py +81 -0
  96. notionary/page/content/parser/parsers/divider.py +33 -0
  97. notionary/page/content/parser/parsers/embed.py +33 -0
  98. notionary/page/content/parser/parsers/equation.py +65 -0
  99. notionary/page/content/parser/parsers/file.py +42 -0
  100. notionary/page/content/parser/parsers/heading.py +115 -0
  101. notionary/page/content/parser/parsers/image.py +42 -0
  102. notionary/page/content/parser/parsers/numbered_list.py +89 -0
  103. notionary/page/content/parser/parsers/paragraph.py +37 -0
  104. notionary/page/content/parser/parsers/pdf.py +42 -0
  105. notionary/page/content/parser/parsers/quote.py +125 -0
  106. notionary/page/content/parser/parsers/space.py +41 -0
  107. notionary/page/content/parser/parsers/table.py +144 -0
  108. notionary/page/content/parser/parsers/table_of_contents.py +32 -0
  109. notionary/page/content/parser/parsers/todo.py +96 -0
  110. notionary/page/content/parser/parsers/toggle.py +70 -0
  111. notionary/page/content/parser/parsers/video.py +42 -0
  112. notionary/page/content/parser/post_processing/handlers/__init__.py +5 -0
  113. notionary/page/content/parser/post_processing/handlers/rich_text_length.py +95 -0
  114. notionary/page/content/parser/post_processing/handlers/rich_text_length_truncation.py +114 -0
  115. notionary/page/content/parser/post_processing/port.py +9 -0
  116. notionary/page/content/parser/post_processing/service.py +16 -0
  117. notionary/page/content/parser/pre_processsing/handlers/__init__.py +11 -0
  118. notionary/page/content/parser/pre_processsing/handlers/column_syntax.py +130 -0
  119. notionary/page/content/parser/pre_processsing/handlers/indentation.py +84 -0
  120. notionary/page/content/parser/pre_processsing/handlers/port.py +7 -0
  121. notionary/page/content/parser/pre_processsing/handlers/whitespace.py +73 -0
  122. notionary/page/content/parser/pre_processsing/service.py +15 -0
  123. notionary/page/content/parser/service.py +78 -0
  124. notionary/page/content/renderer/context.py +51 -0
  125. notionary/page/content/renderer/factory.py +231 -0
  126. notionary/page/content/renderer/post_processing/handlers/__init__.py +5 -0
  127. notionary/page/content/renderer/post_processing/handlers/numbered_list.py +156 -0
  128. notionary/page/content/renderer/post_processing/port.py +7 -0
  129. notionary/page/content/renderer/post_processing/service.py +15 -0
  130. notionary/page/content/renderer/renderers/__init__.py +55 -0
  131. notionary/page/content/renderer/renderers/audio.py +31 -0
  132. notionary/page/content/renderer/renderers/base.py +31 -0
  133. notionary/page/content/renderer/renderers/bookmark.py +25 -0
  134. notionary/page/content/renderer/renderers/breadcrumb.py +21 -0
  135. notionary/page/content/renderer/renderers/bulleted_list.py +48 -0
  136. notionary/page/content/renderer/renderers/callout.py +50 -0
  137. notionary/page/content/renderer/renderers/captioned_block.py +58 -0
  138. notionary/page/content/renderer/renderers/code.py +34 -0
  139. notionary/page/content/renderer/renderers/column.py +53 -0
  140. notionary/page/content/renderer/renderers/column_list.py +44 -0
  141. notionary/page/content/renderer/renderers/divider.py +22 -0
  142. notionary/page/content/renderer/renderers/embed.py +25 -0
  143. notionary/page/content/renderer/renderers/equation.py +37 -0
  144. notionary/page/content/renderer/renderers/fallback.py +24 -0
  145. notionary/page/content/renderer/renderers/file.py +40 -0
  146. notionary/page/content/renderer/renderers/heading.py +95 -0
  147. notionary/page/content/renderer/renderers/image.py +31 -0
  148. notionary/page/content/renderer/renderers/numbered_list.py +42 -0
  149. notionary/page/content/renderer/renderers/paragraph.py +40 -0
  150. notionary/page/content/renderer/renderers/pdf.py +31 -0
  151. notionary/page/content/renderer/renderers/quote.py +49 -0
  152. notionary/page/content/renderer/renderers/table.py +115 -0
  153. notionary/page/content/renderer/renderers/table_of_contents.py +26 -0
  154. notionary/page/content/renderer/renderers/table_row.py +17 -0
  155. notionary/page/content/renderer/renderers/todo.py +56 -0
  156. notionary/page/content/renderer/renderers/toggle.py +52 -0
  157. notionary/page/content/renderer/renderers/video.py +31 -0
  158. notionary/page/content/renderer/service.py +50 -0
  159. notionary/page/content/service.py +68 -0
  160. notionary/page/content/syntax/__init__.py +4 -0
  161. notionary/page/content/syntax/grammar.py +10 -0
  162. notionary/page/content/syntax/models.py +66 -0
  163. notionary/page/content/syntax/registry.py +393 -0
  164. notionary/page/page_context.py +7 -16
  165. notionary/page/page_http_client.py +15 -0
  166. notionary/page/page_metadata_update_client.py +19 -0
  167. notionary/page/properties/client.py +144 -0
  168. notionary/page/properties/factory.py +26 -0
  169. notionary/page/properties/models.py +308 -0
  170. notionary/page/properties/service.py +261 -0
  171. notionary/page/schemas.py +13 -0
  172. notionary/page/service.py +225 -0
  173. notionary/shared/entity/client.py +29 -0
  174. notionary/shared/entity/dto_parsers.py +53 -0
  175. notionary/shared/entity/entity_metadata_update_client.py +41 -0
  176. notionary/shared/entity/schemas.py +45 -0
  177. notionary/shared/entity/service.py +171 -0
  178. notionary/shared/models/cover.py +20 -0
  179. notionary/shared/models/file.py +21 -0
  180. notionary/shared/models/icon.py +28 -0
  181. notionary/shared/models/parent.py +41 -0
  182. notionary/shared/properties/type.py +30 -0
  183. notionary/shared/typings.py +3 -0
  184. notionary/user/__init__.py +4 -8
  185. notionary/user/base.py +138 -0
  186. notionary/user/bot.py +70 -0
  187. notionary/user/client.py +22 -111
  188. notionary/user/person.py +41 -0
  189. notionary/user/schemas.py +67 -0
  190. notionary/user/service.py +65 -0
  191. notionary/utils/date.py +51 -0
  192. notionary/utils/decorators.py +122 -0
  193. notionary/utils/fuzzy.py +68 -0
  194. notionary/utils/mixins/logging.py +58 -0
  195. notionary/utils/pagination.py +100 -0
  196. notionary/utils/uuid_utils.py +20 -0
  197. notionary/workspace/__init__.py +4 -0
  198. notionary/workspace/client.py +62 -0
  199. notionary/workspace/query/__init__.py +3 -0
  200. notionary/workspace/query/builder.py +60 -0
  201. notionary/workspace/query/models.py +61 -0
  202. notionary/workspace/query/service.py +100 -0
  203. notionary/workspace/schemas.py +21 -0
  204. notionary/workspace/service.py +116 -0
  205. notionary-0.3.0.dist-info/METADATA +201 -0
  206. notionary-0.3.0.dist-info/RECORD +209 -0
  207. {notionary-0.2.27.dist-info → notionary-0.3.0.dist-info}/WHEEL +1 -1
  208. {notionary-0.2.27.dist-info → notionary-0.3.0.dist-info/licenses}/LICENSE +9 -9
  209. notionary/base_notion_client.py +0 -219
  210. notionary/blocks/_bootstrap.py +0 -271
  211. notionary/blocks/audio/__init__.py +0 -11
  212. notionary/blocks/audio/audio_element.py +0 -158
  213. notionary/blocks/audio/audio_markdown_node.py +0 -24
  214. notionary/blocks/audio/audio_models.py +0 -10
  215. notionary/blocks/base_block_element.py +0 -42
  216. notionary/blocks/bookmark/__init__.py +0 -12
  217. notionary/blocks/bookmark/bookmark_element.py +0 -83
  218. notionary/blocks/bookmark/bookmark_markdown_node.py +0 -28
  219. notionary/blocks/bookmark/bookmark_models.py +0 -15
  220. notionary/blocks/breadcrumbs/__init__.py +0 -15
  221. notionary/blocks/breadcrumbs/breadcrumb_element.py +0 -39
  222. notionary/blocks/breadcrumbs/breadcrumb_markdown_node.py +0 -13
  223. notionary/blocks/breadcrumbs/breadcrumb_models.py +0 -12
  224. notionary/blocks/bulleted_list/__init__.py +0 -15
  225. notionary/blocks/bulleted_list/bulleted_list_element.py +0 -74
  226. notionary/blocks/bulleted_list/bulleted_list_markdown_node.py +0 -20
  227. notionary/blocks/bulleted_list/bulleted_list_models.py +0 -17
  228. notionary/blocks/callout/__init__.py +0 -12
  229. notionary/blocks/callout/callout_element.py +0 -99
  230. notionary/blocks/callout/callout_markdown_node.py +0 -19
  231. notionary/blocks/callout/callout_models.py +0 -33
  232. notionary/blocks/child_database/__init__.py +0 -14
  233. notionary/blocks/child_database/child_database_element.py +0 -59
  234. notionary/blocks/child_database/child_database_models.py +0 -12
  235. notionary/blocks/child_page/__init__.py +0 -9
  236. notionary/blocks/child_page/child_page_element.py +0 -94
  237. notionary/blocks/child_page/child_page_models.py +0 -12
  238. notionary/blocks/code/__init__.py +0 -11
  239. notionary/blocks/code/code_element.py +0 -149
  240. notionary/blocks/code/code_markdown_node.py +0 -80
  241. notionary/blocks/code/code_models.py +0 -94
  242. notionary/blocks/column/__init__.py +0 -25
  243. notionary/blocks/column/column_element.py +0 -65
  244. notionary/blocks/column/column_list_element.py +0 -52
  245. notionary/blocks/column/column_list_markdown_node.py +0 -34
  246. notionary/blocks/column/column_markdown_node.py +0 -42
  247. notionary/blocks/column/column_models.py +0 -26
  248. notionary/blocks/divider/__init__.py +0 -12
  249. notionary/blocks/divider/divider_element.py +0 -41
  250. notionary/blocks/divider/divider_markdown_node.py +0 -11
  251. notionary/blocks/divider/divider_models.py +0 -12
  252. notionary/blocks/embed/__init__.py +0 -12
  253. notionary/blocks/embed/embed_element.py +0 -98
  254. notionary/blocks/embed/embed_markdown_node.py +0 -19
  255. notionary/blocks/embed/embed_models.py +0 -14
  256. notionary/blocks/equation/__init__.py +0 -13
  257. notionary/blocks/equation/equation_element.py +0 -133
  258. notionary/blocks/equation/equation_element_markdown_node.py +0 -23
  259. notionary/blocks/equation/equation_models.py +0 -11
  260. notionary/blocks/file/__init__.py +0 -23
  261. notionary/blocks/file/file_element.py +0 -133
  262. notionary/blocks/file/file_element_markdown_node.py +0 -24
  263. notionary/blocks/file/file_element_models.py +0 -39
  264. notionary/blocks/heading/__init__.py +0 -19
  265. notionary/blocks/heading/heading_element.py +0 -112
  266. notionary/blocks/heading/heading_markdown_node.py +0 -16
  267. notionary/blocks/heading/heading_models.py +0 -29
  268. notionary/blocks/image_block/__init__.py +0 -11
  269. notionary/blocks/image_block/image_element.py +0 -130
  270. notionary/blocks/image_block/image_markdown_node.py +0 -25
  271. notionary/blocks/image_block/image_models.py +0 -10
  272. notionary/blocks/markdown/markdown_builder.py +0 -525
  273. notionary/blocks/markdown/markdown_document_model.py +0 -0
  274. notionary/blocks/markdown/markdown_node.py +0 -25
  275. notionary/blocks/mixins/captions/__init__.py +0 -4
  276. notionary/blocks/mixins/captions/caption_markdown_node_mixin.py +0 -31
  277. notionary/blocks/mixins/captions/caption_mixin.py +0 -92
  278. notionary/blocks/mixins/file_upload/__init__.py +0 -3
  279. notionary/blocks/mixins/file_upload/file_upload_mixin.py +0 -320
  280. notionary/blocks/models.py +0 -174
  281. notionary/blocks/numbered_list/__init__.py +0 -16
  282. notionary/blocks/numbered_list/numbered_list_element.py +0 -65
  283. notionary/blocks/numbered_list/numbered_list_markdown_node.py +0 -17
  284. notionary/blocks/numbered_list/numbered_list_models.py +0 -17
  285. notionary/blocks/paragraph/__init__.py +0 -15
  286. notionary/blocks/paragraph/paragraph_element.py +0 -58
  287. notionary/blocks/paragraph/paragraph_markdown_node.py +0 -16
  288. notionary/blocks/paragraph/paragraph_models.py +0 -16
  289. notionary/blocks/pdf/__init__.py +0 -11
  290. notionary/blocks/pdf/pdf_element.py +0 -146
  291. notionary/blocks/pdf/pdf_markdown_node.py +0 -24
  292. notionary/blocks/pdf/pdf_models.py +0 -11
  293. notionary/blocks/quote/__init__.py +0 -14
  294. notionary/blocks/quote/quote_element.py +0 -75
  295. notionary/blocks/quote/quote_markdown_node.py +0 -16
  296. notionary/blocks/quote/quote_models.py +0 -18
  297. notionary/blocks/registry/__init__.py +0 -3
  298. notionary/blocks/registry/block_registry.py +0 -150
  299. notionary/blocks/rich_text/__init__.py +0 -33
  300. notionary/blocks/rich_text/rich_text_models.py +0 -221
  301. notionary/blocks/rich_text/text_inline_formatter.py +0 -456
  302. notionary/blocks/syntax_prompt_builder.py +0 -137
  303. notionary/blocks/table/__init__.py +0 -19
  304. notionary/blocks/table/table_element.py +0 -225
  305. notionary/blocks/table/table_markdown_node.py +0 -42
  306. notionary/blocks/table/table_models.py +0 -28
  307. notionary/blocks/table_of_contents/__init__.py +0 -17
  308. notionary/blocks/table_of_contents/table_of_contents_element.py +0 -80
  309. notionary/blocks/table_of_contents/table_of_contents_markdown_node.py +0 -21
  310. notionary/blocks/table_of_contents/table_of_contents_models.py +0 -18
  311. notionary/blocks/todo/__init__.py +0 -12
  312. notionary/blocks/todo/todo_element.py +0 -81
  313. notionary/blocks/todo/todo_markdown_node.py +0 -21
  314. notionary/blocks/todo/todo_models.py +0 -18
  315. notionary/blocks/toggle/__init__.py +0 -12
  316. notionary/blocks/toggle/toggle_element.py +0 -112
  317. notionary/blocks/toggle/toggle_markdown_node.py +0 -31
  318. notionary/blocks/toggle/toggle_models.py +0 -17
  319. notionary/blocks/toggleable_heading/__init__.py +0 -11
  320. notionary/blocks/toggleable_heading/toggleable_heading_element.py +0 -115
  321. notionary/blocks/toggleable_heading/toggleable_heading_markdown_node.py +0 -34
  322. notionary/blocks/types.py +0 -130
  323. notionary/blocks/video/__init__.py +0 -11
  324. notionary/blocks/video/video_element.py +0 -187
  325. notionary/blocks/video/video_element_models.py +0 -10
  326. notionary/blocks/video/video_markdown_node.py +0 -26
  327. notionary/database/__init__.py +0 -4
  328. notionary/database/database.py +0 -480
  329. notionary/database/database_filter_builder.py +0 -173
  330. notionary/database/database_provider.py +0 -227
  331. notionary/database/exceptions.py +0 -13
  332. notionary/database/models.py +0 -337
  333. notionary/database/notion_database.py +0 -487
  334. notionary/file_upload/__init__.py +0 -7
  335. notionary/page/client.py +0 -124
  336. notionary/page/markdown_whitespace_processor.py +0 -129
  337. notionary/page/models.py +0 -322
  338. notionary/page/notion_page.py +0 -712
  339. notionary/page/page_content_deleting_service.py +0 -117
  340. notionary/page/page_content_writer.py +0 -80
  341. notionary/page/property_formatter.py +0 -99
  342. notionary/page/reader/handler/__init__.py +0 -19
  343. notionary/page/reader/handler/base_block_renderer.py +0 -44
  344. notionary/page/reader/handler/block_processing_context.py +0 -35
  345. notionary/page/reader/handler/block_rendering_context.py +0 -48
  346. notionary/page/reader/handler/column_list_renderer.py +0 -51
  347. notionary/page/reader/handler/column_renderer.py +0 -60
  348. notionary/page/reader/handler/equation_renderer.py +0 -0
  349. notionary/page/reader/handler/line_renderer.py +0 -73
  350. notionary/page/reader/handler/numbered_list_renderer.py +0 -85
  351. notionary/page/reader/handler/toggle_renderer.py +0 -69
  352. notionary/page/reader/handler/toggleable_heading_renderer.py +0 -89
  353. notionary/page/reader/page_content_retriever.py +0 -81
  354. notionary/page/search_filter_builder.py +0 -132
  355. notionary/page/utils.py +0 -60
  356. notionary/page/writer/handler/__init__.py +0 -24
  357. notionary/page/writer/handler/code_handler.py +0 -72
  358. notionary/page/writer/handler/column_handler.py +0 -141
  359. notionary/page/writer/handler/column_list_handler.py +0 -139
  360. notionary/page/writer/handler/equation_handler.py +0 -74
  361. notionary/page/writer/handler/line_handler.py +0 -35
  362. notionary/page/writer/handler/line_processing_context.py +0 -54
  363. notionary/page/writer/handler/regular_line_handler.py +0 -86
  364. notionary/page/writer/handler/table_handler.py +0 -66
  365. notionary/page/writer/handler/toggle_handler.py +0 -159
  366. notionary/page/writer/handler/toggleable_heading_handler.py +0 -174
  367. notionary/page/writer/markdown_to_notion_converter.py +0 -139
  368. notionary/page/writer/markdown_to_notion_converter_context.py +0 -30
  369. notionary/page/writer/markdown_to_notion_text_length_post_processor.py +0 -0
  370. notionary/page/writer/notion_text_length_processor.py +0 -150
  371. notionary/schemas/__init__.py +0 -3
  372. notionary/schemas/base.py +0 -73
  373. notionary/shared/__init__.py +0 -3
  374. notionary/shared/name_to_id_resolver.py +0 -203
  375. notionary/telemetry/__init__.py +0 -19
  376. notionary/telemetry/service.py +0 -136
  377. notionary/telemetry/views.py +0 -73
  378. notionary/user/base_notion_user.py +0 -53
  379. notionary/user/models.py +0 -84
  380. notionary/user/notion_bot_user.py +0 -226
  381. notionary/user/notion_user.py +0 -255
  382. notionary/user/notion_user_manager.py +0 -101
  383. notionary/util/__init__.py +0 -15
  384. notionary/util/concurrency_limiter.py +0 -0
  385. notionary/util/factory_decorator.py +0 -0
  386. notionary/util/factory_only.py +0 -37
  387. notionary/util/fuzzy.py +0 -75
  388. notionary/util/logging_mixin.py +0 -59
  389. notionary/util/page_id_utils.py +0 -27
  390. notionary/util/singleton.py +0 -18
  391. notionary/util/singleton_metaclass.py +0 -22
  392. notionary/workspace.py +0 -105
  393. notionary-0.2.27.dist-info/METADATA +0 -270
  394. notionary-0.2.27.dist-info/RECORD +0 -202
  395. /notionary/{database → user}/factory.py +0 -0
@@ -0,0 +1,122 @@
1
+ import asyncio
2
+ import functools
3
+ import logging
4
+ import time
5
+ from collections.abc import Callable, Coroutine
6
+ from typing import Any, ParamSpec, TypeVar
7
+
8
+ P = ParamSpec("P")
9
+ R = TypeVar("R")
10
+ T = TypeVar("T")
11
+
12
+ type SyncFunc = Callable[P, R]
13
+ type AsyncFunc = Callable[P, Coroutine[Any, Any, R]]
14
+ type SyncDecorator = Callable[[SyncFunc], SyncFunc]
15
+ type AsyncDecorator = Callable[[AsyncFunc], AsyncFunc]
16
+
17
+
18
+ def singleton(cls):
19
+ instance = [None]
20
+
21
+ def wrapper(*args, **kwargs):
22
+ if instance[0] is None:
23
+ instance[0] = cls(*args, **kwargs)
24
+ return instance[0]
25
+
26
+ return wrapper
27
+
28
+
29
+ def time_execution_sync(additional_text: str = "", min_duration_to_log: float = 0.25) -> SyncDecorator:
30
+ def decorator(func: SyncFunc) -> SyncFunc:
31
+ @functools.wraps(func)
32
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
33
+ start_time = time.perf_counter()
34
+ result = func(*args, **kwargs)
35
+ execution_time = time.perf_counter() - start_time
36
+
37
+ if execution_time > min_duration_to_log:
38
+ logger = _get_logger_from_context(args, func)
39
+ function_name = additional_text.strip("-") or func.__name__
40
+ logger.debug(f"⏳ {function_name}() took {execution_time:.2f}s")
41
+
42
+ return result
43
+
44
+ return wrapper
45
+
46
+ return decorator
47
+
48
+
49
+ def time_execution_async(
50
+ additional_text: str = "",
51
+ min_duration_to_log: float = 0.25,
52
+ ) -> AsyncDecorator:
53
+ def decorator(func: AsyncFunc) -> AsyncFunc:
54
+ @functools.wraps(func)
55
+ async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
56
+ start_time = time.perf_counter()
57
+ result = await func(*args, **kwargs)
58
+ execution_time = time.perf_counter() - start_time
59
+
60
+ if execution_time > min_duration_to_log:
61
+ logger = _get_logger_from_context(args, func)
62
+ function_name = additional_text.strip("-") or func.__name__
63
+ logger.debug(f"⏳ {function_name}() took {execution_time:.2f}s")
64
+
65
+ return result
66
+
67
+ return wrapper
68
+
69
+ return decorator
70
+
71
+
72
+ def _get_logger_from_context(args: tuple, func: Callable) -> logging.Logger:
73
+ if _has_instance_logger(args):
74
+ return _extract_instance_logger(args)
75
+
76
+ return _get_module_logger(func)
77
+
78
+
79
+ def _has_instance_logger(args: tuple) -> bool:
80
+ return bool(args) and hasattr(args[0], "logger")
81
+
82
+
83
+ def _extract_instance_logger(args: tuple) -> logging.Logger:
84
+ return args[0].logger
85
+
86
+
87
+ def _get_module_logger(func: Callable) -> logging.Logger:
88
+ return logging.getLogger(func.__module__)
89
+
90
+
91
+ def async_retry(
92
+ max_retries: int = 3,
93
+ initial_delay: float = 1.0,
94
+ backoff_factor: float = 2.0,
95
+ retry_on_exceptions: tuple[type[Exception], ...] | None = None,
96
+ ):
97
+ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
98
+ @functools.wraps(func)
99
+ async def wrapper(*args: Any, **kwargs: Any) -> Any:
100
+ delay = initial_delay
101
+ last_exception = None
102
+
103
+ for attempt in range(max_retries + 1):
104
+ try:
105
+ return await func(*args, **kwargs)
106
+ except Exception as e:
107
+ last_exception = e
108
+
109
+ if retry_on_exceptions is not None and not isinstance(e, retry_on_exceptions):
110
+ raise
111
+
112
+ if attempt == max_retries:
113
+ raise
114
+
115
+ await asyncio.sleep(delay)
116
+ delay *= backoff_factor
117
+
118
+ raise last_exception
119
+
120
+ return wrapper
121
+
122
+ return decorator
@@ -0,0 +1,68 @@
1
+ import difflib
2
+ from collections.abc import Callable
3
+ from dataclasses import dataclass
4
+ from typing import Generic, TypeVar
5
+
6
+ T = TypeVar("T")
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class _MatchResult(Generic[T]):
11
+ item: T
12
+ similarity: float
13
+
14
+
15
+ def find_best_match(
16
+ query: str,
17
+ items: list[T],
18
+ text_extractor: Callable[[T], str],
19
+ min_similarity: float,
20
+ ) -> T | None:
21
+ matches = _find_best_matches(query, items, text_extractor, min_similarity, limit=1)
22
+ return matches[0].item if matches else None
23
+
24
+
25
+ def find_all_matches(
26
+ query: str,
27
+ items: list[T],
28
+ text_extractor: Callable[[T], str],
29
+ min_similarity: float,
30
+ ) -> list[T]:
31
+ matches = _find_best_matches(query, items, text_extractor, min_similarity, limit=None)
32
+ return [match.item for match in matches]
33
+
34
+
35
+ def _find_best_matches(
36
+ query: str,
37
+ items: list[T],
38
+ text_extractor: Callable[[T], str],
39
+ min_similarity: float = 0.0,
40
+ limit: int | None = None,
41
+ ) -> list[_MatchResult[T]]:
42
+ results = []
43
+
44
+ for item in items:
45
+ text = text_extractor(item)
46
+ similarity = _calculate_similarity(query, text)
47
+
48
+ if similarity >= min_similarity:
49
+ results.append(_MatchResult(item=item, similarity=similarity))
50
+
51
+ results = _sort_by_highest_similarity_first(results)
52
+
53
+ if limit:
54
+ return results[:limit]
55
+
56
+ return results
57
+
58
+
59
+ def _sort_by_highest_similarity_first(results: list[_MatchResult]) -> list[_MatchResult]:
60
+ return sorted(results, key=lambda x: x.similarity, reverse=True)
61
+
62
+
63
+ def _calculate_similarity(query: str, target: str) -> float:
64
+ return difflib.SequenceMatcher(
65
+ isjunk=None,
66
+ a=query.lower().strip(),
67
+ b=target.lower().strip(),
68
+ ).ratio()
@@ -0,0 +1,58 @@
1
+ import logging
2
+ import os
3
+ from typing import ClassVar
4
+
5
+ from dotenv import load_dotenv
6
+
7
+ load_dotenv(override=True)
8
+
9
+ logger = logging.getLogger("notionary")
10
+ logger.addHandler(logging.NullHandler())
11
+
12
+
13
+ def configure_library_logging(level: str = "WARNING") -> None:
14
+ log_level = getattr(logging, level.upper(), logging.WARNING)
15
+
16
+ library_logger = logging.getLogger("notionary")
17
+
18
+ if library_logger.handlers:
19
+ library_logger.handlers.clear()
20
+
21
+ handler = logging.StreamHandler()
22
+ handler.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
23
+
24
+ library_logger.setLevel(log_level)
25
+ library_logger.addHandler(handler)
26
+
27
+ _suppress_noisy_third_party_loggers()
28
+
29
+
30
+ def _suppress_noisy_third_party_loggers() -> None:
31
+ noisy_loggers = ["httpx", "httpcore", "httpcore.connection", "httpcore.http11"]
32
+
33
+ for logger_name in noisy_loggers:
34
+ logging.getLogger(logger_name).setLevel(logging.WARNING)
35
+
36
+
37
+ def _auto_configure_from_environment() -> None:
38
+ env_log_level = os.getenv("NOTIONARY_LOG_LEVEL")
39
+
40
+ if env_log_level:
41
+ configure_library_logging(env_log_level)
42
+
43
+
44
+ _auto_configure_from_environment()
45
+
46
+
47
+ class LoggingMixin:
48
+ logger: ClassVar[logging.Logger] = None
49
+
50
+ def __init_subclass__(cls, **kwargs):
51
+ super().__init_subclass__(**kwargs)
52
+ cls.logger = logging.getLogger(f"notionary.{cls.__name__}")
53
+
54
+ @property
55
+ def instance_logger(self) -> logging.Logger:
56
+ if not hasattr(self, "_logger"):
57
+ self._logger = logging.getLogger(f"notionary.{self.__class__.__name__}")
58
+ return self._logger
@@ -0,0 +1,100 @@
1
+ from collections.abc import AsyncGenerator, Callable, Coroutine
2
+ from typing import Any
3
+
4
+ from pydantic import BaseModel
5
+
6
+
7
+ class PaginatedResponse(BaseModel):
8
+ results: list[Any]
9
+ has_more: bool
10
+ next_cursor: str | None
11
+
12
+
13
+ async def _fetch_data(
14
+ api_call: Callable[..., Coroutine[Any, Any, PaginatedResponse]],
15
+ page_size: int | None = None,
16
+ **kwargs,
17
+ ) -> AsyncGenerator[PaginatedResponse]:
18
+ next_cursor = None
19
+ has_more = True
20
+ total_fetched = 0
21
+
22
+ while has_more and _should_continue_fetching(page_size, total_fetched):
23
+ request_params = _build_request_params(kwargs, next_cursor, page_size)
24
+ response = await api_call(**request_params)
25
+
26
+ limited_results = _apply_result_limit(response.results, page_size, total_fetched)
27
+ total_fetched += len(limited_results)
28
+
29
+ yield _create_limited_response(response, limited_results)
30
+
31
+ if _has_reached_limit(page_size, total_fetched):
32
+ break
33
+
34
+ has_more = response.has_more
35
+ next_cursor = response.next_cursor
36
+
37
+
38
+ def _should_continue_fetching(page_size: int | None, total_fetched: int) -> bool:
39
+ if page_size is None:
40
+ return True
41
+ return total_fetched < page_size
42
+
43
+
44
+ def _build_request_params(
45
+ base_kwargs: dict[str, Any],
46
+ cursor: str | None,
47
+ page_size: int | None,
48
+ ) -> dict[str, Any]:
49
+ params = base_kwargs.copy()
50
+
51
+ if cursor:
52
+ params["start_cursor"] = cursor
53
+
54
+ if page_size:
55
+ params["page_size"] = page_size
56
+
57
+ return params
58
+
59
+
60
+ def _apply_result_limit(results: list[Any], page_size: int | None, total_fetched: int) -> list[Any]:
61
+ if page_size is None:
62
+ return results
63
+
64
+ remaining = page_size - total_fetched
65
+ return results[:remaining]
66
+
67
+
68
+ def _has_reached_limit(page_size: int | None, total_fetched: int) -> bool:
69
+ if page_size is None:
70
+ return False
71
+ return total_fetched >= page_size
72
+
73
+
74
+ def _create_limited_response(original: PaginatedResponse, limited_results: list[Any]) -> PaginatedResponse:
75
+ return PaginatedResponse(
76
+ results=limited_results,
77
+ has_more=original.has_more and len(limited_results) == len(original.results),
78
+ next_cursor=original.next_cursor,
79
+ )
80
+
81
+
82
+ async def paginate_notion_api(
83
+ api_call: Callable[..., Coroutine[Any, Any, PaginatedResponse]],
84
+ page_size: int | None = None,
85
+ **kwargs,
86
+ ) -> list[Any]:
87
+ all_results = []
88
+ async for page in _fetch_data(api_call, page_size=page_size, **kwargs):
89
+ all_results.extend(page.results)
90
+ return all_results
91
+
92
+
93
+ async def paginate_notion_api_generator(
94
+ api_call: Callable[..., Coroutine[Any, Any, PaginatedResponse]],
95
+ page_size: int | None = None,
96
+ **kwargs,
97
+ ) -> AsyncGenerator[Any]:
98
+ async for page in _fetch_data(api_call, page_size=page_size, **kwargs):
99
+ for item in page.results:
100
+ yield item
@@ -0,0 +1,20 @@
1
+ import re
2
+
3
+
4
+ def extract_uuid(source: str) -> str | None:
5
+ UUID_RAW_PATTERN = r"([a-f0-9]{32})"
6
+
7
+ if _is_valid_uuid(source):
8
+ return source
9
+
10
+ match = re.search(UUID_RAW_PATTERN, source.lower())
11
+ if not match:
12
+ return None
13
+
14
+ uuid_raw = match.group(1)
15
+ return f"{uuid_raw[0:8]}-{uuid_raw[8:12]}-{uuid_raw[12:16]}-{uuid_raw[16:20]}-{uuid_raw[20:32]}"
16
+
17
+
18
+ def _is_valid_uuid(uuid: str) -> bool:
19
+ UUID_PATTERN = r"^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$"
20
+ return bool(re.match(UUID_PATTERN, uuid.lower()))
@@ -0,0 +1,4 @@
1
+ from .query import WorkspaceQueryConfigBuilder
2
+ from .service import NotionWorkspace
3
+
4
+ __all__ = ["NotionWorkspace", "WorkspaceQueryConfigBuilder"]
@@ -0,0 +1,62 @@
1
+ from collections.abc import AsyncGenerator
2
+
3
+ from notionary.data_source.schemas import DataSourceDto
4
+ from notionary.http.client import NotionHttpClient
5
+ from notionary.page.schemas import NotionPageDto
6
+ from notionary.shared.typings import JsonDict
7
+ from notionary.utils.pagination import paginate_notion_api_generator
8
+ from notionary.workspace.query.models import WorkspaceQueryConfig
9
+ from notionary.workspace.schemas import DataSourceSearchResponse, PageSearchResponse
10
+
11
+
12
+ class WorkspaceClient:
13
+ DEFAULT_PAGE_SIZE = 100
14
+
15
+ def __init__(self, http_client: NotionHttpClient | None = None) -> None:
16
+ self._http_client = http_client or NotionHttpClient()
17
+
18
+ async def query_pages_stream(
19
+ self,
20
+ search_config: WorkspaceQueryConfig,
21
+ ) -> AsyncGenerator[NotionPageDto]:
22
+ async for page in paginate_notion_api_generator(
23
+ self._query_pages,
24
+ search_config=search_config,
25
+ ):
26
+ yield page
27
+
28
+ async def query_data_sources_stream(
29
+ self,
30
+ search_config: WorkspaceQueryConfig,
31
+ ) -> AsyncGenerator[DataSourceDto]:
32
+ async for data_source in paginate_notion_api_generator(
33
+ self._query_data_sources,
34
+ search_config=search_config,
35
+ ):
36
+ yield data_source
37
+
38
+ async def _query_pages(
39
+ self,
40
+ search_config: WorkspaceQueryConfig,
41
+ start_cursor: str | None = None,
42
+ ) -> PageSearchResponse:
43
+ if start_cursor:
44
+ search_config.start_cursor = start_cursor
45
+
46
+ response = await self._execute_search(search_config)
47
+ return PageSearchResponse.model_validate(response)
48
+
49
+ async def _query_data_sources(
50
+ self,
51
+ search_config: WorkspaceQueryConfig,
52
+ start_cursor: str | None = None,
53
+ ) -> DataSourceSearchResponse:
54
+ if start_cursor:
55
+ search_config.start_cursor = start_cursor
56
+
57
+ response = await self._execute_search(search_config)
58
+ return DataSourceSearchResponse.model_validate(response)
59
+
60
+ async def _execute_search(self, config: WorkspaceQueryConfig) -> JsonDict:
61
+ serialized_config = config.model_dump(exclude_none=True, by_alias=True)
62
+ return await self._http_client.post("search", serialized_config)
@@ -0,0 +1,3 @@
1
+ from .builder import WorkspaceQueryConfigBuilder
2
+
3
+ __all__ = ["WorkspaceQueryConfigBuilder"]
@@ -0,0 +1,60 @@
1
+ from typing import Self
2
+
3
+ from notionary.workspace.query.models import (
4
+ SortDirection,
5
+ SortTimestamp,
6
+ WorkspaceQueryConfig,
7
+ WorkspaceQueryObjectType,
8
+ )
9
+
10
+
11
+ class WorkspaceQueryConfigBuilder:
12
+ def __init__(self, config: WorkspaceQueryConfig = None) -> None:
13
+ self.config = config or WorkspaceQueryConfig()
14
+
15
+ def with_query(self, query: str) -> Self:
16
+ self.config.query = query
17
+ return self
18
+
19
+ def with_pages_only(self) -> Self:
20
+ self.config.object_type = WorkspaceQueryObjectType.PAGE
21
+ return self
22
+
23
+ def with_data_sources_only(self) -> Self:
24
+ self.config.object_type = WorkspaceQueryObjectType.DATA_SOURCE
25
+ return self
26
+
27
+ def with_sort_direction(self, direction: SortDirection) -> Self:
28
+ self.config.sort_direction = direction
29
+ return self
30
+
31
+ def with_sort_ascending(self) -> Self:
32
+ return self.with_sort_direction(SortDirection.ASCENDING)
33
+
34
+ def with_sort_descending(self) -> Self:
35
+ return self.with_sort_direction(SortDirection.DESCENDING)
36
+
37
+ def with_sort_timestamp(self, timestamp: SortTimestamp) -> Self:
38
+ self.config.sort_timestamp = timestamp
39
+ return self
40
+
41
+ def with_sort_by_created_time(self) -> Self:
42
+ return self.with_sort_timestamp(SortTimestamp.CREATED_TIME)
43
+
44
+ def with_sort_by_last_edited(self) -> Self:
45
+ return self.with_sort_timestamp(SortTimestamp.LAST_EDITED_TIME)
46
+
47
+ def with_page_size(self, size: int) -> Self:
48
+ self.config.page_size = min(size, 100)
49
+ return self
50
+
51
+ def with_start_cursor(self, cursor: str | None) -> Self:
52
+ self.config.start_cursor = cursor
53
+ return self
54
+
55
+ def without_cursor(self) -> Self:
56
+ self.config.start_cursor = None
57
+ return self
58
+
59
+ def build(self) -> WorkspaceQueryConfig:
60
+ return self.config
@@ -0,0 +1,61 @@
1
+ from enum import StrEnum
2
+
3
+ from pydantic import BaseModel, Field, field_validator, model_serializer
4
+
5
+ from notionary.shared.typings import JsonDict
6
+
7
+
8
+ class SortDirection(StrEnum):
9
+ ASCENDING = "ascending"
10
+ DESCENDING = "descending"
11
+
12
+
13
+ class SortTimestamp(StrEnum):
14
+ LAST_EDITED_TIME = "last_edited_time"
15
+ CREATED_TIME = "created_time"
16
+
17
+
18
+ class WorkspaceQueryObjectType(StrEnum):
19
+ PAGE = "page"
20
+ DATA_SOURCE = "data_source"
21
+
22
+
23
+ class WorkspaceQueryConfig(BaseModel):
24
+ query: str | None = None
25
+ object_type: WorkspaceQueryObjectType | None = None
26
+ sort_direction: SortDirection = SortDirection.DESCENDING
27
+ sort_timestamp: SortTimestamp = SortTimestamp.LAST_EDITED_TIME
28
+ page_size: int = Field(default=100, ge=1, le=100)
29
+ start_cursor: str | None = None
30
+
31
+ @field_validator("query")
32
+ @classmethod
33
+ def replace_empty_query_with_none(cls, value: str | None) -> str | None:
34
+ if value is not None and not value.strip():
35
+ return None
36
+ return value
37
+
38
+ @model_serializer
39
+ def serialize_model(self) -> JsonDict:
40
+ search_dict: JsonDict = {}
41
+
42
+ if self.query:
43
+ search_dict["query"] = self.query
44
+
45
+ if self.object_type:
46
+ search_dict["filter"] = {
47
+ "property": "object",
48
+ "value": self.object_type.value,
49
+ }
50
+
51
+ search_dict["sort"] = {
52
+ "direction": self.sort_direction.value,
53
+ "timestamp": self.sort_timestamp.value,
54
+ }
55
+
56
+ search_dict["page_size"] = self.page_size
57
+
58
+ if self.start_cursor:
59
+ search_dict["start_cursor"] = self.start_cursor
60
+
61
+ return search_dict
@@ -0,0 +1,100 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from collections.abc import AsyncIterator
5
+ from typing import TYPE_CHECKING, Protocol
6
+
7
+ from notionary.exceptions.search import DatabaseNotFound, DataSourceNotFound, PageNotFound
8
+ from notionary.utils.fuzzy import find_all_matches
9
+ from notionary.workspace.client import WorkspaceClient
10
+ from notionary.workspace.query.builder import WorkspaceQueryConfigBuilder
11
+ from notionary.workspace.query.models import WorkspaceQueryConfig
12
+
13
+ if TYPE_CHECKING:
14
+ from notionary import NotionDatabase, NotionDataSource, NotionPage
15
+
16
+
17
+ class SearchableEntity(Protocol):
18
+ title: str
19
+
20
+
21
+ class WorkspaceQueryService:
22
+ def __init__(self, client: WorkspaceClient | None = None) -> None:
23
+ self._client = client or WorkspaceClient()
24
+
25
+ async def get_pages_stream(self, search_config: WorkspaceQueryConfig) -> AsyncIterator[NotionPage]:
26
+ from notionary import NotionPage
27
+
28
+ async for page_dto in self._client.query_pages_stream(search_config):
29
+ yield await NotionPage.from_id(page_dto.id)
30
+
31
+ async def get_pages(self, search_config: WorkspaceQueryConfig) -> list[NotionPage]:
32
+ from notionary import NotionPage
33
+
34
+ page_dtos = [dto async for dto in self._client.query_pages_stream(search_config)]
35
+ page_tasks = [NotionPage.from_id(dto.id) for dto in page_dtos]
36
+ return await asyncio.gather(*page_tasks)
37
+
38
+ async def get_data_sources_stream(self, search_config: WorkspaceQueryConfig) -> AsyncIterator[NotionDataSource]:
39
+ from notionary import NotionDataSource
40
+
41
+ async for data_source_dto in self._client.query_data_sources_stream(search_config):
42
+ yield await NotionDataSource.from_id(data_source_dto.id)
43
+
44
+ async def get_data_sources(self, search_config: WorkspaceQueryConfig) -> list[NotionDataSource]:
45
+ from notionary import NotionDataSource
46
+
47
+ data_source_dtos = [dto async for dto in self._client.query_data_sources_stream(search_config)]
48
+ data_source_tasks = [NotionDataSource.from_id(dto.id) for dto in data_source_dtos]
49
+ return await asyncio.gather(*data_source_tasks)
50
+
51
+ async def find_data_source(self, query: str) -> NotionDataSource:
52
+ config = WorkspaceQueryConfigBuilder().with_query(query).with_data_sources_only().with_page_size(100).build()
53
+ data_sources = await self.get_data_sources(config)
54
+ return self._find_exact_match(data_sources, query, DataSourceNotFound)
55
+
56
+ async def find_page(self, query: str) -> NotionPage:
57
+ config = WorkspaceQueryConfigBuilder().with_query(query).with_pages_only().with_page_size(100).build()
58
+ pages = await self.get_pages(config)
59
+ return self._find_exact_match(pages, query, PageNotFound)
60
+
61
+ async def find_database(self, query: str) -> NotionDatabase:
62
+ config = WorkspaceQueryConfigBuilder().with_query(query).with_data_sources_only().with_page_size(100).build()
63
+ data_sources = await self.get_data_sources(config)
64
+
65
+ parent_database_tasks = [data_source.get_parent_database() for data_source in data_sources]
66
+ parent_databases = await asyncio.gather(*parent_database_tasks)
67
+ potential_databases = [database for database in parent_databases if database is not None]
68
+
69
+ return self._find_exact_match(potential_databases, query, DatabaseNotFound)
70
+
71
+ def _find_exact_match(
72
+ self,
73
+ search_results: list[SearchableEntity],
74
+ query: str,
75
+ exception_class: type[Exception],
76
+ ) -> SearchableEntity:
77
+ if not search_results:
78
+ raise exception_class(query, [])
79
+
80
+ query_lower = query.lower()
81
+ exact_matches = [result for result in search_results if result.title.lower() == query_lower]
82
+
83
+ if exact_matches:
84
+ return exact_matches[0]
85
+
86
+ suggestions = self._get_fuzzy_suggestions(search_results, query)
87
+ raise exception_class(query, suggestions)
88
+
89
+ def _get_fuzzy_suggestions(self, search_results: list[SearchableEntity], query: str) -> list[str]:
90
+ sorted_by_similarity = find_all_matches(
91
+ query=query,
92
+ items=search_results,
93
+ text_extractor=lambda entity: entity.title,
94
+ min_similarity=0.6,
95
+ )
96
+
97
+ if sorted_by_similarity:
98
+ return [result.title for result in sorted_by_similarity[:5]]
99
+
100
+ return [result.title for result in search_results[:5]]
@@ -0,0 +1,21 @@
1
+ from typing import Generic, Literal, TypeVar
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from notionary.data_source.schemas import DataSourceDto
6
+ from notionary.page.schemas import NotionPageDto
7
+
8
+ T = TypeVar("T", bound=BaseModel)
9
+
10
+ PageOrDataSource = DataSourceDto | NotionPageDto
11
+
12
+
13
+ class SearchResponse(BaseModel, Generic[T]):
14
+ results: list[T]
15
+ next_cursor: str | None = None
16
+ has_more: bool
17
+ type: Literal["page_or_data_source"]
18
+
19
+
20
+ PageSearchResponse = SearchResponse[NotionPageDto]
21
+ DataSourceSearchResponse = SearchResponse[DataSourceDto]