Skip to content

Commit 5da1b32

Browse files
committed
Add --json parameter and test for it
1 parent 70ef62b commit 5da1b32

3 files changed

Lines changed: 235 additions & 12 deletions

File tree

src/dbt_jobs_as_code/main.py

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import os
23
import sys
34
from importlib.metadata import version
@@ -10,7 +11,7 @@
1011
from ruamel.yaml import YAML
1112

1213
from dbt_jobs_as_code.client import DBTCloud
13-
from dbt_jobs_as_code.cloud_yaml_mapping.change_set import build_change_set
14+
from dbt_jobs_as_code.cloud_yaml_mapping.change_set import build_change_set, json_serializer_type
1415
from dbt_jobs_as_code.cloud_yaml_mapping.validate_link import can_be_linked
1516
from dbt_jobs_as_code.exporter.export import export_jobs_yml
1617
from dbt_jobs_as_code.importer import check_job_fields, fetch_jobs, get_account_id
@@ -58,6 +59,13 @@
5859
help="The path to your vars_yml YML file (or pattern for those files) when using a templated job YML file.",
5960
)
6061

62+
option_json_output = click.option(
63+
"--json",
64+
"output_json",
65+
is_flag=True,
66+
help="Output results in JSON format instead of human-readable text.",
67+
)
68+
6169

6270
@click.group(
6371
help=f"dbt-jobs-as-code {VERSION}\n\nA CLI to allow defining dbt Cloud jobs as code",
@@ -75,13 +83,15 @@ def cli() -> None:
7583
@option_project_ids
7684
@option_environment_ids
7785
@option_limit_projects_envs_to_yml
86+
@option_json_output
7887
def sync(
7988
config: str,
8089
vars_yml,
8190
project_id,
8291
environment_id,
8392
limit_projects_envs_to_yml,
8493
disable_ssl_verification,
94+
output_json: bool,
8595
):
8696
"""Synchronize a dbt Cloud job config file against dbt Cloud.
8797
This command will update dbt Cloud with the changes in the local YML file. It is recommended to run a `plan` first to see what will be changed.
@@ -111,13 +121,20 @@ def sync(
111121
cloud_project_ids,
112122
cloud_environment_ids,
113123
limit_projects_envs_to_yml,
124+
output_json=output_json,
114125
)
115126
if len(change_set) == 0:
116-
logger.success("-- SYNC -- No changes detected.")
127+
if output_json:
128+
print(json.dumps({"job_changes": [], "env_var_overwrite_changes": []}))
129+
else:
130+
logger.success("-- SYNC -- No changes detected.")
117131
else:
118-
logger.info("-- SYNC -- {count} changes detected.", count=len(change_set))
119-
console = Console()
120-
console.log(change_set.to_table())
132+
if output_json:
133+
print(json.dumps(change_set.to_json()))
134+
else:
135+
logger.info("-- SYNC -- {count} changes detected.", count=len(change_set))
136+
console = Console()
137+
console.log(change_set.to_table())
121138
change_set.apply()
122139

123140
if not change_set.apply_success:
@@ -132,13 +149,15 @@ def sync(
132149
@option_project_ids
133150
@option_environment_ids
134151
@option_limit_projects_envs_to_yml
152+
@option_json_output
135153
def plan(
136154
config: str,
137155
vars_yml: str,
138156
project_id: List[int],
139157
environment_id: List[int],
140158
limit_projects_envs_to_yml: bool,
141159
disable_ssl_verification: bool,
160+
output_json: bool,
142161
):
143162
"""Check the difference between a local file and dbt Cloud without updating dbt Cloud.
144163
This command will not update dbt Cloud.
@@ -167,13 +186,20 @@ def plan(
167186
cloud_project_ids,
168187
cloud_environment_ids,
169188
limit_projects_envs_to_yml,
189+
output_json=output_json,
170190
)
171191
if len(change_set) == 0:
172-
logger.success("-- PLAN -- No changes detected.")
192+
if output_json:
193+
print(json.dumps({"job_changes": [], "env_var_overwrite_changes": []}))
194+
else:
195+
logger.success("-- PLAN -- No changes detected.")
173196
else:
174-
logger.info("-- PLAN -- {count} changes detected.", count=len(change_set))
175-
console = Console()
176-
console.log(change_set.to_table())
197+
if output_json:
198+
print(json.dumps(change_set.to_json(), default=json_serializer_type))
199+
else:
200+
logger.info("-- PLAN -- {count} changes detected.", count=len(change_set))
201+
console = Console()
202+
console.log(change_set.to_table())
177203

178204

179205
@cli.command()

tests/schemas/test_check_job_mapping_same.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,8 @@ def test_check_job_mapping_same():
3131
triggers={},
3232
)
3333

