bigeye-cli 0.3.60__tar.gz → 0.3.62__tar.gz

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 (38) hide show
  1. {bigeye_cli-0.3.60 → bigeye_cli-0.3.62}/PKG-INFO +2 -2
  2. bigeye_cli-0.3.62/bigeye_cli/bigconfig/bigconfig_commands.py +198 -0
  3. {bigeye_cli-0.3.60 → bigeye_cli-0.3.62}/bigeye_cli/catalog/catalog_commands.py +229 -10
  4. {bigeye_cli-0.3.60 → bigeye_cli-0.3.62}/bigeye_cli/deltas/deltas_commands.py +88 -43
  5. {bigeye_cli-0.3.60 → bigeye_cli-0.3.62}/bigeye_cli/exceptions/exceptions.py +10 -0
  6. {bigeye_cli-0.3.60 → bigeye_cli-0.3.62}/bigeye_cli/lineage/lineage_commands.py +55 -11
  7. {bigeye_cli-0.3.60 → bigeye_cli-0.3.62}/bigeye_cli/metric/metric_commands.py +3 -3
  8. bigeye_cli-0.3.62/bigeye_cli/model/github_report.py +74 -0
  9. {bigeye_cli-0.3.60 → bigeye_cli-0.3.62}/bigeye_cli/model/vendor_report.py +17 -15
  10. bigeye_cli-0.3.62/bigeye_cli/workspace/workspace_commands.py +575 -0
  11. {bigeye_cli-0.3.60 → bigeye_cli-0.3.62}/pyproject.toml +2 -2
  12. bigeye_cli-0.3.60/bigeye_cli/bigconfig/bigconfig_commands.py +0 -161
  13. bigeye_cli-0.3.60/bigeye_cli/model/github_report.py +0 -58
  14. bigeye_cli-0.3.60/bigeye_cli/workspace/workspace_commands.py +0 -490
  15. {bigeye_cli-0.3.60 → bigeye_cli-0.3.62}/LICENSE +0 -0
  16. {bigeye_cli-0.3.60 → bigeye_cli-0.3.62}/bigeye_cli/CLI_DOCS.md +0 -0
  17. {bigeye_cli-0.3.60 → bigeye_cli-0.3.62}/bigeye_cli/__init__.py +0 -0
  18. {bigeye_cli-0.3.60 → bigeye_cli-0.3.62}/bigeye_cli/__main__.py +0 -0
  19. {bigeye_cli-0.3.60 → bigeye_cli-0.3.62}/bigeye_cli/__version__.py +0 -0
  20. {bigeye_cli-0.3.60 → bigeye_cli-0.3.62}/bigeye_cli/bigconfig/__init__.py +0 -0
  21. {bigeye_cli-0.3.60 → bigeye_cli-0.3.62}/bigeye_cli/bigconfig/bigconfig_options.py +0 -0
  22. {bigeye_cli-0.3.60 → bigeye_cli-0.3.62}/bigeye_cli/catalog/__init__.py +0 -0
  23. {bigeye_cli-0.3.60 → bigeye_cli-0.3.62}/bigeye_cli/collections/__init__.py +0 -0
  24. {bigeye_cli-0.3.60 → bigeye_cli-0.3.62}/bigeye_cli/collections/collection_commands.py +0 -0
  25. {bigeye_cli-0.3.60 → bigeye_cli-0.3.62}/bigeye_cli/configure/__init__.py +0 -0
  26. {bigeye_cli-0.3.60 → bigeye_cli-0.3.62}/bigeye_cli/configure/configure_commands.py +0 -0
  27. {bigeye_cli-0.3.60 → bigeye_cli-0.3.62}/bigeye_cli/deltas/__init__.py +0 -0
  28. {bigeye_cli-0.3.60 → bigeye_cli-0.3.62}/bigeye_cli/enums.py +0 -0
  29. {bigeye_cli-0.3.60 → bigeye_cli-0.3.62}/bigeye_cli/exceptions/__init__.py +0 -0
  30. {bigeye_cli-0.3.60 → bigeye_cli-0.3.62}/bigeye_cli/functions.py +0 -0
  31. {bigeye_cli-0.3.60 → bigeye_cli-0.3.62}/bigeye_cli/global_options.py +0 -0
  32. {bigeye_cli-0.3.60 → bigeye_cli-0.3.62}/bigeye_cli/issues/__init__.py +0 -0
  33. {bigeye_cli-0.3.60 → bigeye_cli-0.3.62}/bigeye_cli/issues/issue_commands.py +0 -0
  34. {bigeye_cli-0.3.60 → bigeye_cli-0.3.62}/bigeye_cli/lineage/__init__.py +0 -0
  35. {bigeye_cli-0.3.60 → bigeye_cli-0.3.62}/bigeye_cli/metric/__init__.py +0 -0
  36. {bigeye_cli-0.3.60 → bigeye_cli-0.3.62}/bigeye_cli/model/__init__.py +0 -0
  37. {bigeye_cli-0.3.60 → bigeye_cli-0.3.62}/bigeye_cli/model/cicd_conf.py +0 -0
  38. {bigeye_cli-0.3.60 → bigeye_cli-0.3.62}/bigeye_cli/workspace/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: bigeye-cli
