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
@@ -1,11 +1,11 @@
1
+ import traceback
1
2
  from io import BytesIO
2
3
  from pathlib import Path
3
- from typing import BinaryIO, Optional
4
+ from typing import BinaryIO
4
5
 
5
6
  import aiofiles
6
7
  import httpx
7
8
 
8
- from notionary.base_notion_client import BaseNotionClient
9
9
  from notionary.file_upload.models import (
10
10
  FileUploadCompleteRequest,
11
11
  FileUploadCreateRequest,
@@ -13,21 +13,22 @@ from notionary.file_upload.models import (
13
13
  FileUploadResponse,
14
14
  UploadMode,
15
15
  )
16
+ from notionary.http.client import NotionHttpClient
16
17
 
17
18
 
18
- class NotionFileUploadClient(BaseNotionClient):
19
+ class FileUploadHttpClient(NotionHttpClient):
19
20
  """
20
21
  Client for Notion file upload operations.
21
- Inherits base HTTP functionality from BaseNotionClient.
22
+ Inherits base HTTP functionality from NotionHttpClient.
22
23
  """
23
24
 
24
25
  async def create_file_upload(
25
26
  self,
26
27
  filename: str,
27
- content_type: Optional[str] = None,
28
- content_length: Optional[int] = None,
28
+ content_type: str | None = None,
29
+ content_length: int | None = None,
29
30
  mode: UploadMode = UploadMode.SINGLE_PART,
30
- ) -> Optional[FileUploadResponse]:
31
+ ) -> FileUploadResponse | None:
31
32
  """
32
33
  Create a new file upload.
33
34
 
@@ -61,8 +62,8 @@ class NotionFileUploadClient(BaseNotionClient):
61
62
  self,
62
63
  file_upload_id: str,
63
64
  file_content: BinaryIO,
64
- filename: Optional[str] = None,
65
- part_number: Optional[int] = None,
65
+ filename: str | None = None,
66
+ part_number: int | None = None,
66
67
  ) -> bool:
67
68
  """
68
69
  Send file content to Notion.
@@ -76,8 +77,6 @@ class NotionFileUploadClient(BaseNotionClient):
76
77
  Returns:
77
78
  True if successful, False otherwise
78
79
  """
79
- await self.ensure_initialized()
80
-
81
80
  if not self.client:
82
81
  self.logger.error("HTTP client not initialized")
83
82
  return False
@@ -110,9 +109,7 @@ class NotionFileUploadClient(BaseNotionClient):
110
109
  # Explicitly do NOT set Content-Type - let httpx handle multipart
111
110
  }
112
111
 
113
- self.logger.debug(
114
- "Sending file upload to %s with filename %s", url, filename
115
- )
112
+ self.logger.debug("Sending file upload to %s with filename %s", url, filename)
116
113
 
117
114
  # Use a temporary client for the multipart upload
118
115
  async with httpx.AsyncClient(timeout=self.timeout) as upload_client:
@@ -130,7 +127,7 @@ class NotionFileUploadClient(BaseNotionClient):
130
127
  except httpx.HTTPStatusError as e:
131
128
  try:
132
129
  error_text = e.response.text
133
- except:
130
+ except Exception:
134
131
  error_text = "Unable to read error response"
135
132
  error_msg = f"HTTP {e.response.status_code}: {error_text}"
136
133
  self.logger.error("Send file upload failed (%s): %s", url, error_msg)
@@ -142,14 +139,11 @@ class NotionFileUploadClient(BaseNotionClient):
142
139
 
143
140
  except Exception as e:
144
141
  self.logger.error("Unexpected error in send_file_upload: %s", str(e))
145
- import traceback
146
142
 
147
143
  self.logger.debug("Full traceback: %s", traceback.format_exc())
148
144
  return False
149
145
 
150
- async def complete_file_upload(
151
- self, file_upload_id: str
152
- ) -> Optional[FileUploadResponse]:
146
+ async def complete_file_upload(self, file_upload_id: str) -> FileUploadResponse | None:
153
147
  """
154
148
  Complete a multi-part file upload.
