digitalai_release_sdk 26.3.0b1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. digitalai_release_sdk-26.3.0b1/.gitignore +22 -0
  2. digitalai_release_sdk-26.3.0b1/LICENSE +21 -0
  3. digitalai_release_sdk-26.3.0b1/PKG-INFO +101 -0
  4. digitalai_release_sdk-26.3.0b1/README.md +73 -0
  5. digitalai_release_sdk-26.3.0b1/digitalai/__init__.py +0 -0
  6. digitalai_release_sdk-26.3.0b1/digitalai/release/__init__.py +0 -0
  7. digitalai_release_sdk-26.3.0b1/digitalai/release/integration/__init__.py +6 -0
  8. digitalai_release_sdk-26.3.0b1/digitalai/release/integration/base_task.py +219 -0
  9. digitalai_release_sdk-26.3.0b1/digitalai/release/integration/exceptions.py +8 -0
  10. digitalai_release_sdk-26.3.0b1/digitalai/release/integration/ids.py +52 -0
  11. digitalai_release_sdk-26.3.0b1/digitalai/release/integration/input_context.py +163 -0
  12. digitalai_release_sdk-26.3.0b1/digitalai/release/integration/job_data_encryptor.py +116 -0
  13. digitalai_release_sdk-26.3.0b1/digitalai/release/integration/k8s.py +62 -0
  14. digitalai_release_sdk-26.3.0b1/digitalai/release/integration/logger.py +20 -0
  15. digitalai_release_sdk-26.3.0b1/digitalai/release/integration/masked_io.py +58 -0
  16. digitalai_release_sdk-26.3.0b1/digitalai/release/integration/output_context.py +24 -0
  17. digitalai_release_sdk-26.3.0b1/digitalai/release/integration/reporting_records.py +108 -0
  18. digitalai_release_sdk-26.3.0b1/digitalai/release/integration/watcher.py +57 -0
  19. digitalai_release_sdk-26.3.0b1/digitalai/release/integration/wrapper.py +319 -0
  20. digitalai_release_sdk-26.3.0b1/digitalai/release/release_api_client.py +96 -0
  21. digitalai_release_sdk-26.3.0b1/pyproject.toml +72 -0
