mage-ai 0.9.65__py3-none-any.whl → 0.9.67__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/monitors/BaseMonitor.py +38 -16
- mage_ai/api/policies/BackfillPolicy.py +1 -0
- mage_ai/api/resources/OauthResource.py +13 -5
- mage_ai/api/resources/SessionResource.py +6 -4
- mage_ai/authentication/ldap.py +19 -9
- mage_ai/authentication/oauth/constants.py +8 -14
- mage_ai/authentication/oauth/utils.py +18 -6
- mage_ai/authentication/providers/active_directory.py +21 -16
- mage_ai/authentication/providers/azure_devops.py +18 -0
- mage_ai/authentication/providers/bitbucket.py +10 -9
- mage_ai/authentication/providers/constants.py +2 -0
- mage_ai/authentication/providers/ghe.py +5 -9
- mage_ai/authentication/providers/gitlab.py +6 -9
- mage_ai/authentication/providers/google.py +9 -6
- mage_ai/authentication/providers/oidc.py +6 -4
- mage_ai/authentication/providers/okta.py +9 -6
- mage_ai/cluster_manager/kubernetes/workload_manager.py +10 -0
- mage_ai/cluster_manager/workspace/base.py +6 -1
- mage_ai/cluster_manager/workspace/kubernetes.py +3 -0
- mage_ai/data_preparation/decorators.py +15 -0
- mage_ai/data_preparation/executors/streaming_pipeline_executor.py +22 -12
- mage_ai/data_preparation/git/__init__.py +10 -1
- mage_ai/data_preparation/git/api.py +3 -0
- mage_ai/data_preparation/git/clients/azure_devops.py +106 -0
- mage_ai/data_preparation/git/clients/base.py +6 -0
- mage_ai/data_preparation/git/clients/gitlab.py +3 -2
- mage_ai/data_preparation/git/utils.py +31 -29
- mage_ai/data_preparation/models/block/__init__.py +27 -18
- mage_ai/data_preparation/models/block/dbt/block_sql.py +164 -0
- mage_ai/data_preparation/models/block/dynamic/variables.py +1 -2
- mage_ai/data_preparation/models/pipeline.py +3 -3
- mage_ai/data_preparation/models/triggers/__init__.py +6 -1
- mage_ai/data_preparation/preferences.py +42 -37
- mage_ai/data_preparation/repo_manager.py +21 -0
- mage_ai/data_preparation/storage/gcs_storage.py +27 -2
- mage_ai/data_preparation/storage/local_storage.py +18 -3
- mage_ai/data_preparation/storage/s3_storage.py +7 -2
- mage_ai/data_preparation/templates/data_loaders/streaming/generic_python.py +23 -0
- mage_ai/data_preparation/templates/main/metadata.yaml +6 -0
- mage_ai/data_preparation/templates/template.py +6 -2
- mage_ai/data_preparation/variable_manager.py +2 -1
- mage_ai/io/base.py +3 -0
- mage_ai/io/bigquery.py +2 -0
- mage_ai/io/export_utils.py +14 -9
- mage_ai/io/mssql.py +104 -25
- mage_ai/io/mysql.py +10 -9
- mage_ai/io/oracledb.py +14 -2
- mage_ai/io/postgres.py +3 -0
- mage_ai/io/sql.py +14 -6
- mage_ai/io/trino.py +10 -8
- mage_ai/orchestration/db/migrations/versions/90d978a8aef8_update_unique_constraint_for_secret.py +11 -5
- mage_ai/orchestration/db/models/schedules.py +25 -1
- mage_ai/orchestration/db/models/schedules_project_platform.py +24 -1
- mage_ai/orchestration/job_manager.py +6 -1
- mage_ai/orchestration/pipeline_scheduler_original.py +16 -10
- mage_ai/server/constants.py +1 -1
- mage_ai/server/file_observer.py +10 -0
- mage_ai/server/frontend_dist/404.html +2 -2
- mage_ai/server/frontend_dist/_next/static/chunks/{1557-a754b04510d50b80.js → 1557-01f0843dc6ac4971.js} +1 -1
- mage_ai/server/frontend_dist/_next/static/chunks/9440-4069842b90d4b801.js +1 -0
- mage_ai/server/frontend_dist/_next/static/chunks/pages/_app-1c1ffd928f5a00f7.js +1 -0
- mage_ai/server/frontend_dist/_next/static/chunks/pages/index-b7b8695a7f9efde2.js +1 -0
- mage_ai/server/frontend_dist/_next/static/chunks/pages/oauth-30e34ee15d410331.js +1 -0
- mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/backfills/[...slug]-ff4bd7a8ec3bab40.js +1 -0
- mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/backfills-aaf393c86fc1bda3.js +1 -0
- mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/{edit-8d32ac7e8f023779.js → edit-36377e679da2cd91.js} +1 -1
- mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/logs-3f5c14076ddde20e.js +1 -0
- mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/{triggers-fe08120d9a6fb1b0.js → triggers-f508c2f261297724.js} +1 -1
- mage_ai/server/{frontend_dist_base_path_template/_next/static/chunks/pages/pipelines-bcdb4ad41dd4c7d5.js → frontend_dist/_next/static/chunks/pages/pipelines-f99e99aa8f45529c.js} +1 -1
- mage_ai/server/frontend_dist/_next/static/chunks/pages/{sign-in-19b36600d908b711.js → sign-in-7d38b2f7c3e918a1.js} +1 -1
- mage_ai/server/frontend_dist/_next/static/chunks/pages/version-control-5753fac7c1bfdc88.js +1 -0
- mage_ai/server/frontend_dist/_next/static/chunks/{webpack-ac7fdc472bedf682.js → webpack-d079359c241db804.js} +1 -1
- mage_ai/server/frontend_dist/_next/static/{ZMrJfDouIX5AMb_RteRbL → vPsMu6Fi2zrHaf2fRXKRO}/_buildManifest.js +1 -1
- mage_ai/server/frontend_dist/block-layout.html +2 -2
- mage_ai/server/frontend_dist/compute.html +2 -2
- mage_ai/server/frontend_dist/files.html +2 -2
- mage_ai/server/frontend_dist/global-data-products/[...slug].html +2 -2
- mage_ai/server/frontend_dist/global-data-products.html +2 -2
- mage_ai/server/frontend_dist/global-hooks/[...slug].html +2 -2
- mage_ai/server/frontend_dist/global-hooks.html +2 -2
- mage_ai/server/frontend_dist/index.html +2 -2
- mage_ai/server/frontend_dist/manage/files.html +2 -2
- mage_ai/server/frontend_dist/manage/settings.html +2 -2
- mage_ai/server/frontend_dist/manage/users/[user].html +2 -2
- mage_ai/server/frontend_dist/manage/users/new.html +2 -2
- mage_ai/server/frontend_dist/manage/users.html +2 -2
- mage_ai/server/frontend_dist/manage.html +2 -2
- mage_ai/server/frontend_dist/oauth.html +2 -2
- mage_ai/server/frontend_dist/overview.html +2 -2
- mage_ai/server/frontend_dist/pipeline-runs.html +2 -2
- mage_ai/server/frontend_dist/pipelines/[pipeline]/backfills/[...slug].html +2 -2
- mage_ai/server/frontend_dist/pipelines/[pipeline]/backfills.html +2 -2
- mage_ai/server/frontend_dist/pipelines/[pipeline]/dashboard.html +2 -2
- mage_ai/server/frontend_dist/pipelines/[pipeline]/edit.html +2 -2
- mage_ai/server/frontend_dist/pipelines/[pipeline]/logs.html +2 -2
- mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors/block-runs.html +2 -2
- mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors/block-runtime.html +2 -2
- mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors.html +2 -2
- mage_ai/server/frontend_dist/pipelines/[pipeline]/runs/[run].html +2 -2
- mage_ai/server/frontend_dist/pipelines/[pipeline]/runs.html +2 -2
- mage_ai/server/frontend_dist/pipelines/[pipeline]/settings.html +2 -2
- mage_ai/server/frontend_dist/pipelines/[pipeline]/syncs.html +2 -2
- mage_ai/server/frontend_dist/pipelines/[pipeline]/triggers/[...slug].html +2 -2
- mage_ai/server/frontend_dist/pipelines/[pipeline]/triggers.html +2 -2
- mage_ai/server/frontend_dist/pipelines/[pipeline].html +2 -2
- mage_ai/server/frontend_dist/pipelines.html +2 -2
- mage_ai/server/frontend_dist/platform/global-hooks/[...slug].html +2 -2
- mage_ai/server/frontend_dist/platform/global-hooks.html +2 -2
- mage_ai/server/frontend_dist/settings/account/profile.html +2 -2
- mage_ai/server/frontend_dist/settings/platform/preferences.html +2 -2
- mage_ai/server/frontend_dist/settings/platform/settings.html +2 -2
- mage_ai/server/frontend_dist/settings/workspace/permissions/[...slug].html +2 -2
- mage_ai/server/frontend_dist/settings/workspace/permissions.html +2 -2
- mage_ai/server/frontend_dist/settings/workspace/preferences.html +2 -2
- mage_ai/server/frontend_dist/settings/workspace/roles/[...slug].html +2 -2
- mage_ai/server/frontend_dist/settings/workspace/roles.html +2 -2
- mage_ai/server/frontend_dist/settings/workspace/sync-data.html +2 -2
- mage_ai/server/frontend_dist/settings/workspace/users/[...slug].html +2 -2
- mage_ai/server/frontend_dist/settings/workspace/users.html +2 -2
- mage_ai/server/frontend_dist/settings.html +2 -2
- mage_ai/server/frontend_dist/sign-in.html +2 -2
- mage_ai/server/frontend_dist/templates/[...slug].html +2 -2
- mage_ai/server/frontend_dist/templates.html +2 -2
- mage_ai/server/frontend_dist/terminal.html +2 -2
- mage_ai/server/frontend_dist/test.html +2 -2
- mage_ai/server/frontend_dist/triggers.html +2 -2
- mage_ai/server/frontend_dist/version-control.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/404.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/{1557-a754b04510d50b80.js → 1557-01f0843dc6ac4971.js} +1 -1
- mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/9440-4069842b90d4b801.js +1 -0
- mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/_app-1c1ffd928f5a00f7.js +1 -0
- mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/index-b7b8695a7f9efde2.js +1 -0
- mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/oauth-30e34ee15d410331.js +1 -0
- mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/backfills/[...slug]-ff4bd7a8ec3bab40.js +1 -0
- mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/backfills-aaf393c86fc1bda3.js +1 -0
- mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/{edit-8d32ac7e8f023779.js → edit-36377e679da2cd91.js} +1 -1
- mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/logs-3f5c14076ddde20e.js +1 -0
- mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/{triggers-fe08120d9a6fb1b0.js → triggers-f508c2f261297724.js} +1 -1
- mage_ai/server/{frontend_dist/_next/static/chunks/pages/pipelines-bcdb4ad41dd4c7d5.js → frontend_dist_base_path_template/_next/static/chunks/pages/pipelines-f99e99aa8f45529c.js} +1 -1
- mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/{sign-in-19b36600d908b711.js → sign-in-7d38b2f7c3e918a1.js} +1 -1
- mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/version-control-5753fac7c1bfdc88.js +1 -0
- mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/{webpack-481689d9989710cd.js → webpack-68c003fb6a175cd7.js} +1 -1
- mage_ai/server/frontend_dist_base_path_template/_next/static/{QYwFH4sievaq5XyUjRriy → khKiaJtwrslgMmp4YSa1f}/_buildManifest.js +1 -1
- mage_ai/server/frontend_dist_base_path_template/block-layout.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/compute.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/files.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/global-data-products/[...slug].html +2 -2
- mage_ai/server/frontend_dist_base_path_template/global-data-products.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/global-hooks/[...slug].html +2 -2
- mage_ai/server/frontend_dist_base_path_template/global-hooks.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/index.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/manage/files.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/manage/settings.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/manage/users/[user].html +2 -2
- mage_ai/server/frontend_dist_base_path_template/manage/users/new.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/manage/users.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/manage.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/oauth.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/overview.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/pipeline-runs.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/backfills/[...slug].html +2 -2
- mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/backfills.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/dashboard.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/edit.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/logs.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/monitors/block-runs.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/monitors/block-runtime.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/monitors.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/runs/[run].html +2 -2
- mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/runs.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/settings.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/syncs.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/triggers/[...slug].html +2 -2
- mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/triggers.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline].html +2 -2
- mage_ai/server/frontend_dist_base_path_template/pipelines.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/platform/global-hooks/[...slug].html +2 -2
- mage_ai/server/frontend_dist_base_path_template/platform/global-hooks.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/settings/account/profile.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/settings/platform/preferences.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/settings/platform/settings.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/settings/workspace/permissions/[...slug].html +2 -2
- mage_ai/server/frontend_dist_base_path_template/settings/workspace/permissions.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/settings/workspace/preferences.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/settings/workspace/roles/[...slug].html +2 -2
- mage_ai/server/frontend_dist_base_path_template/settings/workspace/roles.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/settings/workspace/sync-data.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/settings/workspace/users/[...slug].html +2 -2
- mage_ai/server/frontend_dist_base_path_template/settings/workspace/users.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/settings.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/sign-in.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/templates/[...slug].html +2 -2
- mage_ai/server/frontend_dist_base_path_template/templates.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/terminal.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/test.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/triggers.html +2 -2
- mage_ai/server/frontend_dist_base_path_template/version-control.html +2 -2
- mage_ai/server/server.py +117 -87
- mage_ai/server/utils/output_display.py +6 -1
- mage_ai/services/aws/s3/s3.py +8 -2
- mage_ai/services/slack/slack.py +8 -8
- mage_ai/settings/__init__.py +36 -186
- mage_ai/settings/backends.py +95 -0
- mage_ai/settings/keys/__init__.py +1 -0
- mage_ai/settings/keys/auth.py +76 -0
- mage_ai/settings/server.py +187 -0
- mage_ai/shared/io.py +2 -2
- mage_ai/shared/logger.py +12 -6
- mage_ai/streaming/sources/base_python.py +30 -0
- mage_ai/streaming/sources/source_factory.py +25 -0
- mage_ai/tests/api/endpoints/test_oauths.py +13 -5
- mage_ai/tests/api/operations/test_operations.py +2 -2
- mage_ai/tests/api/operations/test_sessions.py +83 -48
- mage_ai/tests/authentication/oauth/test_utils.py +56 -6
- mage_ai/tests/authentication/providers/test_active_directory.py +9 -15
- mage_ai/tests/data_preparation/models/test_block.py +39 -2
- mage_ai/tests/orchestration/db/models/test_schedules.py +33 -1
- {mage_ai-0.9.65.dist-info → mage_ai-0.9.67.dist-info}/METADATA +2 -1
- {mage_ai-0.9.65.dist-info → mage_ai-0.9.67.dist-info}/RECORD +225 -217
- {mage_ai-0.9.65.dist-info → mage_ai-0.9.67.dist-info}/WHEEL +1 -1
- mage_ai/server/frontend_dist/_next/static/chunks/9440-2bcbdc765ed82062.js +0 -1
- mage_ai/server/frontend_dist/_next/static/chunks/pages/_app-2ae1d919333f01fe.js +0 -1
- mage_ai/server/frontend_dist/_next/static/chunks/pages/index-64851458dde54ad9.js +0 -1
- mage_ai/server/frontend_dist/_next/static/chunks/pages/oauth-abe5ba687cb93509.js +0 -1
- mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/backfills/[...slug]-22e49726eeed16ae.js +0 -1
- mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/backfills-c74507dce89b41a2.js +0 -1
- mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/logs-cf656cbe37ecaacc.js +0 -1
- mage_ai/server/frontend_dist/_next/static/chunks/pages/version-control-690206d30d8b412b.js +0 -1
- mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/9440-2bcbdc765ed82062.js +0 -1
- mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/_app-2ae1d919333f01fe.js +0 -1
- mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/index-64851458dde54ad9.js +0 -1
- mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/oauth-abe5ba687cb93509.js +0 -1
- mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/backfills/[...slug]-22e49726eeed16ae.js +0 -1
- mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/backfills-c74507dce89b41a2.js +0 -1
- mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/logs-cf656cbe37ecaacc.js +0 -1
- mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/version-control-690206d30d8b412b.js +0 -1
- mage_ai/settings/sso.py +0 -27
- /mage_ai/server/frontend_dist/_next/static/{ZMrJfDouIX5AMb_RteRbL → vPsMu6Fi2zrHaf2fRXKRO}/_ssgManifest.js +0 -0
- /mage_ai/server/frontend_dist_base_path_template/_next/static/{QYwFH4sievaq5XyUjRriy → khKiaJtwrslgMmp4YSa1f}/_ssgManifest.js +0 -0
- {mage_ai-0.9.65.dist-info → mage_ai-0.9.67.dist-info}/LICENSE +0 -0
- {mage_ai-0.9.65.dist-info → mage_ai-0.9.67.dist-info}/entry_points.txt +0 -0
- {mage_ai-0.9.65.dist-info → mage_ai-0.9.67.dist-info}/top_level.txt +0 -0
mage_ai/io/export_utils.py
CHANGED
|
@@ -104,6 +104,7 @@ def gen_table_creation_query(
|
|
|
104
104
|
dtypes: Mapping[str, str],
|
|
105
105
|
schema_name: str,
|
|
106
106
|
table_name: str,
|
|
107
|
+
auto_clean_name: bool = True,
|
|
107
108
|
case_sensitive: bool = False,
|
|
108
109
|
unique_constraints: List[str] = None,
|
|
109
110
|
overwrite_types: Dict = None,
|
|
@@ -123,16 +124,16 @@ def gen_table_creation_query(
|
|
|
123
124
|
if unique_constraints is None:
|
|
124
125
|
unique_constraints = []
|
|
125
126
|
query = []
|
|
126
|
-
|
|
127
|
+
for cname in dtypes:
|
|
128
|
+
if overwrite_types is not None and cname in overwrite_types.keys():
|
|
129
|
+
dtypes[cname] = overwrite_types[cname]
|
|
127
130
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
+
if auto_clean_name:
|
|
132
|
+
cleaned_col_name = clean_name(cname, case_sensitive=case_sensitive)
|
|
133
|
+
else:
|
|
134
|
+
cleaned_col_name = cname
|
|
131
135
|
|
|
132
|
-
|
|
133
|
-
else:
|
|
134
|
-
for cname in dtypes:
|
|
135
|
-
query.append(f'"{clean_name(cname, case_sensitive=case_sensitive)}" {dtypes[cname]}')
|
|
136
|
+
query.append(f'"{cleaned_col_name}" {dtypes[cname]}')
|
|
136
137
|
|
|
137
138
|
if schema_name:
|
|
138
139
|
full_table_name = f'{schema_name}.{table_name}'
|
|
@@ -142,7 +143,11 @@ def gen_table_creation_query(
|
|
|
142
143
|
if unique_constraints:
|
|
143
144
|
unique_constraints_clean = []
|
|
144
145
|
for col in unique_constraints:
|
|
145
|
-
|
|
146
|
+
if auto_clean_name:
|
|
147
|
+
cleaned_col_name = clean_name(col, case_sensitive=case_sensitive)
|
|
148
|
+
else:
|
|
149
|
+
cleaned_col_name = col
|
|
150
|
+
unique_constraints_clean.append(cleaned_col_name)
|
|
146
151
|
unique_constraints_escaped = [f'"{col}"'
|
|
147
152
|
for col in unique_constraints_clean]
|
|
148
153
|
index_name = '_'.join([
|
mage_ai/io/mssql.py
CHANGED
|
@@ -11,10 +11,24 @@ from sqlalchemy.engine import URL
|
|
|
11
11
|
|
|
12
12
|
from mage_ai.io.base import QUERY_ROW_LIMIT, ExportWritePolicy
|
|
13
13
|
from mage_ai.io.config import BaseConfigLoader, ConfigKey
|
|
14
|
+
from mage_ai.io.constants import UNIQUE_CONFLICT_METHOD_UPDATE
|
|
14
15
|
from mage_ai.io.export_utils import PandasTypes
|
|
15
16
|
from mage_ai.io.sql import BaseSQL
|
|
16
17
|
from mage_ai.shared.parsers import encode_complex
|
|
17
18
|
|
|
19
|
+
MERGE_TABLE_SQL = '''MERGE {table_name} AS t
|
|
20
|
+
USING (VALUES
|
|
21
|
+
({values_placeholder})
|
|
22
|
+
) s({columns})
|
|
23
|
+
ON {on_clause}
|
|
24
|
+
WHEN NOT MATCHED THEN
|
|
25
|
+
INSERT ({insert})
|
|
26
|
+
VALUES ({values})
|
|
27
|
+
WHEN MATCHED THEN UPDATE SET
|
|
28
|
+
{update}
|
|
29
|
+
;
|
|
30
|
+
'''
|
|
31
|
+
|
|
18
32
|
|
|
19
33
|
class MSSQL(BaseSQL):
|
|
20
34
|
def __init__(
|
|
@@ -36,7 +50,7 @@ class MSSQL(BaseSQL):
|
|
|
36
50
|
schema=schema,
|
|
37
51
|
port=port,
|
|
38
52
|
verbose=verbose,
|
|
39
|
-
**kwargs
|
|
53
|
+
**kwargs,
|
|
40
54
|
)
|
|
41
55
|
|
|
42
56
|
@property
|
|
@@ -71,26 +85,29 @@ class MSSQL(BaseSQL):
|
|
|
71
85
|
# ref: https://github.com/mkleehammer/pyodbc/issues/134#issuecomment-281739794
|
|
72
86
|
# now struct.unpack: e.g., (2017, 3, 16, 10, 35, 18, 500000000, -6, 0)
|
|
73
87
|
tup = struct.unpack("<6hI2h", dto_value)
|
|
74
|
-
return datetime(
|
|
75
|
-
|
|
88
|
+
return datetime(
|
|
89
|
+
tup[0],
|
|
90
|
+
tup[1],
|
|
91
|
+
tup[2],
|
|
92
|
+
tup[3],
|
|
93
|
+
tup[4],
|
|
94
|
+
tup[5],
|
|
95
|
+
tup[6] // 1000,
|
|
96
|
+
timezone(timedelta(hours=tup[7], minutes=tup[8])),
|
|
97
|
+
)
|
|
76
98
|
|
|
77
99
|
self._ctx.add_output_converter(-155, handle_datetimeoffset)
|
|
78
100
|
|
|
79
|
-
def build_create_schema_command(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
) -> str:
|
|
83
|
-
return '\n'.join([
|
|
101
|
+
def build_create_schema_command(self, schema_name: str) -> str:
|
|
102
|
+
return '\n'.join(
|
|
103
|
+
[
|
|
84
104
|
'IF NOT EXISTS (',
|
|
85
105
|
f'SELECT * FROM information_schema.schemata WHERE schema_name = \'{schema_name}\')',
|
|
86
|
-
f'BEGIN EXEC(\'CREATE SCHEMA {schema_name}\') END'
|
|
87
|
-
]
|
|
106
|
+
f'BEGIN EXEC(\'CREATE SCHEMA {schema_name}\') END',
|
|
107
|
+
]
|
|
108
|
+
)
|
|
88
109
|
|
|
89
|
-
def build_create_table_as_command(
|
|
90
|
-
self,
|
|
91
|
-
table_name: str,
|
|
92
|
-
query_string: str
|
|
93
|
-
) -> str:
|
|
110
|
+
def build_create_table_as_command(self, table_name: str, query_string: str) -> str:
|
|
94
111
|
return 'SELECT * INTO {}\nFROM ({}) AS prev'.format(
|
|
95
112
|
table_name,
|
|
96
113
|
query_string,
|
|
@@ -98,10 +115,14 @@ class MSSQL(BaseSQL):
|
|
|
98
115
|
|
|
99
116
|
def table_exists(self, schema_name: str, table_name: str) -> bool:
|
|
100
117
|
with self.conn.cursor() as cur:
|
|
101
|
-
cur.execute(
|
|
102
|
-
'
|
|
103
|
-
|
|
104
|
-
|
|
118
|
+
cur.execute(
|
|
119
|
+
'\n'.join(
|
|
120
|
+
[
|
|
121
|
+
'SELECT TOP 1 * FROM information_schema.tables ',
|
|
122
|
+
f'WHERE table_schema = \'{schema_name}\' AND table_name = \'{table_name}\'',
|
|
123
|
+
]
|
|
124
|
+
)
|
|
125
|
+
)
|
|
105
126
|
return len(cur.fetchall()) >= 1
|
|
106
127
|
|
|
107
128
|
def upload_dataframe(
|
|
@@ -146,7 +167,9 @@ class MSSQL(BaseSQL):
|
|
|
146
167
|
|
|
147
168
|
# Remove extraneous surrounding double quotes
|
|
148
169
|
# that get added while performing conversion to string.
|
|
149
|
-
df_[col] = df_[col].apply(
|
|
170
|
+
df_[col] = df_[col].apply(
|
|
171
|
+
lambda x: x.strip('"') if x and isinstance(x, str) else x
|
|
172
|
+
)
|
|
150
173
|
df_.replace({np.NaN: None}, inplace=True)
|
|
151
174
|
for _, row in df_.iterrows():
|
|
152
175
|
values.append(tuple(row))
|
|
@@ -159,8 +182,8 @@ class MSSQL(BaseSQL):
|
|
|
159
182
|
df: DataFrame,
|
|
160
183
|
schema_name: str,
|
|
161
184
|
table_name: str,
|
|
162
|
-
if_exists: ExportWritePolicy =
|
|
163
|
-
|
|
185
|
+
if_exists: ExportWritePolicy = None,
|
|
186
|
+
**kwargs,
|
|
164
187
|
):
|
|
165
188
|
connection_url = URL.create(
|
|
166
189
|
'mssql+pyodbc',
|
|
@@ -178,7 +201,55 @@ class MSSQL(BaseSQL):
|
|
|
178
201
|
connection_url,
|
|
179
202
|
fast_executemany=True,
|
|
180
203
|
)
|
|
181
|
-
|
|
204
|
+
|
|
205
|
+
unique_conflict_method = kwargs.get('unique_conflict_method')
|
|
206
|
+
unique_constraints = kwargs.get('unique_constraints')
|
|
207
|
+
|
|
208
|
+
if unique_conflict_method and unique_constraints:
|
|
209
|
+
|
|
210
|
+
def merge_table(
|
|
211
|
+
table, conn, keys, data_iter, unique_constraints=unique_constraints
|
|
212
|
+
):
|
|
213
|
+
dbapi_conn = conn.connection
|
|
214
|
+
with dbapi_conn.cursor() as cur:
|
|
215
|
+
if table.schema:
|
|
216
|
+
table_name = f'[{table.schema}].[{table.name}]'
|
|
217
|
+
else:
|
|
218
|
+
table_name = table.name
|
|
219
|
+
|
|
220
|
+
values_placeholder = ', '.join(['?' for i in range(len(keys))])
|
|
221
|
+
values = [tuple(row) for row in data_iter]
|
|
222
|
+
sql = MERGE_TABLE_SQL.format(
|
|
223
|
+
table_name=table_name,
|
|
224
|
+
values_placeholder=values_placeholder,
|
|
225
|
+
columns=', '.join([f'[{k}]' for k in keys]),
|
|
226
|
+
on_clause=' AND '.join(
|
|
227
|
+
[f's.[{k}] = t.[{k}]' for k in unique_constraints]
|
|
228
|
+
),
|
|
229
|
+
insert=', '.join([f'[{c}]' for c in keys]),
|
|
230
|
+
values=', '.join([f's.[{c}]' for c in keys]),
|
|
231
|
+
update=', '.join([f'[{c}] = s.[{c}]' for c in keys]),
|
|
232
|
+
)
|
|
233
|
+
cur.executemany(sql, values)
|
|
234
|
+
|
|
235
|
+
if UNIQUE_CONFLICT_METHOD_UPDATE == unique_conflict_method:
|
|
236
|
+
df.to_sql(
|
|
237
|
+
table_name,
|
|
238
|
+
engine,
|
|
239
|
+
schema=schema_name,
|
|
240
|
+
if_exists=if_exists or ExportWritePolicy.APPEND,
|
|
241
|
+
index=False,
|
|
242
|
+
method=merge_table,
|
|
243
|
+
)
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
df.to_sql(
|
|
247
|
+
table_name,
|
|
248
|
+
engine,
|
|
249
|
+
schema=schema_name,
|
|
250
|
+
if_exists=if_exists or ExportWritePolicy.REPLACE,
|
|
251
|
+
index=False,
|
|
252
|
+
)
|
|
182
253
|
|
|
183
254
|
def get_type(self, column: Series, dtype: str) -> str:
|
|
184
255
|
if dtype in (
|
|
@@ -210,7 +281,11 @@ class MSSQL(BaseSQL):
|
|
|
210
281
|
return 'text'
|
|
211
282
|
elif dtype == PandasTypes.BYTES:
|
|
212
283
|
return 'varbinary(255)'
|
|
213
|
-
elif dtype in (
|
|
284
|
+
elif dtype in (
|
|
285
|
+
PandasTypes.FLOATING,
|
|
286
|
+
PandasTypes.DECIMAL,
|
|
287
|
+
PandasTypes.MIXED_INTEGER_FLOAT,
|
|
288
|
+
):
|
|
214
289
|
return 'decimal'
|
|
215
290
|
elif dtype == PandasTypes.INTEGER:
|
|
216
291
|
max_int, min_int = column.max(), column.min()
|
|
@@ -222,7 +297,11 @@ class MSSQL(BaseSQL):
|
|
|
222
297
|
return 'bigint'
|
|
223
298
|
elif dtype == PandasTypes.BOOLEAN:
|
|
224
299
|
return 'char(52)'
|
|
225
|
-
elif dtype in (
|
|
300
|
+
elif dtype in (
|
|
301
|
+
PandasTypes.TIMEDELTA,
|
|
302
|
+
PandasTypes.TIMEDELTA64,
|
|
303
|
+
PandasTypes.PERIOD,
|
|
304
|
+
):
|
|
226
305
|
return 'bigint'
|
|
227
306
|
elif dtype == PandasTypes.EMPTY:
|
|
228
307
|
return 'char(255)'
|
mage_ai/io/mysql.py
CHANGED
|
@@ -52,6 +52,8 @@ class MySQL(BaseSQL):
|
|
|
52
52
|
dtypes: Mapping[str, str],
|
|
53
53
|
schema_name: str,
|
|
54
54
|
table_name: str,
|
|
55
|
+
auto_clean_name: bool = True,
|
|
56
|
+
case_sensitive: bool = False,
|
|
55
57
|
unique_constraints: List[str] = None,
|
|
56
58
|
overwrite_types: Dict = None,
|
|
57
59
|
**kwargs,
|
|
@@ -59,15 +61,14 @@ class MySQL(BaseSQL):
|
|
|
59
61
|
if unique_constraints is None:
|
|
60
62
|
unique_constraints = []
|
|
61
63
|
query = []
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
query.append(f'`{clean_name(cname)}` {dtypes[cname]} NULL')
|
|
64
|
+
for cname in dtypes:
|
|
65
|
+
if overwrite_types is not None and cname in overwrite_types.keys():
|
|
66
|
+
dtypes[cname] = overwrite_types[cname]
|
|
67
|
+
if auto_clean_name:
|
|
68
|
+
cleaned_col_name = clean_name(cname, case_sensitive=case_sensitive)
|
|
69
|
+
else:
|
|
70
|
+
cleaned_col_name = cname
|
|
71
|
+
query.append(f'`{cleaned_col_name}` {dtypes[cname]} NULL')
|
|
71
72
|
|
|
72
73
|
return f'CREATE TABLE {table_name} (' + ','.join(query) + ');'
|
|
73
74
|
|
mage_ai/io/oracledb.py
CHANGED
|
@@ -7,6 +7,9 @@ from pandas import DataFrame, read_sql
|
|
|
7
7
|
from mage_ai.io.base import QUERY_ROW_LIMIT
|
|
8
8
|
from mage_ai.io.config import BaseConfigLoader, ConfigKey
|
|
9
9
|
from mage_ai.io.sql import BaseSQL
|
|
10
|
+
from mage_ai.server.logger import Logger
|
|
11
|
+
|
|
12
|
+
logger = Logger().new_server_logger(__name__)
|
|
10
13
|
|
|
11
14
|
|
|
12
15
|
class OracleDB(BaseSQL):
|
|
@@ -40,8 +43,17 @@ class OracleDB(BaseSQL):
|
|
|
40
43
|
)
|
|
41
44
|
|
|
42
45
|
def open(self) -> None:
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
if self.settings['mode'] and self.settings['mode'].lower() == 'thick':
|
|
47
|
+
logger.info('Initializing Oracle thick mode.')
|
|
48
|
+
oracledb.init_oracle_client()
|
|
49
|
+
with self.printer.print_msg(f'Opening connection to OracleDB database \
|
|
50
|
+
({self.settings["mode"]} mode)'):
|
|
51
|
+
connection_dsn = "{}:{}/{}".format(
|
|
52
|
+
self.settings['host'],
|
|
53
|
+
self.settings['port'],
|
|
54
|
+
self.settings['service_name'])
|
|
55
|
+
self._ctx = oracledb.connect(
|
|
56
|
+
user=self.settings['user'], password=self.settings['password'], dsn=connection_dsn)
|
|
45
57
|
|
|
46
58
|
def load(
|
|
47
59
|
self,
|
mage_ai/io/postgres.py
CHANGED
|
@@ -284,6 +284,7 @@ class Postgres(BaseSQL):
|
|
|
284
284
|
dtypes: List[str],
|
|
285
285
|
full_table_name: str,
|
|
286
286
|
allow_reserved_words: bool = False,
|
|
287
|
+
auto_clean_name: bool = True,
|
|
287
288
|
buffer: Union[IO, None] = None,
|
|
288
289
|
case_sensitive: bool = False,
|
|
289
290
|
unique_conflict_method: str = None,
|
|
@@ -355,6 +356,7 @@ class Postgres(BaseSQL):
|
|
|
355
356
|
cleaned_col = self._clean_column_name(
|
|
356
357
|
col,
|
|
357
358
|
allow_reserved_words=allow_reserved_words,
|
|
359
|
+
auto_clean_name=auto_clean_name,
|
|
358
360
|
case_sensitive=case_sensitive,
|
|
359
361
|
)
|
|
360
362
|
cleaned_unique_constraints.append(f'"{cleaned_col}"')
|
|
@@ -364,6 +366,7 @@ class Postgres(BaseSQL):
|
|
|
364
366
|
cleaned_col = self._clean_column_name(
|
|
365
367
|
col,
|
|
366
368
|
allow_reserved_words=allow_reserved_words,
|
|
369
|
+
auto_clean_name=auto_clean_name,
|
|
367
370
|
case_sensitive=case_sensitive,
|
|
368
371
|
)
|
|
369
372
|
cleaned_columns.append(f'"{cleaned_col}"')
|
mage_ai/io/sql.py
CHANGED
|
@@ -52,6 +52,7 @@ class BaseSQL(BaseSQLConnection):
|
|
|
52
52
|
dtypes: Mapping[str, str],
|
|
53
53
|
schema_name: str,
|
|
54
54
|
table_name: str,
|
|
55
|
+
auto_clean_name: bool = True,
|
|
55
56
|
case_sensitive: bool = False,
|
|
56
57
|
unique_constraints: List[str] = None,
|
|
57
58
|
overwrite_types: Dict = None,
|
|
@@ -63,6 +64,7 @@ class BaseSQL(BaseSQLConnection):
|
|
|
63
64
|
dtypes,
|
|
64
65
|
schema_name,
|
|
65
66
|
table_name,
|
|
67
|
+
auto_clean_name=auto_clean_name,
|
|
66
68
|
case_sensitive=case_sensitive,
|
|
67
69
|
unique_constraints=unique_constraints,
|
|
68
70
|
overwrite_types=overwrite_types,
|
|
@@ -224,6 +226,7 @@ class BaseSQL(BaseSQLConnection):
|
|
|
224
226
|
verbose: bool = True,
|
|
225
227
|
# Other optional configs
|
|
226
228
|
allow_reserved_words: bool = False,
|
|
229
|
+
auto_clean_name: bool = True,
|
|
227
230
|
case_sensitive: bool = False,
|
|
228
231
|
cascade_on_drop: bool = False,
|
|
229
232
|
drop_table_on_replace: bool = False,
|
|
@@ -276,12 +279,13 @@ class BaseSQL(BaseSQLConnection):
|
|
|
276
279
|
df = clean_df_for_export(df, self.clean, dtypes)
|
|
277
280
|
|
|
278
281
|
# Clean column names
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
282
|
+
if auto_clean_name:
|
|
283
|
+
col_mapping = {col: self._clean_column_name(
|
|
284
|
+
col,
|
|
285
|
+
allow_reserved_words=allow_reserved_words,
|
|
286
|
+
case_sensitive=case_sensitive)
|
|
287
|
+
for col in df.columns}
|
|
288
|
+
df = df.rename(columns=col_mapping)
|
|
285
289
|
dtypes = infer_dtypes(df)
|
|
286
290
|
|
|
287
291
|
def __process():
|
|
@@ -292,6 +296,8 @@ class BaseSQL(BaseSQLConnection):
|
|
|
292
296
|
schema_name,
|
|
293
297
|
table_name,
|
|
294
298
|
if_exists=if_exists,
|
|
299
|
+
unique_conflict_method=unique_conflict_method,
|
|
300
|
+
unique_constraints=unique_constraints,
|
|
295
301
|
)
|
|
296
302
|
return
|
|
297
303
|
|
|
@@ -340,6 +346,7 @@ class BaseSQL(BaseSQLConnection):
|
|
|
340
346
|
db_dtypes,
|
|
341
347
|
schema_name,
|
|
342
348
|
table_name,
|
|
349
|
+
auto_clean_name=auto_clean_name,
|
|
343
350
|
case_sensitive=case_sensitive,
|
|
344
351
|
unique_constraints=unique_constraints,
|
|
345
352
|
overwrite_types=overwrite_types,
|
|
@@ -354,6 +361,7 @@ class BaseSQL(BaseSQLConnection):
|
|
|
354
361
|
allow_reserved_words=allow_reserved_words,
|
|
355
362
|
buffer=buffer,
|
|
356
363
|
case_sensitive=case_sensitive,
|
|
364
|
+
auto_clean_name=auto_clean_name,
|
|
357
365
|
unique_conflict_method=unique_conflict_method,
|
|
358
366
|
unique_constraints=unique_constraints,
|
|
359
367
|
**kwargs,
|
mage_ai/io/trino.py
CHANGED
|
@@ -119,6 +119,8 @@ class Trino(BaseSQL):
|
|
|
119
119
|
dtypes: Mapping[str, str],
|
|
120
120
|
schema_name: str,
|
|
121
121
|
table_name: str,
|
|
122
|
+
auto_clean_name: bool = True,
|
|
123
|
+
case_sensitive: bool = False,
|
|
122
124
|
unique_constraints: List[str] = None,
|
|
123
125
|
overwrite_types: Dict = None,
|
|
124
126
|
**kwargs,
|
|
@@ -126,14 +128,14 @@ class Trino(BaseSQL):
|
|
|
126
128
|
if unique_constraints is None:
|
|
127
129
|
unique_constraints = []
|
|
128
130
|
query = []
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
131
|
+
for cname in dtypes:
|
|
132
|
+
if overwrite_types is not None and cname in overwrite_types.keys():
|
|
133
|
+
dtypes[cname] = overwrite_types[cname]
|
|
134
|
+
if auto_clean_name:
|
|
135
|
+
cleaned_col_name = clean_name(cname, case_sensitive=case_sensitive)
|
|
136
|
+
else:
|
|
137
|
+
cleaned_col_name = cname
|
|
138
|
+
query.append(f'"{cleaned_col_name}" {dtypes[cname]}')
|
|
137
139
|
|
|
138
140
|
full_table_name = '.'.join(list(filter(lambda x: x, [
|
|
139
141
|
schema_name,
|
mage_ai/orchestration/db/migrations/versions/90d978a8aef8_update_unique_constraint_for_secret.py
CHANGED
|
@@ -20,11 +20,17 @@ def upgrade() -> None:
|
|
|
20
20
|
# ### commands auto generated by Alembic - please adjust! ###
|
|
21
21
|
try:
|
|
22
22
|
bind = op.get_bind()
|
|
23
|
-
insp = sa.inspect(bind
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
for
|
|
27
|
-
|
|
23
|
+
insp = sa.inspect(bind)
|
|
24
|
+
if bind.engine.name == 'mssql':
|
|
25
|
+
indexes = insp.get_indexes('secret')
|
|
26
|
+
for i in indexes:
|
|
27
|
+
if i['unique'] is True:
|
|
28
|
+
op.execute(f"ALTER TABLE secret DROP CONSTRAINT {i['name']};")
|
|
29
|
+
else:
|
|
30
|
+
with op.batch_alter_table('secret', schema=None) as batch_op:
|
|
31
|
+
unique_constraints = insp.get_unique_constraints('secret')
|
|
32
|
+
for constraint in unique_constraints:
|
|
33
|
+
batch_op.drop_constraint(constraint['name'], type_='unique')
|
|
28
34
|
except ValueError:
|
|
29
35
|
with op.batch_alter_table('secret', schema=None) as batch_op:
|
|
30
36
|
batch_op.add_column(sa.Column('name_new', sa.String(length=255), nullable=True))
|
|
@@ -337,6 +337,7 @@ class PipelineSchedule(PipelineScheduleProjectPlatformMixin, BaseModel):
|
|
|
337
337
|
last_enabled_at=last_enabled_at,
|
|
338
338
|
name=trigger_config.name,
|
|
339
339
|
pipeline_uuid=trigger_config.pipeline_uuid,
|
|
340
|
+
repo_path=trigger_config.repo_path,
|
|
340
341
|
schedule_interval=trigger_config.schedule_interval,
|
|
341
342
|
schedule_type=trigger_config.schedule_type,
|
|
342
343
|
settings=trigger_config.settings,
|
|
@@ -351,6 +352,7 @@ class PipelineSchedule(PipelineScheduleProjectPlatformMixin, BaseModel):
|
|
|
351
352
|
existing_trigger.last_enabled_at != kwargs.get('last_enabled_at'),
|
|
352
353
|
existing_trigger.name != kwargs.get('name'),
|
|
353
354
|
existing_trigger.pipeline_uuid != kwargs.get('pipeline_uuid'),
|
|
355
|
+
existing_trigger.repo_path != kwargs.get('repo_path'),
|
|
354
356
|
existing_trigger.schedule_interval != kwargs.get('schedule_interval'),
|
|
355
357
|
existing_trigger.schedule_type != kwargs.get('schedule_type'),
|
|
356
358
|
existing_trigger.settings != kwargs.get('settings'),
|
|
@@ -1548,7 +1550,12 @@ class PipelineRun(PipelineRunProjectPlatformMixin, BaseModel):
|
|
|
1548
1550
|
interval_start_datetime = self.execution_date
|
|
1549
1551
|
interval_start_datetime_previous = None
|
|
1550
1552
|
|
|
1551
|
-
if
|
|
1553
|
+
if (
|
|
1554
|
+
ScheduleInterval.ONCE == self.pipeline_schedule.schedule_interval
|
|
1555
|
+
or ScheduleInterval.ALWAYS_ON == self.pipeline_schedule.schedule_interval
|
|
1556
|
+
):
|
|
1557
|
+
pass
|
|
1558
|
+
elif ScheduleInterval.DAILY == self.pipeline_schedule.schedule_interval:
|
|
1552
1559
|
interval_seconds = 60 * 60 * 24
|
|
1553
1560
|
elif ScheduleInterval.HOURLY == self.pipeline_schedule.schedule_interval:
|
|
1554
1561
|
interval_seconds = 60 * 60 * 1
|
|
@@ -1559,6 +1566,23 @@ class PipelineRun(PipelineRunProjectPlatformMixin, BaseModel):
|
|
|
1559
1566
|
)
|
|
1560
1567
|
elif ScheduleInterval.WEEKLY == self.pipeline_schedule.schedule_interval:
|
|
1561
1568
|
interval_seconds = 60 * 60 * 24 * 7
|
|
1569
|
+
else:
|
|
1570
|
+
try:
|
|
1571
|
+
cron_itr = croniter(
|
|
1572
|
+
self.pipeline_schedule.schedule_interval,
|
|
1573
|
+
self.execution_date,
|
|
1574
|
+
)
|
|
1575
|
+
current = cron_itr.get_current(datetime)
|
|
1576
|
+
interval_start_datetime_previous = cron_itr.get_prev(datetime)
|
|
1577
|
+
# get_prev and get_next changes the state of the cron iterator, so we need
|
|
1578
|
+
# to call get_next again to go back to the original state
|
|
1579
|
+
cron_itr.get_next()
|
|
1580
|
+
interval_end_datetime = cron_itr.get_next(datetime)
|
|
1581
|
+
interval_seconds = (
|
|
1582
|
+
interval_end_datetime.timestamp() - current.timestamp()
|
|
1583
|
+
)
|
|
1584
|
+
except Exception:
|
|
1585
|
+
pass
|
|
1562
1586
|
|
|
1563
1587
|
if interval_seconds and not interval_end_datetime:
|
|
1564
1588
|
interval_end_datetime = interval_start_datetime + timedelta(
|
|
@@ -5,6 +5,7 @@ from typing import Dict, List
|
|
|
5
5
|
|
|
6
6
|
import dateutil.parser
|
|
7
7
|
import pytz
|
|
8
|
+
from croniter import croniter
|
|
8
9
|
from dateutil.relativedelta import relativedelta
|
|
9
10
|
from sqlalchemy import or_
|
|
10
11
|
from sqlalchemy.sql import func
|
|
@@ -298,7 +299,12 @@ class PipelineRunProjectPlatformMixin:
|
|
|
298
299
|
interval_start_datetime = self.execution_date
|
|
299
300
|
interval_start_datetime_previous = None
|
|
300
301
|
|
|
301
|
-
if
|
|
302
|
+
if (
|
|
303
|
+
ScheduleInterval.ONCE == self.pipeline_schedule.schedule_interval
|
|
304
|
+
or ScheduleInterval.ALWAYS_ON == self.pipeline_schedule.schedule_interval
|
|
305
|
+
):
|
|
306
|
+
pass
|
|
307
|
+
elif ScheduleInterval.DAILY == self.pipeline_schedule.schedule_interval:
|
|
302
308
|
interval_seconds = 60 * 60 * 24
|
|
303
309
|
elif ScheduleInterval.HOURLY == self.pipeline_schedule.schedule_interval:
|
|
304
310
|
interval_seconds = 60 * 60 * 1
|
|
@@ -309,6 +315,23 @@ class PipelineRunProjectPlatformMixin:
|
|
|
309
315
|
)
|
|
310
316
|
elif ScheduleInterval.WEEKLY == self.pipeline_schedule.schedule_interval:
|
|
311
317
|
interval_seconds = 60 * 60 * 24 * 7
|
|
318
|
+
else:
|
|
319
|
+
try:
|
|
320
|
+
cron_itr = croniter(
|
|
321
|
+
self.pipeline_schedule.schedule_interval,
|
|
322
|
+
self.execution_date,
|
|
323
|
+
)
|
|
324
|
+
current = cron_itr.get_current(datetime)
|
|
325
|
+
interval_start_datetime_previous = cron_itr.get_prev(datetime)
|
|
326
|
+
# get_prev and get_next changes the state of the cron iterator, so we need
|
|
327
|
+
# to call get_next again to go back to the original state
|
|
328
|
+
cron_itr.get_next()
|
|
329
|
+
interval_end_datetime = cron_itr.get_next(datetime)
|
|
330
|
+
interval_seconds = (
|
|
331
|
+
interval_end_datetime.timestamp() - current.timestamp()
|
|
332
|
+
)
|
|
333
|
+
except Exception:
|
|
334
|
+
pass
|
|
312
335
|
|
|
313
336
|
if interval_seconds and not interval_end_datetime:
|
|
314
337
|
interval_end_datetime = interval_start_datetime + timedelta(
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import traceback
|
|
1
2
|
from enum import Enum
|
|
2
3
|
from typing import Callable, Dict, Union
|
|
3
4
|
|
|
@@ -89,4 +90,8 @@ class JobManager:
|
|
|
89
90
|
return f'{job_type}_{uid}'
|
|
90
91
|
|
|
91
92
|
|
|
92
|
-
|
|
93
|
+
try:
|
|
94
|
+
job_manager = JobManager()
|
|
95
|
+
except Exception:
|
|
96
|
+
traceback.print_exc()
|
|
97
|
+
job_manager = None
|
|
@@ -240,19 +240,14 @@ class PipelineScheduler:
|
|
|
240
240
|
failed_block_runs = self.pipeline_run.failed_block_runs
|
|
241
241
|
error_msg = 'Failed blocks: '\
|
|
242
242
|
f'{", ".join([b.block_uuid for b in failed_block_runs])}.'
|
|
243
|
-
self.
|
|
244
|
-
error=error_msg,
|
|
245
|
-
pipeline=self.pipeline,
|
|
246
|
-
pipeline_run=self.pipeline_run,
|
|
247
|
-
)
|
|
243
|
+
self.on_pipeline_run_failure(error_msg)
|
|
248
244
|
else:
|
|
249
245
|
self.pipeline_run.complete()
|
|
250
246
|
self.notification_sender.send_pipeline_run_success_message(
|
|
251
247
|
pipeline=self.pipeline,
|
|
252
248
|
pipeline_run=self.pipeline_run,
|
|
253
249
|
)
|
|
254
|
-
|
|
255
|
-
asyncio.run(UsageStatisticLogger().pipeline_run_ended(self.pipeline_run))
|
|
250
|
+
asyncio.run(UsageStatisticLogger().pipeline_run_ended(self.pipeline_run))
|
|
256
251
|
|
|
257
252
|
self.logger_manager.output_logs_to_destination()
|
|
258
253
|
|
|
@@ -323,12 +318,20 @@ class PipelineScheduler:
|
|
|
323
318
|
self.__schedule_blocks(block_runs)
|
|
324
319
|
|
|
325
320
|
@safe_db_query
|
|
326
|
-
def on_pipeline_run_failure(self,
|
|
321
|
+
def on_pipeline_run_failure(self, error_msg: str) -> None:
|
|
322
|
+
failed_block_runs = self.pipeline_run.failed_block_runs
|
|
323
|
+
for br in failed_block_runs:
|
|
324
|
+
if br.metrics:
|
|
325
|
+
message = br.metrics.get('error', {}).get('message')
|
|
326
|
+
if message:
|
|
327
|
+
error_msg += f'\nError for block {br.block_uuid}:\n{message}'
|
|
328
|
+
break
|
|
329
|
+
|
|
327
330
|
asyncio.run(UsageStatisticLogger().pipeline_run_ended(self.pipeline_run))
|
|
328
331
|
self.notification_sender.send_pipeline_run_failure_message(
|
|
329
332
|
pipeline=self.pipeline,
|
|
330
333
|
pipeline_run=self.pipeline_run,
|
|
331
|
-
error=
|
|
334
|
+
error=error_msg,
|
|
332
335
|
)
|
|
333
336
|
# Cancel block runs that are still in progress for the pipeline run.
|
|
334
337
|
cancel_block_runs_and_jobs(self.pipeline_run, self.pipeline)
|
|
@@ -1422,7 +1425,10 @@ def schedule_all():
|
|
|
1422
1425
|
))
|
|
1423
1426
|
|
|
1424
1427
|
# Sync schedules from yaml file to DB
|
|
1425
|
-
|
|
1428
|
+
try:
|
|
1429
|
+
sync_schedules(list(repo_pipelines))
|
|
1430
|
+
except Exception:
|
|
1431
|
+
logger.exception('Failed to sync schedules')
|
|
1426
1432
|
|
|
1427
1433
|
active_pipeline_schedules = list(PipelineSchedule.active_schedules(
|
|
1428
1434
|
pipeline_uuids=repo_pipelines,
|
mage_ai/server/constants.py
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from watchdog.events import FileSystemEvent, FileSystemEventHandler
|
|
2
|
+
|
|
3
|
+
from mage_ai.data_preparation.repo_manager import update_settings_on_metadata_change
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MetadataEventHandler(FileSystemEventHandler):
|
|
7
|
+
def on_modified(self, event: FileSystemEvent):
|
|
8
|
+
super().on_modified(event)
|
|
9
|
+
|
|
10
|
+
update_settings_on_metadata_change()
|