rostran/core/resources.py (193 lines of code) (raw):
from typing import Union
from openpyxl.cell.cell import Cell
from .exceptions import (
InvalidTemplateResource,
InvalidTemplateResources,
)
from .properties import Properties
from .utils import get_and_validate_cell, sorted_data, Graph
class Resource:
PROPERTIES_ = (
TYPE,
PROPERTIES,
DEPENDS_ON,
CONDITION,
DELETION_POLICY,
METADATA,
) = (
"Type",
"Properties",
"DependsOn",
"Condition",
"DeletionPolicy",
"Metadata",
)
DELETION_POLICIES = (RETAIN, DELETE) = ("Retain", "Delete")
def __init__(
self,
resource_id: str,
resource_type: str,
properties: Properties = None,
depends_on: Union[str, list] = None,
condition: str = None,
deletion_policy: str = None,
metadata: dict = None,
other_properties: dict = None,
):
self.resource_id = resource_id
self.type = resource_type
self.properties = properties or Properties()
self.depends_on = depends_on
self.condition = condition
self.deletion_policy = deletion_policy
self.metadata = metadata
self.other_properties = other_properties
@classmethod
def initialize(cls, resource_id: str, value: dict):
if not isinstance(value, dict):
raise InvalidTemplateResource(
name=resource_id,
reason=f"The type of value ({value}) should be dict",
)
value = value.copy()
props = value.pop(cls.PROPERTIES, None)
properties = Properties.initialize(props) if props else None
resource = cls(
resource_id,
resource_type=value.pop(cls.TYPE, None),
properties=properties,
depends_on=value.pop(cls.DEPENDS_ON, None),
condition=value.pop(cls.CONDITION, None),
deletion_policy=value.pop(cls.DELETION_POLICY, None),
metadata=value.pop(cls.METADATA, None),
other_properties=value,
)
resource.validate()
return resource
@classmethod
def initialize_from_excel(cls, header_cell: Cell, data_cell: Cell):
# resource type
orig_resource_type = header_cell.value
resource_type = get_and_validate_cell(header_cell, InvalidTemplateResource)
if not resource_type.startswith("ALIYUN::"):
resource_type = f"ALIYUN::{resource_type}"
if len(resource_type.split("::")) != 3:
raise InvalidTemplateResource(
name=orig_resource_type,
reason=f"Value of {header_cell} must be format of "
f"{{Product}}::{{Resource}} or ALIYUN::{{Product}}::{{Resource}}",
)
resource_id = data_cell.value
resource = cls(resource_id=resource_id, resource_type=resource_type)
resource.validate()
return resource
def validate(self):
if not isinstance(self.resource_id, str):
raise InvalidTemplateResource(
name=self.resource_id,
reason=f"The type should be str",
)
if not self.resource_id:
raise InvalidTemplateResource(
name=self.resource_id,
reason=f"ResourceId should not be empty",
)
if not isinstance(self.type, str):
raise InvalidTemplateResource(
name=self.resource_id,
reason=f"The type should be str",
)
if not self.type:
raise InvalidTemplateResource(
name=self.type,
reason=f"ResourceType should not be empty",
)
if self.depends_on is not None and not isinstance(self.depends_on, (str, list)):
raise InvalidTemplateResource(
name=self.resource_id,
reason=f"The type of {self.DEPENDS_ON} should be str or list",
)
if self.condition is not None and not isinstance(self.condition, str):
raise InvalidTemplateResource(
name=self.resource_id,
reason=f"The type of {self.CONDITION} should be str",
)
if (
self.deletion_policy is not None
and self.deletion_policy not in self.DELETION_POLICIES
):
raise InvalidTemplateResource(
name=self.resource_id,
reason=f"DeletionPolicy {self.deletion_policy} is not supported. "
f"Allowed types: {self.DELETION_POLICIES}",
)
if self.metadata is not None and not isinstance(self.metadata, dict):
raise InvalidTemplateResource(
name=self.resource_id,
reason=f"The type of {self.METADATA} should be dict",
)
def as_dict(self, format=False):
data = {Resource.TYPE: self.type}
if self.condition:
data[Resource.CONDITION] = self.condition
data[Resource.PROPERTIES] = self.properties.as_dict(format=format)
if self.depends_on:
if format and isinstance(self.depends_on, list):
depends_on = sorted_data(self.depends_on)
else:
depends_on = self.depends_on
data[Resource.DEPENDS_ON] = depends_on
if self.deletion_policy:
data[Resource.DELETION_POLICY] = self.deletion_policy
if self.metadata:
data[Resource.METADATA] = self.metadata
if self.other_properties:
other_properties = (
sorted_data(self.other_properties) if format else self.other_properties
)
data.update(other_properties)
return data
def get_depends_on_set(self):
depends_on_set = set()
if self.depends_on:
if not isinstance(self.depends_on, list):
depends_on = [self.depends_on]
else:
depends_on = self.depends_on
depends_on_set.update(depends_on)
depends_on_set.update(self.properties.get_depends_on_set())
return depends_on_set
class Resources(dict):
@classmethod
def initialize(cls, data: dict):
if not isinstance(data, dict):
raise InvalidTemplateResources(
reason=f"The type of data ({data}) should be dict"
)
resources = cls()
for resource_id, value in data.items():
resource = Resource.initialize(resource_id, value)
resources.add(resource)
return resources
def add(self, resource: Resource):
self[resource.resource_id] = resource
def as_dict(self, format=False) -> dict:
data = {}
keys = self.keys()
if format:
graph = Graph()
for key in keys:
resource: Resource = self[key]
depends_on_set = resource.get_depends_on_set()
if depends_on_set:
for depends_on in sorted(depends_on_set):
if depends_on in self:
graph.add_edge(key, depends_on)
else:
graph.add_edge(key)
else:
graph.add_edge(key)
keys = graph.topo_sort()
for key in keys:
resource: Resource = self[key]
data[key] = resource.as_dict(format)
return data