Skip to content

Commit e2cc35b

Browse files
Merge pull request #49 from DataKitchen/release/4.26.1
Release/4.26.1
2 parents 808a14f + 9de63e6 commit e2cc35b

File tree

88 files changed

+2091
-817
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

88 files changed

+2091
-817
lines changed

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ build-backend = "setuptools.build_meta"
88

99
[project]
1010
name = "dataops-testgen"
11-
version = "4.22.2"
11+
version = "4.26.1"
1212
description = "DataKitchen's Data Quality DataOps TestGen"
1313
authors = [
1414
{ "name" = "DataKitchen, Inc.", "email" = "[email protected]" },
@@ -58,6 +58,7 @@ dependencies = [
5858
"pydantic==1.10.13",
5959
"streamlit-pydantic==0.6.0",
6060
"cron-converter==1.2.1",
61+
"cron-descriptor==2.0.5",
6162

6263
# Pinned to match the manually compiled libs or for security
6364
"pyarrow==18.1.0",

testgen/commands/run_execute_tests.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,12 @@ def run_execution_steps_in_background(project_code, test_suite):
125125
if settings.IS_DEBUG:
126126
LOG.info(msg + ". Running in debug mode (new thread instead of new process).")
127127
empty_cache()
128+
username = None
129+
if session.auth:
130+
username = session.auth.user_display
128131
background_thread = threading.Thread(
129132
target=run_execution_steps,
130-
args=(project_code, test_suite, session.auth.user_display),
133+
args=(project_code, test_suite, username),
131134
)
132135
background_thread.start()
133136
else:

testgen/commands/run_launch_db_config.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,10 @@ def _get_params_mapping() -> dict:
7272

7373

7474
@with_database_session
75-
def run_launch_db_config(delete_db: bool) -> None:
75+
def run_launch_db_config(delete_db: bool, drop_users_and_roles: bool = True) -> None:
7676
params_mapping = _get_params_mapping()
7777

78-
create_database(get_tg_db(), params_mapping, drop_existing=delete_db, drop_users_and_roles=True)
78+
create_database(get_tg_db(), params_mapping, drop_existing=delete_db, drop_users_and_roles=drop_users_and_roles)
7979

8080
queries = get_queries_for_command("dbsetup", params_mapping)
8181

@@ -91,4 +91,8 @@ def run_launch_db_config(delete_db: bool) -> None:
9191
project_code=settings.PROJECT_KEY,
9292
table_groups_name=settings.DEFAULT_TABLE_GROUPS_NAME,
9393
)
94-
).save()
94+
).save()
95+
96+
97+
def get_app_db_params_mapping() -> dict:
98+
return _get_params_mapping()

