prefect 3.6.12__py3-none-any.whl → 3.6.13__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 (266) hide show
  1. prefect/_build_info.py +3 -3
  2. prefect/_internal/compatibility/blocks.py +18 -0
  3. prefect/_internal/urls.py +29 -0
  4. prefect/_states.py +4 -1
  5. prefect/client/orchestration/_concurrency_limits/client.py +16 -16
  6. prefect/client/schemas/filters.py +24 -0
  7. prefect/client/subscriptions.py +1 -1
  8. prefect/events/clients.py +52 -13
  9. prefect/flow_engine.py +192 -10
  10. prefect/flows.py +61 -2
  11. prefect/results.py +262 -21
  12. prefect/runner/runner.py +29 -82
  13. prefect/runner/storage.py +6 -25
  14. prefect/server/schemas/filters.py +45 -0
  15. prefect/server/ui-v2/assets/{artifact-card-Ds2tntMW.js → artifact-card-CUEaRDGw.js} +2 -2
  16. prefect/server/ui-v2/assets/{artifact-card-Ds2tntMW.js.map → artifact-card-CUEaRDGw.js.map} +1 -1
  17. prefect/server/ui-v2/assets/{artifact._id-erPNL0a8.js → artifact._id-Ca6VCUS0.js} +2 -2
  18. prefect/server/ui-v2/assets/{artifact._id-erPNL0a8.js.map → artifact._id-Ca6VCUS0.js.map} +1 -1
  19. prefect/server/ui-v2/assets/{automation-wizard-Bx9AQxRV.js → automation-wizard-z26pICBl.js} +2 -2
  20. prefect/server/ui-v2/assets/{automation-wizard-Bx9AQxRV.js.map → automation-wizard-z26pICBl.js.map} +1 -1
  21. prefect/server/ui-v2/assets/automation._id-DuodjY5t.js +2 -0
  22. prefect/server/ui-v2/assets/{automation._id-IHh5QJwE.js.map → automation._id-DuodjY5t.js.map} +1 -1
  23. prefect/server/ui-v2/assets/{automation_._id.edit-Ds8vO0c5.js → automation_._id.edit-CcuJhc-y.js} +2 -2
  24. prefect/server/ui-v2/assets/{automation_._id.edit-Ds8vO0c5.js.map → automation_._id.edit-CcuJhc-y.js.map} +1 -1
  25. prefect/server/ui-v2/assets/{automations-header-C6aUe5Tw.js → automations-header-CgOWwuc6.js} +2 -2
  26. prefect/server/ui-v2/assets/{automations-header-C6aUe5Tw.js.map → automations-header-CgOWwuc6.js.map} +1 -1
  27. prefect/server/ui-v2/assets/{base-job-template-form-section-a-9TFMcW.js → base-job-template-form-section-DN9HyjM5.js} +2 -2
  28. prefect/server/ui-v2/assets/{base-job-template-form-section-a-9TFMcW.js.map → base-job-template-form-section-DN9HyjM5.js.map} +1 -1
  29. prefect/server/ui-v2/assets/{block-type-details-eq3Wg2ht.js → block-type-details-rrToxL5r.js} +2 -2
  30. prefect/server/ui-v2/assets/{block-type-details-eq3Wg2ht.js.map → block-type-details-rrToxL5r.js.map} +1 -1
  31. prefect/server/ui-v2/assets/block-type-logo-DURScH8H.js +2 -0
  32. prefect/server/ui-v2/assets/{block-type-logo-B31LJm5H.js.map → block-type-logo-DURScH8H.js.map} +1 -1
  33. prefect/server/ui-v2/assets/{block._id-OVdz3htC.js → block._id-Bue6lnrN.js} +2 -2
  34. prefect/server/ui-v2/assets/{block._id-OVdz3htC.js.map → block._id-Bue6lnrN.js.map} +1 -1
  35. prefect/server/ui-v2/assets/block_._id.edit-QY0OzN_b.js +2 -0
  36. prefect/server/ui-v2/assets/{block_._id.edit-CIhIBJrm.js.map → block_._id.edit-QY0OzN_b.js.map} +1 -1
  37. prefect/server/ui-v2/assets/catalog-CGjRGWv6.js +2 -0
  38. prefect/server/ui-v2/assets/{catalog-CfniU0UV.js.map → catalog-CGjRGWv6.js.map} +1 -1
  39. prefect/server/ui-v2/assets/catalog_._slug-BMFNZhEb.js +2 -0
  40. prefect/server/ui-v2/assets/{catalog_._slug-C9p86T4s.js.map → catalog_._slug-BMFNZhEb.js.map} +1 -1
  41. prefect/server/ui-v2/assets/catalog_._slug_.create-CuFA3kUC.js +2 -0
  42. prefect/server/ui-v2/assets/{catalog_._slug_.create-BhSunL__.js.map → catalog_._slug_.create-CuFA3kUC.js.map} +1 -1
  43. prefect/server/ui-v2/assets/{collapsible-DBD7wjpi.js → collapsible-Ell2FjrX.js} +2 -2
  44. prefect/server/ui-v2/assets/{collapsible-DBD7wjpi.js.map → collapsible-Ell2FjrX.js.map} +1 -1
  45. prefect/server/ui-v2/assets/{concurrency-limit._id-C-XWq7Tf.js → concurrency-limit._id-Rr1ysUct.js} +2 -2
  46. prefect/server/ui-v2/assets/{concurrency-limit._id-C-XWq7Tf.js.map → concurrency-limit._id-Rr1ysUct.js.map} +1 -1
  47. prefect/server/ui-v2/assets/{create-noFojqGL.js → create-CQK8-uO1.js} +2 -2
  48. prefect/server/ui-v2/assets/{create-noFojqGL.js.map → create-CQK8-uO1.js.map} +1 -1
  49. prefect/server/ui-v2/assets/{create-Bj2t5YQq.js → create-ewA3uq_h.js} +2 -2
  50. prefect/server/ui-v2/assets/{create-Bj2t5YQq.js.map → create-ewA3uq_h.js.map} +1 -1
  51. prefect/server/ui-v2/assets/{data-table-BjFlVIyC.js → data-table-Bx1uYX_M.js} +2 -2
  52. prefect/server/ui-v2/assets/{data-table-BjFlVIyC.js.map → data-table-Bx1uYX_M.js.map} +1 -1
  53. prefect/server/ui-v2/assets/delete-confirmation-dialog-CqKsUEj_.js +2 -0
  54. prefect/server/ui-v2/assets/{delete-confirmation-dialog-COdZmNfa.js.map → delete-confirmation-dialog-CqKsUEj_.js.map} +1 -1
  55. prefect/server/ui-v2/assets/{deployment-action-header-C6v2kZ6V.js → deployment-action-header-Bz5COdER.js} +2 -2
  56. prefect/server/ui-v2/assets/{deployment-action-header-C6v2kZ6V.js.map → deployment-action-header-Bz5COdER.js.map} +1 -1
  57. prefect/server/ui-v2/assets/{deployment-form-D9d5sZM2.js → deployment-form-CrlZlNoj.js} +3 -3
  58. prefect/server/ui-v2/assets/{deployment-form-D9d5sZM2.js.map → deployment-form-CrlZlNoj.js.map} +1 -1
  59. prefect/server/ui-v2/assets/{deployment-links-Dy-M1Que.js → deployment-links-D9ZR_vmp.js} +2 -2
  60. prefect/server/ui-v2/assets/{deployment-links-Dy-M1Que.js.map → deployment-links-D9ZR_vmp.js.map} +1 -1
  61. prefect/server/ui-v2/assets/deployment._id-BV0rSqba.js +2 -0
  62. prefect/server/ui-v2/assets/{deployment._id-DVmqclRz.js.map → deployment._id-BV0rSqba.js.map} +1 -1
  63. prefect/server/ui-v2/assets/{deployment._id-CZlY9261.js → deployment._id-DvHhx-qN.js} +2 -2
  64. prefect/server/ui-v2/assets/{deployment._id-CZlY9261.js.map → deployment._id-DvHhx-qN.js.map} +1 -1
  65. prefect/server/ui-v2/assets/deployment_._id.duplicate-BrEOenqP.js +2 -0
  66. prefect/server/ui-v2/assets/{deployment_._id.duplicate-DVMjpk5m.js.map → deployment_._id.duplicate-BrEOenqP.js.map} +1 -1
  67. prefect/server/ui-v2/assets/deployment_._id.edit-BbYKPK42.js +2 -0
  68. prefect/server/ui-v2/assets/{deployment_._id.edit-Ck_P6KDn.js.map → deployment_._id.edit-BbYKPK42.js.map} +1 -1
  69. prefect/server/ui-v2/assets/{deployment_._id.run-Cht7AHDG.js → deployment_._id.run-Dv7S_MJR.js} +2 -2
  70. prefect/server/ui-v2/assets/{deployment_._id.run-Cht7AHDG.js.map → deployment_._id.run-Dv7S_MJR.js.map} +1 -1
  71. prefect/server/ui-v2/assets/{dropdown-menu-xZ_3w9OP.js → dropdown-menu-e0Fqb6aw.js} +2 -2
  72. prefect/server/ui-v2/assets/{dropdown-menu-xZ_3w9OP.js.map → dropdown-menu-e0Fqb6aw.js.map} +1 -1
  73. prefect/server/ui-v2/assets/{event._eventDate._eventId-CpexuXd6.js → event._eventDate._eventId-Cp4UmGqq.js} +2 -2
  74. prefect/server/ui-v2/assets/{event._eventDate._eventId-CpexuXd6.js.map → event._eventDate._eventId-Cp4UmGqq.js.map} +1 -1
  75. prefect/server/ui-v2/assets/flow-run-graph-BrqoR3E2.js +2 -0
  76. prefect/server/ui-v2/assets/flow-run-graph-BrqoR3E2.js.map +1 -0
  77. prefect/server/ui-v2/assets/{flow-run._id-ZBlKBwPO.js → flow-run._id-BOp38Pbq.js} +2 -2
  78. prefect/server/ui-v2/assets/{flow-run._id-ZBlKBwPO.js.map → flow-run._id-BOp38Pbq.js.map} +1 -1
  79. prefect/server/ui-v2/assets/flow-run._id-DJnTDEN_.js +2 -0
  80. prefect/server/ui-v2/assets/{flow-run._id-OL0YhyLW.js.map → flow-run._id-DJnTDEN_.js.map} +1 -1
  81. prefect/server/ui-v2/assets/flow-run._id-D_wY_rBe.js +4 -0
  82. prefect/server/ui-v2/assets/flow-run._id-D_wY_rBe.js.map +1 -0
  83. prefect/server/ui-v2/assets/flow-runs-pagination-Bq2ZUzM6.js +2 -0
  84. prefect/server/ui-v2/assets/flow-runs-pagination-Bq2ZUzM6.js.map +1 -0
  85. prefect/server/ui-v2/assets/flow._id-eCBL95rg.js +2 -0
  86. prefect/server/ui-v2/assets/{flow._id-DhrCicwR.js.map → flow._id-eCBL95rg.js.map} +1 -1
  87. prefect/server/ui-v2/assets/{form-BTub_PhK.js → form-DNerk3LS.js} +2 -2
  88. prefect/server/ui-v2/assets/{form-BTub_PhK.js.map → form-DNerk3LS.js.map} +1 -1
  89. prefect/server/ui-v2/assets/{header-6wmrKLU3.js → header-B0ejRncu.js} +2 -2
  90. prefect/server/ui-v2/assets/{header-6wmrKLU3.js.map → header-B0ejRncu.js.map} +1 -1
  91. prefect/server/ui-v2/assets/{header-Dp9qi8fq.js → header-DwagHBlF.js} +2 -2
  92. prefect/server/ui-v2/assets/{header-Dp9qi8fq.js.map → header-DwagHBlF.js.map} +1 -1
  93. prefect/server/ui-v2/assets/{header-B75eb688.js → header-huSvwxKI.js} +2 -2
  94. prefect/server/ui-v2/assets/{header-B75eb688.js.map → header-huSvwxKI.js.map} +1 -1
  95. prefect/server/ui-v2/assets/{index-DSaSov8V.js → index-BO3SOwdV.js} +2 -2
  96. prefect/server/ui-v2/assets/{index-DSaSov8V.js.map → index-BO3SOwdV.js.map} +1 -1
  97. prefect/server/ui-v2/assets/{index-DOkFJdYY.js → index-BTPE3vs7.js} +2 -2
  98. prefect/server/ui-v2/assets/{index-DOkFJdYY.js.map → index-BTPE3vs7.js.map} +1 -1
  99. prefect/server/ui-v2/assets/index-BU4yZRd3.js +2 -0
  100. prefect/server/ui-v2/assets/{index-Cs8eFQKw.js.map → index-BU4yZRd3.js.map} +1 -1
  101. prefect/server/ui-v2/assets/index-BhALpenO.js +2 -0
  102. prefect/server/ui-v2/assets/{index-qPlIYf3i.js.map → index-BhALpenO.js.map} +1 -1
  103. prefect/server/ui-v2/assets/index-BzN9bQeM.js +2 -0
  104. prefect/server/ui-v2/assets/{index-CT_nG86y.js.map → index-BzN9bQeM.js.map} +1 -1
  105. prefect/server/ui-v2/assets/{index-zpb5iSCL.js → index-CBhi1P9g.js} +2 -2
  106. prefect/server/ui-v2/assets/{index-zpb5iSCL.js.map → index-CBhi1P9g.js.map} +1 -1
  107. prefect/server/ui-v2/assets/{index-UN2Tx4jH.js → index-CRDz4nhM.js} +2 -2
  108. prefect/server/ui-v2/assets/{index-UN2Tx4jH.js.map → index-CRDz4nhM.js.map} +1 -1
  109. prefect/server/ui-v2/assets/index-CTnoa3Ho.js +2 -0
  110. prefect/server/ui-v2/assets/index-CTnoa3Ho.js.map +1 -0
  111. prefect/server/ui-v2/assets/{index-DzMGV8GV.js → index-CWkbSdxY.js} +2 -2
  112. prefect/server/ui-v2/assets/{index-DzMGV8GV.js.map → index-CWkbSdxY.js.map} +1 -1
  113. prefect/server/ui-v2/assets/index-CgOsOj5t.js +2 -0
  114. prefect/server/ui-v2/assets/{index-7ThYp9SY.js.map → index-CgOsOj5t.js.map} +1 -1
  115. prefect/server/ui-v2/assets/{index-CGWoVV2s.js → index-CnIJUujl.js} +2 -2
  116. prefect/server/ui-v2/assets/{index-CGWoVV2s.js.map → index-CnIJUujl.js.map} +1 -1
  117. prefect/server/ui-v2/assets/{index-D08xgmV0.js → index-Cutg_A1j.js} +2 -2
  118. prefect/server/ui-v2/assets/{index-D08xgmV0.js.map → index-Cutg_A1j.js.map} +1 -1
  119. prefect/server/ui-v2/assets/index-D5RdrxkU.js +17 -0
  120. prefect/server/ui-v2/assets/index-D5RdrxkU.js.map +1 -0
  121. prefect/server/ui-v2/assets/{index-DfiNsXba.js → index-DDiyFpIV.js} +2 -2
  122. prefect/server/ui-v2/assets/{index-DfiNsXba.js.map → index-DDiyFpIV.js.map} +1 -1
  123. prefect/server/ui-v2/assets/{index-BA7ACCoL.js → index-DI2DC5gd.js} +2 -2
  124. prefect/server/ui-v2/assets/{index-BA7ACCoL.js.map → index-DI2DC5gd.js.map} +1 -1
  125. prefect/server/ui-v2/assets/index-Dg_duvDx.js +2 -0
  126. prefect/server/ui-v2/assets/{index-B_3f8Hcb.js.map → index-Dg_duvDx.js.map} +1 -1
  127. prefect/server/ui-v2/assets/index-Dspw5HFj.js +2 -0
  128. prefect/server/ui-v2/assets/{index-7-r4ia_S.js.map → index-Dspw5HFj.js.map} +1 -1
  129. prefect/server/ui-v2/assets/index-NY089eTx.css +1 -0
  130. prefect/server/ui-v2/assets/{index-DYOACRXY.js → index-VOOLxiSE.js} +2 -2
  131. prefect/server/ui-v2/assets/{index-DYOACRXY.js.map → index-VOOLxiSE.js.map} +1 -1
  132. prefect/server/ui-v2/assets/{index-HGoNWFfP.js → index-Wfs7Cjew.js} +2 -2
  133. prefect/server/ui-v2/assets/{index-HGoNWFfP.js.map → index-Wfs7Cjew.js.map} +1 -1
  134. prefect/server/ui-v2/assets/index-dSUEBAqg.js +2 -0
  135. prefect/server/ui-v2/assets/{index-H6bwm6L6.js.map → index-dSUEBAqg.js.map} +1 -1
  136. prefect/server/ui-v2/assets/{index-D3ILnEzm.js → index-uvH5a3zO.js} +2 -2
  137. prefect/server/ui-v2/assets/{index-D3ILnEzm.js.map → index-uvH5a3zO.js.map} +1 -1
  138. prefect/server/ui-v2/assets/{json-input-9UPGqxTw.js → json-input-gXz7BuJj.js} +2 -2
  139. prefect/server/ui-v2/assets/{json-input-9UPGqxTw.js.map → json-input-gXz7BuJj.js.map} +1 -1
  140. prefect/server/ui-v2/assets/{key._key-CTFfXO_k.js → key._key-CJPbLXwU.js} +2 -2
  141. prefect/server/ui-v2/assets/{key._key-CTFfXO_k.js.map → key._key-CJPbLXwU.js.map} +1 -1
  142. prefect/server/ui-v2/assets/{lazy-markdown-BHwIrC8E.js → lazy-markdown-wPid80zf.js} +2 -2
  143. prefect/server/ui-v2/assets/{lazy-markdown-BHwIrC8E.js.map → lazy-markdown-wPid80zf.js.map} +1 -1
  144. prefect/server/ui-v2/assets/{login-kqmT29n7.js → login-DC63bGXK.js} +2 -2
  145. prefect/server/ui-v2/assets/{login-kqmT29n7.js.map → login-DC63bGXK.js.map} +1 -1
  146. prefect/server/ui-v2/assets/{markdown-input-BesmAbLS.js → markdown-input-BhqrU6Eo.js} +2 -2
  147. prefect/server/ui-v2/assets/{markdown-input-BesmAbLS.js.map → markdown-input-BhqrU6Eo.js.map} +1 -1
  148. prefect/server/ui-v2/assets/{python-example-snippet-COTWYn1Y.js → python-example-snippet-3jtXWQZN.js} +3 -3
  149. prefect/server/ui-v2/assets/{python-example-snippet-COTWYn1Y.js.map → python-example-snippet-3jtXWQZN.js.map} +1 -1
  150. prefect/server/ui-v2/assets/{python-input-Bjccebi0.js → python-input-ZVi-v324.js} +2 -2
  151. prefect/server/ui-v2/assets/{python-input-Bjccebi0.js.map → python-input-ZVi-v324.js.map} +1 -1
  152. prefect/server/ui-v2/assets/{radio-group-DkAK0M2h.js → radio-group-BBMLpHGc.js} +2 -2
  153. prefect/server/ui-v2/assets/{radio-group-DkAK0M2h.js.map → radio-group-BBMLpHGc.js.map} +1 -1
  154. prefect/server/ui-v2/assets/route-error-state-BJXl8qkX.js +2 -0
  155. prefect/server/ui-v2/assets/{route-error-state-ALftyvGl.js.map → route-error-state-BJXl8qkX.js.map} +1 -1
  156. prefect/server/ui-v2/assets/{schema-form-BR4E-WXE.js → schema-form-BEqYjsM-.js} +2 -2
  157. prefect/server/ui-v2/assets/{schema-form-BR4E-WXE.js.map → schema-form-BEqYjsM-.js.map} +1 -1
  158. prefect/server/ui-v2/assets/{schema-form-input-string-format-datetime-BhL8C5NS.js → schema-form-input-string-format-datetime-l3xt3PWf.js} +4 -4
  159. prefect/server/ui-v2/assets/{schema-form-input-string-format-datetime-BhL8C5NS.js.map → schema-form-input-string-format-datetime-l3xt3PWf.js.map} +1 -1
  160. prefect/server/ui-v2/assets/settings-txD0dR5Q.js +2 -0
  161. prefect/server/ui-v2/assets/settings-txD0dR5Q.js.map +1 -0
  162. prefect/server/ui-v2/assets/{sort-filter-BD4vwJXt.js → sort-filter-DHPFdKZ2.js} +2 -2
  163. prefect/server/ui-v2/assets/{sort-filter-BD4vwJXt.js.map → sort-filter-DHPFdKZ2.js.map} +1 -1
  164. prefect/server/ui-v2/assets/state-colors-CAAf0Eg3.js +2 -0
  165. prefect/server/ui-v2/assets/state-colors-CAAf0Eg3.js.map +1 -0
  166. prefect/server/ui-v2/assets/table-ULfpXJXB.js +2 -0
  167. prefect/server/ui-v2/assets/table-ULfpXJXB.js.map +1 -0
  168. prefect/server/ui-v2/assets/tags-input-BLzMOTDb.js +2 -0
  169. prefect/server/ui-v2/assets/tags-input-BLzMOTDb.js.map +1 -0
  170. prefect/server/ui-v2/assets/task-run-concurrency-limits-reset-dialog-BpeKHk7g.js +2 -0
  171. prefect/server/ui-v2/assets/{task-run-concurrency-limits-reset-dialog-CG3den1B.js.map → task-run-concurrency-limits-reset-dialog-BpeKHk7g.js.map} +1 -1
  172. prefect/server/ui-v2/assets/{task-run._id-DOcIzVi0.js → task-run._id-CkOl9MJs.js} +3 -3
  173. prefect/server/ui-v2/assets/{task-run._id-DOcIzVi0.js.map → task-run._id-CkOl9MJs.js.map} +1 -1
  174. prefect/server/ui-v2/assets/task-run._id-udkz1lhh.js +2 -0
  175. prefect/server/ui-v2/assets/{task-run._id-CnIVqU6v.js.map → task-run._id-udkz1lhh.js.map} +1 -1
  176. prefect/server/ui-v2/assets/task-runs-pagination-B7D5K_FM.js +2 -0
  177. prefect/server/ui-v2/assets/{task-runs-pagination-DLSAz-Ur.js.map → task-runs-pagination-B7D5K_FM.js.map} +1 -1
  178. prefect/server/ui-v2/assets/{textarea-D8LjlIx7.js → textarea-C4bdj7Jk.js} +2 -2
  179. prefect/server/ui-v2/assets/{textarea-D8LjlIx7.js.map → textarea-C4bdj7Jk.js.map} +1 -1
  180. prefect/server/ui-v2/assets/{timezone-select-BG3cL3-U.js → timezone-select-AdlSRQxZ.js} +2 -2
  181. prefect/server/ui-v2/assets/{timezone-select-BG3cL3-U.js.map → timezone-select-AdlSRQxZ.js.map} +1 -1
  182. prefect/server/ui-v2/assets/{toggle-group-D3zeurIL.js → toggle-group-C-vxYz4l.js} +2 -2
  183. prefect/server/ui-v2/assets/{toggle-group-D3zeurIL.js.map → toggle-group-C-vxYz4l.js.map} +1 -1
  184. prefect/server/ui-v2/assets/use-delete-automation-confirmation-dialog-Cqhaqtqe.js +2 -0
  185. prefect/server/ui-v2/assets/{use-delete-automation-confirmation-dialog-Bzy2ML2T.js.map → use-delete-automation-confirmation-dialog-Cqhaqtqe.js.map} +1 -1
  186. prefect/server/ui-v2/assets/{use-delete-block-document-confirmation-dialog-DRAP-Tnu.js → use-delete-block-document-confirmation-dialog-GjNhFxZe.js} +2 -2
  187. prefect/server/ui-v2/assets/{use-delete-block-document-confirmation-dialog-DRAP-Tnu.js.map → use-delete-block-document-confirmation-dialog-GjNhFxZe.js.map} +1 -1
  188. prefect/server/ui-v2/assets/{use-flow-runs-selected-rows-D4yWonR8.js → use-flow-runs-selected-rows-DfwmswyR.js} +2 -2
  189. prefect/server/ui-v2/assets/{use-flow-runs-selected-rows-D4yWonR8.js.map → use-flow-runs-selected-rows-DfwmswyR.js.map} +1 -1
  190. prefect/server/ui-v2/assets/{use-get-artifacts-flow-task-runs-DZeBiVd9.js → use-get-artifacts-flow-task-runs-BEBpG_5J.js} +2 -2
  191. prefect/server/ui-v2/assets/{use-get-artifacts-flow-task-runs-DZeBiVd9.js.map → use-get-artifacts-flow-task-runs-BEBpG_5J.js.map} +1 -1
  192. prefect/server/ui-v2/assets/{use-quick-run-BxAMqZDM.js → use-quick-run-BYBRcDwC.js} +2 -2
  193. prefect/server/ui-v2/assets/{use-quick-run-BxAMqZDM.js.map → use-quick-run-BYBRcDwC.js.map} +1 -1
  194. prefect/server/ui-v2/assets/{use-stepper-T3wAKNvM.js → use-stepper-Bk97vOTm.js} +2 -2
  195. prefect/server/ui-v2/assets/{use-stepper-T3wAKNvM.js.map → use-stepper-Bk97vOTm.js.map} +1 -1
  196. prefect/server/ui-v2/assets/{utilities-B2JMf8iI.js → utilities-BQwGFLk5.js} +2 -2
  197. prefect/server/ui-v2/assets/{utilities-B2JMf8iI.js.map → utilities-BQwGFLk5.js.map} +1 -1
  198. prefect/server/ui-v2/assets/{work-pool-filter-CZz0AJlt.js → work-pool-filter-Ddhp_M-L.js} +2 -2
  199. prefect/server/ui-v2/assets/{work-pool-filter-CZz0AJlt.js.map → work-pool-filter-Ddhp_M-L.js.map} +1 -1
  200. prefect/server/ui-v2/assets/work-pool-queue-toggle-DX3eV3R_.js +2 -0
  201. prefect/server/ui-v2/assets/{work-pool-queue-toggle-D4eeo-hi.js.map → work-pool-queue-toggle-DX3eV3R_.js.map} +1 -1
  202. prefect/server/ui-v2/assets/{work-pool._workPoolName-DrWddu9K.js → work-pool._workPoolName-BaRIsXBX.js} +2 -2
  203. prefect/server/ui-v2/assets/{work-pool._workPoolName-DrWddu9K.js.map → work-pool._workPoolName-BaRIsXBX.js.map} +1 -1
  204. prefect/server/ui-v2/assets/work-pool_._workPoolName.edit-dhS_Xz32.js +2 -0
  205. prefect/server/ui-v2/assets/{work-pool_._workPoolName.edit-CA0ePjCk.js.map → work-pool_._workPoolName.edit-dhS_Xz32.js.map} +1 -1
  206. prefect/server/ui-v2/assets/{work-pool_._workPoolName.queue._workQueueName-DpUnE86v.js → work-pool_._workPoolName.queue._workQueueName-BlstL9Se.js} +2 -2
  207. prefect/server/ui-v2/assets/{work-pool_._workPoolName.queue._workQueueName-DpUnE86v.js.map → work-pool_._workPoolName.queue._workQueueName-BlstL9Se.js.map} +1 -1
  208. prefect/server/ui-v2/assets/{work-queue-icon-text-BjiA7vAW.js → work-queue-icon-text-_Ez8e2dw.js} +2 -2
  209. prefect/server/ui-v2/assets/{work-queue-icon-text-BjiA7vAW.js.map → work-queue-icon-text-_Ez8e2dw.js.map} +1 -1
  210. prefect/server/ui-v2/index.html +2 -2
  211. prefect/settings/models/flows.py +14 -1
  212. prefect/settings/models/runner.py +16 -6
  213. prefect/task_engine.py +17 -3
  214. prefect/task_worker.py +99 -17
  215. prefect/tasks.py +2 -2
  216. prefect/testing/fixtures.py +41 -6
  217. {prefect-3.6.12.dist-info → prefect-3.6.13.dist-info}/METADATA +1 -1
  218. {prefect-3.6.12.dist-info → prefect-3.6.13.dist-info}/RECORD +221 -220
  219. prefect/server/ui-v2/assets/automation._id-IHh5QJwE.js +0 -2
  220. prefect/server/ui-v2/assets/block-type-logo-B31LJm5H.js +0 -2
  221. prefect/server/ui-v2/assets/block_._id.edit-CIhIBJrm.js +0 -2
  222. prefect/server/ui-v2/assets/catalog-CfniU0UV.js +0 -2
  223. prefect/server/ui-v2/assets/catalog_._slug-C9p86T4s.js +0 -2
  224. prefect/server/ui-v2/assets/catalog_._slug_.create-BhSunL__.js +0 -2
  225. prefect/server/ui-v2/assets/delete-confirmation-dialog-COdZmNfa.js +0 -2
  226. prefect/server/ui-v2/assets/deployment._id-DVmqclRz.js +0 -2
  227. prefect/server/ui-v2/assets/deployment_._id.duplicate-DVMjpk5m.js +0 -2
  228. prefect/server/ui-v2/assets/deployment_._id.edit-Ck_P6KDn.js +0 -2
  229. prefect/server/ui-v2/assets/flow-run-graph-CfoPEAgQ.js +0 -2
  230. prefect/server/ui-v2/assets/flow-run-graph-CfoPEAgQ.js.map +0 -1
  231. prefect/server/ui-v2/assets/flow-run._id-C-qxwEBp.js +0 -4
  232. prefect/server/ui-v2/assets/flow-run._id-C-qxwEBp.js.map +0 -1
  233. prefect/server/ui-v2/assets/flow-run._id-OL0YhyLW.js +0 -2
  234. prefect/server/ui-v2/assets/flow-runs-pagination-DnwkJapB.js +0 -2
  235. prefect/server/ui-v2/assets/flow-runs-pagination-DnwkJapB.js.map +0 -1
  236. prefect/server/ui-v2/assets/flow._id-DhrCicwR.js +0 -2
  237. prefect/server/ui-v2/assets/index-7-r4ia_S.js +0 -2
  238. prefect/server/ui-v2/assets/index-7ThYp9SY.js +0 -2
  239. prefect/server/ui-v2/assets/index-B7zHzWQW.css +0 -1
  240. prefect/server/ui-v2/assets/index-B_3f8Hcb.js +0 -2
  241. prefect/server/ui-v2/assets/index-BiCd-Iuz.js +0 -2
  242. prefect/server/ui-v2/assets/index-BiCd-Iuz.js.map +0 -1
  243. prefect/server/ui-v2/assets/index-CT_nG86y.js +0 -2
  244. prefect/server/ui-v2/assets/index-Cs8eFQKw.js +0 -2
  245. prefect/server/ui-v2/assets/index-H6bwm6L6.js +0 -2
  246. prefect/server/ui-v2/assets/index-WYPZo52S.js +0 -17
  247. prefect/server/ui-v2/assets/index-WYPZo52S.js.map +0 -1
  248. prefect/server/ui-v2/assets/index-qPlIYf3i.js +0 -2
  249. prefect/server/ui-v2/assets/route-error-state-ALftyvGl.js +0 -2
  250. prefect/server/ui-v2/assets/settings-BL0X8cDU.js +0 -2
  251. prefect/server/ui-v2/assets/settings-BL0X8cDU.js.map +0 -1
  252. prefect/server/ui-v2/assets/table-CEAx-qHs.js +0 -2
  253. prefect/server/ui-v2/assets/table-CEAx-qHs.js.map +0 -1
  254. prefect/server/ui-v2/assets/tags-input-D1RJZEUA.js +0 -2
  255. prefect/server/ui-v2/assets/tags-input-D1RJZEUA.js.map +0 -1
  256. prefect/server/ui-v2/assets/task-run-concurrency-limits-reset-dialog-CG3den1B.js +0 -2
  257. prefect/server/ui-v2/assets/task-run._id-CnIVqU6v.js +0 -2
  258. prefect/server/ui-v2/assets/task-runs-pagination-DLSAz-Ur.js +0 -2
  259. prefect/server/ui-v2/assets/use-delete-automation-confirmation-dialog-Bzy2ML2T.js +0 -2
  260. prefect/server/ui-v2/assets/use-local-storage-CpxMp5wR.js +0 -2
  261. prefect/server/ui-v2/assets/use-local-storage-CpxMp5wR.js.map +0 -1
  262. prefect/server/ui-v2/assets/work-pool-queue-toggle-D4eeo-hi.js +0 -2
  263. prefect/server/ui-v2/assets/work-pool_._workPoolName.edit-CA0ePjCk.js +0 -2
  264. {prefect-3.6.12.dist-info → prefect-3.6.13.dist-info}/WHEEL +0 -0
  265. {prefect-3.6.12.dist-info → prefect-3.6.13.dist-info}/entry_points.txt +0 -0
  266. {prefect-3.6.12.dist-info → prefect-3.6.13.dist-info}/licenses/LICENSE +0 -0
