great-expectations-cloud 20240523.0.dev0__py3-none-any.whl → 20251124.0.dev1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. great_expectations_cloud/agent/__init__.py +3 -0
  2. great_expectations_cloud/agent/actions/__init__.py +8 -5
  3. great_expectations_cloud/agent/actions/agent_action.py +21 -6
  4. great_expectations_cloud/agent/actions/draft_datasource_config_action.py +45 -24
  5. great_expectations_cloud/agent/actions/generate_data_quality_check_expectations_action.py +557 -0
  6. great_expectations_cloud/agent/actions/list_asset_names.py +65 -0
  7. great_expectations_cloud/agent/actions/run_checkpoint.py +74 -27
  8. great_expectations_cloud/agent/actions/run_metric_list_action.py +11 -5
  9. great_expectations_cloud/agent/actions/run_scheduled_checkpoint.py +67 -0
  10. great_expectations_cloud/agent/actions/run_window_checkpoint.py +66 -0
  11. great_expectations_cloud/agent/actions/utils.py +35 -0
  12. great_expectations_cloud/agent/agent.py +444 -101
  13. great_expectations_cloud/agent/cli.py +2 -2
  14. great_expectations_cloud/agent/config.py +19 -5
  15. great_expectations_cloud/agent/event_handler.py +49 -12
  16. great_expectations_cloud/agent/exceptions.py +9 -0
  17. great_expectations_cloud/agent/message_service/asyncio_rabbit_mq_client.py +80 -14
  18. great_expectations_cloud/agent/message_service/subscriber.py +8 -5
  19. great_expectations_cloud/agent/models.py +197 -20
  20. great_expectations_cloud/agent/utils.py +84 -0
  21. great_expectations_cloud/logging/logging_cfg.py +20 -4
  22. great_expectations_cloud/py.typed +0 -0
  23. {great_expectations_cloud-20240523.0.dev0.dist-info → great_expectations_cloud-20251124.0.dev1.dist-info}/METADATA +54 -46
  24. great_expectations_cloud-20251124.0.dev1.dist-info/RECORD +34 -0
  25. {great_expectations_cloud-20240523.0.dev0.dist-info → great_expectations_cloud-20251124.0.dev1.dist-info}/WHEEL +1 -1
  26. great_expectations_cloud/agent/actions/data_assistants/__init__.py +0 -8
  27. great_expectations_cloud/agent/actions/data_assistants/run_missingness_data_assistant.py +0 -45
  28. great_expectations_cloud/agent/actions/data_assistants/run_onboarding_data_assistant.py +0 -45
  29. great_expectations_cloud/agent/actions/data_assistants/utils.py +0 -123
  30. great_expectations_cloud/agent/actions/list_table_names.py +0 -76
  31. great_expectations_cloud-20240523.0.dev0.dist-info/RECORD +0 -32
  32. {great_expectations_cloud-20240523.0.dev0.dist-info → great_expectations_cloud-20251124.0.dev1.dist-info}/entry_points.txt +0 -0
  33. {great_expectations_cloud-20240523.0.dev0.dist-info → great_expectations_cloud-20251124.0.dev1.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,557 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from collections.abc import Sequence
5
+ from enum import Enum
6
+ from http import HTTPStatus
7
+ from typing import TYPE_CHECKING, Any, Final
8
+ from urllib.parse import urljoin
9
+ from uuid import UUID
10
+
11
+ import great_expectations.expectations as gx_expectations
12
+ from great_expectations.core.http import create_session
13
+ from great_expectations.exceptions import (
14
+ GXCloudError,
15
+ InvalidExpectationConfigurationError,
16
+ )
17
+ from great_expectations.expectations.metadata_types import (
18
+ DataQualityIssues,
19
+ FailureSeverity,
20
+ )
21
+ from great_expectations.expectations.window import Offset, Window
22
+ from great_expectations.experimental.metric_repository.batch_inspector import (
23
+ BatchInspector,
24
+ )
25
+ from great_expectations.experimental.metric_repository.cloud_data_store import (
26
+ CloudDataStore,
27
+ )
28
+ from great_expectations.experimental.metric_repository.metric_list_metric_retriever import (
29
+ MetricListMetricRetriever,
30
+ )
31
+ from great_expectations.experimental.metric_repository.metric_repository import (
32
+ MetricRepository,
33
+ )
34
+ from great_expectations.experimental.metric_repository.metrics import (
35
+ ColumnMetric,
36
+ MetricRun,
37
+ MetricTypes,
38
+ )
39
+ from typing_extensions import override
40
+
41
+ from great_expectations_cloud.agent.actions import ActionResult, AgentAction
42
+ from great_expectations_cloud.agent.event_handler import register_event_action
43
+ from great_expectations_cloud.agent.exceptions import GXAgentError
44
+ from great_expectations_cloud.agent.models import (
45
+ CreatedResource,
46
+ DomainContext,
47
+ GenerateDataQualityCheckExpectationsEvent,
48
+ )
49
+ from great_expectations_cloud.agent.utils import (
50
+ TriangularInterpolationOptions,
51
+ param_safe_unique_id,
52
+ triangular_interpolation,
53
+ )
54
+
55
+ if TYPE_CHECKING:
56
+ from great_expectations.core.suite_parameters import SuiteParameterDict
57
+ from great_expectations.data_context import CloudDataContext
58
+ from great_expectations.datasource.fluent import DataAsset
59
+
60
+ LOGGER: Final[logging.Logger] = logging.getLogger(__name__)
61
+ LOGGER.setLevel(logging.DEBUG)
62
+
63
+
64
+ class ExpectationConstraintFunction(str, Enum):
65
+ """Expectation constraint functions."""
66
+
67
+ FORECAST = "forecast"
68
+ MEAN = "mean"
69
+
70
+
71
+ class PartialGenerateDataQualityCheckExpectationError(GXAgentError):
72
+ def __init__(self, assets_with_errors: list[str], assets_attempted: int):
73
+ message_header = f"Unable to autogenerate expectations for {len(assets_with_errors)} of {assets_attempted} Data Assets."
74
+ errors = ", ".join(assets_with_errors)
75
+ message_footer = "Check your connection details, delete and recreate these Data Assets."
76
+ message = f"{message_header}\n\u2022 {errors}\n{message_footer}"
77
+ super().__init__(message)
78
+
79
+
80
+ class GenerateDataQualityCheckExpectationsAction(
81
+ AgentAction[GenerateDataQualityCheckExpectationsEvent]
82
+ ):
83
+ def __init__( # noqa: PLR0913 # Refactor opportunity
84
+ self,
85
+ context: CloudDataContext,
86
+ base_url: str,
87
+ domain_context: DomainContext,
88
+ auth_key: str,
89
+ metric_repository: MetricRepository | None = None,
90
+ batch_inspector: BatchInspector | None = None,
91
+ ):
92
+ super().__init__(
93
+ context=context, base_url=base_url, domain_context=domain_context, auth_key=auth_key
94
+ )
95
+ self._metric_repository = metric_repository or MetricRepository(
96
+ data_store=CloudDataStore(self._context)
97
+ )
98
+ self._batch_inspector = batch_inspector or BatchInspector(
99
+ context, [MetricListMetricRetriever(self._context)]
100
+ )
101
+
102
+ @override
103
+ def run(self, event: GenerateDataQualityCheckExpectationsEvent, id: str) -> ActionResult:
104
+ created_resources: list[CreatedResource] = []
105
+ assets_with_errors: list[str] = []
106
+ selected_dqis: Sequence[DataQualityIssues] = event.selected_data_quality_issues or []
107
+ created_via: str | None = event.created_via or None
108
+ for asset_name in event.data_assets:
109
+ try:
110
+ data_asset = self._retrieve_asset_from_asset_name(event, asset_name)
111
+
112
+ metric_run, metric_run_id = self._get_metrics(data_asset)
113
+ created_resources.append(
114
+ CreatedResource(resource_id=str(metric_run_id), type="MetricRun")
115
+ )
116
+
117
+ if selected_dqis:
118
+ pre_existing_anomaly_detection_coverage = (
119
+ self._get_current_anomaly_detection_coverage(data_asset.id)
120
+ )
121
+
122
+ if self._should_add_volume_change_detection_coverage(
123
+ selected_data_quality_issues=selected_dqis,
124
+ pre_existing_anomaly_detection_coverage=pre_existing_anomaly_detection_coverage,
125
+ ):
126
+ volume_change_expectation_id = self._add_volume_change_expectation(
127
+ asset_id=data_asset.id,
128
+ use_forecast=event.use_forecast,
129
+ created_via=created_via,
130
+ )
131
+ created_resources.append(
132
+ CreatedResource(
133
+ resource_id=str(volume_change_expectation_id), type="Expectation"
134
+ )
135
+ )
136
+
137
+ if self._should_add_schema_change_detection_coverage(
138
+ selected_data_quality_issues=selected_dqis,
139
+ pre_existing_anomaly_detection_coverage=pre_existing_anomaly_detection_coverage,
140
+ ):
141
+ schema_change_expectation_id = self._add_schema_change_expectation(
142
+ metric_run=metric_run, asset_id=data_asset.id, created_via=created_via
143
+ )
144
+ created_resources.append(
145
+ CreatedResource(
146
+ resource_id=str(schema_change_expectation_id), type="Expectation"
147
+ )
148
+ )
149
+
150
+ if DataQualityIssues.COMPLETENESS in selected_dqis:
151
+ pre_existing_completeness_change_expectations = (
152
+ pre_existing_anomaly_detection_coverage.get(
153
+ DataQualityIssues.COMPLETENESS, []
154
+ )
155
+ )
156
+ completeness_change_expectation_ids = self._add_completeness_change_expectations(
157
+ metric_run=metric_run,
158
+ asset_id=data_asset.id,
159
+ pre_existing_completeness_change_expectations=pre_existing_completeness_change_expectations,
160
+ created_via=created_via,
161
+ use_forecast=event.use_forecast,
162
+ )
163
+ for exp_id in completeness_change_expectation_ids:
164
+ created_resources.append(
165
+ CreatedResource(resource_id=str(exp_id), type="Expectation")
166
+ )
167
+
168
+ except Exception as e:
169
+ LOGGER.exception("Failed to generate expectations for %s: %s", asset_name, str(e)) # noqa: TRY401
170
+ assets_with_errors.append(asset_name)
171
+
172
+ if assets_with_errors:
173
+ raise PartialGenerateDataQualityCheckExpectationError(
174
+ assets_with_errors=assets_with_errors,
175
+ assets_attempted=len(event.data_assets),
176
+ )
177
+
178
+ return ActionResult(
179
+ id=id,
180
+ type=event.type,
181
+ created_resources=created_resources,
182
+ )
183
+
184
+ def _retrieve_asset_from_asset_name(
185
+ self, event: GenerateDataQualityCheckExpectationsEvent, asset_name: str
186
+ ) -> DataAsset[Any, Any]:
187
+ try:
188
+ datasource = self._context.data_sources.get(event.datasource_name)
189
+ data_asset = datasource.get_asset(asset_name)
190
+ data_asset.test_connection() # raises `TestConnectionError` on failure
191
+
192
+ except Exception as e:
193
+ # TODO - see if this can be made more specific
194
+ raise RuntimeError(f"Failed to retrieve asset: {e}") from e # noqa: TRY003 # want to keep this informative for now
195
+
196
+ return data_asset # type: ignore[no-any-return] # unable to narrow types strictly based on names
197
+
198
+ def _get_metrics(self, data_asset: DataAsset[Any, Any]) -> tuple[MetricRun, UUID]:
199
+ batch_request = data_asset.build_batch_request()
200
+ if data_asset.id is None:
201
+ raise RuntimeError("DataAsset.id is None") # noqa: TRY003
202
+ metric_run = self._batch_inspector.compute_metric_list_run(
203
+ data_asset_id=data_asset.id,
204
+ batch_request=batch_request,
205
+ metric_list=[
206
+ MetricTypes.TABLE_COLUMNS,
207
+ MetricTypes.TABLE_COLUMN_TYPES,
208
+ MetricTypes.COLUMN_NON_NULL_COUNT,
209
+ MetricTypes.TABLE_ROW_COUNT,
210
+ ],
211
+ )
212
+ metric_run_id = self._metric_repository.add_metric_run(metric_run)
213
+ # Note: This exception is raised after the metric run is added to the repository so that
214
+ # the user can still access any computed metrics even if one of the metrics fails.
215
+ self._raise_on_any_metric_exception(metric_run)
216
+
217
+ return metric_run, metric_run_id
218
+
219
+ def _get_current_anomaly_detection_coverage(
220
+ self, data_asset_id: UUID | None
221
+ ) -> dict[DataQualityIssues, list[dict[Any, Any]]]:
222
+ """
223
+ This function returns a dict mapping Data Quality Issues to a list of ExpectationConfiguration dicts.
224
+ """
225
+ url = urljoin(
226
+ base=self._base_url,
227
+ url=f"/api/v1/organizations/{self._domain_context.organization_id}/workspaces/{self._domain_context.workspace_id}/expectations/",
228
+ )
229
+ with create_session(access_token=self._auth_key) as session:
230
+ response = session.get(
231
+ url=url,
232
+ params={"anomaly_detection": str(True), "data_asset_id": str(data_asset_id)},
233
+ )
234
+
235
+ if not response.ok:
236
+ raise GXCloudError(
237
+ message=f"GenerateDataQualityCheckExpectationsAction encountered an error while connecting to GX Cloud. "
238
+ f"Unable to retrieve Anomaly Detection Expectations for Asset with ID={data_asset_id}.",
239
+ response=response,
240
+ )
241
+ data = response.json()
242
+ try:
243
+ return data["data"] # type: ignore[no-any-return]
244
+
245
+ except KeyError as e:
246
+ raise GXCloudError(
247
+ message="Malformed response received from GX Cloud",
248
+ response=response,
249
+ ) from e
250
+
251
+ def _should_add_volume_change_detection_coverage(
252
+ self,
253
+ selected_data_quality_issues: Sequence[DataQualityIssues],
254
+ pre_existing_anomaly_detection_coverage: dict[
255
+ DataQualityIssues, list[dict[Any, Any]]
256
+ ], # list of ExpectationConfiguration dicts
257
+ ) -> bool:
258
+ return (
259
+ DataQualityIssues.VOLUME in selected_data_quality_issues
260
+ and len(pre_existing_anomaly_detection_coverage.get(DataQualityIssues.VOLUME, [])) == 0
261
+ )
262
+
263
+ def _should_add_schema_change_detection_coverage(
264
+ self,
265
+ selected_data_quality_issues: Sequence[DataQualityIssues],
266
+ pre_existing_anomaly_detection_coverage: dict[
267
+ DataQualityIssues, list[dict[Any, Any]]
268
+ ], # list of ExpectationConfiguration dicts
269
+ ) -> bool:
270
+ return (
271
+ DataQualityIssues.SCHEMA in selected_data_quality_issues
272
+ and len(pre_existing_anomaly_detection_coverage.get(DataQualityIssues.SCHEMA, [])) == 0
273
+ )
274
+
275
+ def _add_volume_change_expectation(
276
+ self, asset_id: UUID | None, use_forecast: bool, created_via: str | None
277
+ ) -> UUID:
278
+ unique_id = param_safe_unique_id(16)
279
+ lower_bound_param_name = f"{unique_id}_min_value_min"
280
+ upper_bound_param_name = f"{unique_id}_max_value_max"
281
+ min_value: SuiteParameterDict[str, str] | None = {"$PARAMETER": lower_bound_param_name}
282
+ max_value: SuiteParameterDict[str, str] | None = {"$PARAMETER": upper_bound_param_name}
283
+ windows = []
284
+ strict_min = False
285
+ strict_max = False
286
+
287
+ if use_forecast:
288
+ windows += [
289
+ Window(
290
+ constraint_fn=ExpectationConstraintFunction.FORECAST,
291
+ parameter_name=lower_bound_param_name,
292
+ range=1,
293
+ offset=Offset(positive=0.0, negative=0.0),
294
+ strict=True,
295
+ ),
296
+ Window(
297
+ constraint_fn=ExpectationConstraintFunction.FORECAST,
298
+ parameter_name=upper_bound_param_name,
299
+ range=1,
300
+ offset=Offset(positive=0.0, negative=0.0),
301
+ strict=True,
302
+ ),
303
+ ]
304
+ else:
305
+ windows += [
306
+ Window(
307
+ constraint_fn=ExpectationConstraintFunction.MEAN,
308
+ parameter_name=lower_bound_param_name,
309
+ range=1,
310
+ offset=Offset(positive=0.0, negative=0.0),
311
+ strict=True,
312
+ )
313
+ ]
314
+ max_value = None
315
+ strict_min = True
316
+
317
+ expectation = gx_expectations.ExpectTableRowCountToBeBetween(
318
+ windows=windows,
319
+ strict_min=strict_min,
320
+ strict_max=strict_max,
321
+ min_value=min_value,
322
+ max_value=max_value,
323
+ severity=FailureSeverity.WARNING,
324
+ )
325
+ expectation_id = self._create_expectation_for_asset(
326
+ expectation=expectation, asset_id=asset_id, created_via=created_via
327
+ )
328
+ return expectation_id
329
+
330
+ def _add_schema_change_expectation(
331
+ self, metric_run: MetricRun, asset_id: UUID | None, created_via: str | None
332
+ ) -> UUID:
333
+ # Find the TABLE_COLUMNS metric by type instead of assuming it's at position 0
334
+ table_columns_metric = next(
335
+ (
336
+ metric
337
+ for metric in metric_run.metrics
338
+ if metric.metric_name == MetricTypes.TABLE_COLUMNS
339
+ ),
340
+ None,
341
+ )
342
+ if not table_columns_metric:
343
+ raise RuntimeError("missing TABLE_COLUMNS metric") # noqa: TRY003
344
+
345
+ expectation = gx_expectations.ExpectTableColumnsToMatchSet(
346
+ column_set=table_columns_metric.value,
347
+ severity=FailureSeverity.WARNING,
348
+ )
349
+ expectation_id = self._create_expectation_for_asset(
350
+ expectation=expectation, asset_id=asset_id, created_via=created_via
351
+ )
352
+ return expectation_id
353
+
354
+ def _add_completeness_change_expectations(
355
+ self,
356
+ metric_run: MetricRun,
357
+ asset_id: UUID | None,
358
+ pre_existing_completeness_change_expectations: list[
359
+ dict[Any, Any]
360
+ ], # list of ExpectationConfiguration dicts
361
+ created_via: str | None,
362
+ use_forecast: bool = False,
363
+ ) -> list[UUID]:
364
+ table_row_count = next(
365
+ metric
366
+ for metric in metric_run.metrics
367
+ if metric.metric_name == MetricTypes.TABLE_ROW_COUNT
368
+ )
369
+
370
+ if not table_row_count:
371
+ raise RuntimeError("missing TABLE_ROW_COUNT metric") # noqa: TRY003
372
+
373
+ column_null_values_metric: list[ColumnMetric[int]] = [
374
+ metric
375
+ for metric in metric_run.metrics
376
+ if isinstance(metric, ColumnMetric)
377
+ and metric.metric_name == MetricTypes.COLUMN_NON_NULL_COUNT
378
+ ]
379
+
380
+ if not column_null_values_metric or len(column_null_values_metric) == 0:
381
+ raise RuntimeError("missing COLUMN_NON_NULL_COUNT metrics") # noqa: TRY003
382
+
383
+ expectation_ids = []
384
+ # Single-expectation approach using ExpectColumnProportionOfNonNullValuesToBeBetween
385
+ # Expectations are only added to columns that do not have coverage
386
+ columns_missing_completeness_coverage = self._get_columns_missing_completeness_coverage(
387
+ column_null_values_metric=column_null_values_metric,
388
+ pre_existing_completeness_change_expectations=pre_existing_completeness_change_expectations,
389
+ )
390
+ for column in columns_missing_completeness_coverage:
391
+ column_name = column.column
392
+ non_null_count = column.value
393
+ row_count = table_row_count.value
394
+ expectation: gx_expectations.Expectation
395
+
396
+ # Single-expectation approach using ExpectColumnProportionOfNonNullValuesToBeBetween
397
+ unique_id = param_safe_unique_id(16)
398
+ min_param_name = f"{unique_id}_proportion_min"
399
+ max_param_name = f"{unique_id}_proportion_max"
400
+
401
+ # Calculate non-null proportion
402
+ non_null_proportion = non_null_count / row_count if row_count > 0 else 0
403
+
404
+ if use_forecast:
405
+ expectation = gx_expectations.ExpectColumnProportionOfNonNullValuesToBeBetween(
406
+ windows=[
407
+ Window(
408
+ constraint_fn=ExpectationConstraintFunction.FORECAST,
409
+ parameter_name=min_param_name,
410
+ range=1,
411
+ offset=Offset(positive=0.0, negative=0.0),
412
+ strict=True,
413
+ ),
414
+ Window(
415
+ constraint_fn=ExpectationConstraintFunction.FORECAST,
416
+ parameter_name=max_param_name,
417
+ range=1,
418
+ offset=Offset(positive=0.0, negative=0.0),
419
+ strict=True,
420
+ ),
421
+ ],
422
+ column=column_name,
423
+ min_value={"$PARAMETER": min_param_name},
424
+ max_value={"$PARAMETER": max_param_name},
425
+ severity=FailureSeverity.WARNING,
426
+ )
427
+ elif non_null_proportion == 0:
428
+ expectation = gx_expectations.ExpectColumnProportionOfNonNullValuesToBeBetween(
429
+ column=column_name,
430
+ max_value=0,
431
+ severity=FailureSeverity.WARNING,
432
+ )
433
+ elif non_null_proportion == 1:
434
+ expectation = gx_expectations.ExpectColumnProportionOfNonNullValuesToBeBetween(
435
+ column=column_name,
436
+ min_value=1,
437
+ severity=FailureSeverity.WARNING,
438
+ )
439
+ else:
440
+ # Use triangular interpolation to compute min/max values
441
+ interpolated_offset = self._compute_triangular_interpolation_offset(
442
+ value=non_null_proportion, input_range=(0.0, 1.0)
443
+ )
444
+
445
+ expectation = gx_expectations.ExpectColumnProportionOfNonNullValuesToBeBetween(
446
+ windows=[
447
+ Window(
448
+ constraint_fn=ExpectationConstraintFunction.MEAN,
449
+ parameter_name=min_param_name,
450
+ range=5,
451
+ offset=Offset(
452
+ positive=interpolated_offset, negative=interpolated_offset
453
+ ),
454
+ strict=False,
455
+ ),
456
+ Window(
457
+ constraint_fn=ExpectationConstraintFunction.MEAN,
458
+ parameter_name=max_param_name,
459
+ range=5,
460
+ offset=Offset(
461
+ positive=interpolated_offset, negative=interpolated_offset
462
+ ),
463
+ strict=False,
464
+ ),
465
+ ],
466
+ column=column_name,
467
+ min_value={"$PARAMETER": min_param_name},
468
+ max_value={"$PARAMETER": max_param_name},
469
+ severity=FailureSeverity.WARNING,
470
+ )
471
+
472
+ expectation_id = self._create_expectation_for_asset(
473
+ expectation=expectation, asset_id=asset_id, created_via=created_via
474
+ )
475
+ expectation_ids.append(expectation_id)
476
+
477
+ return expectation_ids
478
+
479
+ def _get_columns_missing_completeness_coverage(
480
+ self,
481
+ column_null_values_metric: list[ColumnMetric[int]],
482
+ pre_existing_completeness_change_expectations: list[
483
+ dict[Any, Any]
484
+ ], # list of ExpectationConfiguration dicts
485
+ ) -> list[ColumnMetric[int]]:
486
+ try:
487
+ columns_with_completeness_coverage = {
488
+ expectation.get("config").get("kwargs").get("column") # type: ignore[union-attr]
489
+ for expectation in pre_existing_completeness_change_expectations
490
+ }
491
+ except AttributeError as e:
492
+ raise InvalidExpectationConfigurationError(str(e)) from e
493
+ columns_without_completeness_coverage = [
494
+ column
495
+ for column in column_null_values_metric
496
+ if column.column not in columns_with_completeness_coverage
497
+ ]
498
+ return columns_without_completeness_coverage
499
+
500
+ def _compute_triangular_interpolation_offset(
501
+ self, value: float, input_range: tuple[float, float]
502
+ ) -> float:
503
+ """
504
+ Compute triangular interpolation offset for expectation windows.
505
+
506
+ Args:
507
+ value: The input value to interpolate
508
+ input_range: The input range as (min, max) tuple
509
+
510
+ Returns:
511
+ The computed interpolation offset
512
+ """
513
+ options = TriangularInterpolationOptions(
514
+ input_range=input_range,
515
+ output_range=(0, 0.1),
516
+ round_precision=5,
517
+ )
518
+ return max(0.0001, round(triangular_interpolation(value, options), 5))
519
+
520
+ def _create_expectation_for_asset(
521
+ self,
522
+ expectation: gx_expectations.Expectation,
523
+ asset_id: UUID | None,
524
+ created_via: str | None,
525
+ ) -> UUID:
526
+ url = urljoin(
527
+ base=self._base_url,
528
+ url=f"/api/v1/organizations/{self._domain_context.organization_id}/workspaces/{self._domain_context.workspace_id}/expectations/{asset_id}",
529
+ )
530
+
531
+ expectation_payload = expectation.configuration.to_json_dict()
532
+ expectation_payload["autogenerated"] = True
533
+ if created_via is not None:
534
+ expectation_payload["created_via"] = created_via
535
+
536
+ # Backend expects `expectation_type` instead of `type`:
537
+ expectation_type = expectation_payload.pop("type")
538
+ expectation_payload["expectation_type"] = expectation_type
539
+
540
+ with create_session(access_token=self._auth_key) as session:
541
+ response = session.post(url=url, json=expectation_payload)
542
+
543
+ if response.status_code != HTTPStatus.CREATED:
544
+ message = f"Failed to add autogenerated expectation: {expectation_type}"
545
+ raise GXAgentError(message)
546
+ return UUID(response.json()["data"]["id"])
547
+
548
+ def _raise_on_any_metric_exception(self, metric_run: MetricRun) -> None:
549
+ if any(metric.exception for metric in metric_run.metrics):
550
+ raise RuntimeError( # noqa: TRY003 # one off error
551
+ "One or more metrics failed to compute."
552
+ )
553
+
554
+
555
+ register_event_action(
556
+ "1", GenerateDataQualityCheckExpectationsEvent, GenerateDataQualityCheckExpectationsAction
557
+ )
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+
3
+ from urllib.parse import urljoin
4
+
5
+ from great_expectations.core.http import create_session
6
+ from great_expectations.datasource.fluent import SQLDatasource
7
+ from great_expectations.exceptions import GXCloudError
8
+ from typing_extensions import override
9
+
10
+ from great_expectations_cloud.agent.actions.agent_action import (
11
+ ActionResult,
12
+ AgentAction,
13
+ )
14
+ from great_expectations_cloud.agent.actions.utils import get_asset_names
15
+ from great_expectations_cloud.agent.event_handler import register_event_action
16
+ from great_expectations_cloud.agent.models import ListAssetNamesEvent
17
+
18
+
19
+ class ListAssetNamesAction(AgentAction[ListAssetNamesEvent]):
20
+ # TODO: New actions need to be created that are compatible with GX v1 and registered for v1.
21
+ # This action is registered for v0, see register_event_action()
22
+
23
+ @override
24
+ def run(self, event: ListAssetNamesEvent, id: str) -> ActionResult:
25
+ datasource_name: str = event.datasource_name
26
+ datasource = self._context.data_sources.get(name=datasource_name)
27
+ if not isinstance(datasource, SQLDatasource):
28
+ raise TypeError( # noqa: TRY003 # one off error
29
+ f"This operation requires a SQL Data Source but got {type(datasource).__name__}."
30
+ )
31
+
32
+ asset_names = get_asset_names(datasource)
33
+
34
+ self._add_or_update_asset_names_list(
35
+ datasource_id=str(datasource.id),
36
+ asset_names=asset_names,
37
+ )
38
+
39
+ return ActionResult(
40
+ id=id,
41
+ type=event.type,
42
+ created_resources=[],
43
+ )
44
+
45
+ def _add_or_update_asset_names_list(self, datasource_id: str, asset_names: list[str]) -> None:
46
+ with create_session(access_token=self._auth_key) as session:
47
+ url = urljoin(
48
+ base=self._base_url,
49
+ url=f"/api/v1/organizations/{self._domain_context.organization_id}/workspaces/{self._domain_context.workspace_id}/table-names/{datasource_id}",
50
+ )
51
+ response = session.put(
52
+ url=url,
53
+ json={"data": {"table_names": asset_names}},
54
+ )
55
+ if response.status_code != 200: # noqa: PLR2004
56
+ raise GXCloudError(
57
+ message=f"ListTableNamesAction encountered an error while connecting to GX Cloud. "
58
+ f"Unable to update "
59
+ f"table_names for Data Source with ID"
60
+ f"={datasource_id}.",
61
+ response=response,
62
+ )
63
+
64
+
65
+ register_event_action("1", ListAssetNamesEvent, ListAssetNamesAction)