155
149
 
@@ -161,9 +155,7 @@ class NotionFileUploadClient(BaseNotionClient):
161
155
  """
162
156
  request_data = FileUploadCompleteRequest()
163
157
 
164
- response = await self.post(
165
- f"file_uploads/{file_upload_id}/complete", data=request_data.model_dump()
166
- )
158
+ response = await self.post(f"file_uploads/{file_upload_id}/complete", data=request_data.model_dump())
167
159
  if response is None:
168
160
  return None
169
161
 
@@ -173,9 +165,7 @@ class NotionFileUploadClient(BaseNotionClient):
173
165
  self.logger.error("Failed to validate complete file upload response: %s", e)
174
166
  return None
175
167
 
176
- async def retrieve_file_upload(
177
- self, file_upload_id: str
178
- ) -> Optional[FileUploadResponse]:
168
+ async def retrieve_file_upload(self, file_upload_id: str) -> FileUploadResponse | None:
179
169
  """
180
170
  Retrieve details of a file upload.
181
171
 
@@ -196,8 +186,8 @@ class NotionFileUploadClient(BaseNotionClient):
196
186
  return None
197
187
 
198
188
  async def list_file_uploads(
199
- self, page_size: int = 100, start_cursor: Optional[str] = None
200
- ) -> Optional[FileUploadListResponse]:
189
+ self, page_size: int = 100, start_cursor: str | None = None
190
+ ) -> FileUploadListResponse | None:
201
191
  """
202
192
  List file uploads for the current bot integration.
203
193
 
@@ -222,9 +212,7 @@ class NotionFileUploadClient(BaseNotionClient):
222
212
  self.logger.error("Failed to validate list file uploads response: %s", e)
223
213
  return None
224
214
 
225
- async def send_file_from_path(
226
- self, file_upload_id: str, file_path: Path, part_number: Optional[int] = None
227
- ) -> bool:
215
+ async def send_file_from_path(self, file_upload_id: str, file_path: Path, part_number: int | None = None) -> bool:
228
216
  """
229
217
  Convenience method to send file from file path.
230
218
 
@@ -1,6 +1,5 @@
1
1
  from enum import Enum
2
-
3
- from typing import Literal, Optional
2
+ from typing import Literal
4
3
 
5
4
  from pydantic import BaseModel
6
5
 
@@ -25,9 +24,9 @@ class FileUploadResponse(BaseModel):
25
24
  upload_url: str
26
25
  archived: bool
27
26
  status: str # "pending", "uploaded", "failed", etc.
28
- filename: Optional[str] = None
29
- content_type: Optional[str] = None
30
- content_length: Optional[int] = None
27
+ filename: str | None = None
28
+ content_type: str | None = None
29
+ content_length: int | None = None
31
30
  request_id: str
32
31
 
33
32
 
@@ -38,7 +37,7 @@ class FileUploadListResponse(BaseModel):
38
37
 
39
38
  object: Literal["list"]
40
39
  results: list[FileUploadResponse]
41
- next_cursor: Optional[str] = None
40
+ next_cursor: str | None = None
42
41
  has_more: bool
43
42
  type: Literal["file_upload"]
44
43
  file_upload: dict = {}
