Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 63 additions & 8 deletions src/pytfe/resources/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@
RequiredWorkspaceError,
TerraformVersionValidForPlanOnlyError,
)
from ..models.apply import Apply
from ..models.comment import Comment
from ..models.configuration_version import ConfigurationVersion
from ..models.cost_estimate import CostEstimate
from ..models.plan import Plan
from ..models.policy_check import PolicyCheck
from ..models.run import (
Run,
RunApplyOptions,
Expand All @@ -21,10 +27,57 @@
RunListOptions,
RunReadOptions,
)
from ..models.run_event import RunEvent
from ..models.task_stage import TaskStage
from ..models.user import User
from ..models.workspace import Workspace
from ..utils import _safe_str, valid_string, valid_string_id
from ._base import _Service


def transform_relationships(relationships: dict) -> Any:
"""
Transform relationships dict to map relationship names to their model objects.
Single IDs become model instances, multiple IDs become lists of model instances.
"""
result = {}

# Map relationship keys to their model constructors
model_map = {
"apply": Apply,
"configuration-version": ConfigurationVersion,
"cost-estimate": CostEstimate,
"created-by": User,
"confirmed-by": User,
"plan": Plan,
"workspace": Workspace,
"policy-checks": PolicyCheck,
"run-events": RunEvent,
"task-stages": TaskStage,
"comments": Comment,
}

for key, value in relationships.items():
data = value.get("data")

if data is None:
continue

model_class = model_map.get(key)
if not model_class:
# Unknown relationship type, skip it
continue

if isinstance(data, list):
# Multiple entries - create list of model instances
result[key] = [model_class(id=item["id"]) for item in data if "id" in item]
elif isinstance(data, dict) and "id" in data:
# Single entry - create model instance
result[key] = model_class(id=data["id"])

return result


class Runs(_Service):
def list(
self, workspace_id: str, options: RunListOptions | None = None
Expand Down Expand Up @@ -94,10 +147,11 @@ def create(self, options: RunCreateOptions) -> Run:
)
d = r.json().get("data", {})
attrs = d.get("attributes", {})
return Run(
id=_safe_str(d.get("id")),
**{k.replace("-", "_"): v for k, v in attrs.items()},
)
relationships = transform_relationships(d.get("relationships", {}))
combined = {
k.replace("-", "_"): v for k, v in {**attrs, **relationships}.items()
}
return Run(id=_safe_str(d.get("id")), **combined)

def read(self, run_id: str) -> Run:
"""Read a run by its ID."""
Expand All @@ -119,10 +173,11 @@ def read_with_options(
)
d = r.json().get("data", {})
attrs = d.get("attributes", {})
return Run(
id=_safe_str(d.get("id")),
**{k.replace("-", "_"): v for k, v in attrs.items()},
)
relationships = transform_relationships(d.get("relationships", {}))
combined = {
k.replace("-", "_"): v for k, v in {**attrs, **relationships}.items()
}
return Run(id=_safe_str(d.get("id")), **combined)

def apply(self, run_id: str, options: RunApplyOptions | None = None) -> None:
"""Apply a run by its ID."""
Expand Down
111 changes: 107 additions & 4 deletions tests/units/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,14 +294,113 @@ def test_read_with_options_success(self, runs_service):
mock_response_data = {
"data": {
"id": "run-detailed-123",
"type": "runs",
"attributes": {
"status": "planned",
"source": "tfe-api",
"message": "Detailed read test",
"created-at": "2023-01-01T12:00:00Z",
"actions": {
"is-cancelable": False,
"is-confirmable": False,
"is-discardable": False,
"is-force-cancelable": False,
},
"allow-config-generation": False,
"allow-empty-apply": False,
"auto-apply": False,
"canceled-at": None,
"created-at": "2026-02-19T01:58:46.126Z",
"has-changes": True,
"is-destroy": False,
"message": "Triggered via CLI",
"plan-only": False,
"refresh": True,
"refresh-only": False,
"replace-addrs": None,
"save-plan": False,
"source": "terraform+cloud",
"status-timestamps": {
"errored-at": "2026-02-19T01:59:19+00:00",
"planned-at": "2026-02-19T01:59:16+00:00",
"queuing-at": "2026-02-19T01:58:46+00:00",
"planning-at": "2026-02-19T01:58:48+00:00",
"plan-queued-at": "2026-02-19T01:58:46+00:00",
"plan-queueable-at": "2026-02-19T01:58:46+00:00",
},
"status": "errored",
"target-addrs": None,
"trigger-reason": "manual",
"terraform-version": "1.13.5",
"updated-at": "2026-02-19T01:59:19.891Z",
"permissions": {
"can-apply": True,
"can-cancel": True,
"can-comment": True,
"can-discard": True,
"can-force-execute": True,
"can-force-cancel": True,
"can-override-policy-check": True,
},
"variables": [],
"invoke-action-addrs": None,
},
"relationships": {
"workspace": {
"data": {"id": "ws-a2Kntu53K79hsPRH", "type": "workspaces"}
},
"apply": {
"data": {"id": "apply-Y1rVt6MpiwzdMjbK", "type": "applies"},
"links": {"related": "/api/v2/runs/run-detailed-123/apply"},
},
"configuration-version": {
"data": {
"id": "cv-bakH4hn9cPXb2yZq",
"type": "configuration-versions",
},
"links": {
"related": "/api/v2/runs/run-detailed-123/configuration-version"
},
},
"created-by": {
"data": {"id": "user-FRJGnNMX6fpe9Cdd", "type": "users"},
"links": {
"related": "/api/v2/runs/run-detailed-123/created-by"
},
},
"plan": {
"data": {"id": "plan-WooDdHWZnSE3Zs8j", "type": "plans"},
"links": {"related": "/api/v2/runs/run-detailed-123/plan"},
},
"run-events": {
"data": [
{"id": "re-bqJGaaCrt5QZfexJ", "type": "run-events"},
{"id": "re-j8d6eWyfyHSUbX7x", "type": "run-events"},
{"id": "re-UAXd9VyRTXZy3hpx", "type": "run-events"},
{"id": "re-DFFf51Doi8mmHC9G", "type": "run-events"},
{"id": "re-U2m4RMQhEY9voN1K", "type": "run-events"},
{"id": "re-WWfUbu5NTWdYKgBs", "type": "run-events"},
],
"links": {
"related": "/api/v2/runs/run-detailed-123/run-events"
},
},
"task-stages": {
"data": [],
"links": {
"related": "/api/v2/runs/run-detailed-123/task-stages"
},
},
"policy-checks": {
"data": [
{"id": "polchk-JxgtJ56kFifnngyT", "type": "policy-checks"}
],
"links": {
"related": "/api/v2/runs/run-detailed-123/policy-checks"
},
},
"comments": {
"data": [],
"links": {"related": "/api/v2/runs/run-detailed-123/comments"},
},
},
"links": {"self": "/api/v2/runs/run-detailed-123"},
}
}

Expand Down Expand Up @@ -330,6 +429,10 @@ def test_read_with_options_success(self, runs_service):
# Verify result
assert isinstance(result, Run)
assert result.id == "run-detailed-123"
assert result.created_by.id == "user-FRJGnNMX6fpe9Cdd"
assert result.plan.id == "plan-WooDdHWZnSE3Zs8j"
assert result.apply.id == "apply-Y1rVt6MpiwzdMjbK"
assert result.workspace.id == "ws-a2Kntu53K79hsPRH"

def test_apply_run_success(self, runs_service):
"""Test successful apply operation."""
Expand Down
Loading