Skip to content

Commit 5fd26cb

Browse files
Add TaskStage API support with models, resource, tests and examples
1 parent 26ff34e commit 5fd26cb

6 files changed

Lines changed: 422 additions & 2 deletions

File tree

examples/task_stage_example.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""
2+
Example usage of TaskStages API
3+
4+
This demonstrates how to:
5+
- Read a task stage
6+
- List task stages for a run
7+
- Override a task stage
8+
"""
9+
10+
from pytfe.client import TFEClient
11+
12+
# Initialize client (make sure your auth/env is configured)
13+
client = TFEClient()
14+
15+
# ---------------------------
16+
# Read a task stage
17+
# ---------------------------
18+
# Fetch a single task stage by ID
19+
# Replace "ts-123" with a real task stage ID
20+
stage = client.task_stages.read("ts-abc123xyz")
21+
print(stage)
22+
23+
24+
# ---------------------------
25+
# List task stages for a run
26+
# ---------------------------
27+
# Fetch all task stages for a run
28+
# Replace "run-123" with a real run ID
29+
# for stage in client.task_stages.list("run-123"):
30+
# print(stage)
31+
32+
33+
# ---------------------------
34+
# Override a task stage
35+
# ---------------------------
36+
# Override a task stage (if allowed)
37+
# Replace "ts-123" with a real task stage ID
38+
# client.task_stages.override("ts-123", comment="Approved")

src/pytfe/client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from .resources.run_event import RunEvents
3535
from .resources.run_task import RunTasks
3636
from .resources.run_trigger import RunTriggers
37+
from .resources.task_stage import TaskStages
3738
from .resources.ssh_keys import SSHKeys
3839
from .resources.state_version_outputs import StateVersionOutputs
3940
from .resources.state_versions import StateVersions
@@ -94,6 +95,7 @@ def __init__(self, config: TFEConfig | None = None):
9495
self.run_tasks = RunTasks(self._transport)
9596
self.run_triggers = RunTriggers(self._transport)
9697
self.runs = Runs(self._transport)
98+
self.task_stages = TaskStages(self._transport)
9799
self.query_runs = QueryRuns(self._transport)
98100
self.run_events = RunEvents(self._transport)
99101
self.policies = Policies(self._transport)

src/pytfe/models/task_result.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Copyright IBM Corp. 2025, 2026
2+
# SPDX-License-Identifier: MPL-2.0
3+
4+
from __future__ import annotations
5+
6+
from datetime import datetime
7+
from enum import Enum
8+
from typing import Optional, TYPE_CHECKING
9+
10+
from pydantic import BaseModel, ConfigDict, Field
11+
12+
class TaskResultStatus(str, Enum):
13+
passed = "passed"
14+
failed = "failed"
15+
pending = "pending"
16+
running = "running"
17+
unreachable = "unreachable"
18+
errored = "errored"
19+
20+
21+
class TaskEnforcementLevel(str, Enum):
22+
advisory = "advisory"
23+
mandatory = "mandatory"
24+
25+
26+
class TaskResultStatusTimestamps(BaseModel):
27+
model_config = ConfigDict(populate_by_name=True, validate_by_name=True)
28+
29+
errored_at: Optional[datetime] = Field(None, alias="errored-at")
30+
running_at: Optional[datetime] = Field(None, alias="running-at")
31+
canceled_at: Optional[datetime] = Field(None, alias="canceled-at")
32+
failed_at: Optional[datetime] = Field(None, alias="failed-at")
33+
passed_at: Optional[datetime] = Field(None, alias="passed-at")
34+
35+
36+
class TaskResult(BaseModel):
37+
model_config = ConfigDict(populate_by_name=True, validate_by_name=True)
38+
39+
id: str
40+
status: TaskResultStatus = Field(..., alias="status")
41+
message: str = Field(..., alias="message")
42+
43+
status_timestamps: TaskResultStatusTimestamps = Field(..., alias="status-timestamps")
44+
45+
url: str = Field(..., alias="url")
46+
47+
created_at: datetime = Field(..., alias="created-at")
48+
updated_at: datetime = Field(..., alias="updated-at")
49+
50+
task_id: str = Field(..., alias="task-id")
51+
task_name: str = Field(..., alias="task-name")
52+
task_url: str = Field(..., alias="task-url")
53+
54+
workspace_task_id: str = Field(..., alias="workspace-task-id")
55+
workspace_task_enforcement_level: TaskEnforcementLevel = Field(
56+
..., alias="workspace-task-enforcement-level"
57+
)
58+
59+
agent_pool_id: Optional[str] = Field(None, alias="agent-pool-id")
60+
task_stage: Optional[dict] = Field(None, alias="task-stage")

src/pytfe/models/task_stage.py

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,59 @@
33

44
from __future__ import annotations
55

6-
from pydantic import BaseModel, ConfigDict
6+
from datetime import datetime
7+
from enum import Enum
8+
from typing import List, Optional, TYPE_CHECKING
79

10+
from pydantic import BaseModel, ConfigDict, Field
811

9-
# TaskStage represents a HCP Terraform or Terraform Enterprise run's stage where run tasks can occur
12+
if TYPE_CHECKING:
13+
from pytfe.models.task_result import TaskResult
14+
from pytfe.models.policy_evaluation import PolicyEvaluation
15+
16+
class Stage(str, Enum):
17+
pre_plan = "pre_plan"
18+
post_plan = "post_plan"
19+
pre_apply = "pre_apply"
20+
post_apply = "post_apply"
21+
22+
class TaskStageStatus(str, Enum):
23+
pending = "pending"
24+
running = "running"
25+
passed = "passed"
26+
failed = "failed"
27+
awaiting_override = "awaiting_override"
28+
canceled = "canceled"
29+
errored = "errored"
30+
unreachable = "unreachable"
31+
32+
class TaskStageStatusTimestamps(BaseModel):
33+
model_config = ConfigDict(populate_by_name=True, validate_by_name=True)
34+
35+
errored_at: Optional[datetime] = Field(None, alias="errored-at")
36+
running_at: Optional[datetime] = Field(None, alias="running-at")
37+
canceled_at: Optional[datetime] = Field(None, alias="canceled-at")
38+
failed_at: Optional[datetime] = Field(None, alias="failed-at")
39+
passed_at: Optional[datetime] = Field(None, alias="passed-at")
40+
41+
class Permissions(BaseModel):
42+
model_config = ConfigDict(populate_by_name=True, validate_by_name=True)
43+
44+
can_override_policy: Optional[bool] = Field(None, alias="can-override-policy")
45+
can_override_tasks: Optional[bool] = Field(None, alias="can-override-tasks")
46+
can_override: Optional[bool] = Field(None, alias="can-override")
47+
48+
class Actions(BaseModel):
49+
model_config = ConfigDict(populate_by_name=True, validate_by_name=True)
50+
51+
is_overridable: Optional[bool] = Field(None, alias="is-overridable")
52+
53+
# TaskStage represents a HCP Terraform or Terraform Enterprise run's stage
1054
class TaskStage(BaseModel):
1155
model_config = ConfigDict(populate_by_name=True, validate_by_name=True)
1256

1357
id: str
58+
1459
# stage: Stage = Field(..., alias="stage")
1560
# status: TaskStageStatus = Field(..., alias="status")
1661
# status_timestamps: TaskStageStatusTimestamps = Field(..., alias="status-timestamps")
@@ -19,7 +64,28 @@ class TaskStage(BaseModel):
1964
# permissions: Permissions = Field(..., alias="permissions")
2065
# actions: Actions = Field(..., alias="actions")
2166

67+
stage: Stage = Field(..., alias="stage")
68+
status: TaskStageStatus = Field(..., alias="status")
69+
status_timestamps: TaskStageStatusTimestamps = Field(..., alias="status-timestamps")
70+
71+
created_at: datetime = Field(..., alias="created-at")
72+
updated_at: datetime = Field(..., alias="updated-at")
73+
74+
permissions: Optional[Permissions] = Field(None, alias="permissions")
75+
actions: Optional[Actions] = Field(None, alias="actions")
76+
2277
# # Relations
2378
# run: Run = Field(..., alias="run")
2479
# task_results: list[TaskResult] = Field(..., alias="task-results")
2580
# policy_evaluations: list[PolicyEvaluation] = Field(..., alias="policy-evaluations")
81+
82+
run: Optional[dict] = Field(None, alias="run")
83+
task_results: Optional[List["TaskResult"]] = Field(None, alias="task-results")
84+
policy_evaluations: Optional[List["PolicyEvaluation"]] = Field(
85+
None, alias="policy-evaluations"
86+
)
87+
88+
from pytfe.models.task_result import TaskResult
89+
from pytfe.models.policy_evaluation import PolicyEvaluation
90+
91+
TaskStage.model_rebuild()

src/pytfe/resources/task_stage.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Copyright IBM Corp. 2025, 2026
2+
# SPDX-License-Identifier: MPL-2.0
3+
4+
from __future__ import annotations
5+
6+
from collections.abc import Iterator
7+
from typing import Any
8+
9+
from ..errors import InvalidRunIDError
10+
from ..models.task_stage import TaskStage
11+
from ..utils import _safe_str, valid_string_id
12+
from ._base import _Service
13+
14+
15+
class TaskStages(_Service):
16+
"""TaskStages provides access to task stage endpoints."""
17+
18+
# Read
19+
def read(self, task_stage_id: str) -> TaskStage:
20+
if not valid_string_id(task_stage_id):
21+
raise ValueError("Invalid task_stage_id")
22+
23+
response = self.t.request(
24+
"GET",
25+
f"/api/v2/task-stages/{task_stage_id}",
26+
)
27+
28+
data = response.json().get("data", {})
29+
attributes = data.get("attributes", {})
30+
attributes["id"] = _safe_str(data.get("id"))
31+
32+
return TaskStage.model_validate(attributes)
33+
34+
# List
35+
def list(self, run_id: str) -> Iterator[TaskStage]:
36+
if not valid_string_id(run_id):
37+
raise InvalidRunIDError()
38+
39+
path = f"/api/v2/runs/{run_id}/task-stages"
40+
41+
for item in self._list(path):
42+
attributes = item.get("attributes", {})
43+
attributes["id"] = item.get("id")
44+
45+
yield TaskStage.model_validate(attributes)
46+
47+
# Override
48+
def override(
49+
self,
50+
task_stage_id: str,
51+
comment: str | None = None,
52+
) -> TaskStage:
53+
if not valid_string_id(task_stage_id):
54+
raise ValueError("Invalid task_stage_id")
55+
56+
body: dict[str, Any] | None = (
57+
{"comment": comment} if comment else None
58+
)
59+
60+
response = self.t.request(
61+
"POST",
62+
f"/api/v2/task-stages/{task_stage_id}/actions/override",
63+
json_body=body,
64+
)
65+
66+
data = response.json().get("data", {})
67+
attributes = data.get("attributes", {})
68+
attributes["id"] = _safe_str(data.get("id"))
69+
70+
return TaskStage.model_validate(attributes)

0 commit comments

Comments
 (0)