3
- Version: 0.3.60
3
+ Version: 0.3.62
4
4
  Summary: Bigeye CLI offers developer tools for maintaining your developer workspace.
5
5
  Home-page: https://docs.bigeye.com/docs
6
6
  License: Proprietary
@@ -13,7 +13,7 @@ Classifier: Programming Language :: Python :: 3.9
13
13
  Classifier: Programming Language :: Python :: 3.10
14
14
  Classifier: Programming Language :: Python :: 3.11
15
15
  Requires-Dist: PyGithub (>=1.57,<2.0)
16
- Requires-Dist: bigeye-sdk (>=0.4.80,<0.5.0)
16
+ Requires-Dist: bigeye-sdk (>=0.4.83,<0.5.0)
17
17
  Requires-Dist: click (>=8.0.4,<9.0.0)
18
18
  Requires-Dist: rich (>=12.4.4,<13.0.0)
19
19
  Description-Content-Type: text/markdown
@@ -0,0 +1,198 @@
1
+ import os
2
+ from pathlib import Path
3
+ from typing import Optional, List
4
+
5
+ import typer
6
+
7
+ from bigeye_cli.functions import cli_client_factory
8
+ from bigeye_cli import global_options
9
+ from bigeye_cli.bigconfig import bigconfig_options
10
+ from bigeye_cli.model.github_report import GitHubReport
11
+ from bigeye_sdk.controller.metric_controller import MetricController
12
+ from bigeye_sdk.controller.metric_suite_controller import MetricSuiteController
13
+ from bigeye_sdk.generated.com.bigeye.models.generated import MetricInfoList
14
+ from bigeye_sdk.model.protobuf_message_facade import SimpleCollection
15
+ from bigeye_sdk.log import get_logger
16
+
17
+ log = get_logger(__file__)
18
+
19
+ app = typer.Typer(no_args_is_help=True, help="Bigconfig Commands for Bigeye CLI")
20
+
21
+ """
22
+ File should contain commands relating to deploying Bigconfig files.
23
+ """
24
+
25
+
26
+ @app.command()
27
+ def plan(
28
+ bigeye_conf: str = global_options.bigeye_conf,
29
+ config_file: str = global_options.config_file,
30
+ workspace: str = global_options.workspace,
31
+ input_path: Optional[List[str]] = bigconfig_options.input_path,
32
+ output_path: str = bigconfig_options.output_path,
33
+ purge_source_names: Optional[List[str]] = bigconfig_options.purge_source_names,
34
+ purge_all_sources: bool = bigconfig_options.purge_all_sources,
35
+ no_queue: bool = bigconfig_options.no_queue,
36
+ recursive: bool = bigconfig_options.recursive,
37
+ strict_mode: bool = bigconfig_options.strict_mode,
38
+ cicd: bool = typer.Option(
39
+ False,
40
+ "--cicd_report",
41
+ "-cicd",
42
+ help="Add the report to a pull request. (Github only)",
43
+ ),
44
+ ):
45
+ """Executes a plan for purging sources or processing bigconfig files in the input path/current
46
+ working directory."""
47
+
48
+ client = cli_client_factory(bigeye_conf, config_file, workspace)
49
+ mc = MetricSuiteController(client=client)
50
+
51
+ # Resolve args to first true or first non None values
52
+ if not input_path:
53
+ input_path = client.config.bigconfig_input_path or [Path.cwd()]
54
+ if not output_path:
55
+ output_path = client.config.bigconfig_output_path or Path.cwd()
56
+ strict_mode = strict_mode or client.config.bigconfig_strict_mode
57
+
58
+ if purge_source_names or purge_all_sources:
59
+ report = mc.execute_purge(
60
+ purge_source_names=purge_source_names,
61
+ purge_all_sources=purge_all_sources,
62
+ output_path=output_path,
63
+ apply=False,
64
+ )
65
+ else:
66
+ report = mc.execute_bigconfig(
67
+ input_path=input_path,
68
+ output_path=output_path,
69
+ apply=False,
70
+ recursive=recursive,
71
+ no_queue=no_queue,
72
+ strict_mode=strict_mode,
73
+ )
74
+ if cicd:
75
+ gh_report = GitHubReport(github_token=os.environ["GITHUB_TOKEN"])
76
+ gh_report.publish_bigconfig_plan(plan=report)
77
+
78
+
79
+ @app.command()
80
+ def apply(
81
+ bigeye_conf: str = global_options.bigeye_conf,
82
+ config_file: str = global_options.config_file,
83
+ workspace: str = global_options.workspace,
84
+ input_path: Optional[List[str]] = bigconfig_options.input_path,
85
+ output_path: str = bigconfig_options.output_path,
86
+ purge_source_names: Optional[List[str]] = bigconfig_options.purge_source_names,
87
+ purge_all_sources: bool = bigconfig_options.purge_all_sources,
88
+ no_queue: bool = bigconfig_options.no_queue,
89
+ recursive: bool = bigconfig_options.recursive,
90
+ strict_mode: bool = bigconfig_options.strict_mode,
91
+ auto_approve: bool = bigconfig_options.auto_approve,
92
+ ):
93
+ """Applies a purge of deployed metrics or applies Bigconfig files from the input path/current working directory to
94
+ the Bigeye workspace."""
95
+
96
+ client = cli_client_factory(bigeye_conf, config_file, workspace)
97
+ mc = MetricSuiteController(client=client)
98
+
99
+ # Resolve args to first true or first non None values
100
+ if not input_path:
101
+ input_path = client.config.bigconfig_input_path or [Path.cwd()]
102
+ if not output_path:
103
+ output_path = client.config.bigconfig_output_path or Path.cwd()
104
+ strict_mode = strict_mode or client.config.bigconfig_strict_mode
105
+ auto_approve = auto_approve or client.config.bigconfig_auto_approve
106
+
107
+ if purge_source_names or purge_all_sources:
108
+ mc.execute_purge(
109
+ purge_source_names=purge_source_names,
110
+ purge_all_sources=purge_all_sources,
111
+ output_path=output_path,
112
+ apply=True,
113
+ )
114
+ else:
115
+ mc.execute_bigconfig(
116
+ input_path=input_path,
117
+ output_path=output_path,
118
+ apply=True,
119
+ no_queue=no_queue,
120
+ recursive=recursive,
121
+ strict_mode=strict_mode,
122
+ auto_approve=auto_approve,
123
+ )
124
+
125
+
126
+ @app.command()
127
+ def export(
128
+ bigeye_conf: str = global_options.bigeye_conf,
129
+ config_file: str = global_options.config_file,
130
+ workspace: str = global_options.workspace,
131
+ output_path: str = bigconfig_options.output_path,
132
+ collection_id: int = typer.Option(
133
+ None, "--collection_id", "-cid", help="Collection ID of metrics to be exported."
134
+ ),
135
+ table_id: int = typer.Option(
136
+ None, "--table_id", "-tid", help="Table ID of metrics to be exported."
137
+ ),
138
+ metric_ids: List[int] = typer.Option(
139
+ None, "--metric_id", "-mid", help="Metric ID of metrics to be exported."
140
+ ),
141
+ ):
142
+ """Exports existing metrics into a valid Bigconfig file."""
143
+
144
+ client = cli_client_factory(bigeye_conf, config_file, workspace)
145
+ mc = MetricController(client=client)
146
+ collection: SimpleCollection = SimpleCollection(
147
+ name="Sample", description="SDK Generated"
148
+ )
149
+
150
+ # Require at least 1 option to be specified
151
+ if not collection_id and not table_id and not metric_ids:
152
+ raise typer.BadParameter(
153
+ "At least 1 option must be provided for bigconfig export. Please provide the "
154
+ "collection id (-cid), table id (-tid), or list of metric ids (-mid). "
155
+ )
156
+
157
+ # Get existing collection if passed
158
+ if collection_id:
159
+ collection: SimpleCollection = SimpleCollection.from_datawatch_object(
160
+ client.get_collection(collection_id=collection_id).collection
161
+ )
162
+ metric_ids.extend(collection.metric_ids)
163
+
164
+ # Get metrics from table if passed
165
+ if table_id:
166
+ metrics_from_table = client.get_metric_info_batch_post(
167
+ table_ids=[table_id]
168
+ ).metrics
169
+ metric_ids.extend([m.metric_configuration.id for m in metrics_from_table])
170
+ if not metrics_from_table:
171
+ raise typer.BadParameter(
172
+ f"No metrics found for given table id. Please check arguments and try again."
173
+ )
174
+ if not collection_id:
175
+ collection.name = metrics_from_table[0].metric_metadata.dataset_name
176
+
177
+ # Pull metric information
178
+ metrics: MetricInfoList = client.get_metric_info_batch_post(metric_ids=metric_ids)
179
+
180
+ if not output_path:
181
+ output_path = client.config.bigconfig_output_path or Path.cwd()
182
+
183
+ if not metrics.metrics:
184
+ raise typer.BadParameter(
185
+ "No metrics found for given arguments. Please check again."
186
+ )
187
+
188
+ bigconfig = mc.metric_info_to_bigconfig(metric_info=metrics, collection=collection)
189
+
190
+ file_name = collection.name.lower().replace(" ", "_")
191
+ output = bigconfig.save(
192
+ output_path=output_path,
193
+ default_file_name=f"{file_name}.bigconfig",
194
+ custom_formatter=bigconfig.format_bigconfig_export,
195
+ )
196
+ typer.secho(
197
+ f"\nSample bigconfig file generated at: {output}\n", fg="green", bold=True
198
+ )
@@ -1,23 +1,37 @@
1
1
  import json