prefect/_build_info.py CHANGED
@@ -1,5 +1,5 @@
1
1
  # Generated by versioningit
2
- __version__ = "3.6.12"
3
- __build_date__ = "2026-01-20 18:20:13.303126+00:00"
4
- __git_commit__ = "eb710303965c126b83f41f180d257b258b815543"
2
+ __version__ = "3.6.13"
3
+ __build_date__ = "2026-01-23 03:49:46.368256+00:00"
4
+ __git_commit__ = "9eb03a74b91426b564a1aa313670eb08a8b9ece8"
5
5
  __dirty__ = False
@@ -4,6 +4,24 @@ from typing import Any, Union
4
4
  from prefect.filesystems import NullFileSystem, WritableFileSystem
5
5
 
6
6
 
7
+ def call_explicitly_sync_block_method(
8
+ block: Union[WritableFileSystem, NullFileSystem],
9
+ method: str,
10
+ args: tuple[Any, ...],
11
+ kwargs: dict[str, Any],
12
+ ) -> Any:
13
+ """
14
+ Call a block method synchronously.
15
+
16
+ TODO: remove this once we have explicit sync/async methods on all storage blocks
17
+
18
+ see https://github.com/PrefectHQ/prefect/issues/15008
19
+ """
20
+ # Pass _sync=True to ensure we get synchronous execution even when called
21
+ # from an async context (e.g., within a sync flow running in an async test)
22
+ return getattr(block, method)(*args, _sync=True, **kwargs)
23
+
24
+
7
25
  async def call_explicitly_async_block_method(
8
26
  block: Union[WritableFileSystem, NullFileSystem],
9
27
  method: str,
@@ -0,0 +1,29 @@
1
+ from urllib.parse import urlparse, urlunparse
2
+
3
+
4
+ def strip_auth_from_url(url: str) -> str:
5
+ """
6
+ Remove authentication credentials (username/password) from a URL.
7
+
8
+ Useful for sanitizing URLs before including them in error messages
9
+ or logs to avoid leaking secrets.
10
+ """
11
+ parsed = urlparse(url)
12
+
13
+ if not parsed.hostname:
14
+ return url
15
+
16
+ netloc = parsed.hostname
17
+ if parsed.port:
18
+ netloc = f"{netloc}:{parsed.port}"
19
+
20
+ return urlunparse(
21
+ (
22
+ parsed.scheme,
23
+ netloc,
24
+ parsed.path,
25
+ parsed.params,
26
+ parsed.query,
27
+ parsed.fragment,
28
+ )
29
+ )
prefect/_states.py CHANGED
@@ -58,10 +58,13 @@ def exception_to_crashed_state_sync(
58
58
  # calling it from sync-only code raises an error. Since anyio cancellation
59
59
  # exceptions can only occur in async contexts anyway, we can safely skip
60
60
  # this check when no async backend is running.
61
+ # anyio 4.12+ raises anyio.NoEventLoopError, older versions raise
62
+ # sniffio.AsyncLibraryNotFoundError. Catch both for compatibility.
63
+ # TODO: remove sniffio handling once anyio lower bound is >=4.12.1
61
64
  try:
62
65
  cancelled_exc_class = anyio.get_cancelled_exc_class()
63
66
  is_anyio_cancelled = isinstance(exc, cancelled_exc_class)
64
- except sniffio.AsyncLibraryNotFoundError:
67
+ except (sniffio.AsyncLibraryNotFoundError, anyio.NoEventLoopError):
65
68
  is_anyio_cancelled = False
66
69
 
67
70
  if is_anyio_cancelled:
@@ -446,22 +446,22 @@ class ConcurrencyLimitClient(BaseClient):
446
446
  existing_limit = None
447
447
 
448
448
  if not existing_limit:
449
+ create_kwargs: dict[str, Any] = {"name": name, "limit": limit}
450
+ if slot_decay_per_second is not None:
451
+ create_kwargs["slot_decay_per_second"] = slot_decay_per_second
449
452
  self.create_global_concurrency_limit(
450
- GlobalConcurrencyLimitCreate(
451
- name=name,
452
- limit=limit,
453
- slot_decay_per_second=slot_decay_per_second,
454
- )
453
+ GlobalConcurrencyLimitCreate(**create_kwargs)
455
454
  )
456
455
  elif existing_limit.limit != limit or (
457
456
  slot_decay_per_second is not None
458
457
  and existing_limit.slot_decay_per_second != slot_decay_per_second
459
458
  ):
459
+ update_kwargs: dict[str, Any] = {"limit": limit}
460
+ if slot_decay_per_second is not None:
461
+ update_kwargs["slot_decay_per_second"] = slot_decay_per_second
460
462
  self.update_global_concurrency_limit(
461
463
  name,
462
- GlobalConcurrencyLimitUpdate(
463
- limit=limit, slot_decay_per_second=slot_decay_per_second
464
- ),
464
+ GlobalConcurrencyLimitUpdate(**update_kwargs),
465
465
  )
466
466
 
467
467
  def read_global_concurrency_limits(
@@ -908,22 +908,22 @@ class ConcurrencyLimitAsyncClient(BaseAsyncClient):
908
908
  existing_limit = None
909
909
 
910
910
  if not existing_limit:
911
+ create_kwargs: dict[str, Any] = {"name": name, "limit": limit}
912
+ if slot_decay_per_second is not None:
913
+ create_kwargs["slot_decay_per_second"] = slot_decay_per_second
911
914
  await self.create_global_concurrency_limit(
912
- GlobalConcurrencyLimitCreate(
913
- name=name,
914
- limit=limit,
915
- slot_decay_per_second=slot_decay_per_second,
916
- )
915
+ GlobalConcurrencyLimitCreate(**create_kwargs)
917
916
  )
918
917
  elif existing_limit.limit != limit or (
919
918
  slot_decay_per_second is not None
920
919
  and existing_limit.slot_decay_per_second != slot_decay_per_second
921
920
  ):
921
+ update_kwargs: dict[str, Any] = {"limit": limit}
922
+ if slot_decay_per_second is not None:
923
+ update_kwargs["slot_decay_per_second"] = slot_decay_per_second
922
924
  await self.update_global_concurrency_limit(
923
925
  name,
924
- GlobalConcurrencyLimitUpdate(
925
- limit=limit, slot_decay_per_second=slot_decay_per_second
926
- ),
926
+ GlobalConcurrencyLimitUpdate(**update_kwargs),
927
927
  )
928
928
 
929
929
  async def read_global_concurrency_limits(
@@ -281,6 +281,27 @@ class FlowRunFilterIdempotencyKey(PrefectBaseModel):
281
281
  )
282
282
 
283
283
 
284
+ class FlowRunFilterCreatedBy(PrefectBaseModel, OperatorMixin):
285
+ """Filter by `FlowRun.created_by`."""
286
+
287
+ id_: Optional[List[UUID]] = Field(
288
+ default=None,
289
+ description="A list of creator IDs to include",
290
+ )
291
+ type_: Optional[List[str]] = Field(
292
+ default=None,
293
+ description=(
294
+ "A list of creator types to include. For example, 'DEPLOYMENT' for "
295
+ "scheduled runs or 'AUTOMATION' for runs triggered by automations."
296
+ ),
297
+ examples=[["DEPLOYMENT", "AUTOMATION"]],
298
+ )
299
+ is_null_: Optional[bool] = Field(
300
+ default=None,
301
+ description="If true, only include flow runs without a creator",
302
+ )
303
+
304
+
284
305
  class FlowRunFilter(PrefectBaseModel, OperatorMixin):
285
306
  """Filter flow runs. Only flow runs matching all criteria will be returned"""
286
307
 
@@ -324,6 +345,9 @@ class FlowRunFilter(PrefectBaseModel, OperatorMixin):
324
345
  idempotency_key: Optional[FlowRunFilterIdempotencyKey] = Field(
325
346
  default=None, description="Filter criteria for `FlowRun.idempotency_key`"
326
347
  )
348
+ created_by: Optional[FlowRunFilterCreatedBy] = Field(
349
+ default=None, description="Filter criteria for `FlowRun.created_by`"
350
+ )
327
351
 
328
352
 
329
353
  class TaskRunFilterFlowRunId(PrefectBaseModel):
@@ -90,7 +90,7 @@ class Subscription(Generic[S]):
90
90
  )
91
91
 
92
92
  auth: dict[str, Any] = orjson.loads(await websocket.recv())
93
- assert auth["type"] == "auth_success", auth.get("message")
93
+ assert auth["type"] == "auth_success", auth.get("reason", "")
94
94
 
95
95
  message: dict[str, Any] = {"type": "subscribe", "keys": self.keys}
96
96
  if self.client_id:
prefect/events/clients.py CHANGED
@@ -73,6 +73,17 @@ if TYPE_CHECKING:
73
73
 
74
74
  logger: "logging.Logger" = get_logger(__name__)
75
75
 
76
+ # Exceptions that indicate transient network issues and should trigger retries.
77
+ # These are used consistently across all event client retry loops.
78
+ # - ConnectionClosed: WebSocket connection was closed (e.g., server restart, load balancer timeout)
79
+ # - TimeoutError: Connection or operation timed out
80
+ # - OSError: Network-level errors (connection refused, DNS failures, network unreachable, etc.)
81
+ RETRYABLE_EXCEPTIONS: Tuple[Type[Exception], ...] = (
82
+ ConnectionClosed,
83
+ TimeoutError,
84
+ OSError,
85
+ )
86
+
76
87
 
77
88
  def http_to_ws(url: str) -> str:
78
89
  return url.replace("https://", "wss://").replace("http://", "ws://").rstrip("/")
@@ -278,14 +289,30 @@ class PrefectEventsClient(EventsClient):
278
289
  self._checkpoint_every = checkpoint_every
279
290
 
280
291
  async def __aenter__(self) -> Self:
281
- # Don't handle any errors in the initial connection, because these are most
282
- # likely a permission or configuration issue that should propagate
283
292
  await super().__aenter__()
284
- try:
285
- await self._reconnect()
286
- except Exception as e:
287
- self._log_connection_error(e)
288
- raise
293
+ # Ensure at least one connection attempt even if reconnection_attempts is negative
294
+ max_attempts = max(1, self._reconnection_attempts + 1)
295
+ for i in range(max_attempts):
296
+ try:
297
+ await self._reconnect()
298
+ break
299
+ except RETRYABLE_EXCEPTIONS as e:
300
+ logger.debug(
301
+ "Initial connection attempt %s/%s failed: %s",
302
+ i + 1,
303
+ max_attempts,
304
+ str(e),
305
+ )
306
+ if i == max_attempts - 1:
307
+ self._log_connection_error(e)
308
+ raise
309
+ if i > 2:
310
+ await asyncio.sleep(1)
311
+ except Exception as e:
312
+ # Non-retryable exceptions (config/permission issues) should
313
+ # propagate immediately with a warning
314
+ self._log_connection_error(e)
315
+ raise
289
316
  return self
290
317
 
291
318
  async def __aexit__(
@@ -295,7 +322,13 @@ class PrefectEventsClient(EventsClient):
295
322
  exc_tb: Optional[TracebackType],
296
323
  ) -> None:
297
324
  self._websocket = None
298
- await self._connect.__aexit__(exc_type, exc_val, exc_tb)
325
+ # Only call __aexit__ on the connection if it was successfully established.
326
+ # The websockets library sets the "connection" attribute on the connect
327
+ # object only after __aenter__() completes successfully. Without this guard,
328
+ # we would get an AttributeError when cleaning up a connection that failed
329
+ # during establishment.
330
+ if hasattr(self._connect, "connection"):
331
+ await self._connect.__aexit__(exc_type, exc_val, exc_tb)
299
332
  return await super().__aexit__(exc_type, exc_val, exc_tb)
300
333
 
301
334
  def _log_debug(self, message: str, *args: Any, **kwargs: Any) -> None:
@@ -402,8 +435,8 @@ class PrefectEventsClient(EventsClient):
402
435
  await self._checkpoint()
403
436
 
404
437
  return
405
- except ConnectionClosed as e:
406
- self._log_debug("Got ConnectionClosed error.")
438
+ except RETRYABLE_EXCEPTIONS as e:
439
+ self._log_debug("Got retryable error: %s", type(e).__name__)
407
440
  if i == self._reconnection_attempts:
408
441
  # this was our final chance, log warning and raise
409
442
  self._log_connection_error(e)
@@ -643,7 +676,13 @@ class PrefectEventSubscriber:
643
676
  exc_tb: Optional[TracebackType],
644
677
  ) -> None:
645
678
  self._websocket = None
646
- await self._connect.__aexit__(exc_type, exc_val, exc_tb)
679
+ # Only call __aexit__ on the connection if it was successfully established.
680
+ # The websockets library sets the "connection" attribute on the connect
681
+ # object only after __aenter__() completes successfully. Without this guard,
682
+ # we would get an AttributeError when cleaning up a connection that failed
683
+ # during establishment.
684
+ if hasattr(self._connect, "connection"):
685
+ await self._connect.__aexit__(exc_type, exc_val, exc_tb)
647
686
 
648
687
  def __aiter__(self) -> Self:
649
688
  return self
@@ -681,9 +720,9 @@ class PrefectEventSubscriber:
681
720
  except ConnectionClosedOK:
682
721
  logger.debug('Connection closed with "OK" status')
683
722
  raise StopAsyncIteration
684
- except ConnectionClosed:
723
+ except RETRYABLE_EXCEPTIONS:
685
724
  logger.debug(
686
- "Connection closed with %s/%s attempts",
725
+ "Retryable error with %s/%s attempts",
687
726
  i + 1,
688
727
  self._reconnection_attempts,
689
728
  )
prefect/flow_engine.py CHANGED
@@ -5,6 +5,7 @@ import logging
5
5
  import multiprocessing
6
6
  import multiprocessing.context
7
7
  import os
8
+ import threading
8
9
  import time
9
10
  from contextlib import (
10
11
  AsyncExitStack,
@@ -36,7 +37,7 @@ from anyio import CancelScope
36
37
  from opentelemetry import propagate, trace
37
38
  from typing_extensions import ParamSpec
38
39
 
39
- from prefect import Task
40
+ from prefect import Task, __version__
40
41
  from prefect.client.orchestration import PrefectClient, SyncPrefectClient, get_client
41
42
  from prefect.client.schemas import FlowRun, TaskRun
42
43
  from prefect.client.schemas.filters import FlowRunFilter
@@ -60,6 +61,8 @@ from prefect.context import (
60
61
  serialize_context,
61
62
  )
62
63
  from prefect.engine import handle_engine_signals
64
+ from prefect.events.related import RelatedResource, tags_as_related_resources
65
+ from prefect.events.utilities import emit_event
63
66
  from prefect.exceptions import (
64
67
  Abort,
65
68
  MissingFlowError,
@@ -161,6 +164,129 @@ def load_flow_and_flow_run(flow_run_id: UUID) -> tuple[FlowRun, Flow[..., Any]]:
161
164
  return flow_run, flow
162
165
 
163
166
 
167
+ @contextmanager
168
+ def send_heartbeats_sync(
169
+ engine: "FlowRunEngine[Any, Any]",
170
+ ) -> Generator[None, None, None]:
171
+ """Context manager that maintains heartbeats for a sync flow run.
172
+
173
+ Heartbeats are emitted at regular intervals while the flow is running.
174
+ The loop checks the flow run state before each heartbeat and stops
175
+ if the run reaches a terminal state.
176
+
177
+ Args:
178
+ engine: The FlowRunEngine instance to emit heartbeats for.
179
+
180
+ Yields:
181
+ None
182
+ """
183
+ heartbeat_seconds = engine.heartbeat_seconds
184
+ if heartbeat_seconds is None:
185
+ yield
186
+ return
187
+
188
+ stop_event = threading.Event()
189
+
190
+ def heartbeat_loop() -> None:
191
+ while not stop_event.is_set():
192
+ # Check state before emitting - don't emit if final
193
+ if (
194
+ engine.flow_run
195
+ and engine.flow_run.state
196
+ and engine.flow_run.state.is_final()
197
+ ):
198
+ engine.logger.debug("Flow run in terminal state, stopping heartbeat")
199
+ return
200
+
201
+ try:
202
+ engine._emit_flow_run_heartbeat()
203
+ except Exception:
204
+ engine.logger.debug("Failed to emit heartbeat", exc_info=True)
205
+
206
+ # Sleep in increments to allow quick shutdown
207
+ for _ in range(heartbeat_seconds):
208
+ if stop_event.is_set():
209
+ return
210
+ time.sleep(1)
211
+
212
+ thread = threading.Thread(target=heartbeat_loop, daemon=True)
213
+ thread.start()
214
+ engine.logger.debug("Started flow run heartbeat context")
215
+
216
+ try:
217
+ yield
218
+ finally:
219
+ stop_event.set()
220
+ thread.join(timeout=2)
221
+ engine.logger.debug("Stopped flow run heartbeat context")
222
+
223
+
224
+ @asynccontextmanager
225
+ async def send_heartbeats_async(
226
+ engine: "AsyncFlowRunEngine[Any, Any]",
227
+ ) -> AsyncGenerator[None, None]:
228
+ """Async context manager that maintains heartbeats for an async flow run.
229
+
230
+ Heartbeats are emitted at regular intervals while the flow is running.
231
+ The loop checks the flow run state before each heartbeat and stops
232
+ if the run reaches a terminal state.
233
+
234
+ Args:
235
+ engine: The AsyncFlowRunEngine instance to emit heartbeats for.
236
+
237
+ Yields:
238
+ None
239
+ """
240
+ heartbeat_seconds = engine.heartbeat_seconds
241
+ if heartbeat_seconds is None:
242
+ yield
243
+ return
244
+
245
+ stop_flag = False
246
+
247
+ async def heartbeat_loop() -> None:
248
+ nonlocal stop_flag
249
+ try:
250
+ while not stop_flag:
251
+ # Check state before emitting - don't emit if final
252
+ if (
253
+ engine.flow_run
254
+ and engine.flow_run.state
255
+ and engine.flow_run.state.is_final()
256
+ ):
257
+ engine.logger.debug(
258
+ "Flow run in terminal state, stopping heartbeat"
259
+ )
260
+ return
261
+
262
+ try:
263
+ engine._emit_flow_run_heartbeat()
264
+ except Exception:
265
+ engine.logger.debug("Failed to emit heartbeat", exc_info=True)
266
+
267
+ # Sleep in increments to allow quick shutdown (parity with sync version)
268
+ for _ in range(heartbeat_seconds):
269
+ if stop_flag:
270
+ return
271
+ await asyncio.sleep(1)
272
+ except asyncio.CancelledError:
273
+ engine.logger.debug("Heartbeat loop cancelled")
274
+
275
+ task = asyncio.create_task(heartbeat_loop())
276
+ engine.logger.debug("Started flow run heartbeat context")
277
+
278
+ try:
279
+ yield
280
+ finally:
281
+ stop_flag = True
282
+ task.cancel()
283
+ try:
284
+ await task
285
+ except asyncio.CancelledError:
286
+ pass
287
+ engine.logger.debug("Stopped flow run heartbeat context")
288
+
289
+
164
290
  @dataclass
165
291
  class BaseFlowRunEngine(Generic[P, R]):
166
292
  flow: Union[Flow[P, R], Flow[P, Coroutine[Any, Any, R]]]
@@ -200,10 +326,59 @@ class BaseFlowRunEngine(Generic[P, R]):
200
326
  return False # TODO: handle this differently?
201
327
  return getattr(self, "flow_run").state.is_pending()
202
328
 
329
+ @property
330
+ def heartbeat_seconds(self) -> Optional[int]:
331
+ """Get the heartbeat interval from settings."""
332
+ return get_current_settings().flows.heartbeat_frequency
333
+
203
334
  def cancel_all_tasks(self) -> None:
204
335
  if hasattr(self.flow.task_runner, "cancel_all"):
205
336
  self.flow.task_runner.cancel_all() # type: ignore
206
337
 
338
+ def _emit_flow_run_heartbeat(self) -> None:
339
+ """Emit a heartbeat event for the current flow run."""
340
+ if not self.flow_run:
341
+ return
342
+
343
+ related: list[RelatedResource] = []
344
+ tags: list[str] = list(self.flow_run.tags or [])
345
+
346
+ # Add flow as related resource using flow_id for consistency with other events
347
+ if self.flow_run.flow_id:
348
+ related.append(
349
+ RelatedResource.model_validate(
350
+ {
351
+ "prefect.resource.id": f"prefect.flow.{self.flow_run.flow_id}",
352
+ "prefect.resource.role": "flow",
353
+ "prefect.resource.name": self.flow.name if self.flow else "",
354
+ }
355
+ )
356
+ )
357
+
358
+ # Add deployment as related resource if available
359
+ # Note: deployment name is not available on flow_run without an API call
360
+ if self.flow_run.deployment_id:
361
+ related.append(
362
+ RelatedResource.model_validate(
363
+ {
364
+ "prefect.resource.id": f"prefect.deployment.{self.flow_run.deployment_id}",
365
+ "prefect.resource.role": "deployment",
366
+ }
367
+ )
368
+ )
369
+
370
+ related += tags_as_related_resources(set(tags))
371
+
372
+ emit_event(
373
+ event="prefect.flow-run.heartbeat",
374
+ resource={
375
+ "prefect.resource.id": f"prefect.flow-run.{self.flow_run.id}",
376
+ "prefect.resource.name": self.flow_run.name or "",
377
+ "prefect.version": __version__,
378
+ },
379
+ related=related,
380
+ )
381
+
207
382
  def _update_otel_labels(
208
383
  self, span: trace.Span, client: Union[SyncPrefectClient, PrefectClient]
209
384
  ):
@@ -343,6 +518,7 @@ class FlowRunEngine(BaseFlowRunEngine[P, R]):
343
518
 
344
519
  self._telemetry.update_state(state)
345
520
  self.call_hooks(state)
521
+
346
522
  return state
347
523
 
348
524
  def result(self, raise_on_failure: bool = True) -> "Union[R, State, None]":
@@ -791,10 +967,11 @@ class FlowRunEngine(BaseFlowRunEngine[P, R]):
791
967
  seconds=self.flow.timeout_seconds,
792
968
  timeout_exc_type=FlowRunTimeoutError,
793
969
  ):
794
- self.logger.debug(
795
- f"Executing flow {self.flow.name!r} for flow run {self.flow_run.name!r}..."
796
- )
797
- yield self
970
+ with send_heartbeats_sync(self):
971
+ self.logger.debug(
972
+ f"Executing flow {self.flow.name!r} for flow run {self.flow_run.name!r}..."
973
+ )
974
+ yield self
798
975
  except TimeoutError as exc:
799
976
  self.handle_timeout(exc)
800
977
  except Exception as exc:
@@ -930,6 +1107,7 @@ class AsyncFlowRunEngine(BaseFlowRunEngine[P, R]):
930
1107
 
931
1108
  self._telemetry.update_state(state)
932
1109
  await self.call_hooks(state)
1110
+
933
1111
  return state
934
1112
 
935
1113
  async def result(self, raise_on_failure: bool = True) -> "Union[R, State, None]":
@@ -1381,10 +1559,11 @@ class AsyncFlowRunEngine(BaseFlowRunEngine[P, R]):
1381
1559
  seconds=self.flow.timeout_seconds,
1382
1560
  timeout_exc_type=FlowRunTimeoutError,
1383
1561
  ):
1384
- self.logger.debug(
1385
- f"Executing flow {self.flow.name!r} for flow run {self.flow_run.name!r}..."
1386
- )
1387
- yield self
1562
+ async with send_heartbeats_async(self):
1563
+ self.logger.debug(
1564
+ f"Executing flow {self.flow.name!r} for flow run {self.flow_run.name!r}..."
1565
+ )
1566
+ yield self
1388
1567
  except TimeoutError as exc:
1389
1568
  await self.handle_timeout(exc)
1390
1569
  except Exception as exc:
@@ -1614,6 +1793,7 @@ def run_flow_in_subprocess(
1614
1793
  parameters: dict[str, Any] | None = None,
1615
1794
  wait_for: Iterable[PrefectFuture[Any]] | None = None,
1616
1795
  context: dict[str, Any] | None = None,
1796
+ env: dict[str, str] | None = None,
1617
1797
  ) -> multiprocessing.context.SpawnProcess:
1618
1798
  """
1619
1799
  Run a flow in a subprocess.
@@ -1630,6 +1810,7 @@ def run_flow_in_subprocess(
1630
1810
  the current context will be used. A serialized context should be provided if
1631
1811
  this function is called in a separate memory space from the parent run (e.g.
1632
1812
  in a subprocess or on another machine).
1813
+ env: Additional environment variables to set in the subprocess.
1633
1814
 
1634
1815
  Returns:
1635
1816
  A multiprocessing.context.SpawnProcess representing the process that is running the flow.
@@ -1672,7 +1853,8 @@ def run_flow_in_subprocess(
1672
1853
  | {
1673
1854
  # TODO: make this a thing we can pass into the engine
1674
1855
  "PREFECT__ENABLE_CANCELLATION_AND_CRASHED_HOOKS": "false",
1675
- },
1856
+ }
1857
+ | (env or {}),
1676
1858
  flow=flow,
1677
1859
  flow_run=flow_run,
1678
1860
  parameters=parameters,
prefect/flows.py CHANGED
@@ -1717,8 +1717,7 @@ class Flow(Generic[P, R]):
1717
1717
  return_type=return_type,
1718
1718
  )
1719
1719
 
1720
- @sync_compatible
1721
- async def visualize(self, *args: "P.args", **kwargs: "P.kwargs"):
1720
+ async def avisualize(self, *args: "P.args", **kwargs: "P.kwargs") -> None:
1722
1721
  """
1723
1722
  Generates a graphviz object representing the current flow. In IPython notebooks,
1724
1723
  it's rendered inline, otherwise in a new window as a PNG.
@@ -1776,6 +1775,66 @@ class Flow(Generic[P, R]):
1776
1775
  new_exception.__traceback__ = e.__traceback__
1777
1776
  raise new_exception
1778
1777
 
1778
+ @async_dispatch(avisualize)
1779
+ def visualize(self, *args: "P.args", **kwargs: "P.kwargs") -> None:
1780
+ """
1781
+ Generates a graphviz object representing the current flow. In IPython notebooks,
1782
+ it's rendered inline, otherwise in a new window as a PNG.
1783
+
1784
+ Raises:
1785
+ - ImportError: If `graphviz` isn't installed.
1786
+ - GraphvizExecutableNotFoundError: If the `dot` executable isn't found.
1787
+ - FlowVisualizationError: If the flow can't be visualized for any other reason.
1788
+ """
1789
+ from prefect.utilities.visualization import (
1790
+ FlowVisualizationError,
1791
+ GraphvizExecutableNotFoundError,
1792
+ GraphvizImportError,
1793
+ TaskVizTracker,
1794
+ VisualizationUnsupportedError,
1795
+ build_task_dependencies,
1796
+ visualize_task_dependencies,
1797
+ )
1798
+
1799
+ if not PREFECT_TESTING_UNIT_TEST_MODE:
1800
+ warnings.warn(
1801
+ "`flow.visualize()` will execute code inside of your flow that is not"
1802
+ " decorated with `@task` or `@flow`."
1803
+ )
1804
+
1805
+ try:
1806
+ with TaskVizTracker() as tracker:
1807
+ if self.isasync:
1808
+ # Run async flow via event loop
1809
+ run_coro_as_sync(self.fn(*args, **kwargs))
1810
+ else:
1811
+ self.fn(*args, **kwargs)
1812
+
1813
+ graph = build_task_dependencies(tracker)
1814
+
1815
+ visualize_task_dependencies(graph, self.name)
1816
+
1817
+ except GraphvizImportError:
1818
+ raise
1819
+ except GraphvizExecutableNotFoundError:
1820
+ raise
1821
+ except VisualizationUnsupportedError:
1822
+ raise
1823
+ except FlowVisualizationError:
1824
+ raise
1825
+ except Exception as e:
1826
+ msg = (
1827
+ "It's possible you are trying to visualize a flow that contains "
1828
+ "code that directly interacts with the result of a task"
1829
+ " inside of the flow. \nTry passing a `viz_return_value` "
1830
+ "to the task decorator, e.g. `@task(viz_return_value=[1, 2, 3]).`"
1831
+ )
1832
+
1833
+ new_exception = type(e)(str(e) + "\n" + msg)
1834
+ # Copy traceback information from the original exception
1835
+ new_exception.__traceback__ = e.__traceback__
1836
+ raise new_exception
1837
+
1779
1838
 
1780
1839
  class FlowDecorator:
1781
1840
  @overload