Source
from collections.abc import Callable
from typing import Any
import demistomock as demisto # noqa: F401
from CommonServerPython import * # noqa: F401
class ContextData:
def __init__(
self,
context: dict[str, Any] | None = None,
inputs: dict[str, Any] | None = None,
incident: dict[str, Any] | None = None,
value: dict[str, Any] | None = None,
):
self.__context = context
self.__value = value
self.__specials = {
"inputs": inputs if isinstance(inputs, dict) else {},
"incident": incident if isinstance(incident, dict) else {},
}
def get(self, key: str | None = None) -> Any:
"""Get the context value
:param key: The dt expressions (string within ${}).
:return: The value.
"""
if not key:
return None
dx = self.__context
if key != "." and not key.startswith(".=") and key.startswith("."):
dx = self.__value
key = key[1:]
else:
for prefix in self.__specials:
if prefix == key or (key.startswith(prefix) and key[len(prefix) : len(prefix) + 1] in (".", "(", "=")):
dx = self.__specials
break
return demisto.dt(dx, key)
class Formatter:
def __init__(self, start_marker: str, end_marker: str, keep_symbol_to_null: bool):
if not start_marker:
raise ValueError("start-marker is required.")
self.__start_marker = start_marker
self.__end_marker = end_marker
self.__keep_symbol_to_null = keep_symbol_to_null
@staticmethod
def __is_end_mark(source: str, ci: int, end_marker: str) -> bool:
if end_marker:
return source[ci : ci + len(end_marker)] == end_marker
else:
c = source[ci]
if c.isspace():
return True
elif c.isascii():
return c != "_" and not c.isalnum()
else:
return False
def __extract(
self,
source: str,
extractor: Callable[[str, ContextData | None], Any] | None,
dx: ContextData | None,
si: int,
markers: tuple[str, str] | None,
) -> tuple[Any, int | None]:
"""Extract a template text, or an enclosed value within starting and ending marks
:param source: The template text, or the enclosed value starts with the next charactor of a start marker
:param extractor: The function to extract an enclosed value as DT
:param dx: The context data
:param si: The index of `source` to start extracting
:param markers: The start and end marker to find an end position for parsing an enclosed value.
It must be None when the template text is given to `source`.
:return: The extracted value and index of `source` when parsing ended.
The index is the next after the end marker when extracting the enclosed value.
"""
out = None
ci = si
while ci < len(source):
if markers is not None and Formatter.__is_end_mark(source, ci, markers[1]):
key = source[si:ci] if out is None else str(out) + source[si:ci]
if extractor:
if (xval := extractor(key, dx)) is None and self.__keep_symbol_to_null:
xval = markers[0] + key + markers[1]
else:
xval = key
return xval, ci + len(markers[1])
elif extractor and source[ci : ci + len(self.__start_marker)] == self.__start_marker:
xval, ei = self.__extract(
source, extractor, dx, ci + len(self.__start_marker), (self.__start_marker, self.__end_marker)
)
if si != ci:
out = source[si:ci] if out is None else str(out) + source[si:ci]
if ei is None:
xval = self.__start_marker
ei = ci + len(self.__start_marker)
if out is None:
out = xval
elif xval is not None:
out = str(out) + str(xval)
si = ci = ei
elif markers is None:
ci += 1
elif endc := {"(": ")", "{": "}", "[": "]", '"': '"', "'": "'"}.get(source[ci]):
_, ei = self.__extract(source, None, dx, ci + 1, (source[ci], endc))
ci = ci + 1 if ei is None else ei
elif source[ci] == "\\":
ci += 2
else:
ci += 1
if markers is not None:
# unbalanced braces, brackets, quotes, etc.
return None, None
elif not extractor:
return None, ci
elif si >= len(source):
return out, ci
elif out is None:
return source[si:], ci
else:
return str(out) + source[si:], ci
def build(self, template: Any, extractor: Callable[[str, ContextData | None], Any] | None, dx: ContextData | None) -> Any:
"""Format a text from a template including DT expressions
:param template: The template.
:param extractor: The extractor to get real value within ${dt}.
:param dx: The context instance.
:return: The text built from the template.
"""
if isinstance(template, dict):
return {self.build(k, extractor, dx): self.build(v, extractor, dx) for k, v in template.items()}
elif isinstance(template, list):
return [self.build(v, extractor, dx) for v in template]
elif isinstance(template, str):
return self.__extract(template, extractor, dx, 0, None)[0] if template else ""
else:
return template
def extract_dt(dtstr: str, dx: ContextData | None) -> Any:
"""Extract dt expression
:param dtstr: The dt expressions (string within ${}).
:param dx: The context instance.
:return: The value extracted.
"""
try:
return dx.get(dtstr) if dx else dtstr
except Exception:
return None
def main():
args = assign_params(**demisto.args())
try:
value = args.get("value")
template = args.get("template")
template_type = args.get("template_type", "raw")
variable_markers = argToList(args.get("variable_markers", "${,}"))
if not variable_markers or not variable_markers[0]:
raise ValueError("variable_markers must have a start marker.")
elif len(variable_markers) >= 3:
raise ValueError("too many values for variable_markers.")
elif len(variable_markers) == 1:
variable_markers = variable_markers + [""]
dx = args.get("ctx_data")
if dx and isinstance(dx, str):
dx = json.loads(dx)
if not template:
template = value
value = None
elif template_type == "json":
template = json.loads(template)
elif template_type != "raw":
raise DemistoException(f"Invalid template type: {template_type}")
dx = ContextData(context=dx, inputs=args.get("ctx_inputs"), incident=args.get("ctx_inc"), value=value)
formatter = Formatter(variable_markers[0], variable_markers[1], argToBoolean(args.get("keep_symbol_to_null", False)))
output = formatter.build(template, extract_dt, dx)
output = [] if output is None else output
except Exception as err:
# Don't return an error by return_error() as this is transformer.
raise DemistoException(str(err))
return_results(output)
if __name__ in ("__builtin__", "builtins", "__main__"):
main()
README
Build text from a template that can include DT expressions.
Script Data
| Name |
Description |
| Script Type |
python3 |
| Tags |
transformer, general |
| Cortex XSOAR Version |
6.5.0 |
Inputs
| Argument Name |
Description |
| value |
The context to refer a value with ${.xxx} if `template` is not empty, otherwise the template text. |
| template |
The template text |
| template_type |
The template type |
| ctx_data |
Context Data: Input . (single dot) on `From previous tasks` to enable to extract the context data. |
| ctx_inputs |
`inputs` context: Input ‘inputs’ (no quotation) on `From previous tasks` to enable ${inputs.} expression in DT. |
| ctx_inc |
`demisto` context: Input ‘incident’ (no quotation) on `From previous tasks` to enable ${incident.} expression in DT. |
| variable_markers |
The pair of start and end markers to bracket a variable name. |
| keep_symbol_to_null |
Set to true not to replace a value if the variable is null, otherwise false. |
Outputs
There are no outputs for this script.
Getting Started
The transformer builds a text from a template text which includes variables just like:
This is a test message for ${user_name}.
The template will be formatted to This is a test message for John Doe. by replacing the variable parameters.
By default, a variable name starts with ${ and ends with } . You can change the start marker and end marker by specifying the variable_markers parameter.
Examples
Replace variables in a text based on the context data
Parameters
| Argument Name |
Value |
Note |
| value |
|
Any value |
| template |
My name is ${first_name} ${last_name}. |
|
| template_type |
|
|
| ctx_data |
. |
Make sure that From previous tasks is selected |
| ctx_inputs |
|
|
| ctx_inc |
|
|
| variable_markers |
|
|
| keep_symbol_to_null |
|
|
Context Data
{
"first_name": "John",
"last_name": "Doe"
}
Output
My name is John Doe.
Replace variables in a text based on the value
Parameters
| Argument Name |
Value |
Note |
| value |
See the value |
|
| template |
My name is ${.first_name} ${.last_name}. |
|
| template_type |
|
|
| ctx_data |
|
|
| ctx_inputs |
|
|
| ctx_inc |
|
|
| variable_markers |
|
|
| keep_symbol_to_null |
|
|
value (structured)
{
"first_name": "John",
"last_name": "Doe"
}
Output
My name is John Doe.
Format the template given to the value
Parameters
| Argument Name |
Value |
Note |
| value |
My name is ${first_name} ${last_name}. |
|
| template |
|
|
| template_type |
|
|
| ctx_data |
. |
Make sure that From previous tasks is selected |
| ctx_inputs |
|
|
| ctx_inc |
|
|
| variable_markers |
|
|
| keep_symbol_to_null |
|
|
Context Data
{
"first_name": "John",
"last_name": "Doe"
}
Output
My name is John Doe.
Change the variable start and end marker to the windows command shell style such as %name%
Parameters
| Argument Name |
Value |
Note |
| value |
|
Any value |
| template |
My name is %first_name% %last_name%. |
|
| template_type |
|
|
| ctx_data |
. |
Make sure that From previous tasks is selected |
| ctx_inputs |
|
|
| ctx_inc |
|
|
| variable_markers |
%,% |
|
| keep_symbol_to_null |
|
|
Context Data
{
"first_name": "John",
"last_name": "Doe"
}
Output
My name is John Doe.
Change the variable start and end marker to the UNIX shell style such as $name
Parameters
| Argument Name |
Value |
Note |
| value |
|
Any value |
| template |
My name is $first_name $last_name. |
|
| template_type |
|
|
| ctx_data |
. |
Make sure that From previous tasks is selected |
| ctx_inputs |
|
|
| ctx_inc |
|
|
| variable_markers |
$ |
|
| keep_symbol_to_null |
|
|
Context Data
{
"first_name": "John",
"last_name": "Doe"
}
Output
My name is John Doe.
Keep variable names if they are missing in the context
Parameters
| Argument Name |
Value |
Note |
| value |
|
Any value |
| template |
My name is ${first_name} ${last_name}. |
|
| template_type |
|
|
| ctx_data |
. |
Make sure that From previous tasks is selected |
| ctx_inputs |
|
|
| ctx_inc |
|
|
| variable_markers |
|
|
| keep_symbol_to_null |
true |
|
Context Data
{
"first_name": "John"
}
Output
My name is John ${last_name}
Use DTs to build variables
Parameters
| Argument Name |
Value |
Note |
| value |
|
Any value |
| template |
My name is ${first_name=val.toUpperCase()} ${last_name=val.toUpperCase()}. |
|
| template_type |
|
|
| ctx_data |
. |
Make sure that From previous tasks is selected |
| ctx_inputs |
|
|
| ctx_inc |
|
|
| variable_markers |
|
|
| keep_symbol_to_null |
|
|
Context Data
{
"first_name": "John",
"last_name": "Doe"
}
Output
My name is JOHN DOE.
Convert all the values in a structured data
Parameters
| Argument Name |
Value |
Note |
| value |
|
Any value |
| template |
See the template |
|
| template_type |
|
|
| ctx_data |
. |
Make sure that From previous tasks is selected |
| ctx_inputs |
|
|
| ctx_inc |
|
|
| variable_markers |
|
|
| keep_symbol_to_null |
|
|
template (structured)
{
"1": "First name is ${first_name}",
"2": "Last name is ${last_name}",
"3": [
"First name is ${first_name}",
"Last name is ${last_name}"
]
}
Context Data
{
"first_name": "John",
"last_name": "Doe"
}
Output
{
"1": "First name is John",
"2": "Last name is Doe",
"3": [
"First name is John",
"Last name is Doe"
]
}
Convert all the values in a JSON text
Parameters
| Argument Name |
Value |
Note |
| value |
|
Any value |
| template |
See the template |
|
| template_type |
json |
|
| ctx_data |
. |
Make sure that From previous tasks is selected |
| ctx_inputs |
|
|
| ctx_inc |
|
|
| variable_markers |
|
|
| keep_symbol_to_null |
|
|
template
{
"1": "First name is ${first_name}",
"2": "Last name is ${last_name}",
"3": [
"First name is ${first_name}",
"Last name is ${last_name}"
]
}
Context Data
{
"first_name": "John",
"last_name": "Doe"
}
Output
{
"1": "First name is John",
"2": "Last name is Doe",
"3": [
"First name is John",
"Last name is Doe"
]
}