@@ -51,8 +50,8 @@ class FileUploadCreateRequest(BaseModel):
51
50
  """
52
51
 
53
52
  filename: str
54
- content_type: Optional[str] = None
55
- content_length: Optional[int] = None
53
+ content_type: str | None = None
54
+ content_length: int | None = None
56
55
  mode: UploadMode = UploadMode.SINGLE_PART
57
56
 
58
57
  def model_dump(self, **kwargs):
@@ -3,10 +3,9 @@ import mimetypes
3
3
  from datetime import datetime, timedelta
4
4
  from io import BytesIO
5
5
  from pathlib import Path
6
- from typing import Optional
7
6
 
8
7
  from notionary.file_upload.models import FileUploadResponse, UploadMode
9
- from notionary.util import LoggingMixin
8
+ from notionary.utils.mixins.logging import LoggingMixin
10
9
 
11
10
 
12
11
  class NotionFileUpload(LoggingMixin):
@@ -20,15 +19,13 @@ class NotionFileUpload(LoggingMixin):
20
19
  MULTI_PART_CHUNK_SIZE = 10 * 1024 * 1024 # 10MB per part
21
20
  MAX_FILENAME_BYTES = 900
22
21
 
23
- def __init__(self, token: Optional[str] = None):
22
+ def __init__(self, token: str | None = None):
24
23
  """Initialize the file upload service."""
25
- from notionary.file_upload import NotionFileUploadClient
24
+ from notionary.file_upload import FileUploadHttpClient
26
25
 
27
- self.client = NotionFileUploadClient(token=token)
26
+ self.client = FileUploadHttpClient(token=token)
28
27
 
29
- async def upload_file(
30
- self, file_path: Path, filename: Optional[str] = None
31
- ) -> Optional[FileUploadResponse]:
28
+ async def upload_file(self, file_path: Path, filename: str | None = None) -> FileUploadResponse | None:
32
29
  """
33
30
  Upload a file to Notion, automatically choosing single-part or multi-part based on size.
34
31
 
@@ -62,8 +59,8 @@ class NotionFileUpload(LoggingMixin):
62
59
  return await self._upload_large_file(file_path, filename, file_size)
63
60
 
64
61
  async def upload_from_bytes(
65
- self, file_content: bytes, filename: str, content_type: Optional[str] = None
66
- ) -> Optional[FileUploadResponse]:
62
+ self, file_content: bytes, filename: str, content_type: str | None = None
63
+ ) -> FileUploadResponse | None:
67
64
  """
68
65
  Upload file content from bytes.
69
66
 
@@ -92,15 +89,11 @@ class NotionFileUpload(LoggingMixin):
92
89
 
93
90
  # Choose upload method based on size
94
91
  if file_size <= self.SINGLE_PART_MAX_SIZE:
95
- return await self._upload_small_file_from_bytes(
96
- file_content, filename, content_type, file_size
97
- )
92
+ return await self._upload_small_file_from_bytes(file_content, filename, content_type, file_size)
98
93
  else:
99
- return await self._upload_large_file_from_bytes(
100
- file_content, filename, content_type, file_size
101
- )
94
+ return await self._upload_large_file_from_bytes(file_content, filename, content_type, file_size)
102
95
 
103
- async def get_upload_status(self, file_upload_id: str) -> Optional[str]:
96
+ async def get_upload_status(self, file_upload_id: str) -> str | None:
104
97
  """
105
98
  Get the current status of a file upload.
106
99
 
@@ -115,7 +108,7 @@ class NotionFileUpload(LoggingMixin):
115
108
 