2
2
  import logging
3
+ import os
4
+ import sys
5
+ import time
6
+ from collections import defaultdict
7
+ from datetime import datetime
3
8
  from pathlib import Path
4
9
  from typing import Optional, List, Dict
5
10
 
6
11
  import typer
12
+ import yaml
7
13
  from rich.progress import track
8
14
 
15
+ from bigeye_cli.exceptions.exceptions import MissingArgumentException
9
16
  from bigeye_sdk.client.enum import Method
10
17
 
11
18
  from bigeye_cli.enums import OPS_METRICS
12
19
  from bigeye_cli import global_options
13
20
  from bigeye_sdk.functions.dbt import parse_dbt_manifest
14
21
  from bigeye_sdk.functions.file_functs import read_json_file
22
+ from bigeye_sdk.functions.notifications import (
23
+ send_email,
24
+ post_slack_snippet,
25
+ post_webhook_request,
26
+ )
15
27
  from bigeye_sdk.functions.search_and_match_functions import fuzzy_match
16
28
  from bigeye_sdk.model.dbt_manifest import DbtManifest
17
29
  from bigeye_sdk.model.protobuf_enum_facade import (
18
30
  MetricStatus,
19
31
  SimpleMetricCategory,
20
32
  SimpleMetricTemplateFieldType,
33
+ SimpleSchemaChangeOperation,
34
+ SimpleCatalogEntityType,
21
35
  )
