Skip to content

Index

pyvider.hcl.factories

Factory functions for creating Terraform CTY structures.

Classes

HclFactoryError

Bases: ValueError

Custom exception for errors during HCL factory operations.

HclTypeParsingError

Bases: ValueError

Custom exception for errors during HCL type string parsing.

Functions

create_resource_cty

create_resource_cty(
    r_type: str,
    r_name: str,
    attributes_py: dict[str, Any],
    attributes_schema_py: dict[str, str] | None = None,
) -> CtyValue[Any]

Create a Terraform resource CTY structure.

Parameters:

Name Type Description Default
r_type str

Resource type (e.g., "aws_instance")

required
r_name str

Resource name

required
attributes_py dict[str, Any]

Resource attributes as Python dict

required
attributes_schema_py dict[str, str] | None

Optional type strings for attributes

None

Returns:

Type Description
CtyValue[Any]

CTY value representing Terraform resource structure

Raises:

Type Description
HclFactoryError

If validation fails

Example

resource = create_resource_cty( ... r_type="aws_instance", ... r_name="web", ... attributes_py={"ami": "ami-123", "instance_type": "t2.micro"}, ... attributes_schema_py={"ami": "string", "instance_type": "string"} ... )

Source code in pyvider/hcl/factories/resources.py
def create_resource_cty(  # noqa: C901
    r_type: str,
    r_name: str,
    attributes_py: dict[str, Any],
    attributes_schema_py: dict[str, str] | None = None,
) -> CtyValue[Any]:
    """Create a Terraform resource CTY structure.

    Args:
        r_type: Resource type (e.g., "aws_instance")
        r_name: Resource name
        attributes_py: Resource attributes as Python dict
        attributes_schema_py: Optional type strings for attributes

    Returns:
        CTY value representing Terraform resource structure

    Raises:
        HclFactoryError: If validation fails

    Example:
        >>> resource = create_resource_cty(
        ...     r_type="aws_instance",
        ...     r_name="web",
        ...     attributes_py={"ami": "ami-123", "instance_type": "t2.micro"},
        ...     attributes_schema_py={"ami": "string", "instance_type": "string"}
        ... )
    """
    logger.debug("🏭⏳ Creating resource", r_type=r_type, r_name=r_name)

    if not r_type or not r_type.strip():
        logger.error("🏭❌ Empty resource type")
        raise HclFactoryError("Resource type 'r_type' cannot be empty.")

    if not r_name or not r_name.strip():
        logger.error("🏭❌ Empty resource name")
        raise HclFactoryError("Resource name 'r_name' cannot be empty.")

    attributes_cty_schema: dict[str, CtyType[Any]] = {}

    if attributes_schema_py is not None:
        for attr_name, attr_type_str in attributes_schema_py.items():
            try:
                attributes_cty_schema[attr_name] = parse_hcl_type_string(attr_type_str)
            except HclTypeParsingError as e:
                logger.error(
                    "🏭❌ Attribute type parsing failed",
                    r_type=r_type,
                    r_name=r_name,
                    attr_name=attr_name,
                    type_str=attr_type_str,
                    error=str(e),
                )
                raise HclFactoryError(
                    f"Invalid type string for attribute '{attr_name}' ('{attr_type_str}') "
                    f"in resource '{r_type}.{r_name}': {e}"
                ) from e

        for attr_name in attributes_py:
            if attr_name not in attributes_cty_schema:
                logger.error(
                    "🏭❌ Missing type for attribute",
                    r_type=r_type,
                    r_name=r_name,
                    attr_name=attr_name,
                )
                raise HclFactoryError(
                    f"Missing type string in attributes_schema_py for attribute '{attr_name}' "
                    f"of resource '{r_type}.{r_name}'."
                )

        resource_attributes_obj_type = CtyObject(attributes_cty_schema)
        try:
            resource_attributes_obj_type.validate(attributes_py)
        except CtyValidationError as e:
            logger.error(
                "🏭❌ Attribute validation failed",
                r_type=r_type,
                r_name=r_name,
                error=str(e),
            )
            raise HclFactoryError(
                f"One or more attributes for resource '{r_type}.{r_name}' are not compatible "
                f"with the provided schema: {e}"
            ) from e
    else:
        logger.debug("🏭⏳ Inferring attribute types", r_type=r_type, r_name=r_name)
        inferred_attributes_cty = auto_infer_cty_type(attributes_py)
        if isinstance(inferred_attributes_cty.type, CtyObject):
            attributes_cty_schema = inferred_attributes_cty.type.attribute_types
        else:
            logger.error("🏭❌ Type inference failed", r_type=r_type, r_name=r_name)
            raise HclFactoryError("Could not infer object type from attributes.")

    root_py_struct = {"resource": [{r_type: [{r_name: attributes_py}]}]}
    root_schema = CtyObject(
        {
            "resource": CtyList(
                element_type=CtyObject(
                    {r_type: CtyList(element_type=CtyObject({r_name: CtyObject(attributes_cty_schema)}))}
                )
            )
        }
    )

    try:
        result = root_schema.validate(root_py_struct)
        return result  # type: ignore[no-any-return]
    except CtyError as e:
        logger.error("🏭❌ Resource creation failed", r_type=r_type, r_name=r_name, error=str(e))
        raise HclFactoryError(f"Internal error creating resource CtyValue: {e}") from e