testgen/commands/run_profiling_bridge.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import testgen.common.process_service as process_service
1111
from testgen import settings
1212
from testgen.commands.queries.profiling_query import CProfilingSQL
13+
from testgen.commands.run_execute_tests import run_execution_steps_in_background
14+
from testgen.commands.run_generate_tests import run_test_gen_queries
1315
from testgen.commands.run_refresh_score_cards_results import run_refresh_score_cards_results
1416
from testgen.common import (
1517
date_service,
@@ -25,6 +27,7 @@
2527
from testgen.common.mixpanel_service import MixpanelService
2628
from testgen.common.models import with_database_session
2729
from testgen.common.models.connection import Connection
30+
from testgen.common.models.test_suite import TestSuite
2831
from testgen.ui.session import session
2932

3033
LOG = logging.getLogger("testgen")
@@ -211,7 +214,7 @@ def run_profiling_in_background(table_group_id):
211214
empty_cache()
212215
background_thread = threading.Thread(
213216
target=run_profiling_queries,
214-
args=(table_group_id, session.auth.user_display),
217+
args=(table_group_id, session.auth.user_display if session.auth else None),
215218
)
216219
background_thread.start()
217220
else:
@@ -238,6 +241,9 @@ def run_profiling_queries(table_group_id: str, username: str | None = None, spin
238241
profiling_run_id = str(uuid.uuid4())
239242

240243
params = get_profiling_params(table_group_id)
244+
needs_monitor_tests_generated = (
245+
bool(params["monitor_test_suite_id"]) and not params["last_complete_profile_run_id"]
246+
)
241247

242248
LOG.info("CurrentStep: Initializing Query Generator")
243249
clsProfiling = CProfilingSQL(params["project_code"], connection.sql_flavor, minutes_offset=minutes_offset)
@@ -471,7 +477,24 @@ def run_profiling_queries(table_group_id: str, username: str | None = None, spin
471477
scoring_duration=(datetime.now(UTC) - end_time).total_seconds(),
472478
)
473479

480+
if needs_monitor_tests_generated:
481+
_generate_monitor_tests(params["project_code"], table_group_id, params["monitor_test_suite_id"])
482+
474483
return f"""
475484
Profiling completed {"with errors. Check log for details." if has_errors else "successfully."}
476485
Run ID: {profiling_run_id}
477486
"""
487+
488+
489+
@with_database_session
490+
def _generate_monitor_tests(project_code: str, table_group_id: str, test_suite_id: str) -> None:
491+
try:
492+
monitor_test_suite = TestSuite.get(test_suite_id)
493+
if not monitor_test_suite:
494+
LOG.info("Skipping test generation on missing monitor test suite")
495+
else:
496+
LOG.info("Generating monitor tests")
497+
run_test_gen_queries(table_group_id, monitor_test_suite.test_suite, "Monitor")
498+
run_execution_steps_in_background(project_code, monitor_test_suite.test_suite)
499+
except Exception:
500+
LOG.exception("Error generating monitor tests")

testgen/commands/run_quick_start.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import click
44

55
from testgen import settings
6-
from testgen.commands.run_launch_db_config import run_launch_db_config
6+
from testgen.commands.run_launch_db_config import get_app_db_params_mapping, run_launch_db_config
77
from testgen.common.credentials import get_tg_schema
88
from testgen.common.database.database_service import (
99
create_database,
@@ -117,6 +117,14 @@ def run_quick_start(delete_target_db: bool) -> None:
117117
delete_db = True
118118
run_launch_db_config(delete_db)
119119

120+
click.echo("Seeding the application db")
121+
app_db_params = get_app_db_params_mapping()
122+
execute_db_queries(
123+
[
124+
(replace_params(read_template_sql_file("initial_data_seeding.sql", "quick_start"), app_db_params), app_db_params),
125+
],
126+
)
127+
120128
# Schema and Populate target db
121129
click.echo(f"Populating target db : {target_db_name}")
122130
execute_db_queries(

testgen/commands/run_refresh_score_cards_results.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def run_refresh_score_cards_results(
7272
history_entry.add_as_cutoff()
7373
definition.save()
7474
LOG.info(
75-
"CurrentStep: Done rereshing scorecard %s in project %s",
75+
"CurrentStep: Done refreshing scorecard %s in project %s",
7676
definition.name,
7777
definition.project_code,
7878
)

testgen/common/database/flavor/flavor_service.py

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from abc import abstractmethod
2-
from typing import Literal, TypedDict
2+
from typing import Any, Literal, TypedDict
3+
from urllib.parse import parse_qs, urlparse
34

45
from testgen.common.encrypt import DecryptText
56

@@ -37,19 +38,21 @@ class FlavorService:
3738
private_key_passphrase = None
3839
http_path = None
3940
catalog = None
41+
warehouse = None
4042

4143
def init(self, connection_params: ConnectionParams):
42-
self.url = connection_params.get("url", None)
44+
self.url = connection_params.get("url") or ""
4345
self.connect_by_url = connection_params.get("connect_by_url", False)
44-
self.username = connection_params.get("project_user")
45-
self.host = connection_params.get("project_host")
46-
self.port = connection_params.get("project_port")
47-
self.dbname = connection_params.get("project_db")
46+
self.username = connection_params.get("project_user") or ""
47+
self.host = connection_params.get("project_host") or ""
48+
self.port = connection_params.get("project_port") or ""
49+
self.dbname = connection_params.get("project_db") or ""
4850
self.flavor = connection_params.get("sql_flavor")
4951
self.dbschema = connection_params.get("table_group_schema", None)
5052
self.connect_by_key = connection_params.get("connect_by_key", False)
51-
self.http_path = connection_params.get("http_path", None)
52-
self.catalog = connection_params.get("catalog", None)
53+
self.http_path = connection_params.get("http_path") or ""
54+
self.catalog = connection_params.get("catalog") or ""
55+
self.warehouse = connection_params.get("warehouse") or ""
5356

5457
password = connection_params.get("project_pw_encrypted", None)
5558
if isinstance(password, memoryview) or isinstance(password, bytes):
@@ -90,3 +93,45 @@ def get_connection_string_from_fields(self) -> str:
9093
@abstractmethod
9194
def get_connection_string_head(self) -> str:
9295
raise NotImplementedError("Subclasses must implement this method")
96+
97+
def get_parts_from_connection_string(self) -> dict[str, Any]:
98+
if self.connect_by_url:
99+
if not self.url:
100+
return {}
101+
102+
parsed_url = urlparse(self.get_connection_string())
103+
credentials, location = (
104+
parsed_url.netloc if "@" in parsed_url.netloc else f"@{parsed_url.netloc}"
105+
).split("@")
106+
username, password = (
107+
credentials if ":" in credentials else f"{credentials}:"
108+
).split(":")
109+
host, port = (
110+
location if ":" in location else f"{location}:"
111+
).split(":")
112+
113+
database = (path_patrs[0] if (path_patrs := parsed_url.path.strip("/").split("/")) else "")
114+
115+
extras = {
116+
param_name: param_values[0]
117+
for param_name, param_values in parse_qs(parsed_url.query or "").items()
118+
}
119+
120+
return {
121+
"username": username,
122+
"password": password,
123+
"host": host,
124+
"port": port,
125+
"dbname": database,
126+
**extras,
127+
}
128+
129+
return {
130+
"username": self.username,
131+
"password": self.password,
132+
"host": self.host,
133+
"port": self.port,
134+
"dbname": self.dbname,
135+
"http_path": self.http_path,
136+
"catalog": self.catalog,
137+
}

testgen/common/database/flavor/snowflake_flavor_service.py

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from cryptography.hazmat.backends import default_backend
44
from cryptography.hazmat.primitives import serialization
5+
from snowflake.sqlalchemy import URL
56

67
from testgen.common.database.flavor.flavor_service import FlavorService
78

@@ -38,25 +39,27 @@ def get_connection_string_from_fields(self):
3839
# optionally + '/[schema]' + '?warehouse=xxx'
3940
# NOTE: Snowflake host should NOT include ".snowflakecomputing.com"
4041

41-
def get_raw_host_name(host):
42-
endings = [
43-
".snowflakecomputing.com",
44-
]
45-
for ending in endings:
46-
if host.endswith(ending):
47-
i = host.index(ending)
48-
return host[0:i]
49-
return host
42+
account, _ = self.host.split(".", maxsplit=1) if "." in self.host else ("", "")
43+
host = self.host
44+
if ".snowflakecomputing.com" not in host:
45+
host = f"{host}.snowflakecomputing.com"
5046

51-
raw_host = get_raw_host_name(self.host)
52-
host = raw_host
53-
if self.port != "443":
54-
host += ":" + self.port
47+
extra_params = {}
48+
if self.warehouse:
49+
extra_params["warehouse"] = self.warehouse
5550

56-
if self.connect_by_key:
57-
return f"snowflake://{self.username}@{host}/{self.dbname}/{self.dbschema}"
58-
else:
59-
return f"snowflake://{self.username}:{quote_plus(self.password)}@{host}/{self.dbname}/{self.dbschema}"
51+
connection_url = URL(
52+
host=host,
53+
port=int(self.port if str(self.port).isdigit() else 443),
54+
account=account,
55+
user=self.username,
56+
password="" if self.connect_by_key else self.password,
57+
database=self.dbname,
58+
schema=self.dbschema or "",
59+
**extra_params,
60+
)
61+
62+
return connection_url
6063

6164
def get_pre_connection_queries(self):
6265
return [

testgen/common/get_pipeline_parms.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ class ProfilingParams(BaseParams):
2121
profile_sample_min_count: int
2222
profile_do_pair_rules: str
2323
profile_pair_rule_pct: int
24+
monitor_test_suite_id: str | None
25+
last_complete_profile_run_id: str | None
2426

2527

2628
class TestGenerationParams(BaseParams):

testgen/common/mixpanel_service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def send_event(self, event_name, include_usage=False, **properties):
5757
properties.setdefault("instance_id", self.instance_id)
5858
properties.setdefault("edition", settings.DOCKER_HUB_REPOSITORY)
5959
properties.setdefault("version", settings.VERSION)
60-
properties.setdefault("username", session.auth.user_display)
60+
properties.setdefault("username", session.auth.user_display if session.auth else None)
6161
properties.setdefault("distinct_id", self.get_distinct_id(properties["username"]))
6262
if include_usage:
6363
properties.update(self.get_usage())

0 commit comments

Comments
 (0)