magic_hour 0.9.5__py3-none-any.whl → 0.44.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 (264) hide show
  1. magic_hour/README.md +34 -0
  2. magic_hour/__init__.py +1 -1
  3. magic_hour/client.py +8 -17
  4. magic_hour/environment.py +13 -1
  5. magic_hour/helpers/__init__.py +4 -0
  6. magic_hour/helpers/download.py +77 -0
  7. magic_hour/helpers/logger.py +8 -0
  8. magic_hour/resources/v1/README.md +32 -0
  9. magic_hour/resources/v1/ai_clothes_changer/README.md +94 -5
  10. magic_hour/resources/v1/ai_clothes_changer/client.py +161 -16
  11. magic_hour/resources/v1/ai_face_editor/README.md +195 -0
  12. magic_hour/resources/v1/ai_face_editor/__init__.py +4 -0
  13. magic_hour/resources/v1/ai_face_editor/client.py +324 -0
  14. magic_hour/resources/v1/ai_gif_generator/README.md +116 -0
  15. magic_hour/resources/v1/ai_gif_generator/__init__.py +4 -0
  16. magic_hour/resources/v1/ai_gif_generator/client.py +257 -0
  17. magic_hour/resources/v1/ai_headshot_generator/README.md +81 -3
  18. magic_hour/resources/v1/ai_headshot_generator/client.py +167 -18
  19. magic_hour/resources/v1/ai_image_editor/README.md +125 -0
  20. magic_hour/resources/v1/ai_image_editor/__init__.py +4 -0
  21. magic_hour/resources/v1/ai_image_editor/client.py +290 -0
  22. magic_hour/resources/v1/ai_image_generator/README.md +99 -5
  23. magic_hour/resources/v1/ai_image_generator/client.py +170 -24
  24. magic_hour/resources/v1/ai_image_upscaler/README.md +89 -3
  25. magic_hour/resources/v1/ai_image_upscaler/client.py +173 -20
  26. magic_hour/resources/v1/ai_meme_generator/README.md +129 -0
  27. magic_hour/resources/v1/ai_meme_generator/__init__.py +4 -0
  28. magic_hour/resources/v1/ai_meme_generator/client.py +253 -0
  29. magic_hour/resources/v1/ai_photo_editor/README.md +119 -4
  30. magic_hour/resources/v1/ai_photo_editor/client.py +199 -18
  31. magic_hour/resources/v1/ai_qr_code_generator/README.md +84 -3
  32. magic_hour/resources/v1/ai_qr_code_generator/client.py +140 -18
  33. magic_hour/resources/v1/ai_talking_photo/README.md +137 -0
  34. magic_hour/resources/v1/ai_talking_photo/__init__.py +4 -0
  35. magic_hour/resources/v1/ai_talking_photo/client.py +326 -0
  36. magic_hour/resources/v1/ai_voice_cloner/README.md +62 -0
  37. magic_hour/resources/v1/ai_voice_cloner/__init__.py +4 -0
  38. magic_hour/resources/v1/ai_voice_cloner/client.py +272 -0
  39. magic_hour/resources/v1/ai_voice_generator/README.md +112 -0
  40. magic_hour/resources/v1/ai_voice_generator/__init__.py +4 -0
  41. magic_hour/resources/v1/ai_voice_generator/client.py +241 -0
  42. magic_hour/resources/v1/animation/README.md +128 -6
  43. magic_hour/resources/v1/animation/client.py +247 -22
  44. magic_hour/resources/v1/audio_projects/README.md +135 -0
  45. magic_hour/resources/v1/audio_projects/__init__.py +12 -0
  46. magic_hour/resources/v1/audio_projects/client.py +310 -0
  47. magic_hour/resources/v1/audio_projects/client_test.py +520 -0
  48. magic_hour/resources/v1/auto_subtitle_generator/README.md +128 -0
  49. magic_hour/resources/v1/auto_subtitle_generator/__init__.py +4 -0
  50. magic_hour/resources/v1/auto_subtitle_generator/client.py +346 -0
  51. magic_hour/resources/v1/client.py +75 -1
  52. magic_hour/resources/v1/face_detection/README.md +157 -0
  53. magic_hour/resources/v1/face_detection/__init__.py +12 -0
  54. magic_hour/resources/v1/face_detection/client.py +380 -0
  55. magic_hour/resources/v1/face_swap/README.md +137 -9
  56. magic_hour/resources/v1/face_swap/client.py +329 -38
  57. magic_hour/resources/v1/face_swap_photo/README.md +118 -3
  58. magic_hour/resources/v1/face_swap_photo/client.py +199 -14
  59. magic_hour/resources/v1/files/README.md +39 -0
  60. magic_hour/resources/v1/files/client.py +351 -1
  61. magic_hour/resources/v1/files/client_test.py +414 -0
  62. magic_hour/resources/v1/files/upload_urls/README.md +38 -17
  63. magic_hour/resources/v1/files/upload_urls/client.py +38 -34
  64. magic_hour/resources/v1/image_background_remover/README.md +96 -5
  65. magic_hour/resources/v1/image_background_remover/client.py +151 -16
  66. magic_hour/resources/v1/image_projects/README.md +82 -10
  67. magic_hour/resources/v1/image_projects/__init__.py +10 -2
  68. magic_hour/resources/v1/image_projects/client.py +154 -16
  69. magic_hour/resources/v1/image_projects/client_test.py +527 -0
  70. magic_hour/resources/v1/image_to_video/README.md +96 -11
  71. magic_hour/resources/v1/image_to_video/client.py +282 -38
  72. magic_hour/resources/v1/lip_sync/README.md +112 -9
  73. magic_hour/resources/v1/lip_sync/client.py +288 -34
  74. magic_hour/resources/v1/photo_colorizer/README.md +107 -0
  75. magic_hour/resources/v1/photo_colorizer/__init__.py +4 -0
  76. magic_hour/resources/v1/photo_colorizer/client.py +248 -0
  77. magic_hour/resources/v1/text_to_video/README.md +96 -7
  78. magic_hour/resources/v1/text_to_video/client.py +204 -18
  79. magic_hour/resources/v1/video_projects/README.md +81 -9
  80. magic_hour/resources/v1/video_projects/__init__.py +10 -2
  81. magic_hour/resources/v1/video_projects/client.py +151 -14
  82. magic_hour/resources/v1/video_projects/client_test.py +527 -0
  83. magic_hour/resources/v1/video_to_video/README.md +119 -15
  84. magic_hour/resources/v1/video_to_video/client.py +299 -46
  85. magic_hour/types/models/__init__.py +92 -56
  86. magic_hour/types/models/v1_ai_clothes_changer_create_response.py +33 -0
  87. magic_hour/types/models/v1_ai_face_editor_create_response.py +33 -0
  88. magic_hour/types/models/v1_ai_gif_generator_create_response.py +33 -0
  89. magic_hour/types/models/v1_ai_headshot_generator_create_response.py +33 -0
  90. magic_hour/types/models/v1_ai_image_editor_create_response.py +33 -0
  91. magic_hour/types/models/v1_ai_image_generator_create_response.py +33 -0
  92. magic_hour/types/models/v1_ai_image_upscaler_create_response.py +33 -0
  93. magic_hour/types/models/v1_ai_meme_generator_create_response.py +33 -0
  94. magic_hour/types/models/v1_ai_photo_editor_create_response.py +33 -0
  95. magic_hour/types/models/v1_ai_qr_code_generator_create_response.py +33 -0
  96. magic_hour/types/models/v1_ai_talking_photo_create_response.py +35 -0
  97. magic_hour/types/models/v1_ai_voice_cloner_create_response.py +27 -0
  98. magic_hour/types/models/v1_ai_voice_generator_create_response.py +27 -0
  99. magic_hour/types/models/v1_animation_create_response.py +35 -0
  100. magic_hour/types/models/v1_audio_projects_get_response.py +72 -0
  101. magic_hour/types/models/v1_audio_projects_get_response_downloads_item.py +19 -0
  102. magic_hour/types/models/{get_v1_image_projects_id_response_error.py → v1_audio_projects_get_response_error.py} +2 -2
  103. magic_hour/types/models/v1_auto_subtitle_generator_create_response.py +35 -0
  104. magic_hour/types/models/v1_face_detection_create_response.py +25 -0
  105. magic_hour/types/models/v1_face_detection_get_response.py +45 -0
  106. magic_hour/types/models/v1_face_detection_get_response_faces_item.py +25 -0
  107. magic_hour/types/models/v1_face_swap_create_response.py +35 -0
  108. magic_hour/types/models/v1_face_swap_photo_create_response.py +33 -0
  109. magic_hour/types/models/v1_files_upload_urls_create_response.py +24 -0
  110. magic_hour/types/models/{post_v1_files_upload_urls_response_items_item.py → v1_files_upload_urls_create_response_items_item.py} +2 -2
  111. magic_hour/types/models/v1_image_background_remover_create_response.py +33 -0
  112. magic_hour/types/models/{get_v1_image_projects_id_response.py → v1_image_projects_get_response.py} +20 -18
  113. magic_hour/types/models/{get_v1_video_projects_id_response_downloads_item.py → v1_image_projects_get_response_downloads_item.py} +1 -1
  114. magic_hour/types/models/{get_v1_video_projects_id_response_error.py → v1_image_projects_get_response_error.py} +2 -2
  115. magic_hour/types/models/v1_image_to_video_create_response.py +35 -0
  116. magic_hour/types/models/v1_lip_sync_create_response.py +35 -0
  117. magic_hour/types/models/v1_photo_colorizer_create_response.py +33 -0
  118. magic_hour/types/models/v1_text_to_video_create_response.py +35 -0
  119. magic_hour/types/models/{get_v1_video_projects_id_response.py → v1_video_projects_get_response.py} +26 -23
  120. magic_hour/types/models/{get_v1_video_projects_id_response_download.py → v1_video_projects_get_response_download.py} +1 -1
  121. magic_hour/types/models/{get_v1_image_projects_id_response_downloads_item.py → v1_video_projects_get_response_downloads_item.py} +1 -1
  122. magic_hour/types/models/v1_video_projects_get_response_error.py +25 -0
  123. magic_hour/types/models/v1_video_to_video_create_response.py +35 -0
  124. magic_hour/types/params/__init__.py +422 -176
  125. magic_hour/types/params/v1_ai_clothes_changer_create_body.py +40 -0
  126. magic_hour/types/params/v1_ai_clothes_changer_create_body_assets.py +58 -0
  127. magic_hour/types/params/v1_ai_clothes_changer_generate_body_assets.py +33 -0
  128. magic_hour/types/params/v1_ai_face_editor_create_body.py +52 -0
  129. magic_hour/types/params/v1_ai_face_editor_create_body_assets.py +33 -0
  130. magic_hour/types/params/v1_ai_face_editor_create_body_style.py +137 -0
  131. magic_hour/types/params/v1_ai_face_editor_generate_body_assets.py +17 -0
  132. magic_hour/types/params/v1_ai_gif_generator_create_body.py +47 -0
  133. magic_hour/types/params/{post_v1_ai_image_generator_body_style.py → v1_ai_gif_generator_create_body_style.py} +5 -5
  134. magic_hour/types/params/v1_ai_headshot_generator_create_body.py +49 -0
  135. magic_hour/types/params/v1_ai_headshot_generator_create_body_assets.py +33 -0
  136. magic_hour/types/params/v1_ai_headshot_generator_create_body_style.py +27 -0
  137. magic_hour/types/params/v1_ai_headshot_generator_generate_body_assets.py +17 -0
  138. magic_hour/types/params/v1_ai_image_editor_create_body.py +49 -0
  139. magic_hour/types/params/v1_ai_image_editor_create_body_assets.py +47 -0
  140. magic_hour/types/params/v1_ai_image_editor_create_body_style.py +41 -0
  141. magic_hour/types/params/v1_ai_image_editor_generate_body_assets.py +28 -0
  142. magic_hour/types/params/{post_v1_ai_image_generator_body.py → v1_ai_image_generator_create_body.py} +17 -11
  143. magic_hour/types/params/v1_ai_image_generator_create_body_style.py +127 -0
  144. magic_hour/types/params/v1_ai_image_upscaler_create_body.py +59 -0
  145. magic_hour/types/params/v1_ai_image_upscaler_create_body_assets.py +33 -0
  146. magic_hour/types/params/{post_v1_ai_image_upscaler_body_style.py → v1_ai_image_upscaler_create_body_style.py} +4 -4
  147. magic_hour/types/params/v1_ai_image_upscaler_generate_body_assets.py +17 -0
  148. magic_hour/types/params/v1_ai_meme_generator_create_body.py +37 -0
  149. magic_hour/types/params/v1_ai_meme_generator_create_body_style.py +73 -0
  150. magic_hour/types/params/{post_v1_ai_photo_editor_body.py → v1_ai_photo_editor_create_body.py} +15 -15
  151. magic_hour/types/params/v1_ai_photo_editor_create_body_assets.py +33 -0
  152. magic_hour/types/params/{post_v1_ai_photo_editor_body_style.py → v1_ai_photo_editor_create_body_style.py} +20 -4
  153. magic_hour/types/params/v1_ai_photo_editor_generate_body_assets.py +17 -0
  154. magic_hour/types/params/v1_ai_qr_code_generator_create_body.py +45 -0
  155. magic_hour/types/params/{post_v1_ai_qr_code_generator_body_style.py → v1_ai_qr_code_generator_create_body_style.py} +4 -4
  156. magic_hour/types/params/v1_ai_talking_photo_create_body.py +68 -0
  157. magic_hour/types/params/v1_ai_talking_photo_create_body_assets.py +46 -0
  158. magic_hour/types/params/v1_ai_talking_photo_create_body_style.py +44 -0
  159. magic_hour/types/params/v1_ai_talking_photo_generate_body_assets.py +26 -0
  160. magic_hour/types/params/v1_ai_voice_cloner_create_body.py +49 -0
  161. magic_hour/types/params/v1_ai_voice_cloner_create_body_assets.py +33 -0
  162. magic_hour/types/params/v1_ai_voice_cloner_create_body_style.py +28 -0
  163. magic_hour/types/params/v1_ai_voice_cloner_generate_body_assets.py +28 -0
  164. magic_hour/types/params/v1_ai_voice_generator_create_body.py +40 -0
  165. magic_hour/types/params/v1_ai_voice_generator_create_body_style.py +440 -0
  166. magic_hour/types/params/{post_v1_animation_body.py → v1_animation_create_body.py} +16 -16
  167. magic_hour/types/params/{post_v1_animation_body_assets.py → v1_animation_create_body_assets.py} +15 -5
  168. magic_hour/types/params/{post_v1_animation_body_style.py → v1_animation_create_body_style.py} +13 -10
  169. magic_hour/types/params/v1_animation_generate_body_assets.py +39 -0
  170. magic_hour/types/params/v1_auto_subtitle_generator_create_body.py +78 -0
  171. magic_hour/types/params/v1_auto_subtitle_generator_create_body_assets.py +33 -0
  172. magic_hour/types/params/v1_auto_subtitle_generator_create_body_style.py +56 -0
  173. magic_hour/types/params/v1_auto_subtitle_generator_create_body_style_custom_config.py +86 -0
  174. magic_hour/types/params/v1_auto_subtitle_generator_generate_body_assets.py +17 -0
  175. magic_hour/types/params/v1_face_detection_create_body.py +44 -0
  176. magic_hour/types/params/v1_face_detection_create_body_assets.py +33 -0
  177. magic_hour/types/params/v1_face_detection_generate_body_assets.py +17 -0
  178. magic_hour/types/params/v1_face_swap_create_body.py +92 -0
  179. magic_hour/types/params/v1_face_swap_create_body_assets.py +91 -0
  180. magic_hour/types/params/v1_face_swap_create_body_assets_face_mappings_item.py +44 -0
  181. magic_hour/types/params/v1_face_swap_create_body_style.py +33 -0
  182. magic_hour/types/params/v1_face_swap_generate_body_assets.py +56 -0
  183. magic_hour/types/params/v1_face_swap_generate_body_assets_face_mappings_item.py +25 -0
  184. magic_hour/types/params/v1_face_swap_photo_create_body.py +40 -0
  185. magic_hour/types/params/v1_face_swap_photo_create_body_assets.py +76 -0
  186. magic_hour/types/params/v1_face_swap_photo_create_body_assets_face_mappings_item.py +44 -0
  187. magic_hour/types/params/v1_face_swap_photo_generate_body_assets.py +47 -0
  188. magic_hour/types/params/v1_face_swap_photo_generate_body_assets_face_mappings_item.py +25 -0
  189. magic_hour/types/params/v1_files_upload_urls_create_body.py +36 -0
  190. magic_hour/types/params/v1_files_upload_urls_create_body_items_item.py +38 -0
  191. magic_hour/types/params/v1_image_background_remover_create_body.py +40 -0
  192. magic_hour/types/params/v1_image_background_remover_create_body_assets.py +49 -0
  193. magic_hour/types/params/v1_image_background_remover_generate_body_assets.py +27 -0
  194. magic_hour/types/params/v1_image_to_video_create_body.py +101 -0
  195. magic_hour/types/params/v1_image_to_video_create_body_assets.py +33 -0
  196. magic_hour/types/params/v1_image_to_video_create_body_style.py +53 -0
  197. magic_hour/types/params/v1_image_to_video_generate_body_assets.py +17 -0
  198. magic_hour/types/params/v1_lip_sync_create_body.py +100 -0
  199. magic_hour/types/params/{post_v1_lip_sync_body_assets.py → v1_lip_sync_create_body_assets.py} +15 -5
  200. magic_hour/types/params/v1_lip_sync_create_body_style.py +37 -0
  201. magic_hour/types/params/v1_lip_sync_generate_body_assets.py +36 -0
  202. magic_hour/types/params/v1_photo_colorizer_create_body.py +40 -0
  203. magic_hour/types/params/v1_photo_colorizer_create_body_assets.py +33 -0
  204. magic_hour/types/params/v1_photo_colorizer_generate_body_assets.py +17 -0
  205. magic_hour/types/params/v1_text_to_video_create_body.py +78 -0
  206. magic_hour/types/params/v1_text_to_video_create_body_style.py +43 -0
  207. magic_hour/types/params/v1_video_to_video_create_body.py +101 -0
  208. magic_hour/types/params/{post_v1_video_to_video_body_assets.py → v1_video_to_video_create_body_assets.py} +9 -4
  209. magic_hour/types/params/{post_v1_video_to_video_body_style.py → v1_video_to_video_create_body_style.py} +68 -26
  210. magic_hour/types/params/v1_video_to_video_generate_body_assets.py +27 -0
  211. magic_hour-0.44.0.dist-info/METADATA +328 -0
  212. magic_hour-0.44.0.dist-info/RECORD +231 -0
  213. magic_hour/core/__init__.py +0 -52
  214. magic_hour/core/api_error.py +0 -56
  215. magic_hour/core/auth.py +0 -314
  216. magic_hour/core/base_client.py +0 -618
  217. magic_hour/core/binary_response.py +0 -23
  218. magic_hour/core/query.py +0 -106
  219. magic_hour/core/request.py +0 -156
  220. magic_hour/core/response.py +0 -293
  221. magic_hour/core/type_utils.py +0 -28
  222. magic_hour/core/utils.py +0 -55
  223. magic_hour/types/models/post_v1_ai_clothes_changer_response.py +0 -25
  224. magic_hour/types/models/post_v1_ai_headshot_generator_response.py +0 -25
  225. magic_hour/types/models/post_v1_ai_image_generator_response.py +0 -25
  226. magic_hour/types/models/post_v1_ai_image_upscaler_response.py +0 -25
  227. magic_hour/types/models/post_v1_ai_photo_editor_response.py +0 -25
  228. magic_hour/types/models/post_v1_ai_qr_code_generator_response.py +0 -25
  229. magic_hour/types/models/post_v1_animation_response.py +0 -25
  230. magic_hour/types/models/post_v1_face_swap_photo_response.py +0 -25
  231. magic_hour/types/models/post_v1_face_swap_response.py +0 -25
  232. magic_hour/types/models/post_v1_files_upload_urls_response.py +0 -21
  233. magic_hour/types/models/post_v1_image_background_remover_response.py +0 -25
  234. magic_hour/types/models/post_v1_image_to_video_response.py +0 -25
  235. magic_hour/types/models/post_v1_lip_sync_response.py +0 -25
  236. magic_hour/types/models/post_v1_text_to_video_response.py +0 -25
  237. magic_hour/types/models/post_v1_video_to_video_response.py +0 -25
  238. magic_hour/types/params/post_v1_ai_clothes_changer_body.py +0 -40
  239. magic_hour/types/params/post_v1_ai_clothes_changer_body_assets.py +0 -45
  240. magic_hour/types/params/post_v1_ai_headshot_generator_body.py +0 -40
  241. magic_hour/types/params/post_v1_ai_headshot_generator_body_assets.py +0 -28
  242. magic_hour/types/params/post_v1_ai_image_upscaler_body.py +0 -57
  243. magic_hour/types/params/post_v1_ai_image_upscaler_body_assets.py +0 -28
  244. magic_hour/types/params/post_v1_ai_photo_editor_body_assets.py +0 -28
  245. magic_hour/types/params/post_v1_ai_qr_code_generator_body.py +0 -45
  246. magic_hour/types/params/post_v1_face_swap_body.py +0 -72
  247. magic_hour/types/params/post_v1_face_swap_body_assets.py +0 -52
  248. magic_hour/types/params/post_v1_face_swap_photo_body.py +0 -40
  249. magic_hour/types/params/post_v1_face_swap_photo_body_assets.py +0 -36
  250. magic_hour/types/params/post_v1_files_upload_urls_body.py +0 -31
  251. magic_hour/types/params/post_v1_files_upload_urls_body_items_item.py +0 -38
  252. magic_hour/types/params/post_v1_image_background_remover_body.py +0 -40
  253. magic_hour/types/params/post_v1_image_background_remover_body_assets.py +0 -28
  254. magic_hour/types/params/post_v1_image_to_video_body.py +0 -73
  255. magic_hour/types/params/post_v1_image_to_video_body_assets.py +0 -28
  256. magic_hour/types/params/post_v1_image_to_video_body_style.py +0 -37
  257. magic_hour/types/params/post_v1_lip_sync_body.py +0 -80
  258. magic_hour/types/params/post_v1_text_to_video_body.py +0 -57
  259. magic_hour/types/params/post_v1_text_to_video_body_style.py +0 -28
  260. magic_hour/types/params/post_v1_video_to_video_body.py +0 -93
  261. magic_hour-0.9.5.dist-info/METADATA +0 -133
  262. magic_hour-0.9.5.dist-info/RECORD +0 -132
  263. {magic_hour-0.9.5.dist-info → magic_hour-0.44.0.dist-info}/LICENSE +0 -0
  264. {magic_hour-0.9.5.dist-info → magic_hour-0.44.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,527 @@
1
+ import datetime
2
+ import pytest
3
+ import httpx
4
+ from pathlib import Path
5
+ from typing import Any, Generator, Literal, Union, List
6
+ from unittest.mock import Mock, AsyncMock
7
+
8
+ from magic_hour.types import models
9
+ from magic_hour.resources.v1.video_projects.client import (
10
+ VideoProjectsClient,
11
+ AsyncVideoProjectsClient,
12
+ )
13
+
14
+
15
+ class DummyResponse(models.V1VideoProjectsGetResponse):
16
+ """Helper response with defaults"""
17
+
18
+ def __init__(
19
+ self,
20
+ *,
21
+ status: Literal[
22
+ "complete", "queued", "rendering", "error", "canceled"
23
+ ] = "complete",
24
+ download_url: Union[str, None] = None,
25
+ error: Union[str, None] = None,
26
+ ):
27
+ # Create error object if error string is provided
28
+ error_obj = None
29
+ if error:
30
+ error_obj = models.V1VideoProjectsGetResponseError(
31
+ code="TEST_ERROR", message=error
32
+ )
33
+
34
+ super().__init__(
35
+ id="test-id",
36
+ created_at=datetime.datetime.now().isoformat(),
37
+ credits_charged=0,
38
+ download=None,
39
+ downloads=[
40
+ models.V1VideoProjectsGetResponseDownloadsItem(
41
+ url=download_url, expires_at="2024-01-01T00:00:00Z"
42
+ )
43
+ ]
44
+ if download_url
45
+ else [],
46
+ enabled=True,
47
+ end_seconds=10.0,
48
+ error=error_obj,
49
+ fps=30.0,
50
+ height=1080,
51
+ name="test-name",
52
+ start_seconds=0.0,
53
+ status=status,
54
+ total_frame_cost=0,
55
+ type="test-type",
56
+ width=1920,
57
+ )
58
+
59
+
60
+ @pytest.fixture
61
+ def mock_base_client() -> Generator[Mock, None, None]:
62
+ yield Mock()
63
+
64
+
65
+ @pytest.fixture
66
+ def mock_async_base_client() -> Generator[AsyncMock, None, None]:
67
+ yield AsyncMock()
68
+
69
+
70
+ def test_delete_calls_base_client(mock_base_client: Mock) -> None:
71
+ client = VideoProjectsClient(base_client=mock_base_client)
72
+ client.delete(id="123")
73
+
74
+ mock_base_client.request.assert_called_once()
75
+ call = mock_base_client.request.call_args[1]
76
+ assert call["method"] == "DELETE"
77
+ assert "/v1/video-projects/123" in call["path"]
78
+
79
+
80
+ def test_get_calls_base_client(mock_base_client: Mock) -> None:
81
+ client = VideoProjectsClient(base_client=mock_base_client)
82
+ mock_base_client.request.return_value = DummyResponse()
83
+
84
+ resp = client.get(id="abc")
85
+
86
+ mock_base_client.request.assert_called_once()
87
+ assert isinstance(resp, models.V1VideoProjectsGetResponse)
88
+ assert resp.id == "test-id"
89
+
90
+
91
+ def test_check_result_no_wait_no_download(mock_base_client: Mock) -> None:
92
+ client = VideoProjectsClient(base_client=mock_base_client)
93
+ mock_base_client.request.return_value = DummyResponse(status="queued")
94
+
95
+ resp = client.check_result(
96
+ id="xyz",
97
+ wait_for_completion=False,
98
+ download_outputs=False,
99
+ )
100
+
101
+ assert resp.downloaded_paths is None
102
+
103
+
104
+ def test_check_result_wait_until_complete(
105
+ monkeypatch: Any, mock_base_client: Mock
106
+ ) -> None:
107
+ client = VideoProjectsClient(base_client=mock_base_client)
108
+
109
+ # First calls return queued, then complete
110
+ mock_base_client.request.side_effect = [
111
+ DummyResponse(status="queued"),
112
+ DummyResponse(status="queued"),
113
+ DummyResponse(status="complete"),
114
+ ]
115
+
116
+ monkeypatch.setattr("time.sleep", lambda _: None) # type: ignore
117
+
118
+ resp = client.check_result(
119
+ id="xyz", wait_for_completion=True, download_outputs=False
120
+ )
121
+
122
+ assert resp.status == "complete"
123
+ assert resp.downloaded_paths is None
124
+
125
+
126
+ def test_check_result_download_outputs(
127
+ tmp_path: Path, mock_base_client: Mock, monkeypatch: Any
128
+ ) -> None:
129
+ client = VideoProjectsClient(base_client=mock_base_client)
130
+
131
+ file_url = "https://example.com/file.mp4"
132
+ mock_base_client.request.return_value = DummyResponse(
133
+ status="complete",
134
+ download_url=file_url,
135
+ )
136
+
137
+ # Create a mock response for httpx
138
+ mock_request = httpx.Request("GET", "https://example.com/file.mp4")
139
+ mock_response = httpx.Response(200, content=b"fake mp4", request=mock_request)
140
+
141
+ # Mock the httpx.Client class
142
+ class MockClient:
143
+ def __init__(self):
144
+ pass
145
+
146
+ def __enter__(self) -> "MockClient":
147
+ return self
148
+
149
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
150
+ pass
151
+
152
+ def get(self, url: str) -> httpx.Response:
153
+ return mock_response
154
+
155
+ monkeypatch.setattr(httpx, "Client", MockClient)
156
+
157
+ resp = client.check_result(
158
+ id="xyz",
159
+ wait_for_completion=True,
160
+ download_outputs=True,
161
+ download_directory=str(tmp_path),
162
+ )
163
+
164
+ assert resp.status == "complete"
165
+ assert resp.downloaded_paths
166
+ saved_file = Path(resp.downloaded_paths[0])
167
+ assert saved_file.exists()
168
+ assert saved_file.read_bytes() == b"fake mp4"
169
+
170
+
171
+ def test_check_result_error_status(mock_base_client: Mock) -> None:
172
+ client = VideoProjectsClient(base_client=mock_base_client)
173
+ mock_base_client.request.return_value = DummyResponse(status="error", error="Boom!")
174
+
175
+ resp = client.check_result(
176
+ id="err", wait_for_completion=True, download_outputs=False
177
+ )
178
+ assert resp.status == "error"
179
+ assert resp.error is not None
180
+ assert resp.error.message == "Boom!"
181
+ assert resp.downloaded_paths is None
182
+
183
+
184
+ def test_check_result_canceled_status(mock_base_client: Mock) -> None:
185
+ client = VideoProjectsClient(base_client=mock_base_client)
186
+ mock_base_client.request.return_value = DummyResponse(status="canceled")
187
+
188
+ resp = client.check_result(
189
+ id="cancel", wait_for_completion=True, download_outputs=False
190
+ )
191
+ assert resp.status == "canceled"
192
+ assert resp.downloaded_paths is None
193
+
194
+
195
+ def test_check_result_poll_interval_default(
196
+ mock_base_client: Mock, monkeypatch: Any
197
+ ) -> None:
198
+ client = VideoProjectsClient(base_client=mock_base_client)
199
+
200
+ # First calls return queued, then complete
201
+ mock_base_client.request.side_effect = [
202
+ DummyResponse(status="queued"),
203
+ DummyResponse(status="complete"),
204
+ ]
205
+
206
+ # Mock time.sleep to track calls
207
+ sleep_calls: List[float] = []
208
+
209
+ def mock_sleep(seconds: float) -> None:
210
+ sleep_calls.append(seconds)
211
+
212
+ monkeypatch.setattr("time.sleep", mock_sleep)
213
+
214
+ resp = client.check_result(
215
+ id="xyz", wait_for_completion=True, download_outputs=False
216
+ )
217
+
218
+ assert resp.status == "complete"
219
+ # Should have slept once with default interval (0.5)
220
+ assert len(sleep_calls) == 1
221
+ assert sleep_calls[0] == 0.5
222
+
223
+
224
+ def test_check_result_poll_interval_custom(
225
+ mock_base_client: Mock, monkeypatch: Any
226
+ ) -> None:
227
+ client = VideoProjectsClient(base_client=mock_base_client)
228
+
229
+ # Set custom poll interval
230
+ monkeypatch.setenv("MAGIC_HOUR_POLL_INTERVAL", "1.0")
231
+
232
+ # First calls return queued, then complete
233
+ mock_base_client.request.side_effect = [
234
+ DummyResponse(status="queued"),
235
+ DummyResponse(status="complete"),
236
+ ]
237
+
238
+ # Mock time.sleep to track calls
239
+ sleep_calls: List[float] = []
240
+
241
+ def mock_sleep(seconds: float) -> None:
242
+ sleep_calls.append(seconds)
243
+
244
+ monkeypatch.setattr("time.sleep", mock_sleep)
245
+
246
+ resp = client.check_result(
247
+ id="xyz", wait_for_completion=True, download_outputs=False
248
+ )
249
+
250
+ assert resp.status == "complete"
251
+ # Should have slept once with custom interval (1.0)
252
+ assert len(sleep_calls) == 1
253
+ assert sleep_calls[0] == 1.0
254
+
255
+
256
+ def test_check_result_poll_interval_multiple_polls(
257
+ mock_base_client: Mock, monkeypatch: Any
258
+ ) -> None:
259
+ client = VideoProjectsClient(base_client=mock_base_client)
260
+
261
+ # Set custom poll interval
262
+ monkeypatch.setenv("MAGIC_HOUR_POLL_INTERVAL", "0.1")
263
+
264
+ # Multiple calls return queued before complete
265
+ mock_base_client.request.side_effect = [
266
+ DummyResponse(status="queued"),
267
+ DummyResponse(status="queued"),
268
+ DummyResponse(status="queued"),
269
+ DummyResponse(status="complete"),
270
+ ]
271
+
272
+ # Mock time.sleep to track calls
273
+ sleep_calls: List[float] = []
274
+
275
+ def mock_sleep(seconds: float) -> None:
276
+ sleep_calls.append(seconds)
277
+
278
+ monkeypatch.setattr("time.sleep", mock_sleep)
279
+
280
+ resp = client.check_result(
281
+ id="xyz", wait_for_completion=True, download_outputs=False
282
+ )
283
+
284
+ assert resp.status == "complete"
285
+ # Should have slept 3 times with custom interval (0.1)
286
+ assert len(sleep_calls) == 3
287
+ assert all(sleep_time == 0.1 for sleep_time in sleep_calls)
288
+
289
+
290
+ @pytest.mark.asyncio
291
+ async def test_async_delete_calls_base_client(
292
+ mock_async_base_client: AsyncMock,
293
+ ) -> None:
294
+ client = AsyncVideoProjectsClient(base_client=mock_async_base_client)
295
+ await client.delete(id="456")
296
+
297
+ mock_async_base_client.request.assert_called_once()
298
+ call = mock_async_base_client.request.call_args[1]
299
+ assert call["method"] == "DELETE"
300
+ assert "/v1/video-projects/456" in call["path"]
301
+
302
+
303
+ @pytest.mark.asyncio
304
+ async def test_async_get_calls_base_client(mock_async_base_client: AsyncMock) -> None:
305
+ client = AsyncVideoProjectsClient(base_client=mock_async_base_client)
306
+ mock_async_base_client.request.return_value = DummyResponse()
307
+
308
+ resp = await client.get(id="zzz")
309
+
310
+ mock_async_base_client.request.assert_called_once()
311
+ assert isinstance(resp, models.V1VideoProjectsGetResponse)
312
+ assert resp.id == "test-id"
313
+
314
+
315
+ @pytest.mark.asyncio
316
+ async def test_async_check_result_no_wait_no_download(
317
+ mock_async_base_client: AsyncMock,
318
+ ) -> None:
319
+ client = AsyncVideoProjectsClient(base_client=mock_async_base_client)
320
+ mock_async_base_client.request.return_value = DummyResponse(status="queued")
321
+
322
+ resp = await client.check_result(
323
+ id="xyz",
324
+ wait_for_completion=False,
325
+ download_outputs=False,
326
+ )
327
+
328
+ assert resp.downloaded_paths is None
329
+
330
+
331
+ @pytest.mark.asyncio
332
+ async def test_async_check_result_wait_until_complete(
333
+ mock_async_base_client: AsyncMock, monkeypatch: Any
334
+ ) -> None:
335
+ client = AsyncVideoProjectsClient(base_client=mock_async_base_client)
336
+
337
+ # First calls return queued, then complete
338
+ mock_async_base_client.request.side_effect = [
339
+ DummyResponse(status="queued"),
340
+ DummyResponse(status="queued"),
341
+ DummyResponse(status="complete"),
342
+ ]
343
+
344
+ monkeypatch.setattr("time.sleep", lambda _: None) # type: ignore
345
+
346
+ resp = await client.check_result(
347
+ id="xyz", wait_for_completion=True, download_outputs=False
348
+ )
349
+
350
+ assert resp.status == "complete"
351
+ assert resp.downloaded_paths is None
352
+
353
+
354
+ @pytest.mark.asyncio
355
+ async def test_async_check_result_download_outputs(
356
+ tmp_path: Path, mock_async_base_client: AsyncMock, monkeypatch: Any
357
+ ) -> None:
358
+ client = AsyncVideoProjectsClient(base_client=mock_async_base_client)
359
+
360
+ file_url = "https://example.com/file.mp4"
361
+ mock_async_base_client.request.return_value = DummyResponse(
362
+ status="complete",
363
+ download_url=file_url,
364
+ )
365
+
366
+ # Create a mock response for httpx
367
+ mock_request = httpx.Request("GET", "https://example.com/file.mp4")
368
+ mock_response = httpx.Response(200, content=b"fake mp4", request=mock_request)
369
+
370
+ # Mock the httpx.AsyncClient class
371
+ class MockAsyncClient:
372
+ def __init__(self):
373
+ pass
374
+
375
+ async def __aenter__(self) -> "MockAsyncClient":
376
+ return self
377
+
378
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
379
+ pass
380
+
381
+ async def get(self, url: str) -> httpx.Response:
382
+ return mock_response
383
+
384
+ monkeypatch.setattr(httpx, "AsyncClient", MockAsyncClient)
385
+
386
+ resp = await client.check_result(
387
+ id="xyz",
388
+ wait_for_completion=True,
389
+ download_outputs=True,
390
+ download_directory=str(tmp_path),
391
+ )
392
+
393
+ assert resp.status == "complete"
394
+ assert resp.downloaded_paths
395
+ saved_file = Path(resp.downloaded_paths[0])
396
+ assert saved_file.exists()
397
+ assert saved_file.read_bytes() == b"fake mp4"
398
+
399
+
400
+ @pytest.mark.asyncio
401
+ async def test_async_check_result_error_status(
402
+ mock_async_base_client: AsyncMock,
403
+ ) -> None:
404
+ client = AsyncVideoProjectsClient(base_client=mock_async_base_client)
405
+ mock_async_base_client.request.return_value = DummyResponse(
406
+ status="error", error="Boom!"
407
+ )
408
+
409
+ resp = await client.check_result(
410
+ id="err", wait_for_completion=True, download_outputs=False
411
+ )
412
+ assert resp.status == "error"
413
+ assert resp.error is not None
414
+ assert resp.error.message == "Boom!"
415
+ assert resp.downloaded_paths is None
416
+
417
+
418
+ @pytest.mark.asyncio
419
+ async def test_async_check_result_canceled_status(
420
+ mock_async_base_client: AsyncMock,
421
+ ) -> None:
422
+ client = AsyncVideoProjectsClient(base_client=mock_async_base_client)
423
+ mock_async_base_client.request.return_value = DummyResponse(status="canceled")
424
+
425
+ resp = await client.check_result(
426
+ id="cancel", wait_for_completion=True, download_outputs=False
427
+ )
428
+ assert resp.status == "canceled"
429
+ assert resp.downloaded_paths is None
430
+
431
+
432
+ @pytest.mark.asyncio
433
+ async def test_async_check_result_poll_interval_default(
434
+ mock_async_base_client: AsyncMock, monkeypatch: Any
435
+ ) -> None:
436
+ client = AsyncVideoProjectsClient(base_client=mock_async_base_client)
437
+
438
+ # First calls return queued, then complete
439
+ mock_async_base_client.request.side_effect = [
440
+ DummyResponse(status="queued"),
441
+ DummyResponse(status="complete"),
442
+ ]
443
+
444
+ # Mock time.sleep to track calls
445
+ sleep_calls: List[float] = []
446
+
447
+ def mock_sleep(seconds: float) -> None:
448
+ sleep_calls.append(seconds)
449
+
450
+ monkeypatch.setattr("time.sleep", mock_sleep)
451
+
452
+ resp = await client.check_result(
453
+ id="xyz", wait_for_completion=True, download_outputs=False
454
+ )
455
+
456
+ assert resp.status == "complete"
457
+ # Should have slept once with default interval (0.5)
458
+ assert len(sleep_calls) == 1
459
+ assert sleep_calls[0] == 0.5
460
+
461
+
462
+ @pytest.mark.asyncio
463
+ async def test_async_check_result_poll_interval_custom(
464
+ mock_async_base_client: AsyncMock, monkeypatch: Any
465
+ ) -> None:
466
+ client = AsyncVideoProjectsClient(base_client=mock_async_base_client)
467
+
468
+ # Set custom poll interval
469
+ monkeypatch.setenv("MAGIC_HOUR_POLL_INTERVAL", "2.0")
470
+
471
+ # First calls return queued, then complete
472
+ mock_async_base_client.request.side_effect = [
473
+ DummyResponse(status="queued"),
474
+ DummyResponse(status="complete"),
475
+ ]
476
+
477
+ # Mock time.sleep to track calls
478
+ sleep_calls: List[float] = []
479
+
480
+ def mock_sleep(seconds: float) -> None:
481
+ sleep_calls.append(seconds)
482
+
483
+ monkeypatch.setattr("time.sleep", mock_sleep)
484
+
485
+ resp = await client.check_result(
486
+ id="xyz", wait_for_completion=True, download_outputs=False
487
+ )
488
+
489
+ assert resp.status == "complete"
490
+ # Should have slept once with custom interval (2.0)
491
+ assert len(sleep_calls) == 1
492
+ assert sleep_calls[0] == 2.0
493
+
494
+
495
+ @pytest.mark.asyncio
496
+ async def test_async_check_result_poll_interval_multiple_polls(
497
+ mock_async_base_client: AsyncMock, monkeypatch: Any
498
+ ) -> None:
499
+ client = AsyncVideoProjectsClient(base_client=mock_async_base_client)
500
+
501
+ # Set custom poll interval
502
+ monkeypatch.setenv("MAGIC_HOUR_POLL_INTERVAL", "0.3")
503
+
504
+ # Multiple calls return queued before complete
505
+ mock_async_base_client.request.side_effect = [
506
+ DummyResponse(status="queued"),
507
+ DummyResponse(status="queued"),
508
+ DummyResponse(status="queued"),
509
+ DummyResponse(status="complete"),
510
+ ]
511
+
512
+ # Mock time.sleep to track calls
513
+ sleep_calls: List[float] = []
514
+
515
+ def mock_sleep(seconds: float) -> None:
516
+ sleep_calls.append(seconds)
517
+
518
+ monkeypatch.setattr("time.sleep", mock_sleep)
519
+
520
+ resp = await client.check_result(
521
+ id="xyz", wait_for_completion=True, download_outputs=False
522
+ )
523
+
524
+ assert resp.status == "complete"
525
+ # Should have slept 3 times with custom interval (0.3)
526
+ assert len(sleep_calls) == 3
527
+ assert all(sleep_time == 0.3 for sleep_time in sleep_calls)