mage-ai 0.9.76__py3-none-any.whl → 0.9.77__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.
- mage_ai/api/resources/GitFileResource.py +8 -0
- mage_ai/cli/main.py +6 -1
- mage_ai/data_preparation/executors/block_executor.py +8 -3
- mage_ai/data_preparation/executors/pipeline_executor.py +35 -19
- mage_ai/data_preparation/models/block/__init__.py +29 -22
- mage_ai/data_preparation/models/block/outputs.py +1 -1
- mage_ai/data_preparation/models/constants.py +2 -0
- mage_ai/data_preparation/storage/local_storage.py +4 -1
- mage_ai/io/config.py +1 -0
- mage_ai/io/mssql.py +16 -9
- mage_ai/io/postgres.py +3 -0
- mage_ai/orchestration/db/migrations/versions/39d36f1dab73_create_genericjob.py +47 -0
- mage_ai/orchestration/db/models/oauth.py +2 -1
- mage_ai/orchestration/db/models/schedules.py +105 -0
- mage_ai/orchestration/job_manager.py +19 -0
- mage_ai/orchestration/notification/sender.py +2 -2
- mage_ai/orchestration/pipeline_scheduler_original.py +146 -1
- mage_ai/orchestration/queue/config.py +11 -1
- mage_ai/orchestration/queue/process_queue.py +2 -0
- mage_ai/server/api/base.py +41 -0
- mage_ai/server/api/constants.py +1 -0
- mage_ai/server/constants.py +1 -1
- mage_ai/server/scheduler_manager.py +2 -0
- mage_ai/server/terminal_server.py +3 -0
- mage_ai/settings/server.py +2 -0
- mage_ai/streaming/sources/kafka.py +2 -1
- mage_ai/tests/data_preparation/executors/test_block_executor.py +3 -3
- mage_ai/tests/data_preparation/models/test_variable.py +2 -0
- mage_ai/tests/io/create_table/test_postgresql.py +3 -2
- mage_ai/tests/orchestration/notification/test_sender.py +5 -1
- mage_ai/tests/streaming/sources/test_kafka.py +2 -2
- {mage_ai-0.9.76.dist-info → mage_ai-0.9.77.dist-info}/METADATA +70 -107
- {mage_ai-0.9.76.dist-info → mage_ai-0.9.77.dist-info}/RECORD +37 -36
- {mage_ai-0.9.76.dist-info → mage_ai-0.9.77.dist-info}/WHEEL +1 -1
- {mage_ai-0.9.76.dist-info → mage_ai-0.9.77.dist-info}/entry_points.txt +0 -0
- {mage_ai-0.9.76.dist-info → mage_ai-0.9.77.dist-info}/licenses/LICENSE +0 -0
- {mage_ai-0.9.76.dist-info → mage_ai-0.9.77.dist-info}/top_level.txt +0 -0
|
@@ -32,6 +32,14 @@ class GitFileResource(GenericResource):
|
|
|
32
32
|
pass
|
|
33
33
|
|
|
34
34
|
file_path_absolute = os.path.join(git_manager.repo_path, file_path)
|
|
35
|
+
|
|
36
|
+
# Prevent path traversal by resolving the absolute path location
|
|
37
|
+
# and checking if it's within the repo
|
|
38
|
+
if not os.path.abspath(file_path_absolute).startswith(
|
|
39
|
+
os.path.abspath(git_manager.repo_path)
|
|
40
|
+
):
|
|
41
|
+
raise Exception("Access denied: Attempted path traversal")
|
|
42
|
+
|
|
35
43
|
file = File.from_path(file_path_absolute)
|
|
36
44
|
if not file.exists():
|
|
37
45
|
file = File.from_path(file_path_absolute, '')
|
mage_ai/cli/main.py
CHANGED
|
@@ -194,7 +194,11 @@ def run(
|
|
|
194
194
|
from mage_ai.orchestration.db.models.schedules import PipelineRun
|
|
195
195
|
from mage_ai.orchestration.utils.git import log_git_sync, run_git_sync
|
|
196
196
|
from mage_ai.server.logger import Logger
|
|
197
|
-
from mage_ai.settings import
|
|
197
|
+
from mage_ai.settings import (
|
|
198
|
+
SENTRY_DSN,
|
|
199
|
+
SENTRY_SERVER_NAME,
|
|
200
|
+
SENTRY_TRACES_SAMPLE_RATE,
|
|
201
|
+
)
|
|
198
202
|
from mage_ai.shared.hash import merge_dict
|
|
199
203
|
|
|
200
204
|
logger = Logger().new_server_logger(__name__)
|
|
@@ -204,6 +208,7 @@ def run(
|
|
|
204
208
|
sentry_sdk.init(
|
|
205
209
|
sentry_dsn,
|
|
206
210
|
traces_sample_rate=SENTRY_TRACES_SAMPLE_RATE,
|
|
211
|
+
server_name=SENTRY_SERVER_NAME,
|
|
207
212
|
)
|
|
208
213
|
(enable_new_relic, application) = initialize_new_relic()
|
|
209
214
|
|
|
@@ -693,7 +693,7 @@ class BlockExecutor:
|
|
|
693
693
|
),
|
|
694
694
|
tags=tags,
|
|
695
695
|
)
|
|
696
|
-
self.
|
|
696
|
+
self.execute_callback(
|
|
697
697
|
'on_failure',
|
|
698
698
|
block_run_id=block_run_id,
|
|
699
699
|
callback_kwargs=dict(__error=error, retry=self.retry_metadata),
|
|
@@ -748,7 +748,7 @@ class BlockExecutor:
|
|
|
748
748
|
# success callback because this isn’t the last data integration block that needs
|
|
749
749
|
# to run.
|
|
750
750
|
if not data_integration_metadata or is_original_block:
|
|
751
|
-
self.
|
|
751
|
+
self.execute_callback(
|
|
752
752
|
'on_success',
|
|
753
753
|
block_run_id=block_run_id,
|
|
754
754
|
callback_kwargs=dict(retry=self.retry_metadata),
|
|
@@ -1253,7 +1253,7 @@ class BlockExecutor:
|
|
|
1253
1253
|
|
|
1254
1254
|
return result
|
|
1255
1255
|
|
|
1256
|
-
def
|
|
1256
|
+
def execute_callback(
|
|
1257
1257
|
self,
|
|
1258
1258
|
callback: str,
|
|
1259
1259
|
global_vars: Dict,
|
|
@@ -1275,6 +1275,11 @@ class BlockExecutor:
|
|
|
1275
1275
|
dynamic_block_index: Index of the dynamic block.
|
|
1276
1276
|
dynamic_upstream_block_uuids: List of UUIDs of the dynamic upstream blocks.
|
|
1277
1277
|
"""
|
|
1278
|
+
if logging_tags is None:
|
|
1279
|
+
logging_tags = self.build_tags(
|
|
1280
|
+
block_run_id=block_run_id,
|
|
1281
|
+
pipeline_run_id=pipeline_run.id if pipeline_run is not None else None,
|
|
1282
|
+
)
|
|
1278
1283
|
upstream_block_uuids_override = []
|
|
1279
1284
|
if is_dynamic_block_child(self.block):
|
|
1280
1285
|
if not self.block_run and block_run_id:
|
|
@@ -10,10 +10,13 @@ from mage_ai.data_preparation.logging.logger import DictLogger
|
|
|
10
10
|
from mage_ai.data_preparation.logging.logger_manager_factory import LoggerManagerFactory
|
|
11
11
|
from mage_ai.data_preparation.models.pipeline import Pipeline
|
|
12
12
|
from mage_ai.orchestration.db.models.schedules import BlockRun, PipelineRun
|
|
13
|
+
from mage_ai.server.logger import Logger
|
|
13
14
|
from mage_ai.shared.hash import merge_dict
|
|
14
15
|
from mage_ai.usage_statistics.constants import EventNameType, EventObjectType
|
|
15
16
|
from mage_ai.usage_statistics.logger import UsageStatisticLogger
|
|
16
17
|
|
|
18
|
+
logger = Logger().new_server_logger(__name__)
|
|
19
|
+
|
|
17
20
|
|
|
18
21
|
class PipelineExecutor:
|
|
19
22
|
def __init__(self, pipeline: Pipeline, execution_partition: str = None):
|
|
@@ -53,25 +56,38 @@ class PipelineExecutor:
|
|
|
53
56
|
update_status (bool): Whether to update the execution status.
|
|
54
57
|
**kwargs: Additional keyword arguments.
|
|
55
58
|
"""
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
59
|
+
# Create the async task to execute
|
|
60
|
+
async def _execute_task():
|
|
61
|
+
if pipeline_run_id is None:
|
|
62
|
+
# Execute the pipeline without block runs
|
|
63
|
+
await self.pipeline.execute(
|
|
64
|
+
analyze_outputs=analyze_outputs,
|
|
65
|
+
global_vars=global_vars,
|
|
66
|
+
run_sensors=run_sensors,
|
|
67
|
+
run_tests=run_tests,
|
|
68
|
+
update_status=update_status,
|
|
69
|
+
)
|
|
70
|
+
else:
|
|
71
|
+
# Supported pipeline types: Standard batch pipeline
|
|
72
|
+
pipeline_run = PipelineRun.query.get(pipeline_run_id)
|
|
73
|
+
if pipeline_run.status != PipelineRun.PipelineRunStatus.RUNNING:
|
|
74
|
+
return
|
|
75
|
+
await self.__run_blocks(
|
|
76
|
+
pipeline_run,
|
|
77
|
+
allow_blocks_to_fail=allow_blocks_to_fail,
|
|
78
|
+
global_vars=global_vars,
|
|
79
|
+
)
|
|
80
|
+
# Execute the task based on current context
|
|
81
|
+
try:
|
|
82
|
+
loop = asyncio.get_running_loop()
|
|
83
|
+
logger.info(f'[PipelineExecutor] Found running loop {loop}')
|
|
84
|
+
# We're in an async context, use create_task
|
|
85
|
+
task = asyncio.create_task(_execute_task())
|
|
86
|
+
loop.run_until_complete(task)
|
|
87
|
+
except RuntimeError:
|
|
88
|
+
# No running loop, safe to use asyncio.run
|
|
89
|
+
logger.info('[PipelineExecutor] No running loop, using asyncio.run')
|
|
90
|
+
asyncio.run(_execute_task())
|
|
75
91
|
|
|
76
92
|
self.logger_manager.output_logs_to_destination()
|
|
77
93
|
|
|
@@ -4464,29 +4464,36 @@ class CallbackBlock(AddonBlock):
|
|
|
4464
4464
|
elif 'on_success' == callback:
|
|
4465
4465
|
callback_functions_legacy = success_functions
|
|
4466
4466
|
callback_status = CallbackStatus.SUCCESS
|
|
4467
|
+
elif 'on_cancelled' == callback:
|
|
4468
|
+
callback_functions_legacy = []
|
|
4469
|
+
callback_status = CallbackStatus.CANCELLED
|
|
4467
4470
|
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4471
|
+
if callback_status in [CallbackStatus.FAILURE, CallbackStatus.SUCCESS]:
|
|
4472
|
+
# Fetch input variables
|
|
4473
|
+
input_vars, kwargs_vars, upstream_block_uuids = self.fetch_input_variables(
|
|
4474
|
+
None,
|
|
4475
|
+
dynamic_block_index=dynamic_block_index,
|
|
4476
|
+
dynamic_block_indexes=dynamic_block_indexes,
|
|
4477
|
+
dynamic_upstream_block_uuids=dynamic_upstream_block_uuids,
|
|
4478
|
+
execution_partition=execution_partition,
|
|
4479
|
+
from_notebook=from_notebook,
|
|
4480
|
+
global_vars=global_vars,
|
|
4481
|
+
metadata=metadata,
|
|
4482
|
+
upstream_block_uuids=[parent_block.uuid] if parent_block else None,
|
|
4483
|
+
upstream_block_uuids_override=upstream_block_uuids_override,
|
|
4484
|
+
)
|
|
4485
|
+
# Copied logic from the method self.execute_block
|
|
4486
|
+
outputs_from_input_vars = {}
|
|
4487
|
+
upstream_block_uuids_length = len(upstream_block_uuids)
|
|
4488
|
+
for idx, input_var in enumerate(input_vars):
|
|
4489
|
+
if idx < upstream_block_uuids_length:
|
|
4490
|
+
upstream_block_uuid = upstream_block_uuids[idx]
|
|
4491
|
+
outputs_from_input_vars[upstream_block_uuid] = input_var
|
|
4492
|
+
outputs_from_input_vars[f'df_{idx + 1}'] = input_var
|
|
4493
|
+
else:
|
|
4494
|
+
input_vars = []
|
|
4495
|
+
kwargs_vars = []
|
|
4496
|
+
upstream_block_uuids = []
|
|
4490
4497
|
|
|
4491
4498
|
global_vars_copy = global_vars.copy()
|
|
4492
4499
|
for kwargs_var in kwargs_vars:
|
|
@@ -343,7 +343,7 @@ def format_output_data(
|
|
|
343
343
|
columns=columns_to_display,
|
|
344
344
|
rows=[
|
|
345
345
|
list(row.values())
|
|
346
|
-
for row in json.loads(data[columns_to_display].
|
|
346
|
+
for row in json.loads(json.dumps(data[columns_to_display].to_dicts()))
|
|
347
347
|
],
|
|
348
348
|
),
|
|
349
349
|
resource_usage=resource_usage,
|
|
@@ -82,6 +82,7 @@ class BlockColor(StrEnum):
|
|
|
82
82
|
|
|
83
83
|
|
|
84
84
|
class CallbackStatus(StrEnum):
|
|
85
|
+
CANCELLED = 'cancelled'
|
|
85
86
|
FAILURE = 'failure'
|
|
86
87
|
SUCCESS = 'success'
|
|
87
88
|
|
|
@@ -126,6 +127,7 @@ BLOCK_LANGUAGE_TO_FILE_EXTENSION = {
|
|
|
126
127
|
|
|
127
128
|
|
|
128
129
|
CALLBACK_STATUSES = [
|
|
130
|
+
CallbackStatus.CANCELLED,
|
|
129
131
|
CallbackStatus.FAILURE,
|
|
130
132
|
CallbackStatus.SUCCESS,
|
|
131
133
|
]
|
|
@@ -121,7 +121,10 @@ class LocalStorage(BaseStorage):
|
|
|
121
121
|
return pd.read_parquet(file_path, engine='pyarrow')
|
|
122
122
|
|
|
123
123
|
def read_polars_parquet(self, file_path: str, **kwargs) -> pl.DataFrame:
|
|
124
|
-
|
|
124
|
+
try:
|
|
125
|
+
return pl.read_parquet(file_path, use_pyarrow=True)
|
|
126
|
+
except Exception:
|
|
127
|
+
return pl.read_parquet(file_path)
|
|
125
128
|
|
|
126
129
|
def write_csv(self, df: pd.DataFrame, file_path: str) -> None:
|
|
127
130
|
File.create_parent_directories(file_path)
|
mage_ai/io/config.py
CHANGED
mage_ai/io/mssql.py
CHANGED
|
@@ -38,12 +38,14 @@ class MSSQL(BaseSQL):
|
|
|
38
38
|
host: str,
|
|
39
39
|
password: str,
|
|
40
40
|
user: str,
|
|
41
|
+
authentication: str = None,
|
|
41
42
|
schema: str = None,
|
|
42
43
|
port: int = 1433,
|
|
43
44
|
verbose: bool = True,
|
|
44
45
|
**kwargs,
|
|
45
46
|
) -> None:
|
|
46
47
|
super().__init__(
|
|
48
|
+
authentication=authentication,
|
|
47
49
|
database=database,
|
|
48
50
|
server=host,
|
|
49
51
|
user=user,
|
|
@@ -61,15 +63,19 @@ class MSSQL(BaseSQL):
|
|
|
61
63
|
database = self.settings['database']
|
|
62
64
|
username = self.settings['user']
|
|
63
65
|
password = self.settings['password']
|
|
64
|
-
|
|
65
|
-
f'DRIVER={{{driver}}}
|
|
66
|
-
f'SERVER={server}
|
|
67
|
-
f'DATABASE={database}
|
|
68
|
-
f'UID={username}
|
|
69
|
-
f'PWD={password}
|
|
70
|
-
'ENCRYPT=yes
|
|
71
|
-
'TrustServerCertificate=yes
|
|
72
|
-
|
|
66
|
+
conn_opts = [
|
|
67
|
+
f'DRIVER={{{driver}}}',
|
|
68
|
+
f'SERVER={server}',
|
|
69
|
+
f'DATABASE={database}',
|
|
70
|
+
f'UID={username}',
|
|
71
|
+
f'PWD={password}',
|
|
72
|
+
'ENCRYPT=yes',
|
|
73
|
+
'TrustServerCertificate=yes',
|
|
74
|
+
]
|
|
75
|
+
if self.settings.get('authentication'):
|
|
76
|
+
authentication = self.settings.get('authentication')
|
|
77
|
+
conn_opts.append(f'Authentication={authentication}')
|
|
78
|
+
return ';'.join(conn_opts)
|
|
73
79
|
|
|
74
80
|
def default_schema(self) -> str:
|
|
75
81
|
return self.settings.get('schema') or 'dbo'
|
|
@@ -318,6 +324,7 @@ class MSSQL(BaseSQL):
|
|
|
318
324
|
@classmethod
|
|
319
325
|
def with_config(cls, config: BaseConfigLoader) -> 'MSSQL':
|
|
320
326
|
return cls(
|
|
327
|
+
authentication=config[ConfigKey.MSSQL_AUTHENTICATION],
|
|
321
328
|
database=config[ConfigKey.MSSQL_DATABASE],
|
|
322
329
|
schema=config[ConfigKey.MSSQL_SCHEMA],
|
|
323
330
|
driver=config[ConfigKey.MSSQL_DRIVER],
|
mage_ai/io/postgres.py
CHANGED
|
@@ -329,18 +329,21 @@ class Postgres(BaseSQL):
|
|
|
329
329
|
return simplejson.dumps(
|
|
330
330
|
val,
|
|
331
331
|
default=encode_complex,
|
|
332
|
+
ensure_ascii=False,
|
|
332
333
|
ignore_nan=True,
|
|
333
334
|
)
|
|
334
335
|
elif type(val) is list and len(val) >= 1 and type(val[0]) is dict:
|
|
335
336
|
return simplejson.dumps(
|
|
336
337
|
val,
|
|
337
338
|
default=encode_complex,
|
|
339
|
+
ensure_ascii=False,
|
|
338
340
|
ignore_nan=True,
|
|
339
341
|
)
|
|
340
342
|
elif not use_insert_command and type(val) is list:
|
|
341
343
|
return self._clean_array_value(simplejson.dumps(
|
|
342
344
|
val,
|
|
343
345
|
default=encode_complex,
|
|
346
|
+
ensure_ascii=False,
|
|
344
347
|
ignore_nan=True,
|
|
345
348
|
))
|
|
346
349
|
return val
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Create GenericJob
|
|
2
|
+
|
|
3
|
+
Revision ID: 39d36f1dab73
|
|
4
|
+
Revises: 0227396a216c
|
|
5
|
+
Create Date: 2025-08-27 20:45:29.756869
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
from alembic import op
|
|
9
|
+
import sqlalchemy as sa
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# revision identifiers, used by Alembic.
|
|
13
|
+
revision = '39d36f1dab73'
|
|
14
|
+
down_revision = '0227396a216c'
|
|
15
|
+
branch_labels = None
|
|
16
|
+
depends_on = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def upgrade() -> None:
|
|
20
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
21
|
+
op.create_table('generic_job',
|
|
22
|
+
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
|
23
|
+
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
|
|
24
|
+
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
|
|
25
|
+
sa.Column('job_type', sa.Enum('CANCEL_PIPELINE_RUN', name='jobtype'), nullable=False),
|
|
26
|
+
sa.Column('status', sa.Enum('INITIAL', 'QUEUED', 'RUNNING', 'COMPLETED', 'FAILED', name='jobstatus'), nullable=True),
|
|
27
|
+
sa.Column('payload', sa.JSON(), nullable=True),
|
|
28
|
+
sa.Column('extra_metadata', sa.JSON(), nullable=True),
|
|
29
|
+
sa.Column('started_at', sa.DateTime(timezone=True), nullable=True),
|
|
30
|
+
sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True),
|
|
31
|
+
sa.Column('repo_path', sa.String(length=255), nullable=True),
|
|
32
|
+
sa.PrimaryKeyConstraint('id')
|
|
33
|
+
)
|
|
34
|
+
with op.batch_alter_table('generic_job', schema=None) as batch_op:
|
|
35
|
+
batch_op.create_index(batch_op.f('ix_generic_job_job_type'), ['job_type'], unique=False)
|
|
36
|
+
batch_op.create_index(batch_op.f('ix_generic_job_status'), ['status'], unique=False)
|
|
37
|
+
# ### end Alembic commands ###
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def downgrade() -> None:
|
|
41
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
42
|
+
with op.batch_alter_table('generic_job', schema=None) as batch_op:
|
|
43
|
+
batch_op.drop_index(batch_op.f('ix_generic_job_status'))
|
|
44
|
+
batch_op.drop_index(batch_op.f('ix_generic_job_job_type'))
|
|
45
|
+
|
|
46
|
+
op.drop_table('generic_job')
|
|
47
|
+
# ### end Alembic commands ###
|
|
@@ -882,4 +882,5 @@ class Oauth2AccessToken(BaseModel):
|
|
|
882
882
|
def is_valid(self) -> bool:
|
|
883
883
|
return self.token and \
|
|
884
884
|
self.expires and \
|
|
885
|
-
self.expires >= datetime.utcnow().replace(tzinfo=self.expires.tzinfo)
|
|
885
|
+
self.expires >= datetime.utcnow().replace(tzinfo=self.expires.tzinfo) and \
|
|
886
|
+
self.user is not None
|
|
@@ -1804,6 +1804,111 @@ class BlockRun(BlockRunProjectPlatformMixin, BaseModel):
|
|
|
1804
1804
|
)
|
|
1805
1805
|
|
|
1806
1806
|
|
|
1807
|
+
class GenericJob(BaseModel):
|
|
1808
|
+
class JobStatus(StrEnum):
|
|
1809
|
+
INITIAL = 'initial'
|
|
1810
|
+
QUEUED = 'queued'
|
|
1811
|
+
RUNNING = 'running'
|
|
1812
|
+
COMPLETED = 'completed'
|
|
1813
|
+
FAILED = 'failed'
|
|
1814
|
+
|
|
1815
|
+
class JobType(StrEnum):
|
|
1816
|
+
CANCEL_PIPELINE_RUN = 'cancel_pipeline_run'
|
|
1817
|
+
|
|
1818
|
+
job_type = Column(Enum(JobType), nullable=False, index=True)
|
|
1819
|
+
status = Column(Enum(JobStatus), default=JobStatus.INITIAL, index=True)
|
|
1820
|
+
payload = Column(JSON) # Job-specific data (e.g., pipeline_run_id)
|
|
1821
|
+
extra_metadata = Column(JSON) # Additional metadata for the job
|
|
1822
|
+
started_at = Column(DateTime(timezone=True))
|
|
1823
|
+
completed_at = Column(DateTime(timezone=True))
|
|
1824
|
+
repo_path = Column(String(255))
|
|
1825
|
+
|
|
1826
|
+
@classproperty
|
|
1827
|
+
def repo_query(cls):
|
|
1828
|
+
return cls.query.filter(
|
|
1829
|
+
or_(
|
|
1830
|
+
GenericJob.repo_path == get_repo_path(),
|
|
1831
|
+
GenericJob.repo_path.is_(None),
|
|
1832
|
+
)
|
|
1833
|
+
)
|
|
1834
|
+
|
|
1835
|
+
@classmethod
|
|
1836
|
+
@safe_db_query
|
|
1837
|
+
def enqueue_cancel_pipeline_run(
|
|
1838
|
+
cls,
|
|
1839
|
+
pipeline_run_id: int,
|
|
1840
|
+
cancelled_block_run_ids: List,
|
|
1841
|
+
repo_path: str = None,
|
|
1842
|
+
) -> 'GenericJob':
|
|
1843
|
+
"""Enqueue a job to cancel a pipeline run."""
|
|
1844
|
+
if repo_path is None:
|
|
1845
|
+
repo_path = get_repo_path()
|
|
1846
|
+
|
|
1847
|
+
job = cls.create(
|
|
1848
|
+
job_type=cls.JobType.CANCEL_PIPELINE_RUN,
|
|
1849
|
+
payload=dict(
|
|
1850
|
+
pipeline_run_id=pipeline_run_id,
|
|
1851
|
+
cancelled_block_run_ids=cancelled_block_run_ids,
|
|
1852
|
+
),
|
|
1853
|
+
repo_path=repo_path,
|
|
1854
|
+
status=cls.JobStatus.INITIAL
|
|
1855
|
+
)
|
|
1856
|
+
return job
|
|
1857
|
+
|
|
1858
|
+
@classmethod
|
|
1859
|
+
@safe_db_query
|
|
1860
|
+
def get_jobs_with_initial_status(
|
|
1861
|
+
cls,
|
|
1862
|
+
limit: int = 10,
|
|
1863
|
+
repo_path: str = None,
|
|
1864
|
+
) -> List['GenericJob']:
|
|
1865
|
+
"""Get up to N jobs with INITIAL status for processing."""
|
|
1866
|
+
query = cls.repo_query.filter(
|
|
1867
|
+
cls.status == cls.JobStatus.INITIAL,
|
|
1868
|
+
)
|
|
1869
|
+
|
|
1870
|
+
if repo_path:
|
|
1871
|
+
query = query.filter(cls.repo_path == repo_path)
|
|
1872
|
+
|
|
1873
|
+
jobs = query.order_by(cls.created_at.asc()).limit(limit).all()
|
|
1874
|
+
|
|
1875
|
+
return jobs
|
|
1876
|
+
|
|
1877
|
+
@safe_db_query
|
|
1878
|
+
def mark_completed(self):
|
|
1879
|
+
"""Mark the job as completed successfully."""
|
|
1880
|
+
self.update(
|
|
1881
|
+
status=self.JobStatus.COMPLETED,
|
|
1882
|
+
completed_at=datetime.now(tz=pytz.UTC)
|
|
1883
|
+
)
|
|
1884
|
+
|
|
1885
|
+
@safe_db_query
|
|
1886
|
+
def mark_failed(self, metadata: dict = None):
|
|
1887
|
+
"""Mark the job as failed."""
|
|
1888
|
+
self.update(
|
|
1889
|
+
status=self.JobStatus.FAILED,
|
|
1890
|
+
completed_at=datetime.now(tz=pytz.UTC),
|
|
1891
|
+
extra_metadata=metadata or {}
|
|
1892
|
+
)
|
|
1893
|
+
|
|
1894
|
+
@safe_db_query
|
|
1895
|
+
def mark_queued(self):
|
|
1896
|
+
"""Mark the job as queued."""
|
|
1897
|
+
self.update(
|
|
1898
|
+
status=self.JobStatus.QUEUED,
|
|
1899
|
+
)
|
|
1900
|
+
|
|
1901
|
+
@safe_db_query
|
|
1902
|
+
def mark_running(self):
|
|
1903
|
+
"""Mark the job as queued."""
|
|
1904
|
+
self.update(
|
|
1905
|
+
status=self.JobStatus.RUNNING,
|
|
1906
|
+
)
|
|
1907
|
+
|
|
1908
|
+
def __repr__(self):
|
|
1909
|
+
return f'GenericJob(id={self.id}, type={self.job_type}, status={self.status})'
|
|
1910
|
+
|
|
1911
|
+
|
|
1807
1912
|
class EventMatcher(BaseModel):
|
|
1808
1913
|
class EventType(StrEnum):
|
|
1809
1914
|
AWS_EVENT = 'aws_event'
|
|
@@ -8,6 +8,7 @@ class JobType(StrEnum):
|
|
|
8
8
|
BLOCK_RUN = 'block_run'
|
|
9
9
|
PIPELINE_RUN = 'pipeline_run'
|
|
10
10
|
INTEGRATION_STREAM = 'integration_stream'
|
|
11
|
+
GENERIC_JOB = 'generic_job'
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
class JobManager:
|
|
@@ -62,6 +63,19 @@ class JobManager:
|
|
|
62
63
|
logging_tags=logging_tags,
|
|
63
64
|
)
|
|
64
65
|
|
|
66
|
+
def has_generic_job(
|
|
67
|
+
self,
|
|
68
|
+
generic_job_id: int,
|
|
69
|
+
logger=None,
|
|
70
|
+
logging_tags: Dict = None,
|
|
71
|
+
) -> bool:
|
|
72
|
+
job_id = self.__job_id(JobType.GENERIC_JOB, generic_job_id)
|
|
73
|
+
return self.queue.has_job(
|
|
74
|
+
job_id,
|
|
75
|
+
logger=logger,
|
|
76
|
+
logging_tags=logging_tags,
|
|
77
|
+
)
|
|
78
|
+
|
|
65
79
|
def kill_block_run_job(self, block_run_id):
|
|
66
80
|
print(f'Kill block run id: {block_run_id}')
|
|
67
81
|
job_id = self.__job_id(JobType.BLOCK_RUN, block_run_id)
|
|
@@ -78,6 +92,11 @@ class JobManager:
|
|
|
78
92
|
job_id = self.__job_id(JobType.INTEGRATION_STREAM, id)
|
|
79
93
|
return self.queue.kill_job(job_id)
|
|
80
94
|
|
|
95
|
+
def kill_generic_job(self, generic_job_id):
|
|
96
|
+
print(f'Kill generic job id: {generic_job_id}')
|
|
97
|
+
job_id = self.__job_id(JobType.GENERIC_JOB, generic_job_id)
|
|
98
|
+
return self.queue.kill_job(job_id)
|
|
99
|
+
|
|
81
100
|
def start(self):
|
|
82
101
|
self.queue.start()
|
|
83
102
|
|
|
@@ -69,7 +69,7 @@ class NotificationSender:
|
|
|
69
69
|
|
|
70
70
|
if self.config.teams_config is not None and self.config.teams_config.is_valid:
|
|
71
71
|
try:
|
|
72
|
-
send_teams_message(self.config.teams_config, summary)
|
|
72
|
+
send_teams_message(self.config.teams_config, summary, title)
|
|
73
73
|
except Exception:
|
|
74
74
|
traceback.print_exc()
|
|
75
75
|
|
|
@@ -87,7 +87,7 @@ class NotificationSender:
|
|
|
87
87
|
|
|
88
88
|
if self.config.google_chat_config is not None and self.config.google_chat_config.is_valid:
|
|
89
89
|
try:
|
|
90
|
-
send_google_chat_message(self.config.google_chat_config, summary)
|
|
90
|
+
send_google_chat_message(self.config.google_chat_config, summary, title)
|
|
91
91
|
except Exception:
|
|
92
92
|
traceback.print_exc()
|
|
93
93
|
|