acryl-datahub-cloud 0.3.11rc0__py3-none-any.whl → 0.3.16.1rc0__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 acryl-datahub-cloud might be problematic. Click here for more details.
- acryl_datahub_cloud/_codegen_config.json +1 -1
- acryl_datahub_cloud/acryl_cs_issues/models.py +5 -3
- acryl_datahub_cloud/action_request/action_request_owner_source.py +36 -6
- acryl_datahub_cloud/datahub_forms_notifications/__init__.py +0 -0
- acryl_datahub_cloud/datahub_forms_notifications/forms_notifications_source.py +569 -0
- acryl_datahub_cloud/datahub_forms_notifications/get_feature_flag.gql +7 -0
- acryl_datahub_cloud/datahub_forms_notifications/get_search_results_total.gql +14 -0
- acryl_datahub_cloud/datahub_forms_notifications/query.py +17 -0
- acryl_datahub_cloud/datahub_forms_notifications/scroll_forms_for_notification.gql +29 -0
- acryl_datahub_cloud/datahub_forms_notifications/send_form_notification_request.gql +5 -0
- acryl_datahub_cloud/datahub_reporting/datahub_dataset.py +37 -13
- acryl_datahub_cloud/datahub_reporting/datahub_form_reporting.py +55 -24
- acryl_datahub_cloud/datahub_reporting/extract_graph.py +4 -3
- acryl_datahub_cloud/datahub_reporting/extract_sql.py +242 -51
- acryl_datahub_cloud/datahub_reporting/forms.py +1 -1
- acryl_datahub_cloud/datahub_reporting/forms_config.py +3 -2
- acryl_datahub_cloud/datahub_restore/source.py +3 -2
- acryl_datahub_cloud/datahub_usage_reporting/excluded.py +94 -0
- acryl_datahub_cloud/datahub_usage_reporting/query_builder.py +48 -8
- acryl_datahub_cloud/datahub_usage_reporting/usage_feature_reporter.py +518 -77
- acryl_datahub_cloud/elasticsearch/graph_service.py +76 -14
- acryl_datahub_cloud/graphql_utils.py +64 -0
- acryl_datahub_cloud/lineage_features/source.py +555 -49
- acryl_datahub_cloud/metadata/_urns/urn_defs.py +2296 -1900
- acryl_datahub_cloud/metadata/com/linkedin/pegasus2avro/actionworkflow/__init__.py +53 -0
- acryl_datahub_cloud/metadata/com/linkedin/pegasus2avro/anomaly/__init__.py +2 -0
- acryl_datahub_cloud/metadata/com/linkedin/pegasus2avro/application/__init__.py +19 -0
- acryl_datahub_cloud/metadata/com/linkedin/pegasus2avro/assertion/__init__.py +4 -2
- acryl_datahub_cloud/metadata/com/linkedin/pegasus2avro/common/__init__.py +6 -0
- acryl_datahub_cloud/metadata/com/linkedin/pegasus2avro/conversation/__init__.py +29 -0
- acryl_datahub_cloud/metadata/com/linkedin/pegasus2avro/event/notification/settings/__init__.py +2 -0
- acryl_datahub_cloud/metadata/com/linkedin/pegasus2avro/execution/__init__.py +2 -0
- acryl_datahub_cloud/metadata/com/linkedin/pegasus2avro/file/__init__.py +19 -0
- acryl_datahub_cloud/metadata/com/linkedin/pegasus2avro/form/__init__.py +8 -0
- acryl_datahub_cloud/metadata/com/linkedin/pegasus2avro/identity/__init__.py +8 -0
- acryl_datahub_cloud/metadata/com/linkedin/pegasus2avro/knowledge/__init__.py +33 -0
- acryl_datahub_cloud/metadata/com/linkedin/pegasus2avro/logical/__init__.py +15 -0
- acryl_datahub_cloud/metadata/com/linkedin/pegasus2avro/metadata/key/__init__.py +12 -0
- acryl_datahub_cloud/metadata/com/linkedin/pegasus2avro/metadata/search/features/__init__.py +2 -0
- acryl_datahub_cloud/metadata/com/linkedin/pegasus2avro/module/__init__.py +31 -0
- acryl_datahub_cloud/metadata/com/linkedin/pegasus2avro/notification/__init__.py +19 -0
- acryl_datahub_cloud/metadata/com/linkedin/pegasus2avro/platform/event/v1/__init__.py +4 -0
- acryl_datahub_cloud/metadata/com/linkedin/pegasus2avro/role/__init__.py +2 -0
- acryl_datahub_cloud/metadata/com/linkedin/pegasus2avro/settings/asset/__init__.py +19 -0
- acryl_datahub_cloud/metadata/com/linkedin/pegasus2avro/settings/global/__init__.py +28 -0
- acryl_datahub_cloud/metadata/com/linkedin/pegasus2avro/template/__init__.py +31 -0
- acryl_datahub_cloud/metadata/schema.avsc +25091 -20557
- acryl_datahub_cloud/metadata/schema_classes.py +29269 -23863
- acryl_datahub_cloud/metadata/schemas/ActionRequestInfo.avsc +235 -2
- acryl_datahub_cloud/metadata/schemas/ActionWorkflowInfo.avsc +683 -0
- acryl_datahub_cloud/metadata/schemas/ActionWorkflowKey.avsc +21 -0
- acryl_datahub_cloud/metadata/schemas/Actors.avsc +38 -1
- acryl_datahub_cloud/metadata/schemas/ApplicationKey.avsc +31 -0
- acryl_datahub_cloud/metadata/schemas/ApplicationProperties.avsc +75 -0
- acryl_datahub_cloud/metadata/schemas/Applications.avsc +38 -0
- acryl_datahub_cloud/metadata/schemas/AssertionAnalyticsRunEvent.avsc +353 -215
- acryl_datahub_cloud/metadata/schemas/AssertionInfo.avsc +147 -20
- acryl_datahub_cloud/metadata/schemas/AssertionKey.avsc +1 -1
- acryl_datahub_cloud/metadata/schemas/AssertionRunEvent.avsc +166 -21
- acryl_datahub_cloud/metadata/schemas/{AssertionSummary.avsc → AssertionRunSummary.avsc} +15 -2
- acryl_datahub_cloud/metadata/schemas/AssertionsSummary.avsc +54 -0
- acryl_datahub_cloud/metadata/schemas/AssetSettings.avsc +63 -0
- acryl_datahub_cloud/metadata/schemas/BusinessAttributeInfo.avsc +7 -3
- acryl_datahub_cloud/metadata/schemas/ChartInfo.avsc +20 -6
- acryl_datahub_cloud/metadata/schemas/ChartKey.avsc +1 -0
- acryl_datahub_cloud/metadata/schemas/ConstraintInfo.avsc +12 -1
- acryl_datahub_cloud/metadata/schemas/ContainerKey.avsc +1 -0
- acryl_datahub_cloud/metadata/schemas/ContainerProperties.avsc +16 -5
- acryl_datahub_cloud/metadata/schemas/CorpGroupEditableInfo.avsc +2 -1
- acryl_datahub_cloud/metadata/schemas/CorpGroupInfo.avsc +7 -3
- acryl_datahub_cloud/metadata/schemas/CorpGroupKey.avsc +2 -1
- acryl_datahub_cloud/metadata/schemas/CorpGroupSettings.avsc +127 -2
- acryl_datahub_cloud/metadata/schemas/CorpUserEditableInfo.avsc +1 -1
- acryl_datahub_cloud/metadata/schemas/CorpUserInfo.avsc +18 -2
- acryl_datahub_cloud/metadata/schemas/CorpUserInvitationStatus.avsc +106 -0
- acryl_datahub_cloud/metadata/schemas/CorpUserKey.avsc +4 -1
- acryl_datahub_cloud/metadata/schemas/CorpUserSettings.avsc +304 -2
- acryl_datahub_cloud/metadata/schemas/CorpUserUsageFeatures.avsc +86 -0
- acryl_datahub_cloud/metadata/schemas/DashboardInfo.avsc +11 -5
- acryl_datahub_cloud/metadata/schemas/DashboardKey.avsc +1 -0
- acryl_datahub_cloud/metadata/schemas/DataFlowInfo.avsc +15 -5
- acryl_datahub_cloud/metadata/schemas/DataFlowKey.avsc +1 -0
- acryl_datahub_cloud/metadata/schemas/DataHubAiConversationInfo.avsc +256 -0
- acryl_datahub_cloud/metadata/schemas/DataHubAiConversationKey.avsc +22 -0
- acryl_datahub_cloud/metadata/schemas/DataHubFileInfo.avsc +234 -0
- acryl_datahub_cloud/metadata/schemas/DataHubFileKey.avsc +22 -0
- acryl_datahub_cloud/metadata/schemas/DataHubIngestionSourceKey.avsc +2 -1
- acryl_datahub_cloud/metadata/schemas/DataHubPageModuleKey.avsc +21 -0
- acryl_datahub_cloud/metadata/schemas/DataHubPageModuleProperties.avsc +308 -0
- acryl_datahub_cloud/metadata/schemas/DataHubPageTemplateKey.avsc +21 -0
- acryl_datahub_cloud/metadata/schemas/DataHubPageTemplateProperties.avsc +251 -0
- acryl_datahub_cloud/metadata/schemas/DataHubPolicyInfo.avsc +12 -1
- acryl_datahub_cloud/metadata/schemas/DataJobInfo.avsc +13 -4
- acryl_datahub_cloud/metadata/schemas/DataJobInputOutput.avsc +8 -0
- acryl_datahub_cloud/metadata/schemas/DataJobKey.avsc +1 -0
- acryl_datahub_cloud/metadata/schemas/DataPlatformInfo.avsc +3 -1
- acryl_datahub_cloud/metadata/schemas/DataPlatformInstanceProperties.avsc +5 -2
- acryl_datahub_cloud/metadata/schemas/DataProcessKey.avsc +4 -0
- acryl_datahub_cloud/metadata/schemas/DataProductKey.avsc +2 -0
- acryl_datahub_cloud/metadata/schemas/DataProductProperties.avsc +6 -3
- acryl_datahub_cloud/metadata/schemas/DataTypeInfo.avsc +5 -0
- acryl_datahub_cloud/metadata/schemas/DatasetKey.avsc +10 -2
- acryl_datahub_cloud/metadata/schemas/DatasetProperties.avsc +12 -5
- acryl_datahub_cloud/metadata/schemas/DatasetUsageStatistics.avsc +8 -0
- acryl_datahub_cloud/metadata/schemas/DocumentInfo.avsc +407 -0
- acryl_datahub_cloud/metadata/schemas/DocumentKey.avsc +35 -0
- acryl_datahub_cloud/metadata/schemas/DocumentSettings.avsc +79 -0
- acryl_datahub_cloud/metadata/schemas/DomainKey.avsc +2 -0
- acryl_datahub_cloud/metadata/schemas/DomainProperties.avsc +7 -3
- acryl_datahub_cloud/metadata/schemas/EditableContainerProperties.avsc +2 -1
- acryl_datahub_cloud/metadata/schemas/EditableDashboardProperties.avsc +2 -1
- acryl_datahub_cloud/metadata/schemas/EditableDataFlowProperties.avsc +2 -1
- acryl_datahub_cloud/metadata/schemas/EditableDataJobProperties.avsc +2 -1
- acryl_datahub_cloud/metadata/schemas/EditableDatasetProperties.avsc +2 -1
- acryl_datahub_cloud/metadata/schemas/EditableERModelRelationshipProperties.avsc +2 -1
- acryl_datahub_cloud/metadata/schemas/EditableMLFeatureProperties.avsc +2 -1
- acryl_datahub_cloud/metadata/schemas/EditableMLFeatureTableProperties.avsc +2 -1
- acryl_datahub_cloud/metadata/schemas/EditableMLModelGroupProperties.avsc +2 -1
- acryl_datahub_cloud/metadata/schemas/EditableMLModelProperties.avsc +2 -1
- acryl_datahub_cloud/metadata/schemas/EditableNotebookProperties.avsc +2 -1
- acryl_datahub_cloud/metadata/schemas/EditableSchemaMetadata.avsc +4 -2
- acryl_datahub_cloud/metadata/schemas/EntityTypeInfo.avsc +5 -0
- acryl_datahub_cloud/metadata/schemas/ExecutionRequestArtifactsLocation.avsc +16 -0
- acryl_datahub_cloud/metadata/schemas/ExecutionRequestKey.avsc +2 -1
- acryl_datahub_cloud/metadata/schemas/FormAssignmentStatus.avsc +36 -0
- acryl_datahub_cloud/metadata/schemas/FormInfo.avsc +6 -0
- acryl_datahub_cloud/metadata/schemas/FormKey.avsc +3 -1
- acryl_datahub_cloud/metadata/schemas/FormNotifications.avsc +69 -0
- acryl_datahub_cloud/metadata/schemas/FormSettings.avsc +30 -0
- acryl_datahub_cloud/metadata/schemas/GlobalSettingsInfo.avsc +416 -0
- acryl_datahub_cloud/metadata/schemas/GlobalTags.avsc +2 -1
- acryl_datahub_cloud/metadata/schemas/GlossaryNodeInfo.avsc +3 -1
- acryl_datahub_cloud/metadata/schemas/GlossaryNodeKey.avsc +1 -0
- acryl_datahub_cloud/metadata/schemas/GlossaryTermInfo.avsc +3 -1
- acryl_datahub_cloud/metadata/schemas/GlossaryTermKey.avsc +2 -0
- acryl_datahub_cloud/metadata/schemas/IcebergWarehouseInfo.avsc +4 -0
- acryl_datahub_cloud/metadata/schemas/IncidentActivityEvent.avsc +3 -3
- acryl_datahub_cloud/metadata/schemas/IncidentInfo.avsc +3 -3
- acryl_datahub_cloud/metadata/schemas/InferredMetadata.avsc +71 -1
- acryl_datahub_cloud/metadata/schemas/InputFields.avsc +2 -1
- acryl_datahub_cloud/metadata/schemas/InviteToken.avsc +26 -0
- acryl_datahub_cloud/metadata/schemas/LineageFeatures.avsc +67 -42
- acryl_datahub_cloud/metadata/schemas/LogicalParent.avsc +145 -0
- acryl_datahub_cloud/metadata/schemas/MLFeatureKey.avsc +4 -1
- acryl_datahub_cloud/metadata/schemas/MLFeatureTableKey.avsc +4 -1
- acryl_datahub_cloud/metadata/schemas/MLModelDeploymentKey.avsc +7 -1
- acryl_datahub_cloud/metadata/schemas/MLModelGroupKey.avsc +9 -1
- acryl_datahub_cloud/metadata/schemas/MLModelKey.avsc +9 -1
- acryl_datahub_cloud/metadata/schemas/MLModelProperties.avsc +4 -2
- acryl_datahub_cloud/metadata/schemas/MLPrimaryKeyKey.avsc +4 -1
- acryl_datahub_cloud/metadata/schemas/MetadataChangeEvent.avsc +418 -97
- acryl_datahub_cloud/metadata/schemas/MetadataChangeLog.avsc +62 -44
- acryl_datahub_cloud/metadata/schemas/MetadataChangeProposal.avsc +61 -0
- acryl_datahub_cloud/metadata/schemas/MonitorAnomalyEvent.avsc +54 -9
- acryl_datahub_cloud/metadata/schemas/MonitorInfo.avsc +163 -23
- acryl_datahub_cloud/metadata/schemas/MonitorKey.avsc +9 -1
- acryl_datahub_cloud/metadata/schemas/MonitorSuiteInfo.avsc +128 -3
- acryl_datahub_cloud/metadata/schemas/NotebookInfo.avsc +5 -2
- acryl_datahub_cloud/metadata/schemas/NotebookKey.avsc +1 -0
- acryl_datahub_cloud/metadata/schemas/NotificationRequest.avsc +91 -4
- acryl_datahub_cloud/metadata/schemas/Operation.avsc +17 -0
- acryl_datahub_cloud/metadata/schemas/Ownership.avsc +71 -1
- acryl_datahub_cloud/metadata/schemas/QuerySubjects.avsc +2 -13
- acryl_datahub_cloud/metadata/schemas/RelationshipChangeEvent.avsc +215 -0
- acryl_datahub_cloud/metadata/schemas/RoleProperties.avsc +3 -1
- acryl_datahub_cloud/metadata/schemas/SchemaFieldInfo.avsc +3 -1
- acryl_datahub_cloud/metadata/schemas/SchemaFieldKey.avsc +3 -0
- acryl_datahub_cloud/metadata/schemas/SchemaMetadata.avsc +2 -1
- acryl_datahub_cloud/metadata/schemas/SemanticContent.avsc +123 -0
- acryl_datahub_cloud/metadata/schemas/StructuredProperties.avsc +69 -0
- acryl_datahub_cloud/metadata/schemas/StructuredPropertyDefinition.avsc +15 -4
- acryl_datahub_cloud/metadata/schemas/StructuredPropertySettings.avsc +9 -0
- acryl_datahub_cloud/metadata/schemas/SubscriptionInfo.avsc +136 -5
- acryl_datahub_cloud/metadata/schemas/SubscriptionKey.avsc +2 -1
- acryl_datahub_cloud/metadata/schemas/SystemMetadata.avsc +61 -0
- acryl_datahub_cloud/metadata/schemas/TagProperties.avsc +3 -1
- acryl_datahub_cloud/metadata/schemas/TestInfo.avsc +2 -1
- acryl_datahub_cloud/metadata/schemas/UpstreamLineage.avsc +9 -0
- acryl_datahub_cloud/metadata/schemas/UsageFeatures.avsc +10 -0
- acryl_datahub_cloud/notifications/__init__.py +0 -0
- acryl_datahub_cloud/notifications/notification_recipient_builder.py +399 -0
- acryl_datahub_cloud/sdk/__init__.py +69 -0
- acryl_datahub_cloud/sdk/assertion/__init__.py +58 -0
- acryl_datahub_cloud/sdk/assertion/assertion_base.py +779 -0
- acryl_datahub_cloud/sdk/assertion/column_metric_assertion.py +191 -0
- acryl_datahub_cloud/sdk/assertion/column_value_assertion.py +431 -0
- acryl_datahub_cloud/sdk/assertion/freshness_assertion.py +201 -0
- acryl_datahub_cloud/sdk/assertion/schema_assertion.py +268 -0
- acryl_datahub_cloud/sdk/assertion/smart_column_metric_assertion.py +212 -0
- acryl_datahub_cloud/sdk/assertion/smart_freshness_assertion.py +165 -0
- acryl_datahub_cloud/sdk/assertion/smart_sql_assertion.py +156 -0
- acryl_datahub_cloud/sdk/assertion/smart_volume_assertion.py +162 -0
- acryl_datahub_cloud/sdk/assertion/sql_assertion.py +273 -0
- acryl_datahub_cloud/sdk/assertion/types.py +20 -0
- acryl_datahub_cloud/sdk/assertion/volume_assertion.py +156 -0
- acryl_datahub_cloud/sdk/assertion_client/__init__.py +0 -0
- acryl_datahub_cloud/sdk/assertion_client/column_metric.py +545 -0
- acryl_datahub_cloud/sdk/assertion_client/column_value.py +617 -0
- acryl_datahub_cloud/sdk/assertion_client/freshness.py +371 -0
- acryl_datahub_cloud/sdk/assertion_client/helpers.py +166 -0
- acryl_datahub_cloud/sdk/assertion_client/schema.py +358 -0
- acryl_datahub_cloud/sdk/assertion_client/smart_column_metric.py +540 -0
- acryl_datahub_cloud/sdk/assertion_client/smart_freshness.py +373 -0
- acryl_datahub_cloud/sdk/assertion_client/smart_sql.py +411 -0
- acryl_datahub_cloud/sdk/assertion_client/smart_volume.py +380 -0
- acryl_datahub_cloud/sdk/assertion_client/sql.py +410 -0
- acryl_datahub_cloud/sdk/assertion_client/volume.py +446 -0
- acryl_datahub_cloud/sdk/assertion_input/__init__.py +0 -0
- acryl_datahub_cloud/sdk/assertion_input/assertion_input.py +1470 -0
- acryl_datahub_cloud/sdk/assertion_input/column_assertion_constants.py +114 -0
- acryl_datahub_cloud/sdk/assertion_input/column_assertion_utils.py +284 -0
- acryl_datahub_cloud/sdk/assertion_input/column_metric_assertion_input.py +759 -0
- acryl_datahub_cloud/sdk/assertion_input/column_metric_constants.py +109 -0
- acryl_datahub_cloud/sdk/assertion_input/column_value_assertion_input.py +810 -0
- acryl_datahub_cloud/sdk/assertion_input/freshness_assertion_input.py +305 -0
- acryl_datahub_cloud/sdk/assertion_input/schema_assertion_input.py +413 -0
- acryl_datahub_cloud/sdk/assertion_input/smart_column_metric_assertion_input.py +793 -0
- acryl_datahub_cloud/sdk/assertion_input/smart_freshness_assertion_input.py +218 -0
- acryl_datahub_cloud/sdk/assertion_input/smart_sql_assertion_input.py +181 -0
- acryl_datahub_cloud/sdk/assertion_input/smart_volume_assertion_input.py +189 -0
- acryl_datahub_cloud/sdk/assertion_input/sql_assertion_input.py +320 -0
- acryl_datahub_cloud/sdk/assertion_input/volume_assertion_input.py +635 -0
- acryl_datahub_cloud/sdk/assertions_client.py +1074 -0
- acryl_datahub_cloud/sdk/entities/__init__.py +0 -0
- acryl_datahub_cloud/sdk/entities/assertion.py +439 -0
- acryl_datahub_cloud/sdk/entities/monitor.py +291 -0
- acryl_datahub_cloud/sdk/entities/subscription.py +100 -0
- acryl_datahub_cloud/sdk/errors.py +34 -0
- acryl_datahub_cloud/sdk/resolver_client.py +42 -0
- acryl_datahub_cloud/sdk/subscription_client.py +737 -0
- {acryl_datahub_cloud-0.3.11rc0.dist-info → acryl_datahub_cloud-0.3.16.1rc0.dist-info}/METADATA +55 -49
- {acryl_datahub_cloud-0.3.11rc0.dist-info → acryl_datahub_cloud-0.3.16.1rc0.dist-info}/RECORD +235 -142
- {acryl_datahub_cloud-0.3.11rc0.dist-info → acryl_datahub_cloud-0.3.16.1rc0.dist-info}/WHEEL +1 -1
- {acryl_datahub_cloud-0.3.11rc0.dist-info → acryl_datahub_cloud-0.3.16.1rc0.dist-info}/entry_points.txt +1 -0
- acryl_datahub_cloud/_sdk_extras/__init__.py +0 -4
- acryl_datahub_cloud/_sdk_extras/assertion.py +0 -15
- acryl_datahub_cloud/_sdk_extras/assertions_client.py +0 -23
- {acryl_datahub_cloud-0.3.11rc0.dist-info → acryl_datahub_cloud-0.3.16.1rc0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
from tenacity import (
|
|
8
|
+
retry,
|
|
9
|
+
retry_if_exception_type,
|
|
10
|
+
stop_after_attempt,
|
|
11
|
+
wait_exponential,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from acryl_datahub_cloud.datahub_forms_notifications.query import (
|
|
15
|
+
GRAPHQL_GET_FEATURE_FLAG,
|
|
16
|
+
GRAPHQL_GET_SEARCH_RESULTS_TOTAL,
|
|
17
|
+
GRAPHQL_SCROLL_FORMS_FOR_NOTIFICATIONS,
|
|
18
|
+
GRAPHQL_SEND_FORM_NOTIFICATION_REQUEST,
|
|
19
|
+
)
|
|
20
|
+
from acryl_datahub_cloud.graphql_utils import parse_extra_properties_for_model
|
|
21
|
+
from acryl_datahub_cloud.notifications.notification_recipient_builder import (
|
|
22
|
+
NotificationRecipientBuilder,
|
|
23
|
+
)
|
|
24
|
+
from datahub.emitter.mcp import MetadataChangeProposalWrapper
|
|
25
|
+
from datahub.ingestion.api.common import PipelineContext
|
|
26
|
+
from datahub.ingestion.api.decorators import (
|
|
27
|
+
SupportStatus,
|
|
28
|
+
config_class,
|
|
29
|
+
platform_name,
|
|
30
|
+
support_status,
|
|
31
|
+
)
|
|
32
|
+
from datahub.ingestion.api.source import Source, SourceReport
|
|
33
|
+
from datahub.ingestion.api.workunit import MetadataWorkUnit
|
|
34
|
+
from datahub.ingestion.graph.client import DataHubGraph
|
|
35
|
+
from datahub.ingestion.graph.filters import RawSearchFilter
|
|
36
|
+
from datahub.metadata.schema_classes import (
|
|
37
|
+
FormInfoClass,
|
|
38
|
+
FormNotificationDetailsClass,
|
|
39
|
+
FormNotificationEntryClass,
|
|
40
|
+
FormNotificationsClass,
|
|
41
|
+
FormSettingsClass,
|
|
42
|
+
FormStateClass,
|
|
43
|
+
FormTypeClass,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
logger = logging.getLogger(__name__)
|
|
47
|
+
|
|
48
|
+
USER_URN_PREFIX = "urn:li:corpuser"
|
|
49
|
+
GROUP_URN_PREFIX = "urn:li:corpGroup"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class DataHubFormsNotificationsSourceConfig(BaseModel):
|
|
53
|
+
form_urns: Optional[List[str]] = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class DataHubDatasetSearchRow(BaseModel):
|
|
57
|
+
urn: str
|
|
58
|
+
owners: List[str] = []
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class DataHubFormsNotificationsSourceReport(SourceReport):
|
|
63
|
+
notifications_sent: int = (
|
|
64
|
+
0 # the number of recipients we sent notifications out for
|
|
65
|
+
)
|
|
66
|
+
forms_count: int = (
|
|
67
|
+
0 # the number of forms that we sent at least one nitification for
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@platform_name(id="datahub", platform_name="DataHub")
|
|
72
|
+
@config_class(DataHubFormsNotificationsSourceConfig)
|
|
73
|
+
@support_status(SupportStatus.INCUBATING)
|
|
74
|
+
class DataHubFormsNotificationsSource(Source):
|
|
75
|
+
"""Forms Notification Source that notifies recipients for compliance forms tasks"""
|
|
76
|
+
|
|
77
|
+
def __init__(
|
|
78
|
+
self, config: DataHubFormsNotificationsSourceConfig, ctx: PipelineContext
|
|
79
|
+
):
|
|
80
|
+
super().__init__(ctx)
|
|
81
|
+
self.config: DataHubFormsNotificationsSourceConfig = config
|
|
82
|
+
self.report = DataHubFormsNotificationsSourceReport()
|
|
83
|
+
self.graph: DataHubGraph = ctx.require_graph(
|
|
84
|
+
"Loading default graph coordinates."
|
|
85
|
+
)
|
|
86
|
+
self.group_to_users_map: Dict[str, List[str]] = {}
|
|
87
|
+
self.recipient_builder: NotificationRecipientBuilder = (
|
|
88
|
+
NotificationRecipientBuilder(self.graph)
|
|
89
|
+
)
|
|
90
|
+
self.user_to_form_notifications: Dict[str, FormNotificationsClass] = {}
|
|
91
|
+
|
|
92
|
+
def get_workunits(self) -> Iterable[MetadataWorkUnit]:
|
|
93
|
+
# end early if the feature flag is not enabled
|
|
94
|
+
if not self.is_feature_flag_enabled():
|
|
95
|
+
return []
|
|
96
|
+
|
|
97
|
+
self.notify_form_assignees()
|
|
98
|
+
|
|
99
|
+
# This source doesn't produce any work units
|
|
100
|
+
return []
|
|
101
|
+
|
|
102
|
+
def is_feature_flag_enabled(self) -> bool:
|
|
103
|
+
response = self.execute_graphql_with_retry(
|
|
104
|
+
GRAPHQL_GET_FEATURE_FLAG, variables={}
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
result = response.get("appConfig", {})
|
|
108
|
+
featureFlags = result.get("featureFlags", {})
|
|
109
|
+
is_enabled = featureFlags.get("formsNotificationsEnabled", False)
|
|
110
|
+
|
|
111
|
+
if not is_enabled:
|
|
112
|
+
logger.error(
|
|
113
|
+
"Tried running datahub-forms-notifications with formsNotificationsEnabled disabled"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return is_enabled
|
|
117
|
+
|
|
118
|
+
def notify_form_assignees(self) -> None:
|
|
119
|
+
for urn, form in self.get_forms():
|
|
120
|
+
if not self.is_form_complete(urn, form.type):
|
|
121
|
+
assignees = self.get_form_assignees(urn, form)
|
|
122
|
+
self.process_notify_on_publish(
|
|
123
|
+
assignees, form.name, urn, form.description
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def process_notify_on_publish(
|
|
127
|
+
self,
|
|
128
|
+
form_assignees: List[str],
|
|
129
|
+
form_name: str,
|
|
130
|
+
form_urn: str,
|
|
131
|
+
form_details: str | None,
|
|
132
|
+
) -> None:
|
|
133
|
+
"""
|
|
134
|
+
Take in form assignees, find the ones who haven't been notified on publish, and build a notification for them.
|
|
135
|
+
"""
|
|
136
|
+
filtered_assignees = self.filter_assignees_to_notify(form_assignees, form_urn)
|
|
137
|
+
recipients = []
|
|
138
|
+
if self.recipient_builder is not None:
|
|
139
|
+
recipients = self.recipient_builder.build_actor_recipients(
|
|
140
|
+
filtered_assignees, "COMPLIANCE_FORM_PUBLISH", True
|
|
141
|
+
)
|
|
142
|
+
recipient_count = len(recipients)
|
|
143
|
+
|
|
144
|
+
if recipient_count > 0:
|
|
145
|
+
self.report.notifications_sent += recipient_count
|
|
146
|
+
self.report.forms_count += 1
|
|
147
|
+
|
|
148
|
+
parameters = [{"key": "formName", "value": form_name}]
|
|
149
|
+
if form_details is not None:
|
|
150
|
+
parameters.append({"key": "formDetails", "value": form_details})
|
|
151
|
+
|
|
152
|
+
response = self.execute_graphql_with_retry(
|
|
153
|
+
GRAPHQL_SEND_FORM_NOTIFICATION_REQUEST,
|
|
154
|
+
variables={
|
|
155
|
+
"input": {
|
|
156
|
+
"type": "BROADCAST_COMPLIANCE_FORM_PUBLISH",
|
|
157
|
+
"parameters": parameters,
|
|
158
|
+
"recipients": self.recipient_builder.convert_recipients_to_json_objects(
|
|
159
|
+
recipients
|
|
160
|
+
),
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
if not response.get("sendFormNotificationRequest", False):
|
|
166
|
+
logger.error(
|
|
167
|
+
f"Issue sending the notification request for this job. Response: {response}"
|
|
168
|
+
)
|
|
169
|
+
else:
|
|
170
|
+
unique_actor_urns = set(
|
|
171
|
+
[
|
|
172
|
+
recipient.get("actor")
|
|
173
|
+
for recipient in recipients
|
|
174
|
+
if recipient.get("actor") is not None
|
|
175
|
+
]
|
|
176
|
+
)
|
|
177
|
+
for actor_urn in unique_actor_urns:
|
|
178
|
+
self.update_form_notifications(actor_urn, form_urn)
|
|
179
|
+
|
|
180
|
+
def update_form_notifications(self, user_urn: str, form_urn: str) -> None:
|
|
181
|
+
"""
|
|
182
|
+
After sending a notification, update the user's formNotifications aspect
|
|
183
|
+
to track that we sent the notification that we did
|
|
184
|
+
"""
|
|
185
|
+
# get or create default formNotifications aspect
|
|
186
|
+
form_notifications = self.user_to_form_notifications.get(user_urn)
|
|
187
|
+
if form_notifications is None:
|
|
188
|
+
form_notifications = FormNotificationsClass(notificationDetails=[])
|
|
189
|
+
|
|
190
|
+
# get the notification details for our specific form or create default
|
|
191
|
+
details_for_form = self.get_notification_details_for_form(
|
|
192
|
+
user_urn, form_urn, form_notifications
|
|
193
|
+
)
|
|
194
|
+
if details_for_form is None:
|
|
195
|
+
details_for_form = FormNotificationDetailsClass(
|
|
196
|
+
formUrn=form_urn, notificationLog=[]
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# add new notification log entry for this occasion
|
|
200
|
+
new_notification_log_entry = FormNotificationEntryClass(
|
|
201
|
+
time=int(time.time() * 1000),
|
|
202
|
+
notificationType="BROADCAST_COMPLIANCE_FORM_PUBLISH",
|
|
203
|
+
)
|
|
204
|
+
details_for_form.notificationLog.append(new_notification_log_entry)
|
|
205
|
+
|
|
206
|
+
# filter out details for given form so we can add updated one
|
|
207
|
+
final_notification_details = [
|
|
208
|
+
details
|
|
209
|
+
for details in form_notifications.notificationDetails
|
|
210
|
+
if details.formUrn != form_urn
|
|
211
|
+
]
|
|
212
|
+
final_notification_details.append(details_for_form)
|
|
213
|
+
|
|
214
|
+
# update the aspect with the final notification details
|
|
215
|
+
form_notifications.notificationDetails = final_notification_details
|
|
216
|
+
|
|
217
|
+
self.user_to_form_notifications[user_urn] = form_notifications
|
|
218
|
+
|
|
219
|
+
self.graph.emit(
|
|
220
|
+
MetadataChangeProposalWrapper(
|
|
221
|
+
entityUrn=user_urn,
|
|
222
|
+
aspect=form_notifications,
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
@retry(
|
|
227
|
+
retry=retry_if_exception_type((Exception, ConnectionError)),
|
|
228
|
+
stop=stop_after_attempt(3),
|
|
229
|
+
wait=wait_exponential(multiplier=1, min=4, max=10),
|
|
230
|
+
reraise=True,
|
|
231
|
+
)
|
|
232
|
+
def execute_graphql_with_retry(
|
|
233
|
+
self, query: str, variables: Dict[str, Any]
|
|
234
|
+
) -> Dict[str, Any]:
|
|
235
|
+
"""Execute GraphQL query with retry logic"""
|
|
236
|
+
if self.graph is None:
|
|
237
|
+
raise ValueError("Graph client not initialized")
|
|
238
|
+
response = self.graph.execute_graphql(query, variables=variables)
|
|
239
|
+
error = response.get("error")
|
|
240
|
+
if error:
|
|
241
|
+
raise Exception(f"GraphQL error: {error}")
|
|
242
|
+
return response
|
|
243
|
+
|
|
244
|
+
def get_forms(self) -> List[Tuple[str, FormInfoClass]]:
|
|
245
|
+
"""
|
|
246
|
+
Get forms and their formInfo aspect either from the forms provided in the config
|
|
247
|
+
or search for forms that are published and notifyAssigneesOnPublish = True.
|
|
248
|
+
This method will only return forms that are published and have notifications enabled.
|
|
249
|
+
"""
|
|
250
|
+
form_urns = []
|
|
251
|
+
|
|
252
|
+
if self.config.form_urns is not None:
|
|
253
|
+
form_urns = self.config.form_urns
|
|
254
|
+
else:
|
|
255
|
+
form_urns = self.search_for_forms()
|
|
256
|
+
|
|
257
|
+
form_urns_with_notifications_enabled = (
|
|
258
|
+
self.get_form_urns_with_notifications_enabled(form_urns)
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
return self.get_form_infos(form_urns_with_notifications_enabled)
|
|
262
|
+
|
|
263
|
+
def get_form_urns_with_notifications_enabled(
|
|
264
|
+
self, form_urns: List[str]
|
|
265
|
+
) -> List[str]:
|
|
266
|
+
"""
|
|
267
|
+
Get formSettings aspects and check if notifications are enabled for a given form urn.
|
|
268
|
+
If notifications are enabled, add to filtered list and return.
|
|
269
|
+
"""
|
|
270
|
+
filtered_form_urns: List[str] = []
|
|
271
|
+
|
|
272
|
+
if len(form_urns) > 0:
|
|
273
|
+
entities = self.graph.get_entities("form", form_urns, ["formSettings"])
|
|
274
|
+
for urn, entity in entities.items():
|
|
275
|
+
form_tuple = entity.get(FormSettingsClass.ASPECT_NAME, (None, None))
|
|
276
|
+
if form_tuple and form_tuple[0]:
|
|
277
|
+
if not isinstance(form_tuple[0], FormSettingsClass):
|
|
278
|
+
logger.error(
|
|
279
|
+
f"{form_tuple[0]} is not of type FormInfo for urn: {urn}"
|
|
280
|
+
)
|
|
281
|
+
else:
|
|
282
|
+
form_settings = form_tuple[0]
|
|
283
|
+
if form_settings.notificationSettings.notifyAssigneesOnPublish:
|
|
284
|
+
filtered_form_urns.append(urn)
|
|
285
|
+
|
|
286
|
+
return filtered_form_urns
|
|
287
|
+
|
|
288
|
+
def get_form_infos(self, form_urns: List[str]) -> List[Tuple[str, FormInfoClass]]:
|
|
289
|
+
"""
|
|
290
|
+
Get formInfo aspects for a list of form urns and return the formInfos of forms
|
|
291
|
+
that are published. If a form is not published, we don't want to notify.
|
|
292
|
+
"""
|
|
293
|
+
form_infos: List[Tuple[str, FormInfoClass]] = []
|
|
294
|
+
|
|
295
|
+
if len(form_urns) > 0:
|
|
296
|
+
entities = self.graph.get_entities("form", form_urns, ["formInfo"])
|
|
297
|
+
for urn, entity in entities.items():
|
|
298
|
+
form_tuple = entity.get(FormInfoClass.ASPECT_NAME, (None, None))
|
|
299
|
+
if form_tuple and form_tuple[0]:
|
|
300
|
+
if not isinstance(form_tuple[0], FormInfoClass):
|
|
301
|
+
logger.error(
|
|
302
|
+
f"{form_tuple[0]} is not of type FormInfo for urn: {urn}"
|
|
303
|
+
)
|
|
304
|
+
else:
|
|
305
|
+
form_info = form_tuple[0]
|
|
306
|
+
if form_info.status.state == FormStateClass.PUBLISHED:
|
|
307
|
+
form_infos.append((urn, form_tuple[0]))
|
|
308
|
+
|
|
309
|
+
return form_infos
|
|
310
|
+
|
|
311
|
+
def search_for_forms(self) -> List[str]:
|
|
312
|
+
scroll_id: Optional[str] = None
|
|
313
|
+
form_urns: List[str] = []
|
|
314
|
+
|
|
315
|
+
try:
|
|
316
|
+
while True:
|
|
317
|
+
next_scroll_id, results = self.scroll_forms_to_notify_for(scroll_id)
|
|
318
|
+
|
|
319
|
+
for result in results:
|
|
320
|
+
form_urn = result.get("entity", {}).get("urn", None)
|
|
321
|
+
if form_urn is None:
|
|
322
|
+
self.report.report_warning(
|
|
323
|
+
message="Failed to resolve entity urn for form! Skipping...",
|
|
324
|
+
context=f"Response: {str(result)}",
|
|
325
|
+
)
|
|
326
|
+
else:
|
|
327
|
+
form_urns.append(form_urn)
|
|
328
|
+
|
|
329
|
+
if next_scroll_id is None:
|
|
330
|
+
break
|
|
331
|
+
else:
|
|
332
|
+
scroll_id = next_scroll_id
|
|
333
|
+
|
|
334
|
+
time.sleep(1)
|
|
335
|
+
|
|
336
|
+
except Exception as e:
|
|
337
|
+
self.report.report_failure(
|
|
338
|
+
title="Failed to search for forms to send notifications for",
|
|
339
|
+
message="Error occurred while searching for forms to send notifications for",
|
|
340
|
+
context=f"message = {str(e)}",
|
|
341
|
+
exc=e,
|
|
342
|
+
)
|
|
343
|
+
return form_urns
|
|
344
|
+
|
|
345
|
+
return form_urns
|
|
346
|
+
|
|
347
|
+
def scroll_forms_to_notify_for(
|
|
348
|
+
self, scroll_id: Optional[str]
|
|
349
|
+
) -> Tuple[Optional[str], List[Dict[str, Any]]]:
|
|
350
|
+
"""Scroll through shared entities with retry logic"""
|
|
351
|
+
response = self.execute_graphql_with_retry(
|
|
352
|
+
GRAPHQL_SCROLL_FORMS_FOR_NOTIFICATIONS,
|
|
353
|
+
variables={
|
|
354
|
+
"scrollId": scroll_id,
|
|
355
|
+
"count": 500,
|
|
356
|
+
},
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
result = response.get("scrollAcrossEntities", {})
|
|
360
|
+
return result.get("nextScrollId"), result.get("searchResults", [])
|
|
361
|
+
|
|
362
|
+
def get_form_assignees(self, form_urn: str, form: FormInfoClass) -> List[str]:
|
|
363
|
+
"""
|
|
364
|
+
Form assignees are provided explicitly on the form and the owners of assets with this form
|
|
365
|
+
if it's an ownership form.
|
|
366
|
+
For form notifications, we want to get users from a user group and send notifications to
|
|
367
|
+
those users specifically
|
|
368
|
+
"""
|
|
369
|
+
user_urns = form.actors.users if form.actors.users is not None else []
|
|
370
|
+
group_urns = form.actors.groups if form.actors.groups is not None else []
|
|
371
|
+
|
|
372
|
+
if form.actors.owners:
|
|
373
|
+
(user_owners, group_owners) = self.get_owners_of_assets_for_form(
|
|
374
|
+
form_urn, form
|
|
375
|
+
)
|
|
376
|
+
user_urns.extend(user_owners)
|
|
377
|
+
group_urns.extend(group_owners)
|
|
378
|
+
|
|
379
|
+
for group_urn in group_urns:
|
|
380
|
+
user_urns.extend(self._get_users_in_group(group_urn))
|
|
381
|
+
|
|
382
|
+
return list(set(user_urns))
|
|
383
|
+
|
|
384
|
+
def get_owners_of_assets_for_form(
|
|
385
|
+
self, form_urn: str, form: FormInfoClass
|
|
386
|
+
) -> Tuple[List[str], List[str]]:
|
|
387
|
+
"""
|
|
388
|
+
Filter to get assets that are not complete for this form and using the extra_source_fields parameter
|
|
389
|
+
we pull owners from the asset's elastic row. self.graph.get_results_by_filter will paginate over assets
|
|
390
|
+
"""
|
|
391
|
+
user_urns = []
|
|
392
|
+
group_urns = []
|
|
393
|
+
|
|
394
|
+
extra_fields = [f for f in DataHubDatasetSearchRow.model_fields]
|
|
395
|
+
results = self.graph.get_results_by_filter(
|
|
396
|
+
extra_or_filters=self._get_incomplete_assets_for_form(form_urn, form.type),
|
|
397
|
+
extra_source_fields=extra_fields,
|
|
398
|
+
skip_cache=True,
|
|
399
|
+
)
|
|
400
|
+
for result in results:
|
|
401
|
+
extra_properties = result["extraProperties"]
|
|
402
|
+
extra_properties_map = parse_extra_properties_for_model(
|
|
403
|
+
extra_properties, DataHubDatasetSearchRow
|
|
404
|
+
)
|
|
405
|
+
search_row = DataHubDatasetSearchRow(**extra_properties_map)
|
|
406
|
+
for owner in search_row.owners:
|
|
407
|
+
if owner.startswith(USER_URN_PREFIX):
|
|
408
|
+
user_urns.append(owner)
|
|
409
|
+
elif owner.startswith(GROUP_URN_PREFIX):
|
|
410
|
+
group_urns.append(owner)
|
|
411
|
+
else:
|
|
412
|
+
logger.warning(
|
|
413
|
+
f"Found unexpected owner {owner} for asset {search_row.urn}"
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
return (user_urns, group_urns)
|
|
417
|
+
|
|
418
|
+
def filter_assignees_to_notify(
|
|
419
|
+
self, user_urns: List[str], form_urn: str
|
|
420
|
+
) -> List[str]:
|
|
421
|
+
"""
|
|
422
|
+
Filter out any users who have already received the publish notification type in the past
|
|
423
|
+
"""
|
|
424
|
+
filtered_users = []
|
|
425
|
+
|
|
426
|
+
self.populate_user_to_form_notifications(user_urns)
|
|
427
|
+
|
|
428
|
+
for user in user_urns:
|
|
429
|
+
form_notifications = self.user_to_form_notifications.get(user)
|
|
430
|
+
if form_notifications is None or not self.has_user_been_sent_notification(
|
|
431
|
+
user, form_urn, form_notifications, "BROADCAST_COMPLIANCE_FORM_PUBLISH"
|
|
432
|
+
):
|
|
433
|
+
filtered_users.append(user)
|
|
434
|
+
|
|
435
|
+
return filtered_users
|
|
436
|
+
|
|
437
|
+
def has_user_been_sent_notification(
|
|
438
|
+
self,
|
|
439
|
+
user_urn: str,
|
|
440
|
+
form_urn: str,
|
|
441
|
+
form_notifications: FormNotificationsClass,
|
|
442
|
+
notification_type: str,
|
|
443
|
+
) -> bool:
|
|
444
|
+
notification_details = self.get_notification_details_for_form(
|
|
445
|
+
user_urn, form_urn, form_notifications
|
|
446
|
+
)
|
|
447
|
+
if notification_details is None:
|
|
448
|
+
return False
|
|
449
|
+
|
|
450
|
+
notification_types_sent_for_form = [
|
|
451
|
+
entry.notificationType for entry in notification_details.notificationLog
|
|
452
|
+
]
|
|
453
|
+
|
|
454
|
+
return notification_type in notification_types_sent_for_form
|
|
455
|
+
|
|
456
|
+
def get_notification_details_for_form(
|
|
457
|
+
self, user_urn: str, form_urn: str, form_notifications: FormNotificationsClass
|
|
458
|
+
) -> FormNotificationDetailsClass | None:
|
|
459
|
+
notification_details_for_form = [
|
|
460
|
+
detail
|
|
461
|
+
for detail in form_notifications.notificationDetails
|
|
462
|
+
if detail.formUrn == form_urn
|
|
463
|
+
]
|
|
464
|
+
|
|
465
|
+
notification_details = None
|
|
466
|
+
if len(notification_details_for_form) > 1:
|
|
467
|
+
logger.warning(
|
|
468
|
+
f"Found more than one notificationDetails for a given form for user {user_urn} in {form_notifications}"
|
|
469
|
+
)
|
|
470
|
+
# grab first one
|
|
471
|
+
notification_details = notification_details_for_form[0]
|
|
472
|
+
elif len(notification_details_for_form) == 1:
|
|
473
|
+
notification_details = notification_details_for_form[0]
|
|
474
|
+
|
|
475
|
+
return notification_details
|
|
476
|
+
|
|
477
|
+
def populate_user_to_form_notifications(self, user_urns: List[str]) -> None:
|
|
478
|
+
new_users = [
|
|
479
|
+
urn for urn in user_urns if urn not in self.user_to_form_notifications
|
|
480
|
+
]
|
|
481
|
+
|
|
482
|
+
if len(new_users) == 0:
|
|
483
|
+
return
|
|
484
|
+
|
|
485
|
+
entities = self.graph.get_entities("corpuser", new_users, ["formNotifications"])
|
|
486
|
+
for urn, entity in entities.items():
|
|
487
|
+
user_tuple = entity.get(FormNotificationsClass.ASPECT_NAME, (None, None))
|
|
488
|
+
if user_tuple and user_tuple[0]:
|
|
489
|
+
if not isinstance(user_tuple[0], FormNotificationsClass):
|
|
490
|
+
logger.error(
|
|
491
|
+
f"{user_tuple[0]} is not of type FormNotifications for urn: {urn}"
|
|
492
|
+
)
|
|
493
|
+
else:
|
|
494
|
+
self.user_to_form_notifications[urn] = user_tuple[0]
|
|
495
|
+
|
|
496
|
+
def _get_users_in_group(self, group_urn: str) -> List[str]:
|
|
497
|
+
"""
|
|
498
|
+
Using a relationship query, get users inside of a group. Store these users in memory if we've
|
|
499
|
+
already fetched the users for this group.
|
|
500
|
+
"""
|
|
501
|
+
if (users_in_group := self.group_to_users_map.get(group_urn)) is not None:
|
|
502
|
+
return users_in_group
|
|
503
|
+
|
|
504
|
+
group_member_urns = []
|
|
505
|
+
members = self.graph.get_related_entities(
|
|
506
|
+
group_urn,
|
|
507
|
+
["IsMemberOfGroup", "IsMemberOfNativeGroup"],
|
|
508
|
+
self.graph.RelationshipDirection.INCOMING,
|
|
509
|
+
)
|
|
510
|
+
member_urns = [member.urn for member in members]
|
|
511
|
+
for member_urn in member_urns:
|
|
512
|
+
if member_urn.startswith(USER_URN_PREFIX):
|
|
513
|
+
group_member_urns.append(member_urn)
|
|
514
|
+
else:
|
|
515
|
+
logger.warning(
|
|
516
|
+
f"Unexpected group member {member_urn} found in group {group_urn}"
|
|
517
|
+
)
|
|
518
|
+
self.group_to_users_map[group_urn] = group_member_urns
|
|
519
|
+
|
|
520
|
+
return group_member_urns
|
|
521
|
+
|
|
522
|
+
def _get_verification_form_filter(self, form_urn: str) -> RawSearchFilter:
|
|
523
|
+
return [
|
|
524
|
+
{"and": [{"field": "incompleteForms", "values": [form_urn]}]},
|
|
525
|
+
{
|
|
526
|
+
"and": [
|
|
527
|
+
{"field": "completedForms", "values": [form_urn]},
|
|
528
|
+
{"field": "verifiedForms", "values": [form_urn], "negated": True},
|
|
529
|
+
]
|
|
530
|
+
},
|
|
531
|
+
]
|
|
532
|
+
|
|
533
|
+
def _get_completion_form_filter(self, form_urn: str) -> RawSearchFilter:
|
|
534
|
+
return [{"and": [{"field": "incompleteForms", "values": [form_urn]}]}]
|
|
535
|
+
|
|
536
|
+
def _get_incomplete_assets_for_form(
|
|
537
|
+
self, form_urn: str, form_type: str | FormTypeClass
|
|
538
|
+
) -> RawSearchFilter:
|
|
539
|
+
return (
|
|
540
|
+
self._get_completion_form_filter(form_urn)
|
|
541
|
+
if form_type == FormTypeClass.COMPLETION
|
|
542
|
+
else self._get_verification_form_filter(form_urn)
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
def is_form_complete(self, form_urn: str, form_type: str | FormTypeClass) -> int:
|
|
546
|
+
"""
|
|
547
|
+
Returns whether this form is complete - meaning no assets have any work left to do for it.
|
|
548
|
+
This takes into account the type of form to know if it's fully complete.
|
|
549
|
+
"""
|
|
550
|
+
response = self.execute_graphql_with_retry(
|
|
551
|
+
GRAPHQL_GET_SEARCH_RESULTS_TOTAL,
|
|
552
|
+
variables={
|
|
553
|
+
"count": 0,
|
|
554
|
+
"orFilters": self._get_incomplete_assets_for_form(form_urn, form_type),
|
|
555
|
+
},
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
result = response.get("searchAcrossEntities", {})
|
|
559
|
+
total = result.get("total", -1)
|
|
560
|
+
if total < 0:
|
|
561
|
+
logger.warning(
|
|
562
|
+
f"Error evaluating if form with urn {form_urn} is complete. Skipping."
|
|
563
|
+
)
|
|
564
|
+
return True
|
|
565
|
+
else:
|
|
566
|
+
return total == 0
|
|
567
|
+
|
|
568
|
+
def get_report(self) -> SourceReport:
|
|
569
|
+
return self.report
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
|
|
3
|
+
GRAPHQL_SCROLL_FORMS_FOR_NOTIFICATIONS = (
|
|
4
|
+
pathlib.Path(__file__).parent / "scroll_forms_for_notification.gql"
|
|
5
|
+
).read_text()
|
|
6
|
+
|
|
7
|
+
GRAPHQL_GET_SEARCH_RESULTS_TOTAL = (
|
|
8
|
+
pathlib.Path(__file__).parent / "get_search_results_total.gql"
|
|
9
|
+
).read_text()
|
|
10
|
+
|
|
11
|
+
GRAPHQL_SEND_FORM_NOTIFICATION_REQUEST = (
|
|
12
|
+
pathlib.Path(__file__).parent / "send_form_notification_request.gql"
|
|
13
|
+
).read_text()
|
|
14
|
+
|
|
15
|
+
GRAPHQL_GET_FEATURE_FLAG = (
|
|
16
|
+
pathlib.Path(__file__).parent / "get_feature_flag.gql"
|
|
17
|
+
).read_text()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
query scrollFormsToNotifyFor($scrollId: String, $count: Int!) {
|
|
2
|
+
scrollAcrossEntities(
|
|
3
|
+
input: {
|
|
4
|
+
types: [FORM]
|
|
5
|
+
query: "*"
|
|
6
|
+
scrollId: $scrollId
|
|
7
|
+
count: $count
|
|
8
|
+
searchFlags: { skipCache: true }
|
|
9
|
+
orFilters: [
|
|
10
|
+
{
|
|
11
|
+
and: [
|
|
12
|
+
{ field: "formStatus", values: ["PUBLISHED"] }
|
|
13
|
+
{ field: "notifyAssigneesOnPublish", values: ["true"] }
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
) {
|
|
19
|
+
nextScrollId
|
|
20
|
+
count
|
|
21
|
+
total
|
|
22
|
+
searchResults {
|
|
23
|
+
entity {
|
|
24
|
+
urn
|
|
25
|
+
type
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|