notionary 0.2.26__py3-none-any.whl → 0.2.28__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 (387) hide show
  1. notionary/__init__.py +5 -20
  2. notionary/blocks/client.py +87 -215
  3. notionary/blocks/enums.py +167 -0
  4. notionary/blocks/rich_text/markdown_rich_text_converter.py +266 -0
  5. notionary/blocks/rich_text/models.py +164 -0
  6. notionary/blocks/rich_text/name_id_resolver/__init__.py +11 -0
  7. notionary/blocks/rich_text/name_id_resolver/database.py +31 -0
  8. notionary/blocks/rich_text/name_id_resolver/page.py +34 -0
  9. notionary/blocks/rich_text/name_id_resolver/person.py +37 -0
  10. notionary/blocks/rich_text/name_id_resolver/port.py +11 -0
  11. notionary/blocks/rich_text/rich_text_markdown_converter.py +132 -0
  12. notionary/blocks/rich_text/rich_text_patterns.py +39 -0
  13. notionary/blocks/schemas.py +746 -0
  14. notionary/comments/client.py +52 -187
  15. notionary/comments/factory.py +40 -0
  16. notionary/comments/models.py +5 -127
  17. notionary/comments/schemas.py +240 -0
  18. notionary/comments/service.py +34 -0
  19. notionary/data_source/http/client.py +11 -0
  20. notionary/data_source/http/data_source_instance_client.py +94 -0
  21. notionary/data_source/properties/models.py +406 -0
  22. notionary/data_source/query/builder.py +429 -0
  23. notionary/data_source/query/resolver.py +114 -0
  24. notionary/data_source/query/schema.py +304 -0
  25. notionary/data_source/query/validator.py +73 -0
  26. notionary/data_source/schemas.py +27 -0
  27. notionary/data_source/service.py +353 -0
  28. notionary/database/client.py +30 -135
  29. notionary/database/database_metadata_update_client.py +19 -0
  30. notionary/database/schemas.py +29 -0
  31. notionary/database/service.py +169 -0
  32. notionary/exceptions/__init__.py +33 -0
  33. notionary/exceptions/api.py +41 -0
  34. notionary/exceptions/base.py +2 -0
  35. notionary/exceptions/block_parsing.py +16 -0
  36. notionary/exceptions/data_source/__init__.py +6 -0
  37. notionary/exceptions/data_source/builder.py +182 -0
  38. notionary/exceptions/data_source/properties.py +34 -0
  39. notionary/exceptions/properties.py +58 -0
  40. notionary/exceptions/search.py +33 -0
  41. notionary/file_upload/client.py +18 -30
  42. notionary/file_upload/models.py +7 -8
  43. notionary/file_upload/{notion_file_upload.py → service.py} +29 -64
  44. notionary/http/client.py +205 -0
  45. notionary/http/models.py +49 -0
  46. notionary/page/blocks/client.py +1 -0
  47. notionary/page/content/factory.py +68 -0
  48. notionary/page/content/markdown/__init__.py +5 -0
  49. notionary/page/content/markdown/builder.py +304 -0
  50. notionary/page/content/markdown/nodes/__init__.py +54 -0
  51. notionary/page/content/markdown/nodes/audio.py +23 -0
  52. notionary/page/content/markdown/nodes/base.py +12 -0
  53. notionary/page/content/markdown/nodes/bookmark.py +25 -0
  54. notionary/page/content/markdown/nodes/breadcrumb.py +14 -0
  55. notionary/page/content/markdown/nodes/bulleted_list.py +18 -0
  56. notionary/page/content/markdown/nodes/callout.py +32 -0
  57. notionary/page/content/markdown/nodes/code.py +30 -0
  58. notionary/page/content/markdown/nodes/columns.py +51 -0
  59. notionary/page/content/markdown/nodes/divider.py +14 -0
  60. notionary/page/content/markdown/nodes/embed.py +23 -0
  61. notionary/page/content/markdown/nodes/equation.py +19 -0
  62. notionary/page/content/markdown/nodes/file.py +23 -0
  63. notionary/page/content/markdown/nodes/heading.py +16 -0
  64. notionary/page/content/markdown/nodes/image.py +23 -0
  65. notionary/page/content/markdown/nodes/mixins/caption.py +12 -0
  66. notionary/page/content/markdown/nodes/numbered_list.py +15 -0
  67. notionary/page/content/markdown/nodes/paragraph.py +14 -0
  68. notionary/page/content/markdown/nodes/pdf.py +23 -0
  69. notionary/page/content/markdown/nodes/quote.py +15 -0
  70. notionary/page/content/markdown/nodes/space.py +14 -0
  71. notionary/page/content/markdown/nodes/table.py +45 -0
  72. notionary/page/content/markdown/nodes/table_of_contents.py +14 -0
  73. notionary/page/content/markdown/nodes/todo.py +22 -0
  74. notionary/page/content/markdown/nodes/toggle.py +28 -0
  75. notionary/page/content/markdown/nodes/toggleable_heading.py +35 -0
  76. notionary/page/content/markdown/nodes/video.py +23 -0
  77. notionary/page/content/parser/context.py +49 -0
  78. notionary/page/content/parser/factory.py +219 -0
  79. notionary/page/content/parser/parsers/__init__.py +60 -0
  80. notionary/page/content/parser/parsers/audio.py +40 -0
  81. notionary/page/content/parser/parsers/base.py +30 -0
  82. notionary/page/content/parser/parsers/bookmark.py +33 -0
  83. notionary/page/content/parser/parsers/breadcrumb.py +33 -0
  84. notionary/page/content/parser/parsers/bulleted_list.py +41 -0
  85. notionary/page/content/parser/parsers/callout.py +129 -0
  86. notionary/page/content/parser/parsers/caption.py +55 -0
  87. notionary/page/content/parser/parsers/code.py +81 -0
  88. notionary/page/content/parser/parsers/column.py +117 -0
  89. notionary/page/content/parser/parsers/column_list.py +81 -0
  90. notionary/page/content/parser/parsers/divider.py +33 -0
  91. notionary/page/content/parser/parsers/embed.py +33 -0
  92. notionary/page/content/parser/parsers/equation.py +65 -0
  93. notionary/page/content/parser/parsers/file.py +42 -0
  94. notionary/page/content/parser/parsers/heading.py +58 -0
  95. notionary/page/content/parser/parsers/image.py +42 -0
  96. notionary/page/content/parser/parsers/numbered_list.py +45 -0
  97. notionary/page/content/parser/parsers/paragraph.py +36 -0
  98. notionary/page/content/parser/parsers/pdf.py +42 -0
  99. notionary/page/content/parser/parsers/quote.py +65 -0
  100. notionary/page/content/parser/parsers/space.py +35 -0
  101. notionary/page/content/parser/parsers/table.py +144 -0
  102. notionary/page/content/parser/parsers/table_of_contents.py +32 -0
  103. notionary/page/content/parser/parsers/todo.py +58 -0
  104. notionary/page/content/parser/parsers/toggle.py +127 -0
  105. notionary/page/content/parser/parsers/toggleable_heading.py +150 -0
  106. notionary/page/content/parser/parsers/video.py +42 -0
  107. notionary/page/content/parser/post_processing/handlers/__init__.py +5 -0
  108. notionary/page/content/parser/post_processing/handlers/rich_text_length.py +93 -0
  109. notionary/page/content/parser/post_processing/handlers/rich_text_length_truncation.py +93 -0
  110. notionary/page/content/parser/post_processing/port.py +9 -0
  111. notionary/page/content/parser/post_processing/service.py +16 -0
  112. notionary/page/content/parser/pre_processsing/handlers/__init__.py +9 -0
  113. notionary/page/content/parser/pre_processsing/handlers/column_syntax.py +80 -0
  114. notionary/page/content/parser/pre_processsing/handlers/port.py +7 -0
  115. notionary/page/content/parser/pre_processsing/handlers/whitespace.py +68 -0
  116. notionary/page/content/parser/pre_processsing/service.py +15 -0
  117. notionary/page/content/parser/service.py +69 -0
  118. notionary/page/content/renderer/context.py +48 -0
  119. notionary/page/content/renderer/factory.py +240 -0
  120. notionary/page/content/renderer/post_processing/handlers/__init__.py +5 -0
  121. notionary/page/content/renderer/post_processing/handlers/numbered_list_placeholdere.py +62 -0
  122. notionary/page/content/renderer/post_processing/port.py +7 -0
  123. notionary/page/content/renderer/post_processing/service.py +15 -0
  124. notionary/page/content/renderer/renderers/__init__.py +57 -0
  125. notionary/page/content/renderer/renderers/audio.py +31 -0
  126. notionary/page/content/renderer/renderers/base.py +31 -0
  127. notionary/page/content/renderer/renderers/bookmark.py +25 -0
  128. notionary/page/content/renderer/renderers/breadcrumb.py +21 -0
  129. notionary/page/content/renderer/renderers/bulleted_list.py +48 -0
  130. notionary/page/content/renderer/renderers/callout.py +65 -0
  131. notionary/page/content/renderer/renderers/captioned_block.py +58 -0
  132. notionary/page/content/renderer/renderers/code.py +34 -0
  133. notionary/page/content/renderer/renderers/column.py +44 -0
  134. notionary/page/content/renderer/renderers/column_list.py +31 -0
  135. notionary/page/content/renderer/renderers/divider.py +22 -0
  136. notionary/page/content/renderer/renderers/embed.py +25 -0
  137. notionary/page/content/renderer/renderers/equation.py +37 -0
  138. notionary/page/content/renderer/renderers/fallback.py +24 -0
  139. notionary/page/content/renderer/renderers/file.py +40 -0
  140. notionary/page/content/renderer/renderers/heading.py +69 -0
  141. notionary/page/content/renderer/renderers/image.py +31 -0
  142. notionary/page/content/renderer/renderers/numbered_list.py +41 -0
  143. notionary/page/content/renderer/renderers/paragraph.py +40 -0
  144. notionary/page/content/renderer/renderers/pdf.py +31 -0
  145. notionary/page/content/renderer/renderers/quote.py +49 -0
  146. notionary/page/content/renderer/renderers/table.py +115 -0
  147. notionary/page/content/renderer/renderers/table_of_contents.py +26 -0
  148. notionary/page/content/renderer/renderers/table_row.py +17 -0
  149. notionary/page/content/renderer/renderers/todo.py +56 -0
  150. notionary/page/content/renderer/renderers/toggle.py +53 -0
  151. notionary/page/content/renderer/renderers/toggleable_heading.py +78 -0
  152. notionary/page/content/renderer/renderers/video.py +31 -0
  153. notionary/page/content/renderer/service.py +50 -0
  154. notionary/page/content/service.py +65 -0
  155. notionary/page/content/syntax/models.py +68 -0
  156. notionary/page/content/syntax/service.py +453 -0
  157. notionary/page/page_context.py +7 -16
  158. notionary/page/page_http_client.py +15 -0
  159. notionary/page/page_metadata_update_client.py +19 -0
  160. notionary/page/properties/client.py +144 -0
  161. notionary/page/properties/factory.py +26 -0
  162. notionary/page/properties/models.py +307 -0
  163. notionary/page/properties/service.py +257 -0
  164. notionary/page/schemas.py +13 -0
  165. notionary/page/service.py +222 -0
  166. notionary/shared/entity/client.py +29 -0
  167. notionary/shared/entity/dto_parsers.py +53 -0
  168. notionary/shared/entity/entity_metadata_update_client.py +41 -0
  169. notionary/shared/entity/schemas.py +45 -0
  170. notionary/shared/entity/service.py +171 -0
  171. notionary/shared/models/cover.py +20 -0
  172. notionary/shared/models/file.py +21 -0
  173. notionary/shared/models/icon.py +28 -0
  174. notionary/shared/models/parent.py +41 -0
  175. notionary/shared/properties/type.py +30 -0
  176. notionary/user/__init__.py +4 -8
  177. notionary/user/base.py +89 -0
  178. notionary/user/bot.py +70 -0
  179. notionary/user/client.py +22 -111
  180. notionary/user/person.py +41 -0
  181. notionary/user/schemas.py +67 -0
  182. notionary/user/service.py +65 -0
  183. notionary/utils/async_retry.py +39 -0
  184. notionary/utils/date.py +51 -0
  185. notionary/utils/fuzzy.py +56 -0
  186. notionary/{util/logging_mixin.py → utils/mixins/logging.py} +4 -16
  187. notionary/utils/pagination.py +50 -0
  188. notionary/utils/singleton.py +13 -0
  189. notionary/utils/uuid_utils.py +20 -0
  190. notionary/workspace/__init__.py +3 -0
  191. notionary/workspace/client.py +62 -0
  192. notionary/workspace/query/builder.py +60 -0
  193. notionary/workspace/query/models.py +60 -0
  194. notionary/workspace/query/service.py +93 -0
  195. notionary/workspace/schemas.py +21 -0
  196. notionary/workspace/service.py +116 -0
  197. {notionary-0.2.26.dist-info → notionary-0.2.28.dist-info}/METADATA +54 -49
  198. notionary-0.2.28.dist-info/RECORD +200 -0
  199. {notionary-0.2.26.dist-info → notionary-0.2.28.dist-info}/WHEEL +1 -1
  200. {notionary-0.2.26.dist-info → notionary-0.2.28.dist-info/licenses}/LICENSE +9 -9
  201. notionary/base_notion_client.py +0 -219
  202. notionary/blocks/__init__.py +0 -5
  203. notionary/blocks/_bootstrap.py +0 -271
  204. notionary/blocks/audio/__init__.py +0 -11
  205. notionary/blocks/audio/audio_element.py +0 -158
  206. notionary/blocks/audio/audio_markdown_node.py +0 -24
  207. notionary/blocks/audio/audio_models.py +0 -10
  208. notionary/blocks/base_block_element.py +0 -42
  209. notionary/blocks/bookmark/__init__.py +0 -12
  210. notionary/blocks/bookmark/bookmark_element.py +0 -83
  211. notionary/blocks/bookmark/bookmark_markdown_node.py +0 -28
  212. notionary/blocks/bookmark/bookmark_models.py +0 -15
  213. notionary/blocks/breadcrumbs/__init__.py +0 -15
  214. notionary/blocks/breadcrumbs/breadcrumb_element.py +0 -39
  215. notionary/blocks/breadcrumbs/breadcrumb_markdown_node.py +0 -13
  216. notionary/blocks/breadcrumbs/breadcrumb_models.py +0 -12
  217. notionary/blocks/bulleted_list/__init__.py +0 -15
  218. notionary/blocks/bulleted_list/bulleted_list_element.py +0 -74
  219. notionary/blocks/bulleted_list/bulleted_list_markdown_node.py +0 -20
  220. notionary/blocks/bulleted_list/bulleted_list_models.py +0 -17
  221. notionary/blocks/callout/__init__.py +0 -12
  222. notionary/blocks/callout/callout_element.py +0 -99
  223. notionary/blocks/callout/callout_markdown_node.py +0 -19
  224. notionary/blocks/callout/callout_models.py +0 -33
  225. notionary/blocks/child_database/__init__.py +0 -14
  226. notionary/blocks/child_database/child_database_element.py +0 -59
  227. notionary/blocks/child_database/child_database_models.py +0 -12
  228. notionary/blocks/child_page/__init__.py +0 -9
  229. notionary/blocks/child_page/child_page_element.py +0 -94
  230. notionary/blocks/child_page/child_page_models.py +0 -12
  231. notionary/blocks/code/__init__.py +0 -11
  232. notionary/blocks/code/code_element.py +0 -149
  233. notionary/blocks/code/code_markdown_node.py +0 -80
  234. notionary/blocks/code/code_models.py +0 -94
  235. notionary/blocks/column/__init__.py +0 -25
  236. notionary/blocks/column/column_element.py +0 -65
  237. notionary/blocks/column/column_list_element.py +0 -52
  238. notionary/blocks/column/column_list_markdown_node.py +0 -34
  239. notionary/blocks/column/column_markdown_node.py +0 -42
  240. notionary/blocks/column/column_models.py +0 -26
  241. notionary/blocks/divider/__init__.py +0 -12
  242. notionary/blocks/divider/divider_element.py +0 -41
  243. notionary/blocks/divider/divider_markdown_node.py +0 -11
  244. notionary/blocks/divider/divider_models.py +0 -12
  245. notionary/blocks/embed/__init__.py +0 -12
  246. notionary/blocks/embed/embed_element.py +0 -98
  247. notionary/blocks/embed/embed_markdown_node.py +0 -19
  248. notionary/blocks/embed/embed_models.py +0 -14
  249. notionary/blocks/equation/__init__.py +0 -13
  250. notionary/blocks/equation/equation_element.py +0 -133
  251. notionary/blocks/equation/equation_element_markdown_node.py +0 -23
  252. notionary/blocks/equation/equation_models.py +0 -11
  253. notionary/blocks/file/__init__.py +0 -23
  254. notionary/blocks/file/file_element.py +0 -133
  255. notionary/blocks/file/file_element_markdown_node.py +0 -24
  256. notionary/blocks/file/file_element_models.py +0 -39
  257. notionary/blocks/heading/__init__.py +0 -19
  258. notionary/blocks/heading/heading_element.py +0 -112
  259. notionary/blocks/heading/heading_markdown_node.py +0 -16
  260. notionary/blocks/heading/heading_models.py +0 -29
  261. notionary/blocks/image_block/__init__.py +0 -11
  262. notionary/blocks/image_block/image_element.py +0 -130
  263. notionary/blocks/image_block/image_markdown_node.py +0 -25
  264. notionary/blocks/image_block/image_models.py +0 -10
  265. notionary/blocks/markdown/markdown_builder.py +0 -525
  266. notionary/blocks/markdown/markdown_document_model.py +0 -0
  267. notionary/blocks/markdown/markdown_node.py +0 -25
  268. notionary/blocks/mixins/captions/__init__.py +0 -4
  269. notionary/blocks/mixins/captions/caption_markdown_node_mixin.py +0 -31
  270. notionary/blocks/mixins/captions/caption_mixin.py +0 -92
  271. notionary/blocks/mixins/file_upload/__init__.py +0 -3
  272. notionary/blocks/mixins/file_upload/file_upload_mixin.py +0 -320
  273. notionary/blocks/models.py +0 -174
  274. notionary/blocks/numbered_list/__init__.py +0 -16
  275. notionary/blocks/numbered_list/numbered_list_element.py +0 -65
  276. notionary/blocks/numbered_list/numbered_list_markdown_node.py +0 -17
  277. notionary/blocks/numbered_list/numbered_list_models.py +0 -17
  278. notionary/blocks/paragraph/__init__.py +0 -15
  279. notionary/blocks/paragraph/paragraph_element.py +0 -58
  280. notionary/blocks/paragraph/paragraph_markdown_node.py +0 -16
  281. notionary/blocks/paragraph/paragraph_models.py +0 -16
  282. notionary/blocks/pdf/__init__.py +0 -11
  283. notionary/blocks/pdf/pdf_element.py +0 -146
  284. notionary/blocks/pdf/pdf_markdown_node.py +0 -24
  285. notionary/blocks/pdf/pdf_models.py +0 -11
  286. notionary/blocks/quote/__init__.py +0 -14
  287. notionary/blocks/quote/quote_element.py +0 -75
  288. notionary/blocks/quote/quote_markdown_node.py +0 -16
  289. notionary/blocks/quote/quote_models.py +0 -18
  290. notionary/blocks/registry/__init__.py +0 -3
  291. notionary/blocks/registry/block_registry.py +0 -150
  292. notionary/blocks/rich_text/__init__.py +0 -33
  293. notionary/blocks/rich_text/rich_text_models.py +0 -221
  294. notionary/blocks/rich_text/text_inline_formatter.py +0 -456
  295. notionary/blocks/syntax_prompt_builder.py +0 -137
  296. notionary/blocks/table/__init__.py +0 -19
  297. notionary/blocks/table/table_element.py +0 -225
  298. notionary/blocks/table/table_markdown_node.py +0 -42
  299. notionary/blocks/table/table_models.py +0 -28
  300. notionary/blocks/table_of_contents/__init__.py +0 -17
  301. notionary/blocks/table_of_contents/table_of_contents_element.py +0 -80
  302. notionary/blocks/table_of_contents/table_of_contents_markdown_node.py +0 -21
  303. notionary/blocks/table_of_contents/table_of_contents_models.py +0 -18
  304. notionary/blocks/todo/__init__.py +0 -12
  305. notionary/blocks/todo/todo_element.py +0 -81
  306. notionary/blocks/todo/todo_markdown_node.py +0 -21
  307. notionary/blocks/todo/todo_models.py +0 -18
  308. notionary/blocks/toggle/__init__.py +0 -12
  309. notionary/blocks/toggle/toggle_element.py +0 -112
  310. notionary/blocks/toggle/toggle_markdown_node.py +0 -31
  311. notionary/blocks/toggle/toggle_models.py +0 -17
  312. notionary/blocks/toggleable_heading/__init__.py +0 -11
  313. notionary/blocks/toggleable_heading/toggleable_heading_element.py +0 -115
  314. notionary/blocks/toggleable_heading/toggleable_heading_markdown_node.py +0 -34
  315. notionary/blocks/types.py +0 -130
  316. notionary/blocks/video/__init__.py +0 -11
  317. notionary/blocks/video/video_element.py +0 -187
  318. notionary/blocks/video/video_element_models.py +0 -10
  319. notionary/blocks/video/video_markdown_node.py +0 -26
  320. notionary/comments/__init__.py +0 -26
  321. notionary/database/__init__.py +0 -4
  322. notionary/database/database.py +0 -480
  323. notionary/database/database_filter_builder.py +0 -173
  324. notionary/database/database_provider.py +0 -227
  325. notionary/database/exceptions.py +0 -13
  326. notionary/database/factory.py +0 -0
  327. notionary/database/models.py +0 -337
  328. notionary/database/notion_database.py +0 -487
  329. notionary/file_upload/__init__.py +0 -7
  330. notionary/page/client.py +0 -124
  331. notionary/page/markdown_whitespace_processor.py +0 -129
  332. notionary/page/models.py +0 -322
  333. notionary/page/notion_page.py +0 -674
  334. notionary/page/page_content_deleting_service.py +0 -117
  335. notionary/page/page_content_writer.py +0 -80
  336. notionary/page/property_formatter.py +0 -99
  337. notionary/page/reader/handler/__init__.py +0 -19
  338. notionary/page/reader/handler/base_block_renderer.py +0 -44
  339. notionary/page/reader/handler/block_processing_context.py +0 -35
  340. notionary/page/reader/handler/block_rendering_context.py +0 -48
  341. notionary/page/reader/handler/column_list_renderer.py +0 -51
  342. notionary/page/reader/handler/column_renderer.py +0 -60
  343. notionary/page/reader/handler/equation_renderer.py +0 -0
  344. notionary/page/reader/handler/line_renderer.py +0 -73
  345. notionary/page/reader/handler/numbered_list_renderer.py +0 -85
  346. notionary/page/reader/handler/toggle_renderer.py +0 -69
  347. notionary/page/reader/handler/toggleable_heading_renderer.py +0 -89
  348. notionary/page/reader/page_content_retriever.py +0 -81
  349. notionary/page/search_filter_builder.py +0 -132
  350. notionary/page/utils.py +0 -60
  351. notionary/page/writer/handler/__init__.py +0 -24
  352. notionary/page/writer/handler/code_handler.py +0 -72
  353. notionary/page/writer/handler/column_handler.py +0 -141
  354. notionary/page/writer/handler/column_list_handler.py +0 -139
  355. notionary/page/writer/handler/equation_handler.py +0 -74
  356. notionary/page/writer/handler/line_handler.py +0 -35
  357. notionary/page/writer/handler/line_processing_context.py +0 -54
  358. notionary/page/writer/handler/regular_line_handler.py +0 -86
  359. notionary/page/writer/handler/table_handler.py +0 -66
  360. notionary/page/writer/handler/toggle_handler.py +0 -159
  361. notionary/page/writer/handler/toggleable_heading_handler.py +0 -174
  362. notionary/page/writer/markdown_to_notion_converter.py +0 -139
  363. notionary/page/writer/markdown_to_notion_converter_context.py +0 -30
  364. notionary/page/writer/markdown_to_notion_text_length_post_processor.py +0 -0
  365. notionary/page/writer/notion_text_length_processor.py +0 -150
  366. notionary/schemas/__init__.py +0 -3
  367. notionary/schemas/base.py +0 -73
  368. notionary/shared/__init__.py +0 -3
  369. notionary/shared/name_to_id_resolver.py +0 -203
  370. notionary/telemetry/__init__.py +0 -19
  371. notionary/telemetry/service.py +0 -136
  372. notionary/telemetry/views.py +0 -73
  373. notionary/user/base_notion_user.py +0 -53
  374. notionary/user/models.py +0 -84
  375. notionary/user/notion_bot_user.py +0 -226
  376. notionary/user/notion_user.py +0 -255
  377. notionary/user/notion_user_manager.py +0 -101
  378. notionary/util/__init__.py +0 -15
  379. notionary/util/concurrency_limiter.py +0 -0
  380. notionary/util/factory_decorator.py +0 -0
  381. notionary/util/factory_only.py +0 -37
  382. notionary/util/fuzzy.py +0 -75
  383. notionary/util/page_id_utils.py +0 -27
  384. notionary/util/singleton.py +0 -18
  385. notionary/util/singleton_metaclass.py +0 -22
  386. notionary/workspace.py +0 -105
  387. notionary-0.2.26.dist-info/RECORD +0 -202