@@ -0,0 +1,22 @@
1
+ # IDE
2
+ .idea/
3
+
4
+ # Python
5
+ *.py[cod]
6
+ __pycache__/
7
+
8
+ # Venv
9
+ .venv/
10
+ venv
11
+
12
+ # System
13
+ .DS_Store
14
+
15
+ # Pip
16
+ *.egg-info
17
+ dist/
18
+ requirements.txt
19
+
20
+ # Testing
21
+ .pytest_cache/
22
+ .coverage
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Digital.ai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,101 @@
1
+ Metadata-Version: 2.4
2
+ Name: digitalai_release_sdk
3
+ Version: 26.3.0b1
4
+ Summary: Digital.ai Release SDK
5
+ Project-URL: Homepage, https://digital.ai/
6
+ Project-URL: Documentation, https://docs.digital.ai/release/docs/category/python-sdk
7
+ Author-email: "Digital.ai" <pypi-devops@digital.ai>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Software Development :: Libraries
19
+ Requires-Python: >=3.10
20
+ Requires-Dist: dataclasses-json<1.0.0,>=0.6.7
21
+ Requires-Dist: kubernetes<36.0.0,>=35.0.0
22
+ Requires-Dist: pycryptodomex<4.0.0,>=3.23.0
23
+ Requires-Dist: python-dateutil<3.0.0,>=2.9.0
24
+ Requires-Dist: requests<3.0.0,>=2.32.5
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # Digital.ai Release Python SDK
30
+
31
+ The **Digital.ai Release Python SDK** (`digitalai-release-sdk`) provides a set of tools for developers to create container-based integration with Digital.ai Release. It simplifies integration creation by offering built-in functions to interact with the execution environment.
32
+
33
+ ## Features
34
+ - Define custom tasks using the `BaseTask` abstract class.
35
+ - Easily manage input and output properties.
36
+ - Interact with the Digital.ai Release environment seamlessly.
37
+ - Simplified API client for efficient communication with Release API, with support for username/password or personal access token authentication.
38
+ - Built-in helpers to resolve Release entity IDs (release, phase, task, and folder).
39
+
40
+
41
+ ## Installation
42
+ Install the SDK using `pip`:
43
+
44
+ ```sh
45
+ pip install digitalai-release-sdk
46
+ ```
47
+
48
+ ## Getting Started
49
+
50
+ ### Example Task: `hello.py`
51
+
52
+ The following example demonstrates how to create a simple task using the SDK:
53
+
54
+ ```python
55
+ from digitalai.release.integration import BaseTask
56
+
57
+ class Hello(BaseTask):
58
+
59
+ def execute(self) -> None:
60
+ # Get the name from the input
61
+ name = self.input_properties.get('yourName')
62
+ if not name:
63
+ raise ValueError("The 'yourName' field cannot be empty")
64
+
65
+ # Create greeting message
66
+ greeting = f"Hello {name}"
67
+
68
+ # Add greeting to the task's comment section in the UI
69
+ self.add_comment(greeting)
70
+
71
+ # Store greeting as an output property
72
+ self.set_output_property('greeting', greeting)
73
+ ```
74
+
75
+ ## Changelog
76
+
77
+ ### Version 26.3.0 (Beta)
78
+
79
+ #### 🚀 Features
80
+
81
+ - `get_release_api_client()` now supports optional credentials/server URL and `requests` library arguments.
82
+ - Added `get_phase_id()` and `get_folder_id()` helper methods to `BaseTask`.
83
+
84
+ #### 🛠️ Enhancements
85
+
86
+ - Improved stability and error handling for API requests and Kubernetes tasks.
87
+
88
+ ---
89
+
90
+ ## 🔗 Related Resources
91
+
92
+ - 🧪 **Python Template Project**: [release-integration-template-python](https://github.com/digital-ai/release-integration-template-python)
93
+ A starting point for building custom integrations using Digital.ai Release and Python.
94
+
95
+ - 📘 **Official Documentation**: [Digital.ai Release Python SDK Docs](https://docs.digital.ai/release/docs/category/python-sdk)
96
+ Comprehensive guide to using the Python SDK and building custom tasks.
97
+
98
+ - 📦 **Digital.ai Release Python SDK**: [digitalai-release-sdk on PyPI](https://pypi.org/project/digitalai-release-sdk/)
99
+ The official SDK package for integrating with Digital.ai Release.
100
+
101
+
@@ -0,0 +1,73 @@
1
+ # Digital.ai Release Python SDK
2
+
3
+ The **Digital.ai Release Python SDK** (`digitalai-release-sdk`) provides a set of tools for developers to create container-based integration with Digital.ai Release. It simplifies integration creation by offering built-in functions to interact with the execution environment.
4
+
5
+ ## Features
6
+ - Define custom tasks using the `BaseTask` abstract class.
7
+ - Easily manage input and output properties.
8
+ - Interact with the Digital.ai Release environment seamlessly.
9
+ - Simplified API client for efficient communication with Release API, with support for username/password or personal access token authentication.
10
+ - Built-in helpers to resolve Release entity IDs (release, phase, task, and folder).
11
+
12
+
13
+ ## Installation
14
+ Install the SDK using `pip`:
15
+
16
+ ```sh
17
+ pip install digitalai-release-sdk
18
+ ```
19
+
20
+ ## Getting Started
21
+
22
+ ### Example Task: `hello.py`
23
+
24
+ The following example demonstrates how to create a simple task using the SDK:
25
+
26
+ ```python
27
+ from digitalai.release.integration import BaseTask
28
+
29
+ class Hello(BaseTask):
30
+
31
+ def execute(self) -> None:
32
+ # Get the name from the input
33
+ name = self.input_properties.get('yourName')
34
+ if not name:
35
+ raise ValueError("The 'yourName' field cannot be empty")
36
+
37
+ # Create greeting message
38
+ greeting = f"Hello {name}"
39
+
40
+ # Add greeting to the task's comment section in the UI
41
+ self.add_comment(greeting)
42
+
43
+ # Store greeting as an output property
44
+ self.set_output_property('greeting', greeting)
45
+ ```
46
+
47
+ ## Changelog
48
+
49
+ ### Version 26.3.0 (Beta)
50
+
51
+ #### 🚀 Features
52
+
53
+ - `get_release_api_client()` now supports optional credentials/server URL and `requests` library arguments.
54
+ - Added `get_phase_id()` and `get_folder_id()` helper methods to `BaseTask`.
55
+
56
+ #### 🛠️ Enhancements
57
+
58
+ - Improved stability and error handling for API requests and Kubernetes tasks.
59
+
60
+ ---
61
+
62
+ ## 🔗 Related Resources
63
+
64
+ - 🧪 **Python Template Project**: [release-integration-template-python](https://github.com/digital-ai/release-integration-template-python)
65
+ A starting point for building custom integrations using Digital.ai Release and Python.
66
+
67
+ - 📘 **Official Documentation**: [Digital.ai Release Python SDK Docs](https://docs.digital.ai/release/docs/category/python-sdk)
68
+ Comprehensive guide to using the Python SDK and building custom tasks.
69
+
70
+ - 📦 **Digital.ai Release Python SDK**: [digitalai-release-sdk on PyPI](https://pypi.org/project/digitalai-release-sdk/)
71
+ The official SDK package for integrating with Digital.ai Release.
72
+
73
+
File without changes
@@ -0,0 +1,6 @@
1
+ from .base_task import BaseTask
2
+ from .input_context import InputContext
3
+ from .output_context import OutputContext
4
+ from .exceptions import AbortException
5
+ from .reporting_records import BuildRecord, PlanRecord, ItsmRecord,CodeComplianceRecord, DeploymentRecord
6
+ from .logger import dai_logger
@@ -0,0 +1,219 @@
1
+ import sys
2
+ from abc import ABC, abstractmethod
3
+ from typing import Any, Dict, Optional
4
+
5
+ from .input_context import AutomatedTaskAsUserContext
6
+ from .output_context import OutputContext
7
+ from .exceptions import AbortException
8
+ from .ids import Ids
9
+ from .logger import dai_logger
10
+ from digitalai.release.release_api_client import ReleaseAPIClient
11
+
12
+
13
+ class BaseTask(ABC):
14
+ """
15
+ An abstract base class representing a task that can be executed.
16
+ """
17
+
18
+ def __init__(self):
19
+ self.task_id = None
20
+ self.release_context = None
21
+ self.release_server_url = None
22
+ self.input_properties = None
23
+ self.output_context = None
24
+
25
+ def execute_task(self) -> None:
26
+ """
27
+ Executes the task by calling the execute method. If an AbortException is raised during execution,
28
+ the task's exit code is set to 1 and the program exits with a status code of 1. If any other exception
29
+ is raised, the task's exit code is also set to 1.
30
+ """
31
+ try:
32
+ self.output_context = OutputContext(0, "", {}, [])
33
+ self.execute()
34
+ except AbortException:
35
+ dai_logger.info("Abort requested")
36
+ self.set_exit_code(1)
37
+ self.set_error_message("Abort requested")
38
+ sys.exit(1)
39
+ except Exception as e:
40
+ dai_logger.error("Unexpected error occurred", exc_info=True)
41
+ self.set_exit_code(1)
42
+ self.set_error_message(str(e))
43
+
44
+ @abstractmethod
45
+ def execute(self) -> None:
46
+ """
47
+ This is an abstract method that must be implemented by subclasses of BaseTask. It represents the main
48
+ logic of the task.
49
+ """
50
+ pass
51
+
52
+ def abort(self) -> None:
53
+ """
54
+ Sets the task's exit code to 1 and exits the program with a status code of 1.
55
+ """
56
+ self.set_exit_code(1)
57
+ sys.exit(1)
58
+
59
+ def get_output_context(self) -> OutputContext:
60
+ """
61
+ Returns the OutputContext object associated with the task.
62
+ """
63
+ return self.output_context
64
+
65
+ def get_output_properties(self) -> Dict[str, Any]:
66
+ """
67
+ Returns the output properties dictionary of the task's OutputContext object.
68
+ """
69
+ return self.output_context.output_properties
70
+
71
+ def get_input_properties(self) -> Dict[str, Any]:
72
+ """
73
+ Returns the input properties dictionary of the task
74
+ """
75
+ if not self.input_properties:
76
+ raise ValueError(f"Input properties have not been set")
77
+ return self.input_properties
78
+
79
+ def set_output_property(self, name: str, value: Any) -> None:
80
+ """
81
+ Sets the name and value of an output property of the task
82
+ """
83
+ if not name:
84
+ raise ValueError("Output property name cannot be empty")
85
+
86
+ accepted_data_types = (str, int, list, dict, bool)
87
+
88
+ if value and not isinstance(value, accepted_data_types):
89
+ raise ValueError(
90
+ f"Invalid data type for value '{value}' in name '{name}' in set_output_property. Accepted data types "
91
+ f"are: str, int, list, dict, bool")
92
+
93
+ self.output_context.output_properties[name] = value
94
+
95
+ def set_exit_code(self, value) -> None:
96
+ """
97
+ Sets the exit code of the task's OutputContext object.
98
+ """
99
+ self.output_context.exit_code = value
100
+
101
+ def set_error_message(self, value) -> None:
102
+ """
103
+ Sets the error message of the task's OutputContext object.
104
+ """
105
+ self.output_context.job_error_message = value
106
+
107
+ def add_comment(self, comment: str) -> None:
108
+ """
109
+ Logs a comment of the task.
110
+ """
111
+ dai_logger.debug(f"##[start: comment]{comment}##[end: comment]")
112
+
113
+ def set_status_line(self, status_line: str) -> None:
114
+ """
115
+ Set the status of the task.
116
+ """
117
+ dai_logger.debug(f"##[start: status]{status_line}##[end: status]")
118
+
119
+ def add_reporting_record(self, reporting_record: Any) -> None:
120
+ """
121
+ Adds a reporting record to the OutputContext
122
+ """
123
+ self.output_context.reporting_records.append(reporting_record)
124
+
125
+ def get_release_server_url(self) -> str:
126
+ """
127
+ Returns the Release server URL of the associated task
128
+ """
129
+ return self.release_server_url
130
+
131
+ def get_task_user(self) -> Optional[AutomatedTaskAsUserContext]:
132
+ """
133
+ Returns the user details that are executing the task, or ``None`` when no
134
+ release context is available.
135
+ """
136
+ if not self.release_context:
137
+ return None
138
+ return self.release_context.automated_task_as_user
139
+
140
+ def get_release_id(self) -> str:
141
+ """
142
+ Returns the Release ID of the task
143
+ """
144
+ return self.release_context.id
145
+
146
+ def get_task_id(self) -> str:
147
+ """
148
+ Returns the Task ID of the task
149
+ """
150
+ return self.task_id
151
+
152
+ def get_phase_id(self) -> str:
153
+ """
154
+ Returns the Phase ID of the task, derived from the task id.
155
+ """
156
+ return Ids.phase_id_from(self.get_task_id())
157
+
158
+ def get_folder_id(self) -> str:
159
+ """
160
+ Returns the ID of the folder that contains the release, derived from the
161
+ release id.
162
+ """
163
+ return Ids.find_folder_id(self.get_release_id())
164
+
165
+ def get_release_api_client(self,
166
+ server_address: str = None,
167
+ username: str = None,
168
+ password: str = None,
169
+ personal_access_token: str = None,
170
+ **kwargs) -> ReleaseAPIClient:
171
+ """
172
+ Returns a ReleaseAPIClient object.
173
+
174
+ All arguments are optional. When omitted, the client is configured from the
175
+ task context (server URL and the 'Run as user' credentials). Any argument
176
+ that is provided overrides the corresponding task default.
177
+
178
+ :param server_address: Optional Release server URL. Defaults to the task's server URL.
179
+ :param username: Optional username. Defaults to the task user's username.
180
+ :param password: Optional password. Defaults to the task user's password.
181
+ :param personal_access_token: Optional personal access token for authentication.
182
+ :param kwargs: Additional session parameters (e.g., headers, timeout).
183
+ """
184
+ task_user = self.get_task_user()
185
+ server_address = server_address or self.get_release_server_url()
186
+
187
+ if personal_access_token:
188
+ if not server_address:
189
+ raise ValueError(
190
+ "Cannot connect to Release API without server URL. "
191
+ "Make sure that the release server URL is available."
192
+ )
193
+ return ReleaseAPIClient(server_address,
194
+ personal_access_token=personal_access_token,
195
+ **kwargs)
196
+
197
+ username = username or (task_user and task_user.username)
198
+ password = password or (task_user and task_user.password)
199
+ self._validate_api_credentials(server_address, username, password)
200
+ return ReleaseAPIClient(server_address, username, password, **kwargs)
201
+
202
+ def _validate_api_credentials(self, server_address: str = None,
203
+ username: str = None, password: str = None) -> None:
204
+ """
205
+ Validates that the necessary credentials are available for connecting to the Release API.
206
+ """
207
+ task_user = self.get_task_user()
208
+ if not all([
209
+ server_address or self.get_release_server_url(),
210
+ username or (task_user and task_user.username),
211
+ password or (task_user and task_user.password)
212
+ ]):
213
+ raise ValueError(
214
+ "Cannot connect to Release API without server URL, username, or password. "
215
+ "Make sure that the 'Run as user' property is set on the release."
216
+ )
217
+
218
+
219
+
@@ -0,0 +1,8 @@
1
+ class AbortException(BaseException):
2
+ """Exception class to be raised when a process needs to be prematurely terminated.
3
+
4
+ This exception can be caught and handled by the calling code to gracefully terminate
5
+ the process and clean up any resources before exiting.
6
+ """
7
+ pass
8
+
@@ -0,0 +1,52 @@
1
+ """
2
+ Helpers for parsing Release object ids.
3
+
4
+ Release object ids are slash-separated paths, e.g.
5
+ Applications/Folder.../Release.../Phase.../Task...
6
+ The server derives the enclosing phase/folder by walking up that path (see
7
+ com.xebialabs.xlrelease.repository.Ids). A task only receives its own task and
8
+ release ids, so we reproduce the same walk to resolve the phase and folder.
9
+ """
10
+
11
+ _ID_SEPARATOR = '/'
12
+ _PHASE_PREFIX = 'Phase'
13
+ _FOLDER_PREFIX = 'Folder'
14
+
15
+
16
+ class Ids:
17
+ """Path-based parsing of Release object ids (mirrors the server's Ids)."""
18
+
19
+ @staticmethod
20
+ def segment_name(object_id: str) -> str:
21
+ """Return the last path segment of an id (Ids.getName)."""
22
+ if _ID_SEPARATOR not in object_id:
23
+ return object_id
24
+ return object_id[object_id.rfind(_ID_SEPARATOR) + 1:]
25
+
26
+ @staticmethod
27
+ def parent_id(object_id: str) -> str:
28
+ """Return the id with its last path segment removed (Ids.getParentId)."""
29
+ return object_id[:object_id.rfind(_ID_SEPARATOR)]
30
+
31
+ @staticmethod
32
+ def is_root(object_id: str) -> bool:
33
+ """True when the id has no parent, i.e. no separator (Ids.isRoot)."""
34
+ return _ID_SEPARATOR not in object_id
35
+
36
+ @staticmethod
37
+ def phase_id_from(object_id: str) -> str:
38
+ """Return the enclosing phase id of ``object_id`` (Ids.phaseIdFrom)."""
39
+ ancestry = object_id
40
+ while not Ids.segment_name(ancestry).startswith(_PHASE_PREFIX):
41
+ if Ids.is_root(ancestry):
42
+ raise ValueError(f"No phase found in id '{object_id}'")
43
+ ancestry = Ids.parent_id(ancestry)
44
+ return ancestry
45
+
46
+ @staticmethod
47
+ def find_folder_id(object_id: str) -> str:
48
+ """Return the enclosing folder id of ``object_id`` (Ids.findFolderId)."""
49
+ parent = object_id
50
+ while not Ids.segment_name(parent).startswith(_FOLDER_PREFIX) and not Ids.is_root(parent):
51
+ parent = Ids.parent_id(parent)
52
+ return parent
@@ -0,0 +1,163 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any, List, Dict, Optional
5
+ from dataclasses_json import dataclass_json, LetterCase
6
+
7
+
8
+ @dataclass_json
9
+ @dataclass
10
+ class PropertyDefinition:
11
+ """
12
+ Definition of a property.
13
+
14
+ Attributes:
15
+ - name (str): Name of the property.
16
+ - kind (str): Kind of the property (e.g. 'CI', 'string').
17
+ - category (str): Category of the property (e.g. 'input', 'output').
18
+ - password (bool): Whether the property is a password.
19
+ - value (Any): Value of the property.
20
+ """
21
+ name: str
22
+ kind: str
23
+ category: str
24
+ password: bool
25
+ value: Any
26
+
27
+ def property_value(self):
28
+ """
29
+ Get the value of the property, recursively unwrapping nested CI properties.
30
+
31
+ Returns:
32
+ - Any: The value of the property.
33
+ """
34
+ if self.kind == 'CI' and self.value:
35
+ ci = CiDefinition.from_dict(self.value)
36
+ return {p.name: p.property_value() for p in ci.properties}
37
+ else:
38
+ return self.value
39
+
40
+ def secret_value(self):
41
+ """
42
+ Get the password values of the property, recursively unwrapping nested CI properties.
43
+
44
+ Returns:
45
+ - list: A list of password values.
46
+ """
47
+ if self.kind == 'CI' and self.value:
48
+ ci = CiDefinition.from_dict(self.value)
49
+ return [p.value for p in ci.properties if p.password and p.value]
50
+ else:
51
+ return [self.value] if self.password and self.value else []
52
+
53
+
54
+ @dataclass_json
55
+ @dataclass
56
+ class CiDefinition:
57
+ """
58
+ Definition of a CI.
59
+
60
+ Attributes:
61
+ - id (str): ID of the CI.
62
+ - type (str): Type of the CI.
63
+ - properties (List[PropertyDefinition]): List of properties for the CI.
64
+ """
65
+ id: Optional[str] = None
66
+ type: Optional[str] = None
67
+ properties: List[PropertyDefinition] = field(default_factory=list)
68
+
69
+
70
+ @dataclass_json
71
+ @dataclass
72
+ class TaskContext(CiDefinition):
73
+ """
74
+ Context of a task.
75
+
76
+ Attributes:
77
+ - id (str): ID of the CI.
78
+ - type (str): Type of the CI.
79
+ - properties (List[PropertyDefinition]): List of properties for the CI.
80
+ """
81
+
82
+ def output_properties(self) -> List[str]:
83
+ """
84
+ Get the names of the output properties of the task.
85
+
86
+ Returns:
87
+ - list: A list of output property names.
88
+ """
89
+ return [p.name for p in self.properties if p.category == 'output']
90
+
91
+ def secrets(self) -> List[str]:
92
+ """
93
+ Get the password values of the task, recursively unwrapping nested CI properties.
94
+
95
+ Returns:
96
+ - list: A list of password values.
97
+ """
98
+ secret_list = []
99
+ for p in self.properties:
100
+ secret_list.extend(p.secret_value())
101
+ return secret_list
102
+
103
+ def build_locals(self) -> Dict[str, Any]:
104
+ """
105
+ Build a dictionary of the task's property values.
106
+
107
+ Returns:
108
+ - dict: A dictionary of property names to values.
109
+ """
110
+ return {p.name: p.property_value() for p in self.properties}
111
+
112
+ def script_location(self) -> str:
113
+ """
114
+ Get the value of the 'scriptLocation' property.
115
+
116
+ Returns:
117
+ - str: The value of the 'scriptLocation' property.
118
+ """
119
+ return next((p.value for p in self.properties if p.name == 'scriptLocation'), '')
120
+
121
+
122
+ @dataclass_json
123
+ @dataclass
124
+ class AutomatedTaskAsUserContext:
125
+ """
126
+ Context for running an automated task as a specific user.
127
+
128
+ Attributes:
129
+ - username (str): The username to run the task as.
130
+ - password (str): The password for the user.
131
+ """
132
+ username: Optional[str] = None
133
+ password: Optional[str] = None
134
+
135
+
136
+ @dataclass_json(letter_case=LetterCase.CAMEL)
137
+ @dataclass
138
+ class ReleaseContext:
139
+ """
140
+ Context of a release.
141
+
142
+ Attributes:
143
+ - id (str): ID of the release.
144
+ - automated_task_as_user (AutomatedTaskAsUserContext): Context for running
145
+ an automated task as a specific user.
146
+ """
147
+ id: Optional[str] = None
148
+ automated_task_as_user: Optional[AutomatedTaskAsUserContext] = field(default_factory=AutomatedTaskAsUserContext)
149
+
150
+
151
+ @dataclass_json()
152
+ @dataclass
153
+ class InputContext:
154
+ """
155
+ Input context for a task.
156
+
157
+ Attributes:
158
+ - release (ReleaseContext): Context of the release.
159
+ - task (TaskContext): Context of the task.
160
+ """
161
+ release: Optional[ReleaseContext] = None
162
+ task: Optional[TaskContext] = None
163
+