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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
54 changes: 42 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,68 @@
# Digital.ai Release SDK
# Digital.ai Release Python SDK

The Digital.ai Release Python SDK (digitalai-release-sdk) is a set of tools that developers can use to create container-based tasks.
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.

## Features
- Define custom tasks using the `BaseTask` abstract class.
- Easily manage input and output properties.
- Interact with the Digital.ai Release environment seamlessly.
- Simplified API client for efficient communication with Release API.

Developers can use the `BaseTask` abstract class as a starting point to define their custom tasks and take advantage of the other methods and attributes provided by the SDK to interact with the task execution environment.

## Installation
Install the SDK using `pip`:

```shell script
```sh
pip install digitalai-release-sdk
```
## Task Example: hello.py

## Getting Started

### Example Task: `hello.py`

The following example demonstrates how to create a simple task using the SDK:

```python
from digitalai.release.integration import BaseTask

class Hello(BaseTask):

def execute(self) -> None:

# Get the name from the input
name = self.input_properties['yourName']
name = self.input_properties.get('yourName')
if not name:
raise ValueError("The 'yourName' field cannot be empty")

# Create greeting
# Create greeting message
greeting = f"Hello {name}"

# Add greeting to the task's comment section in the UI
self.add_comment(greeting)

# Put greeting in the output of the task
# Store greeting as an output property
self.set_output_property('greeting', greeting)
```

## Changelog
### Version 25.1.0

#### 🚨 Breaking Changes
- **Removed `get_default_api_client()`** from the `BaseTask` class.
- **Removed `digitalai.release.v1` package**, which contained OpenAPI-generated stubs for Release API functions.
- These stubs were difficult to use and had several non-functioning methods.
- A new, simplified API client replaces them for better usability and reliability.
- The removed package will be released as a separate library in the future.

#### ✨ New Features
- **Introduced `get_release_api_client()`** in the `BaseTask` class as a replacement for `get_default_api_client()`.
- **New `ReleaseAPIClient` class** for simplified API interactions.
- Functions in `ReleaseAPIClient` take an **endpoint URL** and **body as a dictionary**, making API calls more intuitive and easier to work with.

#### 🔧 Changes & Improvements
- **Updated minimum Python version requirement to 3.8**.
- **Updated dependency versions** to enhance compatibility and security.
- **Bundled `requests` library** to ensure seamless HTTP request handling.

```
---
**For more details, visit the [official documentation](https://docs.digital.ai/release/docs/category/python-sdk).**

## Documentation
Read more about Digital.ai Release Python SDK [here](https://digital.ai/)
55 changes: 34 additions & 21 deletions digitalai/release/integration/base_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,10 @@
from abc import ABC, abstractmethod
from typing import Any, Dict

from digitalai.release.v1.configuration import Configuration

from digitalai.release.v1.api_client import ApiClient

from .input_context import AutomatedTaskAsUserContext, ReleaseContext
from .input_context import AutomatedTaskAsUserContext
from .output_context import OutputContext
from .exceptions import AbortException
from digitalai.release.release_api_client import ReleaseAPIClient

logger = logging.getLogger("Digitalai")

Expand All @@ -18,6 +15,14 @@ class BaseTask(ABC):
"""
An abstract base class representing a task that can be executed.
"""

def __init__(self):
self.task_id = None
self.release_context = None
self.release_server_url = None
self.input_properties = None
self.output_context = None

def execute_task(self) -> None:
"""
Executes the task by calling the execute method. If an AbortException is raised during execution,
Expand All @@ -30,8 +35,8 @@ def execute_task(self) -> None:
except AbortException:
logger.debug("Abort requested")
self.set_exit_code(1)
sys.exit(1)
self.set_error_message("Abort requested")
sys.exit(1)
except Exception as e:
logger.error("Unexpected error occurred.", exc_info=True)
self.set_exit_code(1)
Expand Down Expand Up @@ -130,21 +135,6 @@ def get_task_user(self) -> AutomatedTaskAsUserContext:
"""
return self.release_context.automated_task_as_user

def get_default_api_client(self) -> ApiClient:
"""
Returns an ApiClient object with default configuration based on the task.
"""
if not all([self.get_release_server_url(), self.get_task_user().username, self.get_task_user().password]):
raise ValueError("Cannot connect to Release API without server URL, username, or password. "
"Make sure that the 'Run as user' property is set on the release.")

configuration = Configuration(
host=self.get_release_server_url(),
username=self.get_task_user().username,
password=self.get_task_user().password)

return ApiClient(configuration)

def get_release_id(self) -> str:
"""
Returns the Release ID of the task
Expand All @@ -157,5 +147,28 @@ def get_task_id(self) -> str:
"""
return self.task_id

def get_release_api_client(self) -> ReleaseAPIClient:
"""
Returns a ReleaseAPIClient object with default configuration based on the task.
"""
self._validate_api_credentials()
return ReleaseAPIClient(self.get_release_server_url(),
self.get_task_user().username,
self.get_task_user().password)

def _validate_api_credentials(self) -> None:
"""
Validates that the necessary credentials are available for connecting to the Release API.
"""
if not all([
self.get_release_server_url(),
self.get_task_user().username,
self.get_task_user().password
]):
raise ValueError(
"Cannot connect to Release API without server URL, username, or password. "
"Make sure that the 'Run as user' property is set on the release."
)



96 changes: 96 additions & 0 deletions digitalai/release/release_api_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import requests


class ReleaseAPIClient:
"""
A client for interacting with the Release API.
Supports authentication via username/password or personal access token.
"""

def __init__(self, server_address, username=None, password=None, personal_access_token=None, **kwargs):
"""
Initializes the API client.

:param server_address: Base URL of the Release API server.
:param username: Optional username for basic authentication.
:param password: Optional password for basic authentication.
:param personal_access_token: Optional personal access token for authentication.
:param kwargs: Additional session parameters (e.g., headers, timeout).
"""
if not server_address:
raise ValueError("server_address must not be empty.")

self.server_address = server_address.rstrip('/') # Remove trailing slash if present
self.session = requests.Session()
self.session.headers.update({"Accept": "application/json"})

# Set authentication method
if username and password:
self.session.auth = (username, password)
elif personal_access_token:
self.session.headers.update({"x-release-personal-token": personal_access_token})
else:
raise ValueError("Either username and password or a personal access token must be provided.")

# Apply additional session configurations
for key, value in kwargs.items():
if key == 'headers':
self.session.headers.update(value) # Merge custom headers
elif hasattr(self.session, key) and key != 'auth': # Skip 'auth' key
setattr(self.session, key, value)

def _request(self, method, endpoint, params=None, json=None, data=None, **kwargs):
"""
Internal method to send an HTTP request.

:param method: HTTP method (GET, POST, PUT, DELETE, PATCH).
:param endpoint: API endpoint (relative path).
:param params: Optional query parameters.
:param json: Optional JSON payload.
:param data: Optional raw data payload.
:param kwargs: Additional request options.
:return: Response object.
"""
if not endpoint:
raise ValueError("Endpoint must not be empty.")

kwargs.pop('auth', None) # Remove 'auth' key if present to avoid conflicts
url = f"{self.server_address}/{endpoint.lstrip('/')}" # Construct full URL

response = self.session.request(
method, url, params=params, data=data, json=json, **kwargs
)

return response

def get(self, endpoint, params=None, **kwargs):
"""Sends a GET request to the specified endpoint."""
return self._request("GET", endpoint, params=params, **kwargs)

def post(self, endpoint, json=None, data=None, **kwargs):
"""Sends a POST request to the specified endpoint."""
return self._request("POST", endpoint, data=data, json=json, **kwargs)

def put(self, endpoint, json=None, data=None, **kwargs):
"""Sends a PUT request to the specified endpoint."""
return self._request("PUT", endpoint, data=data, json=json, **kwargs)

def delete(self, endpoint, params=None, **kwargs):
"""Sends a DELETE request to the specified endpoint."""
return self._request("DELETE", endpoint, params=params, **kwargs)

def patch(self, endpoint, json=None, data=None, **kwargs):
"""Sends a PATCH request to the specified endpoint."""
return self._request("PATCH", endpoint, data=data, json=json, **kwargs)

def close(self):
"""Closes the session."""
self.session.close()

def __enter__(self):
"""Enables the use of 'with' statements for automatic resource management."""
return self

def __exit__(self, exc_type, exc_value, traceback):
"""Ensures the session is closed when exiting a 'with' block."""
self.close()
Loading