mage-ai 0.9.14__py3-none-any.whl → 0.9.15__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.

Potentially problematic release.


This version of mage-ai might be problematic. Click here for more details.

Files changed (138) hide show
  1. mage_ai/api/resources/PipelineRunResource.py +8 -5
  2. mage_ai/data_preparation/executors/block_executor.py +5 -7
  3. mage_ai/data_preparation/templates/constants.py +16 -0
  4. mage_ai/data_preparation/templates/data_exporters/mssql.py +32 -0
  5. mage_ai/data_preparation/templates/data_loaders/mssql.py +27 -0
  6. mage_ai/io/base.py +5 -0
  7. mage_ai/io/mssql.py +1 -0
  8. mage_ai/orchestration/db/models/base.py +15 -15
  9. mage_ai/orchestration/db/models/oauth.py +35 -4
  10. mage_ai/orchestration/pipeline_scheduler.py +12 -6
  11. mage_ai/server/constants.py +1 -1
  12. mage_ai/server/frontend_dist/404.html +2 -2
  13. mage_ai/server/frontend_dist/404.html.html +2 -2
  14. mage_ai/server/frontend_dist/_next/static/chunks/{2786-f71862671f66d948.js → 2786-756b3834d10b8711.js} +1 -1
  15. mage_ai/server/frontend_dist/_next/static/chunks/3391-e1578fe1bdb5a557.js +1 -0
  16. mage_ai/server/frontend_dist/_next/static/chunks/6299-9b785c07dacf22b3.js +1 -0
  17. mage_ai/server/frontend_dist/_next/static/chunks/8952-050e60a8b1eaa32a.js +1 -0
  18. mage_ai/server/frontend_dist/_next/static/chunks/pages/files-03841b6dd7d240f6.js +1 -0
  19. mage_ai/server/frontend_dist/_next/static/chunks/pages/manage/settings-6e2c0e3f818fd4de.js +1 -0
  20. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/edit-6e59dc4e57dd2680.js +1 -0
  21. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/logs-02295848c811e26a.js +1 -0
  22. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/triggers/{[...slug]-ce11db27e4af7869.js → [...slug]-91889109af36a793.js} +1 -1
  23. mage_ai/server/frontend_dist/_next/static/{CNjkRIWPaAu1yJUmIaX_q → h5mR3pjfn_EQy348CZ_ok}/_buildManifest.js +1 -1
  24. mage_ai/server/frontend_dist/files.html +2 -2
  25. mage_ai/server/frontend_dist/global-data-products/[...slug].html +2 -2
  26. mage_ai/server/frontend_dist/global-data-products.html +2 -2
  27. mage_ai/server/frontend_dist/index.html +2 -2
  28. mage_ai/server/frontend_dist/manage/settings.html +2 -2
  29. mage_ai/server/frontend_dist/manage/users/[user].html +2 -2
  30. mage_ai/server/frontend_dist/manage/users/new.html +2 -2
  31. mage_ai/server/frontend_dist/manage/users.html +2 -2
  32. mage_ai/server/frontend_dist/manage.html +2 -2
  33. mage_ai/server/frontend_dist/overview.html +2 -2
  34. mage_ai/server/frontend_dist/pipeline-runs.html +2 -2
  35. mage_ai/server/frontend_dist/pipelines/[pipeline]/backfills/[...slug].html +2 -2
  36. mage_ai/server/frontend_dist/pipelines/[pipeline]/backfills.html +2 -2
  37. mage_ai/server/frontend_dist/pipelines/[pipeline]/edit.html +2 -2
  38. mage_ai/server/frontend_dist/pipelines/[pipeline]/logs.html +2 -2
  39. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors/block-runs.html +2 -2
  40. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors/block-runtime.html +2 -2
  41. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors.html +2 -2
  42. mage_ai/server/frontend_dist/pipelines/[pipeline]/runs/[run].html +2 -2
  43. mage_ai/server/frontend_dist/pipelines/[pipeline]/runs.html +2 -2
  44. mage_ai/server/frontend_dist/pipelines/[pipeline]/settings.html +2 -2
  45. mage_ai/server/frontend_dist/pipelines/[pipeline]/syncs.html +2 -2
  46. mage_ai/server/frontend_dist/pipelines/[pipeline]/triggers/[...slug].html +2 -2
  47. mage_ai/server/frontend_dist/pipelines/[pipeline]/triggers.html +2 -2
  48. mage_ai/server/frontend_dist/pipelines/[pipeline].html +2 -2
  49. mage_ai/server/frontend_dist/pipelines.html +2 -2
  50. mage_ai/server/frontend_dist/settings/account/profile.html +2 -2
  51. mage_ai/server/frontend_dist/settings/workspace/preferences.html +2 -2
  52. mage_ai/server/frontend_dist/settings/workspace/sync-data.html +2 -2
  53. mage_ai/server/frontend_dist/settings/workspace/users.html +2 -2
  54. mage_ai/server/frontend_dist/settings.html +2 -2
  55. mage_ai/server/frontend_dist/sign-in.html +2 -2
  56. mage_ai/server/frontend_dist/templates/[...slug].html +2 -2
  57. mage_ai/server/frontend_dist/templates.html +2 -2
  58. mage_ai/server/frontend_dist/terminal.html +2 -2
  59. mage_ai/server/frontend_dist/test.html +2 -2
  60. mage_ai/server/frontend_dist/triggers.html +2 -2
  61. mage_ai/server/frontend_dist/version-control.html +2 -2
  62. mage_ai/server/frontend_dist_base_path_template/404.html +2 -2
  63. mage_ai/server/frontend_dist_base_path_template/404.html.html +2 -2
  64. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/{2786-f71862671f66d948.js → 2786-756b3834d10b8711.js} +1 -1
  65. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/3391-e1578fe1bdb5a557.js +1 -0
  66. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/6299-9b785c07dacf22b3.js +1 -0
  67. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/8952-050e60a8b1eaa32a.js +1 -0
  68. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/files-03841b6dd7d240f6.js +1 -0
  69. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/manage/settings-6e2c0e3f818fd4de.js +1 -0
  70. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/edit-6e59dc4e57dd2680.js +1 -0
  71. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/logs-02295848c811e26a.js +1 -0
  72. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/triggers/{[...slug]-ce11db27e4af7869.js → [...slug]-91889109af36a793.js} +1 -1
  73. mage_ai/server/frontend_dist_base_path_template/_next/static/{yJvL-3bfsNF3WCStZ10Dm → xykYLW_ARw5SevdiponLI}/_buildManifest.js +1 -1
  74. mage_ai/server/frontend_dist_base_path_template/files.html +2 -2
  75. mage_ai/server/frontend_dist_base_path_template/global-data-products/[...slug].html +2 -2
  76. mage_ai/server/frontend_dist_base_path_template/global-data-products.html +2 -2
  77. mage_ai/server/frontend_dist_base_path_template/index.html +2 -2
  78. mage_ai/server/frontend_dist_base_path_template/manage/settings.html +2 -2
  79. mage_ai/server/frontend_dist_base_path_template/manage/users/[user].html +2 -2
  80. mage_ai/server/frontend_dist_base_path_template/manage/users/new.html +2 -2
  81. mage_ai/server/frontend_dist_base_path_template/manage/users.html +2 -2
  82. mage_ai/server/frontend_dist_base_path_template/manage.html +2 -2
  83. mage_ai/server/frontend_dist_base_path_template/overview.html +2 -2
  84. mage_ai/server/frontend_dist_base_path_template/pipeline-runs.html +2 -2
  85. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/backfills/[...slug].html +2 -2
  86. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/backfills.html +2 -2
  87. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/edit.html +2 -2
  88. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/logs.html +2 -2
  89. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/monitors/block-runs.html +2 -2
  90. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/monitors/block-runtime.html +2 -2
  91. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/monitors.html +2 -2
  92. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/runs/[run].html +2 -2
  93. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/runs.html +2 -2
  94. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/settings.html +2 -2
  95. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/syncs.html +2 -2
  96. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/triggers/[...slug].html +2 -2
  97. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/triggers.html +2 -2
  98. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline].html +2 -2
  99. mage_ai/server/frontend_dist_base_path_template/pipelines.html +2 -2
  100. mage_ai/server/frontend_dist_base_path_template/settings/account/profile.html +2 -2
  101. mage_ai/server/frontend_dist_base_path_template/settings/workspace/preferences.html +2 -2
  102. mage_ai/server/frontend_dist_base_path_template/settings/workspace/sync-data.html +2 -2
  103. mage_ai/server/frontend_dist_base_path_template/settings/workspace/users.html +2 -2
  104. mage_ai/server/frontend_dist_base_path_template/settings.html +2 -2
  105. mage_ai/server/frontend_dist_base_path_template/sign-in.html +2 -2
  106. mage_ai/server/frontend_dist_base_path_template/templates/[...slug].html +2 -2
  107. mage_ai/server/frontend_dist_base_path_template/templates.html +2 -2
  108. mage_ai/server/frontend_dist_base_path_template/terminal.html +2 -2
  109. mage_ai/server/frontend_dist_base_path_template/test.html +2 -2
  110. mage_ai/server/frontend_dist_base_path_template/triggers.html +2 -2
  111. mage_ai/server/frontend_dist_base_path_template/version-control.html +2 -2
  112. mage_ai/server/server.py +12 -1
  113. mage_ai/shared/retry.py +1 -1
  114. {mage_ai-0.9.14.dist-info → mage_ai-0.9.15.dist-info}/METADATA +1 -1
  115. {mage_ai-0.9.14.dist-info → mage_ai-0.9.15.dist-info}/RECORD +123 -122
  116. mage_ai/server/frontend_dist/_next/static/chunks/3391-6f0a0a5c254cd7f2.js +0 -1
  117. mage_ai/server/frontend_dist/_next/static/chunks/6299-fcb702d0f3d3291b.js +0 -1
  118. mage_ai/server/frontend_dist/_next/static/chunks/8952-9d6fa18fa9378989.js +0 -1
  119. mage_ai/server/frontend_dist/_next/static/chunks/pages/files-2dc2a0dfc0ff620d.js +0 -1
  120. mage_ai/server/frontend_dist/_next/static/chunks/pages/manage/settings-6577159a0af52a78.js +0 -1
  121. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/edit-54cb4936accdd2d9.js +0 -1
  122. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/logs-d331e98ad103b13e.js +0 -1
  123. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/3391-6f0a0a5c254cd7f2.js +0 -1
  124. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/6299-fcb702d0f3d3291b.js +0 -1
  125. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/8952-9d6fa18fa9378989.js +0 -1
  126. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/files-2dc2a0dfc0ff620d.js +0 -1
  127. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/manage/settings-6577159a0af52a78.js +0 -1
  128. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/edit-54cb4936accdd2d9.js +0 -1
  129. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/logs-d331e98ad103b13e.js +0 -1
  130. mage_ai/tests/orchestration/db/test_models.py +0 -710
  131. /mage_ai/server/frontend_dist/_next/static/{CNjkRIWPaAu1yJUmIaX_q → h5mR3pjfn_EQy348CZ_ok}/_middlewareManifest.js +0 -0
  132. /mage_ai/server/frontend_dist/_next/static/{CNjkRIWPaAu1yJUmIaX_q → h5mR3pjfn_EQy348CZ_ok}/_ssgManifest.js +0 -0
  133. /mage_ai/server/frontend_dist_base_path_template/_next/static/{yJvL-3bfsNF3WCStZ10Dm → xykYLW_ARw5SevdiponLI}/_middlewareManifest.js +0 -0
  134. /mage_ai/server/frontend_dist_base_path_template/_next/static/{yJvL-3bfsNF3WCStZ10Dm → xykYLW_ARw5SevdiponLI}/_ssgManifest.js +0 -0
  135. {mage_ai-0.9.14.dist-info → mage_ai-0.9.15.dist-info}/LICENSE +0 -0
  136. {mage_ai-0.9.14.dist-info → mage_ai-0.9.15.dist-info}/WHEEL +0 -0
  137. {mage_ai-0.9.14.dist-info → mage_ai-0.9.15.dist-info}/entry_points.txt +0 -0
  138. {mage_ai-0.9.14.dist-info → mage_ai-0.9.15.dist-info}/top_level.txt +0 -0
