Skip to content

Commit 1fc84c3

Browse files
authored
Move version utilities to new package and add initial parse conformance script (#254)
1 parent 5f24bcb commit 1fc84c3

File tree

22 files changed

+900
-66
lines changed

22 files changed

+900
-66
lines changed

packages/dbt_fusion_package_tools/pyproject.toml

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@ name = "dbt-fusion-package-tools"
33
description = "Add your description here"
44
readme = "README.md"
55
requires-python = ">=3.9"
6-
dependencies = []
6+
dependencies = [
7+
"mashumaro>=3.17",
8+
"pyyaml>=6.0.2",
9+
]
710

811
dynamic = ["version"]
912

1013
[project.scripts]
1114
dbt-fusion-package-tools = "dbt_fusion_package_tools:main"
15+
dbt-fusion-package-schema-compat = "dbt_fusion_package_tools.check_parse_conformance:main"
1216

1317
[tool.uv]
1418
package = true
@@ -22,12 +26,6 @@ source = "scm"
2226
version_format = "pdm_build:format_version"
2327

2428
[project.optional-dependencies]
25-
test = [
26-
"pytest>=7.2.0",
27-
"pytest-cov>=4.1.0",
28-
"pre-commit>=4.2.0",
29-
"nox",
30-
]
3129
docs = [
3230
"pydocstyle>=6.3.0",
33-
]
31+
]
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
"""Interface for objects useful to processing hub entries"""
2+
3+
import os
4+
from typing import Optional
5+
import subprocess
6+
from rich.console import Console
7+
from dbt_fusion_package_tools.exceptions import FusionBinaryNotAvailable
8+
9+
from pathlib import Path
10+
11+
console = Console()
12+
error_console = Console(stderr=True)
13+
14+
15+
def check_binary_name(binary_name: str) -> bool:
16+
try:
17+
subprocess.run(
18+
[
19+
binary_name,
20+
"--version",
21+
],
22+
capture_output=True,
23+
timeout=60,
24+
check=True,
25+
)
26+
return True
27+
# indicates that the binary name is not found
28+
except FileNotFoundError:
29+
error_console.log(f"FileNotFoundError: {binary_name} not found on system path")
30+
return False
31+
# indicates that an error occurred when running command
32+
except subprocess.CalledProcessError as process_error:
33+
error_console.log(
34+
f"CalledProcessError: {binary_name} --version exited with return code {process_error.returncode}"
35+
)
36+
error_console.log(process_error.stderr)
37+
return False
38+
except Exception as other_error:
39+
error_console.log(f"{other_error}: An unknown exception occured when running {binary_name} --version")
40+
return False
41+
42+
43+
def find_fusion_binary(custom_name: Optional[str] = None) -> Optional[str]:
44+
possible_binary_names: list[str] = ["dbtf", "dbt"] if custom_name is None else [custom_name]
45+
46+
binary_names_found: set[str] = set()
47+
# test each name
48+
for binary_name in possible_binary_names:
49+
# first check if name exists at all
50+
binary_exists = check_binary_name(binary_name)
51+
if binary_exists:
52+
binary_names_found.add(binary_name)
53+
54+
if len(binary_names_found) == 0:
55+
return None
56+
57+
# now check version returned by each and use first one
58+
# don't need exception handling here because previous step already did it
59+
for valid_binary_name in binary_names_found:
60+
version_result = subprocess.run(
61+
[
62+
valid_binary_name,
63+
"--version",
64+
],
65+
capture_output=True,
66+
timeout=60,
67+
text=True,
68+
)
69+
if "dbt-fusion" in version_result.stdout:
70+
return valid_binary_name
71+
72+
# if we got to the end, then no fusion version has been found
73+
error_console.log(f"Could not find Fusion binary, latest version output is {version_result.stdout}")
74+
return None
75+
76+
77+
def check_fusion_schema_compatibility(repo_path: Path = Path.cwd(), show_fusion_output=True) -> bool:
78+
"""
79+
Check if a dbt package is fusion schema compatible by running 'dbtf parse'.
80+
81+
Args:
82+
repo_path: Path to the dbt package repository
83+
84+
Returns:
85+
True if fusion compatible (dbtf parse exits with code 0), False otherwise
86+
"""
87+
# Add a test profiles.yml to the current directory
88+
profiles_path = repo_path / Path("profiles.yml")
89+
try:
90+
with open(profiles_path, "a") as f:
91+
f.write(
92+
"\n"
93+
"test_schema_compat:\n"
94+
" target: dev\n"
95+
" outputs:\n"
96+
" dev:\n"
97+
" type: postgres\n"
98+
" host: localhost\n"
99+
" port: 5432\n"
100+
" user: postgres\n"
101+
" password: postgres\n"
102+
" dbname: postgres\n"
103+
" schema: public\n"
104+
)
105+
106+
# Ensure the `_DBT_FUSION_STRICT_MODE` is set (this will ensure fusion errors on schema violations)
107+
os.environ["_DBT_FUSION_STRICT_MODE"] = "1"
108+
109+
# Find correct name for Fusion binary
110+
fusion_binary_name: Optional[str] = find_fusion_binary()
111+
if fusion_binary_name is None:
112+
raise FusionBinaryNotAvailable()
113+
114+
try:
115+
# Run dbt deps to install package dependencies
116+
subprocess.run(
117+
[
118+
fusion_binary_name,
119+
"deps",
120+
"--profile",
121+
"test_schema_compat",
122+
"--project-dir",
123+
str(repo_path),
124+
],
125+
text=True,
126+
capture_output=(not show_fusion_output),
127+
timeout=60,
128+
)
129+
# Now try parse
130+
parse_result = subprocess.run(
131+
[
132+
fusion_binary_name,
133+
"parse",
134+
"--profile",
135+
"test_schema_compat",
136+
"--project-dir",
137+
str(repo_path),
138+
],
139+
text=True,
140+
capture_output=(not show_fusion_output),
141+
timeout=60,
142+
)
143+
except Exception as e:
144+
error_console.log(f"{e}: An unknown error occurred when running dbt parse")
145+
return False
146+
147+
# Return True if exit code is 0 (success)
148+
is_compatible = parse_result.returncode == 0
149+
150+
if is_compatible:
151+
console.log(f"Package at {repo_path} is fusion schema compatible")
152+
else:
153+
console.log(f"Package at {repo_path} is not fusion schema compatible")
154+
155+
# Clean up deps
156+
subprocess.run(
157+
[
158+
fusion_binary_name,
159+
"clean",
160+
"--profile",
161+
"test_schema_compat",
162+
"--project-dir",
163+
str(repo_path),
164+
],
165+
timeout=60,
166+
text=True,
167+
capture_output=(not show_fusion_output),
168+
)
169+
# Remove the test profile
170+
os.remove(profiles_path)
171+
172+
return is_compatible
173+
174+
except Exception as e:
175+
error_console.log(f"Error checking fusion compatibility for {repo_path}: {str(e)}")
176+
try:
177+
os.remove(profiles_path)
178+
except Exception:
179+
pass
180+
return False
181+
182+
183+
def main():
184+
check_fusion_schema_compatibility(Path.cwd())
185+
186+
187+
if __name__ == "__main__":
188+
main()
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from typing import Optional
2+
3+
4+
class SemverError(Exception):
5+
def __init__(self, msg: Optional[str] = None) -> None:
6+
self.msg = msg
7+
if msg is not None:
8+
super().__init__(msg)
9+
else:
10+
super().__init__()
11+
12+
13+
class VersionsNotCompatibleError(SemverError):
14+
pass
15+
16+
17+
class FusionBinaryNotAvailable(Exception):
18+
def __init__(self, message="Fusion binary not found on system, please install Fusion first"):
19+
self.message = message
20+
super().__init__(self.message)
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
def output_package_name():
2-
print("====== Package Name =======")
2+
print("====== Package Name =======")

0 commit comments

Comments
 (0)