mage-ai 0.9.74__py3-none-any.whl → 0.9.79__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 (420) hide show
  1. mage_ai/ai/llm_pipeline_wizard.py +6 -4
  2. mage_ai/ai/openai_client.py +7 -5
  3. mage_ai/api/policies/PipelineSchedulePolicy.py +1 -0
  4. mage_ai/api/presenters/PipelineSchedulePresenter.py +11 -2
  5. mage_ai/api/resources/GitFileResource.py +8 -0
  6. mage_ai/api/resources/PipelineScheduleResource.py +20 -14
  7. mage_ai/api/resources/PipelineTriggerResource.py +3 -1
  8. mage_ai/api/resources/SessionResource.py +2 -2
  9. mage_ai/api/resources/SyncResource.py +1 -1
  10. mage_ai/api/resources/UserResource.py +1 -1
  11. mage_ai/cli/main.py +8 -1
  12. mage_ai/data_cleaner/analysis/charts.py +1 -1
  13. mage_ai/data_cleaner/cleaning_rules/reformat_values.py +1 -1
  14. mage_ai/data_integrations/destinations/constants.py +3 -0
  15. mage_ai/data_integrations/sources/constants.py +2 -0
  16. mage_ai/data_preparation/executors/block_executor.py +8 -3
  17. mage_ai/data_preparation/executors/pipeline_executor.py +35 -19
  18. mage_ai/data_preparation/git/utils.py +2 -2
  19. mage_ai/data_preparation/logging/logger_manager.py +31 -2
  20. mage_ai/data_preparation/models/block/__init__.py +33 -27
  21. mage_ai/data_preparation/models/block/dbt/dbt_adapter.py +20 -8
  22. mage_ai/data_preparation/models/block/dynamic/constants.py +0 -1
  23. mage_ai/data_preparation/models/block/dynamic/counter.py +1 -3
  24. mage_ai/data_preparation/models/block/outputs.py +7 -1
  25. mage_ai/data_preparation/models/block/r/__init__.py +16 -5
  26. mage_ai/data_preparation/models/block/sql/__init__.py +2 -0
  27. mage_ai/data_preparation/models/block/sql/mssql.py +8 -0
  28. mage_ai/data_preparation/models/block/sql/utils/shared.py +6 -2
  29. mage_ai/data_preparation/models/constants.py +4 -1
  30. mage_ai/data_preparation/models/pipeline.py +11 -2
  31. mage_ai/data_preparation/models/project/__init__.py +3 -1
  32. mage_ai/data_preparation/models/triggers/__init__.py +1 -1
  33. mage_ai/data_preparation/storage/local_storage.py +4 -1
  34. mage_ai/data_preparation/templates/constants.py +7 -0
  35. mage_ai/data_preparation/templates/data_exporters/streaming/elasticsearch.yaml +3 -0
  36. mage_ai/data_preparation/templates/data_loaders/airtable.py +28 -0
  37. mage_ai/data_preparation/templates/data_loaders/streaming/nats.yaml +6 -3
  38. mage_ai/data_preparation/templates/repo/io_config.yaml +2 -0
  39. mage_ai/io/airtable.py +104 -0
  40. mage_ai/io/base.py +30 -1
  41. mage_ai/io/bigquery.py +36 -0
  42. mage_ai/io/config.py +6 -0
  43. mage_ai/io/mssql.py +21 -9
  44. mage_ai/io/mysql.py +6 -1
  45. mage_ai/io/oracledb.py +2 -4
  46. mage_ai/io/postgres.py +41 -19
  47. mage_ai/io/qdrant.py +1 -1
  48. mage_ai/io/redshift.py +13 -0
  49. mage_ai/io/sql.py +1 -0
  50. mage_ai/io/utils.py +18 -0
  51. mage_ai/orchestration/db/__init__.py +23 -3
  52. mage_ai/orchestration/db/migrations/versions/39d36f1dab73_create_genericjob.py +47 -0
  53. mage_ai/orchestration/db/models/oauth.py +2 -1
  54. mage_ai/orchestration/db/models/schedules.py +108 -6
  55. mage_ai/orchestration/db/models/schedules_project_platform.py +1 -1
  56. mage_ai/orchestration/db/models/secrets.py +11 -1
  57. mage_ai/orchestration/job_manager.py +19 -0
  58. mage_ai/orchestration/metrics/pipeline_run.py +1 -1
  59. mage_ai/orchestration/notification/sender.py +2 -2
  60. mage_ai/orchestration/pipeline_scheduler_original.py +150 -6
  61. mage_ai/orchestration/pipeline_scheduler_project_platform.py +4 -5
  62. mage_ai/orchestration/queue/config.py +11 -1
  63. mage_ai/orchestration/queue/process_queue.py +4 -0
  64. mage_ai/orchestration/utils/distributed_lock.py +8 -1
  65. mage_ai/orchestration/utils/resources.py +56 -2
  66. mage_ai/sample_datasets/salary_survey.csv +52 -52
  67. mage_ai/server/api/base.py +41 -0
  68. mage_ai/server/api/constants.py +1 -0
  69. mage_ai/server/api/triggers.py +9 -0
  70. mage_ai/server/constants.py +1 -1
  71. mage_ai/server/frontend_dist/404.html +3 -3
  72. mage_ai/server/frontend_dist/_next/static/TUo4RceCdMufBTBTq8CAq/_buildManifest.js +1 -0
  73. mage_ai/server/frontend_dist/_next/static/chunks/{1187-839336d276186105.js → 1187-4560c3895e1d7099.js} +1 -1
  74. mage_ai/server/frontend_dist/_next/static/chunks/{1598-0adca9dce3ba4c60.js → 1598-cbf3f5a6078fc3f5.js} +1 -1
  75. mage_ai/server/frontend_dist/_next/static/chunks/2717-638a944d24d5abde.js +1 -0
  76. mage_ai/server/frontend_dist/_next/static/chunks/3548-36f746b1824004f2.js +1 -0
  77. mage_ai/server/frontend_dist/_next/static/chunks/{3763-39a5174f6a3924db.js → 3763-aabe2703076636b0.js} +1 -1
  78. mage_ai/server/frontend_dist/_next/static/chunks/3782-3e2acb5ed45b582b.js +1 -0
  79. mage_ai/server/frontend_dist/_next/static/chunks/449-5e2253c6aba42557.js +1 -0
  80. mage_ai/server/{frontend_dist_base_path_template/_next/static/chunks/5627-d5e559859dd0e1e0.js → frontend_dist/_next/static/chunks/5627-10e76bafa5a26f5f.js} +1 -1
  81. mage_ai/server/frontend_dist/_next/static/chunks/{5699-e49718dfc9eb2854.js → 5699-e99379e332bd0b41.js} +1 -1
  82. mage_ai/server/frontend_dist/_next/static/chunks/{7966-163da2621b8c987c.js → 7966-a5a7db345ce81263.js} +1 -1
  83. mage_ai/server/frontend_dist/_next/static/chunks/pages/_app-b697b35dfc4e6e26.js +2 -0
  84. mage_ai/server/{frontend_dist_base_path_template/_next/static/chunks/pages/compute-ed67fa8e81662e8b.js → frontend_dist/_next/static/chunks/pages/compute-9e2dea78024e3bb4.js} +1 -1
  85. mage_ai/server/{frontend_dist_base_path_template/_next/static/chunks/pages/files-e0ecd7ced09a63b2.js → frontend_dist/_next/static/chunks/pages/files-e08c7fe76f968f9c.js} +1 -1
  86. mage_ai/server/frontend_dist/_next/static/chunks/pages/global-data-products/{[...slug]-c7a729477ecda50e.js → [...slug]-30c3807057a4e65b.js} +1 -1
  87. mage_ai/server/frontend_dist/_next/static/chunks/pages/{global-data-products-fd6ae6a358a60a0c.js → global-data-products-8dcb3b31af9e0e39.js} +1 -1
  88. mage_ai/server/{frontend_dist_base_path_template/_next/static/chunks/pages/global-hooks/[...slug]-8e50243797a7fe59.js → frontend_dist/_next/static/chunks/pages/global-hooks/[...slug]-85a64b64d27214b6.js} +1 -1
  89. mage_ai/server/frontend_dist/_next/static/chunks/pages/{global-hooks-d0c003446332dc0d.js → global-hooks-4ff959d51b8a9502.js} +1 -1
  90. mage_ai/server/frontend_dist/_next/static/chunks/pages/manage/{files-a69ed8e9f814490c.js → files-d08a460641d0efaa.js} +1 -1
  91. mage_ai/server/frontend_dist/_next/static/chunks/pages/manage/{overview-1aad7093c6d39257.js → overview-aae747f487e08d51.js} +1 -1
  92. mage_ai/server/frontend_dist/_next/static/chunks/pages/manage/{pipeline-runs-528d30e0d13b0cc7.js → pipeline-runs-09a842d64a6ada62.js} +1 -1
  93. mage_ai/server/frontend_dist/_next/static/chunks/pages/manage/{settings-fb9201d9cf63031d.js → settings-2e98e57d9376a458.js} +1 -1
  94. mage_ai/server/frontend_dist/_next/static/chunks/pages/manage/users/{[user]-000f5a980a07da39.js → [user]-7be6e41ad66089bb.js} +1 -1
  95. mage_ai/server/{frontend_dist_base_path_template/_next/static/chunks/pages/manage/users/new-e4e613f6e817a733.js → frontend_dist/_next/static/chunks/pages/manage/users/new-4c088833063bfa07.js} +1 -1
  96. mage_ai/server/frontend_dist/_next/static/chunks/pages/manage/users-5db54821a3059c69.js +1 -0
  97. mage_ai/server/{frontend_dist_base_path_template/_next/static/chunks/pages/manage-34d718b8a4066c23.js → frontend_dist/_next/static/chunks/pages/manage-868fcd8cbeb265f0.js} +1 -1
  98. mage_ai/server/frontend_dist/_next/static/chunks/pages/{oauth-3bfd1b8d7f036726.js → oauth-6ceceb62191dfe8a.js} +1 -1
  99. mage_ai/server/frontend_dist/_next/static/chunks/pages/overview-f65416f6dbe30ad3.js +1 -0
  100. mage_ai/server/frontend_dist/_next/static/chunks/pages/{pipeline-runs-5f8c100e648efa8a.js → pipeline-runs-2d0136b51b57de93.js} +1 -1
  101. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/backfills/{[...slug]-688c652f3296bb9c.js → [...slug]-1ad5238742e25b4c.js} +1 -1
  102. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/backfills-bd11e87d026bfbf9.js +1 -0
  103. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/{dashboard-1236e36d39b1637d.js → dashboard-0f4f47f721b0723f.js} +1 -1
  104. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/edit-5ae8efe9e0530212.js +1 -0
  105. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/logs-fe91dfb0091f6bc6.js +1 -0
  106. mage_ai/server/{frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/monitors/block-runs-0d68d4bf6290fefb.js → frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/monitors/block-runs-cf794b2d22a80f31.js} +1 -1
  107. mage_ai/server/{frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/monitors/block-runtime-9254358d58f07714.js → frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/monitors/block-runtime-a964caef91bed9e1.js} +1 -1
  108. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/{monitors-821001e690caebe2.js → monitors-80bebb4401eefe25.js} +1 -1
  109. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/runs/[run]-2eae7cb017027682.js +1 -0
  110. mage_ai/server/{frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/runs-2d4b2a0800a66b33.js → frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/runs-776b2e5b0b6ceba8.js} +1 -1
  111. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/settings-03d9bca3bc5e6088.js +1 -0
  112. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/syncs-707ed8ca942ca802.js +1 -0
  113. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/triggers/{[...slug]-259143ed3cf59e31.js → [...slug]-8429f17d4146e1ec.js} +1 -1
  114. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/triggers-193045d9836d8d80.js +1 -0
  115. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines-d25d07db166cbb04.js +1 -0
  116. mage_ai/server/frontend_dist/_next/static/chunks/pages/platform/global-hooks/{[...slug]-5eeec927e4202b63.js → [...slug]-6834ae87bd668cb2.js} +1 -1
  117. mage_ai/server/{frontend_dist_base_path_template/_next/static/chunks/pages/platform/global-hooks-fbe9ad995d46d837.js → frontend_dist/_next/static/chunks/pages/platform/global-hooks-b3f7309a23e592b2.js} +1 -1
  118. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/account/{profile-fc659962d4015cb3.js → profile-f8b7374385e1f1bf.js} +1 -1
  119. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/platform/preferences-8de68502a9afa299.js +1 -0
  120. mage_ai/server/{frontend_dist_base_path_template/_next/static/chunks/pages/settings/platform/settings-a4f88c334414402b.js → frontend_dist/_next/static/chunks/pages/settings/platform/settings-50fb6a34f3913f1f.js} +1 -1
  121. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/permissions/{[...slug]-4deb9579ef99a3c6.js → [...slug]-2e5c098c21ea32b7.js} +1 -1
  122. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/{permissions-e0cda2f2bfce8d61.js → permissions-54e4b15b9585bfc4.js} +1 -1
  123. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/preferences-040f83d75d0f6537.js +1 -0
  124. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/roles/{[...slug]-910257d16c604ebd.js → [...slug]-95088f43034e3c95.js} +1 -1
  125. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/{roles-4f7a0756806cee34.js → roles-e9149e1fcf218f42.js} +1 -1
  126. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/{sync-data-208d6f955204d704.js → sync-data-75b67ae4a00818ef.js} +1 -1
  127. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/users/{[...slug]-c89dc67e5a1706a8.js → [...slug]-557dda05ca6c6124.js} +1 -1
  128. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/users-fa61dc6c1370e6a5.js +1 -0
  129. mage_ai/server/frontend_dist/_next/static/chunks/pages/{sign-in-054b33312d3193c3.js → sign-in-593c40985d63fcf7.js} +1 -1
  130. mage_ai/server/frontend_dist/_next/static/chunks/pages/templates/{[...slug]-b6ed6a5d818bfd20.js → [...slug]-252c4b6b818345d5.js} +1 -1
  131. mage_ai/server/frontend_dist/_next/static/chunks/pages/{templates-852357bc983af2ea.js → templates-ca528bc607753ab8.js} +1 -1
  132. mage_ai/server/{frontend_dist_base_path_template/_next/static/chunks/pages/terminal-1f9c56d671bbc67d.js → frontend_dist/_next/static/chunks/pages/terminal-287362c1defcc96b.js} +1 -1
  133. mage_ai/server/frontend_dist/_next/static/chunks/pages/test-2f83af8c9f1378fe.js +1 -0
  134. mage_ai/server/frontend_dist/_next/static/chunks/pages/triggers-d9de73fb799efed8.js +1 -0
  135. mage_ai/server/{frontend_dist_base_path_template/_next/static/chunks/pages/version-control-ae3469b992a341d6.js → frontend_dist/_next/static/chunks/pages/version-control-573f0225d7a703ed.js} +1 -1
  136. mage_ai/server/frontend_dist/block-layout.html +3 -3
  137. mage_ai/server/frontend_dist/compute.html +6 -6
  138. mage_ai/server/frontend_dist/files.html +6 -6
  139. mage_ai/server/frontend_dist/global-data-products/[...slug].html +6 -6
  140. mage_ai/server/frontend_dist/global-data-products.html +6 -6
  141. mage_ai/server/frontend_dist/global-hooks/[...slug].html +6 -6
  142. mage_ai/server/frontend_dist/global-hooks.html +6 -6
  143. mage_ai/server/frontend_dist/index.html +3 -3
  144. mage_ai/server/frontend_dist/manage/files.html +6 -6
  145. mage_ai/server/frontend_dist/manage/overview.html +6 -6
  146. mage_ai/server/frontend_dist/manage/pipeline-runs.html +6 -6
  147. mage_ai/server/frontend_dist/manage/settings.html +6 -6
  148. mage_ai/server/frontend_dist/manage/users/[user].html +6 -6
  149. mage_ai/server/frontend_dist/manage/users/new.html +6 -6
  150. mage_ai/server/frontend_dist/manage/users.html +6 -6
  151. mage_ai/server/frontend_dist/manage.html +6 -6
  152. mage_ai/server/frontend_dist/oauth.html +5 -5
  153. mage_ai/server/frontend_dist/overview.html +6 -6
  154. mage_ai/server/frontend_dist/pipeline-runs.html +6 -6
  155. mage_ai/server/frontend_dist/pipelines/[pipeline]/backfills/[...slug].html +6 -6
  156. mage_ai/server/frontend_dist/pipelines/[pipeline]/backfills.html +6 -6
  157. mage_ai/server/frontend_dist/pipelines/[pipeline]/dashboard.html +6 -6
  158. mage_ai/server/frontend_dist/pipelines/[pipeline]/edit.html +3 -3
  159. mage_ai/server/frontend_dist/pipelines/[pipeline]/logs.html +6 -6
  160. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors/block-runs.html +6 -6
  161. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors/block-runtime.html +6 -6
  162. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors.html +6 -6
  163. mage_ai/server/frontend_dist/pipelines/[pipeline]/runs/[run].html +6 -6
  164. mage_ai/server/frontend_dist/pipelines/[pipeline]/runs.html +6 -6
  165. mage_ai/server/frontend_dist/pipelines/[pipeline]/settings.html +6 -6
  166. mage_ai/server/frontend_dist/pipelines/[pipeline]/syncs.html +6 -6
  167. mage_ai/server/frontend_dist/pipelines/[pipeline]/triggers/[...slug].html +6 -6
  168. mage_ai/server/frontend_dist/pipelines/[pipeline]/triggers.html +6 -6
  169. mage_ai/server/frontend_dist/pipelines/[pipeline].html +3 -3
  170. mage_ai/server/frontend_dist/pipelines.html +6 -6
  171. mage_ai/server/frontend_dist/platform/global-hooks/[...slug].html +6 -6
  172. mage_ai/server/frontend_dist/platform/global-hooks.html +6 -6
  173. mage_ai/server/frontend_dist/settings/account/profile.html +6 -6
  174. mage_ai/server/frontend_dist/settings/platform/preferences.html +6 -6
  175. mage_ai/server/frontend_dist/settings/platform/settings.html +6 -6
  176. mage_ai/server/frontend_dist/settings/workspace/permissions/[...slug].html +6 -6
  177. mage_ai/server/frontend_dist/settings/workspace/permissions.html +6 -6
  178. mage_ai/server/frontend_dist/settings/workspace/preferences.html +6 -6
  179. mage_ai/server/frontend_dist/settings/workspace/roles/[...slug].html +6 -6
  180. mage_ai/server/frontend_dist/settings/workspace/roles.html +6 -6
  181. mage_ai/server/frontend_dist/settings/workspace/sync-data.html +6 -6
  182. mage_ai/server/frontend_dist/settings/workspace/users/[...slug].html +6 -6
  183. mage_ai/server/frontend_dist/settings/workspace/users.html +6 -6
  184. mage_ai/server/frontend_dist/settings.html +3 -3
  185. mage_ai/server/frontend_dist/sign-in.html +7 -7
  186. mage_ai/server/frontend_dist/templates/[...slug].html +6 -6
  187. mage_ai/server/frontend_dist/templates.html +6 -6
  188. mage_ai/server/frontend_dist/terminal.html +6 -6
  189. mage_ai/server/frontend_dist/test.html +3 -3
  190. mage_ai/server/frontend_dist/triggers.html +6 -6
  191. mage_ai/server/frontend_dist/v2/canvas.html +2 -2
  192. mage_ai/server/frontend_dist/v2.html +2 -2
  193. mage_ai/server/frontend_dist/version-control.html +6 -6
  194. mage_ai/server/frontend_dist_base_path_template/404.html +3 -3
  195. mage_ai/server/frontend_dist_base_path_template/_next/static/2QL-FT4lFR0a9bDZ7lNd9/_buildManifest.js +1 -0
  196. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/{1187-839336d276186105.js → 1187-4560c3895e1d7099.js} +1 -1
  197. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/{1598-0adca9dce3ba4c60.js → 1598-cbf3f5a6078fc3f5.js} +1 -1
  198. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/2717-638a944d24d5abde.js +1 -0
  199. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/3548-36f746b1824004f2.js +1 -0
  200. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/{3763-39a5174f6a3924db.js → 3763-aabe2703076636b0.js} +1 -1
  201. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/3782-3e2acb5ed45b582b.js +1 -0
  202. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/449-5e2253c6aba42557.js +1 -0
  203. mage_ai/server/{frontend_dist/_next/static/chunks/5627-d5e559859dd0e1e0.js → frontend_dist_base_path_template/_next/static/chunks/5627-10e76bafa5a26f5f.js} +1 -1
  204. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/{5699-e49718dfc9eb2854.js → 5699-e99379e332bd0b41.js} +1 -1
  205. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/{7966-163da2621b8c987c.js → 7966-a5a7db345ce81263.js} +1 -1
  206. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/_app-f205accb03b9ff43.js +2 -0
  207. mage_ai/server/{frontend_dist/_next/static/chunks/pages/compute-ed67fa8e81662e8b.js → frontend_dist_base_path_template/_next/static/chunks/pages/compute-9e2dea78024e3bb4.js} +1 -1
  208. mage_ai/server/{frontend_dist/_next/static/chunks/pages/files-e0ecd7ced09a63b2.js → frontend_dist_base_path_template/_next/static/chunks/pages/files-e08c7fe76f968f9c.js} +1 -1
  209. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/global-data-products/{[...slug]-c7a729477ecda50e.js → [...slug]-30c3807057a4e65b.js} +1 -1
  210. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/{global-data-products-fd6ae6a358a60a0c.js → global-data-products-8dcb3b31af9e0e39.js} +1 -1
  211. mage_ai/server/{frontend_dist/_next/static/chunks/pages/global-hooks/[...slug]-8e50243797a7fe59.js → frontend_dist_base_path_template/_next/static/chunks/pages/global-hooks/[...slug]-85a64b64d27214b6.js} +1 -1
  212. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/{global-hooks-d0c003446332dc0d.js → global-hooks-4ff959d51b8a9502.js} +1 -1
  213. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/manage/{files-a69ed8e9f814490c.js → files-d08a460641d0efaa.js} +1 -1
  214. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/manage/{overview-1aad7093c6d39257.js → overview-aae747f487e08d51.js} +1 -1
  215. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/manage/{pipeline-runs-528d30e0d13b0cc7.js → pipeline-runs-09a842d64a6ada62.js} +1 -1
  216. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/manage/{settings-fb9201d9cf63031d.js → settings-2e98e57d9376a458.js} +1 -1
  217. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/manage/users/{[user]-000f5a980a07da39.js → [user]-7be6e41ad66089bb.js} +1 -1
  218. mage_ai/server/{frontend_dist/_next/static/chunks/pages/manage/users/new-e4e613f6e817a733.js → frontend_dist_base_path_template/_next/static/chunks/pages/manage/users/new-4c088833063bfa07.js} +1 -1
  219. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/manage/users-5db54821a3059c69.js +1 -0
  220. mage_ai/server/{frontend_dist/_next/static/chunks/pages/manage-34d718b8a4066c23.js → frontend_dist_base_path_template/_next/static/chunks/pages/manage-868fcd8cbeb265f0.js} +1 -1
  221. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/{oauth-3bfd1b8d7f036726.js → oauth-6ceceb62191dfe8a.js} +1 -1
  222. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/overview-f65416f6dbe30ad3.js +1 -0
  223. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/{pipeline-runs-5f8c100e648efa8a.js → pipeline-runs-2d0136b51b57de93.js} +1 -1
  224. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/backfills/{[...slug]-688c652f3296bb9c.js → [...slug]-1ad5238742e25b4c.js} +1 -1
  225. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/backfills-bd11e87d026bfbf9.js +1 -0
  226. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/{dashboard-1236e36d39b1637d.js → dashboard-0f4f47f721b0723f.js} +1 -1
  227. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/edit-5ae8efe9e0530212.js +1 -0
  228. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/logs-fe91dfb0091f6bc6.js +1 -0
  229. mage_ai/server/{frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/monitors/block-runs-0d68d4bf6290fefb.js → frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/monitors/block-runs-cf794b2d22a80f31.js} +1 -1
  230. mage_ai/server/{frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/monitors/block-runtime-9254358d58f07714.js → frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/monitors/block-runtime-a964caef91bed9e1.js} +1 -1
  231. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/{monitors-821001e690caebe2.js → monitors-80bebb4401eefe25.js} +1 -1
  232. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/runs/[run]-2eae7cb017027682.js +1 -0
  233. mage_ai/server/{frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/runs-2d4b2a0800a66b33.js → frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/runs-776b2e5b0b6ceba8.js} +1 -1
  234. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/settings-03d9bca3bc5e6088.js +1 -0
  235. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/syncs-707ed8ca942ca802.js +1 -0
  236. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/triggers/{[...slug]-259143ed3cf59e31.js → [...slug]-8429f17d4146e1ec.js} +1 -1
  237. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/triggers-193045d9836d8d80.js +1 -0
  238. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines-d25d07db166cbb04.js +1 -0
  239. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/platform/global-hooks/{[...slug]-5eeec927e4202b63.js → [...slug]-6834ae87bd668cb2.js} +1 -1
  240. mage_ai/server/{frontend_dist/_next/static/chunks/pages/platform/global-hooks-fbe9ad995d46d837.js → frontend_dist_base_path_template/_next/static/chunks/pages/platform/global-hooks-b3f7309a23e592b2.js} +1 -1
  241. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/account/{profile-fc659962d4015cb3.js → profile-f8b7374385e1f1bf.js} +1 -1
  242. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/platform/preferences-8de68502a9afa299.js +1 -0
  243. mage_ai/server/{frontend_dist/_next/static/chunks/pages/settings/platform/settings-a4f88c334414402b.js → frontend_dist_base_path_template/_next/static/chunks/pages/settings/platform/settings-50fb6a34f3913f1f.js} +1 -1
  244. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/permissions/{[...slug]-4deb9579ef99a3c6.js → [...slug]-2e5c098c21ea32b7.js} +1 -1
  245. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/{permissions-e0cda2f2bfce8d61.js → permissions-54e4b15b9585bfc4.js} +1 -1
  246. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/preferences-040f83d75d0f6537.js +1 -0
  247. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/roles/{[...slug]-910257d16c604ebd.js → [...slug]-95088f43034e3c95.js} +1 -1
  248. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/{roles-4f7a0756806cee34.js → roles-e9149e1fcf218f42.js} +1 -1
  249. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/{sync-data-208d6f955204d704.js → sync-data-75b67ae4a00818ef.js} +1 -1
  250. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/users/{[...slug]-c89dc67e5a1706a8.js → [...slug]-557dda05ca6c6124.js} +1 -1
  251. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/users-fa61dc6c1370e6a5.js +1 -0
  252. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/{sign-in-054b33312d3193c3.js → sign-in-593c40985d63fcf7.js} +1 -1
  253. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/templates/{[...slug]-b6ed6a5d818bfd20.js → [...slug]-252c4b6b818345d5.js} +1 -1
  254. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/{templates-852357bc983af2ea.js → templates-ca528bc607753ab8.js} +1 -1
  255. mage_ai/server/{frontend_dist/_next/static/chunks/pages/terminal-1f9c56d671bbc67d.js → frontend_dist_base_path_template/_next/static/chunks/pages/terminal-287362c1defcc96b.js} +1 -1
  256. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/test-2f83af8c9f1378fe.js +1 -0
  257. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/triggers-d9de73fb799efed8.js +1 -0
  258. mage_ai/server/{frontend_dist/_next/static/chunks/pages/version-control-ae3469b992a341d6.js → frontend_dist_base_path_template/_next/static/chunks/pages/version-control-573f0225d7a703ed.js} +1 -1
  259. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/{webpack-12ad70eb5c31aa92.js → webpack-5f4be622608d9267.js} +1 -1
  260. mage_ai/server/frontend_dist_base_path_template/block-layout.html +3 -3
  261. mage_ai/server/frontend_dist_base_path_template/compute.html +6 -6
  262. mage_ai/server/frontend_dist_base_path_template/files.html +6 -6
  263. mage_ai/server/frontend_dist_base_path_template/global-data-products/[...slug].html +6 -6
  264. mage_ai/server/frontend_dist_base_path_template/global-data-products.html +6 -6
  265. mage_ai/server/frontend_dist_base_path_template/global-hooks/[...slug].html +6 -6
  266. mage_ai/server/frontend_dist_base_path_template/global-hooks.html +6 -6
  267. mage_ai/server/frontend_dist_base_path_template/index.html +3 -3
  268. mage_ai/server/frontend_dist_base_path_template/manage/files.html +6 -6
  269. mage_ai/server/frontend_dist_base_path_template/manage/overview.html +6 -6
  270. mage_ai/server/frontend_dist_base_path_template/manage/pipeline-runs.html +6 -6
  271. mage_ai/server/frontend_dist_base_path_template/manage/settings.html +6 -6
  272. mage_ai/server/frontend_dist_base_path_template/manage/users/[user].html +6 -6
  273. mage_ai/server/frontend_dist_base_path_template/manage/users/new.html +6 -6
  274. mage_ai/server/frontend_dist_base_path_template/manage/users.html +6 -6
  275. mage_ai/server/frontend_dist_base_path_template/manage.html +6 -6
  276. mage_ai/server/frontend_dist_base_path_template/oauth.html +5 -5
  277. mage_ai/server/frontend_dist_base_path_template/overview.html +6 -6
  278. mage_ai/server/frontend_dist_base_path_template/pipeline-runs.html +6 -6
  279. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/backfills/[...slug].html +6 -6
  280. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/backfills.html +6 -6
  281. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/dashboard.html +6 -6
  282. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/edit.html +3 -3
  283. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/logs.html +6 -6
  284. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/monitors/block-runs.html +6 -6
  285. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/monitors/block-runtime.html +6 -6
  286. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/monitors.html +6 -6
  287. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/runs/[run].html +6 -6
  288. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/runs.html +6 -6
  289. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/settings.html +6 -6
  290. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/syncs.html +6 -6
  291. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/triggers/[...slug].html +6 -6
  292. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/triggers.html +6 -6
  293. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline].html +3 -3
  294. mage_ai/server/frontend_dist_base_path_template/pipelines.html +6 -6
  295. mage_ai/server/frontend_dist_base_path_template/platform/global-hooks/[...slug].html +6 -6
  296. mage_ai/server/frontend_dist_base_path_template/platform/global-hooks.html +6 -6
  297. mage_ai/server/frontend_dist_base_path_template/settings/account/profile.html +6 -6
  298. mage_ai/server/frontend_dist_base_path_template/settings/platform/preferences.html +6 -6
  299. mage_ai/server/frontend_dist_base_path_template/settings/platform/settings.html +6 -6
  300. mage_ai/server/frontend_dist_base_path_template/settings/workspace/permissions/[...slug].html +6 -6
  301. mage_ai/server/frontend_dist_base_path_template/settings/workspace/permissions.html +6 -6
  302. mage_ai/server/frontend_dist_base_path_template/settings/workspace/preferences.html +6 -6
  303. mage_ai/server/frontend_dist_base_path_template/settings/workspace/roles/[...slug].html +6 -6
  304. mage_ai/server/frontend_dist_base_path_template/settings/workspace/roles.html +6 -6
  305. mage_ai/server/frontend_dist_base_path_template/settings/workspace/sync-data.html +6 -6
  306. mage_ai/server/frontend_dist_base_path_template/settings/workspace/users/[...slug].html +6 -6
  307. mage_ai/server/frontend_dist_base_path_template/settings/workspace/users.html +6 -6
  308. mage_ai/server/frontend_dist_base_path_template/settings.html +3 -3
  309. mage_ai/server/frontend_dist_base_path_template/sign-in.html +7 -7
  310. mage_ai/server/frontend_dist_base_path_template/templates/[...slug].html +6 -6
  311. mage_ai/server/frontend_dist_base_path_template/templates.html +6 -6
  312. mage_ai/server/frontend_dist_base_path_template/terminal.html +6 -6
  313. mage_ai/server/frontend_dist_base_path_template/test.html +3 -3
  314. mage_ai/server/frontend_dist_base_path_template/triggers.html +6 -6
  315. mage_ai/server/frontend_dist_base_path_template/v2/canvas.html +2 -2
  316. mage_ai/server/frontend_dist_base_path_template/v2.html +2 -2
  317. mage_ai/server/frontend_dist_base_path_template/version-control.html +6 -6
  318. mage_ai/server/scheduler_manager.py +2 -0
  319. mage_ai/server/server.py +13 -0
  320. mage_ai/server/terminal_server.py +3 -0
  321. mage_ai/server/utils/output_display.py +5 -3
  322. mage_ai/services/aws/events/events.py +2 -2
  323. mage_ai/services/gcp/cloud_run/cloud_run.py +1 -1
  324. mage_ai/services/teams/config.py +13 -2
  325. mage_ai/services/teams/teams.py +13 -11
  326. mage_ai/settings/server.py +12 -1
  327. mage_ai/shared/constants.py +3 -1
  328. mage_ai/shared/croniter.py +1398 -0
  329. mage_ai/shared/enum.py +2 -5
  330. mage_ai/shared/environments.py +27 -3
  331. mage_ai/streaming/sinks/elasticsearch.py +15 -5
  332. mage_ai/streaming/sinks/kafka.py +21 -3
  333. mage_ai/streaming/sources/kafka.py +64 -7
  334. mage_ai/streaming/sources/kafka_oauth.py +182 -0
  335. mage_ai/tests/api/endpoints/test_blocks.py +1 -101
  336. mage_ai/tests/api/endpoints/test_configuration_options.py +1 -48
  337. mage_ai/tests/api/endpoints/test_configuration_options_project_platform.py +68 -0
  338. mage_ai/tests/api/endpoints/test_custom_designs.py +1 -106
  339. mage_ai/tests/api/endpoints/test_custom_designs_project_platform.py +132 -0
  340. mage_ai/tests/api/endpoints/test_dbt_blocks.py +111 -0
  341. mage_ai/tests/api/endpoints/test_file_contents.py +0 -48
  342. mage_ai/tests/api/endpoints/test_file_contents_with_project_platform.py +66 -0
  343. mage_ai/tests/api/endpoints/test_pipelines.py +0 -134
  344. mage_ai/tests/api/endpoints/test_pipelines_with_project_platform.py +143 -0
  345. mage_ai/tests/data_preparation/executors/test_block_executor.py +3 -3
  346. mage_ai/tests/data_preparation/logging/test_logger_manager.py +24 -5
  347. mage_ai/tests/data_preparation/models/block/dynamic/test_counter.py +1 -3
  348. mage_ai/tests/data_preparation/models/block/hook/test_hook_block.py +3 -3
  349. mage_ai/tests/data_preparation/models/test_block.py +7 -0
  350. mage_ai/tests/data_preparation/models/test_pipeline.py +55 -0
  351. mage_ai/tests/data_preparation/models/test_variable.py +2 -0
  352. mage_ai/tests/data_preparation/test_repo_manager.py +0 -63
  353. mage_ai/tests/data_preparation/test_repo_manager_project_platform.py +67 -0
  354. mage_ai/tests/data_preparation/test_variable_manager.py +0 -51
  355. mage_ai/tests/data_preparation/test_variable_manager_project_platform.py +41 -0
  356. mage_ai/tests/io/create_table/test_postgresql.py +28 -0
  357. mage_ai/tests/orchestration/db/models/test_schedules.py +1 -1
  358. mage_ai/tests/orchestration/notification/test_config.py +3 -3
  359. mage_ai/tests/orchestration/notification/test_sender.py +5 -1
  360. mage_ai/tests/orchestration/utils/__init__.py +0 -0
  361. mage_ai/tests/orchestration/utils/test_resources.py +235 -0
  362. mage_ai/tests/shared/test_croniter.py +2541 -0
  363. mage_ai/tests/streaming/sinks/test_kafka.py +130 -0
  364. mage_ai/tests/streaming/sources/test_kafka.py +125 -3
  365. mage_ai/tests/streaming/sources/test_kafka_oauth.py +208 -0
  366. mage_ai/tests/streaming/sources/test_kafka_raw_value.py +105 -0
  367. mage_ai/usage_statistics/logger.py +99 -15
  368. mage_ai-0.9.79.dist-info/METADATA +358 -0
  369. {mage_ai-0.9.74.dist-info → mage_ai-0.9.79.dist-info}/RECORD +377 -359
  370. {mage_ai-0.9.74.dist-info → mage_ai-0.9.79.dist-info}/WHEEL +1 -1
  371. mage_ai/server/frontend_dist/_next/static/chunks/1557-1ad0c64c2d08e569.js +0 -1
  372. mage_ai/server/frontend_dist/_next/static/chunks/2717-d65056b6b5e124eb.js +0 -1
  373. mage_ai/server/frontend_dist/_next/static/chunks/3548-b2c5edfb710886a6.js +0 -1
  374. mage_ai/server/frontend_dist/_next/static/chunks/3782-4b3091e550f809a2.js +0 -1
  375. mage_ai/server/frontend_dist/_next/static/chunks/pages/_app-5bdff745074fb350.js +0 -2
  376. mage_ai/server/frontend_dist/_next/static/chunks/pages/manage/users-a5e9d77ed5b50205.js +0 -1
  377. mage_ai/server/frontend_dist/_next/static/chunks/pages/overview-69af3253ad0d0d89.js +0 -1
  378. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/backfills-fe112809feb25b05.js +0 -1
  379. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/edit-e9ca358209cdf93d.js +0 -1
  380. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/logs-a29f1615d2e7d330.js +0 -1
  381. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/runs/[run]-6d382ae5bad9745c.js +0 -1
  382. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/settings-cd49372ae1702963.js +0 -1
  383. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/syncs-135be8974f7f5f2b.js +0 -1
  384. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/triggers-3af13e89adff4d6c.js +0 -1
  385. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines-b578b075a8d857e3.js +0 -1
  386. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/platform/preferences-058d283ee178c038.js +0 -1
  387. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/preferences-7b02bb70462144cb.js +0 -1
  388. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/users-5212c01a9dc558da.js +0 -1
  389. mage_ai/server/frontend_dist/_next/static/chunks/pages/test-86b12cc12d4a625f.js +0 -1
  390. mage_ai/server/frontend_dist/_next/static/chunks/pages/triggers-2481c40b18d5b6d4.js +0 -1
  391. mage_ai/server/frontend_dist/_next/static/pLWT6Sqd09xYpufCVIqnz/_buildManifest.js +0 -1
  392. mage_ai/server/frontend_dist_base_path_template/_next/static/JQewSAObpbhO0wrdAM6Ng/_buildManifest.js +0 -1
  393. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/1557-1ad0c64c2d08e569.js +0 -1
  394. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/2717-d65056b6b5e124eb.js +0 -1
  395. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/3548-b2c5edfb710886a6.js +0 -1
  396. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/3782-4b3091e550f809a2.js +0 -1
  397. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/_app-90de19bc03f1484b.js +0 -2
  398. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/manage/users-a5e9d77ed5b50205.js +0 -1
  399. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/overview-69af3253ad0d0d89.js +0 -1
  400. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/backfills-fe112809feb25b05.js +0 -1
  401. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/edit-e9ca358209cdf93d.js +0 -1
  402. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/logs-a29f1615d2e7d330.js +0 -1
  403. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/runs/[run]-6d382ae5bad9745c.js +0 -1
  404. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/settings-cd49372ae1702963.js +0 -1
  405. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/syncs-135be8974f7f5f2b.js +0 -1
  406. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/triggers-3af13e89adff4d6c.js +0 -1
  407. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines-b578b075a8d857e3.js +0 -1
  408. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/platform/preferences-058d283ee178c038.js +0 -1
  409. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/preferences-7b02bb70462144cb.js +0 -1
  410. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/users-5212c01a9dc558da.js +0 -1
  411. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/test-86b12cc12d4a625f.js +0 -1
  412. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/triggers-2481c40b18d5b6d4.js +0 -1
  413. mage_ai-0.9.74.dist-info/METADATA +0 -544
  414. /mage_ai/server/frontend_dist/_next/static/{pLWT6Sqd09xYpufCVIqnz → TUo4RceCdMufBTBTq8CAq}/_ssgManifest.js +0 -0
  415. /mage_ai/server/frontend_dist/_next/static/chunks/pages/{_app-5bdff745074fb350.js.LICENSE.txt → _app-b697b35dfc4e6e26.js.LICENSE.txt} +0 -0
  416. /mage_ai/server/frontend_dist_base_path_template/_next/static/{JQewSAObpbhO0wrdAM6Ng → 2QL-FT4lFR0a9bDZ7lNd9}/_ssgManifest.js +0 -0
  417. /mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/{_app-90de19bc03f1484b.js.LICENSE.txt → _app-f205accb03b9ff43.js.LICENSE.txt} +0 -0
  418. {mage_ai-0.9.74.dist-info → mage_ai-0.9.79.dist-info}/entry_points.txt +0 -0
  419. {mage_ai-0.9.74.dist-info → mage_ai-0.9.79.dist-info/licenses}/LICENSE +0 -0
  420. {mage_ai-0.9.74.dist-info → mage_ai-0.9.79.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1398 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ This code is pasted from https://github.com/corpusops/croniter
6
+ as it is declared unmaintained and abandoned by the original author.
7
+ """
8
+
9
+ from __future__ import absolute_import, division, print_function
10
+
11
+ import binascii
12
+ import calendar
13
+ import copy
14
+ import datetime
15
+ import math
16
+ import platform
17
+ import random
18
+ import re
19
+ import struct
20
+ import sys
21
+ import traceback as _traceback
22
+ from time import time
23
+
24
+ # as pytz is optional in thirdparty libs but we need it for good support under
25
+ # python2, just test that it's well installed
26
+ import pytz # noqa
27
+ from dateutil.relativedelta import relativedelta
28
+ from dateutil.tz import tzutc
29
+
30
+
31
+ def is_32bit():
32
+ """
33
+ Detect if Python is running in 32-bit mode.
34
+ Compatible with Python 2.6 and later versions.
35
+ Returns True if running on 32-bit Python, False for 64-bit.
36
+ """
37
+ # Method 1: Check pointer size
38
+ bits = struct.calcsize("P") * 8
39
+
40
+ # Method 2: Check platform architecture string
41
+ try:
42
+ architecture = platform.architecture()[0]
43
+ except RuntimeError:
44
+ architecture = None
45
+
46
+ # Method 3: Check maxsize (sys.maxint in Python 2)
47
+ try:
48
+ # Python 2
49
+ is_small_maxsize = sys.maxint <= 2 ** 32
50
+ except AttributeError:
51
+ # Python 3
52
+ is_small_maxsize = sys.maxsize <= 2 ** 32
53
+
54
+ # Evaluate all available methods
55
+ is_32 = False
56
+
57
+ if bits == 32:
58
+ is_32 = True
59
+ elif architecture and "32" in architecture:
60
+ is_32 = True
61
+ elif is_small_maxsize:
62
+ is_32 = True
63
+
64
+ return is_32
65
+
66
+
67
+ try:
68
+ # https://github.com/python/cpython/issues/101069 detection
69
+ if is_32bit():
70
+ datetime.datetime.fromtimestamp(3999999999)
71
+ OVERFLOW32B_MODE = False
72
+ except OverflowError:
73
+ OVERFLOW32B_MODE = True
74
+
75
+ try:
76
+ from collections import OrderedDict
77
+ except ImportError:
78
+ OrderedDict = dict # py26 degraded mode, expanders order will not be immutable
79
+
80
+
81
+ try:
82
+ # py3 recent
83
+ UTC_DT = datetime.timezone.utc
84
+ except AttributeError:
85
+ UTC_DT = pytz.utc
86
+ EPOCH = datetime.datetime.fromtimestamp(0, UTC_DT)
87
+
88
+ # fmt: off
89
+ M_ALPHAS = {
90
+ "jan": 1, "feb": 2, "mar": 3, "apr": 4, # noqa: E241
91
+ "may": 5, "jun": 6, "jul": 7, "aug": 8, # noqa: E241
92
+ "sep": 9, "oct": 10, "nov": 11, "dec": 12,
93
+ }
94
+ DOW_ALPHAS = {
95
+ "sun": 0, "mon": 1, "tue": 2, "wed": 3, "thu": 4, "fri": 5, "sat": 6
96
+ }
97
+
98
+ MINUTE_FIELD = 0
99
+ HOUR_FIELD = 1
100
+ DAY_FIELD = 2
101
+ MONTH_FIELD = 3
102
+ DOW_FIELD = 4
103
+ SECOND_FIELD = 5
104
+ YEAR_FIELD = 6
105
+
106
+ UNIX_FIELDS = (MINUTE_FIELD, HOUR_FIELD, DAY_FIELD, MONTH_FIELD, DOW_FIELD) # noqa: E222
107
+ SECOND_FIELDS = (MINUTE_FIELD, HOUR_FIELD, DAY_FIELD,
108
+ MONTH_FIELD, DOW_FIELD, SECOND_FIELD) # noqa: E222
109
+ YEAR_FIELDS = (MINUTE_FIELD, HOUR_FIELD, DAY_FIELD, MONTH_FIELD,
110
+ DOW_FIELD, SECOND_FIELD, YEAR_FIELD) # noqa: E222
111
+ # fmt: on
112
+
113
+ step_search_re = re.compile(r"^([^-]+)-([^-/]+)(/(\d+))?$")
114
+ only_int_re = re.compile(r"^\d+$")
115
+
116
+ WEEKDAYS = "|".join(DOW_ALPHAS.keys())
117
+ MONTHS = "|".join(M_ALPHAS.keys())
118
+ star_or_int_re = re.compile(r"^(\d+|\*)$")
119
+ special_dow_re = re.compile(
120
+ (r"^(?P<pre>((?P<he>(({WEEKDAYS})(-({WEEKDAYS}))?)").format(WEEKDAYS=WEEKDAYS)
121
+ + (r"|(({MONTHS})(-({MONTHS}))?)|\w+)#)|l)(?P<last>\d+)$").format(MONTHS=MONTHS)
122
+ )
123
+ re_star = re.compile("[*]")
124
+ hash_expression_re = re.compile(
125
+ r"^(?P<hash_type>h|r)(\((?P<range_begin>\d+)-(?P<range_end>\d+)\))?(\/(?P<divisor>\d+))?$"
126
+ )
127
+
128
+ CRON_FIELDS = {
129
+ "unix": UNIX_FIELDS,
130
+ "second": SECOND_FIELDS,
131
+ "year": YEAR_FIELDS,
132
+ len(UNIX_FIELDS): UNIX_FIELDS,
133
+ len(SECOND_FIELDS): SECOND_FIELDS,
134
+ len(YEAR_FIELDS): YEAR_FIELDS,
135
+ }
136
+ UNIX_CRON_LEN = len(UNIX_FIELDS)
137
+ SECOND_CRON_LEN = len(SECOND_FIELDS)
138
+ YEAR_CRON_LEN = len(YEAR_FIELDS)
139
+ # retrocompat
140
+ VALID_LEN_EXPRESSION = set(a for a in CRON_FIELDS if isinstance(a, int))
141
+ TIMESTAMP_TO_DT_CACHE = {}
142
+ EXPRESSIONS = {}
143
+ MARKER = object()
144
+
145
+
146
+ def timedelta_to_seconds(td):
147
+ return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10 ** 6) / 10 ** 6
148
+
149
+
150
+ def datetime_to_timestamp(d):
151
+ if d.tzinfo is not None:
152
+ d = d.replace(tzinfo=None) - d.utcoffset()
153
+
154
+ return timedelta_to_seconds(d - datetime.datetime(1970, 1, 1))
155
+
156
+
157
+ class CroniterError(ValueError):
158
+ """General top-level Croniter base exception"""
159
+
160
+
161
+ class CroniterBadTypeRangeError(TypeError):
162
+ """."""
163
+
164
+
165
+ class CroniterBadCronError(CroniterError):
166
+ """Syntax, unknown value, or range error within a cron expression"""
167
+
168
+
169
+ class CroniterUnsupportedSyntaxError(CroniterBadCronError):
170
+ """Valid cron syntax, but likely to produce inaccurate results"""
171
+
172
+ # Extending CroniterBadCronError, which may be contridatory, but this allows
173
+ # catching both errors with a single exception. From a user perspective
174
+ # these will likely be handled the same way.
175
+
176
+
177
+ class CroniterBadDateError(CroniterError):
178
+ """Unable to find next/prev timestamp match"""
179
+
180
+
181
+ class CroniterNotAlphaError(CroniterBadCronError):
182
+ """Cron syntax contains an invalid day or month abbreviation"""
183
+
184
+
185
+ class croniter(object):
186
+ MONTHS_IN_YEAR = 12
187
+
188
+ # This helps with expanding `*` fields into `lower-upper` ranges. Each item
189
+ # in this tuple maps to the corresponding field index
190
+ RANGES = (
191
+ (0, 59),
192
+ (0, 23),
193
+ (1, 31),
194
+ (1, 12),
195
+ (0, 6),
196
+ (0, 59),
197
+ (1970, 2099),
198
+ )
199
+ DAYS = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
200
+
201
+ ALPHACONV = (
202
+ {}, # 0: min
203
+ {}, # 1: hour
204
+ {"l": "l"}, # 2: dom
205
+ # 3: mon
206
+ copy.deepcopy(M_ALPHAS),
207
+ # 4: dow
208
+ copy.deepcopy(DOW_ALPHAS),
209
+ # 5: second
210
+ {},
211
+ # 6: year
212
+ {},
213
+ )
214
+
215
+ LOWMAP = (
216
+ {},
217
+ {},
218
+ {0: 1},
219
+ {0: 1},
220
+ {7: 0},
221
+ {},
222
+ {},
223
+ )
224
+
225
+ LEN_MEANS_ALL = (
226
+ 60,
227
+ 24,
228
+ 31,
229
+ 12,
230
+ 7,
231
+ 60,
232
+ 130,
233
+ )
234
+
235
+ def __init__(
236
+ self,
237
+ expr_format,
238
+ start_time=None,
239
+ ret_type=float,
240
+ day_or=True,
241
+ max_years_between_matches=None,
242
+ is_prev=False,
243
+ hash_id=None,
244
+ implement_cron_bug=False,
245
+ second_at_beginning=None,
246
+ expand_from_start_time=False,
247
+ ):
248
+ self._ret_type = ret_type
249
+ self._day_or = day_or
250
+ self._implement_cron_bug = implement_cron_bug
251
+ self.second_at_beginning = bool(second_at_beginning)
252
+ self._expand_from_start_time = expand_from_start_time
253
+
254
+ if hash_id:
255
+ if not isinstance(hash_id, (bytes, str)):
256
+ raise TypeError("hash_id must be bytes or UTF-8 string")
257
+ if not isinstance(hash_id, bytes):
258
+ hash_id = hash_id.encode("UTF-8")
259
+
260
+ self._max_years_btw_matches_explicitly_set = max_years_between_matches is not None
261
+ if not self._max_years_btw_matches_explicitly_set:
262
+ max_years_between_matches = 50
263
+ self._max_years_between_matches = max(int(max_years_between_matches), 1)
264
+
265
+ if start_time is None:
266
+ start_time = time()
267
+
268
+ self.tzinfo = None
269
+
270
+ self.start_time = None
271
+ self.dst_start_time = None
272
+ self.cur = None
273
+ self.set_current(start_time, force=False)
274
+
275
+ self.expanded, self.nth_weekday_of_month = self.expand(
276
+ expr_format,
277
+ hash_id=hash_id,
278
+ from_timestamp=self.dst_start_time if self._expand_from_start_time else None,
279
+ second_at_beginning=second_at_beginning,
280
+ )
281
+ self.fields = CRON_FIELDS[len(self.expanded)]
282
+ self.expressions = EXPRESSIONS[(expr_format, hash_id, second_at_beginning)]
283
+ self._is_prev = is_prev
284
+
285
+ @classmethod
286
+ def _alphaconv(cls, index, key, expressions):
287
+ try:
288
+ return cls.ALPHACONV[index][key]
289
+ except KeyError:
290
+ raise CroniterNotAlphaError("[{0}] is not acceptable".format(" ".join(expressions)))
291
+
292
+ def get_next(self, ret_type=None, start_time=None, update_current=True):
293
+ if start_time and self._expand_from_start_time:
294
+ raise ValueError("start_time is not supported when "
295
+ "using expand_from_start_time = True.")
296
+ return self._get_next(
297
+ ret_type=ret_type,
298
+ start_time=start_time,
299
+ is_prev=False,
300
+ update_current=update_current,
301
+ )
302
+
303
+ def get_prev(self, ret_type=None, start_time=None, update_current=True):
304
+ return self._get_next(
305
+ ret_type=ret_type,
306
+ start_time=start_time,
307
+ is_prev=True,
308
+ update_current=update_current,
309
+ )
310
+
311
+ def get_current(self, ret_type=None):
312
+ ret_type = ret_type or self._ret_type
313
+ if issubclass(ret_type, datetime.datetime):
314
+ return self.timestamp_to_datetime(self.cur)
315
+ return self.cur
316
+
317
+ def set_current(self, start_time, force=True):
318
+ if (force or (self.cur is None)) and start_time is not None:
319
+ if isinstance(start_time, datetime.datetime):
320
+ self.tzinfo = start_time.tzinfo
321
+ start_time = self.datetime_to_timestamp(start_time)
322
+
323
+ self.start_time = start_time
324
+ self.dst_start_time = start_time
325
+ self.cur = start_time
326
+ return self.cur
327
+
328
+ @staticmethod
329
+ def datetime_to_timestamp(d):
330
+ """
331
+ Converts a `datetime` object `d` into a UNIX timestamp.
332
+ """
333
+ return datetime_to_timestamp(d)
334
+
335
+ _datetime_to_timestamp = datetime_to_timestamp # retrocompat
336
+
337
+ def timestamp_to_datetime(self, timestamp, tzinfo=MARKER):
338
+ """
339
+ Converts a UNIX `timestamp` into a `datetime` object.
340
+ """
341
+ if tzinfo is MARKER: # allow to give tzinfo=None even if self.tzinfo is set
342
+ tzinfo = self.tzinfo
343
+ k = timestamp
344
+ if tzinfo:
345
+ k = (timestamp, repr(tzinfo))
346
+ try:
347
+ return TIMESTAMP_TO_DT_CACHE[k]
348
+ except KeyError:
349
+ pass
350
+ if OVERFLOW32B_MODE:
351
+ # degraded mode to workaround Y2038
352
+ # see https://github.com/python/cpython/issues/101069
353
+ result = EPOCH.replace(tzinfo=None) + datetime.timedelta(seconds=timestamp)
354
+ else:
355
+ result = datetime.datetime.fromtimestamp(timestamp, tz=tzutc()).replace(tzinfo=None)
356
+ if tzinfo:
357
+ result = result.replace(tzinfo=UTC_DT).astimezone(tzinfo)
358
+ TIMESTAMP_TO_DT_CACHE[(result, repr(result.tzinfo))] = result
359
+ return result
360
+
361
+ _timestamp_to_datetime = timestamp_to_datetime # retrocompat
362
+
363
+ @staticmethod
364
+ def timedelta_to_seconds(td):
365
+ """
366
+ Converts a 'datetime.timedelta' object `td` into seconds contained in
367
+ the duration.
368
+ Note: We cannot use `timedelta.total_seconds()` because this is not
369
+ supported by Python 2.6.
370
+ """
371
+ return timedelta_to_seconds(td)
372
+
373
+ _timedelta_to_seconds = timedelta_to_seconds # retrocompat
374
+
375
+ def _get_next(
376
+ self,
377
+ ret_type=None,
378
+ start_time=None,
379
+ is_prev=None,
380
+ update_current=None,
381
+ ):
382
+ if update_current is None:
383
+ update_current = True
384
+ self.set_current(start_time, force=True)
385
+ if is_prev is None:
386
+ is_prev = self._is_prev
387
+ self._is_prev = is_prev
388
+ expanded = self.expanded[:]
389
+ nth_weekday_of_month = self.nth_weekday_of_month.copy()
390
+
391
+ ret_type = ret_type or self._ret_type
392
+
393
+ if not issubclass(ret_type, (float, datetime.datetime)):
394
+ raise TypeError("Invalid ret_type, only 'float' or 'datetime' is acceptable.")
395
+
396
+ # exception to support day of month and day of week as defined in cron
397
+ dom_dow_exception_processed = False
398
+ if (expanded[DAY_FIELD][0] != "*" and expanded[DOW_FIELD][0] != "*") and self._day_or:
399
+ # If requested, handle a bug in vixie cron/ISC cron
400
+ # where day_of_month and day_of_week form
401
+ # an intersection (AND) instead of a union (OR) if either
402
+ # field is an asterisk or starts with an asterisk
403
+ # (https://crontab.guru/cron-bug.html)
404
+ if self._implement_cron_bug and (
405
+ re_star.match(self.expressions[DAY_FIELD]) or
406
+ re_star.match(self.expressions[DOW_FIELD])
407
+ ):
408
+ # To produce a schedule identical to the cron bug,
409
+ # we'll bypass the code that
410
+ # makes a union of DOM and DOW, and instead skip to the
411
+ # code that does an intersect instead
412
+ pass
413
+ else:
414
+ bak = expanded[DOW_FIELD]
415
+ expanded[DOW_FIELD] = ["*"]
416
+ t1 = self._calc(self.cur, expanded, nth_weekday_of_month, is_prev)
417
+ expanded[DOW_FIELD] = bak
418
+ expanded[DAY_FIELD] = ["*"]
419
+
420
+ t2 = self._calc(self.cur, expanded, nth_weekday_of_month, is_prev)
421
+ if not is_prev:
422
+ result = t1 if t1 < t2 else t2
423
+ else:
424
+ result = t1 if t1 > t2 else t2
425
+ dom_dow_exception_processed = True
426
+
427
+ if not dom_dow_exception_processed:
428
+ result = self._calc(self.cur, expanded, nth_weekday_of_month, is_prev)
429
+
430
+ # DST Handling for cron job spanning across days
431
+ dtstarttime = self._timestamp_to_datetime(self.dst_start_time)
432
+ dtstarttime_utcoffset = dtstarttime.utcoffset() or datetime.timedelta(0)
433
+ dtresult = self.timestamp_to_datetime(result)
434
+ lag = lag_hours = 0
435
+ # do we trigger DST on next crontab (handle backward changes)
436
+ dtresult_utcoffset = dtstarttime_utcoffset
437
+ if dtresult and self.tzinfo:
438
+ dtresult_utcoffset = dtresult.utcoffset()
439
+ lag_hours = self._timedelta_to_seconds(dtresult - dtstarttime) / (60 * 60)
440
+ lag = self._timedelta_to_seconds(dtresult_utcoffset - dtstarttime_utcoffset)
441
+ hours_before_midnight = 24 - dtstarttime.hour
442
+ if dtresult_utcoffset != dtstarttime_utcoffset:
443
+ if (lag > 0 and abs(lag_hours) >= hours_before_midnight) or (
444
+ lag < 0 and ((3600 * abs(lag_hours) + abs(lag)) >= hours_before_midnight * 3600)
445
+ ):
446
+ dtresult_adjusted = dtresult - datetime.timedelta(seconds=lag)
447
+ result_adjusted = self._datetime_to_timestamp(dtresult_adjusted)
448
+ # Do the actual adjust only if the result time actually exists
449
+ if self._timestamp_to_datetime(result_adjusted).tzinfo == dtresult_adjusted.tzinfo:
450
+ dtresult = dtresult_adjusted
451
+ result = result_adjusted
452
+ self.dst_start_time = result
453
+ if update_current:
454
+ self.cur = result
455
+ if issubclass(ret_type, datetime.datetime):
456
+ result = dtresult
457
+ return result
458
+
459
+ # iterator protocol, to enable direct use of croniter
460
+ # objects in a loop, like "for dt in croniter("5 0 * * *'): ..."
461
+ # or for combining multiple croniters into single
462
+ # dates feed using 'itertools' module
463
+ def all_next(self, ret_type=None, start_time=None, update_current=None):
464
+ """
465
+ Returns a generator yielding consecutive dates.
466
+
467
+ May be used instead of an implicit call to __iter__ whenever a
468
+ non-default `ret_type` needs to be specified.
469
+ """
470
+ # In a Python 3.7+ world: contextlib.suppress and
471
+ # contextlib.nullcontext could be used instead
472
+ try:
473
+ while True:
474
+ self._is_prev = False
475
+ yield self._get_next(
476
+ ret_type=ret_type,
477
+ start_time=start_time,
478
+ update_current=update_current,
479
+ )
480
+ start_time = None
481
+ except CroniterBadDateError:
482
+ if self._max_years_btw_matches_explicitly_set:
483
+ return
484
+ raise
485
+
486
+ def all_prev(self, ret_type=None, start_time=None, update_current=None):
487
+ """
488
+ Returns a generator yielding previous dates.
489
+ """
490
+ try:
491
+ while True:
492
+ self._is_prev = True
493
+ yield self._get_next(
494
+ ret_type=ret_type,
495
+ start_time=start_time,
496
+ update_current=update_current,
497
+ )
498
+ start_time = None
499
+ except CroniterBadDateError:
500
+ if self._max_years_btw_matches_explicitly_set:
501
+ return
502
+ raise
503
+
504
+ def iter(self, *args, **kwargs):
505
+ return self.all_prev if self._is_prev else self.all_next
506
+
507
+ def __iter__(self):
508
+ return self
509
+
510
+ __next__ = next = _get_next
511
+
512
+ def _calc(self, now, expanded, nth_weekday_of_month, is_prev):
513
+ if is_prev:
514
+ now = math.ceil(now)
515
+ nearest_diff_method = self._get_prev_nearest_diff
516
+ sign = -1
517
+ offset = 1 if (len(expanded) > UNIX_CRON_LEN or now % 60 > 0) else 60
518
+ else:
519
+ now = math.floor(now)
520
+ nearest_diff_method = self._get_next_nearest_diff
521
+ sign = 1
522
+ offset = 1 if (len(expanded) > UNIX_CRON_LEN) else 60
523
+
524
+ dst = now = self.timestamp_to_datetime(now + sign * offset)
525
+
526
+ month, year = dst.month, dst.year
527
+ current_year = now.year
528
+ DAYS = self.DAYS
529
+
530
+ def proc_year(d):
531
+ if len(expanded) == YEAR_CRON_LEN:
532
+ try:
533
+ expanded[YEAR_FIELD].index("*")
534
+ except ValueError:
535
+ # use None as range_val to indicate no loop
536
+ diff_year = nearest_diff_method(d.year, expanded[YEAR_FIELD], None)
537
+ if diff_year is None:
538
+ return None, d
539
+ if diff_year != 0:
540
+ if is_prev:
541
+ d += relativedelta(
542
+ years=diff_year,
543
+ month=12,
544
+ day=31,
545
+ hour=23,
546
+ minute=59,
547
+ second=59,
548
+ )
549
+ else:
550
+ d += relativedelta(
551
+ years=diff_year,
552
+ month=1,
553
+ day=1,
554
+ hour=0,
555
+ minute=0,
556
+ second=0,
557
+ )
558
+ return True, d
559
+ return False, d
560
+
561
+ def proc_month(d):
562
+ try:
563
+ expanded[MONTH_FIELD].index("*")
564
+ except ValueError:
565
+ diff_month = nearest_diff_method(
566
+ d.month, expanded[MONTH_FIELD], self.MONTHS_IN_YEAR)
567
+ reset_day = 1
568
+
569
+ if diff_month is not None and diff_month != 0:
570
+ if is_prev:
571
+ d += relativedelta(months=diff_month)
572
+ reset_day = DAYS[d.month - 1]
573
+ if d.month == 2 and self.is_leap(d.year) is True:
574
+ reset_day += 1
575
+ d += relativedelta(day=reset_day, hour=23, minute=59, second=59)
576
+ else:
577
+ d += relativedelta(
578
+ months=diff_month, day=reset_day, hour=0, minute=0, second=0)
579
+ return True, d
580
+ return False, d
581
+
582
+ def proc_day_of_month(d):
583
+ try:
584
+ expanded[DAY_FIELD].index("*")
585
+ except ValueError:
586
+ days = DAYS[month - 1]
587
+ if month == 2 and self.is_leap(year) is True:
588
+ days += 1
589
+ if "l" in expanded[DAY_FIELD] and days == d.day:
590
+ return False, d
591
+
592
+ if is_prev:
593
+ days_in_prev_month = DAYS[(month - 2) % self.MONTHS_IN_YEAR]
594
+ diff_day = nearest_diff_method(d.day, expanded[DAY_FIELD], days_in_prev_month)
595
+ else:
596
+ diff_day = nearest_diff_method(d.day, expanded[DAY_FIELD], days)
597
+
598
+ if diff_day is not None and diff_day != 0:
599
+ if is_prev:
600
+ d += relativedelta(days=diff_day, hour=23, minute=59, second=59)
601
+ else:
602
+ d += relativedelta(days=diff_day, hour=0, minute=0, second=0)
603
+ return True, d
604
+ return False, d
605
+
606
+ def proc_day_of_week(d):
607
+ try:
608
+ expanded[DOW_FIELD].index("*")
609
+ except ValueError:
610
+ diff_day_of_week = nearest_diff_method(d.isoweekday() % 7, expanded[DOW_FIELD], 7)
611
+ if diff_day_of_week is not None and diff_day_of_week != 0:
612
+ if is_prev:
613
+ d += relativedelta(days=diff_day_of_week, hour=23, minute=59, second=59)
614
+ else:
615
+ d += relativedelta(days=diff_day_of_week, hour=0, minute=0, second=0)
616
+ return True, d
617
+ return False, d
618
+
619
+ def proc_day_of_week_nth(d):
620
+ if "*" in nth_weekday_of_month:
621
+ s = nth_weekday_of_month["*"]
622
+ for i in range(0, 7):
623
+ if i in nth_weekday_of_month:
624
+ nth_weekday_of_month[i].update(s)
625
+ else:
626
+ nth_weekday_of_month[i] = s
627
+ del nth_weekday_of_month["*"]
628
+
629
+ candidates = []
630
+ for wday, nth in nth_weekday_of_month.items():
631
+ c = self._get_nth_weekday_of_month(d.year, d.month, wday)
632
+ for n in nth:
633
+ if n == "l":
634
+ candidate = c[-1]
635
+ elif len(c) < n:
636
+ continue
637
+ else:
638
+ candidate = c[n - 1]
639
+ if (is_prev and candidate <= d.day) or (not is_prev and d.day <= candidate):
640
+ candidates.append(candidate)
641
+
642
+ if not candidates:
643
+ if is_prev:
644
+ d += relativedelta(days=-d.day, hour=23, minute=59, second=59)
645
+ else:
646
+ days = DAYS[month - 1]
647
+ if month == 2 and self.is_leap(year) is True:
648
+ days += 1
649
+ d += relativedelta(days=(days - d.day + 1), hour=0, minute=0, second=0)
650
+ return True, d
651
+
652
+ candidates.sort()
653
+ diff_day = (candidates[-1] if is_prev else candidates[0]) - d.day
654
+ if diff_day != 0:
655
+ if is_prev:
656
+ d += relativedelta(days=diff_day, hour=23, minute=59, second=59)
657
+ else:
658
+ d += relativedelta(days=diff_day, hour=0, minute=0, second=0)
659
+ return True, d
660
+ return False, d
661
+
662
+ def proc_hour(d):
663
+ try:
664
+ expanded[HOUR_FIELD].index("*")
665
+ except ValueError:
666
+ diff_hour = nearest_diff_method(d.hour, expanded[HOUR_FIELD], 24)
667
+ if diff_hour is not None and diff_hour != 0:
668
+ if is_prev:
669
+ d += relativedelta(hours=diff_hour, minute=59, second=59)
670
+ else:
671
+ d += relativedelta(hours=diff_hour, minute=0, second=0)
672
+ return True, d
673
+ return False, d
674
+
675
+ def proc_minute(d):
676
+ try:
677
+ expanded[MINUTE_FIELD].index("*")
678
+ except ValueError:
679
+ diff_min = nearest_diff_method(d.minute, expanded[MINUTE_FIELD], 60)
680
+ if diff_min is not None and diff_min != 0:
681
+ if is_prev:
682
+ d += relativedelta(minutes=diff_min, second=59)
683
+ else:
684
+ d += relativedelta(minutes=diff_min, second=0)
685
+ return True, d
686
+ return False, d
687
+
688
+ def proc_second(d):
689
+ if len(expanded) > UNIX_CRON_LEN:
690
+ try:
691
+ expanded[SECOND_FIELD].index("*")
692
+ except ValueError:
693
+ diff_sec = nearest_diff_method(d.second, expanded[SECOND_FIELD], 60)
694
+ if diff_sec is not None and diff_sec != 0:
695
+ d += relativedelta(seconds=diff_sec)
696
+ return True, d
697
+ else:
698
+ d += relativedelta(second=0)
699
+ return False, d
700
+
701
+ procs = [
702
+ proc_year,
703
+ proc_month,
704
+ proc_day_of_month,
705
+ (proc_day_of_week_nth if nth_weekday_of_month else proc_day_of_week),
706
+ proc_hour,
707
+ proc_minute,
708
+ proc_second,
709
+ ]
710
+
711
+ while abs(year - current_year) <= self._max_years_between_matches:
712
+ next = False
713
+ stop = False
714
+ for proc in procs:
715
+ (changed, dst) = proc(dst)
716
+ # `None` can be set mostly for year processing
717
+ # so please see proc_year / _get_prev_nearest_diff / _get_next_nearest_diff
718
+ if changed is None:
719
+ stop = True
720
+ break
721
+ if changed:
722
+ month, year = dst.month, dst.year
723
+ next = True
724
+ break
725
+ if stop:
726
+ break
727
+ if next:
728
+ continue
729
+ return self.datetime_to_timestamp(dst.replace(microsecond=0))
730
+
731
+ if is_prev:
732
+ raise CroniterBadDateError("failed to find prev date")
733
+ raise CroniterBadDateError("failed to find next date")
734
+
735
+ @staticmethod
736
+ def _get_next_nearest(x, to_check):
737
+ small = [item for item in to_check if item < x]
738
+ large = [item for item in to_check if item >= x]
739
+ large.extend(small)
740
+ return large[0]
741
+
742
+ @staticmethod
743
+ def _get_prev_nearest(x, to_check):
744
+ small = [item for item in to_check if item <= x]
745
+ large = [item for item in to_check if item > x]
746
+ small.reverse()
747
+ large.reverse()
748
+ small.extend(large)
749
+ return small[0]
750
+
751
+ @staticmethod
752
+ def _get_next_nearest_diff(x, to_check, range_val):
753
+ """
754
+ `range_val` is the range of a field.
755
+ If no available time, we can move to next loop(like next month).
756
+ `range_val` can also be set to `None` to indicate that there is no loop.
757
+ ( Currently, should only used for `year` field )
758
+ """
759
+ for _, d in enumerate(to_check):
760
+ if d == "l" and range_val is not None:
761
+ # if 'l' then it is the last day of month
762
+ # => its value of range_val
763
+ d = range_val
764
+ if d >= x:
765
+ return d - x
766
+ # When range_val is None and x not exists in to_check,
767
+ # `None` will be returned to suggest no more available time
768
+ if range_val is None:
769
+ return None
770
+ return to_check[0] - x + range_val
771
+
772
+ @staticmethod
773
+ def _get_prev_nearest_diff(x, to_check, range_val):
774
+ """
775
+ `range_val` is the range of a field.
776
+ If no available time, we can move to previous loop(like previous month).
777
+ Range_val can also be set to `None` to indicate that there is no loop.
778
+ ( Currently should only used for `year` field )
779
+ """
780
+ candidates = to_check[:]
781
+ candidates.reverse()
782
+ for d in candidates:
783
+ if d != "l" and d <= x:
784
+ return d - x
785
+ if "l" in candidates:
786
+ return -x
787
+ # When range_val is None and x not exists in to_check,
788
+ # `None` will be returned to suggest no more available time
789
+ if range_val is None:
790
+ return None
791
+ candidate = candidates[0]
792
+ for c in candidates:
793
+ # fixed: c < range_val
794
+ # this code will reject all 31 day of month, 12 month, 59 second,
795
+ # 23 hour and so on.
796
+ # if candidates has just a element, this will not harmful.
797
+ # but candidates have multiple elements, then values equal to
798
+ # range_val will rejected.
799
+ if c <= range_val:
800
+ candidate = c
801
+ break
802
+ # fix crontab "0 6 30 3 *" condidates only a element,
803
+ # then get_prev error return 2021-03-02 06:00:00
804
+ if candidate > range_val:
805
+ return -range_val
806
+ return candidate - x - range_val
807
+
808
+ @staticmethod
809
+ def _get_nth_weekday_of_month(year, month, day_of_week):
810
+ """For a given year/month return a list of days in nth-day-of-month order.
811
+ The last weekday of the month is always [-1].
812
+ """
813
+ w = (day_of_week + 6) % 7
814
+ c = calendar.Calendar(w).monthdayscalendar(year, month)
815
+ if c[0][0] == 0:
816
+ c.pop(0)
817
+ return tuple(i[0] for i in c)
818
+
819
+ @staticmethod
820
+ def is_leap(year):
821
+ return bool(year % 400 == 0 or (year % 4 == 0 and year % 100 != 0))
822
+
823
+ @classmethod
824
+ def value_alias(cls, val, field_index, len_expressions=UNIX_CRON_LEN):
825
+ if isinstance(len_expressions, (list, dict, tuple, set)):
826
+ len_expressions = len(len_expressions)
827
+ if val in cls.LOWMAP[field_index] and not (
828
+ # do not support 0 as a month either for classical 5 fields cron,
829
+ # 6fields second repeat form or 7 fields year form
830
+ # but still let conversion happen if day field is shifted
831
+ (field_index in [DAY_FIELD, MONTH_FIELD] and len_expressions == UNIX_CRON_LEN)
832
+ or (field_index in [MONTH_FIELD, DOW_FIELD] and len_expressions == SECOND_CRON_LEN)
833
+ or (field_index in [DAY_FIELD, MONTH_FIELD, DOW_FIELD] and
834
+ len_expressions == YEAR_CRON_LEN)
835
+ ):
836
+ val = cls.LOWMAP[field_index][val]
837
+ return val
838
+
839
+ @classmethod
840
+ def _expand(
841
+ cls,
842
+ expr_format,
843
+ hash_id=None,
844
+ second_at_beginning=False,
845
+ from_timestamp=None,
846
+ ):
847
+ # Split the expression in components, and normalize L -> l, MON -> mon,
848
+ # etc. Keep expr_format untouched so we can use it in the exception
849
+ # messages.
850
+ expr_aliases = {
851
+ "@midnight": ("0 0 * * *", "h h(0-2) * * * h"),
852
+ "@hourly": ("0 * * * *", "h * * * * h"),
853
+ "@daily": ("0 0 * * *", "h h * * * h"),
854
+ "@weekly": ("0 0 * * 0", "h h * * h h"),
855
+ "@monthly": ("0 0 1 * *", "h h h * * h"),
856
+ "@yearly": ("0 0 1 1 *", "h h h h * h"),
857
+ "@annually": ("0 0 1 1 *", "h h h h * h"),
858
+ }
859
+
860
+ efl = expr_format.lower()
861
+ hash_id_expr = 1 if hash_id is not None else 0
862
+ try:
863
+ efl = expr_aliases[efl][hash_id_expr]
864
+ except KeyError:
865
+ pass
866
+
867
+ expressions = efl.split()
868
+
869
+ if len(expressions) not in VALID_LEN_EXPRESSION:
870
+ raise CroniterBadCronError("Exactly 5, 6 or 7 columns has to "
871
+ "be specified for iterator expression.")
872
+
873
+ if len(expressions) > UNIX_CRON_LEN and second_at_beginning:
874
+ # move second to it's own(6th) field to process by same logical
875
+ expressions.insert(SECOND_FIELD, expressions.pop(0))
876
+
877
+ expanded = []
878
+ nth_weekday_of_month = {}
879
+
880
+ for field_index, expr in enumerate(expressions):
881
+ for _, expander in EXPANDERS.items():
882
+ expr = expander(cls).expand(
883
+ efl,
884
+ field_index,
885
+ expr,
886
+ hash_id=hash_id,
887
+ from_timestamp=from_timestamp,
888
+ )
889
+
890
+ if "?" in expr:
891
+ if expr != "?":
892
+ raise CroniterBadCronError(
893
+ "[{0}] is not acceptable. Question mark can not used "
894
+ "with other characters".format(expr_format)
895
+ )
896
+ if field_index not in [DAY_FIELD, DOW_FIELD]:
897
+ raise CroniterBadCronError(
898
+ "[{0}] is not acceptable. Question mark can only "
899
+ "used in day_of_month or day_of_week".format(
900
+ expr_format
901
+ )
902
+ )
903
+ # currently just trade `?` as `*`
904
+ expr = "*"
905
+
906
+ e_list = expr.split(",")
907
+ res = []
908
+
909
+ while len(e_list) > 0:
910
+ e = e_list.pop()
911
+ nth = None
912
+
913
+ if field_index == DOW_FIELD:
914
+ # Handle special case in the dow expression: 2#3, l3
915
+ special_dow_rem = special_dow_re.match(str(e))
916
+ if special_dow_rem:
917
+ g = special_dow_rem.groupdict()
918
+ he, last = g.get("he", ""), g.get("last", "")
919
+ if he:
920
+ e = he
921
+ try:
922
+ nth = int(last)
923
+ assert 5 >= nth >= 1
924
+ except (KeyError, ValueError, AssertionError):
925
+ raise CroniterBadCronError(
926
+ "[{0}] is not acceptable. Invalid day_of_week "
927
+ "value: '{1}'".format(
928
+ expr_format, nth
929
+ )
930
+ )
931
+ elif last:
932
+ e = last
933
+ nth = g["pre"] # 'l'
934
+
935
+ # Before matching step_search_re, normalize "*" to "{min}-{max}".
936
+ # Example: in the minute field, "*/5" normalizes to "0-59/5"
937
+ t = re.sub(
938
+ r"^\*(\/.+)$",
939
+ r"%d-%d\1" % (cls.RANGES[field_index][0], cls.RANGES[field_index][1]),
940
+ str(e),
941
+ )
942
+ m = step_search_re.search(t)
943
+
944
+ if not m:
945
+ # Before matching step_search_re,
946
+ # normalize "{start}/{step}" to "{start}-{max}/{step}".
947
+ # Example: in the minute field, "10/5" normalizes to "10-59/5"
948
+ t = re.sub(
949
+ r"^(.+)\/(.+)$",
950
+ r"\1-%d/\2" % (cls.RANGES[field_index][1]),
951
+ str(e),
952
+ )
953
+ m = step_search_re.search(t)
954
+
955
+ if m:
956
+ # early abort if low/high are out of bounds
957
+ (low, high, step) = m.group(1), m.group(2), m.group(4) or 1
958
+ if field_index == DAY_FIELD and high == "l":
959
+ high = "31"
960
+
961
+ if not only_int_re.search(low):
962
+ low = "{0}".format(cls._alphaconv(field_index, low, expressions))
963
+
964
+ if not only_int_re.search(high):
965
+ high = "{0}".format(cls._alphaconv(field_index, high, expressions))
966
+
967
+ # normally, it's already guarded by the RE that
968
+ # should not accept not-int values.
969
+ if not only_int_re.search(str(step)):
970
+ raise CroniterBadCronError(
971
+ "[{0}] step '{2}' in field {1} is not acceptable".format(
972
+ expr_format, field_index, step)
973
+ )
974
+ step = int(step)
975
+
976
+ for band in low, high:
977
+ if not only_int_re.search(str(band)):
978
+ raise CroniterBadCronError(
979
+ "[{0}] bands '{2}-{3}' in field {1} are "
980
+ "not acceptable".format(
981
+ expr_format, field_index, low, high
982
+ )
983
+ )
984
+
985
+ low, high = [cls.value_alias(
986
+ int(_val), field_index, expressions) for _val in (low, high)]
987
+
988
+ if max(low, high) > max(cls.RANGES[field_index][0], cls.RANGES[field_index][1]):
989
+ raise CroniterBadCronError("{0} is out of bands".format(expr_format))
990
+
991
+ if from_timestamp:
992
+ low = cls._get_low_from_current_date_number(
993
+ field_index, int(step), int(from_timestamp))
994
+
995
+ # Handle when the second bound of the range is in backtracking order:
996
+ # eg: X-Sun or X-7 (Sat-Sun) in DOW, or X-Jan (Apr-Jan) in MONTH
997
+ if low > high:
998
+ whole_field_range = list(
999
+ range(
1000
+ cls.RANGES[field_index][0],
1001
+ cls.RANGES[field_index][1] + 1,
1002
+ 1,
1003
+ )
1004
+ )
1005
+ # Add FirstBound -> ENDRANGE, respecting step
1006
+ rng = list(range(low, cls.RANGES[field_index][1] + 1, step))
1007
+ # Then 0 -> SecondBound, but skipping n
1008
+ # first occurences according to step
1009
+ # EG to respect such expressions : Apr-Jan/3
1010
+ to_skip = 0
1011
+ if rng:
1012
+ already_skipped = list(reversed(whole_field_range)).index(rng[-1])
1013
+ curpos = whole_field_range.index(rng[-1])
1014
+ if ((curpos + step) > len(whole_field_range)) and (
1015
+ already_skipped < step):
1016
+ to_skip = step - already_skipped
1017
+ rng += list(range(cls.RANGES[field_index][0] + to_skip, high + 1, step))
1018
+ # if we include a range type: Jan-Jan, or Sun-Sun,
1019
+ # it means the whole cycle (all days of week
1020
+ # , # all monthes of year, etc)
1021
+ elif low == high:
1022
+ rng = list(
1023
+ range(
1024
+ cls.RANGES[field_index][0],
1025
+ cls.RANGES[field_index][1] + 1,
1026
+ step,
1027
+ )
1028
+ )
1029
+ else:
1030
+ try:
1031
+ rng = list(range(low, high + 1, step))
1032
+ except ValueError as exc:
1033
+ raise CroniterBadCronError("invalid range: {0}".format(exc))
1034
+
1035
+ rng = (
1036
+ ["{0}#{1}".format(item, nth) for item in rng]
1037
+ if field_index == DOW_FIELD and nth and nth != "l"
1038
+ else rng
1039
+ )
1040
+ e_list += [a for a in rng if a not in e_list]
1041
+ else:
1042
+ if t.startswith("-"):
1043
+ raise CroniterBadCronError(
1044
+ "[{0}] is not acceptable," "negative numbers "
1045
+ "not allowed".format(expr_format)
1046
+ )
1047
+ if not star_or_int_re.search(t):
1048
+ t = cls._alphaconv(field_index, t, expressions)
1049
+
1050
+ try:
1051
+ t = int(t)
1052
+ except ValueError:
1053
+ pass
1054
+
1055
+ t = cls.value_alias(t, field_index, expressions)
1056
+
1057
+ if t not in ["*", "l"] and (
1058
+ int(t) < cls.RANGES[field_index][0] or int(t) > cls.RANGES[field_index][1]
1059
+ ):
1060
+ raise CroniterBadCronError("[{0}] is not acceptable, "
1061
+ "out of range".format(expr_format))
1062
+
1063
+ res.append(t)
1064
+
1065
+ if field_index == DOW_FIELD and nth:
1066
+ if t not in nth_weekday_of_month:
1067
+ nth_weekday_of_month[t] = set()
1068
+ nth_weekday_of_month[t].add(nth)
1069
+
1070
+ res = set(res)
1071
+ res = sorted(res, key=lambda i: "{:02}".format(i) if isinstance(i, int) else i)
1072
+ if len(res) == cls.LEN_MEANS_ALL[field_index]:
1073
+ # Make sure the wildcard is used in the correct way (avoid over-optimization)
1074
+ if (field_index == DAY_FIELD and "*" not in expressions[DOW_FIELD]) or (
1075
+ field_index == DOW_FIELD and "*" not in expressions[DAY_FIELD]
1076
+ ):
1077
+ pass
1078
+ else:
1079
+ res = ["*"]
1080
+
1081
+ expanded.append(["*"] if (len(res) == 1 and res[0] == "*") else res)
1082
+
1083
+ # Check to make sure the dow combo in use is supported
1084
+ if nth_weekday_of_month:
1085
+ dow_expanded_set = set(expanded[DOW_FIELD])
1086
+ dow_expanded_set = dow_expanded_set.difference(nth_weekday_of_month.keys())
1087
+ dow_expanded_set.discard("*")
1088
+ # Skip: if it's all weeks instead of wildcard
1089
+ if dow_expanded_set and len(set(expanded[DOW_FIELD])) != cls.LEN_MEANS_ALL[DOW_FIELD]:
1090
+ raise CroniterUnsupportedSyntaxError(
1091
+ "day-of-week field does not support mixing literal values "
1092
+ "and nth day of week syntax. "
1093
+ "Cron: '{}' dow={} vs nth={}".format(
1094
+ expr_format, dow_expanded_set, nth_weekday_of_month)
1095
+ )
1096
+
1097
+ EXPRESSIONS[(expr_format, hash_id, second_at_beginning)] = expressions
1098
+ return expanded, nth_weekday_of_month
1099
+
1100
+ @classmethod
1101
+ def expand(
1102
+ cls,
1103
+ expr_format,
1104
+ hash_id=None,
1105
+ second_at_beginning=False,
1106
+ from_timestamp=None,
1107
+ ):
1108
+ """
1109
+ Expand a cron expression format into a noramlized format of
1110
+ list[list[int | 'l' | '*']]. The first list representing each element
1111
+ of the epxression, and each sub-list representing the allowed values
1112
+ for that expression component.
1113
+
1114
+ A tuple is returned, the first value being the expanded epxression
1115
+ list, and the second being a `nth_weekday_of_month` mapping.
1116
+
1117
+ Examples:
1118
+
1119
+ # Every minute
1120
+ >>> croniter.expand('* * * * *')
1121
+ ([['*'], ['*'], ['*'], ['*'], ['*']], {})
1122
+
1123
+ # On the hour
1124
+ >>> croniter.expand('0 0 * * *')
1125
+ ([[0], [0], ['*'], ['*'], ['*']], {})
1126
+
1127
+ # Hours 0-5 and 10 monday through friday
1128
+ >>> croniter.expand('0-5,10 * * * mon-fri')
1129
+ ([[0, 1, 2, 3, 4, 5, 10], ['*'], ['*'], ['*'], [1, 2, 3, 4, 5]], {})
1130
+
1131
+ Note that some special values such as nth day of week are expanded to a
1132
+ special mapping format for later processing:
1133
+
1134
+ # Every minute on the 3rd tuesday of the month
1135
+ >>> croniter.expand('* * * * 2#3')
1136
+ ([['*'], ['*'], ['*'], ['*'], [2]], {2: {3}})
1137
+
1138
+ # Every hour on the last day of the month
1139
+ >>> croniter.expand('0 * l * *')
1140
+ ([[0], ['*'], ['l'], ['*'], ['*']], {})
1141
+
1142
+ # On the hour every 15 seconds
1143
+ >>> croniter.expand('0 0 * * * */15')
1144
+ ([[0], [0], ['*'], ['*'], ['*'], [0, 15, 30, 45]], {})
1145
+ """
1146
+ try:
1147
+ return cls._expand(
1148
+ expr_format,
1149
+ hash_id=hash_id,
1150
+ second_at_beginning=second_at_beginning,
1151
+ from_timestamp=from_timestamp,
1152
+ )
1153
+ except ValueError as exc:
1154
+ if isinstance(exc, CroniterError):
1155
+ raise
1156
+ if int(sys.version[0]) >= 3:
1157
+ trace = _traceback.format_exc()
1158
+ raise CroniterBadCronError(trace)
1159
+ raise CroniterBadCronError("{0}".format(exc))
1160
+
1161
+ @classmethod
1162
+ def _get_low_from_current_date_number(cls, field_index, step, from_timestamp):
1163
+ dt = datetime.datetime.fromtimestamp(from_timestamp, tz=UTC_DT)
1164
+ if field_index == MINUTE_FIELD:
1165
+ return dt.minute % step
1166
+ if field_index == HOUR_FIELD:
1167
+ return dt.hour % step
1168
+ if field_index == DAY_FIELD:
1169
+ return ((dt.day - 1) % step) + 1
1170
+ if field_index == MONTH_FIELD:
1171
+ return dt.month % step
1172
+ if field_index == DOW_FIELD:
1173
+ return (dt.weekday() + 1) % step
1174
+
1175
+ raise ValueError("Can't get current date number for index larger than 4")
1176
+
1177
+ @classmethod
1178
+ def is_valid(
1179
+ cls,
1180
+ expression,
1181
+ hash_id=None,
1182
+ encoding="UTF-8",
1183
+ second_at_beginning=False,
1184
+ ):
1185
+ if hash_id:
1186
+ if not isinstance(hash_id, (bytes, str)):
1187
+ raise TypeError("hash_id must be bytes or UTF-8 string")
1188
+ if not isinstance(hash_id, bytes):
1189
+ hash_id = hash_id.encode(encoding)
1190
+ try:
1191
+ cls.expand(expression, hash_id=hash_id, second_at_beginning=second_at_beginning)
1192
+ except CroniterError:
1193
+ return False
1194
+ return True
1195
+
1196
+ @classmethod
1197
+ def match(cls, cron_expression, testdate, day_or=True, second_at_beginning=False):
1198
+ return cls.match_range(cron_expression, testdate, testdate, day_or, second_at_beginning)
1199
+
1200
+ @classmethod
1201
+ def match_range(
1202
+ cls,
1203
+ cron_expression,
1204
+ from_datetime,
1205
+ to_datetime,
1206
+ day_or=True,
1207
+ second_at_beginning=False,
1208
+ ):
1209
+ cron = cls(
1210
+ cron_expression,
1211
+ to_datetime,
1212
+ ret_type=datetime.datetime,
1213
+ day_or=day_or,
1214
+ second_at_beginning=second_at_beginning,
1215
+ )
1216
+ tdp = cron.get_current(datetime.datetime)
1217
+ if not tdp.microsecond:
1218
+ tdp += relativedelta(microseconds=1)
1219
+ cron.set_current(tdp, force=True)
1220
+ try:
1221
+ tdt = cron.get_prev()
1222
+ except CroniterBadDateError:
1223
+ return False
1224
+ precision_in_seconds = 1 if len(cron.expanded) > UNIX_CRON_LEN else 60
1225
+ duration_in_second = (to_datetime - from_datetime).total_seconds() + precision_in_seconds
1226
+ return (max(tdp, tdt) - min(tdp, tdt)).total_seconds() < duration_in_second
1227
+
1228
+
1229
+ def croniter_range(
1230
+ start,
1231
+ stop,
1232
+ expr_format,
1233
+ ret_type=None,
1234
+ day_or=True,
1235
+ exclude_ends=False,
1236
+ _croniter=None,
1237
+ second_at_beginning=False,
1238
+ expand_from_start_time=False,
1239
+ ):
1240
+ """
1241
+ Generator that provides all times from start to stop matching the given cron expression.
1242
+ If the cron expression matches either 'start' and/or 'stop', those times will be returned as
1243
+ well unless 'exclude_ends=True' is passed.
1244
+
1245
+ You can think of this function as sibling to the builtin range function for datetime objects.
1246
+ Like range(start,stop,step), except that here 'step' is a cron expression.
1247
+ """
1248
+ _croniter = _croniter or croniter
1249
+ auto_rt = datetime.datetime
1250
+ # type is used in first if branch for perfs reasons
1251
+ if type(start) is not type(stop) and not (
1252
+ isinstance(start, type(stop)) or isinstance(stop, type(start))):
1253
+ raise CroniterBadTypeRangeError(
1254
+ "The start and stop must be same type. {0} != {1}".format(type(start), type(stop))
1255
+ )
1256
+ if isinstance(start, (float, int)):
1257
+ start, stop = (datetime.datetime.fromtimestamp(t, tzutc())
1258
+ .replace(tzinfo=None) for t in (start, stop))
1259
+ auto_rt = float
1260
+ if ret_type is None:
1261
+ ret_type = auto_rt
1262
+ if not exclude_ends:
1263
+ ms1 = relativedelta(microseconds=1)
1264
+ if start < stop: # Forward (normal) time order
1265
+ start -= ms1
1266
+ stop += ms1
1267
+ else: # Reverse time order
1268
+ start += ms1
1269
+ stop -= ms1
1270
+ year_span = math.floor(abs(stop.year - start.year)) + 1
1271
+ ic = _croniter(
1272
+ expr_format,
1273
+ start,
1274
+ ret_type=datetime.datetime,
1275
+ day_or=day_or,
1276
+ max_years_between_matches=year_span,
1277
+ second_at_beginning=second_at_beginning,
1278
+ expand_from_start_time=expand_from_start_time,
1279
+ )
1280
+ # define a continue (cont) condition function and step function for the main while loop
1281
+ if start < stop: # Forward
1282
+
1283
+ def cont(v):
1284
+ return v < stop
1285
+
1286
+ step = ic.get_next
1287
+ else: # Reverse
1288
+
1289
+ def cont(v):
1290
+ return v > stop
1291
+
1292
+ step = ic.get_prev
1293
+ try:
1294
+ dt = step()
1295
+ while cont(dt):
1296
+ if ret_type is float:
1297
+ yield ic.get_current(float)
1298
+ else:
1299
+ yield dt
1300
+ dt = step()
1301
+ except CroniterBadDateError:
1302
+ # Stop iteration when this exception is raised; no match found within the given year range
1303
+ return
1304
+
1305
+
1306
+ class HashExpander:
1307
+ def __init__(self, cronit):
1308
+ self.cron = cronit
1309
+
1310
+ def do(self, idx, hash_type="h", hash_id=None, range_end=None, range_begin=None):
1311
+ """Return a hashed/random integer given range/hash information"""
1312
+ if range_end is None:
1313
+ range_end = self.cron.RANGES[idx][1]
1314
+ if range_begin is None:
1315
+ range_begin = self.cron.RANGES[idx][0]
1316
+ if hash_type == "r":
1317
+ crc = random.randint(0, 0xFFFFFFFF)
1318
+ else:
1319
+ crc = binascii.crc32(hash_id) & 0xFFFFFFFF
1320
+ return ((crc >> idx) % (range_end - range_begin + 1)) + range_begin
1321
+
1322
+ def match(self, efl, idx, expr, hash_id=None, **kw):
1323
+ return hash_expression_re.match(expr)
1324
+
1325
+ def expand(self, efl, idx, expr, hash_id=None, match="", **kw):
1326
+ """Expand a hashed/random expression to its normal representation"""
1327
+ if match == "":
1328
+ match = self.match(efl, idx, expr, hash_id, **kw)
1329
+ if not match:
1330
+ return expr
1331
+ m = match.groupdict()
1332
+
1333
+ if m["hash_type"] == "h" and hash_id is None:
1334
+ raise CroniterBadCronError("Hashed definitions must include hash_id")
1335
+
1336
+ if m["range_begin"] and m["range_end"]:
1337
+ if int(m["range_begin"]) >= int(m["range_end"]):
1338
+ raise CroniterBadCronError("Range end must be greater than range begin")
1339
+
1340
+ if m["range_begin"] and m["range_end"] and m["divisor"]:
1341
+ # Example: H(30-59)/10 -> 34-59/10 (i.e. 34,44,54)
1342
+ if int(m["divisor"]) == 0:
1343
+ raise CroniterBadCronError("Bad expression: {0}".format(expr))
1344
+
1345
+ return "{0}-{1}/{2}".format(
1346
+ self.do(
1347
+ idx,
1348
+ hash_type=m["hash_type"],
1349
+ hash_id=hash_id,
1350
+ range_begin=int(m["range_begin"]),
1351
+ range_end=int(m["divisor"]) - 1 + int(m["range_begin"]),
1352
+ ),
1353
+ int(m["range_end"]),
1354
+ int(m["divisor"]),
1355
+ )
1356
+ elif m["range_begin"] and m["range_end"]:
1357
+ # Example: H(0-29) -> 12
1358
+ return str(
1359
+ self.do(
1360
+ idx,
1361
+ hash_type=m["hash_type"],
1362
+ hash_id=hash_id,
1363
+ range_end=int(m["range_end"]),
1364
+ range_begin=int(m["range_begin"]),
1365
+ )
1366
+ )
1367
+ elif m["divisor"]:
1368
+ # Example: H/15 -> 7-59/15 (i.e. 7,22,37,52)
1369
+ if int(m["divisor"]) == 0:
1370
+ raise CroniterBadCronError("Bad expression: {0}".format(expr))
1371
+
1372
+ return "{0}-{1}/{2}".format(
1373
+ self.do(
1374
+ idx,
1375
+ hash_type=m["hash_type"],
1376
+ hash_id=hash_id,
1377
+ range_begin=self.cron.RANGES[idx][0],
1378
+ range_end=int(m["divisor"]) - 1 + self.cron.RANGES[idx][0],
1379
+ ),
1380
+ self.cron.RANGES[idx][1],
1381
+ int(m["divisor"]),
1382
+ )
1383
+ else:
1384
+ # Example: H -> 32
1385
+ return str(
1386
+ self.do(
1387
+ idx,
1388
+ hash_type=m["hash_type"],
1389
+ hash_id=hash_id,
1390
+ )
1391
+ )
1392
+
1393
+
1394
+ EXPANDERS = OrderedDict(
1395
+ [
1396
+ ("hash", HashExpander),
1397
+ ]
1398
+ )