"""
Data formatting utilities for FortiOS objects and data structures.
Provides simple, type-agnostic conversion functions that handle any input gracefully.
Never raises exceptions - returns sensible defaults for edge cases.
"""
from __future__ import annotations
import json
from typing import Any
__all__ = [
"to_json",
"to_csv",
"to_dict",
"to_multiline",
"to_list",
"to_quoted",
"to_table",
"to_yaml",
"to_xml",
"to_key_value",
"to_markdown_table",
"to_dictlist",
"to_listdict",
]
[docs]
def to_json(data: Any, indent: int = 2, **kwargs: Any) -> str:
"""
Convert any data to formatted JSON string.
Handles objects with __dict__, converts sets/tuples to lists,
and uses str() fallback for non-serializable types.
Args:
data: Any Python object to convert to JSON
indent: Number of spaces for indentation (default: 2)
**kwargs: Additional arguments passed to json.dumps()
Returns:
Formatted JSON string
"""
def default_handler(obj: Any) -> Any:
"""Handle non-serializable objects."""
if hasattr(obj, "__dict__"):
return obj.__dict__
if isinstance(obj, (set, tuple)):
return list(obj)
return str(obj)
return json.dumps(data, indent=indent, default=default_handler, **kwargs)
[docs]
def to_csv(data: Any, separator: str = ", ") -> str:
"""
Convert any data to comma-separated string.
Args:
data: Any Python object to convert
separator: String to use between items (default: ', ')
Returns:
Comma-separated string
Examples:
>>> to_csv(['port1', 'port2', 'port3'])
'port1, port2, port3'
>>> to_csv({'x': 1, 'y': 2, 'z': 3})
'x=1, y=2, z=3'
>>> to_csv('already a string')
'already a string'
>>> to_csv(None)
''
>>> to_csv([1, 2, 3], separator=' | ')
'1 | 2 | 3'
>>> class Interface:
... def __init__(self):
... self.name = "port1"
... self.ip = "10.0.0.1"
>>> to_csv(Interface())
'name=port1, ip=10.0.0.1'
"""
if data is None:
return ""
if isinstance(data, str):
return data
if isinstance(data, (int, float, bool)):
return str(data)
if isinstance(data, (list, tuple, set)):
return separator.join(str(item) for item in data)
if isinstance(data, dict):
return separator.join(f"{k}={v}" for k, v in data.items())
if hasattr(data, "__dict__"):
return to_csv(data.__dict__, separator)
return str(data)
[docs]
def to_dict(data: Any) -> dict:
"""
Convert any data to dictionary.
Returns a dict with string keys for most inputs (objects, existing dicts),
or integer keys for list/tuple inputs.
Args:
data: Any Python object to convert
Returns:
Dictionary representation of the data
Examples:
>>> class Policy:
... def __init__(self):
... self.name = "Allow-All"
... self.action = "accept"
>>> to_dict(Policy())
{'name': 'Allow-All', 'action': 'accept'}
>>> to_dict({'already': 'a dict'})
{'already': 'a dict'}
>>> to_dict([('a', 1), ('b', 2)])
{'a': 1, 'b': 2}
>>> to_dict(['x', 'y', 'z'])
{0: 'x', 1: 'y', 2: 'z'}
>>> to_dict('simple string')
{'value': 'simple string'}
>>> to_dict(None)
{'value': None}
"""
if isinstance(data, dict):
return data
if hasattr(data, "__dict__"):
return data.__dict__
if isinstance(data, (list, tuple)):
# Check if it's a list of tuples (can be converted to dict)
if data and all(
isinstance(item, (list, tuple)) and len(item) == 2 for item in data
):
try:
return dict(data)
except (TypeError, ValueError):
pass
# Otherwise convert to indexed dict
return {i: v for i, v in enumerate(data)}
# For primitives, wrap in value key
return {"value": data}
[docs]
def to_multiline(data: Any, separator: str = "\n") -> str:
"""
Convert any data to newline-separated string.
Args:
data: Any Python object to convert
separator: String to use between lines (default: '\\n')
Returns:
Newline-separated string
Examples:
>>> print(to_multiline(['port1', 'port2', 'port3']))
port1
port2
port3
>>> print(to_multiline({'name': 'policy1', 'action': 'accept'}))
name: policy1
action: accept
>>> to_multiline('already a string')
'already a string'
>>> to_multiline(None)
''
>>> class Policy:
... def __init__(self):
... self.name = "Allow-All"
... self.policyid = 1
>>> print(to_multiline(Policy()))
name: Allow-All
policyid: 1
"""
if data is None:
return ""
if isinstance(data, str):
return data
if isinstance(data, (int, float, bool)):
return str(data)
if isinstance(data, (list, tuple, set)):
return separator.join(str(item) for item in data)
if isinstance(data, dict):
return separator.join(f"{k}: {v}" for k, v in data.items())
if hasattr(data, "__dict__"):
return to_multiline(data.__dict__, separator)
return str(data)
[docs]
def to_list(data: Any, delimiter: str | None = None) -> list[Any]:
"""
Convert any data to list.
Args:
data: Any Python object to convert
delimiter: If data is a string, split by this delimiter.
If None and data is string with spaces, auto-splits by space.
Common delimiters: ',', ' ', '|', ';', etc.
Returns:
List representation of the data
Examples:
>>> to_list(['already', 'a', 'list'])
['already', 'a', 'list']
>>> to_list(('tuple', 'to', 'list'))
['tuple', 'to', 'list']
>>> to_list({'a', 'b', 'c'}) # set to list
['a', 'b', 'c']
>>> to_list('port1,port2,port3', delimiter=',')
['port1', 'port2', 'port3']
>>> to_list('port1 port2 port3') # auto-splits on space
['port1', 'port2', 'port3']
>>> to_list('80 443 8080') # works with numbers as strings
['80', '443', '8080']
>>> to_list('port1 | port2 | port3', delimiter=' | ')
['port1', 'port2', 'port3']
>>> to_list('single_string') # no spaces, returns as-is
['single_string']
>>> to_list({'name': 'policy1', 'action': 'accept'})
['policy1', 'accept']
>>> to_list(None)
[]
>>> to_list(42)
[42]
>>> class Policy:
... def __init__(self):
... self.name = "Allow-All"
... self.policyid = 1
>>> to_list(Policy())
['Allow-All', 1]
"""
if data is None:
return []
if isinstance(data, list):
return data
if isinstance(data, (tuple, set)):
return list(data)
if isinstance(data, str):
if delimiter is not None:
# Explicit delimiter provided
return [item.strip() for item in data.split(delimiter)]
elif " " in data:
# Auto-split on space if no delimiter specified and string contains spaces
return data.split()
else:
# Single item string with no spaces
return [data]
if isinstance(data, dict):
return list(data.values())
if hasattr(data, "__dict__"):
return list(data.__dict__.values())
# For primitives, return single-item list
return [data]
[docs]
def to_quoted(data: Any, quote: str = '"', separator: str = ", ") -> str:
"""
Convert any data to quoted string representation.
Args:
data: Any Python object to convert
quote: Quote character to use (default: '"')
separator: String to use between quoted items (default: ', ')
Returns:
Quoted string representation
Examples:
>>> to_quoted(['port1', 'port2', 'port3'])
'"port1", "port2", "port3"'
>>> to_quoted({'x': 1, 'y': 2})
'"x", "y"'
>>> to_quoted('hello')
'"hello"'
>>> to_quoted(None)
'""'
>>> to_quoted([1, 2, 3], quote="'")
"'1', '2', '3'"
>>> class Interface:
... def __init__(self):
... self.name = "port1"
... self.vlan = 10
>>> to_quoted(Interface())
'"name", "vlan"'
"""
if data is None:
return f"{quote}{quote}"
if isinstance(data, str):
return f"{quote}{data}{quote}"
if isinstance(data, (int, float, bool)):
return f"{quote}{data}{quote}"
if isinstance(data, (list, tuple, set)):
return separator.join(f"{quote}{item}{quote}" for item in data)
if isinstance(data, dict):
# Quote the keys
return separator.join(f"{quote}{k}{quote}" for k in data.keys())
if hasattr(data, "__dict__"):
return to_quoted(data.__dict__, quote, separator)
return f"{quote}{data}{quote}"
[docs]
def to_table(data: Any, headers: bool = True, delimiter: str = " | ") -> str:
"""
Convert data to table format.
Args:
data: Any Python object to convert (list of dicts, list of objects, etc.)
headers: Whether to include headers (default: True)
delimiter: Column delimiter (default: ' | ')
Returns:
Table-formatted string
Examples:
>>> policies = [
... {'name': 'Allow-Web', 'action': 'accept', 'policyid': 1},
... {'name': 'Block-All', 'action': 'deny', 'policyid': 2}
... ]
>>> print(to_table(policies))
name | action | policyid
Allow-Web | accept | 1
Block-All | deny | 2
>>> to_table(policies, headers=False)
'Allow-Web | accept | 1\\nBlock-All | deny | 2'
>>> to_table(policies, delimiter=' || ')
'name || action || policyid\\nAllow-Web || accept || 1\\nBlock-All || deny || 2'
"""
if data is None:
return ""
if isinstance(data, str):
return data
# Convert single dict to list
if isinstance(data, dict):
data = [data]
# Convert objects with __dict__ to dicts
if not isinstance(data, (list, tuple)):
if hasattr(data, "__dict__"):
data = [data]
else:
return str(data)
# Convert list of objects to list of dicts
processed_data = []
for item in data:
if hasattr(item, "__dict__"):
processed_data.append(item.__dict__)
elif isinstance(item, dict):
processed_data.append(item)
else:
processed_data.append({"value": item})
if not processed_data:
return ""
# Get all unique keys
all_keys = []
for item in processed_data:
for key in item.keys():
if key not in all_keys:
all_keys.append(key)
lines = []
# Add headers
if headers:
lines.append(delimiter.join(str(k) for k in all_keys))
# Add rows
for item in processed_data:
row = delimiter.join(str(item.get(k, "")) for k in all_keys)
lines.append(row)
return "\n".join(lines)
[docs]
def to_yaml(data: Any, indent: int = 2) -> str:
"""
Convert data to YAML-like format (simple, no external dependencies).
Args:
data: Any Python object to convert
indent: Number of spaces for indentation (default: 2)
Returns:
YAML-style string
Examples:
>>> policy = {'name': 'Allow-Web', 'action': 'accept', 'srcintf': ['port1', 'port2']}
>>> print(to_yaml(policy))
name: Allow-Web
action: accept
srcintf:
- port1
- port2
>>> print(to_yaml({'nested': {'key': 'value'}}))
nested:
key: value
"""
def _format_yaml(obj: Any, level: int = 0) -> str:
indent_str = " " * (indent * level)
if obj is None:
return "null"
if isinstance(obj, bool):
return "true" if obj else "false"
if isinstance(obj, (int, float)):
return str(obj)
if isinstance(obj, str):
# Quote if contains special chars
if ":" in obj or "#" in obj or obj.startswith((" ", "-")):
return f"'{obj}'"
return obj
if isinstance(obj, (list, tuple)):
if not obj:
return "[]"
lines = []
for item in obj:
if isinstance(item, (dict, list)):
# Complex item
lines.append(f"{indent_str}- ")
lines.append(_format_yaml(item, level + 1))
else:
# Simple item
lines.append(f"{indent_str}- {_format_yaml(item, 0)}")
return "\n".join(lines)
if isinstance(obj, dict):
if not obj:
return "{}"
lines = []
for key, value in obj.items():
if isinstance(value, dict):
lines.append(f"{indent_str}{key}:")
lines.append(_format_yaml(value, level + 1))
elif isinstance(value, (list, tuple)):
lines.append(f"{indent_str}{key}:")
lines.append(_format_yaml(value, level + 1))
else:
lines.append(
f"{indent_str}{key}: {_format_yaml(value, 0)}"
)
return "\n".join(lines)
if hasattr(obj, "__dict__"):
return _format_yaml(obj.__dict__, level)
return str(obj)
return _format_yaml(data)
[docs]
def to_xml(data: Any, root: str = "data", indent: int = 2) -> str:
"""
Convert data to simple XML format (no external dependencies).
Args:
data: Any Python object to convert
root: Root element name (default: 'data')
indent: Number of spaces for indentation (default: 2)
Returns:
XML string
Examples:
>>> policy = {'name': 'Allow-Web', 'policyid': 1}
>>> print(to_xml(policy, root='policy'))
<policy>
<name>Allow-Web</name>
<policyid>1</policyid>
</policy>
>>> policies = [{'name': 'p1'}, {'name': 'p2'}]
>>> print(to_xml(policies, root='policies'))
<policies>
<item>
<name>p1</name>
</item>
<item>
<name>p2</name>
</item>
</policies>
"""
def _escape_xml(text: str) -> str:
"""Escape special XML characters."""
# Order matters: & must be first to avoid double-escaping
text = text.replace("&", "&")
text = text.replace("<", "<")
text = text.replace(">", ">")
text = text.replace('"', """)
text = text.replace("'", "'")
return text
def _format_xml(obj: Any, tag: str, level: int = 0) -> str:
indent_str = " " * (indent * level)
if obj is None:
return f"{indent_str}<{tag}/>"
if isinstance(obj, (str, int, float, bool)):
value = _escape_xml(str(obj)) if isinstance(obj, str) else obj
return f"{indent_str}<{tag}>{value}</{tag}>"
if isinstance(obj, (list, tuple)):
lines = [f"{indent_str}<{tag}>"]
for item in obj:
lines.append(_format_xml(item, "item", level + 1))
lines.append(f"{indent_str}</{tag}>")
return "\n".join(lines)
if isinstance(obj, dict):
lines = [f"{indent_str}<{tag}>"]
for key, value in obj.items():
# Sanitize key for XML
safe_key = key.replace(" ", "_").replace("-", "_")
lines.append(_format_xml(value, safe_key, level + 1))
lines.append(f"{indent_str}</{tag}>")
return "\n".join(lines)
if hasattr(obj, "__dict__"):
return _format_xml(obj.__dict__, tag, level)
return f"{indent_str}<{tag}>{obj}</{tag}>"
return _format_xml(data, root)
[docs]
def to_key_value(
data: Any, separator: str = "=", delimiter: str = "\n"
) -> str:
"""
Convert data to key=value pairs format.
Args:
data: Any Python object to convert
separator: Separator between key and value (default: '=')
delimiter: Delimiter between pairs (default: '\\n')
Returns:
Key-value pairs string
Examples:
>>> config = {'host': '192.168.1.1', 'port': 443, 'verify': False}
>>> print(to_key_value(config))
host=192.168.1.1
port=443
verify=False
>>> to_key_value(config, separator=': ', delimiter='; ')
'host: 192.168.1.1; port: 443; verify: False'
"""
if data is None:
return ""
if isinstance(data, str):
return data
if isinstance(data, dict):
return delimiter.join(f"{k}{separator}{v}" for k, v in data.items())
if hasattr(data, "__dict__"):
return to_key_value(data.__dict__, separator, delimiter)
if isinstance(data, (list, tuple)):
# For lists, use index as key
return delimiter.join(f"{i}{separator}{v}" for i, v in enumerate(data))
return str(data)
[docs]
def to_markdown_table(data: Any) -> str:
"""
Convert data to Markdown table format.
Args:
data: List of dicts or list of objects
Returns:
Markdown table string
Examples:
>>> policies = [
... {'name': 'Allow-Web', 'action': 'accept'},
... {'name': 'Block-All', 'action': 'deny'}
... ]
>>> print(to_markdown_table(policies))
| name | action |
| --- | --- |
| Allow-Web | accept |
| Block-All | deny |
"""
if data is None:
return ""
if isinstance(data, str):
return data
# Convert single dict to list
if isinstance(data, dict):
data = [data]
# Convert objects with __dict__ to dicts
if not isinstance(data, (list, tuple)):
if hasattr(data, "__dict__"):
data = [data]
else:
return str(data)
# Convert list of objects to list of dicts
processed_data = []
for item in data:
if hasattr(item, "__dict__"):
processed_data.append(item.__dict__)
elif isinstance(item, dict):
processed_data.append(item)
else:
processed_data.append({"value": item})
if not processed_data:
return ""
# Get all unique keys
all_keys = []
for item in processed_data:
for key in item.keys():
if key not in all_keys:
all_keys.append(key)
lines = []
# Add header
lines.append("| " + " | ".join(str(k) for k in all_keys) + " |")
lines.append("| " + " | ".join("---" for _ in all_keys) + " |")
# Add rows
for item in processed_data:
row = "| " + " | ".join(str(item.get(k, "")) for k in all_keys) + " |"
lines.append(row)
return "\n".join(lines)
[docs]
def to_dictlist(data: Any) -> list[dict[str, Any]]:
"""
Convert dict of lists to list of dicts (columnar to row format).
Useful for transforming columnar data into row-based records.
Args:
data: Dict where values are lists, or any convertible data
Returns:
List of dicts where each dict is one row
Examples:
>>> columnar = {'name': ['p1', 'p2'], 'action': ['accept', 'deny']}
>>> to_dictlist(columnar)
[{'name': 'p1', 'action': 'accept'}, {'name': 'p2', 'action': 'deny'}]
>>> to_dictlist({'ports': ['80', '443', '8080']})
[{'ports': '80'}, {'ports': '443'}, {'ports': '8080'}]
>>> # Already list of dicts - returns as-is
>>> to_dictlist([{'name': 'p1'}, {'name': 'p2'}])
[{'name': 'p1'}, {'name': 'p2'}]
>>> to_dictlist(None)
[]
"""
if data is None:
return []
# Already list of dicts
if isinstance(data, list):
if all(isinstance(item, dict) for item in data):
return data
# List of objects with __dict__
if all(hasattr(item, "__dict__") for item in data):
return [item.__dict__ for item in data]
# Convert simple list to list of value dicts
return [{"value": item} for item in data]
# Convert object to dict first
if hasattr(data, "__dict__"):
data = data.__dict__
if not isinstance(data, dict):
return [{"value": data}]
# Dict of lists - convert to list of dicts
# Get all keys
keys = list(data.keys())
if not keys:
return []
# Check if all values are lists/tuples
values = [
data[k] if isinstance(data[k], (list, tuple)) else [data[k]]
for k in keys
]
# Find max length
max_len = max(len(v) for v in values) if values else 0
# Pad lists to same length with None
padded_values = []
for v in values:
if len(v) < max_len:
padded_values.append(list(v) + [None] * (max_len - len(v)))
else:
padded_values.append(list(v))
# Transpose to list of dicts
result = []
for i in range(max_len):
row = {keys[j]: padded_values[j][i] for j in range(len(keys))}
result.append(row)
return result
[docs]
def to_listdict(data: Any) -> dict[str, list[Any]]:
"""
Convert list of dicts to dict of lists (row to columnar format).
Useful for transforming row-based records into columnar data.
Args:
data: List of dicts, list of objects, or any convertible data
Returns:
Dict where keys are field names and values are lists
Examples:
>>> rows = [{'name': 'p1', 'action': 'accept'}, {'name': 'p2', 'action': 'deny'}]
>>> to_listdict(rows)
{'name': ['p1', 'p2'], 'action': ['accept', 'deny']}
>>> to_listdict([{'ports': '80'}, {'ports': '443'}, {'ports': '8080'}])
{'ports': ['80', '443', '8080']}
>>> # Single dict becomes dict of single-item lists
>>> to_listdict({'name': 'p1', 'action': 'accept'})
{'name': ['p1'], 'action': ['accept']}
>>> to_listdict(None)
{}
"""
if data is None:
return {}
# Already dict of lists
if isinstance(data, dict):
# Check if it's already dict of lists
if all(isinstance(v, (list, tuple)) for v in data.values()):
return {k: list(v) for k, v in data.items()}
# Convert single values to lists
return {k: [v] for k, v in data.items()}
# Convert object to dict first
if hasattr(data, "__dict__"):
data = data.__dict__
return {k: [v] for k, v in data.items()}
if not isinstance(data, (list, tuple)):
return {"value": [data]}
# List of dicts - convert to dict of lists
if not data:
return {}
# Convert objects to dicts
processed_data = []
for item in data:
if hasattr(item, "__dict__"):
processed_data.append(item.__dict__)
elif isinstance(item, dict):
processed_data.append(item)
else:
processed_data.append({"value": item})
# Get all unique keys
all_keys = []
for item in processed_data:
for key in item.keys():
if key not in all_keys:
all_keys.append(key)
# Build dict of lists
result = {key: [] for key in all_keys}
for item in processed_data:
for key in all_keys:
result[key].append(item.get(key, None))
return result