@@ -1,710 +0,0 @@
1
- import os
2
- from datetime import datetime, timezone
3
-
4
- from croniter import croniter
5
- from freezegun import freeze_time
6
-
7
- from mage_ai.data_preparation.models.constants import PipelineType
8
- from mage_ai.data_preparation.models.triggers import (
9
- ScheduleInterval,
10
- ScheduleStatus,
11
- ScheduleType,
12
- )
13
- from mage_ai.data_preparation.repo_manager import get_repo_config
14
- from mage_ai.orchestration.db.models.schedules import PipelineRun, PipelineSchedule
15
- from mage_ai.orchestration.pipeline_scheduler import configure_pipeline_run_payload
16
- from mage_ai.shared.hash import merge_dict
17
- from mage_ai.tests.base_test import DBTestCase
18
- from mage_ai.tests.factory import (
19
- create_pipeline_run,
20
- create_pipeline_run_with_schedule,
21
- create_pipeline_with_blocks,
22
- )
23
-
24
-
25
- class PipelineScheduleTests(DBTestCase):
26
- @classmethod
27
- def setUpClass(self):
28
- super().setUpClass()
29
- self.pipeline = create_pipeline_with_blocks(
30
- 'test pipeline',
31
- self.repo_path,
32
- )
33
-
34
- def test_pipeline_runs_count(self):
35
- pipeline_schedule = PipelineSchedule.create(pipeline_uuid='test_pipeline')
36
- for _ in range(5):
37
- create_pipeline_run_with_schedule(
38
- 'test_pipeline',
39
- pipeline_schedule_id=pipeline_schedule.id,
40
- )
41
- self.assertEqual(pipeline_schedule.pipeline_runs_count, 5)
42
-
43
- def test_validate_schedule_interval(self):
44
- PipelineSchedule.create(
45
- pipeline_uuid='test_pipeline',
46
- schedule_interval='@daily'
47
- )
48
- PipelineSchedule.create(
49
- pipeline_uuid='test_pipeline',
50
- schedule_interval='* * * * *'
51
- )
52
- with self.assertRaises(ValueError) as context:
53
- PipelineSchedule.create(
54
- pipeline_uuid='test_pipeline',
55
- schedule_interval='random_str'
56
- )
57
- self.assertTrue('Cron expression is invalid.' in str(context.exception))
58
-
59
- def test_active_schedules(self):
60
- create_pipeline_with_blocks(
61
- 'test active schedule 1',
62
- self.repo_path,
63
- )
64
- create_pipeline_with_blocks(
65
- 'test active schedule 2',
66
- self.repo_path,
67
- )
68
- PipelineSchedule.create(
69
- pipeline_uuid='test_active_schedule_1',
70
- schedule_interval='@daily'
71
- )
72
- pipeline_schedule2 = PipelineSchedule.create(
73
- pipeline_uuid='test_active_schedule_1',
74
- schedule_interval='@daily'
75
- )
76
- pipeline_schedule3 = PipelineSchedule.create(
77
- pipeline_uuid='test_active_schedule_2',
78
- schedule_interval='@daily'
79
- )
80
- PipelineSchedule.create(
81
- pipeline_uuid='test_active_schedule_2',
82
- schedule_interval='@daily'
83
- )
84
- pipeline_schedule2.update(status=ScheduleStatus.ACTIVE)
85
- pipeline_schedule3.update(status=ScheduleStatus.ACTIVE)
86
- results1 = PipelineSchedule.active_schedules(pipeline_uuids=['test_active_schedule_1'])
87
- results2 = PipelineSchedule.active_schedules(pipeline_uuids=['test_active_schedule_2'])
88
- results3 = PipelineSchedule.active_schedules()
89
- self.assertEqual(set([r.id for r in results1]), set([pipeline_schedule2.id]))
90
- self.assertEqual(set([r.id for r in results2]), set([pipeline_schedule3.id]))
91
- self.assertEqual(
92
- set([r.id for r in results3]),
93
- set([pipeline_schedule2.id, pipeline_schedule3.id]),
94
- )
95
-
96
- @freeze_time('2023-10-11 12:13:14')
97
- def test_should_schedule(self):
98
- shared_attrs = dict(
99
- pipeline_uuid='test_pipeline',
100
- schedule_interval=ScheduleInterval.DAILY,
101
- schedule_type=ScheduleType.TIME,
102
- )
103
-
104
- self.assertFalse(PipelineSchedule.create(**shared_attrs).should_schedule())
105
-
106
- self.assertFalse(PipelineSchedule.create(**merge_dict(shared_attrs, dict(
107
- start_time=datetime(2023, 10, 11, 12, 13, 15),
108
- status=ScheduleStatus.ACTIVE,
109
- ))).should_schedule())
110
-
111
- self.assertFalse(PipelineSchedule.create(**merge_dict(shared_attrs, dict(
112
- pipeline_uuid='does_not_exist',
113
- start_time=datetime(2023, 10, 11, 12, 13, 13),
114
- status=ScheduleStatus.ACTIVE,
115
- ))).should_schedule())
116
-
117
- pipeline_schedule1 = PipelineSchedule.create(**merge_dict(shared_attrs, dict(
118
- schedule_interval=ScheduleInterval.ONCE,
119
- start_time=datetime(2023, 10, 11, 12, 13, 13),
120
- status=ScheduleStatus.ACTIVE,
121
- )))
122
- self.assertTrue(pipeline_schedule1.should_schedule())
123
- PipelineRun.create(
124
- pipeline_schedule_id=pipeline_schedule1.id,
125
- pipeline_uuid=pipeline_schedule1.pipeline_uuid,
126
- status=PipelineRun.PipelineRunStatus.COMPLETED,
127
- )
128
- self.assertFalse(pipeline_schedule1.should_schedule())
129
-
130
- self.assertFalse(PipelineSchedule.create(**merge_dict(shared_attrs, dict(
131
- schedule_interval=None,
132
- status=ScheduleStatus.ACTIVE
133
- ))).should_schedule())
134
-
135
- self.assertTrue(PipelineSchedule.create(**merge_dict(shared_attrs, dict(
136
- status=ScheduleStatus.ACTIVE
137
- ))).should_schedule())
138
-
139
- for schedule_interval, execution_date_true, execution_date_false in [
140
- (
141
- ScheduleInterval.HOURLY,
142
- datetime(2023, 10, 11, 11, 0, 0),
143
- datetime(2023, 10, 11, 12, 0, 0),
144
- ),
145
- (
146
- ScheduleInterval.DAILY,
147
- datetime(2023, 10, 10, 0, 0, 0),
148
- datetime(2023, 10, 11, 0, 0, 0),
149
- ),
150
- (
151
- ScheduleInterval.WEEKLY,
152
- datetime(2023, 10, 2, 0, 0, 0),
153
- datetime(2023, 10, 9, 0, 0, 0),
154
- ),
155
- (
156
- ScheduleInterval.MONTHLY,
157
- datetime(2023, 9, 1, 0, 0, 0),
158
- datetime(2023, 10, 1, 0, 0, 0),
159
- ),
160
- ]:
161
- pipeline_schedule = PipelineSchedule.create(**merge_dict(shared_attrs, dict(
162
- schedule_interval=schedule_interval,
163
- start_time=datetime(2023, 10, 11, 12, 13, 13),
164
- status=ScheduleStatus.ACTIVE,
165
- )))
166
- PipelineRun.create(
167
- execution_date=execution_date_true,
168
- pipeline_schedule_id=pipeline_schedule.id,
169
- pipeline_uuid=pipeline_schedule.pipeline_uuid,
170
- status=PipelineRun.PipelineRunStatus.COMPLETED,
171
- )
172
- self.assertTrue(pipeline_schedule.should_schedule())
173
- PipelineRun.create(
174
- execution_date=execution_date_false,
175
- pipeline_schedule_id=pipeline_schedule.id,
176
- pipeline_uuid=pipeline_schedule.pipeline_uuid,
177
- status=PipelineRun.PipelineRunStatus.COMPLETED,
178
- )
179
- self.assertFalse(pipeline_schedule.should_schedule())
180
-
181
- @freeze_time('2023-10-11 12:13:14')
182
- def test_should_schedule_when_landing_time_enabled(self):
183
- shared_attrs = dict(
184
- pipeline_uuid='test_pipeline',
185
- schedule_type=ScheduleType.TIME,
186
- settings=dict(landing_time_enabled=True),
187
- status=ScheduleStatus.ACTIVE,
188
- )
189
-
190
- # No previous runtimes
191
- self.assertTrue(PipelineSchedule.create(**merge_dict(shared_attrs, dict(
192
- schedule_interval=ScheduleInterval.HOURLY,
193
- ))).should_schedule())
194
-
195
- for schedule_interval, previous_runtimes, landing_time_true, landing_time_false in [
196
- (
197
- ScheduleInterval.HOURLY,
198
- # AVG: 4
199
- # STD: 2
200
- [1, 2, 3, 4, 5, 6, 7],
201
- datetime(2023, 10, 12, 13, 13, 20),
202
- datetime(2023, 10, 13, 14, 13, 21),
203
- ),
204
- (
205
- ScheduleInterval.DAILY,
206
- # AVG: 4000
207
- # STD: 1081
208
- [1000, 2000, 3000, 4000, 5000, 6000, 7000],
209
- datetime(2023, 10, 12, 13, 37, 55),
210
- datetime(2023, 10, 13, 13, 37, 56),
211
- ),
212
- (
213
- # 2023-10-11 is a Wednesday
214
- ScheduleInterval.WEEKLY,
215
- # AVG: 216000
216
- # STD: 46662
217
- [
218
- 1 * 86400,
219
- 1.5 * 86400,
220
- 2 * 86400,
221
- 2.5 * 86400,
222
- 3 * 86400,
223
- 3.5 * 86400,
224
- 4 * 86400,
225
- ],
226
- # 2023-10-21 is a Saturday
227
- datetime(2023, 10, 21, 13, 10, 56),
228
- datetime(2023, 10, 28, 13, 10, 57),
229
- ),
230
- (
231
- ScheduleInterval.MONTHLY,
232
- # AVG: 537,536
233
- # STD: 131,304
234
- # 7 days
235
- # 17 hours
236
- # 47 minutes
237
- # 20 seconds
238
- [
239
- 3 * 86400,
240
- 3.5 * 86400,
241
- 5 * 86400,
242
- 5.5 * 86400,
243
- 7 * 86400,
244
- 7.5 * 86400,
245
- 12 * 86400,
246
- ],
247
- datetime(2023, 11, 19, 5, 50, 13),
248
- datetime(2023, 12, 19, 5, 50, 14),
249
- ),
250
- ]:
251
- self.assertTrue(PipelineSchedule.create(**merge_dict(shared_attrs, dict(
252
- schedule_interval=schedule_interval,
253
- start_time=landing_time_true,
254
- ))).should_schedule(previous_runtimes=previous_runtimes))
255
- self.assertFalse(PipelineSchedule.create(**merge_dict(shared_attrs, dict(
256
- schedule_interval=schedule_interval,
257
- start_time=landing_time_false,
258
- ))).should_schedule(previous_runtimes=previous_runtimes))
259
-
260
- def test_landing_time_enabled(self):
261
- for schedule_type in [
262
- ScheduleType.API,
263
- ScheduleType.EVENT,
264
- ]:
265
- self.assertFalse(PipelineSchedule.create(
266
- pipeline_uuid='test_pipeline',
267
- schedule_interval=ScheduleInterval.HOURLY,
268
- schedule_type=schedule_type,
269
- settings=dict(landing_time_enabled=True),
270
- ).landing_time_enabled())
271
-
272
- for schedule_interval in [
273
- '* * * * *',
274
- ScheduleInterval.ONCE,
275
- ]:
276
- self.assertFalse(PipelineSchedule.create(
277
- pipeline_uuid='test_pipeline',
278
- schedule_interval=schedule_interval,
279
- schedule_type=ScheduleType.TIME,
280
- settings=dict(landing_time_enabled=True),
281
- ).landing_time_enabled())
282
-
283
- self.assertFalse(PipelineSchedule.create(
284
- pipeline_uuid='test_pipeline',
285
- schedule_interval=ScheduleInterval.HOURLY,
286
- schedule_type=ScheduleType.TIME,
287
- settings=dict(landing_time_enabled=False),
288
- ).landing_time_enabled())
289
-
290
- self.assertFalse(PipelineSchedule.create(
291
- pipeline_uuid='test_pipeline',
292
- schedule_interval=ScheduleInterval.HOURLY,
293
- schedule_type=ScheduleType.TIME,
294
- settings={},
295
- ).landing_time_enabled())
296
-
297
- self.assertTrue(PipelineSchedule.create(
298
- pipeline_uuid='test_pipeline',
299
- schedule_interval=ScheduleInterval.HOURLY,
300
- schedule_type=ScheduleType.TIME,
301
- settings=dict(landing_time_enabled=True),
302
- ).landing_time_enabled())
303
-
304
- @freeze_time('2023-12-10 12:00:00')
305
- def test_runtime_history(self):
306
- pipeline_schedule = PipelineSchedule.create(
307
- pipeline_uuid='test_pipeline',
308
- schedule_interval=ScheduleInterval.DAILY,
309
- schedule_type=ScheduleType.TIME,
310
- settings=dict(landing_time_enabled=True),
311
- )
312
- self.assertEqual(pipeline_schedule.runtime_history(), [])
313
-
314
- pipeline_schedule2 = PipelineSchedule.create(
315
- pipeline_uuid='test_pipeline',
316
- schedule_interval=ScheduleInterval.DAILY,
317
- schedule_type=ScheduleType.TIME,
318
- settings=dict(landing_time_enabled=True),
319
- )
320
-
321
- for created_at_hour, completed_at_hour, execution_date_hour in [
322
- (1, 2, 4),
323
- (2, 4, 3),
324
- (1, 7, 2),
325
- (2, 9, 1),
326
- (9, 11, 0),
327
- (11, 14, 5),
328
- (14, 18, 6),
329
- (18, 23, 7),
330
- ]:
331
- created_at = datetime(2023, 12, 10, created_at_hour, 0, 0)
332
- completed_at = datetime(2023, 12, 10, completed_at_hour, 0, 0)
333
- execution_date = datetime(2023, 12, 10, execution_date_hour, 0, 0)
334
-
335
- for ps in [pipeline_schedule, pipeline_schedule2]:
336
- PipelineRun.create(
337
- completed_at=completed_at,
338
- created_at=created_at,
339
- execution_date=execution_date,
340
- pipeline_schedule_id=ps.id,
341
- pipeline_uuid=ps.pipeline_uuid,
342
- status=PipelineRun.PipelineRunStatus.COMPLETED,
343
- variables=ps.variables,
344
- )
345
-
346
- created_at = datetime(2023, 12, 10, 0, 0, 0)
347
- completed_at = datetime(2023, 12, 10, 23, 0, 0)
348
- execution_date = datetime(2023, 12, 10, 23, 59, 59)
349
- PipelineRun.create(
350
- completed_at=completed_at,
351
- created_at=created_at,
352
- execution_date=execution_date,
353
- pipeline_schedule_id=pipeline_schedule.id,
354
- pipeline_uuid=pipeline_schedule.pipeline_uuid,
355
- status=PipelineRun.PipelineRunStatus.FAILED,
356
- variables=pipeline_schedule.variables,
357
- )
358
-
359
- self.assertEqual(pipeline_schedule.runtime_history(), [
360
- (5 * 3600.0),
361
- (4 * 3600.0),
362
- (3 * 3600.0),
363
- (1 * 3600.0),
364
- (2 * 3600.0),
365
- (6 * 3600.0),
366
- (7 * 3600.0),
367
- ])
368
-
369
- created_at = datetime(2023, 12, 10, 0, 0, 0)
370
- completed_at = datetime(2023, 12, 10, 23, 0, 0)
371
- execution_date = datetime(2023, 12, 10, 23, 59, 59)
372
- pipeline_run = PipelineRun.create(
373
- completed_at=completed_at,
374
- created_at=created_at,
375
- execution_date=execution_date,
376
- pipeline_schedule_id=pipeline_schedule.id,
377
- pipeline_uuid=pipeline_schedule.pipeline_uuid,
378
- status=PipelineRun.PipelineRunStatus.COMPLETED,
379
- variables=pipeline_schedule.variables,
380
- )
381
-
382
- self.assertEqual(pipeline_schedule.runtime_history(pipeline_run=pipeline_run), [
383
- (23 * 3600.0),
384
- (5 * 3600.0),
385
- (4 * 3600.0),
386
- (3 * 3600.0),
387
- (1 * 3600.0),
388
- (2 * 3600.0),
389
- (6 * 3600.0),
390
- ])
391
- self.assertEqual(pipeline_schedule.runtime_history(
392
- pipeline_run=pipeline_run,
393
- sample_size=5,
394
- ), [
395
- (23 * 3600.0),
396
- (5 * 3600.0),
397
- (4 * 3600.0),
398
- (3 * 3600.0),
399
- (1 * 3600.0),
400
- ])
401
-
402
- created_at = datetime(2023, 12, 10, 0, 0, 0)
403
- completed_at = datetime(2023, 12, 10, 22, 0, 0)
404
- execution_date = datetime(2023, 12, 11, 23, 59, 59)
405
- pipeline_run = PipelineRun.create(
406
- completed_at=completed_at,
407
- created_at=created_at,
408
- execution_date=execution_date,
409
- metrics=dict(previous_runtimes=[
410
- 1.0,
411
- 2.0,
412
- 3.0,
413
- 4.0,
414
- 5.0,
415
- 6.0,
416
- 7.0,
417
- 8.0,
418
- ]),
419
- pipeline_schedule_id=pipeline_schedule.id,
420
- pipeline_uuid=pipeline_schedule.pipeline_uuid,
421
- status=PipelineRun.PipelineRunStatus.COMPLETED,
422
- variables=pipeline_schedule.variables,
423
- )
424
-
425
- self.assertEqual(pipeline_schedule.runtime_history(pipeline_run=pipeline_run), [
426
- (22 * 3600.0),
427
- 1.0,
428
- 2.0,
429
- 3.0,
430
- 4.0,
431
- 5.0,
432
- 6.0,
433
- ])
434
-
435
- self.assertEqual(pipeline_schedule.runtime_history(
436
- pipeline_run=pipeline_run,
437
- sample_size=3,
438
- ), [
439
- (22 * 3600.0),
440
- 1.0,
441
- 2.0,
442
- ])
443
-
444
- def test_runtime_average(self):
445
- pipeline_schedule = PipelineSchedule.create(
446
- pipeline_uuid='test_pipeline',
447
- schedule_interval=ScheduleInterval.DAILY,
448
- schedule_type=ScheduleType.TIME,
449
- settings=dict(landing_time_enabled=True),
450
- )
451
- self.assertEqual(pipeline_schedule.runtime_average(), None)
452
-
453
- created_at = datetime(2023, 12, 10, 0, 0, 0)
454
- completed_at = datetime(2023, 12, 10, 9, 0, 0)
455
- execution_date = datetime(2023, 12, 11, 23, 59, 59)
456
- pipeline_run = PipelineRun.create(
457
- completed_at=completed_at,
458
- created_at=created_at,
459
- execution_date=execution_date,
460
- metrics=dict(previous_runtimes=[
461
- 1 * 3600.0,
462
- 2 * 3600.0,
463
- 3 * 3600.0,
464
- 4 * 3600.0,
465
- 5 * 3600.0,
466
- 6 * 3600.0,
467
- 7 * 3600.0,
468
- 8 * 3600.0,
469
- ]),
470
- pipeline_schedule_id=pipeline_schedule.id,
471
- pipeline_uuid=pipeline_schedule.pipeline_uuid,
472
- status=PipelineRun.PipelineRunStatus.COMPLETED,
473
- variables=pipeline_schedule.variables,
474
- )
475
- self.assertEqual(pipeline_schedule.runtime_average(
476
- pipeline_run=pipeline_run,
477
- ), 15428.57)
478
-
479
- self.assertEqual(pipeline_schedule.runtime_average(
480
- pipeline_run=pipeline_run,
481
- sample_size=5,
482
- ), 13680.0)
483
-
484
- @freeze_time('2023-10-11 12:13:14')
485
- def test_current_execution_date(self):
486
- now = datetime.now(timezone.utc)
487
- shared_attrs = dict(
488
- pipeline_uuid='test_pipeline',
489
- schedule_type=ScheduleType.TIME,
490
- )
491
-
492
- self.assertEqual(PipelineSchedule.create(**shared_attrs).current_execution_date(), None)
493
-
494
- self.assertEqual(
495
- PipelineSchedule.create(**merge_dict(shared_attrs, dict(
496
- schedule_interval=ScheduleInterval.ONCE,
497
- ))).current_execution_date(),
498
- datetime(2023, 10, 11, 12, 13, 14).replace(tzinfo=timezone.utc),
499
- )
500
-
501
- self.assertEqual(
502
- PipelineSchedule.create(**merge_dict(shared_attrs, dict(
503
- schedule_interval=ScheduleInterval.HOURLY,
504
- ))).current_execution_date(),
505
- datetime(2023, 10, 11, 12, 0, 0).replace(tzinfo=timezone.utc),
506
- )
507
-
508
- self.assertEqual(
509
- PipelineSchedule.create(**merge_dict(shared_attrs, dict(
510
- schedule_interval=ScheduleInterval.DAILY,
511
- ))).current_execution_date(),
512
- datetime(2023, 10, 11, 0, 0, 0).replace(tzinfo=timezone.utc),
513
- )
514
-
515
- self.assertEqual(
516
- PipelineSchedule.create(**merge_dict(shared_attrs, dict(
517
- schedule_interval=ScheduleInterval.WEEKLY,
518
- ))).current_execution_date(),
519
- datetime(2023, 10, 9, 0, 0, 0).replace(tzinfo=timezone.utc),
520
- )
521
-
522
- self.assertEqual(
523
- PipelineSchedule.create(**merge_dict(shared_attrs, dict(
524
- schedule_interval=ScheduleInterval.MONTHLY,
525
- ))).current_execution_date(),
526
- datetime(2023, 10, 1, 0, 0, 0).replace(tzinfo=timezone.utc),
527
- )
528
-
529
- cron_itr = croniter('* * * * *', now)
530
- self.assertEqual(
531
- PipelineSchedule.create(**merge_dict(shared_attrs, dict(
532
- schedule_interval='* * * * *',
533
- ))).current_execution_date(),
534
- cron_itr.get_prev(datetime),
535
- )
536
-
537
- @freeze_time('2023-10-11 12:13:14')
538
- def test_current_execution_date_when_landing_time_enabled(self):
539
- now = datetime.now(timezone.utc)
540
- shared_attrs = dict(
541
- pipeline_uuid='test_pipeline',
542
- schedule_interval='@once',
543
- schedule_type=ScheduleType.TIME,
544
- settings=dict(landing_time_enabled=True),
545
- start_time=datetime(2024, 11, 12, 13, 14, 15).replace(tzinfo=timezone.utc),
546
- )
547
-
548
- self.assertEqual(PipelineSchedule.create(**shared_attrs).current_execution_date(), now)
549
-
550
- self.assertEqual(
551
- PipelineSchedule.create(**merge_dict(shared_attrs, dict(
552
- schedule_interval=ScheduleInterval.HOURLY,
553
- ))).current_execution_date(),
554
- datetime(2023, 10, 11, 12, 14, 15).replace(tzinfo=timezone.utc),
555
- )
556
-
557
- self.assertEqual(
558
- PipelineSchedule.create(**merge_dict(shared_attrs, dict(
559
- schedule_interval=ScheduleInterval.DAILY,
560
- ))).current_execution_date(),
561
- datetime(2023, 10, 11, 13, 14, 15).replace(tzinfo=timezone.utc),
562
- )
563
-
564
- self.assertEqual(
565
- PipelineSchedule.create(**merge_dict(shared_attrs, dict(
566
- schedule_interval=ScheduleInterval.WEEKLY,
567
- ))).current_execution_date(),
568
- datetime(2023, 10, 10, 13, 14, 15).replace(tzinfo=timezone.utc),
569
- )
570
-
571
- self.assertEqual(
572
- PipelineSchedule.create(**merge_dict(shared_attrs, dict(
573
- schedule_interval=ScheduleInterval.MONTHLY,
574
- ))).current_execution_date(),
575
- datetime(2023, 10, 12, 13, 14, 15).replace(tzinfo=timezone.utc),
576
- )
577
-
578
- cron_itr = croniter('* * * * *', now)
579
- self.assertEqual(
580
- PipelineSchedule.create(**merge_dict(shared_attrs, dict(
581
- schedule_interval='* * * * *',
582
- ))).current_execution_date(),
583
- cron_itr.get_prev(datetime),
584
- )
585
-
586
-
587
- class PipelineRunTests(DBTestCase):
588
- @classmethod
589
- def setUpClass(self):
590
- super().setUpClass()
591
- self.pipeline = create_pipeline_with_blocks(
592
- 'test pipeline',
593
- self.repo_path,
594
- )
595
-
596
- def test_block_runs_count(self):
597
- pipeline_run = create_pipeline_run(pipeline_uuid='test_pipeline')
598
- block_count = len(self.__class__.pipeline.get_executable_blocks())
599
- self.assertEqual(pipeline_run.block_runs_count, block_count)
600
-
601
- def test_execution_partition(self):
602
- execution_date = datetime.now()
603
- pipeline_run = create_pipeline_run_with_schedule(
604
- pipeline_uuid='test_pipeline',
605
- execution_date=execution_date,
606
- )
607
- execution_date_str = execution_date.strftime(format='%Y%m%dT%H%M%S')
608
- self.assertEqual(
609
- pipeline_run.execution_partition,
610
- f'{pipeline_run.pipeline_schedule_id}/{execution_date_str}',
611
- )
612
-
613
- @freeze_time('2023-05-01 01:20:33')
614
- def test_execution_partition_from_variables(self):
615
- pipeline_schedule = PipelineSchedule.create(pipeline_uuid='test_pipeline')
616
- payload = configure_pipeline_run_payload(pipeline_schedule, PipelineType.PYTHON, dict())[0]
617
- pipeline_run = PipelineRun.create(**payload)
618
- execution_date_str = datetime.utcnow().strftime(format='%Y%m%dT%H%M%S_%f')
619
- self.assertEqual(
620
- pipeline_run.execution_partition,
621
- f'{pipeline_run.pipeline_schedule_id}/{execution_date_str}',
622
- )
623
-
624
- def test_log_file(self):
625
- execution_date = datetime.now()
626
- pipeline_run = create_pipeline_run_with_schedule(
627
- pipeline_uuid='test_pipeline',
628
- execution_date=execution_date,
629
- )
630
- execution_date_str = execution_date.strftime(format='%Y%m%dT%H%M%S')
631
- expected_file_path = os.path.join(
632
- get_repo_config(self.repo_path).variables_dir,
633
- 'pipelines/test_pipeline/.logs',
634
- f'{pipeline_run.pipeline_schedule_id}/{execution_date_str}/pipeline.log',
635
- )
636
- self.assertEqual(pipeline_run.logs.get('path'), expected_file_path)
637
-
638
- def test_active_runs_for_pipelines(self):
639
- create_pipeline_with_blocks(
640
- 'test active run 1',
641
- self.repo_path,
642
- )
643
- create_pipeline_with_blocks(
644
- 'test active run 2',
645
- self.repo_path,
646
- )
647
- pipeline_run = create_pipeline_run_with_schedule(
648
- pipeline_uuid='test_active_run_1',
649
- )
650
- pipeline_run.update(status=PipelineRun.PipelineRunStatus.RUNNING)
651
- pipeline_schedule = pipeline_run.pipeline_schedule
652
- pipeline_run2 = create_pipeline_run_with_schedule(
653
- pipeline_uuid='test_active_run_1',
654
- pipeline_schedule_id=pipeline_schedule.id,
655
- )
656
- pipeline_run2.update(status=PipelineRun.PipelineRunStatus.RUNNING)
657
- create_pipeline_run_with_schedule(
658
- pipeline_uuid='test_active_run_1',
659
- pipeline_schedule_id=pipeline_schedule.id,
660
- )
661
- pipeline_run4 = create_pipeline_run_with_schedule(
662
- pipeline_uuid='test_active_run_2',
663
- )
664
- pipeline_run4.update(status=PipelineRun.PipelineRunStatus.RUNNING)
665
- results1 = PipelineRun.active_runs_for_pipelines(
666
- pipeline_uuids=['test_active_run_1'],
667
- include_block_runs=True,
668
- )
669
- results2 = PipelineRun.active_runs_for_pipelines(
670
- pipeline_uuids=['test_active_run_2'],
671
- include_block_runs=True,
672
- )
673
- results3 = PipelineRun.active_runs_for_pipelines(
674
- pipeline_uuids=['test_active_run_1', 'test_active_run_2'],
675
- include_block_runs=True,
676
- )
677
- self.assertEqual(
678
- set([r.id for r in results1]),
679
- set([pipeline_run.id, pipeline_run2.id]),
680
- )
681
- self.assertEqual(set([r.id for r in results2]), set([pipeline_run4.id]))
682
- self.assertEqual(
683
- set([r.id for r in results3]),
684
- set([pipeline_run.id, pipeline_run2.id, pipeline_run4.id]),
685
- )
686
-
687
-
688
- class BlockRunTests(DBTestCase):
689
- @classmethod
690
- def setUpClass(self):
691
- super().setUpClass()
692
- self.pipeline = create_pipeline_with_blocks(
693
- 'test pipeline',
694
- self.repo_path,
695
- )
696
-
697
- def test_log_file(self):
698
- execution_date = datetime.now()
699
- pipeline_run = create_pipeline_run_with_schedule(
700
- pipeline_uuid='test_pipeline',
701
- execution_date=execution_date,
702
- )
703
- execution_date_str = execution_date.strftime(format='%Y%m%dT%H%M%S')
704
- for b in pipeline_run.block_runs:
705
- expected_file_path = os.path.join(
706
- get_repo_config(self.repo_path).variables_dir,
707
- 'pipelines/test_pipeline/.logs',
708
- f'{pipeline_run.pipeline_schedule_id}/{execution_date_str}/{b.block_uuid}.log',
709
- )
710
- self.assertEqual(b.logs.get('path'), expected_file_path)