34-
assert not check_job_mapping_same(mock_job1, mock_job2)
34+
# Test that the jobs are different
35+
same, diff = check_job_mapping_same(mock_job1, mock_job2)
36+
assert not same
37+
assert diff is not None
38+
assert diff["status"] == "different"

tests/test_main.py

Lines changed: 195 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1-
from unittest.mock import patch
1+
import json
2+
from unittest.mock import Mock, patch
23

34
import pytest
45
from click.testing import CliRunner
56

6-
from dbt_jobs_as_code.main import import_jobs
7+
from dbt_jobs_as_code.cloud_yaml_mapping.change_set import Change, ChangeSet
8+
from dbt_jobs_as_code.main import cli, import_jobs
79
from dbt_jobs_as_code.schemas.common_types import Settings, Triggers
810
from dbt_jobs_as_code.schemas.job import JobDefinition
911

12+
# ============= Fixtures =============
13+
1014

1115
@pytest.fixture
1216
def mock_dbt_cloud():
@@ -55,6 +59,55 @@ def mock_dbt_cloud():
5559
yield instance
5660

5761

62+
@pytest.fixture
63+
def mock_change_set():
64+
"""Create a mock change set with both job and env var changes"""
65+
change_set = ChangeSet()
66+
67+
# Add a job change
68+
change_set.append(
69+
Change(
70+
identifier="job1",
71+
type="job",
72+
action="update",
73+
proj_id=123,
74+
env_id=456,
75+
sync_function=Mock(),
76+
parameters={},
77+
differences={
78+
"values_changed": {
79+
"root['name']": {"new_value": "new_name", "old_value": "old_name"}
80+
}
81+
},
82+
)
83+
)
84+
85+
# Add an env var change
86+
change_set.append(
87+
Change(
88+
identifier="job1:DBT_VAR1",
89+
type="env var overwrite",
90+
action="update",
91+
proj_id=123,
92+
env_id=456,
93+
sync_function=Mock(),
94+
parameters={},
95+
differences={"old_value": "old_val", "new_value": "new_val"},
96+
)
97+
)
98+
99+
return change_set
100+
101+
102+
@pytest.fixture
103+
def mock_empty_change_set():
104+
"""Create an empty change set"""
105+
return ChangeSet()
106+
107+
108+
# ============= Import Command Tests =============
109+
110+
58111
def test_import_jobs_managed_only(mock_dbt_cloud):
59112
"""Test that --managed-only flag only imports jobs with identifiers"""
60113
runner = CliRunner()
@@ -98,3 +151,143 @@ def test_import_jobs_without_managed_only(mock_dbt_cloud):
98151
assert "managed-job-1" in result.stdout
99152
assert "managed-job-2" in result.stdout
100153
assert "Unmanaged Job" in result.stdout
154+
155+
156+
# ============= Plan Command Tests =============
157+
158+
159+
@patch("dbt_jobs_as_code.main.build_change_set")
160+
def test_plan_command_json_output(mock_build_change_set, mock_change_set):
161+
"""Test that plan command produces valid JSON output when --json flag is used"""
162+
mock_build_change_set.return_value = mock_change_set
163+
164+
runner = CliRunner()
165+
result = runner.invoke(cli, ["plan", "--json", "config.yml"])
166+
167+
assert result.exit_code == 0
168+
169+
# Verify the output is valid JSON
170+
json_output = json.loads(result.output)
171+
172+
# Verify structure
173+
assert "job_changes" in json_output
174+
assert "env_var_overwrite_changes" in json_output
175+
176+
# Verify job changes
177+
assert len(json_output["job_changes"]) == 1
178+
job_change = json_output["job_changes"][0]
179+
assert job_change["identifier"] == "job1"
180+
assert job_change["action"] == "UPDATE"
181+
assert "differences" in job_change
182+
183+
# Verify env var changes
184+
assert len(json_output["env_var_overwrite_changes"]) == 1
185+
env_var_change = json_output["env_var_overwrite_changes"][0]
186+
assert env_var_change["identifier"] == "job1:DBT_VAR1"
187+
assert env_var_change["action"] == "UPDATE"
188+
assert "differences" in env_var_change
189+
190+
191+
@patch("dbt_jobs_as_code.main.build_change_set")
192+
def test_plan_command_json_output_no_changes(mock_build_change_set, mock_empty_change_set):
193+
"""Test that plan command produces valid JSON output with no changes"""
194+
mock_build_change_set.return_value = mock_empty_change_set
195+
196+
runner = CliRunner()
197+
result = runner.invoke(cli, ["plan", "--json", "config.yml"])
198+
199+
assert result.exit_code == 0
200+
201+
# Verify the output is valid JSON
202+
json_output = json.loads(result.output)
203+
204+
# Verify structure
205+
assert json_output == {
206+
"job_changes": [],
207+
"env_var_overwrite_changes": [],
208+
}
209+
210+
211+
@patch("dbt_jobs_as_code.main.build_change_set")
212+
def test_plan_command_regular_output(mock_build_change_set, mock_change_set):
213+
"""Test that plan command produces regular output when --json flag is not used"""
214+
mock_build_change_set.return_value = mock_change_set
215+
216+
runner = CliRunner()
217+
result = runner.invoke(cli, ["plan", "config.yml"])
218+
219+
assert result.exit_code == 0
220+
221+
# Verify this is not JSON
222+
with pytest.raises(json.JSONDecodeError):
223+
json.loads(result.output)
224+
225+
226+
# ============= Sync Command Tests =============
227+
228+
229+
@patch("dbt_jobs_as_code.main.build_change_set")
230+
def test_sync_command_json_output(mock_build_change_set, mock_change_set):
231+
"""Test that sync command produces valid JSON output when --json flag is used"""
232+
mock_build_change_set.return_value = mock_change_set
233+
234+
runner = CliRunner()
235+
result = runner.invoke(cli, ["sync", "--json", "config.yml"])
236+
237+
assert result.exit_code == 0
238+
239+
# Verify the output is valid JSON
240+
json_output = json.loads(result.output)
241+
242+
# Verify structure
243+
assert "job_changes" in json_output
244+
assert "env_var_overwrite_changes" in json_output
245+
246+
# Verify job changes
247+
assert len(json_output["job_changes"]) == 1
248+
job_change = json_output["job_changes"][0]
249+
assert job_change["identifier"] == "job1"
250+
assert job_change["action"] == "UPDATE"
251+
assert "differences" in job_change
252+
253+
# Verify env var changes
254+
assert len(json_output["env_var_overwrite_changes"]) == 1
255+
env_var_change = json_output["env_var_overwrite_changes"][0]
256+
assert env_var_change["identifier"] == "job1:DBT_VAR1"
257+
assert env_var_change["action"] == "UPDATE"
258+
assert "differences" in env_var_change
259+
260+
261+
@patch("dbt_jobs_as_code.main.build_change_set")
262+
def test_sync_command_json_output_no_changes(mock_build_change_set, mock_empty_change_set):
263+
"""Test that sync command produces valid JSON output with no changes"""
264+
mock_build_change_set.return_value = mock_empty_change_set
265+
266+
runner = CliRunner()
267+
result = runner.invoke(cli, ["sync", "--json", "config.yml"])
268+
269+
assert result.exit_code == 0
270+
271+
# Verify the output is valid JSON
272+
json_output = json.loads(result.output)
273+
274+
# Verify structure
275+
assert json_output == {
276+
"job_changes": [],
277+
"env_var_overwrite_changes": [],
278+
}
279+
280+
281+
@patch("dbt_jobs_as_code.main.build_change_set")
282+
def test_sync_command_regular_output(mock_build_change_set, mock_change_set):
283+
"""Test that sync command produces regular output when --json flag is not used"""
284+
mock_build_change_set.return_value = mock_change_set
285+
286+
runner = CliRunner()
287+
result = runner.invoke(cli, ["sync", "config.yml"])
288+
289+
assert result.exit_code == 0
290+
291+
# Verify this is not JSON
292+
with pytest.raises(json.JSONDecodeError):
293+
json.loads(result.output)

0 commit comments

Comments
 (0)