116
109
  async def wait_for_upload_completion(
117
110
  self, file_upload_id: str, timeout_seconds: int = 300, poll_interval: int = 2
118
- ) -> Optional[FileUploadResponse]:
111
+ ) -> FileUploadResponse | None:
119
112
  """
120
113
  Wait for a file upload to complete.
121
114
 
@@ -134,9 +127,7 @@ class NotionFileUpload(LoggingMixin):
134
127
  upload_info = await self.client.retrieve_file_upload(file_upload_id)
135
128
 
136
129
  if not upload_info:
137
- self.logger.error(
138
- "Failed to retrieve upload info for %s", file_upload_id
139
- )
130
+ self.logger.error("Failed to retrieve upload info for %s", file_upload_id)
140
131
  return None
141
132
 
142
133
  if upload_info.status == "uploaded":
@@ -168,9 +159,7 @@ class NotionFileUpload(LoggingMixin):
168
159
  while remaining > 0:
169
160
  page_size = min(remaining, 100) # API max per request
170
161
 
171
- response = await self.client.list_file_uploads(
172
- page_size=page_size, start_cursor=start_cursor
173
- )
162
+ response = await self.client.list_file_uploads(page_size=page_size, start_cursor=start_cursor)
174
163
 
175
164
  if not response or not response.results:
176
165
  break
@@ -185,9 +174,7 @@ class NotionFileUpload(LoggingMixin):
185
174
 
186
175
  return uploads[:limit]
187
176
 
188
- async def _upload_small_file(
189
- self, file_path: Path, filename: str, file_size: int
190
- ) -> Optional[FileUploadResponse]:
177
+ async def _upload_small_file(self, file_path: Path, filename: str, file_size: int) -> FileUploadResponse | None:
191
178
  """Upload a small file using single-part upload."""
192
179
  content_type, _ = mimetypes.guess_type(str(file_path))
193
180
 
@@ -204,22 +191,16 @@ class NotionFileUpload(LoggingMixin):
204
191
  return None
205
192
 
206
193
  # Send file content
207
- success = await self.client.send_file_from_path(
208
- file_upload_id=file_upload.id, file_path=file_path
209
- )
194
+ success = await self.client.send_file_from_path(file_upload_id=file_upload.id, file_path=file_path)
210
195
 
211
196
  if not success:
212
197
  self.logger.error("Failed to send file content for %s", filename)
213
198
  return None
214
199
 
215
- self.logger.info(
216
- "Successfully uploaded file: %s (ID: %s)", filename, file_upload.id
217
- )
200
+ self.logger.info("Successfully uploaded file: %s (ID: %s)", filename, file_upload.id)
218
201
  return file_upload
219
202
 
220
- async def _upload_large_file(
221
- self, file_path: Path, filename: str, file_size: int
222
- ) -> Optional[FileUploadResponse]:
203
+ async def _upload_large_file(self, file_path: Path, filename: str, file_size: int) -> FileUploadResponse | None:
223
204
  """Upload a large file using multi-part upload."""
224
205
  content_type, _ = mimetypes.guess_type(str(file_path))
225
206
 
@@ -232,9 +213,7 @@ class NotionFileUpload(LoggingMixin):
232
213
  )
233
214
 
234
215
  if not file_upload:
235
- self.logger.error(
236
- "Failed to create multi-part file upload for %s", filename
237
- )
216
+ self.logger.error("Failed to create multi-part file upload for %s", filename)
238
217
  return None
239
218
 
240
219
  # Upload file in parts
@@ -251,18 +230,16 @@ class NotionFileUpload(LoggingMixin):
251
230
  self.logger.error("Failed to complete file upload for %s", filename)
252
231
  return None
253
232
 
254
- self.logger.info(
255
- "Successfully uploaded large file: %s (ID: %s)", filename, file_upload.id
256
- )
233
+ self.logger.info("Successfully uploaded large file: %s (ID: %s)", filename, file_upload.id)
257
234
  return completed_upload
258
235
 
259
236
  async def _upload_small_file_from_bytes(
260
237
  self,
261
238
  file_content: bytes,
262
239
  filename: str,
263
- content_type: Optional[str],
240
+ content_type: str | None,
264
241
  file_size: int,
265
- ) -> Optional[FileUploadResponse]:
242
+ ) -> FileUploadResponse | None:
266
243
  """Upload small file from bytes."""
267
244
  # Create file upload
268
245
  file_upload = await self.client.create_file_upload(
@@ -290,9 +267,9 @@ class NotionFileUpload(LoggingMixin):
290
267
  self,
291
268
  file_content: bytes,
292
269
  filename: str,
293
- content_type: Optional[str],
270
+ content_type: str | None,
294
271
  file_size: int,
295
- ) -> Optional[FileUploadResponse]:
272
+ ) -> FileUploadResponse | None:
296
273
  """Upload large file from bytes using multi-part."""
297
274
  # Create file upload
298
275
  file_upload = await self.client.create_file_upload(
@@ -314,14 +291,10 @@ class NotionFileUpload(LoggingMixin):
314
291
  # Complete the upload
315
292
  return await self.client.complete_file_upload(file_upload.id)
316
293
 
317
- async def _upload_file_parts(
318
- self, file_upload_id: str, file_path: Path, file_size: int
319
- ) -> bool:
294
+ async def _upload_file_parts(self, file_upload_id: str, file_path: Path, file_size: int) -> bool:
320
295
  """Upload file in parts for multi-part upload."""
321
296
  part_number = 1
322
- total_parts = (
323
- file_size + self.MULTI_PART_CHUNK_SIZE - 1
324
- ) // self.MULTI_PART_CHUNK_SIZE
297
+ total_parts = (file_size + self.MULTI_PART_CHUNK_SIZE - 1) // self.MULTI_PART_CHUNK_SIZE
325
298
 
326
299
  try:
327
300
  import aiofiles
@@ -340,9 +313,7 @@ class NotionFileUpload(LoggingMixin):
340
313
  )
341
314
 
342
315
  if not success:
343
- self.logger.error(
344
- "Failed to upload part %d/%d", part_number, total_parts
345
- )
316
+ self.logger.error("Failed to upload part %d/%d", part_number, total_parts)
346
317
  return False
347
318
 
348
319
  self.logger.debug("Uploaded part %d/%d", part_number, total_parts)
@@ -355,14 +326,10 @@ class NotionFileUpload(LoggingMixin):
355
326
  self.logger.error("Error uploading file parts: %s", e)
356
327
  return False
357
328
 
358
- async def _upload_bytes_parts(
359
- self, file_upload_id: str, file_content: bytes
360
- ) -> bool:
329
+ async def _upload_bytes_parts(self, file_upload_id: str, file_content: bytes) -> bool:
361
330
  """Upload bytes in parts for multi-part upload."""
362
331
  part_number = 1
363
- total_parts = (
364
- len(file_content) + self.MULTI_PART_CHUNK_SIZE - 1
365
- ) // self.MULTI_PART_CHUNK_SIZE
332
+ total_parts = (len(file_content) + self.MULTI_PART_CHUNK_SIZE - 1) // self.MULTI_PART_CHUNK_SIZE
366
333
 
367
334
  for i in range(0, len(file_content), self.MULTI_PART_CHUNK_SIZE):
368
335
  chunk = file_content[i : i + self.MULTI_PART_CHUNK_SIZE]
@@ -374,9 +341,7 @@ class NotionFileUpload(LoggingMixin):
374
341
  )
375
342
 
376
343
  if not success:
377
- self.logger.error(
378
- "Failed to upload part %d/%d", part_number, total_parts
379
- )
344
+ self.logger.error("Failed to upload part %d/%d", part_number, total_parts)
380
345
  return False
381
346
 
382
347
  self.logger.debug("Uploaded part %d/%d", part_number, total_parts)
@@ -0,0 +1,205 @@
1
+ import asyncio
2
+ import os
3
+ from typing import Any
4
+
5
+ import httpx
6
+ from dotenv import load_dotenv
7
+
8
+ from notionary.exceptions.api import (
9
+ NotionApiError,
10
+ NotionAuthenticationError,
11
+ NotionConnectionError,
12
+ NotionPermissionError,
13
+ NotionRateLimitError,
14
+ NotionResourceNotFoundError,
15
+ NotionServerError,
16
+ NotionValidationError,
17
+ )
18
+ from notionary.http.models import HttpMethod
19
+ from notionary.utils.mixins.logging import LoggingMixin
20
+
21
+ load_dotenv()
22
+
23
+
24
+ class NotionHttpClient(LoggingMixin):
25
+ BASE_URL = "https://api.notion.com/v1"
26
+ NOTION_VERSION = "2025-09-03"
27
+
28
+ def __init__(self, timeout: int = 30):
29
+ self.token = self._find_token()
30
+ if not self.token:
31
+ raise ValueError(
32
+ "No Notion API token found in environment variables. Please set one of these environment variables: "
33
+ "NOTION_SECRET, NOTION_INTEGRATION_KEY, or NOTION_TOKEN"
34
+ )
35
+
36
+ self.headers = {
37
+ "Authorization": f"Bearer {self.token}",
38
+ "Content-Type": "application/json",
39
+ "Notion-Version": self.NOTION_VERSION,
40
+ }
41
+
42
+ self.client: httpx.AsyncClient | None = None
43
+ self.timeout = timeout
44
+ self._is_initialized = False
45
+
46
+ def __del__(self):
47
+ """Auto-cleanup when client is destroyed."""
48
+ if not hasattr(self, "client") or not self.client:
49
+ return
50
+
51
+ try:
52
+ loop = asyncio.get_event_loop()
53
+ if not loop.is_running():
54
+ self.logger.warning("Event loop not running, could not auto-close NotionHttpClient")
55
+ return
56
+
57
+ loop.create_task(self.close())
58
+ self.logger.debug("Created cleanup task for NotionHttpClient")
59
+ except RuntimeError:
60
+ self.logger.warning("No event loop available for auto-closing NotionHttpClient")
61
+
62
+ async def __aenter__(self):
63
+ """Async context manager entry."""
64
+ await self._ensure_initialized()
65
+ return self
66
+
67
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
68
+ """Async context manager exit."""
69
+ await self.close()
70
+
71
+ async def close(self) -> None:
72
+ """
73
+ Closes the HTTP client and releases resources.
74
+ """
75
+ if not hasattr(self, "client") or not self.client:
76
+ return
77
+
78
+ await self.client.aclose()
79
+ self.client = None
80
+ self._is_initialized = False
81
+ self.logger.debug("NotionHttpClient closed")
82
+
83
+ async def get(self, endpoint: str, params: dict[str, Any] | None = None) -> dict[str, Any] | None:
84
+ return await self.make_request(HttpMethod.GET, endpoint, params=params)
85
+
86
+ async def post(self, endpoint: str, data: dict[str, Any] | None = None) -> dict[str, Any] | None:
87
+ return await self.make_request(HttpMethod.POST, endpoint, data)
88
+
89
+ async def patch(self, endpoint: str, data: dict[str, Any] | None = None) -> dict[str, Any] | None:
90
+ return await self.make_request(HttpMethod.PATCH, endpoint, data)
91
+
92
+ async def delete(self, endpoint: str) -> dict[str, Any] | None:
93
+ return await self.make_request(HttpMethod.DELETE, endpoint)
94
+
95
+ async def make_request(
96
+ self,
97
+ method: HttpMethod,
98
+ endpoint: str,
99
+ data: dict[str, Any] | None = None,
100
+ params: dict[str, Any] | None = None,
101
+ ) -> dict[str, Any] | None:
102
+ """
103
+ Executes an HTTP request and returns the data or None on error.
104
+
105
+ Args:
106
+ method: HTTP method to use
107
+ endpoint: API endpoint
108
+ data: Request body data (for POST/PATCH)
109
+ params: Query parameters (for GET requests)
110
+ """
111
+ await self._ensure_initialized()
112
+
113
+ url = f"{self.BASE_URL}/{endpoint.lstrip('/')}"
114
+ try:
115
+ self.logger.debug("Sending %s request to %s", method.value.upper(), url)
116
+
117
+ request_kwargs = {}
118
+
119
+ # Add query parameters for GET requests
120
+ if params:
121
+ request_kwargs["params"] = params
122
+
123
+ if method.value in [HttpMethod.POST.value, HttpMethod.PATCH.value] and data is not None:
124
+ request_kwargs["json"] = data
125
+
126
+ response: httpx.Response = await getattr(self.client, method.value)(url, **request_kwargs)
127
+
128
+ response.raise_for_status()
129
+ result_data = response.json()
130
+ self.logger.debug("Request successful: %s", url)
131
+ return result_data
132
+
133
+ except httpx.HTTPStatusError as e:
134
+ self._handle_http_status_error(e)
135
+ except httpx.RequestError as e:
136
+ raise NotionConnectionError(
137
+ f"Failed to connect to Notion API: {e!s}. Please check your internet connection and try again."
138
+ ) from e
139
+
140
+ def _handle_http_status_error(self, e: httpx.HTTPStatusError) -> None:
141
+ status_code = e.response.status_code
142
+ response_text = e.response.text
143
+
144
+ # Map HTTP status codes to specific business exceptions
145
+ if status_code == 401:
146
+ raise NotionAuthenticationError(
147
+ "Invalid or missing API key. Please check your Notion integration token.",
148
+ status_code=status_code,
149
+ response_text=response_text,
150
+ )
151
+ if status_code == 403:
152
+ raise NotionPermissionError(
153
+ "Insufficient permissions. Please check your integration settings at "
154
+ "https://www.notion.so/profile/integrations and ensure the integration "
155
+ "has access to the required pages/databases.",
156
+ status_code=status_code,
157
+ response_text=response_text,
158
+ )
159
+ if status_code == 404:
160
+ raise NotionResourceNotFoundError(
161
+ "The requested resource was not found. Please verify the page/database ID.",
162
+ status_code=status_code,
163
+ response_text=response_text,
164
+ )
165
+ if status_code == 400:
166
+ raise NotionValidationError(
167
+ f"Invalid request data. Please check your input parameters: {response_text}",
168
+ status_code=status_code,
169
+ response_text=response_text,
170
+ )
171
+ if status_code == 429:
172
+ raise NotionRateLimitError(
173
+ "Rate limit exceeded. Please wait before making more requests.",
174
+ status_code=status_code,
175
+ response_text=response_text,
176
+ )
177
+ if 500 <= status_code < 600:
178
+ raise NotionServerError(
179
+ "Notion API server error. Please try again later.",
180
+ status_code=status_code,
181
+ response_text=response_text,
182
+ )
183
+
184
+ raise NotionApiError(
185
+ f"API request failed with status {status_code}: {response_text}",
186
+ status_code=status_code,
187
+ response_text=response_text,
188
+ )
189
+
190
+ def _find_token(self) -> str | None:
191
+ token = next(
192
+ (os.getenv(var) for var in ("NOTION_SECRET", "NOTION_INTEGRATION_KEY", "NOTION_TOKEN") if os.getenv(var)),
193
+ None,
194
+ )
195
+ if token:
196
+ self.logger.debug("Found token in environment variable.")
197
+ return token
198
+ self.logger.warning("No Notion API token found in environment variables")
199
+ return None
200
+
201
+ async def _ensure_initialized(self) -> None:
202
+ if not self._is_initialized or not self.client:
203
+ self.client = httpx.AsyncClient(headers=self.headers, timeout=self.timeout)
204
+ self._is_initialized = True
205
+ self.logger.debug("NotionHttpClient initialized")
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from dataclasses import dataclass, field
5
+ from enum import StrEnum
6
+ from typing import Any
7
+
8
+
9
+ class HttpMethod(StrEnum):
10
+ GET = "get"
11
+ POST = "post"
12
+ PATCH = "patch"
13
+ DELETE = "delete"
14
+
15
+
16
+ @dataclass
17
+ class HttpRequest:
18
+ method: HttpMethod
19
+ endpoint: str
20
+ data: dict[str, Any] | None = None
21
+ params: dict[str, Any] | None = None
22
+ timestamp: float = field(default_factory=time.time)
23
+ cached_response: HttpResponse | None = None
24
+
25
+ @property
26
+ def cache_key(self) -> str:
27
+ key_parts = [self.method.value, self.endpoint]
28
+
29
+ if self.params:
30
+ sorted_params = sorted(self.params.items())
31
+ key_parts.extend(f"{k}={v}" for k, v in sorted_params)
32
+
33
+ return "|".join(key_parts)
34
+
35
+ def __repr__(self) -> str:
36
+ return f"HttpRequest(method={self.method.value}, endpoint={self.endpoint})"
37
+
38
+
39
+ @dataclass
40
+ class HttpResponse:
41
+ data: dict[str, Any] | None
42
+ status_code: int = 200
43
+ headers: dict[str, str] = field(default_factory=dict)
44
+ timestamp: float = field(default_factory=time.time)
45
+ from_cache: bool = False
46
+ error: Exception | None = None
47
+
48
+ def __repr__(self) -> str:
49
+ return f"HttpResponse(status_code={self.status_code}, from_cache={self.from_cache})"
@@ -0,0 +1 @@
1
+ # PageBlockClient