create_variable_cty

create_variable_cty(
    name: str,
    type_str: str,
    default_py: Any | None = None,
    description: str | None = None,
    sensitive: bool | None = None,
    nullable: bool | None = None,
) -> CtyValue[Any]

Create a Terraform variable CTY structure.

Parameters:

Name Type Description Default
name str

Variable name (must be valid identifier)

required
type_str str

HCL type string (e.g., "string", "list(number)")

required
default_py Any | None

Optional default value

None
description str | None

Optional description

None
sensitive bool | None

Optional sensitive flag

None
nullable bool | None

Optional nullable flag

None

Returns:

Type Description
CtyValue[Any]

CTY value representing Terraform variable structure

Raises:

Type Description
HclFactoryError

If validation fails

Example

var = create_variable_cty( ... name="region", ... type_str="string", ... default_py="us-west-2" ... )

Source code in pyvider/hcl/factories/variables.py
def create_variable_cty(  # noqa: C901
    name: str,
    type_str: str,
    default_py: Any | None = None,
    description: str | None = None,
    sensitive: bool | None = None,
    nullable: bool | None = None,
) -> CtyValue[Any]:
    """Create a Terraform variable CTY structure.

    Args:
        name: Variable name (must be valid identifier)
        type_str: HCL type string (e.g., "string", "list(number)")
        default_py: Optional default value
        description: Optional description
        sensitive: Optional sensitive flag
        nullable: Optional nullable flag

    Returns:
        CTY value representing Terraform variable structure

    Raises:
        HclFactoryError: If validation fails

    Example:
        >>> var = create_variable_cty(
        ...     name="region",
        ...     type_str="string",
        ...     default_py="us-west-2"
        ... )
    """
    logger.debug("🏭⏳ Creating variable", name=name, type_str=type_str)

    if not name or not name.isidentifier():
        logger.error("🏭❌ Invalid variable name", name=name)
        raise HclFactoryError(f"Invalid variable name: '{name}'. Must be a valid identifier.")

    try:
        parsed_variable_type = parse_hcl_type_string(type_str)
    except HclTypeParsingError as e:
        logger.error("🏭❌ Type string parsing failed", name=name, type_str=type_str, error=str(e))
        raise HclFactoryError(f"Invalid type string for variable '{name}': {e}") from e

    variable_attrs_py: dict[str, Any] = {"type": type_str}

    if description is not None:
        variable_attrs_py["description"] = description
    if sensitive is not None:
        variable_attrs_py["sensitive"] = sensitive
    if nullable is not None:
        variable_attrs_py["nullable"] = nullable

    if default_py is not None:
        try:
            parsed_variable_type.validate(default_py)
        except CtyValidationError as e:
            logger.error(
                "🏭❌ Default value validation failed",
                name=name,
                type_str=type_str,
                error=str(e),
            )
            raise HclFactoryError(
                f"Default value for variable '{name}' is not compatible with type '{type_str}': {e}"
            ) from e
        variable_attrs_py["default"] = default_py

    variable_attrs_schema: dict[str, CtyType[Any]] = {"type": CtyString()}
    if "description" in variable_attrs_py:
        variable_attrs_schema["description"] = CtyString()
    if "sensitive" in variable_attrs_py:
        variable_attrs_schema["sensitive"] = CtyBool()
    if "nullable" in variable_attrs_py:
        variable_attrs_schema["nullable"] = CtyBool()
    if "default" in variable_attrs_py:
        variable_attrs_schema["default"] = parsed_variable_type

    root_py_struct = {"variable": [{name: variable_attrs_py}]}
    root_schema = CtyObject(
        {"variable": CtyList(element_type=CtyObject({name: CtyObject(variable_attrs_schema)}))}
    )

    try:
        result = root_schema.validate(root_py_struct)
        return result  # type: ignore[no-any-return]
    except CtyError as e:
        logger.error("🏭❌ Variable creation failed", name=name, error=str(e))
        raise HclFactoryError(f"Internal error creating variable CtyValue: {e}") from e

