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,2541 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ from datetime import datetime, timedelta
5
+ from functools import partial
6
+ from time import sleep
7
+
8
+ import dateutil.tz
9
+ import pytz
10
+
11
+ from mage_ai.shared.croniter import (
12
+ VALID_LEN_EXPRESSION,
13
+ CroniterBadCronError,
14
+ CroniterBadDateError,
15
+ CroniterNotAlphaError,
16
+ CroniterUnsupportedSyntaxError,
17
+ croniter,
18
+ datetime_to_timestamp,
19
+ )
20
+
21
+ # from mage_ai.shared.croniter.tests import base
22
+ from mage_ai.tests.base_test import TestCase
23
+
24
+
25
+ class CroniterTest(TestCase):
26
+ def testSecondSec(self):
27
+ base = datetime(2012, 4, 6, 13, 26, 10)
28
+ itr = croniter("* * * * * 15,25", base)
29
+ n = itr.get_next(datetime)
30
+ self.assertEqual(15, n.second)
31
+ n = itr.get_next(datetime)
32
+ self.assertEqual(25, n.second)
33
+ n = itr.get_next(datetime)
34
+ self.assertEqual(15, n.second)
35
+ self.assertEqual(27, n.minute)
36
+
37
+ def testSecond(self):
38
+ base = datetime(2012, 4, 6, 13, 26, 10)
39
+ itr = croniter("*/1 * * * * *", base)
40
+ n1 = itr.get_next(datetime)
41
+ self.assertEqual(base.year, n1.year)
42
+ self.assertEqual(base.month, n1.month)
43
+ self.assertEqual(base.day, n1.day)
44
+ self.assertEqual(base.hour, n1.hour)
45
+ self.assertEqual(base.minute, n1.minute)
46
+ self.assertEqual(base.second + 1, n1.second)
47
+
48
+ def testSecondRepeat(self):
49
+ base = datetime(2012, 4, 6, 13, 26, 36)
50
+ itr = croniter("* * * * * */15", base)
51
+ n1 = itr.get_next(datetime)
52
+ n2 = itr.get_next(datetime)
53
+ n3 = itr.get_next(datetime)
54
+ self.assertEqual(base.year, n1.year)
55
+ self.assertEqual(base.month, n1.month)
56
+ self.assertEqual(base.day, n1.day)
57
+ self.assertEqual(base.hour, n1.hour)
58
+ self.assertEqual(base.minute, n1.minute)
59
+ self.assertEqual(45, n1.second)
60
+ self.assertEqual(base.year, n2.year)
61
+ self.assertEqual(base.month, n2.month)
62
+ self.assertEqual(base.day, n2.day)
63
+ self.assertEqual(base.hour, n2.hour)
64
+ self.assertEqual(base.minute + 1, n2.minute)
65
+ self.assertEqual(0, n2.second)
66
+ self.assertEqual(base.year, n3.year)
67
+ self.assertEqual(base.month, n3.month)
68
+ self.assertEqual(base.day, n3.day)
69
+ self.assertEqual(base.hour, n3.hour)
70
+ self.assertEqual(base.minute + 1, n3.minute)
71
+ self.assertEqual(15, n3.second)
72
+
73
+ def testMinute(self):
74
+ # minute asterisk
75
+ base = datetime(2010, 1, 23, 12, 18)
76
+ itr = croniter("*/1 * * * *", base)
77
+ n1 = itr.get_next(datetime) # 19
78
+ self.assertEqual(base.year, n1.year)
79
+ self.assertEqual(base.month, n1.month)
80
+ self.assertEqual(base.day, n1.day)
81
+ self.assertEqual(base.hour, n1.hour)
82
+ self.assertEqual(base.minute, n1.minute - 1)
83
+ for _ in range(39): # ~ 58
84
+ itr.get_next()
85
+ n2 = itr.get_next(datetime)
86
+ self.assertEqual(n2.minute, 59)
87
+ n3 = itr.get_next(datetime)
88
+ self.assertEqual(n3.minute, 0)
89
+ self.assertEqual(n3.hour, 13)
90
+
91
+ itr = croniter("*/5 * * * *", base)
92
+ n4 = itr.get_next(datetime)
93
+ self.assertEqual(n4.minute, 20)
94
+ for _ in range(6):
95
+ itr.get_next()
96
+ n5 = itr.get_next(datetime)
97
+ self.assertEqual(n5.minute, 55)
98
+ n6 = itr.get_next(datetime)
99
+ self.assertEqual(n6.minute, 0)
100
+ self.assertEqual(n6.hour, 13)
101
+
102
+ def testHour(self):
103
+ base = datetime(2010, 1, 24, 12, 2)
104
+ itr = croniter("0 */3 * * *", base)
105
+ n1 = itr.get_next(datetime)
106
+ self.assertEqual(n1.hour, 15)
107
+ self.assertEqual(n1.minute, 0)
108
+ for _ in range(2):
109
+ itr.get_next()
110
+ n2 = itr.get_next(datetime)
111
+ self.assertEqual(n2.hour, 0)
112
+ self.assertEqual(n2.day, 25)
113
+
114
+ def testDay(self):
115
+ base = datetime(2010, 2, 24, 12, 9)
116
+ itr = croniter("0 0 */3 * *", base)
117
+ n1 = itr.get_next(datetime)
118
+ # 1 4 7 10 13 16 19 22 25 28
119
+ self.assertEqual(n1.day, 25)
120
+ n2 = itr.get_next(datetime)
121
+ self.assertEqual(n2.day, 28)
122
+ n3 = itr.get_next(datetime)
123
+ self.assertEqual(n3.day, 1)
124
+ self.assertEqual(n3.month, 3)
125
+
126
+ # test leap year
127
+ base = datetime(1996, 2, 27)
128
+ itr = croniter("0 0 * * *", base)
129
+ n1 = itr.get_next(datetime)
130
+ self.assertEqual(n1.day, 28)
131
+ self.assertEqual(n1.month, 2)
132
+ n2 = itr.get_next(datetime)
133
+ self.assertEqual(n2.day, 29)
134
+ self.assertEqual(n2.month, 2)
135
+
136
+ base2 = datetime(2000, 2, 27)
137
+ itr2 = croniter("0 0 * * *", base2)
138
+ n3 = itr2.get_next(datetime)
139
+ self.assertEqual(n3.day, 28)
140
+ self.assertEqual(n3.month, 2)
141
+ n4 = itr2.get_next(datetime)
142
+ self.assertEqual(n4.day, 29)
143
+ self.assertEqual(n4.month, 2)
144
+
145
+ def testDay2(self):
146
+ base3 = datetime(2024, 2, 28)
147
+ itr2 = croniter("* * 29 2 *", base3)
148
+ n3 = itr2.get_prev(datetime)
149
+ self.assertEqual(n3.year, 2020)
150
+ self.assertEqual(n3.month, 2)
151
+ self.assertEqual(n3.day, 29)
152
+
153
+ def testWeekDay(self):
154
+ base = datetime(2010, 2, 25)
155
+ itr = croniter("0 0 * * sat", base)
156
+ n1 = itr.get_next(datetime)
157
+ self.assertEqual(n1.isoweekday(), 6)
158
+ self.assertEqual(n1.day, 27)
159
+ n2 = itr.get_next(datetime)
160
+ self.assertEqual(n2.isoweekday(), 6)
161
+ self.assertEqual(n2.day, 6)
162
+ self.assertEqual(n2.month, 3)
163
+
164
+ base = datetime(2010, 1, 25)
165
+ itr = croniter("0 0 1 * wed", base)
166
+ n1 = itr.get_next(datetime)
167
+ self.assertEqual(n1.month, 1)
168
+ self.assertEqual(n1.day, 27)
169
+ self.assertEqual(n1.year, 2010)
170
+ n2 = itr.get_next(datetime)
171
+ self.assertEqual(n2.month, 2)
172
+ self.assertEqual(n2.day, 1)
173
+ self.assertEqual(n2.year, 2010)
174
+ n3 = itr.get_next(datetime)
175
+ self.assertEqual(n3.month, 2)
176
+ self.assertEqual(n3.day, 3)
177
+ self.assertEqual(n3.year, 2010)
178
+
179
+ def testNthWeekDay(self):
180
+ base = datetime(2010, 2, 25)
181
+ itr = croniter("0 0 * * sat#1", base)
182
+ n1 = itr.get_next(datetime)
183
+ self.assertEqual(n1.isoweekday(), 6)
184
+ self.assertEqual(n1.day, 6)
185
+ self.assertEqual(n1.month, 3)
186
+ n2 = itr.get_next(datetime)
187
+ self.assertEqual(n2.isoweekday(), 6)
188
+ self.assertEqual(n2.day, 3)
189
+ self.assertEqual(n2.month, 4)
190
+
191
+ base = datetime(2010, 1, 25)
192
+ itr = croniter("0 0 * * wed#5", base)
193
+ n1 = itr.get_next(datetime)
194
+ self.assertEqual(n1.month, 3)
195
+ self.assertEqual(n1.day, 31)
196
+ self.assertEqual(n1.year, 2010)
197
+ n2 = itr.get_next(datetime)
198
+ self.assertEqual(n2.month, 6)
199
+ self.assertEqual(n2.day, 30)
200
+ self.assertEqual(n2.year, 2010)
201
+ n3 = itr.get_next(datetime)
202
+ self.assertEqual(n3.month, 9)
203
+ self.assertEqual(n3.day, 29)
204
+ self.assertEqual(n3.year, 2010)
205
+
206
+ def testWeekDayDayAnd(self):
207
+ base = datetime(2010, 1, 25)
208
+ itr = croniter("0 0 1 * mon", base, day_or=False)
209
+ n1 = itr.get_next(datetime)
210
+ self.assertEqual(n1.month, 2)
211
+ self.assertEqual(n1.day, 1)
212
+ self.assertEqual(n1.year, 2010)
213
+ n2 = itr.get_next(datetime)
214
+ self.assertEqual(n2.month, 3)
215
+ self.assertEqual(n2.day, 1)
216
+ self.assertEqual(n2.year, 2010)
217
+ n3 = itr.get_next(datetime)
218
+ self.assertEqual(n3.month, 11)
219
+ self.assertEqual(n3.day, 1)
220
+ self.assertEqual(n3.year, 2010)
221
+
222
+ def testDomDowVixieCronBug(self):
223
+ expr = "0 16 */2 * sat"
224
+
225
+ # UNION OF "every odd-numbered day" and "every Saturday"
226
+ itr = croniter(expr, start_time=datetime(2023, 5, 2), ret_type=datetime)
227
+ self.assertEqual(itr.get_next(), datetime(2023, 5, 3, 16, 0, 0)) # Wed May 3 2023
228
+ self.assertEqual(itr.get_next(), datetime(2023, 5, 5, 16, 0, 0)) # Fri May 5 2023
229
+ self.assertEqual(itr.get_next(), datetime(2023, 5, 6, 16, 0, 0)) # Sat May 6 2023
230
+ self.assertEqual(itr.get_next(), datetime(2023, 5, 7, 16, 0, 0)) # Sun May 7 2023
231
+
232
+ # INTERSECTION OF "every odd-numbered day" and "every Saturday"
233
+ itr = croniter(
234
+ expr,
235
+ start_time=datetime(2023, 5, 2),
236
+ ret_type=datetime,
237
+ implement_cron_bug=True,
238
+ )
239
+ self.assertEqual(itr.get_next(), datetime(2023, 5, 13, 16, 0, 0)) # Sat May 13 2023
240
+ self.assertEqual(itr.get_next(), datetime(2023, 5, 27, 16, 0, 0)) # Sat May 27 2023
241
+ self.assertEqual(itr.get_next(), datetime(2023, 6, 3, 16, 0, 0)) # Sat June 3 2023
242
+ self.assertEqual(itr.get_next(), datetime(2023, 6, 17, 16, 0, 0)) # Sun June 17 2023
243
+
244
+ def testMonth(self):
245
+ base = datetime(2010, 1, 25)
246
+ itr = croniter("0 0 1 * *", base)
247
+ n1 = itr.get_next(datetime)
248
+ self.assertEqual(n1.month, 2)
249
+ self.assertEqual(n1.day, 1)
250
+ n2 = itr.get_next(datetime)
251
+ self.assertEqual(n2.month, 3)
252
+ self.assertEqual(n2.day, 1)
253
+ for _ in range(8):
254
+ itr.get_next()
255
+ n3 = itr.get_next(datetime)
256
+ self.assertEqual(n3.month, 12)
257
+ self.assertEqual(n3.year, 2010)
258
+ n4 = itr.get_next(datetime)
259
+ self.assertEqual(n4.month, 1)
260
+ self.assertEqual(n4.year, 2011)
261
+
262
+ def testLastDayOfMonth(self):
263
+ base = datetime(2015, 9, 4)
264
+ itr = croniter("0 0 l * *", base)
265
+ n1 = itr.get_next(datetime)
266
+ self.assertEqual(n1.month, 9)
267
+ self.assertEqual(n1.day, 30)
268
+ n2 = itr.get_next(datetime)
269
+ self.assertEqual(n2.month, 10)
270
+ self.assertEqual(n2.day, 31)
271
+ n3 = itr.get_next(datetime)
272
+ self.assertEqual(n3.month, 11)
273
+ self.assertEqual(n3.day, 30)
274
+ n4 = itr.get_next(datetime)
275
+ self.assertEqual(n4.month, 12)
276
+ self.assertEqual(n4.day, 31)
277
+
278
+ def testRangeWithUppercaseLastDayOfMonth(self):
279
+ base = datetime(2015, 9, 4)
280
+ itr = croniter("0 0 29-L * *", base)
281
+ n1 = itr.get_next(datetime)
282
+ self.assertEqual(n1.month, 9)
283
+ self.assertEqual(n1.day, 29)
284
+ n2 = itr.get_next(datetime)
285
+ self.assertEqual(n2.month, 9)
286
+ self.assertEqual(n2.day, 30)
287
+
288
+ def testPrevLastDayOfMonth(self):
289
+ base = datetime(2009, 12, 31, hour=20)
290
+ itr = croniter("0 0 l * *", base)
291
+ n1 = itr.get_prev(datetime)
292
+ self.assertEqual(n1.month, 12)
293
+ self.assertEqual(n1.day, 31)
294
+
295
+ base = datetime(2009, 12, 31)
296
+ itr = croniter("0 0 l * *", base)
297
+ n1 = itr.get_prev(datetime)
298
+ self.assertEqual(n1.month, 11)
299
+ self.assertEqual(n1.day, 30)
300
+
301
+ base = datetime(2010, 1, 5)
302
+ itr = croniter("0 0 l * *", base)
303
+ n1 = itr.get_prev(datetime)
304
+ self.assertEqual(n1.month, 12)
305
+ self.assertEqual(n1.day, 31)
306
+ n1 = itr.get_prev(datetime)
307
+ self.assertEqual(n1.month, 11)
308
+ self.assertEqual(n1.day, 30)
309
+ n1 = itr.get_prev(datetime)
310
+ self.assertEqual(n1.month, 10)
311
+ self.assertEqual(n1.day, 31)
312
+ n1 = itr.get_prev(datetime)
313
+ self.assertEqual(n1.month, 9)
314
+ self.assertEqual(n1.day, 30)
315
+
316
+ base = datetime(2010, 1, 31, minute=2)
317
+ itr = croniter("* * l * *", base)
318
+ n1 = itr.get_prev(datetime)
319
+ self.assertEqual(n1.month, 1)
320
+ self.assertEqual(n1.day, 31)
321
+ n1 = itr.get_prev(datetime)
322
+ self.assertEqual(n1.month, 1)
323
+ self.assertEqual(n1.day, 31)
324
+ n1 = itr.get_prev(datetime)
325
+ self.assertEqual(n1.month, 12)
326
+ self.assertEqual(n1.day, 31)
327
+ n1 = itr.get_prev(datetime)
328
+ self.assertEqual(n1.month, 12)
329
+ self.assertEqual(n1.day, 31)
330
+
331
+ def testError(self):
332
+ itr = croniter("* * * * *")
333
+ self.assertRaises(TypeError, itr.get_next, str)
334
+ self.assertRaises(ValueError, croniter, "* * * *")
335
+ self.assertRaises(ValueError, croniter, "-90 * * * *")
336
+ self.assertRaises(ValueError, croniter, "a * * * *")
337
+ self.assertRaises(ValueError, croniter, "* * * janu-jun *")
338
+ self.assertRaises(ValueError, croniter, "1-1_0 * * * *")
339
+ self.assertRaises(ValueError, croniter, "0-10/error * * * *")
340
+ self.assertRaises(ValueError, croniter, "0-10/ * * * *")
341
+ self.assertRaises(CroniterBadCronError, croniter, "0-1& * * * *", datetime.now())
342
+ self.assertRaises(ValueError, croniter, "* * 5-100 * *")
343
+
344
+ def testSundayToThursdayWithAlphaConversion(self):
345
+ base = datetime(2010, 8, 25, 15, 56) # wednesday
346
+ itr = croniter("30 22 * * sun-thu", base)
347
+ next = itr.get_next(datetime)
348
+
349
+ self.assertEqual(base.year, next.year)
350
+ self.assertEqual(base.month, next.month)
351
+ self.assertEqual(base.day, next.day)
352
+ self.assertEqual(22, next.hour)
353
+ self.assertEqual(30, next.minute)
354
+
355
+ def testOptimizeCronExpressions(self):
356
+ """Non-optimal cron expressions that can be simplified."""
357
+ wildcard = ["*"]
358
+ m, h, d, mon, dow, s = range(6)
359
+ # Test each field individually
360
+ self.assertEqual(croniter("0-59 0 1 1 0").expanded[m], wildcard)
361
+ self.assertEqual(croniter("0 0-23 1 1 0").expanded[h], wildcard)
362
+ self.assertEqual(croniter("0 0 1-31 1 0").expanded[dow], [0])
363
+ self.assertEqual(croniter("0 0 1-31 1 *").expanded[d], wildcard)
364
+ self.assertEqual(croniter("0 0 1 1-12 0").expanded[mon], wildcard)
365
+ self.assertEqual(croniter("0 0 1 1 0-6").expanded[dow],
366
+ [0, 1, 2, 3, 4, 5, 6])
367
+ self.assertEqual(croniter("0 0 * 1 0-6").expanded[dow], wildcard)
368
+ self.assertEqual(croniter("0 0 1 1 0-6").expanded[dow],
369
+ [0, 1, 2, 3, 4, 5, 6])
370
+ self.assertEqual(croniter("0 0 1 1 0-6,sat#3").expanded[dow],
371
+ [0, 1, 2, 3, 4, 5, 6])
372
+ self.assertEqual(croniter("0 0 * 1 0-6").expanded[dow], wildcard)
373
+ self.assertEqual(croniter("0 0 * 1 0-6,sat#3").expanded[dow], wildcard)
374
+ self.assertEqual(croniter("0 0 1 1 0 0-59").expanded[s], wildcard)
375
+ # Real life examples
376
+ self.assertEqual(croniter("30 1-12,0,10-23 15-21 * fri").expanded[h],
377
+ wildcard)
378
+ self.assertEqual(croniter("30 1-23,0 15-21 * fri").expanded[h],
379
+ wildcard)
380
+
381
+ def testBlockDupRanges(self):
382
+ """Ensure that duplicate/overlapping ranges are squashed"""
383
+ m, h, d, mon, dow, s = range(6)
384
+ self.assertEqual(croniter("* 5,5,1-6 * * *").expanded[h],
385
+ [1, 2, 3, 4, 5, 6])
386
+ self.assertEqual(croniter("* * * * 2-3,4-5,3,3,3").expanded[dow],
387
+ [2, 3, 4, 5])
388
+ self.assertEqual(croniter("* * * * * 1,5,*/20,20,15").expanded[s],
389
+ [0, 1, 5, 15, 20, 40])
390
+ self.assertEqual(croniter("* 4,1-4,5,4 * * *").expanded[h],
391
+ [1, 2, 3, 4, 5])
392
+ # Real life example
393
+ self.assertEqual(croniter("59 23 * 1 wed,fri,mon-thu,tue,tue")
394
+ .expanded[dow], [1, 2, 3, 4, 5])
395
+
396
+ def testPrevMinute(self):
397
+ base = datetime(2010, 8, 25, 15, 56)
398
+ itr = croniter("*/1 * * * *", base)
399
+ prev = itr.get_prev(datetime)
400
+ self.assertEqual(base.year, prev.year)
401
+ self.assertEqual(base.month, prev.month)
402
+ self.assertEqual(base.day, prev.day)
403
+ self.assertEqual(base.hour, prev.hour)
404
+ self.assertEqual(base.minute, prev.minute + 1)
405
+
406
+ base = datetime(2010, 8, 25, 15, 0)
407
+ itr = croniter("*/1 * * * *", base)
408
+ prev = itr.get_prev(datetime)
409
+ self.assertEqual(base.year, prev.year)
410
+ self.assertEqual(base.month, prev.month)
411
+ self.assertEqual(base.day, prev.day)
412
+ self.assertEqual(base.hour, prev.hour + 1)
413
+ self.assertEqual(59, prev.minute)
414
+
415
+ base = datetime(2010, 8, 25, 0, 0)
416
+ itr = croniter("*/1 * * * *", base)
417
+ prev = itr.get_prev(datetime)
418
+ self.assertEqual(base.year, prev.year)
419
+ self.assertEqual(base.month, prev.month)
420
+ self.assertEqual(base.day, prev.day + 1)
421
+ self.assertEqual(23, prev.hour)
422
+ self.assertEqual(59, prev.minute)
423
+
424
+ def testPrevDayOfMonthWithCrossing(self):
425
+ """
426
+ Test getting previous occurrence that crosses into previous month.
427
+ """
428
+ base = datetime(2012, 3, 15, 0, 0)
429
+ itr = croniter("0 0 22 * *", base)
430
+ prev = itr.get_prev(datetime)
431
+ self.assertEqual(prev.year, 2012)
432
+ self.assertEqual(prev.month, 2)
433
+ self.assertEqual(prev.day, 22)
434
+ self.assertEqual(prev.hour, 0)
435
+ self.assertEqual(prev.minute, 0)
436
+
437
+ def testPrevWeekDay(self):
438
+ base = datetime(2010, 8, 25, 15, 56)
439
+ itr = croniter("0 0 * * sat,sun", base)
440
+ prev1 = itr.get_prev(datetime)
441
+ self.assertEqual(prev1.year, base.year)
442
+ self.assertEqual(prev1.month, base.month)
443
+ self.assertEqual(prev1.day, 22)
444
+ self.assertEqual(prev1.hour, 0)
445
+ self.assertEqual(prev1.minute, 0)
446
+
447
+ prev2 = itr.get_prev(datetime)
448
+ self.assertEqual(prev2.year, base.year)
449
+ self.assertEqual(prev2.month, base.month)
450
+ self.assertEqual(prev2.day, 21)
451
+ self.assertEqual(prev2.hour, 0)
452
+ self.assertEqual(prev2.minute, 0)
453
+
454
+ prev3 = itr.get_prev(datetime)
455
+ self.assertEqual(prev3.year, base.year)
456
+ self.assertEqual(prev3.month, base.month)
457
+ self.assertEqual(prev3.day, 15)
458
+ self.assertEqual(prev3.hour, 0)
459
+ self.assertEqual(prev3.minute, 0)
460
+
461
+ def testPrevNthWeekDay(self):
462
+ base = datetime(2010, 8, 25, 15, 56)
463
+ itr = croniter("0 0 * * sat#1,sun#2", base)
464
+ prev1 = itr.get_prev(datetime)
465
+ self.assertEqual(prev1.year, base.year)
466
+ self.assertEqual(prev1.month, base.month)
467
+ self.assertEqual(prev1.day, 8)
468
+ self.assertEqual(prev1.hour, 0)
469
+ self.assertEqual(prev1.minute, 0)
470
+
471
+ prev2 = itr.get_prev(datetime)
472
+ self.assertEqual(prev2.year, base.year)
473
+ self.assertEqual(prev2.month, base.month)
474
+ self.assertEqual(prev2.day, 7)
475
+ self.assertEqual(prev2.hour, 0)
476
+ self.assertEqual(prev2.minute, 0)
477
+
478
+ prev3 = itr.get_prev(datetime)
479
+ self.assertEqual(prev3.year, base.year)
480
+ self.assertEqual(prev3.month, 7)
481
+ self.assertEqual(prev3.day, 11)
482
+ self.assertEqual(prev3.hour, 0)
483
+ self.assertEqual(prev3.minute, 0)
484
+
485
+ def testPrevWeekDay2(self):
486
+ base = datetime(2010, 8, 25, 15, 56)
487
+ itr = croniter("10 0 * * 0", base)
488
+ prev = itr.get_prev(datetime)
489
+ self.assertEqual(prev.day, 22)
490
+ self.assertEqual(prev.hour, 0)
491
+ self.assertEqual(prev.minute, 10)
492
+
493
+ def testISOWeekday(self):
494
+ base = datetime(2010, 2, 25)
495
+ itr = croniter("0 0 * * 6", base)
496
+ n1 = itr.get_next(datetime)
497
+ self.assertEqual(n1.isoweekday(), 6)
498
+ self.assertEqual(n1.day, 27)
499
+ n2 = itr.get_next(datetime)
500
+ self.assertEqual(n2.isoweekday(), 6)
501
+ self.assertEqual(n2.day, 6)
502
+ self.assertEqual(n2.month, 3)
503
+
504
+ def testBug1(self):
505
+ base = datetime(2012, 2, 24)
506
+ itr = croniter("5 0 */2 * *", base)
507
+ n1 = itr.get_prev(datetime)
508
+ self.assertEqual(n1.hour, 0)
509
+ self.assertEqual(n1.minute, 5)
510
+ self.assertEqual(n1.month, 2)
511
+ # month starts from 1, 3 .... then 21, 23
512
+ # so correct is not 22 but 23
513
+ self.assertEqual(n1.day, 23)
514
+
515
+ def testBug2(self):
516
+ base = datetime(2012, 1, 1, 0, 0)
517
+ iter = croniter("0 * * 3 *", base)
518
+ n1 = iter.get_next(datetime)
519
+ self.assertEqual(n1.year, base.year)
520
+ self.assertEqual(n1.month, 3)
521
+ self.assertEqual(n1.day, base.day)
522
+ self.assertEqual(n1.hour, base.hour)
523
+ self.assertEqual(n1.minute, base.minute)
524
+
525
+ n2 = iter.get_next(datetime)
526
+ self.assertEqual(n2.year, base.year)
527
+ self.assertEqual(n2.month, 3)
528
+ self.assertEqual(n2.day, base.day)
529
+ self.assertEqual(n2.hour, base.hour + 1)
530
+ self.assertEqual(n2.minute, base.minute)
531
+
532
+ n3 = iter.get_next(datetime)
533
+ self.assertEqual(n3.year, base.year)
534
+ self.assertEqual(n3.month, 3)
535
+ self.assertEqual(n3.day, base.day)
536
+ self.assertEqual(n3.hour, base.hour + 2)
537
+ self.assertEqual(n3.minute, base.minute)
538
+
539
+ def testBug3(self):
540
+ base = datetime(2013, 3, 1, 12, 17, 34, 257877)
541
+ c = croniter("00 03 16,30 * *", base)
542
+
543
+ n1 = c.get_next(datetime)
544
+ self.assertEqual(n1.month, 3)
545
+ self.assertEqual(n1.day, 16)
546
+
547
+ n2 = c.get_next(datetime)
548
+ self.assertEqual(n2.month, 3)
549
+ self.assertEqual(n2.day, 30)
550
+
551
+ n3 = c.get_next(datetime)
552
+ self.assertEqual(n3.month, 4)
553
+ self.assertEqual(n3.day, 16)
554
+
555
+ n4 = c.get_prev(datetime)
556
+ self.assertEqual(n4.month, 3)
557
+ self.assertEqual(n4.day, 30)
558
+
559
+ n5 = c.get_prev(datetime)
560
+ self.assertEqual(n5.month, 3)
561
+ self.assertEqual(n5.day, 16)
562
+
563
+ n6 = c.get_prev(datetime)
564
+ self.assertEqual(n6.month, 2)
565
+ self.assertEqual(n6.day, 16)
566
+
567
+ def test_bug34(self):
568
+ base = datetime(2012, 2, 24, 0, 0, 0)
569
+ itr = croniter("* * 31 2 *", base)
570
+ try:
571
+ itr.get_next(datetime)
572
+ except CroniterBadDateError as ex:
573
+ self.assertEqual("{0}".format(ex), "failed to find next date")
574
+
575
+ def testBug57(self):
576
+ base = datetime(2012, 2, 24, 0, 0, 0)
577
+ itr = croniter("0 4/6 * * *", base)
578
+ n1 = itr.get_next(datetime)
579
+ self.assertEqual(n1.hour, 4)
580
+ self.assertEqual(n1.minute, 0)
581
+ self.assertEqual(n1.month, 2)
582
+ self.assertEqual(n1.day, 24)
583
+
584
+ n1 = itr.get_prev(datetime)
585
+ self.assertEqual(n1.hour, 22)
586
+ self.assertEqual(n1.minute, 0)
587
+ self.assertEqual(n1.month, 2)
588
+ self.assertEqual(n1.day, 23)
589
+
590
+ itr = croniter("0 0/6 * * *", base)
591
+ n1 = itr.get_next(datetime)
592
+ self.assertEqual(n1.hour, 6)
593
+ self.assertEqual(n1.minute, 0)
594
+ self.assertEqual(n1.month, 2)
595
+ self.assertEqual(n1.day, 24)
596
+
597
+ n1 = itr.get_prev(datetime)
598
+ self.assertEqual(n1.hour, 0)
599
+ self.assertEqual(n1.minute, 0)
600
+ self.assertEqual(n1.month, 2)
601
+ self.assertEqual(n1.day, 24)
602
+
603
+ def test_multiple_months(self):
604
+ base = datetime(2016, 3, 1, 0, 0, 0)
605
+ itr = croniter("0 0 1 3,6,9,12 *", base)
606
+ n1 = itr.get_next(datetime)
607
+ self.assertEqual(n1.hour, 0)
608
+ self.assertEqual(n1.month, 6)
609
+ self.assertEqual(n1.day, 1)
610
+ self.assertEqual(n1.year, 2016)
611
+
612
+ base = datetime(2016, 2, 15, 0, 0, 0)
613
+ itr = croniter("0 0 1 3,6,9,12 *", base)
614
+ n1 = itr.get_next(datetime)
615
+ self.assertEqual(n1.hour, 0)
616
+ self.assertEqual(n1.month, 3)
617
+ self.assertEqual(n1.day, 1)
618
+ self.assertEqual(n1.year, 2016)
619
+
620
+ base = datetime(2016, 12, 3, 10, 0, 0)
621
+ itr = croniter("0 0 1 3,6,9,12 *", base)
622
+ n1 = itr.get_next(datetime)
623
+ self.assertEqual(n1.hour, 0)
624
+ self.assertEqual(n1.month, 3)
625
+ self.assertEqual(n1.day, 1)
626
+ self.assertEqual(n1.year, 2017)
627
+
628
+ # The result with this parameters was incorrect.
629
+ # self.assertEqual(p1.month, 12
630
+ # AssertionError: 9 != 12
631
+ base = datetime(2016, 3, 1, 0, 0, 0)
632
+ itr = croniter("0 0 1 3,6,9,12 *", base)
633
+ p1 = itr.get_prev(datetime)
634
+ self.assertEqual(p1.hour, 0)
635
+ self.assertEqual(p1.month, 12)
636
+ self.assertEqual(p1.day, 1)
637
+ self.assertEqual(p1.year, 2015)
638
+
639
+ # check my change resolves another hidden bug.
640
+ base = datetime(2016, 2, 1, 0, 0, 0)
641
+ itr = croniter("0 0 1,15,31 * *", base)
642
+ p1 = itr.get_prev(datetime)
643
+ self.assertEqual(p1.hour, 0)
644
+ self.assertEqual(p1.month, 1)
645
+ self.assertEqual(p1.day, 31)
646
+ self.assertEqual(p1.year, 2016)
647
+
648
+ base = datetime(2016, 6, 1, 0, 0, 0)
649
+ itr = croniter("0 0 1 3,6,9,12 *", base)
650
+ p1 = itr.get_prev(datetime)
651
+ self.assertEqual(p1.hour, 0)
652
+ self.assertEqual(p1.month, 3)
653
+ self.assertEqual(p1.day, 1)
654
+ self.assertEqual(p1.year, 2016)
655
+
656
+ base = datetime(2016, 3, 1, 0, 0, 0)
657
+ itr = croniter("0 0 1 1,3,6,9,12 *", base)
658
+ p1 = itr.get_prev(datetime)
659
+ self.assertEqual(p1.hour, 0)
660
+ self.assertEqual(p1.month, 1)
661
+ self.assertEqual(p1.day, 1)
662
+ self.assertEqual(p1.year, 2016)
663
+
664
+ base = datetime(2016, 3, 1, 0, 0, 0)
665
+ itr = croniter("0 0 1 1,3,6,9,12 *", base)
666
+ p1 = itr.get_prev(datetime)
667
+ self.assertEqual(p1.hour, 0)
668
+ self.assertEqual(p1.month, 1)
669
+ self.assertEqual(p1.day, 1)
670
+ self.assertEqual(p1.year, 2016)
671
+
672
+ def test_rangeGenerator(self):
673
+ base = datetime(2013, 3, 4, 0, 0)
674
+ itr = croniter("1-9/2 0 1 * *", base)
675
+ n1 = itr.get_next(datetime)
676
+ n2 = itr.get_next(datetime)
677
+ n3 = itr.get_next(datetime)
678
+ n4 = itr.get_next(datetime)
679
+ n5 = itr.get_next(datetime)
680
+ self.assertEqual(n1.minute, 1)
681
+ self.assertEqual(n2.minute, 3)
682
+ self.assertEqual(n3.minute, 5)
683
+ self.assertEqual(n4.minute, 7)
684
+ self.assertEqual(n5.minute, 9)
685
+
686
+ def testPreviousHour(self):
687
+ base = datetime(2012, 6, 23, 17, 41)
688
+ itr = croniter("* 10 * * *", base)
689
+ prev1 = itr.get_prev(datetime)
690
+ self.assertEqual(prev1.year, base.year)
691
+ self.assertEqual(prev1.month, base.month)
692
+ self.assertEqual(prev1.day, base.day)
693
+ self.assertEqual(prev1.hour, 10)
694
+ self.assertEqual(prev1.minute, 59)
695
+
696
+ def testPreviousDay(self):
697
+ base = datetime(2012, 6, 27, 0, 15)
698
+ itr = croniter("* * 26 * *", base)
699
+ prev1 = itr.get_prev(datetime)
700
+ self.assertEqual(prev1.year, base.year)
701
+ self.assertEqual(prev1.month, base.month)
702
+ self.assertEqual(prev1.day, 26)
703
+ self.assertEqual(prev1.hour, 23)
704
+ self.assertEqual(prev1.minute, 59)
705
+
706
+ def testPreviousMonth(self):
707
+ base = datetime(2012, 6, 18, 0, 15)
708
+ itr = croniter("* * * 5 *", base)
709
+ prev1 = itr.get_prev(datetime)
710
+ self.assertEqual(prev1.year, base.year)
711
+ self.assertEqual(prev1.month, 5)
712
+ self.assertEqual(prev1.day, 31)
713
+ self.assertEqual(prev1.hour, 23)
714
+ self.assertEqual(prev1.minute, 59)
715
+
716
+ def testPreviousDow(self):
717
+ base = datetime(2012, 5, 13, 18, 48)
718
+ itr = croniter("* * * * sat", base)
719
+ prev1 = itr.get_prev(datetime)
720
+ self.assertEqual(prev1.year, base.year)
721
+ self.assertEqual(prev1.month, base.month)
722
+ self.assertEqual(prev1.day, 12)
723
+ self.assertEqual(prev1.hour, 23)
724
+ self.assertEqual(prev1.minute, 59)
725
+
726
+ def testGetCurrent(self):
727
+ base = datetime(2012, 9, 25, 11, 24)
728
+ itr = croniter("* * * * *", base)
729
+ res = itr.get_current(datetime)
730
+ self.assertEqual(base.year, res.year)
731
+ self.assertEqual(base.month, res.month)
732
+ self.assertEqual(base.day, res.day)
733
+ self.assertEqual(base.hour, res.hour)
734
+ self.assertEqual(base.minute, res.minute)
735
+
736
+ def testTimezone(self):
737
+ base = datetime(2013, 3, 4, 12, 15)
738
+ itr = croniter("* * * * *", base)
739
+ n1 = itr.get_next(datetime)
740
+ self.assertEqual(n1.tzinfo, None)
741
+
742
+ tokyo = pytz.timezone("Asia/Tokyo")
743
+ itr2 = croniter("* * * * *", tokyo.localize(base))
744
+ n2 = itr2.get_next(datetime)
745
+ self.assertEqual(n2.tzinfo.zone, "Asia/Tokyo")
746
+
747
+ def testTimezoneDateutil(self):
748
+ tokyo = dateutil.tz.gettz("Asia/Tokyo")
749
+ base = datetime(2013, 3, 4, 12, 15, tzinfo=tokyo)
750
+ itr = croniter("* * * * *", base)
751
+ n1 = itr.get_next(datetime)
752
+ self.assertEqual(n1.tzinfo.tzname(n1), "JST")
753
+
754
+ def testInitNoStartTime(self):
755
+ itr = croniter("* * * * *")
756
+ sleep(0.01)
757
+ itr2 = croniter("* * * * *")
758
+ # Greater does not exists in py26
759
+ self.assertTrue(itr2.cur > itr.cur)
760
+
761
+ def assertScheduleTimezone(self, callback, expected_schedule):
762
+ for expected_date, expected_offset in expected_schedule:
763
+ d = callback()
764
+ self.assertEqual(expected_date, d.replace(tzinfo=None))
765
+ self.assertEqual(expected_offset,
766
+ croniter._timedelta_to_seconds(d.utcoffset()))
767
+
768
+ def testTimezoneWinterTime(self):
769
+ tz = pytz.timezone("Europe/Athens")
770
+
771
+ expected_schedule = [
772
+ (datetime(2013, 10, 27, 2, 30, 0), 10800),
773
+ (datetime(2013, 10, 27, 3, 0, 0), 10800),
774
+ (datetime(2013, 10, 27, 3, 30, 0), 10800),
775
+ (datetime(2013, 10, 27, 3, 0, 0), 7200),
776
+ (datetime(2013, 10, 27, 3, 30, 0), 7200),
777
+ (datetime(2013, 10, 27, 4, 0, 0), 7200),
778
+ (datetime(2013, 10, 27, 4, 30, 0), 7200),
779
+ ]
780
+
781
+ start = datetime(2013, 10, 27, 2, 0, 0)
782
+ ct = croniter("*/30 * * * *", tz.localize(start))
783
+ self.assertScheduleTimezone(lambda: ct.get_next(datetime),
784
+ expected_schedule)
785
+
786
+ start = datetime(2013, 10, 27, 5, 0, 0)
787
+ ct = croniter("*/30 * * * *", tz.localize(start))
788
+ self.assertScheduleTimezone(lambda: ct.get_prev(datetime),
789
+ reversed(expected_schedule))
790
+
791
+ def testTimezoneSummerTime(self):
792
+ tz = pytz.timezone("Europe/Athens")
793
+
794
+ expected_schedule = [
795
+ (datetime(2013, 3, 31, 1, 30, 0), 7200),
796
+ (datetime(2013, 3, 31, 2, 0, 0), 7200),
797
+ (datetime(2013, 3, 31, 2, 30, 0), 7200),
798
+ (datetime(2013, 3, 31, 4, 0, 0), 10800),
799
+ (datetime(2013, 3, 31, 4, 30, 0), 10800),
800
+ ]
801
+
802
+ start = datetime(2013, 3, 31, 1, 0, 0)
803
+ ct = croniter("*/30 * * * *", tz.localize(start))
804
+ self.assertScheduleTimezone(lambda: ct.get_next(datetime),
805
+ expected_schedule)
806
+
807
+ start = datetime(2013, 3, 31, 5, 0, 0)
808
+ ct = croniter("*/30 * * * *", tz.localize(start))
809
+ self.assertScheduleTimezone(lambda: ct.get_prev(datetime),
810
+ reversed(expected_schedule))
811
+
812
+ def test_std_dst(self):
813
+ """
814
+ DST tests
815
+
816
+ This fixes https://github.com/taichino/croniter/issues/82
817
+
818
+ """
819
+ tz = pytz.timezone("Europe/Warsaw")
820
+ # -> 2017-03-26 01:59+1:00 -> 03:00+2:00
821
+ local_date = tz.localize(datetime(2017, 3, 26))
822
+ val = croniter("0 0 * * *", local_date).get_next(datetime)
823
+ self.assertEqual(val, tz.localize(datetime(2017, 3, 27)))
824
+ #
825
+ local_date = tz.localize(datetime(2017, 3, 26, 1))
826
+ cr = croniter("0 * * * *", local_date)
827
+ val = cr.get_next(datetime)
828
+ self.assertEqual(val, tz.localize(datetime(2017, 3, 26, 3)))
829
+ val = cr.get_current(datetime)
830
+ self.assertEqual(val, tz.localize(datetime(2017, 3, 26, 3)))
831
+
832
+ # -> 2017-10-29 02:59+2:00 -> 02:00+1:00
833
+ local_date = tz.localize(datetime(2017, 10, 29))
834
+ val = croniter("0 0 * * *", local_date).get_next(datetime)
835
+ self.assertEqual(val, tz.localize(datetime(2017, 10, 30)))
836
+ local_date = tz.localize(datetime(2017, 10, 29, 1, 59))
837
+ val = croniter("0 * * * *", local_date).get_next(datetime)
838
+ self.assertEqual(
839
+ val.replace(tzinfo=None),
840
+ tz.localize(datetime(2017, 10, 29, 2)).replace(tzinfo=None),
841
+ )
842
+ local_date = tz.localize(datetime(2017, 10, 29, 2))
843
+ val = croniter("0 * * * *", local_date).get_next(datetime)
844
+ self.assertEqual(val, tz.localize(datetime(2017, 10, 29, 3)))
845
+ local_date = tz.localize(datetime(2017, 10, 29, 3))
846
+ val = croniter("0 * * * *", local_date).get_next(datetime)
847
+ self.assertEqual(val, tz.localize(datetime(2017, 10, 29, 4)))
848
+ local_date = tz.localize(datetime(2017, 10, 29, 4))
849
+ val = croniter("0 * * * *", local_date).get_next(datetime)
850
+ self.assertEqual(val, tz.localize(datetime(2017, 10, 29, 5)))
851
+ local_date = tz.localize(datetime(2017, 10, 29, 5))
852
+ val = croniter("0 * * * *", local_date).get_next(datetime)
853
+ self.assertEqual(val, tz.localize(datetime(2017, 10, 29, 6)))
854
+
855
+ def test_std_dst2(self):
856
+ """
857
+ DST tests
858
+
859
+ This fixes https://github.com/taichino/croniter/issues/87
860
+
861
+ São Paulo, Brazil: 18/02/2018 00:00 -> 17/02/2018 23:00
862
+
863
+ """
864
+ tz = pytz.timezone("America/Sao_Paulo")
865
+ local_dates = [
866
+ # 17-22: 00 -> 18-00:00
867
+ (tz.localize(datetime(2018, 2, 17, 21, 0, 0)),
868
+ "2018-02-18 00:00:00-03:00"),
869
+ # 17-23: 00 -> 18-00:00
870
+ (tz.localize(datetime(2018, 2, 17, 22, 0, 0)),
871
+ "2018-02-18 00:00:00-03:00"),
872
+ # 17-23: 00 -> 18-00:00
873
+ (tz.localize(datetime(2018, 2, 17, 23, 0, 0)),
874
+ "2018-02-18 00:00:00-03:00"),
875
+ # 18-00: 00 -> 19-00:00
876
+ (tz.localize(datetime(2018, 2, 18, 0, 0, 0)),
877
+ "2018-02-19 00:00:00-03:00"),
878
+ # 17-22: 00 -> 18-00:00
879
+ (tz.localize(datetime(2018, 2, 17, 21, 5, 0)),
880
+ "2018-02-18 00:00:00-03:00"),
881
+ # 17-23: 00 -> 18-00:00
882
+ (tz.localize(datetime(2018, 2, 17, 22, 5, 0)),
883
+ "2018-02-18 00:00:00-03:00"),
884
+ # 17-23: 00 -> 18-00:00
885
+ (tz.localize(datetime(2018, 2, 17, 23, 5, 0)),
886
+ "2018-02-18 00:00:00-03:00"),
887
+ # 18-00: 00 -> 19-00:00
888
+ (tz.localize(datetime(2018, 2, 18, 0, 5, 0)),
889
+ "2018-02-19 00:00:00-03:00"),
890
+ ]
891
+ ret1 = [croniter("0 0 * * *", d[0]).get_next(datetime)
892
+ for d in local_dates]
893
+ sret1 = ["{0}".format(d) for d in ret1]
894
+ lret1 = ["{0}".format(d[1]) for d in local_dates]
895
+ self.assertEqual(sret1, lret1)
896
+
897
+ def test_std_dst3(self):
898
+ """
899
+ DST tests
900
+
901
+ This fixes https://github.com/taichino/croniter/issues/90
902
+
903
+ Adelaide, Australia: 15/04/2020 00:00 -> 15/03/2020
904
+
905
+ """
906
+
907
+ tz = pytz.timezone("Australia/Adelaide")
908
+
909
+ schedule = croniter("0 0 24 * *", tz.localize(datetime(2020, 4, 15)))
910
+ val1 = schedule.get_prev(datetime)
911
+ dt1 = tz.localize(datetime(2020, 3, 24))
912
+ self.assertEqual(val1, dt1)
913
+
914
+ val2 = schedule.get_next(datetime)
915
+ dt2 = tz.localize(datetime(2020, 4, 24))
916
+ self.assertEqual(val2, dt2)
917
+
918
+ def test_error_alpha_cron(self):
919
+ self.assertRaises(CroniterNotAlphaError, croniter.expand,
920
+ "* * * janu-jun *")
921
+
922
+ def test_error_bad_cron(self):
923
+ self.assertRaises(CroniterBadCronError, croniter.expand, "* * * *")
924
+ self.assertRaises(
925
+ CroniterBadCronError,
926
+ croniter.expand,
927
+ ("* " * (max(VALID_LEN_EXPRESSION) + 1)).strip(),
928
+ )
929
+
930
+ def test_is_valid(self):
931
+ self.assertTrue(croniter.is_valid("0 * * * *"))
932
+ self.assertFalse(croniter.is_valid("0 * *"))
933
+ self.assertFalse(croniter.is_valid("* * * janu-jun *"))
934
+ self.assertTrue(croniter.is_valid("H 0 * * *", hash_id="abc"))
935
+
936
+ def test_exactly_the_same_minute(self):
937
+ base = datetime(2018, 3, 5, 12, 30, 50)
938
+ itr = croniter("30 7,12,17 * * *", base)
939
+ n1 = itr.get_prev(datetime)
940
+ self.assertEqual(12, n1.hour)
941
+
942
+ n2 = itr.get_prev(datetime)
943
+ self.assertEqual(7, n2.hour)
944
+
945
+ n3 = itr.get_next(datetime)
946
+ self.assertEqual(12, n3.hour)
947
+
948
+ def test_next_when_now_satisfies_cron(self):
949
+ ts_a = datetime(2018, 5, 21, 0, 3, 0)
950
+ ts_b = datetime(2018, 5, 21, 0, 4, 20)
951
+ test_cron = "4 * * * *"
952
+
953
+ next_a = croniter(test_cron, start_time=ts_a).get_next()
954
+ next_b = croniter(test_cron, start_time=ts_b).get_next()
955
+
956
+ self.assertTrue(next_b > next_a)
957
+
958
+ def test_milliseconds(self):
959
+ """
960
+ https://github.com/taichino/croniter/issues/107
961
+ """
962
+
963
+ _croniter = partial(croniter, "0 10 * * *", ret_type=datetime)
964
+
965
+ dt = datetime(2018, 1, 2, 10, 0, 0, 500)
966
+ self.assertEqual(
967
+ _croniter(start_time=dt).get_prev(),
968
+ datetime(2018, 1, 2, 10, 0),
969
+ )
970
+ self.assertEqual(
971
+ _croniter(start_time=dt).get_next(),
972
+ datetime(2018, 1, 3, 10, 0),
973
+ )
974
+
975
+ dt = datetime(2018, 1, 2, 10, 0, 1, 0)
976
+ self.assertEqual(
977
+ _croniter(start_time=dt).get_prev(),
978
+ datetime(2018, 1, 2, 10, 0),
979
+ )
980
+ self.assertEqual(
981
+ _croniter(start_time=dt).get_next(),
982
+ datetime(2018, 1, 3, 10, 0),
983
+ )
984
+
985
+ dt = datetime(2018, 1, 2, 9, 59, 59, 999999)
986
+ self.assertEqual(
987
+ _croniter(start_time=dt).get_prev(),
988
+ datetime(2018, 1, 1, 10, 0),
989
+ )
990
+ self.assertEqual(
991
+ _croniter(start_time=dt).get_next(),
992
+ datetime(2018, 1, 2, 10, 0),
993
+ )
994
+
995
+ def test_invalid_zerorepeat(self):
996
+ self.assertFalse(croniter.is_valid("*/0 * * * *"))
997
+
998
+ def test_weekday_range(self):
999
+ ret = []
1000
+ # jan 14 is monday
1001
+ dt = datetime(2019, 1, 14, 0, 0, 0, 0)
1002
+ for _ in range(10):
1003
+ c = croniter("0 0 * * 2-4 *", start_time=dt)
1004
+ dt = datetime.fromtimestamp(
1005
+ c.get_next(),
1006
+ dateutil.tz.tzutc()).replace(tzinfo=None)
1007
+ ret.append(dt)
1008
+ dt += timedelta(days=1)
1009
+ sret = ["{0}".format(r) for r in ret]
1010
+ self.assertEqual(
1011
+ sret,
1012
+ [
1013
+ "2019-01-15 00:00:00",
1014
+ "2019-01-16 00:00:01",
1015
+ "2019-01-17 00:00:02",
1016
+ "2019-01-22 00:00:00",
1017
+ "2019-01-23 00:00:01",
1018
+ "2019-01-24 00:00:02",
1019
+ "2019-01-29 00:00:00",
1020
+ "2019-01-30 00:00:01",
1021
+ "2019-01-31 00:00:02",
1022
+ "2019-02-05 00:00:00",
1023
+ ],
1024
+ )
1025
+ ret = []
1026
+ dt = datetime(2019, 1, 14, 0, 0, 0, 0)
1027
+ for _ in range(10):
1028
+ c = croniter("0 0 * * 0-6 *", start_time=dt)
1029
+ dt = datetime.fromtimestamp(
1030
+ c.get_next(),
1031
+ dateutil.tz.tzutc()).replace(tzinfo=None)
1032
+ ret.append(dt)
1033
+ dt += timedelta(days=1)
1034
+ sret = ["{0}".format(r) for r in ret]
1035
+ self.assertEqual(
1036
+ sret,
1037
+ [
1038
+ "2019-01-14 00:00:01",
1039
+ "2019-01-15 00:00:02",
1040
+ "2019-01-16 00:00:03",
1041
+ "2019-01-17 00:00:04",
1042
+ "2019-01-18 00:00:05",
1043
+ "2019-01-19 00:00:06",
1044
+ "2019-01-20 00:00:07",
1045
+ "2019-01-21 00:00:08",
1046
+ "2019-01-22 00:00:09",
1047
+ "2019-01-23 00:00:10",
1048
+ ],
1049
+ )
1050
+
1051
+ def test_issue_monsun_117(self):
1052
+ ret = []
1053
+ dt = datetime(2019, 1, 14, 0, 0, 0, 0)
1054
+ for _ in range(12):
1055
+ # c = croniter("0 0 * * Mon-Sun *", start_time=dt)
1056
+ c = croniter("0 0 * * Wed-Sun *", start_time=dt)
1057
+ dt = datetime.fromtimestamp(
1058
+ c.get_next(),
1059
+ tz=dateutil.tz.tzutc()).replace(tzinfo=None)
1060
+ ret.append(dt)
1061
+ dt += timedelta(days=1)
1062
+ sret = ["{0}".format(r) for r in ret]
1063
+ self.assertEqual(
1064
+ sret,
1065
+ [
1066
+ "2019-01-16 00:00:00",
1067
+ "2019-01-17 00:00:01",
1068
+ "2019-01-18 00:00:02",
1069
+ "2019-01-19 00:00:03",
1070
+ "2019-01-20 00:00:04",
1071
+ "2019-01-23 00:00:00",
1072
+ "2019-01-24 00:00:01",
1073
+ "2019-01-25 00:00:02",
1074
+ "2019-01-26 00:00:03",
1075
+ "2019-01-27 00:00:04",
1076
+ "2019-01-30 00:00:00",
1077
+ "2019-01-31 00:00:01",
1078
+ ],
1079
+ )
1080
+
1081
+ def test_mixdow(self):
1082
+ base = datetime(2018, 10, 1, 0, 0)
1083
+ itr = croniter("1 1 7,14,21,L * *", base)
1084
+ self.assertTrue(isinstance(itr.get_next(), float))
1085
+
1086
+ def test_match(self):
1087
+ self.assertTrue(croniter.match(
1088
+ "0 0 * * *", datetime(2019, 1, 14, 0, 0, 0, 0)))
1089
+ self.assertFalse(croniter.match(
1090
+ "0 0 * * *", datetime(2019, 1, 14, 0, 1, 0, 0)))
1091
+ self.assertTrue(croniter.match(
1092
+ "0 0 * * * 1", datetime(2023, 5, 25, 0, 0, 1, 0)))
1093
+ self.assertFalse(croniter.match(
1094
+ "0 0 * * * 1", datetime(2023, 5, 25, 0, 0, 2, 0)))
1095
+ self.assertTrue(croniter.match(
1096
+ "31 * * * *", datetime(2019, 1, 14, 1, 31, 0, 0)))
1097
+ self.assertTrue(croniter.match(
1098
+ "0 0 10 * wed", datetime(2020, 6, 10, 0, 0, 0, 0), day_or=True))
1099
+ self.assertTrue(croniter.match(
1100
+ "0 0 10 * fri", datetime(2020, 6, 10, 0, 0, 0, 0), day_or=True))
1101
+ self.assertTrue(croniter.match(
1102
+ "0 0 10 * fri", datetime(2020, 6, 12, 0, 0, 0, 0), day_or=True))
1103
+ self.assertTrue(croniter.match(
1104
+ "0 0 10 * wed", datetime(2020, 6, 10, 0, 0, 0, 0), day_or=False))
1105
+ self.assertFalse(croniter.match(
1106
+ "0 0 10 * fri", datetime(2020, 6, 10, 0, 0, 0, 0), day_or=False))
1107
+ self.assertFalse(croniter.match(
1108
+ "0 0 10 * fri", datetime(2020, 6, 12, 0, 0, 0, 0), day_or=False))
1109
+
1110
+ def test_match_handle_bad_cron(self):
1111
+ # some cron expression can"t get prev value and should not raise exception
1112
+ self.assertFalse(croniter.match(
1113
+ "0 0 31 1 1#1", datetime(2020, 1, 31), day_or=False))
1114
+ self.assertFalse(
1115
+ croniter.match(
1116
+ "0 0 31 1 * 0 2024/2",
1117
+ datetime(2020, 1, 31),
1118
+ )
1119
+ )
1120
+
1121
+ def test_match_range(self):
1122
+ self.assertTrue(
1123
+ croniter.match_range(
1124
+ "0 0 * * *",
1125
+ datetime(2019, 1, 13, 0, 59, 0, 0),
1126
+ datetime(2019, 1, 14, 0, 1, 0, 0),
1127
+ )
1128
+ )
1129
+ self.assertFalse(
1130
+ croniter.match_range(
1131
+ "0 0 * * *",
1132
+ datetime(2019, 1, 13, 0, 1, 0, 0),
1133
+ datetime(2019, 1, 13, 0, 59, 0, 0),
1134
+ )
1135
+ )
1136
+ self.assertTrue(
1137
+ croniter.match_range(
1138
+ "0 0 * * * 1",
1139
+ datetime(2023, 5, 25, 0, 0, 0, 0),
1140
+ datetime(2023, 5, 25, 0, 0, 2, 0),
1141
+ )
1142
+ )
1143
+ self.assertFalse(
1144
+ croniter.match_range(
1145
+ "0 0 * * * 1",
1146
+ datetime(2023, 5, 25, 0, 0, 2, 0),
1147
+ datetime(2023, 5, 25, 0, 0, 4, 0),
1148
+ )
1149
+ )
1150
+ self.assertTrue(
1151
+ croniter.match_range(
1152
+ "0 0 * * * 1",
1153
+ datetime(2023, 5, 25, 0, 0, 1, 0),
1154
+ datetime(2023, 5, 25, 0, 0, 4, 0),
1155
+ )
1156
+ )
1157
+ self.assertTrue(
1158
+ croniter.match_range(
1159
+ "31 * * * *",
1160
+ datetime(2019, 1, 14, 1, 30, 0, 0),
1161
+ datetime(2019, 1, 14, 1, 31, 0, 0),
1162
+ )
1163
+ )
1164
+ self.assertTrue(
1165
+ croniter.match_range(
1166
+ "0 0 10 * wed",
1167
+ datetime(2020, 6, 9, 0, 0, 0, 0),
1168
+ datetime(2020, 6, 11, 0, 0, 0, 0),
1169
+ day_or=True,
1170
+ )
1171
+ )
1172
+ self.assertTrue(
1173
+ croniter.match_range(
1174
+ "0 0 10 * fri",
1175
+ datetime(2020, 6, 10, 0, 0, 0, 0),
1176
+ datetime(2020, 6, 11, 0, 0, 0, 0),
1177
+ day_or=True,
1178
+ )
1179
+ )
1180
+ self.assertTrue(
1181
+ croniter.match_range(
1182
+ "0 0 10 * fri",
1183
+ datetime(2020, 6, 11, 0, 0, 0, 0),
1184
+ datetime(2020, 6, 12, 0, 0, 0, 0),
1185
+ day_or=True,
1186
+ )
1187
+ )
1188
+ self.assertTrue(
1189
+ croniter.match_range(
1190
+ "0 0 10 * wed",
1191
+ datetime(2020, 6, 9, 0, 0, 0, 0),
1192
+ datetime(2020, 6, 12, 0, 0, 0, 0),
1193
+ day_or=False,
1194
+ )
1195
+ )
1196
+ self.assertFalse(
1197
+ croniter.match_range(
1198
+ "0 0 10 * fri",
1199
+ datetime(2020, 6, 8, 0, 0, 0, 0),
1200
+ datetime(2020, 6, 9, 0, 0, 0, 0),
1201
+ day_or=False,
1202
+ )
1203
+ )
1204
+ self.assertFalse(
1205
+ croniter.match_range(
1206
+ "0 0 10 * fri",
1207
+ datetime(2020, 6, 7, 0, 0, 0, 0),
1208
+ datetime(2020, 6, 11, 0, 0, 0, 0),
1209
+ day_or=False,
1210
+ )
1211
+ )
1212
+ self.assertFalse(
1213
+ croniter.match_range(
1214
+ "2 4 1 * wed",
1215
+ datetime(2019, 1, 1, 3, 2, 0, 0),
1216
+ datetime(2019, 1, 1, 5, 2, 0, 0),
1217
+ day_or=False,
1218
+ )
1219
+ )
1220
+
1221
+ def test_dst_issue90_st31ny(self):
1222
+ tz = pytz.timezone("Europe/Paris")
1223
+ now = datetime(2020, 3, 29, 1, 59, 55, tzinfo=tz)
1224
+ it = croniter("1 2 * * *", now)
1225
+ #
1226
+ # Taking around DST @ 29/03/20 01:59
1227
+ #
1228
+ ret = [
1229
+ it.get_next(datetime).isoformat(),
1230
+ it.get_prev(datetime).isoformat(),
1231
+ it.get_prev(datetime).isoformat(),
1232
+ it.get_next(datetime).isoformat(),
1233
+ it.get_next(datetime).isoformat(),
1234
+ ]
1235
+ self.assertEqual(
1236
+ ret,
1237
+ [
1238
+ "2020-03-30T02:01:00+02:00",
1239
+ "2020-03-29T01:01:00+01:00",
1240
+ "2020-03-28T03:01:00+01:00",
1241
+ "2020-03-29T03:01:00+02:00",
1242
+ "2020-03-30T02:01:00+02:00",
1243
+ ],
1244
+ )
1245
+ #
1246
+ nowp = datetime(2020, 3, 28, 1, 58, 55, tzinfo=tz)
1247
+ itp = croniter("1 2 * * *", nowp)
1248
+ retp = [
1249
+ itp.get_next(datetime).isoformat(),
1250
+ itp.get_prev(datetime).isoformat(),
1251
+ itp.get_prev(datetime).isoformat(),
1252
+ itp.get_next(datetime).isoformat(),
1253
+ itp.get_next(datetime).isoformat(),
1254
+ ]
1255
+ self.assertEqual(
1256
+ retp,
1257
+ [
1258
+ "2020-03-29T03:01:00+02:00",
1259
+ "2020-03-29T01:01:00+01:00",
1260
+ "2020-03-28T03:01:00+01:00",
1261
+ "2020-03-29T03:01:00+02:00",
1262
+ "2020-03-30T02:01:00+02:00",
1263
+ ],
1264
+ )
1265
+ #
1266
+ nowt = datetime(2020, 3, 29, 2, 0, 0, tzinfo=tz)
1267
+ itt = croniter("1 2 * * *", nowt)
1268
+ rett = [
1269
+ itt.get_next(datetime).isoformat(),
1270
+ itt.get_prev(datetime).isoformat(),
1271
+ itt.get_prev(datetime).isoformat(),
1272
+ itt.get_next(datetime).isoformat(),
1273
+ itt.get_next(datetime).isoformat(),
1274
+ ]
1275
+ self.assertEqual(
1276
+ rett,
1277
+ [
1278
+ "2020-03-30T02:01:00+02:00",
1279
+ "2020-03-29T01:01:00+01:00",
1280
+ "2020-03-28T03:01:00+01:00",
1281
+ "2020-03-29T03:01:00+02:00",
1282
+ "2020-03-30T02:01:00+02:00",
1283
+ ],
1284
+ )
1285
+
1286
+ def test_dst_iter(self):
1287
+ tz = pytz.timezone("Asia/Hebron")
1288
+ now = datetime(2022, 3, 26, 0, 0, 0, tzinfo=tz)
1289
+ it = croniter("0 0 * * *", now)
1290
+ ret = [
1291
+ it.get_next(datetime).isoformat(),
1292
+ it.get_next(datetime).isoformat(),
1293
+ it.get_next(datetime).isoformat(),
1294
+ ]
1295
+ self.assertEqual(
1296
+ ret,
1297
+ [
1298
+ "2022-03-26T00:00:00+02:00",
1299
+ "2022-03-27T01:00:00+03:00",
1300
+ "2022-03-28T00:00:00+03:00",
1301
+ ],
1302
+ )
1303
+
1304
+ def get_nth_weekday_of_month(self, y, m, w):
1305
+ return croniter._get_nth_weekday_of_month(y, m, w)
1306
+
1307
+ def test_nth_wday_simple(self):
1308
+ sun, mon, tue, wed, thu, fri, sat = range(7)
1309
+
1310
+ self.assertEqual(
1311
+ self.get_nth_weekday_of_month(2000, 1, mon), (3, 10, 17, 24, 31))
1312
+ self.assertEqual(
1313
+ self.get_nth_weekday_of_month(2000, 2, tue), (1, 8, 15, 22, 29)) # Leap year
1314
+ self.assertEqual(
1315
+ self.get_nth_weekday_of_month(2000, 3, wed), (1, 8, 15, 22, 29))
1316
+ self.assertEqual(
1317
+ self.get_nth_weekday_of_month(2000, 4, thu), (6, 13, 20, 27))
1318
+ self.assertEqual(
1319
+ self.get_nth_weekday_of_month(2000, 2, fri), (4, 11, 18, 25))
1320
+ self.assertEqual(
1321
+ self.get_nth_weekday_of_month(2000, 2, sat), (5, 12, 19, 26))
1322
+
1323
+ def get_nth_weekday_of_month_before(self, y, m, w):
1324
+ return croniter._get_nth_weekday_of_month(y, m, w)[-1]
1325
+
1326
+ def test_nth_as_last_wday_simple(self):
1327
+ sun, mon, tue, wed, thu, fri, sat = range(7)
1328
+ self.assertEqual(
1329
+ self.get_nth_weekday_of_month_before(2000, 2, tue), 29)
1330
+ self.assertEqual(
1331
+ self.get_nth_weekday_of_month_before(2000, 2, sun), 27)
1332
+ self.assertEqual(
1333
+ self.get_nth_weekday_of_month_before(2000, 2, mon), 28)
1334
+ self.assertEqual(
1335
+ self.get_nth_weekday_of_month_before(2000, 2, wed), 23)
1336
+ self.assertEqual(
1337
+ self.get_nth_weekday_of_month_before(2000, 2, thu), 24)
1338
+ self.assertEqual(
1339
+ self.get_nth_weekday_of_month_before(2000, 2, fri), 25)
1340
+ self.assertEqual(
1341
+ self.get_nth_weekday_of_month_before(2000, 2, sat), 26)
1342
+
1343
+ def test_wdom_core_leap_year(self):
1344
+ sun, mon, tue, wed, thu, fri, sat = range(7)
1345
+ self.assertEqual(
1346
+ self.get_nth_weekday_of_month_before(2000, 2, tue), 29)
1347
+ self.assertEqual(
1348
+ self.get_nth_weekday_of_month_before(2000, 2, sun), 27)
1349
+ self.assertEqual(
1350
+ self.get_nth_weekday_of_month_before(2000, 2, mon), 28)
1351
+ self.assertEqual(
1352
+ self.get_nth_weekday_of_month_before(2000, 2, wed), 23)
1353
+ self.assertEqual(
1354
+ self.get_nth_weekday_of_month_before(2000, 2, thu), 24)
1355
+ self.assertEqual(
1356
+ self.get_nth_weekday_of_month_before(2000, 2, fri), 25)
1357
+ self.assertEqual(
1358
+ self.get_nth_weekday_of_month_before(2000, 2, sat), 26)
1359
+
1360
+ def test_lwom_friday(self):
1361
+ it = croniter("0 0 * * L5", datetime(1987, 1, 15), ret_type=datetime)
1362
+ items = [next(it) for i in range(12)]
1363
+ self.assertListEqual(
1364
+ items,
1365
+ [
1366
+ datetime(1987, 1, 30),
1367
+ datetime(1987, 2, 27),
1368
+ datetime(1987, 3, 27),
1369
+ datetime(1987, 4, 24),
1370
+ datetime(1987, 5, 29),
1371
+ datetime(1987, 6, 26),
1372
+ datetime(1987, 7, 31),
1373
+ datetime(1987, 8, 28),
1374
+ datetime(1987, 9, 25),
1375
+ datetime(1987, 10, 30),
1376
+ datetime(1987, 11, 27),
1377
+ datetime(1987, 12, 25),
1378
+ ],
1379
+ )
1380
+
1381
+ def test_lwom_friday_2hours(self):
1382
+ # This works with +/- "days=1' in proc_day_of_week_last()
1383
+ # and I don't know WHY?!?
1384
+ it = croniter("0 1,5 * * L5",
1385
+ datetime(1987, 1, 15), ret_type=datetime)
1386
+ items = [next(it) for i in range(12)]
1387
+ self.assertListEqual(
1388
+ items,
1389
+ [
1390
+ datetime(1987, 1, 30, 1),
1391
+ datetime(1987, 1, 30, 5),
1392
+ datetime(1987, 2, 27, 1),
1393
+ datetime(1987, 2, 27, 5),
1394
+ datetime(1987, 3, 27, 1),
1395
+ datetime(1987, 3, 27, 5),
1396
+ datetime(1987, 4, 24, 1),
1397
+ datetime(1987, 4, 24, 5),
1398
+ datetime(1987, 5, 29, 1),
1399
+ datetime(1987, 5, 29, 5),
1400
+ datetime(1987, 6, 26, 1),
1401
+ datetime(1987, 6, 26, 5),
1402
+ ],
1403
+ )
1404
+
1405
+ def test_lwom_friday_2xh_2xm(self):
1406
+ it = croniter("0,30 1,5 * * L5",
1407
+ datetime(1987, 1, 15), ret_type=datetime)
1408
+ items = [next(it) for i in range(12)]
1409
+ self.assertListEqual(
1410
+ items,
1411
+ [
1412
+ datetime(1987, 1, 30, 1, 0),
1413
+ datetime(1987, 1, 30, 1, 30),
1414
+ datetime(1987, 1, 30, 5, 0),
1415
+ datetime(1987, 1, 30, 5, 30),
1416
+ datetime(1987, 2, 27, 1, 0),
1417
+ datetime(1987, 2, 27, 1, 30),
1418
+ datetime(1987, 2, 27, 5, 0),
1419
+ datetime(1987, 2, 27, 5, 30),
1420
+ datetime(1987, 3, 27, 1, 0),
1421
+ datetime(1987, 3, 27, 1, 30),
1422
+ datetime(1987, 3, 27, 5, 0),
1423
+ datetime(1987, 3, 27, 5, 30),
1424
+ ],
1425
+ )
1426
+
1427
+ def test_lwom_saturday_rev(self):
1428
+ it = croniter("0 0 * * L6", datetime(2017, 12, 31),
1429
+ ret_type=datetime, is_prev=True)
1430
+ items = [next(it) for i in range(12)]
1431
+ self.assertListEqual(
1432
+ items,
1433
+ [
1434
+ datetime(2017, 12, 30),
1435
+ datetime(2017, 11, 25),
1436
+ datetime(2017, 10, 28),
1437
+ datetime(2017, 9, 30),
1438
+ datetime(2017, 8, 26),
1439
+ datetime(2017, 7, 29),
1440
+ datetime(2017, 6, 24),
1441
+ datetime(2017, 5, 27),
1442
+ datetime(2017, 4, 29),
1443
+ datetime(2017, 3, 25),
1444
+ datetime(2017, 2, 25),
1445
+ datetime(2017, 1, 28),
1446
+ ],
1447
+ )
1448
+
1449
+ def test_lwom_tue_thu(self):
1450
+ it = croniter("0 0 * * L2,L4", datetime(2016, 6, 1),
1451
+ ret_type=datetime)
1452
+ items = [next(it) for i in range(10)]
1453
+ self.assertListEqual(
1454
+ items,
1455
+ [
1456
+ datetime(2016, 6, 28),
1457
+ datetime(2016, 6, 30),
1458
+ datetime(2016, 7, 26),
1459
+ datetime(2016, 7, 28),
1460
+ datetime(2016, 8, 25),
1461
+ # last tuesday comes before the last thursday
1462
+ datetime(2016, 8, 30),
1463
+ datetime(2016, 9, 27),
1464
+ datetime(2016, 9, 29),
1465
+ datetime(2016, 10, 25),
1466
+ datetime(2016, 10, 27),
1467
+ ],
1468
+ )
1469
+
1470
+ def test_hash_mixup_all_fri_3rd_sat(self):
1471
+ # It appears that it's not possible to MIX a literal
1472
+ # dow with a `dow#n` format
1473
+ cron_a = "0 0 * * 6#3"
1474
+ cron_b = "0 0 * * 5"
1475
+ cron_c = "0 0 * * 5,6#3"
1476
+ start = datetime(2021, 3, 1)
1477
+ expect_a = [datetime(2021, 3, 20)]
1478
+ expect_b = [
1479
+ datetime(2021, 3, 5),
1480
+ datetime(2021, 3, 12),
1481
+ datetime(2021, 3, 19),
1482
+ datetime(2021, 3, 26),
1483
+ ]
1484
+ expect_c = sorted(set(expect_a) & set(expect_b))
1485
+
1486
+ def getn(expr, n):
1487
+ it = croniter(expr, start, ret_type=datetime)
1488
+ return [next(it) for i in range(n)]
1489
+
1490
+ self.assertListEqual(getn(cron_a, 1), expect_a)
1491
+ self.assertListEqual(getn(cron_b, 4), expect_b)
1492
+ with self.assertRaises(CroniterUnsupportedSyntaxError):
1493
+ self.assertListEqual(getn(cron_c, 5), expect_c)
1494
+
1495
+ def test_lwom_mixup_all_fri_last_sat(self):
1496
+ # Based on the failure of test_hash_mixup_all_fri_3rd_sat,
1497
+ # we should expect this to fail too as this implementation
1498
+ # simply extends nth_weekday_of_month
1499
+ cron_a = "0 0 * * L6"
1500
+ cron_b = "0 0 * * 5"
1501
+ cron_c = "0 0 * * 5,L6"
1502
+ start = datetime(2021, 3, 1)
1503
+ expect_a = [datetime(2021, 3, 27)]
1504
+ expect_b = [
1505
+ datetime(2021, 3, 5),
1506
+ datetime(2021, 3, 12),
1507
+ datetime(2021, 3, 19),
1508
+ datetime(2021, 3, 26),
1509
+ ]
1510
+ expect_c = sorted(set(expect_a) | set(expect_b))
1511
+
1512
+ def getn(expr, n):
1513
+ it = croniter(expr, start, ret_type=datetime)
1514
+ return [next(it) for i in range(n)]
1515
+
1516
+ self.assertListEqual(getn(cron_a, 1), expect_a)
1517
+ self.assertListEqual(getn(cron_b, 4), expect_b)
1518
+ with self.assertRaises(CroniterUnsupportedSyntaxError):
1519
+ self.assertListEqual(getn(cron_c, 5), expect_c)
1520
+
1521
+ def test_lwom_mixup_firstlast_sat(self):
1522
+ # First saturday, last saturday
1523
+ start = datetime(2021, 3, 1)
1524
+ cron_a = "0 0 * * 6#1"
1525
+ cron_b = "0 0 * * L6"
1526
+ cron_c = "0 0 * * L6,6#1"
1527
+ expect_a = [
1528
+ datetime(2021, 3, 6),
1529
+ datetime(2021, 4, 3),
1530
+ datetime(2021, 5, 1),
1531
+ ]
1532
+ expect_b = [
1533
+ datetime(2021, 3, 27),
1534
+ datetime(2021, 4, 24),
1535
+ datetime(2021, 5, 29),
1536
+ ]
1537
+ expect_c = sorted(expect_a + expect_b)
1538
+
1539
+ def getn(expr, n):
1540
+ it = croniter(expr, start, ret_type=datetime)
1541
+ return [next(it) for i in range(n)]
1542
+
1543
+ self.assertListEqual(getn(cron_a, 3), expect_a)
1544
+ self.assertListEqual(getn(cron_b, 3), expect_b)
1545
+ self.assertListEqual(getn(cron_c, 6), expect_c)
1546
+
1547
+ def test_lwom_mixup_4th_and_last(self):
1548
+ # 4th and last monday
1549
+ start = datetime(2021, 11, 1)
1550
+ cron_a = "0 0 * * 1#4"
1551
+ cron_b = "0 0 * * L1"
1552
+ cron_c = "0 0 * * 1#4,L1"
1553
+ expect_a = [
1554
+ datetime(2021, 11, 22),
1555
+ datetime(2021, 12, 27),
1556
+ datetime(2022, 1, 24),
1557
+ ]
1558
+ expect_b = [
1559
+ datetime(2021, 11, 29),
1560
+ datetime(2021, 12, 27),
1561
+ datetime(2022, 1, 31),
1562
+ ]
1563
+ expect_c = sorted(set(expect_a) | set(expect_b))
1564
+
1565
+ def getn(expr, n):
1566
+ it = croniter(expr, start, ret_type=datetime)
1567
+ return [next(it) for i in range(n)]
1568
+
1569
+ self.assertListEqual(getn(cron_a, 3), expect_a)
1570
+ self.assertListEqual(getn(cron_b, 3), expect_b)
1571
+ self.assertListEqual(getn(cron_c, 5), expect_c)
1572
+
1573
+ def test_configure_second_location(self):
1574
+ base = datetime(2010, 8, 25, 0)
1575
+ itr = croniter("59 58 1 * * *", base, second_at_beginning=True)
1576
+ n = itr.get_next(datetime)
1577
+ self.assertEqual(n.year, base.year)
1578
+ self.assertEqual(n.month, base.month)
1579
+ self.assertEqual(n.day, base.day)
1580
+ self.assertEqual(n.hour, 1)
1581
+ self.assertEqual(n.minute, 58)
1582
+ self.assertEqual(n.second, 59)
1583
+
1584
+ def test_nth_out_of_range(self):
1585
+ with self.assertRaises(CroniterBadCronError):
1586
+ croniter("0 0 * * 1#7")
1587
+ with self.assertRaises(CroniterBadCronError):
1588
+ croniter("0 0 * * 1#0")
1589
+
1590
+ def test_last_out_of_range(self):
1591
+ with self.assertRaises(CroniterBadCronError):
1592
+ croniter("0 0 * * L-1")
1593
+ with self.assertRaises(CroniterBadCronError):
1594
+ croniter("0 0 * * L8")
1595
+
1596
+ def test_question_mark(self):
1597
+ base = datetime(2010, 8, 25, 15, 56)
1598
+ itr = croniter("0 0 1 * ?", base)
1599
+ n = itr.get_next(datetime)
1600
+ self.assertEqual(n.year, base.year)
1601
+ self.assertEqual(n.month, 9)
1602
+ self.assertEqual(n.day, 1)
1603
+ self.assertEqual(n.hour, 0)
1604
+ self.assertEqual(n.minute, 0)
1605
+
1606
+ def test_invalid_question_mark(self):
1607
+ self.assertRaises(CroniterBadCronError, croniter, "? * * * *")
1608
+ self.assertRaises(CroniterBadCronError, croniter, "* ? * * *")
1609
+ self.assertRaises(CroniterBadCronError, croniter, "* * ?,* * *")
1610
+
1611
+ def test_year(self):
1612
+ itr1 = croniter("0 0 11 * * 0 2060", datetime(2050, 1, 1))
1613
+ n1 = itr1.get_next(datetime)
1614
+ self.assertEqual(n1.year, 2060)
1615
+ self.assertEqual(n1.month, 1)
1616
+ self.assertEqual(n1.day, 11)
1617
+ n2 = itr1.get_next(datetime)
1618
+ self.assertEqual(n2.year, 2060)
1619
+ self.assertEqual(n2.month, 2)
1620
+ self.assertEqual(n2.day, 11)
1621
+
1622
+ itr2 = croniter("0 0 11 * * 0 2050-2060", datetime(2055, 1, 30))
1623
+ n3 = itr2.get_next(datetime)
1624
+ self.assertEqual(n3.year, 2055)
1625
+ self.assertEqual(n3.month, 2)
1626
+ self.assertEqual(n3.day, 11)
1627
+
1628
+ itr3 = croniter("0 0 29 2 * 0 2025,2021-2023,2028",
1629
+ datetime(2020, 1, 1))
1630
+ n4 = itr3.get_next(datetime)
1631
+ self.assertEqual(n4.year, 2028)
1632
+ self.assertEqual(n4.month, 2)
1633
+ self.assertEqual(n4.day, 29)
1634
+
1635
+ itr4 = croniter("0 0 29 2 * 0 2025,*", datetime(2020, 1, 1))
1636
+ n5 = itr4.get_next(datetime)
1637
+ self.assertEqual(n5.year, 2020)
1638
+ self.assertEqual(n5.month, 2)
1639
+ self.assertEqual(n5.day, 29)
1640
+
1641
+ itr5 = croniter("0 0 29 2 * 0 2022/3", datetime(2020, 1, 1))
1642
+ n6 = itr5.get_next(datetime)
1643
+ self.assertEqual(n6.year, 2028)
1644
+ self.assertEqual(n6.month, 2)
1645
+ self.assertEqual(n6.day, 29)
1646
+
1647
+ itr6 = croniter("0 0 29 2 * 0 2023-2035/3", datetime(2020, 1, 1))
1648
+ n7 = itr6.get_next(datetime)
1649
+ self.assertEqual(n7.year, 2032)
1650
+ self.assertEqual(n7.month, 2)
1651
+ self.assertEqual(n7.day, 29)
1652
+
1653
+ def test_year_with_other_field(self):
1654
+ itr1 = croniter("0 0 31 11-12 * 0 2023", datetime(2000, 1, 30))
1655
+ n1 = itr1.get_next(datetime)
1656
+ self.assertEqual(n1.year, 2023)
1657
+ self.assertEqual(n1.month, 12)
1658
+ self.assertEqual(n1.day, 31)
1659
+
1660
+ itr2 = croniter("0 0 31 1-2 * 0 2023-2025", datetime(2024, 12, 30))
1661
+ n2 = itr2.get_next(datetime)
1662
+ self.assertEqual(n2.year, 2025)
1663
+ self.assertEqual(n2.month, 1)
1664
+ self.assertEqual(n2.day, 31)
1665
+
1666
+ itr3 = croniter("0 0 1 1 1 0 2020-2030", datetime(2000, 1, 1),
1667
+ day_or=False)
1668
+ n3 = itr3.get_next(datetime)
1669
+ self.assertEqual(n3.year, 2024)
1670
+ self.assertEqual(n3.month, 1)
1671
+ self.assertEqual(n3.day, 1)
1672
+
1673
+ def test_year_get_prev(self):
1674
+ itr1 = croniter("0 0 11 * * 0 2000", datetime(2010, 1, 1))
1675
+ p1 = itr1.get_prev(datetime)
1676
+ self.assertEqual(p1.year, 2000)
1677
+ self.assertEqual(p1.month, 12)
1678
+ self.assertEqual(p1.day, 11)
1679
+
1680
+ itr2 = croniter("0 0 11 * * 0 2000", datetime(2010, 1, 1))
1681
+ p2 = itr2.get_prev(datetime)
1682
+ self.assertEqual(p2.year, 2000)
1683
+ self.assertEqual(p2.month, 12)
1684
+ self.assertEqual(p2.day, 11)
1685
+
1686
+ itr2 = croniter("0 0 29 2 * 0 2010-2030", datetime(2020, 1, 1))
1687
+ p2 = itr2.get_prev(datetime)
1688
+ self.assertEqual(p2.year, 2016)
1689
+ self.assertEqual(p2.month, 2)
1690
+ self.assertEqual(p2.day, 29)
1691
+
1692
+ def test_year_match(self):
1693
+ self.assertTrue(croniter.match("* * * * * * 2024", datetime(2024, 1, 1)))
1694
+ self.assertTrue(
1695
+ croniter.match(
1696
+ "59 58 23 31 12 * 2024",
1697
+ datetime(2024, 12, 31, 23, 58, 59),
1698
+ second_at_beginning=True,
1699
+ )
1700
+ )
1701
+ self.assertFalse(croniter.match("* * * * * * 2024-2026",
1702
+ datetime(2027, 1, 1)))
1703
+ self.assertFalse(croniter.match("* * * * * * 2024/2",
1704
+ datetime(2025, 1, 1)))
1705
+
1706
+ def test_year_bad_date_error(self):
1707
+ with self.assertRaises(CroniterBadDateError):
1708
+ itr = croniter("* * * * * * 2020", datetime(2030, 1, 1))
1709
+ itr.get_next()
1710
+ with self.assertRaises(CroniterBadDateError):
1711
+ itr = croniter("* * * * * * 2020", datetime(2000, 1, 1))
1712
+ itr.get_prev()
1713
+ with self.assertRaises(CroniterBadDateError):
1714
+ itr = croniter("* * 29 2 * * 2021-2023", datetime(2000, 1, 1))
1715
+ itr.get_next()
1716
+
1717
+ def test_year_with_second_at_beginning(self):
1718
+ base = datetime(2050, 1, 1)
1719
+ itr = croniter("59 58 23 31 12 * 2070", base,
1720
+ second_at_beginning=True)
1721
+ n = itr.get_next(datetime)
1722
+ self.assertEqual(n.year, 2070)
1723
+ self.assertEqual(n.month, 12)
1724
+ self.assertEqual(n.day, 31)
1725
+ self.assertEqual(n.hour, 23)
1726
+ self.assertEqual(n.minute, 58)
1727
+ self.assertEqual(n.second, 59)
1728
+
1729
+ def test_invalid_year(self):
1730
+ self.assertRaises(CroniterBadCronError, croniter,
1731
+ "0 0 1 * * 0 1000")
1732
+ self.assertRaises(CroniterBadCronError, croniter,
1733
+ "0 0 1 * * 0 99999")
1734
+ self.assertRaises(CroniterBadCronError, croniter,
1735
+ "0 0 1 * * 0 2070#3")
1736
+
1737
+ def test_issue_47(self):
1738
+ base = datetime(2021, 3, 30, 4, 0)
1739
+ itr = croniter("0 6 30 3 *", base)
1740
+ prev1 = itr.get_prev(datetime)
1741
+ self.assertEqual(prev1.year, base.year - 1)
1742
+ self.assertEqual(prev1.month, 3)
1743
+ self.assertEqual(prev1.day, 30)
1744
+ self.assertEqual(prev1.hour, 6)
1745
+ self.assertEqual(prev1.minute, 0)
1746
+
1747
+ maxDiff = None
1748
+
1749
+ def test_issue_142_dow(self):
1750
+ ret = []
1751
+ for i in range(1, 31):
1752
+ ret.append(
1753
+ (
1754
+ i,
1755
+ croniter("35 * 1-l/8 * *", datetime(2020, 1, i),
1756
+ ret_type=datetime).get_next(),
1757
+ )
1758
+ )
1759
+ i += 1
1760
+ self.assertEqual(
1761
+ ret,
1762
+ [
1763
+ (1, datetime(2020, 1, 1, 0, 35)),
1764
+ (2, datetime(2020, 1, 9, 0, 35)),
1765
+ (3, datetime(2020, 1, 9, 0, 35)),
1766
+ (4, datetime(2020, 1, 9, 0, 35)),
1767
+ (5, datetime(2020, 1, 9, 0, 35)),
1768
+ (6, datetime(2020, 1, 9, 0, 35)),
1769
+ (7, datetime(2020, 1, 9, 0, 35)),
1770
+ (8, datetime(2020, 1, 9, 0, 35)),
1771
+ (9, datetime(2020, 1, 9, 0, 35)),
1772
+ (10, datetime(2020, 1, 17, 0, 35)),
1773
+ (11, datetime(2020, 1, 17, 0, 35)),
1774
+ (12, datetime(2020, 1, 17, 0, 35)),
1775
+ (13, datetime(2020, 1, 17, 0, 35)),
1776
+ (14, datetime(2020, 1, 17, 0, 35)),
1777
+ (15, datetime(2020, 1, 17, 0, 35)),
1778
+ (16, datetime(2020, 1, 17, 0, 35)),
1779
+ (17, datetime(2020, 1, 17, 0, 35)),
1780
+ (18, datetime(2020, 1, 25, 0, 35)),
1781
+ (19, datetime(2020, 1, 25, 0, 35)),
1782
+ (20, datetime(2020, 1, 25, 0, 35)),
1783
+ (21, datetime(2020, 1, 25, 0, 35)),
1784
+ (22, datetime(2020, 1, 25, 0, 35)),
1785
+ (23, datetime(2020, 1, 25, 0, 35)),
1786
+ (24, datetime(2020, 1, 25, 0, 35)),
1787
+ (25, datetime(2020, 1, 25, 0, 35)),
1788
+ (26, datetime(2020, 2, 1, 0, 35)),
1789
+ (27, datetime(2020, 2, 1, 0, 35)),
1790
+ (28, datetime(2020, 2, 1, 0, 35)),
1791
+ (29, datetime(2020, 2, 1, 0, 35)),
1792
+ (30, datetime(2020, 2, 1, 0, 35)),
1793
+ ],
1794
+ )
1795
+
1796
+ def test_issue145_getnext(self):
1797
+ # Example of quarterly event cron schedule
1798
+ start = datetime(2020, 9, 24)
1799
+ cron = "0 13 8 1,4,7,10 wed"
1800
+ with self.assertRaises(CroniterBadDateError):
1801
+ it = croniter(cron, start, day_or=False,
1802
+ max_years_between_matches=1)
1803
+ it.get_next()
1804
+ # New functionality (0.3.35) allowing croniter
1805
+ # to find spare matches of cron patterns across multiple years
1806
+ it = croniter(cron, start, day_or=False, max_years_between_matches=5)
1807
+ self.assertEqual(it.get_next(datetime), datetime(2025, 1, 8, 13))
1808
+
1809
+ def test_explicit_year_forward(self):
1810
+ start = datetime(2020, 9, 24)
1811
+ cron = "0 13 8 1,4,7,10 wed"
1812
+
1813
+ # Expect exception because no explicit range was provided.
1814
+ # Therefore, the caller should be made aware that an
1815
+ # implicit limit was hit.
1816
+ ccron = croniter(cron, start, day_or=False)
1817
+ ccron._max_years_between_matches = 1
1818
+ iterable = ccron.all_next()
1819
+ with self.assertRaises(CroniterBadDateError):
1820
+ next(iterable)
1821
+
1822
+ iterable = croniter(cron, start, day_or=False,
1823
+ max_years_between_matches=5).all_next(
1824
+ datetime)
1825
+ n = next(iterable)
1826
+ self.assertEqual(n, datetime(2025, 1, 8, 13))
1827
+
1828
+ # If the explicitly given lookahead isn't
1829
+ # enough to reach the next date, that's fine.
1830
+ # The caller specified the maximum gap, so no just stop iteration
1831
+ iterable = croniter(cron, start, day_or=False,
1832
+ max_years_between_matches=2).all_next(
1833
+ datetime)
1834
+ with self.assertRaises(StopIteration):
1835
+ next(iterable)
1836
+
1837
+ def test_issue151(self):
1838
+ """."""
1839
+ self.assertTrue(croniter.match(
1840
+ "* * * * *",
1841
+ datetime(2019, 1, 14, 11, 0, 59, 999999)))
1842
+
1843
+ def test_overflow(self):
1844
+ """."""
1845
+ self.assertRaises(CroniterBadCronError, croniter,
1846
+ "0-10000000 * * * *", datetime.now())
1847
+
1848
+ def test_issue156(self):
1849
+ """."""
1850
+ dt = croniter(
1851
+ "* * * * *,0",
1852
+ datetime(2019, 1, 14, 11, 0, 59, 999999)).get_next()
1853
+ self.assertEqual(1547463660.0, dt)
1854
+ self.assertRaises(
1855
+ CroniterBadCronError, croniter,
1856
+ "* * * * *,b")
1857
+ dt = croniter(
1858
+ "0 0 * * *,sat#3",
1859
+ datetime(2019, 1, 14, 11, 0, 59, 999999)).get_next()
1860
+ self.assertEqual(1547856000.0, dt)
1861
+
1862
+ def test_confirm_sort(self):
1863
+ m, h, d, mon, dow, s = range(6)
1864
+ self.assertListEqual(croniter(
1865
+ "0 8,22,10,23 1 1 0").expanded[h], [8, 10, 22, 23])
1866
+ self.assertListEqual(croniter(
1867
+ "0 0 25-L 1 0").expanded[d], [25, 26, 27, 28, 29, 30, 31])
1868
+ self.assertListEqual(croniter(
1869
+ "1 1 7,14,21,L * *").expanded[d], [7, 14, 21, "l"])
1870
+ self.assertListEqual(croniter(
1871
+ "0 0 * * *,sat#3").expanded[dow], ["*", 6])
1872
+
1873
+ def test_issue_k6(self):
1874
+ self.assertRaises(CroniterBadCronError, croniter, "0 0 0 0 0")
1875
+ self.assertRaises(CroniterBadCronError, croniter, "0 0 0 1 0")
1876
+
1877
+ def test_issue_k11(self):
1878
+ now = pytz.timezone("America/New_York").localize(
1879
+ datetime(2019, 1, 14, 11, 0, 59))
1880
+ nextnow = croniter("* * * * * ").next(datetime, start_time=now)
1881
+ nextnow2 = croniter("* * * * * ", now).next(datetime)
1882
+ for nt in nextnow, nextnow2:
1883
+ self.assertEqual(nt.tzinfo.zone, "America/New_York")
1884
+ self.assertEqual(int(
1885
+ croniter._datetime_to_timestamp(nt)), 1547481660)
1886
+
1887
+ def test_issue_k12(self):
1888
+ tz = pytz.timezone("Europe/Athens")
1889
+ base = datetime(2010, 1, 23, 12, 18, tzinfo=tz)
1890
+ itr = croniter("* * * * *")
1891
+ itr.set_current(start_time=base)
1892
+ n1 = itr.get_next() # 19
1893
+
1894
+ self.assertEqual(n1, datetime_to_timestamp(base) + 60)
1895
+
1896
+ def test_issue_k34(self):
1897
+ # invalid cron, but should throw appropriate exception
1898
+ self.assertRaises(CroniterBadCronError, croniter, "4 0 L/2 2 0")
1899
+
1900
+ def test_issue_k33(self):
1901
+ y = 2018
1902
+ # At 11:30 PM, between day 1 and 7 of the month, Monday through Friday, only in January
1903
+ ret = []
1904
+ for i in range(10):
1905
+ cron = croniter("30 23 1-7 JAN MON-FRI#1",
1906
+ datetime(y + i, 1, 1), ret_type=datetime)
1907
+ for _ in range(7):
1908
+ d = cron.get_next()
1909
+ if d.year == y + i:
1910
+ ret.append(d)
1911
+ rets = [
1912
+ datetime(2018, 1, 1, 23, 30),
1913
+ datetime(2018, 1, 2, 23, 30),
1914
+ datetime(2018, 1, 3, 23, 30),
1915
+ datetime(2018, 1, 4, 23, 30),
1916
+ datetime(2018, 1, 5, 23, 30),
1917
+ datetime(2019, 1, 1, 23, 30),
1918
+ datetime(2019, 1, 2, 23, 30),
1919
+ datetime(2019, 1, 3, 23, 30),
1920
+ datetime(2019, 1, 4, 23, 30),
1921
+ datetime(2019, 1, 7, 23, 30),
1922
+ datetime(2020, 1, 1, 23, 30),
1923
+ datetime(2020, 1, 2, 23, 30),
1924
+ datetime(2020, 1, 3, 23, 30),
1925
+ datetime(2020, 1, 6, 23, 30),
1926
+ datetime(2020, 1, 7, 23, 30),
1927
+ datetime(2021, 1, 1, 23, 30),
1928
+ datetime(2021, 1, 4, 23, 30),
1929
+ datetime(2021, 1, 5, 23, 30),
1930
+ datetime(2021, 1, 6, 23, 30),
1931
+ datetime(2021, 1, 7, 23, 30),
1932
+ datetime(2022, 1, 3, 23, 30),
1933
+ datetime(2022, 1, 4, 23, 30),
1934
+ datetime(2022, 1, 5, 23, 30),
1935
+ datetime(2022, 1, 6, 23, 30),
1936
+ datetime(2022, 1, 7, 23, 30),
1937
+ datetime(2023, 1, 2, 23, 30),
1938
+ datetime(2023, 1, 3, 23, 30),
1939
+ datetime(2023, 1, 4, 23, 30),
1940
+ datetime(2023, 1, 5, 23, 30),
1941
+ datetime(2023, 1, 6, 23, 30),
1942
+ datetime(2024, 1, 1, 23, 30),
1943
+ datetime(2024, 1, 2, 23, 30),
1944
+ datetime(2024, 1, 3, 23, 30),
1945
+ datetime(2024, 1, 4, 23, 30),
1946
+ datetime(2024, 1, 5, 23, 30),
1947
+ datetime(2025, 1, 1, 23, 30),
1948
+ datetime(2025, 1, 2, 23, 30),
1949
+ datetime(2025, 1, 3, 23, 30),
1950
+ datetime(2025, 1, 6, 23, 30),
1951
+ datetime(2025, 1, 7, 23, 30),
1952
+ datetime(2026, 1, 1, 23, 30),
1953
+ datetime(2026, 1, 2, 23, 30),
1954
+ datetime(2026, 1, 5, 23, 30),
1955
+ datetime(2026, 1, 6, 23, 30),
1956
+ datetime(2026, 1, 7, 23, 30),
1957
+ datetime(2027, 1, 1, 23, 30),
1958
+ datetime(2027, 1, 4, 23, 30),
1959
+ datetime(2027, 1, 5, 23, 30),
1960
+ datetime(2027, 1, 6, 23, 30),
1961
+ datetime(2027, 1, 7, 23, 30),
1962
+ ]
1963
+ self.assertEqual(ret, rets)
1964
+ croniter.expand("30 6 1-7 MAY MON#1")
1965
+
1966
+ def test_bug_62_leap(self):
1967
+ ret = croniter("15 22 29 2 *",
1968
+ datetime(2024, 2, 29)).get_prev(datetime)
1969
+ self.assertEqual(ret, datetime(2020, 2, 29, 22, 15))
1970
+
1971
+ def test_expand_from_start_time_minute(self):
1972
+ seven_seconds_interval_pattern = "*/7 * * * *"
1973
+ ret1 = croniter(
1974
+ seven_seconds_interval_pattern,
1975
+ start_time=datetime(2024, 7, 11, 10, 11),
1976
+ expand_from_start_time=True,
1977
+ ).get_next(datetime)
1978
+ self.assertEqual(ret1, datetime(2024, 7, 11, 10, 18))
1979
+
1980
+ ret2 = croniter(
1981
+ seven_seconds_interval_pattern,
1982
+ start_time=datetime(2024, 7, 11, 10, 12),
1983
+ expand_from_start_time=True,
1984
+ ).get_next(datetime)
1985
+ self.assertEqual(ret2, datetime(2024, 7, 11, 10, 19))
1986
+
1987
+ ret3 = croniter(
1988
+ seven_seconds_interval_pattern,
1989
+ start_time=datetime(2024, 7, 11, 10, 11),
1990
+ expand_from_start_time=True,
1991
+ ).get_prev(datetime)
1992
+ self.assertEqual(ret3, datetime(2024, 7, 11, 10, 4))
1993
+
1994
+ ret4 = croniter(
1995
+ seven_seconds_interval_pattern,
1996
+ start_time=datetime(2024, 7, 11, 10, 12),
1997
+ expand_from_start_time=True,
1998
+ ).get_prev(datetime)
1999
+ self.assertEqual(ret4, datetime(2024, 7, 11, 10, 5))
2000
+
2001
+ def test_expand_from_start_time_hour(self):
2002
+ seven_hours_interval_pattern = "0 */7 * * *"
2003
+ ret1 = croniter(
2004
+ seven_hours_interval_pattern,
2005
+ start_time=datetime(2024, 7, 11, 15, 0),
2006
+ expand_from_start_time=True,
2007
+ ).get_next(datetime)
2008
+ self.assertEqual(ret1, datetime(2024, 7, 11, 22, 0))
2009
+
2010
+ ret2 = croniter(
2011
+ seven_hours_interval_pattern,
2012
+ start_time=datetime(2024, 7, 11, 16, 0),
2013
+ expand_from_start_time=True,
2014
+ ).get_next(datetime)
2015
+ self.assertEqual(ret2, datetime(2024, 7, 11, 23, 0))
2016
+
2017
+ ret3 = croniter(
2018
+ seven_hours_interval_pattern,
2019
+ start_time=datetime(2024, 7, 11, 15, 0),
2020
+ expand_from_start_time=True,
2021
+ ).get_prev(datetime)
2022
+ self.assertEqual(ret3, datetime(2024, 7, 11, 8, 0))
2023
+
2024
+ ret4 = croniter(
2025
+ seven_hours_interval_pattern,
2026
+ start_time=datetime(2024, 7, 11, 16, 0),
2027
+ expand_from_start_time=True,
2028
+ ).get_prev(datetime)
2029
+ self.assertEqual(ret4, datetime(2024, 7, 11, 9, 0))
2030
+
2031
+ def test_expand_from_start_time_date(self):
2032
+ five_days_interval_pattern = "0 0 */5 * *"
2033
+ ret1 = croniter(
2034
+ five_days_interval_pattern,
2035
+ start_time=datetime(2024, 7, 12),
2036
+ expand_from_start_time=True,
2037
+ ).get_next(datetime)
2038
+ self.assertEqual(ret1, datetime(2024, 7, 17))
2039
+
2040
+ ret2 = croniter(
2041
+ five_days_interval_pattern,
2042
+ start_time=datetime(2024, 7, 13),
2043
+ expand_from_start_time=True,
2044
+ ).get_next(datetime)
2045
+ self.assertEqual(ret2, datetime(2024, 7, 18))
2046
+
2047
+ ret3 = croniter(
2048
+ five_days_interval_pattern,
2049
+ start_time=datetime(2024, 7, 12),
2050
+ expand_from_start_time=True,
2051
+ ).get_prev(datetime)
2052
+ self.assertEqual(ret3, datetime(2024, 7, 7))
2053
+
2054
+ ret4 = croniter(
2055
+ five_days_interval_pattern,
2056
+ start_time=datetime(2024, 7, 13),
2057
+ expand_from_start_time=True,
2058
+ ).get_prev(datetime)
2059
+ self.assertEqual(ret4, datetime(2024, 7, 8))
2060
+
2061
+ def test_expand_from_start_time_month(self):
2062
+ three_monts_interval_pattern = "0 0 1 */3 *"
2063
+ ret1 = croniter(
2064
+ three_monts_interval_pattern,
2065
+ start_time=datetime(2024, 7, 1),
2066
+ expand_from_start_time=True,
2067
+ ).get_next(datetime)
2068
+ self.assertEqual(ret1, datetime(2024, 10, 1))
2069
+
2070
+ ret2 = croniter(
2071
+ three_monts_interval_pattern,
2072
+ start_time=datetime(2024, 8, 1),
2073
+ expand_from_start_time=True,
2074
+ ).get_next(datetime)
2075
+ self.assertEqual(ret2, datetime(2024, 11, 1))
2076
+
2077
+ ret3 = croniter(
2078
+ three_monts_interval_pattern,
2079
+ start_time=datetime(2024, 7, 1),
2080
+ expand_from_start_time=True,
2081
+ ).get_prev(datetime)
2082
+ self.assertEqual(ret3, datetime(2024, 4, 1))
2083
+
2084
+ ret4 = croniter(
2085
+ three_monts_interval_pattern,
2086
+ start_time=datetime(2024, 8, 1),
2087
+ expand_from_start_time=True,
2088
+ ).get_prev(datetime)
2089
+ self.assertEqual(ret4, datetime(2024, 5, 1))
2090
+
2091
+ def test_expand_from_start_time_day_of_week(self):
2092
+ three_monts_interval_pattern = "0 0 * * */2"
2093
+ ret1 = croniter(
2094
+ three_monts_interval_pattern,
2095
+ start_time=datetime(2024, 7, 10),
2096
+ expand_from_start_time=True,
2097
+ ).get_next(datetime)
2098
+ self.assertEqual(ret1, datetime(2024, 7, 12))
2099
+
2100
+ ret2 = croniter(
2101
+ three_monts_interval_pattern,
2102
+ start_time=datetime(2024, 7, 11),
2103
+ expand_from_start_time=True,
2104
+ ).get_next(datetime)
2105
+ self.assertEqual(ret2, datetime(2024, 7, 13))
2106
+
2107
+ ret3 = croniter(
2108
+ three_monts_interval_pattern,
2109
+ start_time=datetime(2024, 7, 10),
2110
+ expand_from_start_time=True,
2111
+ ).get_prev(datetime)
2112
+ self.assertEqual(ret3, datetime(2024, 7, 8))
2113
+
2114
+ ret4 = croniter(
2115
+ three_monts_interval_pattern,
2116
+ start_time=datetime(2024, 7, 11),
2117
+ expand_from_start_time=True,
2118
+ ).get_prev(datetime)
2119
+ self.assertEqual(ret4, datetime(2024, 7, 9))
2120
+
2121
+ def test_get_next_fails_with_expand_from_start_time_true(self):
2122
+ expanded_croniter = croniter("0 0 */5 * *", expand_from_start_time=True)
2123
+ self.assertRaises(
2124
+ ValueError,
2125
+ expanded_croniter.get_next,
2126
+ datetime,
2127
+ start_time=datetime(2024, 7, 12),
2128
+ )
2129
+
2130
+ def test_get_next_update_current(self):
2131
+ cron = croniter("* * * * * *")
2132
+
2133
+ cron.set_current(datetime(2024, 7, 12), force=True)
2134
+ retn = [(cron.get_next(datetime),
2135
+ cron.get_current(datetime)) for a in range(3)]
2136
+ self.assertEqual(
2137
+ retn,
2138
+ [
2139
+ (datetime(2024, 7, 12, 0, 0, 1),
2140
+ datetime(2024, 7, 12, 0, 0, 1)),
2141
+ (datetime(2024, 7, 12, 0, 0, 2),
2142
+ datetime(2024, 7, 12, 0, 0, 2)),
2143
+ (datetime(2024, 7, 12, 0, 0, 3),
2144
+ datetime(2024, 7, 12, 0, 0, 3)),
2145
+ ],
2146
+ )
2147
+
2148
+ retns = (
2149
+ cron.get_next(datetime, start_time=datetime(2024, 7, 12)),
2150
+ cron.get_current(datetime),
2151
+ )
2152
+ self.assertEqual(retn[0], retns)
2153
+
2154
+ cron.set_current(datetime(2024, 7, 12), force=True)
2155
+ retp = [(cron.get_prev(datetime),
2156
+ cron.get_current(datetime)) for a in range(3)]
2157
+ self.assertEqual(
2158
+ retp,
2159
+ [
2160
+ (datetime(2024, 7, 11, 23, 59, 59),
2161
+ datetime(2024, 7, 11, 23, 59, 59)),
2162
+ (datetime(2024, 7, 11, 23, 59, 58),
2163
+ datetime(2024, 7, 11, 23, 59, 58)),
2164
+ (datetime(2024, 7, 11, 23, 59, 57),
2165
+ datetime(2024, 7, 11, 23, 59, 57)),
2166
+ ],
2167
+ )
2168
+
2169
+ retps = (
2170
+ cron.get_prev(datetime, start_time=datetime(2024, 7, 12)),
2171
+ cron.get_current(datetime),
2172
+ )
2173
+ self.assertEqual(retp[0], retps)
2174
+
2175
+ cron.set_current(datetime(2024, 7, 12), force=True)
2176
+ r = cron.all_next(datetime)
2177
+ retan = [(next(r), cron.get_current(datetime)) for a in range(3)]
2178
+
2179
+ r = cron.all_next(datetime, start_time=datetime(2024, 7, 12))
2180
+ retans = [(next(r), cron.get_current(datetime)) for a in range(3)]
2181
+
2182
+ cron.set_current(datetime(2024, 7, 12), force=True)
2183
+ r = cron.all_prev(datetime)
2184
+ retap = [(next(r), cron.get_current(datetime)) for a in range(3)]
2185
+
2186
+ r = cron.all_prev(datetime, start_time=datetime(2024, 7, 12))
2187
+ retaps = [(next(r), cron.get_current(datetime)) for a in range(3)]
2188
+
2189
+ self.assertEqual(retp, retap)
2190
+ self.assertEqual(retp, retaps)
2191
+ self.assertEqual(retn, retan)
2192
+ self.assertEqual(retn, retans)
2193
+
2194
+ cron.set_current(datetime(2024, 7, 12), force=True)
2195
+ uretn = [(cron.get_next(datetime, update_current=False),
2196
+ cron.get_current(datetime)) for a in range(3)]
2197
+ self.assertEqual(
2198
+ uretn,
2199
+ [
2200
+ (datetime(2024, 7, 12, 0, 0, 1), datetime(2024, 7, 12, 0, 0)),
2201
+ (datetime(2024, 7, 12, 0, 0, 1), datetime(2024, 7, 12, 0, 0)),
2202
+ (datetime(2024, 7, 12, 0, 0, 1), datetime(2024, 7, 12, 0, 0)),
2203
+ ],
2204
+ )
2205
+
2206
+ cron.set_current(datetime(2024, 7, 12), force=True)
2207
+ uretp = [(cron.get_prev(datetime, update_current=False),
2208
+ cron.get_current(datetime)) for a in range(3)]
2209
+ self.assertEqual(
2210
+ uretp,
2211
+ [
2212
+ (datetime(2024, 7, 11, 23, 59, 59), datetime(2024, 7, 12, 0, 0)),
2213
+ (datetime(2024, 7, 11, 23, 59, 59), datetime(2024, 7, 12, 0, 0)),
2214
+ (datetime(2024, 7, 11, 23, 59, 59), datetime(2024, 7, 12, 0, 0)),
2215
+ ],
2216
+ )
2217
+
2218
+ cron.set_current(datetime(2024, 7, 12), force=True)
2219
+ r = cron.all_next(datetime, update_current=False)
2220
+ uretan = [(next(r), cron.get_current(datetime)) for a in range(3)]
2221
+
2222
+ cron.set_current(datetime(2024, 7, 12), force=True)
2223
+ r = cron.all_prev(datetime, update_current=False)
2224
+ uretap = [(next(r), cron.get_current(datetime)) for a in range(3)]
2225
+
2226
+ self.assertEqual(uretp, uretap)
2227
+ self.assertEqual(uretn, uretan)
2228
+
2229
+ def test_issue_2038y(self):
2230
+ base = datetime(2040, 1, 1, 0, 0)
2231
+ itr = croniter("* * * * *", base)
2232
+ try:
2233
+ itr.get_next()
2234
+ except OverflowError:
2235
+ raise Exception("overflow not fixed!")
2236
+
2237
+ def test_revert_issue_90_aka_support_DOW7(self):
2238
+ self.assertTrue(croniter.is_valid("* * * * 1-7"))
2239
+ self.assertTrue(croniter.is_valid("* * * * 7"))
2240
+
2241
+ def test_sunday_ranges_to(self):
2242
+ self._test_sunday_ranges(
2243
+ "0 0 * * Sun-Sun",
2244
+ # fmt: off
2245
+ [
2246
+ 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,
2247
+ 14, 15, 16, 17, 18, 19,
2248
+ 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
2249
+ ],
2250
+ # fmt: on
2251
+ )
2252
+
2253
+ self._test_sunday_ranges(
2254
+ "0 0 * * Mon-Sun",
2255
+ # fmt: off
2256
+ [
2257
+ 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,
2258
+ 14, 15, 16, 17, 18, 19,
2259
+ 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
2260
+ ],
2261
+ # fmt: on
2262
+ )
2263
+
2264
+ self._test_sunday_ranges(
2265
+ "0 0 * * Tue-Sun",
2266
+ # fmt: off
2267
+ [
2268
+ 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13,
2269
+ 14, 16, 17, 18, 19, 20, 21,
2270
+ 23, 24, 25, 26, 27, 28, 30, 31, 1, 2, 3, 4,
2271
+ ],
2272
+ # fmt: on
2273
+ )
2274
+
2275
+ self._test_sunday_ranges(
2276
+ "0 0 * * Wed-Sun",
2277
+ # fmt: off
2278
+ [
2279
+ 3, 4, 5, 6, 7, 10, 11, 12, 13, 14,
2280
+ 17, 18, 19, 20, 21, 24, 25,
2281
+ 26, 27, 28, 31, 1, 2, 3, 4, 7, 8, 9, 10, 11,
2282
+ ],
2283
+ # fmt: on
2284
+ )
2285
+
2286
+ self._test_sunday_ranges(
2287
+ "0 0 * * Thu-Sun",
2288
+ # fmt: off
2289
+ [
2290
+ 4, 5, 6, 7, 11, 12, 13, 14, 18, 19,
2291
+ 20, 21, 25, 26, 27, 28, 1,
2292
+ 2, 3, 4, 8, 9, 10, 11, 15, 16, 17, 18, 22, 23,
2293
+ ],
2294
+ # fmt: on
2295
+ )
2296
+
2297
+ self._test_sunday_ranges(
2298
+ "0 0 * * Fri-Sun",
2299
+ # fmt: off
2300
+ [
2301
+ 5, 6, 7, 12, 13, 14, 19, 20, 21, 26,
2302
+ 27, 28, 2, 3, 4, 9, 10, 11,
2303
+ 16, 17, 18, 23, 24, 25, 1, 2, 3, 8, 9, 10,
2304
+ ],
2305
+ # fmt: on
2306
+ )
2307
+
2308
+ self._test_sunday_ranges(
2309
+ "0 0 * * Sat-Sun",
2310
+ # fmt: off
2311
+ [
2312
+ 6, 7, 13, 14, 20, 21, 27, 28, 3, 4, 10,
2313
+ 11, 17, 18, 24, 25, 2, 3,
2314
+ 9, 10, 16, 17, 23, 24, 30, 31, 6, 7, 13, 14,
2315
+ ],
2316
+ # fmt: on
2317
+ )
2318
+
2319
+ def test_sunday_ranges_from(self):
2320
+ self._test_sunday_ranges(
2321
+ "0 0 * * Sun-Mon",
2322
+ # fmt: off
2323
+ [
2324
+ 7, 8, 14, 15, 21, 22, 28, 29, 4, 5, 11,
2325
+ 12, 18, 19, 25, 26, 3, 4,
2326
+ 10, 11, 17, 18, 24, 25, 31, 1, 7, 8, 14, 15,
2327
+ ],
2328
+ # fmt: on
2329
+ )
2330
+
2331
+ self._test_sunday_ranges(
2332
+ "0 0 * * Sun-Tue",
2333
+ # fmt: off
2334
+ [
2335
+ 2, 7, 8, 9, 14, 15, 16, 21, 22, 23, 28,
2336
+ 29, 30, 4, 5, 6, 11, 12,
2337
+ 13, 18, 19, 20, 25, 26, 27, 3, 4, 5, 10, 11,
2338
+ ],
2339
+ # fmt: on
2340
+ )
2341
+
2342
+ self._test_sunday_ranges(
2343
+ "0 0 * * Sun-Wed",
2344
+ # fmt: off
2345
+ [
2346
+ 2, 3, 7, 8, 9, 10, 14, 15, 16, 17, 21, 22,
2347
+ 23, 24, 28, 29, 30, 31,
2348
+ 4, 5, 6, 7, 11, 12, 13, 14, 18, 19, 20, 21,
2349
+ ],
2350
+ # fmt: on
2351
+ )
2352
+
2353
+ self._test_sunday_ranges(
2354
+ "0 0 * * Sun-Thu",
2355
+ # fmt: off
2356
+ [
2357
+ 2, 3, 4, 7, 8, 9, 10, 11, 14, 15, 16, 17,
2358
+ 18, 21, 22, 23, 24, 25,
2359
+ 28, 29, 30, 31, 1, 4, 5, 6, 7, 8, 11, 12,
2360
+ ],
2361
+ # fmt: on
2362
+ )
2363
+
2364
+ self._test_sunday_ranges(
2365
+ "0 0 * * Sun-Fri",
2366
+ # fmt: off
2367
+ [
2368
+ 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 14, 15,
2369
+ 16, 17, 18, 19, 21, 22,
2370
+ 23, 24, 25, 26, 28, 29, 30, 31, 1, 2, 4, 5,
2371
+ ],
2372
+ # fmt: on
2373
+ )
2374
+
2375
+ self._test_sunday_ranges(
2376
+ "0 0 * * Sun-Sat",
2377
+ # fmt: off
2378
+ [
2379
+ 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,
2380
+ 14, 15, 16, 17, 18, 19,
2381
+ 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
2382
+ ],
2383
+ # fmt: on
2384
+ )
2385
+
2386
+ self._test_sunday_ranges(
2387
+ "0 0 * * Thu-Tue/2",
2388
+ # fmt: off
2389
+ [
2390
+ 2, 4, 6, 9, 11, 13, 16, 18, 20, 23, 25,
2391
+ 27, 30, 1, 3, 6, 8, 10,
2392
+ 13, 15, 17, 20, 22, 24, 27, 29, 2, 5, 7, 9,
2393
+ ],
2394
+ # fmt: on
2395
+ )
2396
+
2397
+ self._test_sunday_ranges(
2398
+ "0 0 * * Thu-Tue/3",
2399
+ # fmt: off
2400
+ [
2401
+ 4, 7, 11, 14, 18, 21, 25, 28, 1, 4, 8, 11,
2402
+ 15, 18, 22, 25, 29, 3,
2403
+ 7, 10, 14, 17, 21, 24, 28, 31, 4, 7, 11, 14,
2404
+ ],
2405
+ # fmt: on
2406
+ )
2407
+
2408
+ def test_mth_ranges_from(self):
2409
+ self._test_mth_cron_ranges(
2410
+ "0 0 1 Jan-Dec *",
2411
+ # fmt: off
2412
+ [
2413
+ "24 2", "24 3", "24 4", "24 5", "24 6",
2414
+ "24 7", "24 8", "24 9",
2415
+ "24 10", "24 11", "24 12", "25 1", "25 2",
2416
+ "25 3", "25 4", "25 5",
2417
+ ],
2418
+ # fmt: on
2419
+ )
2420
+ self._test_mth_cron_ranges(
2421
+ "0 0 1 Nov-Mar *",
2422
+ # fmt: off
2423
+ [
2424
+ "24 2", "24 3", "24 11", "24 12", "25 1",
2425
+ "25 2", "25 3", "25 11",
2426
+ "25 12", "26 1", "26 2", "26 3", "26 11",
2427
+ "26 12", "27 1", "27 2",
2428
+ ],
2429
+ # fmt: on
2430
+ )
2431
+ self._test_mth_cron_ranges(
2432
+ "0 0 1 Apr-Feb *",
2433
+ # fmt: off
2434
+ [
2435
+ "24 2", "24 4", "24 5", "24 6", "24 7", "24 8",
2436
+ "24 9", "24 10",
2437
+ "24 11", "24 12", "25 1", "25 2", "25 4", "25 5",
2438
+ "25 6", "25 7",
2439
+ ],
2440
+ # fmt: on
2441
+ )
2442
+ self._test_mth_cron_ranges(
2443
+ "0 0 1 Apr-Mar/3 *",
2444
+ # fmt: off
2445
+ [
2446
+ "24 4", "24 7", "24 10", "25 1", "25 4", "25 7",
2447
+ "25 10", "26 1",
2448
+ "26 4", "26 7", "26 10", "27 1", "27 4", "27 7",
2449
+ "27 10", "28 1",
2450
+ ],
2451
+ # fmt: on
2452
+ )
2453
+ self._test_mth_cron_ranges(
2454
+ "0 0 1 Apr-Mar/2 *",
2455
+ # fmt: off
2456
+ [
2457
+ "24 3", "24 4", "24 6", "24 8", "24 10", "24 12",
2458
+ "25 3", "25 4",
2459
+ "25 6", "25 8", "25 10", "25 12", "26 3", "26 4",
2460
+ "26 6", "26 8",
2461
+ ],
2462
+ # fmt: on
2463
+ )
2464
+ self._test_mth_cron_ranges(
2465
+ "0 0 1 Jan-Aug/2 *",
2466
+ # fmt: off
2467
+ [
2468
+ "24 3", "24 5", "24 7", "25 1", "25 3", "25 5",
2469
+ "25 7", "26 1",
2470
+ "26 3", "26 5", "26 7", "27 1", "27 3", "27 5",
2471
+ "27 7", "28 1",
2472
+ ],
2473
+ # fmt: on
2474
+ )
2475
+ self._test_mth_cron_ranges(
2476
+ "0 0 1 Jan-Aug/4 *",
2477
+ # fmt: off
2478
+ [
2479
+ "24 5", "25 1", "25 5", "26 1", "26 5", "27 1",
2480
+ "27 5", "28 1",
2481
+ "28 5", "29 1", "29 5", "30 1", "30 5", "31 1",
2482
+ "31 5", "32 1",
2483
+ ],
2484
+ # fmt: on
2485
+ )
2486
+
2487
+ def _test_cron_ranges(self, expr, wanted, generator=None, loops=None, start=None, is_prev=None):
2488
+ rets = (generator or gen_x_results)(
2489
+ expr, loops=loops or 10, start=start or datetime(2024, 1, 1), is_prev=is_prev
2490
+ )
2491
+ for ret in rets:
2492
+ self.assertEqual(wanted, ret)
2493
+
2494
+ def _test_mth_cron_ranges(self, expr, wanted, loops=None, start=None, is_prev=None):
2495
+ return self._test_cron_ranges(
2496
+ expr,
2497
+ wanted,
2498
+ generator=gen_x_mth_results,
2499
+ loops=loops or 16,
2500
+ start=start,
2501
+ is_prev=is_prev,
2502
+ )
2503
+
2504
+ def _test_sunday_ranges(self, expr, wanted, loops=None, start=None, is_prev=None):
2505
+ return self._test_cron_ranges(
2506
+ expr,
2507
+ wanted,
2508
+ generator=gen_all_sunday_forms,
2509
+ loops=loops or 30,
2510
+ start=start,
2511
+ is_prev=is_prev,
2512
+ )
2513
+
2514
+
2515
+ def gen_x_mth_results(expr, loops=None, start=None, is_prev=None):
2516
+ start = start or datetime(2024, 1, 1)
2517
+ cron = croniter(expr, start_time=start)
2518
+ n = cron.get_prev if is_prev else cron.get_next
2519
+ return [["{0} {1}".format(str(a.year)[-2:], a.month)
2520
+ for a in [n(datetime) for i in range(loops or 16)]]]
2521
+
2522
+
2523
+ def gen_x_results(expr, loops=None, start=None, is_prev=None):
2524
+ start = start or datetime(2024, 1, 1)
2525
+ cron = croniter(expr, start_time=start)
2526
+ n = cron.get_prev if is_prev else cron.get_next
2527
+ return [[a.isoformat() for a in [n(datetime) for i in range(loops or 30)]]]
2528
+
2529
+
2530
+ def gen_all_sunday_forms(expr, loops=None, start=None, is_prev=None):
2531
+ start = start or datetime(2024, 1, 1)
2532
+ cron = croniter(expr, start_time=start)
2533
+ n = cron.get_prev if is_prev else cron.get_next
2534
+ ret1 = [a.day for a in [n(datetime) for i in range(loops or 30)]]
2535
+ cron = croniter(expr.lower().replace("sun", "7"), start_time=start)
2536
+ n = cron.get_prev if is_prev else cron.get_next
2537
+ ret2 = [a.day for a in [n(datetime) for i in range(loops or 30)]]
2538
+ cron = croniter(expr.lower().replace("sun", "0"), start_time=start)
2539
+ n = cron.get_prev if is_prev else cron.get_next
2540
+ ret3 = [a.day for a in [n(datetime) for i in range(loops or 30)]]
2541
+ return ret1, ret2, ret3