@@ -0,0 +1,429 @@
1
+ from collections.abc import Callable
2
+ from typing import Self
3
+
4
+ from notionary.data_source.properties.models import DataSourceProperty
5
+ from notionary.data_source.query.schema import (
6
+ ArrayOperator,
7
+ BooleanOperator,
8
+ CompoundFilter,
9
+ DataSourceQueryParams,
10
+ DateOperator,
11
+ FieldType,
12
+ FilterCondition,
13
+ InternalFilterCondition,
14
+ LogicalOperator,
15
+ NotionFilter,
16
+ NotionSort,
17
+ NumberOperator,
18
+ Operator,
19
+ OrGroupMarker,
20
+ PropertyFilter,
21
+ PropertySort,
22
+ SortDirection,
23
+ StringOperator,
24
+ TimestampSort,
25
+ TimestampType,
26
+ )
27
+ from notionary.data_source.query.validator import OperatorValidator
28
+ from notionary.exceptions.data_source.properties import DataSourcePropertyNotFound
29
+ from notionary.utils.date import parse_date
30
+
31
+
32
+ class DataSourceQueryBuilder:
33
+ def __init__(
34
+ self,
35
+ properties: dict[str, DataSourceProperty],
36
+ operator_validator: OperatorValidator | None = None,
37
+ date_parser: Callable[[str], str] = parse_date,
38
+ ) -> None:
39
+ self._properties = properties
40
+ self._operator_validator = operator_validator or OperatorValidator()
41
+ self._date_parser = date_parser
42
+
43
+ self._filters: list[InternalFilterCondition] = []
44
+ self._sorts: list[NotionSort] = []
45
+ self._current_property: str | None = None
46
+ self._negate_next = False
47
+ self._or_group: list[FilterCondition] | None = None
48
+
49
+ def where(self, property_name: str) -> Self:
50
+ self._finalize_current_or_group()
51
+ self._ensure_property_exists(property_name)
52
+ self._select_property_without_negation(property_name)
53
+ return self
54
+
55
+ def where_not(self, property_name: str) -> Self:
56
+ self._finalize_current_or_group()
57
+ self._ensure_property_exists(property_name)
58
+ self._select_property_with_negation(property_name)
59
+ return self
60
+
61
+ def and_where(self, property_name: str) -> Self:
62
+ return self.where(property_name)
63
+
64
+ def and_where_not(self, property_name: str) -> Self:
65
+ return self.where_not(property_name)
66
+
67
+ def or_where(self, property_name: str) -> Self:
68
+ self._ensure_or_group_exists()
69
+ self._ensure_property_exists(property_name)
70
+ self._select_property_without_negation(property_name)
71
+ return self
72
+
73
+ def or_where_not(self, property_name: str) -> Self:
74
+ self._ensure_or_group_exists()
75
+ self._ensure_property_exists(property_name)
76
+ self._select_property_with_negation(property_name)
77
+ return self
78
+
79
+ def equals(self, value: str | int | float) -> Self:
80
+ return self._add_filter(StringOperator.EQUALS, value)
81
+
82
+ def does_not_equal(self, value: str | int | float) -> Self:
83
+ return self._add_filter(StringOperator.DOES_NOT_EQUAL, value)
84
+
85
+ def contains(self, value: str) -> Self:
86
+ return self._add_filter(StringOperator.CONTAINS, value)
87
+
88
+ def does_not_contain(self, value: str) -> Self:
89
+ return self._add_filter(StringOperator.DOES_NOT_CONTAIN, value)
90
+
91
+ def starts_with(self, value: str) -> Self:
92
+ return self._add_filter(StringOperator.STARTS_WITH, value)
93
+
94
+ def ends_with(self, value: str) -> Self:
95
+ return self._add_filter(StringOperator.ENDS_WITH, value)
96
+
97
+ def is_empty(self) -> Self:
98
+ return self._add_filter(StringOperator.IS_EMPTY, None)
99
+
100
+ def is_not_empty(self) -> Self:
101
+ return self._add_filter(StringOperator.IS_NOT_EMPTY, None)
102
+
103
+ def greater_than(self, value: float | int) -> Self:
104
+ return self._add_filter(NumberOperator.GREATER_THAN, value)
105
+
106
+ def greater_than_or_equal_to(self, value: float | int) -> Self:
107
+ return self._add_filter(NumberOperator.GREATER_THAN_OR_EQUAL_TO, value)
108
+
109
+ def less_than(self, value: float | int) -> Self:
110
+ return self._add_filter(NumberOperator.LESS_THAN, value)
111
+
112
+ def less_than_or_equal_to(self, value: float | int) -> Self:
113
+ return self._add_filter(NumberOperator.LESS_THAN_OR_EQUAL_TO, value)
114
+
115
+ def is_true(self) -> Self:
116
+ return self._add_filter(BooleanOperator.IS_TRUE, None)
117
+
118
+ def is_false(self) -> Self:
119
+ return self._add_filter(BooleanOperator.IS_FALSE, None)
120
+
121
+ def before(self, date: str) -> Self:
122
+ parsed_date = self._date_parser(date)
123
+ return self._add_filter(DateOperator.BEFORE, parsed_date)
124
+
125
+ def after(self, date: str) -> Self:
126
+ parsed_date = self._date_parser(date)
127
+ return self._add_filter(DateOperator.AFTER, parsed_date)
128
+
129
+ def on_or_before(self, date: str) -> Self:
130
+ parsed_date = self._date_parser(date)
131
+ return self._add_filter(DateOperator.ON_OR_BEFORE, parsed_date)
132
+
133
+ def on_or_after(self, date: str) -> Self:
134
+ parsed_date = self._date_parser(date)
135
+ return self._add_filter(DateOperator.ON_OR_AFTER, parsed_date)
136
+
137
+ def array_contains(self, value: str) -> Self:
138
+ return self._add_filter(ArrayOperator.CONTAINS, value)
139
+
140
+ def array_does_not_contain(self, value: str) -> Self:
141
+ return self._add_filter(ArrayOperator.DOES_NOT_CONTAIN, value)
142
+
143
+ def array_is_empty(self) -> Self:
144
+ return self._add_filter(ArrayOperator.IS_EMPTY, None)
145
+
146
+ def array_is_not_empty(self) -> Self:
147
+ return self._add_filter(ArrayOperator.IS_NOT_EMPTY, None)
148
+
149
+ def relation_contains(self, uuid: str) -> Self:
150
+ return self._add_filter(ArrayOperator.CONTAINS, uuid)
151
+
152
+ def relation_is_empty(self) -> Self:
153
+ return self._add_filter(ArrayOperator.IS_EMPTY, None)
154
+
155
+ def people_contains(self, uuid: str) -> Self:
156
+ return self._add_filter(ArrayOperator.CONTAINS, uuid)
157
+
158
+ def people_is_empty(self) -> Self:
159
+ return self._add_filter(ArrayOperator.IS_EMPTY, None)
160
+
161
+ def order_by(self, property_name: str, direction: SortDirection = SortDirection.ASCENDING) -> Self:
162
+ self._ensure_property_exists(property_name)
163
+ sort = PropertySort(property=property_name, direction=direction)
164
+ self._sorts.append(sort)
165
+ return self
166
+
167
+ def order_by_ascending(self, property_name: str) -> Self:
168
+ return self.order_by(property_name, SortDirection.ASCENDING)
169
+
170
+ def order_by_descending(self, property_name: str) -> Self:
171
+ return self.order_by(property_name, SortDirection.DESCENDING)
172
+
173
+ def order_by_created_time(self, direction: SortDirection = SortDirection.DESCENDING) -> Self:
174
+ sort = TimestampSort(timestamp=TimestampType.CREATED_TIME, direction=direction)
175
+ self._sorts.append(sort)
176
+ return self
177
+
178
+ def order_by_last_edited_time(self, direction: SortDirection = SortDirection.DESCENDING) -> Self:
179
+ sort = TimestampSort(timestamp=TimestampType.LAST_EDITED_TIME, direction=direction)
180
+ self._sorts.append(sort)
181
+ return self
182
+
183
+ def build(self) -> DataSourceQueryParams:
184
+ self._finalize_current_or_group()
185
+ notion_filter = self._create_notion_filter_if_needed()
186
+ sorts = self._create_sorts_if_needed()
187
+ return DataSourceQueryParams(filter=notion_filter, sorts=sorts)
188
+
189
+ def _select_property_without_negation(self, property_name: str) -> None:
190
+ self._current_property = property_name
191
+ self._negate_next = False
192
+
193
+ def _select_property_with_negation(self, property_name: str) -> None:
194
+ self._current_property = property_name
195
+ self._negate_next = True
196
+
197
+ def _ensure_property_exists(self, property_name: str) -> None:
198
+ if self._has_no_properties():
199
+ return
200
+
201
+ if self._property_is_unknown(property_name):
202
+ self._raise_property_not_found_error(property_name)
203
+
204
+ def _has_no_properties(self) -> bool:
205
+ return not self._properties
206
+
207
+ def _property_is_unknown(self, property_name: str) -> bool:
208
+ return property_name not in self._properties
209
+
210
+ def _raise_property_not_found_error(self, property_name: str) -> None:
211
+ available_properties = list(self._properties.keys())
212
+ raise DataSourcePropertyNotFound(
213
+ property_name=property_name,
214
+ available_properties=available_properties,
215
+ )
216
+
217
+ def _ensure_or_group_exists(self) -> None:
218
+ if self._or_group_is_not_active():
219
+ self._start_new_or_group()
220
+
221
+ def _or_group_is_not_active(self) -> bool:
222
+ return self._or_group is None
223
+
224
+ def _start_new_or_group(self) -> None:
225
+ self._or_group = []
226
+ self._move_last_filter_to_or_group()
227
+
228
+ def _move_last_filter_to_or_group(self) -> None:
229
+ if self._has_no_filters():
230
+ return
231
+
232
+ last_filter = self._filters.pop()
233
+ if self._is_regular_filter_condition(last_filter):
234
+ self._or_group.append(last_filter)
235
+
236
+ def _has_no_filters(self) -> bool:
237
+ return not self._filters
238
+
239
+ def _is_regular_filter_condition(self, filter_item: InternalFilterCondition) -> bool:
240
+ return isinstance(filter_item, FilterCondition)
241
+
242
+ def _finalize_current_or_group(self) -> None:
243
+ if self._should_finalize_or_group():
244
+ self._convert_or_group_to_marker()
245
+ self._clear_or_group()
246
+
247
+ def _should_finalize_or_group(self) -> bool:
248
+ return self._or_group is not None and len(self._or_group) > 0
249
+
250
+ def _convert_or_group_to_marker(self) -> None:
251
+ or_marker = OrGroupMarker(conditions=self._or_group)
252
+ self._filters.append(or_marker)
253
+
254
+ def _clear_or_group(self) -> None:
255
+ self._or_group = None
256
+
257
+ def _add_filter(
258
+ self,
259
+ operator: StringOperator | NumberOperator | BooleanOperator | DateOperator | ArrayOperator,
260
+ value: str | int | float | list[str | int | float] | None,
261
+ ) -> Self:
262
+ self._ensure_property_is_selected()
263
+ self._validate_operator_for_current_property(operator)
264
+ final_operator = self._apply_negation_if_needed(operator)
265
+ filter_condition = self._create_filter_condition(final_operator, value)
266
+ self._store_filter_condition(filter_condition)
267
+ self._reset_current_property()
268
+ return self
269
+
270
+ def _validate_operator_for_current_property(self, operator: Operator) -> None:
271
+ if not self._current_property or not self._properties:
272
+ return
273
+
274
+ property_obj = self._properties.get(self._current_property)
275
+ if property_obj:
276
+ self._operator_validator.validate_operator_for_property(self._current_property, property_obj, operator)
277
+ return self
278
+
279
+ def _ensure_property_is_selected(self) -> None:
280
+ if self._no_property_is_selected():
281
+ raise ValueError("No property selected. Use .where(property_name) first.")
282
+
283
+ def _no_property_is_selected(self) -> bool:
284
+ return self._current_property is None
285
+
286
+ def _apply_negation_if_needed(
287
+ self,
288
+ operator: StringOperator | NumberOperator | BooleanOperator | DateOperator | ArrayOperator,
289
+ ) -> StringOperator | NumberOperator | BooleanOperator | DateOperator | ArrayOperator:
290
+ if not self._negate_next:
291
+ return operator
292
+
293
+ negated_operator = self._negate_operator(operator)
294
+ self._negate_next = False
295
+ return negated_operator
296
+
297
+ def _create_filter_condition(
298
+ self,
299
+ operator: StringOperator | NumberOperator | BooleanOperator | DateOperator | ArrayOperator,
300
+ value: str | int | float | list[str | int | float] | None,
301
+ ) -> FilterCondition:
302
+ field_type = self._determine_field_type_from_operator(operator)
303
+ return FilterCondition(
304
+ field=self._current_property,
305
+ field_type=field_type,
306
+ operator=operator,
307
+ value=value,
308
+ )
309
+
310
+ def _store_filter_condition(self, filter_condition: FilterCondition) -> None:
311
+ if self._or_group_is_active():
312
+ self._or_group.append(filter_condition)
313
+ else:
314
+ self._filters.append(filter_condition)
315
+
316
+ def _or_group_is_active(self) -> bool:
317
+ return self._or_group is not None
318
+
319
+ def _reset_current_property(self) -> None:
320
+ self._current_property = None
321
+
322
+ def _create_notion_filter_if_needed(self) -> NotionFilter | None:
323
+ if self._has_no_filters():
324
+ return None
325
+ return self._build_notion_filter()
326
+
327
+ def _create_sorts_if_needed(self) -> list[NotionSort] | None:
328
+ if not self._sorts:
329
+ return None
330
+ return self._sorts
331
+
332
+ def _build_notion_filter(self) -> NotionFilter:
333
+ if self._has_single_filter():
334
+ return self._build_single_filter()
335
+ return self._build_compound_and_filter()
336
+
337
+ def _has_single_filter(self) -> bool:
338
+ return len(self._filters) == 1
339
+
340
+ def _build_single_filter(self) -> NotionFilter:
341
+ return self._build_filter(self._filters[0])
342
+
343
+ def _build_compound_and_filter(self) -> CompoundFilter:
344
+ property_filters = [self._build_filter(f) for f in self._filters]
345
+ return CompoundFilter(operator=LogicalOperator.AND, filters=property_filters)
346
+
347
+ def _build_filter(self, condition: InternalFilterCondition) -> PropertyFilter | CompoundFilter:
348
+ if isinstance(condition, OrGroupMarker):
349
+ return self._build_or_compound_filter(condition)
350
+ return self._build_property_filter(condition)
351
+
352
+ def _build_or_compound_filter(self, or_marker: OrGroupMarker) -> CompoundFilter:
353
+ property_filters = [self._build_property_filter(c) for c in or_marker.conditions]
354
+ return CompoundFilter(operator=LogicalOperator.OR, filters=property_filters)
355
+
356
+ def _build_property_filter(self, condition: FilterCondition) -> PropertyFilter:
357
+ property_definition = self._get_property_definition(condition.field)
358
+ return PropertyFilter(
359
+ property=condition.field,
360
+ property_type=property_definition.type,
361
+ operator=condition.operator,
362
+ value=condition.value,
363
+ )
364
+
365
+ def _get_property_definition(self, property_name: str) -> DataSourceProperty:
366
+ property_definition = self._properties.get(property_name)
367
+ if property_definition is None:
368
+ self._raise_property_not_found_error(property_name)
369
+ return property_definition
370
+
371
+ def _negate_operator(
372
+ self,
373
+ operator: StringOperator | NumberOperator | BooleanOperator | DateOperator | ArrayOperator,
374
+ ) -> StringOperator | NumberOperator | BooleanOperator | DateOperator | ArrayOperator:
375
+ negation_map = {
376
+ StringOperator.EQUALS: StringOperator.DOES_NOT_EQUAL,
377
+ StringOperator.DOES_NOT_EQUAL: StringOperator.EQUALS,
378
+ StringOperator.CONTAINS: StringOperator.DOES_NOT_CONTAIN,
379
+ StringOperator.DOES_NOT_CONTAIN: StringOperator.CONTAINS,
380
+ StringOperator.IS_EMPTY: StringOperator.IS_NOT_EMPTY,
381
+ StringOperator.IS_NOT_EMPTY: StringOperator.IS_EMPTY,
382
+ NumberOperator.EQUALS: NumberOperator.DOES_NOT_EQUAL,
383
+ NumberOperator.DOES_NOT_EQUAL: NumberOperator.EQUALS,
384
+ NumberOperator.GREATER_THAN: NumberOperator.LESS_THAN_OR_EQUAL_TO,
385
+ NumberOperator.GREATER_THAN_OR_EQUAL_TO: NumberOperator.LESS_THAN,
386
+ NumberOperator.LESS_THAN: NumberOperator.GREATER_THAN_OR_EQUAL_TO,
387
+ NumberOperator.LESS_THAN_OR_EQUAL_TO: NumberOperator.GREATER_THAN,
388
+ NumberOperator.IS_EMPTY: NumberOperator.IS_NOT_EMPTY,
389
+ NumberOperator.IS_NOT_EMPTY: NumberOperator.IS_EMPTY,
390
+ BooleanOperator.IS_TRUE: BooleanOperator.IS_FALSE,
391
+ BooleanOperator.IS_FALSE: BooleanOperator.IS_TRUE,
392
+ DateOperator.BEFORE: DateOperator.ON_OR_AFTER,
393
+ DateOperator.AFTER: DateOperator.ON_OR_BEFORE,
394
+ DateOperator.ON_OR_BEFORE: DateOperator.AFTER,
395
+ DateOperator.ON_OR_AFTER: DateOperator.BEFORE,
396
+ DateOperator.IS_EMPTY: DateOperator.IS_NOT_EMPTY,
397
+ DateOperator.IS_NOT_EMPTY: DateOperator.IS_EMPTY,
398
+ ArrayOperator.CONTAINS: ArrayOperator.DOES_NOT_CONTAIN,
399
+ ArrayOperator.DOES_NOT_CONTAIN: ArrayOperator.CONTAINS,
400
+ ArrayOperator.IS_EMPTY: ArrayOperator.IS_NOT_EMPTY,
401
+ ArrayOperator.IS_NOT_EMPTY: ArrayOperator.IS_EMPTY,
402
+ }
403
+
404
+ if operator not in negation_map:
405
+ self._raise_operator_cannot_be_negated_error(operator)
406
+
407
+ return negation_map[operator]
408
+
409
+ def _raise_operator_cannot_be_negated_error(
410
+ self,
411
+ operator: StringOperator | NumberOperator | BooleanOperator | DateOperator | ArrayOperator,
412
+ ) -> None:
413
+ raise ValueError(f"Operator '{operator}' cannot be negated. This should not happen - please report this issue.")
414
+
415
+ def _determine_field_type_from_operator(
416
+ self,
417
+ operator: StringOperator | NumberOperator | BooleanOperator | DateOperator | ArrayOperator,
418
+ ) -> FieldType:
419
+ if isinstance(operator, StringOperator):
420
+ return FieldType.STRING
421
+ if isinstance(operator, NumberOperator):
422
+ return FieldType.NUMBER
423
+ if isinstance(operator, BooleanOperator):
424
+ return FieldType.BOOLEAN
425
+ if isinstance(operator, DateOperator):
426
+ return FieldType.DATE
427
+ if isinstance(operator, ArrayOperator):
428
+ return FieldType.ARRAY
429
+ return FieldType.STRING
@@ -0,0 +1,114 @@
1
+ import re
2
+ from uuid import UUID
3
+
4
+ from notionary.blocks.rich_text.name_id_resolver.page import PageNameIdResolver
5
+ from notionary.blocks.rich_text.name_id_resolver.person import PersonNameIdResolver
6
+ from notionary.data_source.query.schema import (
7
+ CompoundFilter,
8
+ DataSourceQueryParams,
9
+ NotionFilter,
10
+ PropertyFilter,
11
+ )
12
+ from notionary.shared.properties.type import PropertyType
13
+ from notionary.utils.mixins.logging import LoggingMixin
14
+
15
+
16
+ class QueryResolver(LoggingMixin):
17
+ UUID_PATTERN = re.compile(r"^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$", re.IGNORECASE)
18
+
19
+ def __init__(
20
+ self,
21
+ user_resolver: PersonNameIdResolver | None = None,
22
+ page_resolver: PageNameIdResolver | None = None,
23
+ ):
24
+ self._user_resolver = user_resolver or PersonNameIdResolver()
25
+ self._page_resolver = page_resolver or PageNameIdResolver()
26
+
27
+ async def resolve_params(self, params: DataSourceQueryParams) -> DataSourceQueryParams:
28
+ if not params.filter:
29
+ return params
30
+
31
+ resolved_filter = await self._resolve_filter(params.filter)
32
+ return DataSourceQueryParams(filter=resolved_filter, sorts=params.sorts)
33
+
34
+ async def _resolve_filter(self, filter: NotionFilter) -> NotionFilter:
35
+ if isinstance(filter, PropertyFilter):
36
+ return await self._resolve_property_filter(filter)
37
+ elif isinstance(filter, CompoundFilter):
38
+ return await self._resolve_compound_filter(filter)
39
+ return filter
40
+
41
+ async def _resolve_compound_filter(self, compound: CompoundFilter) -> CompoundFilter:
42
+ resolved_filters = []
43
+ for filter in compound.filters:
44
+ resolved = await self._resolve_filter(filter)
45
+ resolved_filters.append(resolved)
46
+
47
+ return CompoundFilter(operator=compound.operator, filters=resolved_filters)
48
+
49
+ async def _resolve_property_filter(self, prop_filter: PropertyFilter) -> PropertyFilter:
50
+ if not self._is_resolvable_property_type(prop_filter.property_type):
51
+ return prop_filter
52
+
53
+ if prop_filter.value is None:
54
+ return prop_filter
55
+
56
+ if self._is_uuid(prop_filter.value):
57
+ return prop_filter
58
+
59
+ resolved_value = await self._resolve_value(prop_filter.value, prop_filter.property_type)
60
+
61
+ return PropertyFilter(
62
+ property=prop_filter.property,
63
+ property_type=prop_filter.property_type,
64
+ operator=prop_filter.operator,
65
+ value=resolved_value,
66
+ )
67
+
68
+ def _is_resolvable_property_type(self, property_type: PropertyType) -> bool:
69
+ return property_type in (PropertyType.PEOPLE, PropertyType.RELATION)
70
+
71
+ def _is_uuid(self, value: str | int | float | bool | list) -> bool:
72
+ if not isinstance(value, str):
73
+ return False
74
+
75
+ return self._is_standard_uuid(value) or self._is_notion_style_uuid(value)
76
+
77
+ def _is_standard_uuid(self, value: str) -> bool:
78
+ try:
79
+ UUID(value)
80
+ return True
81
+ except (ValueError, AttributeError):
82
+ return False
83
+
84
+ def _is_notion_style_uuid(self, value: str) -> bool:
85
+ return bool(self.UUID_PATTERN.match(value))
86
+
87
+ async def _resolve_value(self, value: str, property_type: PropertyType) -> str:
88
+ if property_type == PropertyType.PEOPLE:
89
+ return await self._resolve_user_name_to_id(value)
90
+
91
+ if property_type == PropertyType.RELATION:
92
+ return await self._resolve_page_name_to_id(value)
93
+
94
+ return value
95
+
96
+ def _ensure_value_is_string(self, value: str | int | float | bool | list) -> None:
97
+ if not isinstance(value, str):
98
+ raise ValueError(f"Cannot resolve non-string value: {value}")
99
+
100
+ async def _resolve_user_name_to_id(self, name: str) -> str:
101
+ resolved = await self._user_resolver.resolve_name_to_id(name)
102
+
103
+ if not resolved:
104
+ raise ValueError(f"Could not resolve user name '{name}' to ID")
105
+
106
+ return resolved
107
+
108
+ async def _resolve_page_name_to_id(self, name: str) -> str:
109
+ resolved = await self._page_resolver.resolve_name_to_id(name)
110
+
111
+ if not resolved:
112
+ raise ValueError(f"Could not resolve page name '{name}' to ID")
113
+
114
+ return resolved