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.
- digitalai_release_sdk-26.3.0b1/.gitignore +22 -0
- digitalai_release_sdk-26.3.0b1/LICENSE +21 -0
- digitalai_release_sdk-26.3.0b1/PKG-INFO +101 -0
- digitalai_release_sdk-26.3.0b1/README.md +73 -0
- digitalai_release_sdk-26.3.0b1/digitalai/__init__.py +0 -0
- digitalai_release_sdk-26.3.0b1/digitalai/release/__init__.py +0 -0
- digitalai_release_sdk-26.3.0b1/digitalai/release/integration/__init__.py +6 -0
- digitalai_release_sdk-26.3.0b1/digitalai/release/integration/base_task.py +219 -0
- digitalai_release_sdk-26.3.0b1/digitalai/release/integration/exceptions.py +8 -0
- digitalai_release_sdk-26.3.0b1/digitalai/release/integration/ids.py +52 -0
- digitalai_release_sdk-26.3.0b1/digitalai/release/integration/input_context.py +163 -0
- digitalai_release_sdk-26.3.0b1/digitalai/release/integration/job_data_encryptor.py +116 -0
- digitalai_release_sdk-26.3.0b1/digitalai/release/integration/k8s.py +62 -0
- digitalai_release_sdk-26.3.0b1/digitalai/release/integration/logger.py +20 -0
- digitalai_release_sdk-26.3.0b1/digitalai/release/integration/masked_io.py +58 -0
- digitalai_release_sdk-26.3.0b1/digitalai/release/integration/output_context.py +24 -0
- digitalai_release_sdk-26.3.0b1/digitalai/release/integration/reporting_records.py +108 -0
- digitalai_release_sdk-26.3.0b1/digitalai/release/integration/watcher.py +57 -0
- digitalai_release_sdk-26.3.0b1/digitalai/release/integration/wrapper.py +319 -0
- digitalai_release_sdk-26.3.0b1/digitalai/release/release_api_client.py +96 -0
- digitalai_release_sdk-26.3.0b1/pyproject.toml +72 -0
|
@@ -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
|
|
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
|
+
|