parse_hcl_type_string

parse_hcl_type_string(type_str: str) -> CtyType[Any]

Parse HCL type string into CTY type.

Supports: - Primitives: string, number, bool, any - Lists: list(element_type) - Maps: map(element_type) - Objects: object({attr=type, ...})

Parameters:

Name Type Description Default
type_str str

HCL type string (e.g., "list(string)", "object({name=string})")

required

Returns:

Type Description
CtyType[Any]

Corresponding CTY type

Raises:

Type Description
HclTypeParsingError

If type string is malformed

Example

parse_hcl_type_string("list(string)") CtyList(element_type=CtyString())

Source code in pyvider/hcl/factories/types.py
def parse_hcl_type_string(type_str: str) -> CtyType[Any]:  # noqa: C901
    """Parse HCL type string into CTY type.

    Supports:
    - Primitives: string, number, bool, any
    - Lists: list(element_type)
    - Maps: map(element_type)
    - Objects: object({attr=type, ...})

    Args:
        type_str: HCL type string (e.g., "list(string)", "object({name=string})")

    Returns:
        Corresponding CTY type

    Raises:
        HclTypeParsingError: If type string is malformed

    Example:
        >>> parse_hcl_type_string("list(string)")
        CtyList(element_type=CtyString())
    """
    type_str = type_str.strip()

    if type_str.lower() in PRIMITIVE_TYPE_MAP:
        return PRIMITIVE_TYPE_MAP[type_str.lower()]

    match = COMPLEX_TYPE_REGEX.match(type_str)
    if not match:
        raise HclTypeParsingError(f"Unknown or malformed type string: '{type_str}'")

    type_keyword = match.group(1).lower()
    inner_content = match.group(2).strip()

    if type_keyword == "list":
        if not inner_content:
            raise HclTypeParsingError("List type string is empty, e.g., 'list()'")
        element_type = parse_hcl_type_string(inner_content)
        return CtyList(element_type=element_type)

    if type_keyword == "map":
        if not inner_content:
            raise HclTypeParsingError("Map type string is empty, e.g., 'map()'")
        element_type = parse_hcl_type_string(inner_content)
        return CtyMap(element_type=element_type)

    if type_keyword == "object":
        if not inner_content.startswith("{") or not inner_content.endswith("}"):
            raise HclTypeParsingError(
                f"Object type string content must be enclosed in {{}}, got: '{inner_content}'"
            )
        if inner_content == "{}":
            return CtyObject({})

        attrs_str = inner_content[1:-1].strip()
        if not attrs_str:
            return CtyObject({})

        attributes = _parse_object_attributes_str(attrs_str)
        return CtyObject(attributes)

    raise HclTypeParsingError(f"Unhandled type keyword: '{type_keyword}'")