22
36
  from bigeye_sdk.functions.aws import get_secret
23
37
  from bigeye_sdk.generated.com.bigeye.models.generated import (
@@ -33,6 +47,8 @@ from bigeye_sdk.generated.com.bigeye.models.generated import (
33
47
  Table,
34
48
  User,
35
49
  OwnableType,
50
+ SchemaChangeOperation,
51
+ SchemaChange,
36
52
  )
37
53
  from bigeye_sdk.log import get_logger
38
54
  from bigeye_cli.functions import (
@@ -44,6 +60,7 @@ from bigeye_cli.functions import (
44
60
  )
45
61
  from bigeye_sdk.functions.metric_functions import has_auto_threshold
46
62
  from bigeye_sdk.functions.core_py_functs import int_enum_enum_list_joined
63
+ from bigeye_sdk.model.schema_change import SourceSchemaChange
47
64
 
48
65
  # create logger
49
66
  log = get_logger(__file__)
@@ -430,11 +447,11 @@ def add_source(
430
447
 
431
448
  if secret_name:
432
449
  secret = get_secret(secret_name, secret_region_name)
433
- source_config = json.loads(secret["SecretString"])
450
+ source_config = yaml.safe_load(secret["SecretString"])
434
451
  else:
435
452
  log.info(f"Loading source config file: {source_catalog_config_file}")
436
- with open(source_catalog_config_file) as json_file:
437
- source_config = json.load(json_file)
453
+ with open(source_catalog_config_file) as fin:
454
+ source_config = yaml.safe_load(fin)
438
455
 
439
456
  db_type = source_config["database_type"].upper()
440
457
  source_config["database_type"] = WarehouseType[f"DATABASE_TYPE_{db_type}"]
@@ -803,26 +820,26 @@ def upsert_template(
803
820
  config_file: str = global_options.config_file,
804
821
  workspace: str = global_options.workspace,
805
822
  source_name: str = typer.Option(
806
- ...,
823
+ None,
807
824
  "--source_name",
808
825
  "-sn",
809
826
  help="The name of the source where the template will be defined",
810
827
  ),
811
828
  template_name: str = typer.Option(
812
- ..., "--name", "-tn", help="The user defined name of the template"
829
+ None, "--name", "-tn", help="The user defined name of the template"
813
830
  ),
814
831
  template_body: str = typer.Option(
815
- ..., "--body", "-tb", help="The string to define the template"
832
+ None, "--body", "-tb", help="The string to define the template"
816
833
  ),
817
834
  return_type: SimpleMetricTemplateFieldType = typer.Option(
818
- ...,
835
+ None,
819
836
  "--returns",
820
837
  "-rt",
821
838
  help="The data type returned by the template; i.e. NUMERIC, BOOLEAN",
822
839
  ),
823
840
  # TODO: Update to List[Tuple[str, SimpleMetricTemplateParameterType]] when supported by typer.
824
841
  parameters: List[str] = typer.Option(
825
- ...,
842
+ None,
826
843
  "--params",
827
844
  "-p",
828
845
  help="A list of key/value pairs for parameters; ex. -p column=COLUMN_REFERENCE -p table=STRING",
@@ -833,11 +850,27 @@ def upsert_template(
833
850
  "-id",
834
851
  help="The template ID (Only required when updating a template).",
835
852
  ),
853
+ template_file: Optional[str] = typer.Option(
854
+ None,
855
+ "--template_file",
856
+ "-tf",
857
+ help="A file containing a template definition.",
858
+ )
836
859
  ):
837
860
  """Create or update a template for a source."""
838
861
  # TODO: Lookup template ID, once template names are unique
839
862
  client = cli_client_factory(bigeye_conf, config_file, workspace)
840
863
 
864
+ if template_file:
865
+ with open(template_file) as fin:
866
+ template = yaml.safe_load(stream=fin)
867
+ source_name = template["source_name"]
868
+ template_name = template["name"]
869
+ template_body = template["body"]
870
+ return_type = template["return_type"]
871
+ parameters = template["parameters"]
872
+
873
+
841
874
  source: Source = [s for s in client.get_sources().sources if s.name == source_name][
842
875
  0
843
876
  ]
@@ -1089,7 +1122,10 @@ def ingest_dbt_core_run_info(
1089
1122
  help="The path to the target directory created with dbt run, build, and test commands.",
1090
1123
  ),
1091
1124
  project_name: Optional[str] = typer.Option(
1092
- None, "--project_name", "-pn", help="The name of the dbt project. Will attempt to parse from metadata section of manifest.json, if not present"
1125
+ None,
1126
+ "--project_name",
1127
+ "-pn",
1128
+ help="The name of the dbt project. Will attempt to parse from metadata section of manifest.json, if not present",
1093
1129
  ),
1094
1130
  job_name: Optional[str] = typer.Option(
1095
1131
  None, "--job_name", "-jn", help="The name of the dbt job."
@@ -1122,9 +1158,192 @@ def ingest_dbt_core_run_info(
1122
1158
  run_results_json=json.dumps(run_results_json),
1123
1159
  project_url=project_url,
1124
1160
  job_url=job_url,
1125
- job_run_url=job_run_url
1161
+ job_run_url=job_run_url,
1126
1162
  )
1127
1163
 
1128
1164
  log.info(
1129
1165
  f"The following response was returned:\n\n\t{response.to_json(indent=True)}"
1130
1166
  )
1167
+
1168
+
1169
+ @app.command()
1170
+ def track_schema_changes(
1171
+ bigeye_conf: str = global_options.bigeye_conf,
1172
+ config_file: str = global_options.config_file,
1173
+ workspace: str = global_options.workspace,
1174
+ source_names: List[str] = typer.Option(
1175
+ [],
1176
+ "--source",
1177
+ "-src",
1178
+ help="A list of source names to track. (None will track changes from all sources)",
1179
+ ),
1180
+ since: Optional[str] = typer.Option(
1181
+ "1970-01-01",
1182
+ "--since",
1183
+ "-s",
1184
+ help="Track schema changes from this date. (Formatted as yyyy-MM-dd)",
1185
+ ),
1186
+ frequency: Optional[int] = typer.Option(
1187
+ 0,
1188
+ "--frequency",
1189
+ "-f",
1190
+ help="Every n hours to check for schema changes. (None will only run the check once)",
1191
+ ),
1192
+ change_types: List[SimpleSchemaChangeOperation] = typer.Option(
1193
+ [
1194
+ SimpleSchemaChangeOperation.DELETED,
1195
+ SimpleSchemaChangeOperation.CREATED,
1196
+ SimpleSchemaChangeOperation.TYPE_CHANGED,
1197
+ ],
1198
+ "--change_type",
1199
+ "-ct",
1200
+ help="The types of changes to track; i.e additions, changes, deletions. (None will track all types)",
1201
+ ),
1202
+ notification_channels: List[str] = typer.Option(
1203
+ [],
1204
+ "--channel",
1205
+ "-nc",
1206
+ help="A list of notification channels; e.g. -nc email@mydomain.com -nc #slack_channel -nc https://www.webhook.com",
1207
+ ),
1208
+ smtp_server: Optional[str] = typer.Option(
1209
+ None, "--server", "-smtp", help="The server to send emails."
1210
+ ),
1211
+ smtp_port: Optional[int] = typer.Option(
1212
+ 587, "--port", "-p", help="The server port to send emails."
1213
+ ),
1214
+ slack_token: Optional[str] = typer.Option(
1215
+ None,
1216
+ "--token",
1217
+ "-t",
1218
+ help="The token to utilize the Slack web client. (Will attempt to use environment variable SLACK_BOT_TOKEN, if not set)",
1219
+ ),
1220
+ ):
1221
+ """Track schema changes for sources on demand or every n hour(s) and notify specified channels.
1222
+ Environment variables SMTP_USER and SMTP_PASSWORD MUST be set if using email notification.
1223
+ """
1224
+
1225
+ hour_as_secs = 3600
1226
+ no_reply_at_bigeye = "noreply@bigeye.com"
1227
+
1228
+ try:
1229
+ date_epoch = datetime.strptime(since, "%Y-%m-%d").timestamp()
1230
+ except ValueError as e:
1231
+ log.error(
1232
+ f"Error converting date {since}. Please ensure it formatted as yyyy-MM-dd."
1233
+ )
1234
+ log.info("The process will exit.")
1235
+ raise e
1236
+
1237
+ emails = []
1238
+ slack_channels = []
1239
+ webhooks = []
1240
+ for nc in notification_channels:
1241
+ if "@" in nc:
1242
+ emails.append(nc)
1243
+ elif nc.startswith("#"):
1244
+ slack_channels.append(nc)
1245
+ elif nc.startswith("http"):
1246
+ webhooks.append(nc)
1247
+ else:
1248
+ log.error(f"Unrecognized notification channel: {nc}")
1249
+ log.warning(f"Schema changes will not be sent to {nc}")
1250
+
1251
+ if emails:
1252
+ if not smtp_server:
1253
+ raise MissingArgumentException(
1254
+ "Email listed as notification channel but no email server specified to send one."
1255
+ )
1256
+ if not os.environ.get("SMTP_USER", None):
1257
+ raise MissingArgumentException(
1258
+ "Email listed as notification channel but no value set for environment variable SMTP_USER."
1259
+ )
1260
+ if not os.environ.get("SMTP_PASSWORD", None):
1261
+ raise MissingArgumentException(
1262
+ "Email listed as notification channel but no value set for environment variable SMTP_PASSWORD."
1263
+ )
1264
+
1265
+ if slack_channels:
1266
+ if not slack_token and not os.environ.get("SLACK_BOT_TOKEN", None):
1267
+ raise MissingArgumentException(
1268
+ "Slack listed as notification channel but no value set for slack token or environment variable SLACK_BOT_TOKEN."
1269
+ )
1270
+ if not slack_token:
1271
+ slack_token = os.environ.get("SLACK_BOT_TOKEN")
1272
+
1273
+ change_types = [ct.to_datawatch_object() for ct in change_types]
1274
+
1275
+ client = cli_client_factory(bigeye_conf, config_file, workspace)
1276
+
1277
+ sources = client.get_sources_by_name(source_names=source_names)
1278
+ while True:
1279
+ changes_by_source: Dict[str, List[SchemaChange]] = {}
1280
+ for s in sources.values():
1281
+
1282
+ changes = []
1283
+ since_time = datetime.fromtimestamp(timestamp=date_epoch).strftime(
1284
+ "%m/%d/%Y, %H:%M"
1285
+ )
1286
+ log.info(f"Retrieving changes for source {s.name} since {since_time}")
1287
+ changes.extend(
1288
+ [
1289
+ c
1290
+ for c in client.fetch_schema_changes(source_id=s.id, page_size=1000)
1291
+ if c.change_type in change_types and c.detected_at >= date_epoch
1292
+ ]
1293
+ )
1294
+ if changes:
1295
+ changes_by_source[s.name] = changes
1296
+ else:
1297
+ continue
1298
+
1299
+ source_changes = [
1300
+ SourceSchemaChange(name=k, changes=v) for k, v in changes_by_source.items()
1301
+ ]
1302
+
1303
+ all_alerts = ""
1304
+ alert_webhook = {}
1305
+
1306
+ if source_changes:
1307
+
1308
+ for change in source_changes:
1309
+ all_alerts += f"<br><br>{change.format_message()}</br></br>"
1310
+ alert_webhook.update(change.format_webhook())
1311
+
1312
+
1313
+ if emails:
1314
+ send_email(
1315
+ server_name=smtp_server,
1316
+ port=smtp_port,
1317
+ user_name=os.environ.get("SMTP_USER"),
1318
+ password=os.environ.get("SMTP_PASSWORD"),
1319
+ sender=no_reply_at_bigeye,
1320
+ subject=f"Schema changes detected in Bigeye since {since_time}",
1321
+ recipient=emails,
1322
+ body=all_alerts,
1323
+ )
1324
+
1325
+ if slack_channels:
1326
+ post_slack_snippet(
1327
+ slack_token=slack_token,
1328
+ slack_channel=slack_channels,
1329
+ body=alert_webhook,
1330
+ text=f"*Bigeye schema changes since {since_time}*",
1331
+ title="Bigeye Schema Changes",
1332
+ )
1333
+
1334
+ if webhooks:
1335
+ data = {
1336
+ "name": "Bigeye Schema Changes",
1337
+ "since": since_time,
1338
+ "alerts": alert_webhook,
1339
+ }
1340
+ post_webhook_request(webhook_url=webhooks, data=data)
1341
+
1342
+ if frequency:
1343
+ log.info(f"Continuing to track schema changes.")
1344
+ log.info(f"The process will run again in {frequency} hours.")
1345
+ date_epoch = datetime.now().timestamp()
1346
+ time.sleep(frequency * hour_as_secs)
1347
+ else:
1348
+ log.info("No frequency specified. The process will exit.")
1349
+ sys.exit(0)