@@ -92,7 +92,7 @@ def _make_request(
9292 data : Optional [Mapping [str , Any ]] = None ,
9393 params : Optional [Mapping [str , Any ]] = None ,
9494 session_attr : str = "_get_session" ,
95- ) -> Mapping [ str , Any ] :
95+ ) -> requests . Response :
9696 url = f"{ base_url } /{ endpoint } " if endpoint else base_url
9797
9898 num_retries = 0
@@ -107,7 +107,7 @@ def _make_request(
107107 timeout = self .request_timeout ,
108108 )
109109 response .raise_for_status ()
110- return response . json ()
110+ return response
111111 except RequestException as e :
112112 self ._log .error (
113113 f"Request to dbt Cloud API failed for url { url } with method { method } : { e } "
@@ -143,7 +143,7 @@ def create_job(
143143 """
144144 if not description :
145145 description = "A job that runs dbt models, sources, and tests."
146- return self ._make_request (
146+ response = self ._make_request (
147147 method = "post" ,
148148 endpoint = "jobs" ,
149149 base_url = self .api_v2_url ,
@@ -155,7 +155,8 @@ def create_job(
155155 "description" : description ,
156156 "job_type" : "other" ,
157157 },
158- )["data" ]
158+ )
159+ return response .json ()["data" ]
159160
160161 def list_jobs (
161162 self ,
@@ -185,7 +186,7 @@ def list_jobs(
185186 "limit" : DAGSTER_DBT_CLOUD_LIST_JOBS_INDIVIDUAL_REQUEST_LIMIT ,
186187 "offset" : len (results ),
187188 },
188- )["data" ]:
189+ ). json () ["data" ]:
189190 results .extend (jobs )
190191 if len (jobs ) < DAGSTER_DBT_CLOUD_LIST_JOBS_INDIVIDUAL_REQUEST_LIMIT :
191192 break
@@ -201,7 +202,7 @@ def get_job_details(self, job_id: int) -> Mapping[str, Any]:
201202 method = "get" ,
202203 endpoint = f"jobs/{ job_id } " ,
203204 base_url = self .api_v2_url ,
204- )["data" ]
205+ ). json () ["data" ]
205206
206207 def destroy_job (self , job_id : int ) -> Mapping [str , Any ]:
207208 """Destroys a given dbt Cloud job.
@@ -213,7 +214,7 @@ def destroy_job(self, job_id: int) -> Mapping[str, Any]:
213214 method = "delete" ,
214215 endpoint = f"jobs/{ job_id } " ,
215216 base_url = self .api_v2_url ,
216- )["data" ]
217+ ). json () ["data" ]
217218
218219 def trigger_job_run (
219220 self , job_id : int , steps_override : Optional [Sequence [str ]] = None
@@ -237,7 +238,7 @@ def trigger_job_run(
237238 data = {"steps_override" : steps_override , "cause" : DAGSTER_ADHOC_TRIGGER_CAUSE }
238239 if steps_override
239240 else {"cause" : DAGSTER_ADHOC_TRIGGER_CAUSE },
240- )["data" ]
241+ ). json () ["data" ]
241242
242243 def get_runs_batch (
243244 self ,
@@ -278,26 +279,35 @@ def get_runs_batch(
278279 "finished_at__range" : f"""["{ finished_at_lower_bound .isoformat ()} ", "{ finished_at_upper_bound .isoformat ()} "]""" ,
279280 "order_by" : "finished_at" ,
280281 },
281- )
282+ ). json ()
282283 data = cast ("Sequence[Mapping[str, Any]]" , resp ["data" ])
283284 total_count = resp ["extra" ]["pagination" ]["total_count" ]
284285 return data , total_count
285286
286- def get_run_details (self , run_id : int ) -> Mapping [str , Any ]:
287+ def get_run_details (
288+ self , run_id : int , include_related : Optional [Sequence [str ]] = None
289+ ) -> Mapping [str , Any ]:
287290 """Retrieves the details of a given dbt Cloud Run.
288291
289292 Args:
290- run_id (str ): The dbt Cloud Run ID. You can retrieve this value from the
293+ run_id (int ): The dbt Cloud Run ID. You can retrieve this value from the
291294 URL of the given run in the dbt Cloud UI.
295+ include_related (Optional[Sequence[str]]): List of related fields to pull with the run.
296+ Valid values are "trigger", "job", "debug_logs", and "run_steps".
292297
293298 Returns:
294299 Dict[str, Any]: Parsed json data representing the API response.
295300 """
301+ params = {}
302+ if include_related :
303+ params ["include_related" ] = "," .join (include_related )
304+
296305 return self ._make_request (
297306 method = "get" ,
298307 endpoint = f"runs/{ run_id } " ,
299308 base_url = self .api_v2_url ,
300- )["data" ]
309+ params = params ,
310+ ).json ()["data" ]
301311
302312 def poll_run (
303313 self ,
@@ -352,21 +362,25 @@ def list_run_artifacts(
352362 endpoint = f"runs/{ run_id } /artifacts" ,
353363 base_url = self .api_v2_url ,
354364 session_attr = "_get_artifact_session" ,
355- )["data" ],
365+ ). json () ["data" ],
356366 )
357367
358368 def get_run_artifact (self , run_id : int , path : str ) -> Mapping [str , Any ]:
359369 """Retrieves an artifact at the given path for a given dbt Cloud Run.
360370
371+ Args:
372+ run_id (int): The dbt Cloud Run ID.
373+ path (str): The path to the artifact (e.g., "run_results.json", "manifest.json").
374+
361375 Returns:
362- Dict[str, Any]: Parsed json data representing the API response .
376+ Dict[str, Any]: Parsed json data representing the artifact .
363377 """
364378 return self ._make_request (
365379 method = "get" ,
366380 endpoint = f"runs/{ run_id } /artifacts/{ path } " ,
367381 base_url = self .api_v2_url ,
368382 session_attr = "_get_artifact_session" ,
369- )
383+ ). json ()
370384
371385 def get_run_results_json (self , run_id : int ) -> Mapping [str , Any ]:
372386 """Retrieves the run_results.json artifact of a given dbt Cloud Run.
@@ -384,6 +398,65 @@ def get_run_manifest_json(self, run_id: int) -> Mapping[str, Any]:
384398 """
385399 return self .get_run_artifact (run_id = run_id , path = "manifest.json" )
386400
401+ def get_run_logs (self , run_id : int , max_retries : int = 3 , retry_delay : float = 2.0 ) -> str :
402+ """Retrieves the stdout/stderr logs from a given dbt Cloud Run.
403+
404+ This method fetches logs from the run_steps field by calling get_run_details
405+ with include_related=["run_steps"]. Each step contains a logs field with
406+ the stdout/stderr output for that step.
407+
408+ Note: There can be a slight delay between when a run completes and when the logs
409+ are fully populated in the API. This method will retry a few times if it detects
410+ completed steps with empty logs.
411+
412+ Args:
413+ run_id (int): The dbt Cloud Run ID.
414+ max_retries (int): Maximum number of times to retry fetching logs if empty. Defaults to 3.
415+ retry_delay (float): Time in seconds to wait between retries. Defaults to 2.0.
416+
417+ Returns:
418+ str: The concatenated log text content from all run steps.
419+ """
420+ for attempt in range (max_retries ):
421+ run_details = self .get_run_details (run_id = run_id , include_related = ["run_steps" ])
422+
423+ logs_parts = []
424+ run_steps = run_details .get ("run_steps" , [])
425+ completed_steps_with_empty_logs = 0
426+
427+ for step in run_steps :
428+ step_name = step .get ("name" , "Unknown Step" )
429+ step_logs = step .get ("logs" , "" )
430+ step_status = step .get ("status_humanized" , "unknown" )
431+
432+ # Track completed steps with empty logs
433+ if step_status == "Success" and not step_logs :
434+ completed_steps_with_empty_logs += 1
435+
436+ if step_logs :
437+ logs_parts .append (f"=== Step: { step_name } ===" )
438+ logs_parts .append (step_logs )
439+ logs_parts .append ("" ) # Empty line between steps
440+
441+ # If we have completed steps with empty logs and retries left, wait and try again
442+ if completed_steps_with_empty_logs > 0 and attempt < max_retries - 1 :
443+ self ._log .warning (
444+ f"Found { completed_steps_with_empty_logs } completed steps with empty logs for run { run_id } . "
445+ f"Retrying in { retry_delay } seconds..."
446+ )
447+ time .sleep (retry_delay )
448+ continue
449+
450+ # Either we got all logs or we're out of retries
451+ if completed_steps_with_empty_logs > 0 :
452+ self ._log .warning (
453+ f"Still missing logs for { completed_steps_with_empty_logs } completed steps after { max_retries } attempts"
454+ )
455+
456+ return "\n " .join (logs_parts ) if logs_parts else ""
457+
458+ return ""
459+
387460 def get_project_details (self , project_id : int ) -> Mapping [str , Any ]:
388461 """Retrieves the details of a given dbt Cloud Project.
389462
@@ -398,7 +471,7 @@ def get_project_details(self, project_id: int) -> Mapping[str, Any]:
398471 method = "get" ,
399472 endpoint = f"projects/{ project_id } " ,
400473 base_url = self .api_v2_url ,
401- )["data" ]
474+ ). json () ["data" ]
402475
403476 def get_environment_details (self , environment_id : int ) -> Mapping [str , Any ]:
404477 """Retrieves the details of a given dbt Cloud Environment.
@@ -414,7 +487,7 @@ def get_environment_details(self, environment_id: int) -> Mapping[str, Any]:
414487 method = "get" ,
415488 endpoint = f"environments/{ environment_id } " ,
416489 base_url = self .api_v2_url ,
417- )["data" ]
490+ ). json () ["data" ]
418491
419492 def get_account_details (self ) -> Mapping [str , Any ]:
420493 """Retrieves the details of the account associated to the dbt Cloud workspace.
@@ -426,7 +499,7 @@ def get_account_details(self) -> Mapping[str, Any]:
426499 method = "get" ,
427500 endpoint = None ,
428501 base_url = self .api_v2_url ,
429- )["data" ]
502+ ). json () ["data" ]
430503
431504 def verify_connection (self ) -> None :
432505 """Verifies the connection to the dbt Cloud REST API."""
0 commit comments