From 113cbb22000d9139cf1238474c609e4f1a2c1502 Mon Sep 17 00:00:00 2001 From: Sivaselvan32 Date: Mon, 24 Nov 2025 17:02:56 +0530 Subject: [PATCH 01/18] feat(policy set parameter): added list, create and read methods --- examples/policy_set_parameter.py | 122 +++++++++++++++++ src/pytfe/client.py | 2 + src/pytfe/errors.py | 29 ++++ src/pytfe/models/__init__.py | 13 ++ src/pytfe/models/policy_set_parameter.py | 56 ++++++++ src/pytfe/resources/policy_set_parameter.py | 138 ++++++++++++++++++++ 6 files changed, 360 insertions(+) create mode 100644 examples/policy_set_parameter.py create mode 100644 src/pytfe/models/policy_set_parameter.py create mode 100644 src/pytfe/resources/policy_set_parameter.py diff --git a/examples/policy_set_parameter.py b/examples/policy_set_parameter.py new file mode 100644 index 0000000..e202cf8 --- /dev/null +++ b/examples/policy_set_parameter.py @@ -0,0 +1,122 @@ +from __future__ import annotations + +import argparse +import os + +from pytfe import TFEClient, TFEConfig +from pytfe.models import ( + PolicySetParameterCreateOptions, + PolicySetParameterListOptions, +) + + +def _print_header(title: str): + print("\n" + "=" * 80) + print(title) + print("=" * 80) + + +def main(): + parser = argparse.ArgumentParser( + description="Policy Set Parameters demo for python-tfe SDK" + ) + parser.add_argument( + "--address", default=os.getenv("TFE_ADDRESS", "https://app.terraform.io") + ) + parser.add_argument("--token", default=os.getenv("TFE_TOKEN", "")) + parser.add_argument("--policy-set-id", required=True, help="Policy Set ID") + parser.add_argument("--page", type=int, default=1) + parser.add_argument("--page-size", type=int, default=10) + parser.add_argument("--create", action="store_true", help="Create a test parameter") + parser.add_argument("--read", action="store_true", help="Read a specific parameter") + parser.add_argument("--parameter-id", help="Parameter ID for read operation") + parser.add_argument( + "--key", default="test_param", help="Parameter key for creation" + ) + parser.add_argument( + "--value", default="test_value", help="Parameter value for creation" + ) + parser.add_argument( + "--sensitive", action="store_true", help="Mark parameter as sensitive" + ) + args = parser.parse_args() + + cfg = TFEConfig(address=args.address, token=args.token) + client = TFEClient(cfg) + + # 1) List all parameters for the policy set + _print_header(f"Listing parameters for policy set: {args.policy_set_id}") + + options = PolicySetParameterListOptions( + page_number=args.page, + page_size=args.page_size, + ) + + param_list = client.policy_set_parameters.list(args.policy_set_id, options) + + print(f"Total parameters: {param_list.total_count}") + print(f"Page {param_list.current_page} of {param_list.total_pages}") + print() + + if not param_list.items: + print("No parameters found.") + else: + for param in param_list.items: + # Sensitive parameters will have masked values + value_display = "***SENSITIVE***" if param.sensitive else param.value + print(f"- {param.id}") + print(f" Key: {param.key}") + print(f" Value: {value_display}") + print(f" Category: {param.category.value}") + print(f" Sensitive: {param.sensitive}") + print() + + # 2) Read a specific parameter (if --read flag is provided) + if args.read: + if not args.parameter_id: + print("Error: --parameter-id is required for read operation") + return + + _print_header(f"Reading parameter: {args.parameter_id}") + + param = client.policy_set_parameters.read(args.policy_set_id, args.parameter_id) + + print(f"Parameter ID: {param.id}") + print(f" Key: {param.key}") + value_display = "***SENSITIVE***" if param.sensitive else param.value + print(f" Value: {value_display}") + print(f" Category: {param.category.value}") + print(f" Sensitive: {param.sensitive}") + + # 3) Create a new parameter (if --create flag is provided) + if args.create: + _print_header(f"Creating new parameter with key: {args.key}") + + create_options = PolicySetParameterCreateOptions( + key=args.key, + value=args.value, + sensitive=args.sensitive, + ) + + new_param = client.policy_set_parameters.create( + args.policy_set_id, create_options + ) + + print(f"Created parameter: {new_param.id}") + print(f" Key: {new_param.key}") + value_display = "***SENSITIVE***" if new_param.sensitive else new_param.value + print(f" Value: {value_display}") + print(f" Category: {new_param.category.value}") + print(f" Sensitive: {new_param.sensitive}") + + # List again to show the new parameter + _print_header("Listing parameters after creation") + updated_list = client.policy_set_parameters.list(args.policy_set_id) + print(f"Total parameters: {updated_list.total_count}") + for param in updated_list.items: + value_display = "***SENSITIVE***" if param.sensitive else param.value + print(f"- {param.key}: {value_display} (sensitive={param.sensitive})") + + +if __name__ == "__main__": + main() diff --git a/src/pytfe/client.py b/src/pytfe/client.py index 3894886..4d0227d 100644 --- a/src/pytfe/client.py +++ b/src/pytfe/client.py @@ -16,6 +16,7 @@ from .resources.policy_evaluation import PolicyEvaluations from .resources.policy_set import PolicySets from .resources.policy_set_outcome import PolicySets as PolicySetOutcomes +from .resources.policy_set_parameter import PolicySetParameters from .resources.policy_set_version import PolicySetVersions from .resources.projects import Projects from .resources.query_run import QueryRuns @@ -84,6 +85,7 @@ def __init__(self, config: TFEConfig | None = None): self.policy_evaluations = PolicyEvaluations(self._transport) self.policy_checks = PolicyChecks(self._transport) self.policy_sets = PolicySets(self._transport) + self.policy_set_parameters = PolicySetParameters(self._transport) self.policy_set_outcomes = PolicySetOutcomes(self._transport) self.policy_set_versions = PolicySetVersions(self._transport) diff --git a/src/pytfe/errors.py b/src/pytfe/errors.py index 3eac2be..d365904 100644 --- a/src/pytfe/errors.py +++ b/src/pytfe/errors.py @@ -460,3 +460,32 @@ class InvalidPolicyEvaluationIDError(InvalidValues): def __init__(self, message: str = "invalid value for policy evaluation ID"): super().__init__(message) + + +# Policy Set Parameter errors +class InvalidParamIDError(InvalidValues): + """Raised when an invalid policy set parameter ID is provided.""" + + def __init__(self, message: str = "invalid value for parameter ID"): + super().__init__(message) + + +class RequiredCategoryError(RequiredFieldMissing): + """Raised when a required category field is missing.""" + + def __init__(self, message: str = "category is required"): + super().__init__(message) + + +class InvalidCategoryError(InvalidValues): + """Raised when an invalid category field is provided.""" + + def __init__(self, message: str = "category must be policy-set"): + super().__init__(message) + + +class RequiredKeyError(RequiredFieldMissing): + """Raised when a required key field is missing.""" + + def __init__(self, message: str = "key is required"): + super().__init__(message) diff --git a/src/pytfe/models/__init__.py b/src/pytfe/models/__init__.py index f3eb33b..b19c446 100644 --- a/src/pytfe/models/__init__.py +++ b/src/pytfe/models/__init__.py @@ -133,6 +133,13 @@ PolicySetRemoveWorkspacesOptions, PolicySetUpdateOptions, ) +from .policy_set_parameter import ( + PolicySetParameter, + PolicySetParameterCreateOptions, + PolicySetParameterList, + PolicySetParameterListOptions, + PolicySetParameterUpdateOptions, +) from .policy_types import ( EnforcementLevel, PolicyKind, @@ -586,6 +593,12 @@ "PolicySetRemoveWorkspaceExclusionsOptions", "PolicySetRemoveProjectsOptions", "PolicySetUpdateOptions", + # Policy Set Parameters + "PolicySetParameter", + "PolicySetParameterCreateOptions", + "PolicySetParameterList", + "PolicySetParameterListOptions", + "PolicySetParameterUpdateOptions", "PolicyKind", "EnforcementLevel", # Variable Sets diff --git a/src/pytfe/models/policy_set_parameter.py b/src/pytfe/models/policy_set_parameter.py new file mode 100644 index 0000000..8666a63 --- /dev/null +++ b/src/pytfe/models/policy_set_parameter.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +from pydantic import BaseModel, ConfigDict, Field + +from .policy_set import PolicySet +from .variable import CategoryType + + +class PolicySetParameter(BaseModel): + model_config = ConfigDict(populate_by_name=True, validate_by_name=True) + + id: str + key: str = Field(..., alias="key") + value: str | None = Field(None, alias="value") + category: CategoryType = Field(..., alias="category") + sensitive: bool = Field(..., alias="sensitive") + + # relations + policy_set: PolicySet = Field(..., alias="configurable") + + +class PolicySetParameterList(BaseModel): + model_config = ConfigDict(populate_by_name=True, validate_by_name=True) + + items: list[PolicySetParameter] = Field(default_factory=list) + current_page: int | None = None + total_pages: int | None = None + prev_page: int | None = None + next_page: int | None = None + total_count: int | None = None + + +class PolicySetParameterListOptions(BaseModel): + model_config = ConfigDict(populate_by_name=True, validate_by_name=True) + + page_number: int | None = Field(None, alias="page[number]") + page_size: int | None = Field(None, alias="page[size]") + + +class PolicySetParameterCreateOptions(BaseModel): + model_config = ConfigDict(populate_by_name=True, validate_by_name=True) + + key: str = Field(..., alias="key") + value: str | None = Field(None, alias="value") + + # Required: The Category of the parameter, should always be "policy-set" + category: CategoryType = Field(default=CategoryType.POLICY_SET, alias="category") + sensitive: bool | None = Field(None, alias="sensitive") + + +class PolicySetParameterUpdateOptions(BaseModel): + model_config = ConfigDict(populate_by_name=True, validate_by_name=True) + + key: str | None = Field(None, alias="key") + value: str | None = Field(None, alias="value") + sensitive: bool | None = Field(None, alias="sensitive") diff --git a/src/pytfe/resources/policy_set_parameter.py b/src/pytfe/resources/policy_set_parameter.py new file mode 100644 index 0000000..56fa961 --- /dev/null +++ b/src/pytfe/resources/policy_set_parameter.py @@ -0,0 +1,138 @@ +from __future__ import annotations + +from ..errors import ( + InvalidCategoryError, + InvalidParamIDError, + InvalidPolicySetIDError, + RequiredCategoryError, + RequiredKeyError, +) +from ..models.policy_set_parameter import ( + PolicySetParameter, + PolicySetParameterCreateOptions, + PolicySetParameterList, + PolicySetParameterListOptions, +) +from ..models.variable import CategoryType +from ..utils import valid_string, valid_string_id +from ._base import _Service + + +class PolicySetParameters(_Service): + """ + PolicySetParameters describes all the parameter related methods that the Terraform Enterprise API supports. + TFE API docs: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/policy-set-params + """ + + def list( + self, policy_set_id: str, options: PolicySetParameterListOptions | None = None + ) -> PolicySetParameterList: + """List all the parameters associated with the given policy-set.""" + if not valid_string_id(policy_set_id): + raise InvalidPolicySetIDError() + params = options.model_dump(by_alias=True, exclude_none=True) if options else {} + r = self.t.request( + "GET", + path=f"api/v2/policy-sets/{policy_set_id}/parameters", + params=params, + ) + jd = r.json() + items = [] + meta = jd.get("meta", {}) + pagination = meta.get("pagination", {}) + for d in jd.get("data", []): + attrs = d.get("attributes", {}) + attrs["id"] = d.get("id") + attrs["policy_set"] = ( + d.get("relationships", {}).get("configurable", {}).get("data", {}) + ) + items.append(PolicySetParameter.model_validate(attrs)) + return PolicySetParameterList( + items=items, + current_page=pagination.get("current-page"), + total_pages=pagination.get("total-pages"), + prev_page=pagination.get("prev-page"), + next_page=pagination.get("next-page"), + total_count=pagination.get("total-count"), + ) + + def create( + self, policy_set_id: str, options: PolicySetParameterCreateOptions + ) -> PolicySetParameter: + """Create is used to create a new parameter.""" + if not valid_string_id(policy_set_id): + raise InvalidPolicySetIDError() + + if not valid_string(options.key): + raise RequiredKeyError() + + if options.category is None: + raise RequiredCategoryError() + if options.category != CategoryType.POLICY_SET: + raise InvalidCategoryError() + + attributes = options.model_dump(by_alias=True, exclude_none=True) + payload = { + "data": { + "type": "vars", + "attributes": attributes, + } + } + r = self.t.request( + "POST", + path=f"api/v2/policy-sets/{policy_set_id}/parameters", + json_body=payload, + ) + jd = r.json() + data = jd.get("data", {}) + attrs = data.get("attributes", {}) + attrs["id"] = data.get("id") + attrs["policy_set"] = ( + data.get("relationships", {}).get("configurable", {}).get("data", {}) + ) + return PolicySetParameter.model_validate(attrs) + + def read(self, policy_set_id: str, parameter_id: str) -> PolicySetParameter: + """Read a parameter by its ID.""" + if not valid_string_id(policy_set_id): + raise InvalidPolicySetIDError() + + if not valid_string_id(parameter_id): + raise InvalidParamIDError() + + r = self.t.request( + "GET", + path=f"api/v2/policy-sets/{policy_set_id}/parameters/{parameter_id}", + ) + jd = r.json() + data = jd.get("data", {}) + attrs = data.get("attributes", {}) + attrs["id"] = data.get("id") + attrs["policy_set"] = ( + data.get("relationships", {}).get("configurable", {}).get("data", {}) + ) + return PolicySetParameter.model_validate(attrs) + + +""" + def update( + self, + policy_set_id: str, + parameter_id: str, + options: PolicySetParameterUpdateOptions, + ) -> PolicySetParameter: + if not valid_string_id(policy_set_id): + raise InvalidPolicySetIDError() + + if not valid_string_id(parameter_id): + raise InvalidParamIDError() + return PolicySetParameter() + + def delete(self, policy_set_id: str, parameter_id: str) -> None: + if not valid_string_id(policy_set_id): + raise InvalidPolicySetIDError() + + if not valid_string_id(parameter_id): + raise InvalidParamIDError() + return None +""" From 97e32304f6b32b0e656712e7b09d85d2d3e8da76 Mon Sep 17 00:00:00 2001 From: Sivaselvan32 Date: Mon, 24 Nov 2025 17:04:25 +0530 Subject: [PATCH 02/18] feat(policy set parameter): added update and delete methods --- examples/policy_set_parameter.py | 100 ++++++++++++++++++-- src/pytfe/resources/policy_set_parameter.py | 30 +++++- 2 files changed, 117 insertions(+), 13 deletions(-) diff --git a/examples/policy_set_parameter.py b/examples/policy_set_parameter.py index e202cf8..7730220 100644 --- a/examples/policy_set_parameter.py +++ b/examples/policy_set_parameter.py @@ -7,6 +7,7 @@ from pytfe.models import ( PolicySetParameterCreateOptions, PolicySetParameterListOptions, + PolicySetParameterUpdateOptions, ) @@ -29,13 +30,11 @@ def main(): parser.add_argument("--page-size", type=int, default=10) parser.add_argument("--create", action="store_true", help="Create a test parameter") parser.add_argument("--read", action="store_true", help="Read a specific parameter") - parser.add_argument("--parameter-id", help="Parameter ID for read operation") - parser.add_argument( - "--key", default="test_param", help="Parameter key for creation" - ) - parser.add_argument( - "--value", default="test_value", help="Parameter value for creation" - ) + parser.add_argument("--update", action="store_true", help="Update a parameter") + parser.add_argument("--delete", action="store_true", help="Delete a parameter") + parser.add_argument("--parameter-id", help="Parameter ID for read/update/delete operation") + parser.add_argument("--key", help="Parameter key for creation/update") + parser.add_argument("--value", help="Parameter value for creation/update") parser.add_argument( "--sensitive", action="store_true", help="Mark parameter as sensitive" ) @@ -88,13 +87,96 @@ def main(): print(f" Category: {param.category.value}") print(f" Sensitive: {param.sensitive}") - # 3) Create a new parameter (if --create flag is provided) + # 3) Update a parameter (if --update flag is provided) + if args.update: + if not args.parameter_id: + print("Error: --parameter-id is required for update operation") + return + + _print_header(f"Updating parameter: {args.parameter_id}") + + # First read the current parameter to show before state + current_param = client.policy_set_parameters.read( + args.policy_set_id, args.parameter_id + ) + print("Before update:") + print(f" Key: {current_param.key}") + value_display = ( + "***SENSITIVE***" if current_param.sensitive else current_param.value + ) + print(f" Value: {value_display}") + print(f" Sensitive: {current_param.sensitive}") + + # Update the parameter + update_options = PolicySetParameterUpdateOptions( + key=args.key if args.key else None, + value=args.value if args.value else None, + sensitive=args.sensitive if args.sensitive else None, + ) + + updated_param = client.policy_set_parameters.update( + args.policy_set_id, args.parameter_id, update_options + ) + + print("\nAfter update:") + print(f" Key: {updated_param.key}") + value_display = ( + "***SENSITIVE***" if updated_param.sensitive else updated_param.value + ) + print(f" Value: {value_display}") + print(f" Sensitive: {updated_param.sensitive}") + + # 4) Delete a parameter (if --delete flag is provided) + if args.delete: + if not args.parameter_id: + print("Error: --parameter-id is required for delete operation") + return + + _print_header(f"Deleting parameter: {args.parameter_id}") + + # First read the parameter to show what's being deleted + try: + param_to_delete = client.policy_set_parameters.read( + args.policy_set_id, args.parameter_id + ) + print("Parameter to delete:") + print(f" ID: {param_to_delete.id}") + print(f" Key: {param_to_delete.key}") + value_display = ( + "***SENSITIVE***" if param_to_delete.sensitive else param_to_delete.value + ) + print(f" Value: {value_display}") + print(f" Sensitive: {param_to_delete.sensitive}") + except Exception as e: + print(f"Error reading parameter: {e}") + return + + # Delete the parameter + client.policy_set_parameters.delete(args.policy_set_id, args.parameter_id) + print(f"\nโœ“ Successfully deleted parameter: {args.parameter_id}") + + # List remaining parameters + _print_header("Listing parameters after deletion") + remaining_list = client.policy_set_parameters.list(args.policy_set_id) + print(f"Total parameters: {remaining_list.total_count}") + if remaining_list.items: + for param in remaining_list.items: + value_display = "***SENSITIVE***" if param.sensitive else param.value + print(f"- {param.key}: {value_display} (sensitive={param.sensitive})") + else: + print("No parameters remaining.") + + # 5) Create a new parameter (if --create flag is provided) if args.create: + if not args.key: + print("Error: --key is required for create operation") + return + _print_header(f"Creating new parameter with key: {args.key}") create_options = PolicySetParameterCreateOptions( key=args.key, - value=args.value, + value=args.value if args.value else "", sensitive=args.sensitive, ) diff --git a/src/pytfe/resources/policy_set_parameter.py b/src/pytfe/resources/policy_set_parameter.py index 56fa961..ab79afb 100644 --- a/src/pytfe/resources/policy_set_parameter.py +++ b/src/pytfe/resources/policy_set_parameter.py @@ -12,6 +12,7 @@ PolicySetParameterCreateOptions, PolicySetParameterList, PolicySetParameterListOptions, + PolicySetParameterUpdateOptions, ) from ..models.variable import CategoryType from ..utils import valid_string, valid_string_id @@ -113,8 +114,6 @@ def read(self, policy_set_id: str, parameter_id: str) -> PolicySetParameter: ) return PolicySetParameter.model_validate(attrs) - -""" def update( self, policy_set_id: str, @@ -126,7 +125,27 @@ def update( if not valid_string_id(parameter_id): raise InvalidParamIDError() - return PolicySetParameter() + attributes = options.model_dump(by_alias=True, exclude_none=True) + payload = { + "data": { + "type": "vars", + "id": parameter_id, + "attributes": attributes, + } + } + r = self.t.request( + "PATCH", + path=f"api/v2/policy-sets/{policy_set_id}/parameters/{parameter_id}", + json_body=payload, + ) + jd = r.json() + data = jd.get("data", {}) + attrs = data.get("attributes", {}) + attrs["id"] = data.get("id") + attrs["policy_set"] = ( + data.get("relationships", {}).get("configurable", {}).get("data", {}) + ) + return PolicySetParameter.model_validate(attrs) def delete(self, policy_set_id: str, parameter_id: str) -> None: if not valid_string_id(policy_set_id): @@ -134,5 +153,8 @@ def delete(self, policy_set_id: str, parameter_id: str) -> None: if not valid_string_id(parameter_id): raise InvalidParamIDError() + self.t.request( + "DELETE", + path=f"api/v2/policy-sets/{policy_set_id}/parameters/{parameter_id}", + ) return None -""" From 2dfcd5da52542f1183e50f614d7a89c1a3b7e985 Mon Sep 17 00:00:00 2001 From: Sivaselvan32 Date: Mon, 24 Nov 2025 17:06:10 +0530 Subject: [PATCH 03/18] feat(policy set parameter): adding iterator pattern to list method --- examples/policy_set_parameter.py | 69 ++++++++++++--------- src/pytfe/models/policy_set_parameter.py | 2 +- src/pytfe/resources/policy_set_parameter.py | 45 ++++++-------- 3 files changed, 59 insertions(+), 57 deletions(-) diff --git a/examples/policy_set_parameter.py b/examples/policy_set_parameter.py index 7730220..86b5363 100644 --- a/examples/policy_set_parameter.py +++ b/examples/policy_set_parameter.py @@ -26,13 +26,19 @@ def main(): ) parser.add_argument("--token", default=os.getenv("TFE_TOKEN", "")) parser.add_argument("--policy-set-id", required=True, help="Policy Set ID") - parser.add_argument("--page", type=int, default=1) - parser.add_argument("--page-size", type=int, default=10) + parser.add_argument( + "--page-size", + type=int, + default=100, + help="Page size for fetching parameters (iterator fetches all pages)", + ) parser.add_argument("--create", action="store_true", help="Create a test parameter") parser.add_argument("--read", action="store_true", help="Read a specific parameter") parser.add_argument("--update", action="store_true", help="Update a parameter") parser.add_argument("--delete", action="store_true", help="Delete a parameter") - parser.add_argument("--parameter-id", help="Parameter ID for read/update/delete operation") + parser.add_argument( + "--parameter-id", help="Parameter ID for read/update/delete operation" + ) parser.add_argument("--key", help="Parameter key for creation/update") parser.add_argument("--value", help="Parameter value for creation/update") parser.add_argument( @@ -47,28 +53,25 @@ def main(): _print_header(f"Listing parameters for policy set: {args.policy_set_id}") options = PolicySetParameterListOptions( - page_number=args.page, page_size=args.page_size, ) - param_list = client.policy_set_parameters.list(args.policy_set_id, options) - - print(f"Total parameters: {param_list.total_count}") - print(f"Page {param_list.current_page} of {param_list.total_pages}") - print() + param_count = 0 + for param in client.policy_set_parameters.list(args.policy_set_id, options): + param_count += 1 + # Sensitive parameters will have masked values + value_display = "***SENSITIVE***" if param.sensitive else param.value + print(f"- {param.id}") + print(f" Key: {param.key}") + print(f" Value: {value_display}") + print(f" Category: {param.category.value}") + print(f" Sensitive: {param.sensitive}") + print() - if not param_list.items: + if param_count == 0: print("No parameters found.") else: - for param in param_list.items: - # Sensitive parameters will have masked values - value_display = "***SENSITIVE***" if param.sensitive else param.value - print(f"- {param.id}") - print(f" Key: {param.key}") - print(f" Value: {value_display}") - print(f" Category: {param.category.value}") - print(f" Sensitive: {param.sensitive}") - print() + print(f"Total: {param_count} parameters") # 2) Read a specific parameter (if --read flag is provided) if args.read: @@ -143,7 +146,9 @@ def main(): print(f" ID: {param_to_delete.id}") print(f" Key: {param_to_delete.key}") value_display = ( - "***SENSITIVE***" if param_to_delete.sensitive else param_to_delete.value + "***SENSITIVE***" + if param_to_delete.sensitive + else param_to_delete.value ) print(f" Value: {value_display}") print(f" Sensitive: {param_to_delete.sensitive}") @@ -157,14 +162,17 @@ def main(): # List remaining parameters _print_header("Listing parameters after deletion") - remaining_list = client.policy_set_parameters.list(args.policy_set_id) - print(f"Total parameters: {remaining_list.total_count}") - if remaining_list.items: - for param in remaining_list.items: - value_display = "***SENSITIVE***" if param.sensitive else param.value - print(f"- {param.key}: {value_display} (sensitive={param.sensitive})") - else: + print("Remaining parameters:") + remaining_count = 0 + for param in client.policy_set_parameters.list(args.policy_set_id): + remaining_count += 1 + value_display = "***SENSITIVE***" if param.sensitive else param.value + print(f"- {param.key}: {value_display} (sensitive={param.sensitive})") + + if remaining_count == 0: print("No parameters remaining.") + else: + print(f"\nTotal: {remaining_count} parameters") # 5) Create a new parameter (if --create flag is provided) if args.create: @@ -193,11 +201,12 @@ def main(): # List again to show the new parameter _print_header("Listing parameters after creation") - updated_list = client.policy_set_parameters.list(args.policy_set_id) - print(f"Total parameters: {updated_list.total_count}") - for param in updated_list.items: + param_count = 0 + for param in client.policy_set_parameters.list(args.policy_set_id): + param_count += 1 value_display = "***SENSITIVE***" if param.sensitive else param.value print(f"- {param.key}: {value_display} (sensitive={param.sensitive})") + print(f"\nTotal: {param_count} parameters") if __name__ == "__main__": diff --git a/src/pytfe/models/policy_set_parameter.py b/src/pytfe/models/policy_set_parameter.py index 8666a63..366b6df 100644 --- a/src/pytfe/models/policy_set_parameter.py +++ b/src/pytfe/models/policy_set_parameter.py @@ -33,7 +33,7 @@ class PolicySetParameterList(BaseModel): class PolicySetParameterListOptions(BaseModel): model_config = ConfigDict(populate_by_name=True, validate_by_name=True) - page_number: int | None = Field(None, alias="page[number]") + # page_number: int | None = Field(None, alias="page[number]") page_size: int | None = Field(None, alias="page[size]") diff --git a/src/pytfe/resources/policy_set_parameter.py b/src/pytfe/resources/policy_set_parameter.py index ab79afb..1137854 100644 --- a/src/pytfe/resources/policy_set_parameter.py +++ b/src/pytfe/resources/policy_set_parameter.py @@ -1,5 +1,8 @@ from __future__ import annotations +from collections.abc import Iterator +from typing import Any + from ..errors import ( InvalidCategoryError, InvalidParamIDError, @@ -10,7 +13,6 @@ from ..models.policy_set_parameter import ( PolicySetParameter, PolicySetParameterCreateOptions, - PolicySetParameterList, PolicySetParameterListOptions, PolicySetParameterUpdateOptions, ) @@ -19,6 +21,16 @@ from ._base import _Service +def _policy_set_parameter_from(d: dict[str, Any]) -> PolicySetParameter: + """Convert API response dict to PolicySetParameter model.""" + attrs = d.get("attributes", {}) + attrs["id"] = d.get("id") + attrs["policy_set"] = ( + d.get("relationships", {}).get("configurable", {}).get("data", {}) + ) + return PolicySetParameter.model_validate(attrs) + + class PolicySetParameters(_Service): """ PolicySetParameters describes all the parameter related methods that the Terraform Enterprise API supports. @@ -27,35 +39,14 @@ class PolicySetParameters(_Service): def list( self, policy_set_id: str, options: PolicySetParameterListOptions | None = None - ) -> PolicySetParameterList: + ) -> Iterator[PolicySetParameter]: """List all the parameters associated with the given policy-set.""" if not valid_string_id(policy_set_id): raise InvalidPolicySetIDError() params = options.model_dump(by_alias=True, exclude_none=True) if options else {} - r = self.t.request( - "GET", - path=f"api/v2/policy-sets/{policy_set_id}/parameters", - params=params, - ) - jd = r.json() - items = [] - meta = jd.get("meta", {}) - pagination = meta.get("pagination", {}) - for d in jd.get("data", []): - attrs = d.get("attributes", {}) - attrs["id"] = d.get("id") - attrs["policy_set"] = ( - d.get("relationships", {}).get("configurable", {}).get("data", {}) - ) - items.append(PolicySetParameter.model_validate(attrs)) - return PolicySetParameterList( - items=items, - current_page=pagination.get("current-page"), - total_pages=pagination.get("total-pages"), - prev_page=pagination.get("prev-page"), - next_page=pagination.get("next-page"), - total_count=pagination.get("total-count"), - ) + path = f"/api/v2/policy-sets/{policy_set_id}/parameters" + for item in self._list(path, params=params): + yield _policy_set_parameter_from(item) def create( self, policy_set_id: str, options: PolicySetParameterCreateOptions @@ -120,6 +111,7 @@ def update( parameter_id: str, options: PolicySetParameterUpdateOptions, ) -> PolicySetParameter: + """Update values of an existing parameter.""" if not valid_string_id(policy_set_id): raise InvalidPolicySetIDError() @@ -148,6 +140,7 @@ def update( return PolicySetParameter.model_validate(attrs) def delete(self, policy_set_id: str, parameter_id: str) -> None: + """Delete a parameter by its ID.""" if not valid_string_id(policy_set_id): raise InvalidPolicySetIDError() From 9db92e9d1c473bb43b622976cc191955ddd244e0 Mon Sep 17 00:00:00 2001 From: Sivaselvan32 Date: Tue, 25 Nov 2025 14:47:46 +0530 Subject: [PATCH 04/18] test(policy set parameter): added unit tests --- tests/units/test_policy_set_parameter.py | 393 +++++++++++++++++++++++ 1 file changed, 393 insertions(+) create mode 100644 tests/units/test_policy_set_parameter.py diff --git a/tests/units/test_policy_set_parameter.py b/tests/units/test_policy_set_parameter.py new file mode 100644 index 0000000..05c2c4d --- /dev/null +++ b/tests/units/test_policy_set_parameter.py @@ -0,0 +1,393 @@ +"""Unit tests for the policy_set_parameter module.""" + +from unittest.mock import Mock, patch + +import pytest + +from pytfe._http import HTTPTransport +from pytfe.errors import ( + InvalidCategoryError, + InvalidParamIDError, + InvalidPolicySetIDError, + RequiredKeyError, +) +from pytfe.models import ( + CategoryType, + PolicySetParameter, + PolicySetParameterCreateOptions, + PolicySetParameterListOptions, + PolicySetParameterUpdateOptions, +) +from pytfe.resources.policy_set_parameter import PolicySetParameters + + +class TestPolicySetParameters: + """Test the PolicySetParameters service class.""" + + @pytest.fixture + def mock_transport(self): + """Create a mock HTTPTransport.""" + return Mock(spec=HTTPTransport) + + @pytest.fixture + def policy_set_parameters_service(self, mock_transport): + """Create a PolicySetParameters service with mocked transport.""" + return PolicySetParameters(mock_transport) + + def test_list_parameters_validations(self, policy_set_parameters_service): + """Test list method with invalid policy set ID.""" + + # Test empty policy set ID + with pytest.raises(InvalidPolicySetIDError): + list(policy_set_parameters_service.list("")) + + # Test None policy set ID + with pytest.raises(InvalidPolicySetIDError): + list(policy_set_parameters_service.list(None)) + + def test_list_parameters_success_without_options( + self, policy_set_parameters_service + ): + """Test successful list operation without options.""" + + mock_data = [ + { + "id": "var-123", + "attributes": { + "key": "test_param", + "value": "test_value", + "category": "policy-set", + "sensitive": False, + }, + "relationships": { + "configurable": { + "data": {"id": "polset-123", "type": "policy-sets"} + } + }, + } + ] + + with patch.object(policy_set_parameters_service, "_list") as mock_list: + mock_list.return_value = iter(mock_data) + + result = list(policy_set_parameters_service.list("polset-123")) + + mock_list.assert_called_once_with( + "/api/v2/policy-sets/polset-123/parameters", params={} + ) + + assert len(result) == 1 + assert result[0].id == "var-123" + assert result[0].key == "test_param" + assert result[0].value == "test_value" + assert result[0].category == CategoryType.POLICY_SET + assert result[0].sensitive is False + + def test_list_parameters_with_options(self, policy_set_parameters_service): + """Test successful list operation with pagination options.""" + + mock_data = [] + + with patch.object(policy_set_parameters_service, "_list") as mock_list: + mock_list.return_value = iter(mock_data) + + options = PolicySetParameterListOptions(page_size=10) + result = list(policy_set_parameters_service.list("polset-123", options)) + + mock_list.assert_called_once_with( + "/api/v2/policy-sets/polset-123/parameters", + params={"page[size]": 10}, + ) + + assert len(result) == 0 + + def test_list_parameters_returns_iterator(self, policy_set_parameters_service): + """Test that list method returns an iterator.""" + + with patch.object(policy_set_parameters_service, "_list") as mock_list: + mock_list.return_value = iter([]) + + result = policy_set_parameters_service.list("polset-123") + + # Verify it's an iterator + assert hasattr(result, "__iter__") + assert hasattr(result, "__next__") + + def test_create_parameter_validations(self, policy_set_parameters_service): + """Test create method validations.""" + + # Test invalid policy set ID + options = PolicySetParameterCreateOptions(key="test") + with pytest.raises(InvalidPolicySetIDError): + policy_set_parameters_service.create("", options) + + # Test missing key + options = PolicySetParameterCreateOptions(key="") + with pytest.raises(RequiredKeyError): + policy_set_parameters_service.create("polset-123", options) + + # Test invalid category (not policy-set) + options = PolicySetParameterCreateOptions( + key="test", category=CategoryType.TERRAFORM + ) + with pytest.raises(InvalidCategoryError): + policy_set_parameters_service.create("polset-123", options) + + def test_create_parameter_success( + self, policy_set_parameters_service, mock_transport + ): + """Test successful create operation.""" + + mock_response_data = { + "data": { + "id": "var-456", + "attributes": { + "key": "new_param", + "value": "new_value", + "category": "policy-set", + "sensitive": False, + }, + "relationships": { + "configurable": { + "data": {"id": "polset-123", "type": "policy-sets"} + } + }, + } + } + + mock_response = Mock() + mock_response.json.return_value = mock_response_data + mock_transport.request.return_value = mock_response + + options = PolicySetParameterCreateOptions( + key="new_param", value="new_value", sensitive=False + ) + + result = policy_set_parameters_service.create("polset-123", options) + + mock_transport.request.assert_called_once_with( + "POST", + path="api/v2/policy-sets/polset-123/parameters", + json_body={ + "data": { + "type": "vars", + "attributes": { + "key": "new_param", + "value": "new_value", + "category": "policy-set", + "sensitive": False, + }, + } + }, + ) + + assert isinstance(result, PolicySetParameter) + assert result.id == "var-456" + assert result.key == "new_param" + assert result.value == "new_value" + + def test_create_sensitive_parameter( + self, policy_set_parameters_service, mock_transport + ): + """Test creating a sensitive parameter.""" + + mock_response_data = { + "data": { + "id": "var-789", + "attributes": { + "key": "secret_param", + "value": None, + "category": "policy-set", + "sensitive": True, + }, + "relationships": { + "configurable": { + "data": {"id": "polset-123", "type": "policy-sets"} + } + }, + } + } + + mock_response = Mock() + mock_response.json.return_value = mock_response_data + mock_transport.request.return_value = mock_response + + options = PolicySetParameterCreateOptions( + key="secret_param", value="secret_value", sensitive=True + ) + + result = policy_set_parameters_service.create("polset-123", options) + + assert isinstance(result, PolicySetParameter) + assert result.id == "var-789" + assert result.key == "secret_param" + assert result.value is None # Sensitive values are not returned + assert result.sensitive is True + + def test_read_parameter_validations(self, policy_set_parameters_service): + """Test read method validations.""" + + # Test invalid policy set ID + with pytest.raises(InvalidPolicySetIDError): + policy_set_parameters_service.read("", "var-123") + + # Test invalid parameter ID + with pytest.raises(InvalidParamIDError): + policy_set_parameters_service.read("polset-123", "") + + def test_read_parameter_success( + self, policy_set_parameters_service, mock_transport + ): + """Test successful read operation.""" + + mock_response_data = { + "data": { + "id": "var-789", + "attributes": { + "key": "existing_param", + "value": "existing_value", + "category": "policy-set", + "sensitive": False, + }, + "relationships": { + "configurable": { + "data": {"id": "polset-123", "type": "policy-sets"} + } + }, + } + } + + mock_response = Mock() + mock_response.json.return_value = mock_response_data + mock_transport.request.return_value = mock_response + + result = policy_set_parameters_service.read("polset-123", "var-789") + + mock_transport.request.assert_called_once_with( + "GET", path="api/v2/policy-sets/polset-123/parameters/var-789" + ) + + assert isinstance(result, PolicySetParameter) + assert result.id == "var-789" + assert result.key == "existing_param" + assert result.value == "existing_value" + + def test_update_parameter_validations(self, policy_set_parameters_service): + """Test update method validations.""" + + options = PolicySetParameterUpdateOptions(value="updated") + + # Test invalid policy set ID + with pytest.raises(InvalidPolicySetIDError): + policy_set_parameters_service.update("", "var-123", options) + + # Test invalid parameter ID + with pytest.raises(InvalidParamIDError): + policy_set_parameters_service.update("polset-123", "", options) + + def test_update_parameter_success( + self, policy_set_parameters_service, mock_transport + ): + """Test successful update operation.""" + + mock_response_data = { + "data": { + "id": "var-789", + "attributes": { + "key": "updated_param", + "value": "updated_value", + "category": "policy-set", + "sensitive": False, + }, + "relationships": { + "configurable": { + "data": {"id": "polset-123", "type": "policy-sets"} + } + }, + } + } + + mock_response = Mock() + mock_response.json.return_value = mock_response_data + mock_transport.request.return_value = mock_response + + options = PolicySetParameterUpdateOptions( + key="updated_param", value="updated_value" + ) + + result = policy_set_parameters_service.update("polset-123", "var-789", options) + + mock_transport.request.assert_called_once_with( + "PATCH", + path="api/v2/policy-sets/polset-123/parameters/var-789", + json_body={ + "data": { + "type": "vars", + "id": "var-789", + "attributes": {"key": "updated_param", "value": "updated_value"}, + } + }, + ) + + assert isinstance(result, PolicySetParameter) + assert result.id == "var-789" + assert result.key == "updated_param" + assert result.value == "updated_value" + + def test_update_parameter_to_sensitive( + self, policy_set_parameters_service, mock_transport + ): + """Test updating a parameter to make it sensitive.""" + + mock_response_data = { + "data": { + "id": "var-789", + "attributes": { + "key": "param", + "value": None, + "category": "policy-set", + "sensitive": True, + }, + "relationships": { + "configurable": { + "data": {"id": "polset-123", "type": "policy-sets"} + } + }, + } + } + + mock_response = Mock() + mock_response.json.return_value = mock_response_data + mock_transport.request.return_value = mock_response + + options = PolicySetParameterUpdateOptions(sensitive=True) + + result = policy_set_parameters_service.update("polset-123", "var-789", options) + + assert isinstance(result, PolicySetParameter) + assert result.sensitive is True + assert result.value is None + + def test_delete_parameter_validations(self, policy_set_parameters_service): + """Test delete method validations.""" + + # Test invalid policy set ID + with pytest.raises(InvalidPolicySetIDError): + policy_set_parameters_service.delete("", "var-123") + + # Test invalid parameter ID + with pytest.raises(InvalidParamIDError): + policy_set_parameters_service.delete("polset-123", "") + + def test_delete_parameter_success( + self, policy_set_parameters_service, mock_transport + ): + """Test successful delete operation.""" + + result = policy_set_parameters_service.delete("polset-123", "var-789") + + mock_transport.request.assert_called_once_with( + "DELETE", path="api/v2/policy-sets/polset-123/parameters/var-789" + ) + + assert result is None From e7a60334a3abf4325846969abea7d6195488f76c Mon Sep 17 00:00:00 2001 From: Sivaselvan32 Date: Wed, 26 Nov 2025 13:17:23 +0530 Subject: [PATCH 05/18] feat(policy set parameter): added a helper function --- examples/policy_set_parameter.py | 2 +- src/pytfe/models/__init__.py | 2 - src/pytfe/models/policy_set_parameter.py | 12 ----- src/pytfe/resources/policy_set_parameter.py | 51 +++++++-------------- 4 files changed, 17 insertions(+), 50 deletions(-) diff --git a/examples/policy_set_parameter.py b/examples/policy_set_parameter.py index 86b5363..c2ffea3 100644 --- a/examples/policy_set_parameter.py +++ b/examples/policy_set_parameter.py @@ -30,7 +30,7 @@ def main(): "--page-size", type=int, default=100, - help="Page size for fetching parameters (iterator fetches all pages)", + help="Page size for fetching parameters", ) parser.add_argument("--create", action="store_true", help="Create a test parameter") parser.add_argument("--read", action="store_true", help="Read a specific parameter") diff --git a/src/pytfe/models/__init__.py b/src/pytfe/models/__init__.py index b19c446..c40737e 100644 --- a/src/pytfe/models/__init__.py +++ b/src/pytfe/models/__init__.py @@ -136,7 +136,6 @@ from .policy_set_parameter import ( PolicySetParameter, PolicySetParameterCreateOptions, - PolicySetParameterList, PolicySetParameterListOptions, PolicySetParameterUpdateOptions, ) @@ -596,7 +595,6 @@ # Policy Set Parameters "PolicySetParameter", "PolicySetParameterCreateOptions", - "PolicySetParameterList", "PolicySetParameterListOptions", "PolicySetParameterUpdateOptions", "PolicyKind", diff --git a/src/pytfe/models/policy_set_parameter.py b/src/pytfe/models/policy_set_parameter.py index 366b6df..01a88c2 100644 --- a/src/pytfe/models/policy_set_parameter.py +++ b/src/pytfe/models/policy_set_parameter.py @@ -19,21 +19,9 @@ class PolicySetParameter(BaseModel): policy_set: PolicySet = Field(..., alias="configurable") -class PolicySetParameterList(BaseModel): - model_config = ConfigDict(populate_by_name=True, validate_by_name=True) - - items: list[PolicySetParameter] = Field(default_factory=list) - current_page: int | None = None - total_pages: int | None = None - prev_page: int | None = None - next_page: int | None = None - total_count: int | None = None - - class PolicySetParameterListOptions(BaseModel): model_config = ConfigDict(populate_by_name=True, validate_by_name=True) - # page_number: int | None = Field(None, alias="page[number]") page_size: int | None = Field(None, alias="page[size]") diff --git a/src/pytfe/resources/policy_set_parameter.py b/src/pytfe/resources/policy_set_parameter.py index 1137854..076579c 100644 --- a/src/pytfe/resources/policy_set_parameter.py +++ b/src/pytfe/resources/policy_set_parameter.py @@ -21,16 +21,6 @@ from ._base import _Service -def _policy_set_parameter_from(d: dict[str, Any]) -> PolicySetParameter: - """Convert API response dict to PolicySetParameter model.""" - attrs = d.get("attributes", {}) - attrs["id"] = d.get("id") - attrs["policy_set"] = ( - d.get("relationships", {}).get("configurable", {}).get("data", {}) - ) - return PolicySetParameter.model_validate(attrs) - - class PolicySetParameters(_Service): """ PolicySetParameters describes all the parameter related methods that the Terraform Enterprise API supports. @@ -46,7 +36,7 @@ def list( params = options.model_dump(by_alias=True, exclude_none=True) if options else {} path = f"/api/v2/policy-sets/{policy_set_id}/parameters" for item in self._list(path, params=params): - yield _policy_set_parameter_from(item) + yield self._policy_set_parameter_from(item) def create( self, policy_set_id: str, options: PolicySetParameterCreateOptions @@ -75,14 +65,8 @@ def create( path=f"api/v2/policy-sets/{policy_set_id}/parameters", json_body=payload, ) - jd = r.json() - data = jd.get("data", {}) - attrs = data.get("attributes", {}) - attrs["id"] = data.get("id") - attrs["policy_set"] = ( - data.get("relationships", {}).get("configurable", {}).get("data", {}) - ) - return PolicySetParameter.model_validate(attrs) + data = r.json().get("data", {}) + return self._policy_set_parameter_from(data) def read(self, policy_set_id: str, parameter_id: str) -> PolicySetParameter: """Read a parameter by its ID.""" @@ -96,14 +80,8 @@ def read(self, policy_set_id: str, parameter_id: str) -> PolicySetParameter: "GET", path=f"api/v2/policy-sets/{policy_set_id}/parameters/{parameter_id}", ) - jd = r.json() - data = jd.get("data", {}) - attrs = data.get("attributes", {}) - attrs["id"] = data.get("id") - attrs["policy_set"] = ( - data.get("relationships", {}).get("configurable", {}).get("data", {}) - ) - return PolicySetParameter.model_validate(attrs) + data = r.json().get("data", {}) + return self._policy_set_parameter_from(data) def update( self, @@ -130,14 +108,8 @@ def update( path=f"api/v2/policy-sets/{policy_set_id}/parameters/{parameter_id}", json_body=payload, ) - jd = r.json() - data = jd.get("data", {}) - attrs = data.get("attributes", {}) - attrs["id"] = data.get("id") - attrs["policy_set"] = ( - data.get("relationships", {}).get("configurable", {}).get("data", {}) - ) - return PolicySetParameter.model_validate(attrs) + data = r.json().get("data", {}) + return self._policy_set_parameter_from(data) def delete(self, policy_set_id: str, parameter_id: str) -> None: """Delete a parameter by its ID.""" @@ -151,3 +123,12 @@ def delete(self, policy_set_id: str, parameter_id: str) -> None: path=f"api/v2/policy-sets/{policy_set_id}/parameters/{parameter_id}", ) return None + + def _policy_set_parameter_from(self, d: dict[str, Any]) -> PolicySetParameter: + """Convert API response dict to PolicySetParameter model.""" + attrs = d.get("attributes", {}) + attrs["id"] = d.get("id") + attrs["policy_set"] = ( + d.get("relationships", {}).get("configurable", {}).get("data", {}) + ) + return PolicySetParameter.model_validate(attrs) From 8eda8c2c12db0b94ec8d47bdf29026d80282b832 Mon Sep 17 00:00:00 2001 From: aayushsingh2502 Date: Fri, 28 Nov 2025 00:15:36 +0530 Subject: [PATCH 06/18] Added all api functionality for organization membership and done some code cleanup --- examples/agent.py | 32 +- examples/agent_pool.py | 36 +- examples/configuration_version.py | 196 ++-- examples/notification_configuration.py | 30 +- examples/oauth_client.py | 94 +- examples/oauth_token.py | 48 +- examples/org.py | 104 +-- examples/organization_membership.py | 320 +++++++ examples/project.py | 240 +++-- examples/registry_module.py | 138 +-- examples/registry_provider.py | 50 +- examples/reserved_tag_key.py | 32 +- examples/run_task.py | 20 +- examples/run_trigger.py | 20 +- examples/ssh_keys.py | 62 +- examples/variables.py | 54 +- examples/workspace.py | 66 +- src/pytfe/client.py | 2 + src/pytfe/errors.py | 8 + src/pytfe/models/__init__.py | 22 + src/pytfe/models/organization_membership.py | 97 ++ src/pytfe/models/team.py | 72 ++ src/pytfe/models/user.py | 22 +- .../resources/organization_membership.py | 284 ++++++ src/pytfe/resources/policy_check.py | 2 +- src/pytfe/resources/projects.py | 18 +- tests/units/test_organization_membership.py | 835 ++++++++++++++++++ 27 files changed, 2264 insertions(+), 640 deletions(-) create mode 100644 examples/organization_membership.py create mode 100644 src/pytfe/models/organization_membership.py create mode 100644 src/pytfe/models/team.py create mode 100644 src/pytfe/resources/organization_membership.py create mode 100644 tests/units/test_organization_membership.py diff --git a/examples/agent.py b/examples/agent.py index d756abf..5b0990c 100644 --- a/examples/agent.py +++ b/examples/agent.py @@ -36,29 +36,29 @@ def main(): address = os.environ.get("TFE_ADDRESS", "https://app.terraform.io") if not token: - print("โŒ TFE_TOKEN environment variable is required") + print(" TFE_TOKEN environment variable is required") return 1 if not org: - print("โŒ TFE_ORG environment variable is required") + print(" TFE_ORG environment variable is required") return 1 # Create TFE client config = TFEConfig(token=token, address=address) client = TFEClient(config) - print(f"๐Ÿ”— Connected to: {address}") - print(f"๐Ÿข Organization: {org}") + print(f"Connected to: {address}") + print(f" Organization: {org}") try: # Example 1: Find agent pools to demonstrate agent operations - print("\n๐Ÿ“‹ Finding agent pools...") + print("\n Finding agent pools...") agent_pools = client.agent_pools.list(org) # Convert to list to check if empty and get count pool_list = list(agent_pools) if not pool_list: - print("โš ๏ธ No agent pools found. Create an agent pool first.") + print(" No agent pools found. Create an agent pool first.") return 1 print(f"Found {len(pool_list)} agent pools:") @@ -66,11 +66,11 @@ def main(): print(f" - {pool.name} (ID: {pool.id}, Agents: {pool.agent_count})") # Example 2: List agents in each pool - print("\n๐Ÿค– Listing agents in each pool...") + print("\n Listing agents in each pool...") total_agents = 0 for pool in pool_list: - print(f"\n๐Ÿ“‚ Agents in pool '{pool.name}':") + print(f"\n Agents in pool '{pool.name}':") # Use optional parameters for listing list_options = AgentListOptions(page_size=10) # Optional parameter @@ -91,35 +91,35 @@ def main(): # Example 3: Read detailed agent information try: agent_details = client.agents.read(agent.id) - print(" โœ… Agent details retrieved successfully") + print(" Agent details retrieved successfully") print(f" Full name: {agent_details.name or 'Unnamed'}") print(f" Current status: {agent_details.status}") except NotFound: - print(" โš ๏ธ Agent details not accessible") + print(" Agent details not accessible") except Exception as e: - print(f" โŒ Error reading agent details: {e}") + print(f" Error reading agent details: {e}") print("") else: print(" No agents found in this pool") if total_agents == 0: - print("\nโš ๏ธ No agents found in any pools.") + print("\n No agents found in any pools.") print("To see agents in action:") print("1. Create an agent pool") print("2. Run a Terraform Enterprise agent binary connected to the pool") print("3. Run this example again") else: - print(f"\n๐Ÿ“Š Total agents found across all pools: {total_agents}") + print(f"\n Total agents found across all pools: {total_agents}") - print("\n๐ŸŽ‰ Agent operations completed successfully!") + print("\n Agent operations completed successfully!") return 0 except NotFound as e: - print(f"โŒ Resource not found: {e}") + print(f" Resource not found: {e}") return 1 except Exception as e: - print(f"โŒ Error: {e}") + print(f" Error: {e}") return 1 diff --git a/examples/agent_pool.py b/examples/agent_pool.py index 1b6e15b..acc9750 100644 --- a/examples/agent_pool.py +++ b/examples/agent_pool.py @@ -39,23 +39,23 @@ def main(): address = os.environ.get("TFE_ADDRESS", "https://app.terraform.io") if not token: - print("โŒ TFE_TOKEN environment variable is required") + print(" TFE_TOKEN environment variable is required") return 1 if not org: - print("โŒ TFE_ORG environment variable is required") + print(" TFE_ORG environment variable is required") return 1 # Create TFE client config = TFEConfig(token=token, address=address) client = TFEClient(config=config) - print(f"๐Ÿ”— Connected to: {address}") - print(f"๐Ÿข Organization: {org}") + print(f"Connected to: {address}") + print(f" Organization: {org}") try: # Example 1: List existing agent pools - print("\n๐Ÿ“‹ Listing existing agent pools...") + print("\n Listing existing agent pools...") list_options = AgentPoolListOptions(page_size=10) # Optional parameters agent_pools = client.agent_pools.list(org, options=list_options) @@ -66,7 +66,7 @@ def main(): print(f" - {pool.name} (ID: {pool.id}, Agents: {pool.agent_count})") # Example 2: Create a new agent pool - print("\n๐Ÿ†• Creating a new agent pool...") + print("\n Creating a new agent pool...") unique_name = f"sdk-example-pool-{uuid.uuid4().hex[:8]}" create_options = AgentPoolCreateOptions( @@ -76,10 +76,10 @@ def main(): ) new_pool = client.agent_pools.create(org, create_options) - print(f"โœ… Created agent pool: {new_pool.name} (ID: {new_pool.id})") + print(f"Created agent pool: {new_pool.name} (ID: {new_pool.id})") # Example 3: Read the agent pool - print("\n๐Ÿ“– Reading agent pool details...") + print("\n Reading agent pool details...") pool_details = client.agent_pools.read(new_pool.id) print(f" Name: {pool_details.name}") print(f" Organization Scoped: {pool_details.organization_scoped}") @@ -87,28 +87,28 @@ def main(): print(f" Agent Count: {pool_details.agent_count}") # Example 4: Update the agent pool - print("\nโœ๏ธ Updating agent pool...") + print("\n Updating agent pool...") update_options = AgentPoolUpdateOptions( name=f"{unique_name}-updated", organization_scoped=False, # Making this optional parameter different ) updated_pool = client.agent_pools.update(new_pool.id, update_options) - print(f"โœ… Updated agent pool name to: {updated_pool.name}") + print(f"Updated agent pool name to: {updated_pool.name}") # Example 5: Create an agent token - print("\n๐Ÿ”‘ Creating agent token...") + print("\n Creating agent token...") token_options = AgentTokenCreateOptions( description="SDK example token" # Optional description ) agent_token = client.agent_tokens.create(new_pool.id, token_options) - print(f"โœ… Created agent token: {agent_token.id}") + print(f"Created agent token: {agent_token.id}") if agent_token.token: print(f" Token (first 10 chars): {agent_token.token[:10]}...") # Example 6: List agent tokens - print("\n๐Ÿ“ Listing agent tokens...") + print("\n Listing agent tokens...") tokens = client.agent_tokens.list(new_pool.id) # Convert to list to get count and iterate @@ -120,19 +120,19 @@ def main(): # Example 7: Clean up - delete the token and pool print("\n๐Ÿงน Cleaning up...") client.agent_tokens.delete(agent_token.id) - print("โœ… Deleted agent token") + print("Deleted agent token") client.agent_pools.delete(new_pool.id) - print("โœ… Deleted agent pool") + print("Deleted agent pool") - print("\n๐ŸŽ‰ Agent pool operations completed successfully!") + print("\n Agent pool operations completed successfully!") return 0 except NotFound as e: - print(f"โŒ Resource not found: {e}") + print(f" Resource not found: {e}") return 1 except Exception as e: - print(f"โŒ Error: {e}") + print(f" Error: {e}") return 1 diff --git a/examples/configuration_version.py b/examples/configuration_version.py index e983e94..92840b4 100644 --- a/examples/configuration_version.py +++ b/examples/configuration_version.py @@ -191,7 +191,7 @@ def main(): try: # Basic list without options cv_list = list(client.configuration_versions.list(workspace_id)) - print(f" โœ“ Found {len(cv_list)} configuration versions") + print(f" Found {len(cv_list)} configuration versions") if cv_list: print(" Recent configuration versions:") @@ -228,18 +228,18 @@ def main(): if count >= 10: # Limit to prevent infinite loop break - print(f" โœ“ Found {len(cv_list_opts)} configuration versions with options") + print(f" Found {len(cv_list_opts)} configuration versions with options") print( f" Include options: {[opt.value for opt in list_options.include]}" ) except Exception as opts_error: - print(f" โš  Error with options: {opts_error}") + print(f" Error with options: {opts_error}") print(" This may be expected if the API doesn't support these options") print(" Basic list functionality still works") except Exception as e: - print(f" โœ— Error: {e}") + print(f" Error: {e}") import traceback traceback.print_exc() @@ -258,7 +258,7 @@ def main(): new_cv = client.configuration_versions.create(workspace_id, create_options) created_cv_id = new_cv.id - print(f" โœ“ Created NON-SPECULATIVE CV: {created_cv_id}") + print(f" Created NON-SPECULATIVE CV: {created_cv_id}") print(f" Status: {new_cv.status}") print(f" Speculative: {new_cv.speculative} (will show in runs)") print(f" Auto-queue runs: {new_cv.auto_queue_runs} (will create run)") @@ -266,7 +266,7 @@ def main(): # UPLOAD REAL TERRAFORM CODE IMMEDIATELY if new_cv.upload_url: - print("\n โ†’ Uploading real Terraform configuration...") + print("\n Uploading real Terraform configuration...") with tempfile.TemporaryDirectory() as temp_dir: print(f" Creating Terraform files in: {temp_dir}") @@ -282,7 +282,7 @@ def main(): try: # Create tar.gz archive manually since go-slug isn't available - print(" โ†’ Creating tar.gz archive manually...") + print(" Creating tar.gz archive manually...") import tarfile @@ -296,40 +296,38 @@ def main(): archive_buffer.seek(0) archive_bytes = archive_buffer.getvalue() - print(f" โ†’ Created archive: {len(archive_bytes)} bytes") + print(f" Created archive: {len(archive_bytes)} bytes") # Use the SDK's upload_tar_gzip method instead of direct HTTP calls - print(" โ†’ Uploading archive using SDK method...") + print(" Uploading archive using SDK method...") archive_buffer.seek(0) # Reset buffer position client.configuration_versions.upload_tar_gzip( new_cv.upload_url, archive_buffer ) - print(" โœ“ Terraform configuration uploaded successfully!") + print(" Terraform configuration uploaded successfully!") # Wait and check status - print("\n โ†’ Checking status after upload...") + print("\n Checking status after upload...") time.sleep(5) # Give TFE time to process updated_cv = client.configuration_versions.read(created_cv_id) print(f" Status after upload: {updated_cv.status}") if updated_cv.status.value in ["uploaded", "fetching"]: + print(" REAL configuration version created successfully!") + print(" This CV now contains actual Terraform code") print( - " โœ… REAL configuration version created successfully!" - ) - print(" โ†’ This CV now contains actual Terraform code") - print( - " โ†’ You can now see this CV in your Terraform Cloud workspace!" + " You can now see this CV in your Terraform Cloud workspace!" ) else: - print(f" โš  Status is still: {updated_cv.status.value}") + print(f" Status is still: {updated_cv.status.value}") print(" (Upload may still be processing)") except Exception as e: - print(f" โš  Upload failed: {type(e).__name__}: {e}") - print(" โ†’ CV created but no configuration uploaded") + print(f" Upload failed: {type(e).__name__}: {e}") + print(" CV created but no configuration uploaded") else: - print(" โš  No upload URL - cannot upload Terraform code") + print(" No upload URL - cannot upload Terraform code") # Test 2b: Create standard configuration version for upload testing print("\n 2b. Creating standard configuration version for upload tests:") @@ -341,7 +339,7 @@ def main(): workspace_id, standard_options ) uploadable_cv_id = standard_cv.id # Save for summary display - print(f" โœ“ Created standard CV: {standard_cv.id}") + print(f" Created standard CV: {standard_cv.id}") print(f" Status: {standard_cv.status}") print(f" Speculative: {standard_cv.speculative}") print(f" Auto-queue runs: {standard_cv.auto_queue_runs}") @@ -353,12 +351,12 @@ def main(): ) auto_cv = client.configuration_versions.create(workspace_id, auto_options) - print(f" โœ“ Created auto-queue CV: {auto_cv.id}") + print(f" Created auto-queue CV: {auto_cv.id}") print(f" Auto-queue runs: {auto_cv.auto_queue_runs}") - print(" โš  This will trigger a Terraform run when code is uploaded") + print(" This will trigger a Terraform run when code is uploaded") except Exception as e: - print(f" โœ— Error: {e}") + print(f" Error: {e}") import traceback traceback.print_exc() @@ -371,7 +369,7 @@ def main(): try: cv_details = client.configuration_versions.read(created_cv_id) - print(f" โœ“ Read configuration version: {cv_details.id}") + print(f" Read configuration version: {cv_details.id}") print(f" Status: {cv_details.status}") print(f" Source: {cv_details.source}") if cv_details.status_timestamps: @@ -405,12 +403,12 @@ def main(): for field in required_fields: if hasattr(cv_details, field): value = getattr(cv_details, field) - print(f" โœ“ {field}: {type(value).__name__}") + print(f" {field}: {type(value).__name__}") else: - print(f" โœ— {field}: Missing") + print(f" {field}: Missing") except Exception as e: - print(f" โœ— Error: {e}") + print(f" Error: {e}") import traceback traceback.print_exc() @@ -433,7 +431,7 @@ def main(): upload_url = fresh_cv.upload_url if not upload_url: - print(" โš  No upload URL available for this configuration version") + print(" No upload URL available for this configuration version") print(" Configuration version may not be in uploadable state") else: with tempfile.TemporaryDirectory() as temp_dir: @@ -453,7 +451,7 @@ def main(): try: client.configuration_versions.upload(upload_url, temp_dir) - print(" โœ“ Configuration uploaded successfully!") + print(" Configuration uploaded successfully!") # Check status after upload print("\n Checking status after upload:") @@ -462,25 +460,25 @@ def main(): print(f" Status after upload: {updated_cv.status}") if updated_cv.status.value != "pending": - print(" โœ“ Status changed (upload processed)") + print(" Status changed (upload processed)") else: - print(" โš  Status still pending (may need more time)") + print(" Status still pending (may need more time)") except ImportError as e: if "go-slug" in str(e): - print(" โš  go-slug package not available") + print(" go-slug package not available") print(" Install with: pip install go-slug") print( " Upload function exists but requires go-slug for packaging" ) print( - " โœ“ Function correctly raises ImportError when go-slug unavailable" + " Function correctly raises ImportError when go-slug unavailable" ) else: raise except Exception as e: - print(f" โœ— Error: {e}") + print(f" Error: {e}") import traceback traceback.print_exc() @@ -510,7 +508,7 @@ def main(): downloadable_cvs.append(cv) if not downloadable_cvs: - print(" โš  No uploaded configuration versions found to download") + print(" No uploaded configuration versions found to download") print(" This is not a test failure - upload a configuration first") else: downloadable_cv = downloadable_cvs[0] @@ -518,20 +516,20 @@ def main(): print(f" Status: {downloadable_cv.status}") archive_data = client.configuration_versions.download(downloadable_cv.id) - print(f" โœ“ Downloaded {len(archive_data)} bytes") + print(f" Downloaded {len(archive_data)} bytes") # Validate downloaded data print("\n Validating downloaded data:") if len(archive_data) > 0: - print(" โœ“ Archive data is non-empty") + print(" Archive data is non-empty") # Basic format check if archive_data[:2] == b"\x1f\x8b": - print(" โœ“ Data appears to be gzip format") + print(" Data appears to be gzip format") else: - print(" โš  Data may not be gzip format (could still be valid)") + print(" Data may not be gzip format (could still be valid)") else: - print(" โœ— Archive data is empty") + print(" Archive data is empty") # Test multiple downloads if available if len(downloadable_cvs) > 1: @@ -539,12 +537,12 @@ def main(): for i, cv in enumerate(downloadable_cvs[1:3], 2): try: data = client.configuration_versions.download(cv.id) - print(f" โœ“ CV {i}: {cv.id} - {len(data)} bytes") + print(f" CV {i}: {cv.id} - {len(data)} bytes") except Exception as e: - print(f" โš  CV {i}: {cv.id} - Failed: {type(e).__name__}") + print(f" CV {i}: {cv.id} - Failed: {type(e).__name__}") except Exception as e: - print(f" โœ— Error: {e}") + print(f" Error: {e}") import traceback traceback.print_exc() @@ -568,7 +566,7 @@ def main(): if len(cv_list) < 2: print( - " โš  Need at least 2 configuration versions to test archive functionality" + " Need at least 2 configuration versions to test archive functionality" ) print( " This is not a test failure - create more configuration versions first" @@ -604,7 +602,7 @@ def main(): try: client.configuration_versions.archive(cv_to_archive.id) - print(" โœ“ Archive request sent successfully") + print(" Archive request sent successfully") # Check status after archive request print("\n Checking status after archive request:") @@ -615,30 +613,30 @@ def main(): ) print(f" Status after archive: {updated_cv.status}") if updated_cv.status.value == "archived": - print(" โœ“ Successfully archived") + print(" Successfully archived") else: - print(" โš  Still processing (archive may take time)") + print(" Still processing (archive may take time)") except Exception: print( - " โš  Could not read status after archive (may be expected)" + " Could not read status after archive (may be expected)" ) except Exception as e: if "404" in str(e) or "not found" in str(e).lower(): - print(" โš  CV may have been auto-archived or removed") + print(" CV may have been auto-archived or removed") elif "current" in str(e).lower(): - print(" โš  Cannot archive current configuration version") + print(" Cannot archive current configuration version") print( - " โœ“ Function correctly handles 'current' CV restriction" + " Function correctly handles 'current' CV restriction" ) else: - print(f" โš  Archive failed: {type(e).__name__}: {e}") + print(f" Archive failed: {type(e).__name__}: {e}") else: - print("\n โš  No suitable configuration versions found for archiving") + print("\n No suitable configuration versions found for archiving") print( " Need at least 2 uploaded CVs (to avoid archiving current one)" ) - print(" โœ“ Function correctly validates archivable CVs") + print(" Function correctly validates archivable CVs") # Test archiving already archived CV if already_archived: @@ -648,12 +646,12 @@ def main(): try: client.configuration_versions.archive(already_archived_cv.id) - print(" โœ“ Handled gracefully (no-op for already archived)") + print(" Handled gracefully (no-op for already archived)") except Exception as e: - print(f" โœ“ Correctly rejected: {type(e).__name__}") + print(f" Correctly rejected: {type(e).__name__}") except Exception as e: - print(f" โœ— Error: {e}") + print(f" Error: {e}") import traceback traceback.print_exc() @@ -673,7 +671,7 @@ def main(): created_cv_id, read_options ) - print(f" โœ“ Read configuration version with options: {cv_with_options.id}") + print(f" Read configuration version with options: {cv_with_options.id}") print(f" Status: {cv_with_options.status}") print(f" Source: {cv_with_options.source}") @@ -681,7 +679,7 @@ def main(): hasattr(cv_with_options, "ingress_attributes") and cv_with_options.ingress_attributes ): - print(" โœ“ Ingress attributes included in response") + print(" Ingress attributes included in response") if hasattr(cv_with_options.ingress_attributes, "branch"): print(f" Branch: {cv_with_options.ingress_attributes.branch}") if hasattr(cv_with_options.ingress_attributes, "clone_url"): @@ -689,17 +687,17 @@ def main(): f" Clone URL: {cv_with_options.ingress_attributes.clone_url}" ) else: - print(" โš  No ingress attributes (expected for API-created CVs)") + print(" No ingress attributes (expected for API-created CVs)") print(" Ingress attributes are only present for VCS-connected CVs") except Exception as e: - print(f" โœ— Error: {e}") + print(f" Error: {e}") import traceback traceback.print_exc() else: print("\n7. Testing read_with_options() function:") - print(" โš  Skipped - no configuration version created for testing") + print(" Skipped - no configuration version created for testing") # ===================================================== # TEST 8: CREATE FOR REGISTRY MODULE (BETA) @@ -723,30 +721,30 @@ def main(): registry_cv = client.configuration_versions.create_for_registry_module( module_id ) - print(f" โœ“ Created registry module CV: {registry_cv.id}") + print(f" Created registry module CV: {registry_cv.id}") print(f" Status: {registry_cv.status}") print(f" Source: {registry_cv.source}") except Exception as e: if "404" in str(e) or "not found" in str(e).lower(): print( - " โš  Registry module not found (expected - requires actual module)" + " Registry module not found (expected - requires actual module)" ) print(" Function exists and properly handles missing modules") elif "403" in str(e) or "forbidden" in str(e).lower(): - print(" โš  No permission to access registry modules (expected)") + print(" No permission to access registry modules (expected)") print(" Function exists and properly handles permission errors") elif "AttributeError" in str(e): - print(f" โš  Function parameter error: {e}") + print(f" Function parameter error: {e}") print(" Function exists but may need parameter adjustment") else: print( - f" โš  Registry module CV creation failed: {type(e).__name__}: {e}" + f" Registry module CV creation failed: {type(e).__name__}: {e}" ) print(" This may be expected if no registry modules exist") except Exception as e: - print(f" โœ— Error: {e}") + print(f" Error: {e}") import traceback traceback.print_exc() @@ -795,7 +793,7 @@ def main(): client.configuration_versions.upload_tar_gzip( upload_url, archive_buffer ) - print(" โœ“ Direct tar.gz upload successful!") + print(" Direct tar.gz upload successful!") # Check status after upload time.sleep(2) @@ -805,13 +803,13 @@ def main(): print(f" Status after upload: {updated_upload_cv.status}") except Exception as e: - print(f" โš  Upload failed: {type(e).__name__}: {e}") + print(f" Upload failed: {type(e).__name__}: {e}") print(" This may be expected depending on TFE configuration") else: - print(" โš  No upload URL available - cannot test upload_tar_gzip") + print(" No upload URL available - cannot test upload_tar_gzip") except Exception as e: - print(f" โœ— Error: {e}") + print(f" Error: {e}") import traceback traceback.print_exc() @@ -831,29 +829,29 @@ def main(): print("\n 10a. Testing soft_delete_backing_data():") try: client.configuration_versions.soft_delete_backing_data(created_cv_id) - print(" โœ“ Soft delete backing data request sent successfully") + print(" Soft delete backing data request sent successfully") except Exception as e: if "404" in str(e) or "not found" in str(e).lower(): - print(" โš  CV not found for backing data operation") + print(" CV not found for backing data operation") elif "403" in str(e) or "forbidden" in str(e).lower(): - print(" โš  Enterprise feature - not available (expected)") + print(" Enterprise feature - not available (expected)") else: - print(f" โš  Soft delete failed: {type(e).__name__}: {e}") - print(" โœ“ Function exists and properly handles Enterprise restrictions") + print(f" Soft delete failed: {type(e).__name__}: {e}") + print(" Function exists and properly handles Enterprise restrictions") # Test restore backing data print("\n 10b. Testing restore_backing_data():") try: client.configuration_versions.restore_backing_data(created_cv_id) - print(" โœ“ Restore backing data request sent successfully") + print(" Restore backing data request sent successfully") except Exception as e: if "404" in str(e) or "not found" in str(e).lower(): - print(" โš  CV not found for backing data operation") + print(" CV not found for backing data operation") elif "403" in str(e) or "forbidden" in str(e).lower(): - print(" โš  Enterprise feature - not available (expected)") + print(" Enterprise feature - not available (expected)") else: - print(f" โš  Restore failed: {type(e).__name__}: {e}") - print(" โœ“ Function exists and properly handles Enterprise restrictions") + print(f" Restore failed: {type(e).__name__}: {e}") + print(" Function exists and properly handles Enterprise restrictions") # Test permanently delete backing data print("\n 10c. Testing permanently_delete_backing_data():") @@ -871,15 +869,15 @@ def main(): client.configuration_versions.permanently_delete_backing_data( perm_delete_cv_id ) - print(" โœ“ Permanent delete backing data request sent successfully") + print(" Permanent delete backing data request sent successfully") except Exception as e: if "404" in str(e) or "not found" in str(e).lower(): - print(" โš  CV not found for backing data operation") + print(" CV not found for backing data operation") elif "403" in str(e) or "forbidden" in str(e).lower(): - print(" โš  Enterprise feature - not available (expected)") + print(" Enterprise feature - not available (expected)") else: - print(f" โš  Permanent delete failed: {type(e).__name__}: {e}") - print(" โœ“ Function exists and properly handles Enterprise restrictions") + print(f" Permanent delete failed: {type(e).__name__}: {e}") + print(" Function exists and properly handles Enterprise restrictions") # ===================================================== # TEST SUMMARY @@ -887,20 +885,18 @@ def main(): print("\n" + "=" * 80) print("CONFIGURATION VERSION COMPLETE TESTING SUMMARY") print("=" * 80) - print("โœ… TEST 1: list() - List configuration versions for workspace") - print( - "โœ… TEST 2: create() - Create new configuration versions with different options" - ) - print("โœ… TEST 3: read() - Read configuration version details and validate fields") - print("โœ… TEST 4: upload() - Upload Terraform configurations (requires go-slug)") - print("โœ… TEST 5: download() - Download configuration version archives") - print("โœ… TEST 6: archive() - Archive configuration versions") - print("โœ… TEST 7: read_with_options() - Read with include options") - print("โœ… TEST 8: create_for_registry_module() - Registry module CVs (BETA)") - print("โœ… TEST 9: upload_tar_gzip() - Direct tar.gz archive upload") + print("TEST 1: list() - List configuration versions for workspace") print( - "โœ… TEST 10: Enterprise backing data operations (soft/restore/permanent delete)" + "TEST 2: create() - Create new configuration versions with different options" ) + print("TEST 3: read() - Read configuration version details and validate fields") + print("TEST 4: upload() - Upload Terraform configurations (requires go-slug)") + print("TEST 5: download() - Download configuration version archives") + print("TEST 6: archive() - Archive configuration versions") + print("TEST 7: read_with_options() - Read with include options") + print("TEST 8: create_for_registry_module() - Registry module CVs (BETA)") + print("TEST 9: upload_tar_gzip() - Direct tar.gz archive upload") + print("TEST 10: Enterprise backing data operations (soft/restore/permanent delete)") print("=" * 80) print("ALL 12 configuration version functions have been tested!") print("Review the output above for any errors or warnings.") diff --git a/examples/notification_configuration.py b/examples/notification_configuration.py index 07360f3..ea20380 100644 --- a/examples/notification_configuration.py +++ b/examples/notification_configuration.py @@ -80,10 +80,10 @@ def main(): except Exception as e: error_msg = str(e).lower() if "not found" in error_msg: - print(f" โš ๏ธ Team not found (expected with fake team ID): {team_id}") - print(" ๐Ÿ’ก Teams are not available in HCP Terraform free plan") + print(f" Team not found (expected with fake team ID): {team_id}") + print(" Teams are not available in HCP Terraform free plan") else: - print(f" โŒ Error listing team notifications: {e}") + print(f" Error listing team notifications: {e}") print() @@ -150,13 +150,13 @@ def main(): notification_config_id=notification_id ) print( - f" โœ… Verification successful for notification ID: {notification_id}" + f" Verification successful for notification ID: {notification_id}" ) print(" Note: Verification sends a test payload to the configured URL") except Exception as e: - print(f" โš ๏ธ Verification failed (expected with fake URL): {e}") + print(f" Verification failed (expected with fake URL): {e}") print( - " ๐Ÿ’ก To test verification, use a real webhook URL from Slack, Teams, or Discord" + " To test verification, use a real webhook URL from Slack, Teams, or Discord" ) # ===== Delete the notification configuration ===== @@ -178,15 +178,13 @@ def main(): except Exception as e: error_msg = str(e).lower() if "verification failed" in error_msg and "404" in error_msg: - print(" โš ๏ธ Webhook verification failed (expected with fake URL)") - print( - " ๐Ÿ’ก The fake Slack URL returns 404 - this is normal for testing" - ) - print(" ๐Ÿ”— To test real verification, use a webhook from:") + print(" Webhook verification failed (expected with fake URL)") + print(" The fake Slack URL returns 404 - this is normal for testing") + print(" To test real verification, use a webhook from:") print(" โ€ข webhook.site (instant test URL)") print(" โ€ข Slack, Teams, or Discord webhook") else: - print(f" โŒ Error in workspace notification operations: {e}") + print(f" Error in workspace notification operations: {e}") print() @@ -229,14 +227,14 @@ def main(): ) except Exception as e: - print(f" โŒ Error in team notification operations: {e}") + print(f" Error in team notification operations: {e}") error_msg = str(e).lower() if "not found" in error_msg: - print(" ๐Ÿ’ก Team may not exist or token lacks team permissions") + print(" Team may not exist or token lacks team permissions") elif "forbidden" in error_msg or "unauthorized" in error_msg: - print(" ๐Ÿ’ก Token may lack team notification permissions") + print(" Token may lack team notification permissions") elif "team" in error_msg: - print(" ๐Ÿ’ก Team-specific error - check team settings or plan level") + print(" Team-specific error - check team settings or plan level") print() diff --git a/examples/oauth_client.py b/examples/oauth_client.py index 96671dd..b4c7aa2 100644 --- a/examples/oauth_client.py +++ b/examples/oauth_client.py @@ -71,7 +71,7 @@ def main(): github_token = os.getenv("OAUTH_CLIENT_GITHUB_TOKEN") if not github_token: print( - "\nโš  WARNING: OAUTH_CLIENT_GITHUB_TOKEN not set. GitHub-related tests will be skipped." + "\n WARNING: OAUTH_CLIENT_GITHUB_TOKEN not set. GitHub-related tests will be skipped." ) print( "Set this environment variable to test OAuth client creation with GitHub." @@ -89,7 +89,7 @@ def main(): # Test basic list without options oauth_clients = list(client.oauth_clients.list(organization_name)) - print(f" โœ“ Found {len(oauth_clients)} OAuth clients") + print(f" Found {len(oauth_clients)} OAuth clients") for i, oauth_client in enumerate(oauth_clients[:3], 1): print(f" {i}. {oauth_client.id} - {oauth_client.service_provider}") @@ -111,7 +111,7 @@ def main(): client.oauth_clients.list(organization_name, options) ) print( - f" โœ“ Found {len(oauth_clients_with_options)} OAuth clients with options" + f" Found {len(oauth_clients_with_options)} OAuth clients with options" ) if oauth_clients_with_options: @@ -124,7 +124,7 @@ def main(): ) except Exception as e: - print(f" โœ— Error listing OAuth clients: {e}") + print(f" Error listing OAuth clients: {e}") # ===================================================== # TEST 2: CREATE OAUTH CLIENT @@ -152,7 +152,7 @@ def main(): created_oauth_client = client.oauth_clients.create( organization_name, create_options ) - print(f" โœ“ Created OAuth client: {created_oauth_client.id}") + print(f" Created OAuth client: {created_oauth_client.id}") print(f" Name: {created_oauth_client.name}") print(f" Service Provider: {created_oauth_client.service_provider}") print(f" API URL: {created_oauth_client.api_url}") @@ -162,9 +162,9 @@ def main(): ) except Exception as e: - print(f" โœ— Error creating OAuth client: {e}") + print(f" Error creating OAuth client: {e}") else: - print(" โš  Skipped - OAUTH_CLIENT_GITHUB_TOKEN not set") + print(" Skipped - OAUTH_CLIENT_GITHUB_TOKEN not set") # ===================================================== # TEST 3: READ OAUTH CLIENT @@ -178,7 +178,7 @@ def main(): print(f"Reading OAuth client: {created_oauth_client.id}") read_oauth_client = client.oauth_clients.read(created_oauth_client.id) - print(f" โœ“ Read OAuth client: {read_oauth_client.id}") + print(f" Read OAuth client: {read_oauth_client.id}") print(f" Name: {read_oauth_client.name}") print(f" Service Provider: {read_oauth_client.service_provider}") print(f" Created At: {read_oauth_client.created_at}") @@ -186,7 +186,7 @@ def main(): print(f" Connect Path: {read_oauth_client.connect_path}") except Exception as e: - print(f" โœ— Error reading OAuth client: {e}") + print(f" Error reading OAuth client: {e}") else: # Try to read an existing OAuth client if no client was created try: @@ -196,12 +196,12 @@ def main(): print(f"Reading existing OAuth client: {test_client.id}") read_oauth_client = client.oauth_clients.read(test_client.id) - print(f" โœ“ Read existing OAuth client: {read_oauth_client.id}") + print(f" Read existing OAuth client: {read_oauth_client.id}") print(f" Service Provider: {read_oauth_client.service_provider}") else: - print(" โš  No existing OAuth clients found to test read()") + print(" No existing OAuth clients found to test read()") except Exception as e: - print(f" โœ— Error reading existing OAuth client: {e}") + print(f" Error reading existing OAuth client: {e}") # ===================================================== # TEST 4: READ OAUTH CLIENT WITH OPTIONS @@ -234,7 +234,7 @@ def main(): read_oauth_client = client.oauth_clients.read_with_options( target_client.id, read_options ) - print(f" โœ“ Read OAuth client with options: {read_oauth_client.id}") + print(f" Read OAuth client with options: {read_oauth_client.id}") print(f" OAuth Tokens: {len(read_oauth_client.oauth_tokens or [])}") print(f" Projects: {len(read_oauth_client.projects or [])}") @@ -245,9 +245,9 @@ def main(): print(f" {i}. Token ID: {token.get('id', 'N/A')}") except Exception as e: - print(f" โœ— Error reading OAuth client with options: {e}") + print(f" Error reading OAuth client with options: {e}") else: - print(" โš  No OAuth client available to test read_with_options()") + print(" No OAuth client available to test read_with_options()") # ===================================================== # TEST 5: UPDATE OAUTH CLIENT @@ -268,7 +268,7 @@ def main(): updated_oauth_client = client.oauth_clients.update( created_oauth_client.id, update_options ) - print(f" โœ“ Updated OAuth client: {updated_oauth_client.id}") + print(f" Updated OAuth client: {updated_oauth_client.id}") print(f" Updated Name: {updated_oauth_client.name}") print( f" Updated Organization Scoped: {updated_oauth_client.organization_scoped}" @@ -278,9 +278,9 @@ def main(): created_oauth_client = updated_oauth_client except Exception as e: - print(f" โœ— Error updating OAuth client: {e}") + print(f" Error updating OAuth client: {e}") else: - print(" โš  No OAuth client created to test update()") + print(" No OAuth client created to test update()") # ===================================================== # TEST 6: PREPARE TEST PROJECTS (for project operations) @@ -298,7 +298,7 @@ def main(): {"type": "projects", "id": project.id} for project in projects[:2] ] print( - f" โœ“ Found {len(projects)} projects, using {len(test_projects)} for testing:" + f" Found {len(projects)} projects, using {len(test_projects)} for testing:" ) for i, project_ref in enumerate(test_projects, 1): corresponding_project = projects[i - 1] @@ -306,10 +306,10 @@ def main(): f" {i}. {corresponding_project.name} (ID: {project_ref['id']})" ) else: - print(" โš  No projects found - project operations tests will be skipped") + print(" No projects found - project operations tests will be skipped") except Exception as e: - print(f" โš  Error getting projects: {e}") + print(f" Error getting projects: {e}") # ===================================================== # TEST 7: ADD PROJECTS TO OAUTH CLIENT @@ -327,7 +327,7 @@ def main(): client.oauth_clients.add_projects(created_oauth_client.id, add_options) print( - f" โœ“ Successfully added {len(test_projects)} projects to OAuth client" + f" Successfully added {len(test_projects)} projects to OAuth client" ) # Verify the projects were added by reading the client with projects included @@ -338,16 +338,16 @@ def main(): created_oauth_client.id, read_options ) print( - f" โœ“ Verification: OAuth client now has {len(updated_client.projects or [])} projects" + f" Verification: OAuth client now has {len(updated_client.projects or [])} projects" ) except Exception as e: - print(f" โœ— Error adding projects to OAuth client: {e}") + print(f" Error adding projects to OAuth client: {e}") else: if not created_oauth_client: - print(" โš  No OAuth client created to test add_projects()") + print(" No OAuth client created to test add_projects()") if not test_projects: - print(" โš  No projects available to test add_projects()") + print(" No projects available to test add_projects()") # ===================================================== # TEST 8: REMOVE PROJECTS FROM OAUTH CLIENT @@ -367,7 +367,7 @@ def main(): created_oauth_client.id, remove_options ) print( - f" โœ“ Successfully removed {len(test_projects)} projects from OAuth client" + f" Successfully removed {len(test_projects)} projects from OAuth client" ) # Verify the projects were removed by reading the client with projects included @@ -378,16 +378,16 @@ def main(): created_oauth_client.id, read_options ) print( - f" โœ“ Verification: OAuth client now has {len(updated_client.projects or [])} projects" + f" Verification: OAuth client now has {len(updated_client.projects or [])} projects" ) except Exception as e: - print(f" โœ— Error removing projects from OAuth client: {e}") + print(f" Error removing projects from OAuth client: {e}") else: if not created_oauth_client: - print(" โš  No OAuth client created to test remove_projects()") + print(" No OAuth client created to test remove_projects()") if not test_projects: - print(" โš  No projects available to test remove_projects()") + print(" No projects available to test remove_projects()") # ===================================================== # TEST 9: DELETE OAUTH CLIENT @@ -403,29 +403,27 @@ def main(): # First, let's confirm it exists try: client.oauth_clients.read(created_oauth_client.id) - print(" โœ“ Confirmed OAuth client exists before deletion") + print(" Confirmed OAuth client exists before deletion") except NotFound: - print(" โš  OAuth client not found before deletion attempt") + print(" OAuth client not found before deletion attempt") # Delete the OAuth client client.oauth_clients.delete(created_oauth_client.id) - print(f" โœ“ Successfully deleted OAuth client: {created_oauth_client.id}") + print(f" Successfully deleted OAuth client: {created_oauth_client.id}") # Verify deletion by trying to read it try: client.oauth_clients.read(created_oauth_client.id) - print(" โš  Warning: OAuth client still exists after deletion") + print(" Warning: OAuth client still exists after deletion") except NotFound: - print( - " โœ“ Verification: OAuth client successfully deleted (not found)" - ) + print(" Verification: OAuth client successfully deleted (not found)") except Exception as e: print(f" ? Verification error: {e}") except Exception as e: - print(f" โœ— Error deleting OAuth client: {e}") + print(f" Error deleting OAuth client: {e}") else: - print(" โš  No OAuth client created to test delete()") + print(" No OAuth client created to test delete()") # ===================================================== # SUMMARY @@ -434,14 +432,14 @@ def main(): print("OAUTH CLIENT TESTING COMPLETE") print("=" * 80) print("Functions tested:") - print("โœ“ 1. list() - List OAuth clients for organization") - print("โœ“ 2. create() - Create OAuth client with VCS provider") - print("โœ“ 3. read() - Read OAuth client by ID") - print("โœ“ 4. read_with_options() - Read OAuth client with includes") - print("โœ“ 5. update() - Update existing OAuth client") - print("โœ“ 6. add_projects() - Add projects to OAuth client") - print("โœ“ 7. remove_projects() - Remove projects from OAuth client") - print("โœ“ 8. delete() - Delete OAuth client") + print(" 1. list() - List OAuth clients for organization") + print(" 2. create() - Create OAuth client with VCS provider") + print(" 3. read() - Read OAuth client by ID") + print(" 4. read_with_options() - Read OAuth client with includes") + print(" 5. update() - Update existing OAuth client") + print(" 6. add_projects() - Add projects to OAuth client") + print(" 7. remove_projects() - Remove projects from OAuth client") + print(" 8. delete() - Delete OAuth client") print("\nAll OAuth client functions have been tested!") print("Check the output above for any errors or warnings.") print("=" * 80) diff --git a/examples/oauth_token.py b/examples/oauth_token.py index 725fb59..5a0ae1d 100644 --- a/examples/oauth_token.py +++ b/examples/oauth_token.py @@ -57,7 +57,7 @@ def main(): try: # Test basic list without options token_list = client.oauth_tokens.list(organization_name) - print(f" โœ“ Found {len(token_list.items)} OAuth tokens") + print(f" Found {len(token_list.items)} OAuth tokens") # Show token details for i, token in enumerate(token_list.items[:3], 1): # Show first 3 @@ -78,7 +78,7 @@ def main(): print("\n Testing list() with pagination options:") options = OAuthTokenListOptions(page_size=10, page_number=1) token_list_with_options = client.oauth_tokens.list(organization_name, options) - print(f" โœ“ Found {len(token_list_with_options.items)} tokens with options") + print(f" Found {len(token_list_with_options.items)} tokens with options") if token_list_with_options.current_page: print(f" Current page: {token_list_with_options.current_page}") if token_list_with_options.total_count: @@ -86,10 +86,10 @@ def main(): except NotFound: print( - " โœ“ No OAuth tokens found (organization may not exist or no tokens available)" + " No OAuth tokens found (organization may not exist or no tokens available)" ) except Exception as e: - print(f" โœ— Error: {e}") + print(f" Error: {e}") # ===================================================== # TEST 2: READ OAUTH TOKEN @@ -98,7 +98,7 @@ def main(): print("\n2. Testing read() function:") try: token = client.oauth_tokens.read(test_token_id) - print(f" โœ“ Read OAuth token: {token.id}") + print(f" Read OAuth token: {token.id}") print(f" UID: {token.uid}") print(f" Service Provider User: {token.service_provider_user}") print(f" Has SSH Key: {token.has_ssh_key}") @@ -107,10 +107,10 @@ def main(): print(f" OAuth Client: {token.oauth_client.id}") except Exception as e: - print(f" โœ— Error: {e}") + print(f" Error: {e}") else: print("\n2. Testing read() function:") - print(" โš  Skipped - No OAuth token available to read") + print(" Skipped - No OAuth token available to read") # ===================================================== # TEST 3: UPDATE OAUTH TOKEN @@ -125,23 +125,23 @@ def main(): options = OAuthTokenUpdateOptions(private_ssh_key=ssh_key) updated_token = client.oauth_tokens.update(test_token_id, options) - print(f" โœ“ Updated OAuth token: {updated_token.id}") + print(f" Updated OAuth token: {updated_token.id}") print(f" Has SSH Key after update: {updated_token.has_ssh_key}") # Test updating without SSH key (no changes) print("\n Testing update without changes...") options_empty = OAuthTokenUpdateOptions() updated_token_2 = client.oauth_tokens.update(test_token_id, options_empty) - print(f" โœ“ Updated OAuth token (no changes): {updated_token_2.id}") + print(f" Updated OAuth token (no changes): {updated_token_2.id}") except Exception as e: - print(f" โœ— Error: {e}") + print(f" Error: {e}") print( " Note: This may fail if the SSH key format is invalid or constraints apply" ) else: print("\n3. Testing update() function:") - print(" โš  Skipped - No OAuth token available to update") + print(" Skipped - No OAuth token available to update") # ===================================================== # TEST 4: DELETE OAUTH TOKEN @@ -154,19 +154,19 @@ def main(): try: print(f" Attempting to delete OAuth token: {delete_token_id}") client.oauth_tokens.delete(delete_token_id) - print(f" โœ“ Successfully deleted OAuth token: {delete_token_id}") + print(f" Successfully deleted OAuth token: {delete_token_id}") # Verify deletion by trying to read the token try: client.oauth_tokens.read(delete_token_id) - print(" โœ— Token still exists after deletion!") + print(" Token still exists after deletion!") except NotFound: - print(" โœ“ Confirmed token was deleted - no longer accessible") + print(" Confirmed token was deleted - no longer accessible") except Exception as e: print(f" ? Verification failed: {e}") except Exception as e: - print(f" โœ— Error deleting token: {e}") + print(f" Error deleting token: {e}") # Uncomment the following section ONLY if you have a disposable OAuth token # WARNING: This will permanently delete the OAuth token! @@ -175,21 +175,21 @@ def main(): try: print(f" Attempting to delete OAuth token: {test_token_id}") client.oauth_tokens.delete(test_token_id) - print(f" โœ“ Successfully deleted OAuth token: {test_token_id}") + print(f" Successfully deleted OAuth token: {test_token_id}") # Verify deletion by trying to read the token try: client.oauth_tokens.read(test_token_id) - print(f" โœ— Token still exists after deletion!") + print(f" Token still exists after deletion!") except NotFound: - print(f" โœ“ Confirmed token was deleted - no longer accessible") + print(f" Confirmed token was deleted - no longer accessible") except Exception as e: print(f" ? Verification failed: {e}") except Exception as e: - print(f" โœ— Error deleting token: {e}") + print(f" Error deleting token: {e}") else: - print(" โš  Skipped - No OAuth token available to delete") + print(" Skipped - No OAuth token available to delete") """ # ===================================================== @@ -199,10 +199,10 @@ def main(): print("OAUTH TOKEN TESTING COMPLETE") print("=" * 80) print("Functions tested:") - print("โœ“ 1. list() - List OAuth tokens for organization") - print("โœ“ 2. read() - Read OAuth token by ID") - print("โœ“ 3. update() - Update existing OAuth token") - print("โœ“ 4. delete() - Delete OAuth token (testing with ot-WQf5ARHA1Qxzo9d4)") + print(" 1. list() - List OAuth tokens for organization") + print(" 2. read() - Read OAuth token by ID") + print(" 3. update() - Update existing OAuth token") + print(" 4. delete() - Delete OAuth token (testing with ot-WQf5ARHA1Qxzo9d4)") print("") print("All OAuth token functions have been tested!") print("Check the output above for any errors or warnings.") diff --git a/examples/org.py b/examples/org.py index 93b8f0f..eb767b2 100644 --- a/examples/org.py +++ b/examples/org.py @@ -16,7 +16,7 @@ def test_basic_org_operations(client): try: org_list = client.organizations.list() orgs = list(org_list) - print(f" โœ“ Found {len(orgs)} organizations") + print(f" Found {len(orgs)} organizations") # Show first few organizations for i, org in enumerate(orgs[:5], 1): @@ -30,7 +30,7 @@ def test_basic_org_operations(client): return orgs[0].name if orgs else None # Return first org name for testing except Exception as e: - print(f" โœ— Error listing organizations: {e}") + print(f" Error listing organizations: {e}") return None @@ -42,31 +42,31 @@ def test_org_read_operations(client, org_name): print("\n1. Reading Organization Details:") try: org = client.organizations.read(org_name) - print(f" โœ“ Organization: {org.name}") + print(f" Organization: {org.name}") print(f" ID: {org.id}") print(f" Email: {org.email or 'Not set'}") print(f" Created: {org.created_at or 'Unknown'}") print(f" Execution Mode: {org.default_execution_mode or 'Not set'}") print(f" Two-Factor: {org.two_factor_conformant}") except Exception as e: - print(f" โœ— Error reading organization: {e}") + print(f" Error reading organization: {e}") # Test capacity print("\n2. Reading Organization Capacity:") try: capacity = client.organizations.read_capacity(org_name) - print(" โœ“ Capacity:") + print(" Capacity:") print(f" Pending runs: {capacity.pending}") print(f" Running runs: {capacity.running}") print(f" Total active: {capacity.pending + capacity.running}") except Exception as e: - print(f" โœ— Error reading capacity: {e}") + print(f" Error reading capacity: {e}") # Test entitlements print("\n3. Reading Organization Entitlements:") try: entitlements = client.organizations.read_entitlements(org_name) - print(" โœ“ Entitlements:") + print(" Entitlements:") print(f" Operations: {entitlements.operations}") print(f" Teams: {entitlements.teams}") print(f" State Storage: {entitlements.state_storage}") @@ -76,14 +76,14 @@ def test_org_read_operations(client, org_name): print(f" Private Module Registry: {entitlements.private_module_registry}") print(f" SSO: {entitlements.sso}") except Exception as e: - print(f" โœ— Error reading entitlements: {e}") + print(f" Error reading entitlements: {e}") # Test run queue print("\n4. Reading Organization Run Queue:") try: queue_options = ReadRunQueueOptions(page_number=1, page_size=10) run_queue = client.organizations.read_run_queue(org_name, queue_options) - print(" โœ“ Run Queue:") + print(" Run Queue:") print(f" Items in queue: {len(run_queue.items)}") if run_queue.pagination: @@ -98,7 +98,7 @@ def test_org_read_operations(client, org_name): print(f" ... and {len(run_queue.items) - 3} more runs") except Exception as e: - print(f" โœ— Error reading run queue: {e}") + print(f" Error reading run queue: {e}") def test_data_retention_policies(client, org_name): @@ -111,27 +111,27 @@ def test_data_retention_policies(client, org_name): try: policy_choice = client.organizations.read_data_retention_policy_choice(org_name) if policy_choice is None: - print(" โœ“ No data retention policy currently configured") + print(" No data retention policy currently configured") elif policy_choice.data_retention_policy_delete_older: policy = policy_choice.data_retention_policy_delete_older print( - f" โœ“ Delete Older Policy: {policy.delete_older_than_n_days} days (ID: {policy.id})" + f" Delete Older Policy: {policy.delete_older_than_n_days} days (ID: {policy.id})" ) elif policy_choice.data_retention_policy_dont_delete: policy = policy_choice.data_retention_policy_dont_delete - print(f" โœ“ Don't Delete Policy (ID: {policy.id})") + print(f" Don't Delete Policy (ID: {policy.id})") elif policy_choice.data_retention_policy: policy = policy_choice.data_retention_policy print( - f" โœ“ Legacy Policy: {policy.delete_older_than_n_days} days (ID: {policy.id})" + f" Legacy Policy: {policy.delete_older_than_n_days} days (ID: {policy.id})" ) except Exception as e: if "not found" in str(e).lower() or "404" in str(e): print( - " โš  Data retention policies not available (Terraform Enterprise feature)" + " Data retention policies not available (Terraform Enterprise feature)" ) else: - print(f" โœ— Error reading data retention policy: {e}") + print(f" Error reading data retention policy: {e}") # Test setting delete older policy print("\n2. Setting Delete Older Data Retention Policy (30 days):") @@ -140,14 +140,14 @@ def test_data_retention_policies(client, org_name): policy = client.organizations.set_data_retention_policy_delete_older( org_name, options ) - print(" โœ“ Created Delete Older Policy:") + print(" Created Delete Older Policy:") print(f" ID: {policy.id}") print(f" Delete after: {policy.delete_older_than_n_days} days") except Exception as e: if "not found" in str(e).lower() or "404" in str(e): - print(" โš  Feature not available (Terraform Enterprise only)") + print(" Feature not available (Terraform Enterprise only)") else: - print(f" โœ— Error setting delete older policy: {e}") + print(f" Error setting delete older policy: {e}") # Test updating delete older policy print("\n3. Updating Delete Older Policy (15 days):") @@ -156,14 +156,14 @@ def test_data_retention_policies(client, org_name): policy = client.organizations.set_data_retention_policy_delete_older( org_name, options ) - print(" โœ“ Updated Delete Older Policy:") + print(" Updated Delete Older Policy:") print(f" ID: {policy.id}") print(f" Delete after: {policy.delete_older_than_n_days} days") except Exception as e: if "not found" in str(e).lower() or "404" in str(e): - print(" โš  Feature not available (Terraform Enterprise only)") + print(" Feature not available (Terraform Enterprise only)") else: - print(f" โœ— Error updating delete older policy: {e}") + print(f" Error updating delete older policy: {e}") # Test setting don't delete policy print("\n4. Setting Don't Delete Data Retention Policy:") @@ -172,59 +172,59 @@ def test_data_retention_policies(client, org_name): policy = client.organizations.set_data_retention_policy_dont_delete( org_name, options ) - print(" โœ“ Created Don't Delete Policy:") + print(" Created Don't Delete Policy:") print(f" ID: {policy.id}") print(" Data will never be automatically deleted") except Exception as e: if "not found" in str(e).lower() or "404" in str(e): - print(" โš  Feature not available (Terraform Enterprise only)") + print(" Feature not available (Terraform Enterprise only)") else: - print(f" โœ— Error setting don't delete policy: {e}") + print(f" Error setting don't delete policy: {e}") # Test reading policy after changes print("\n5. Reading Data Retention Policy After Changes:") try: policy_choice = client.organizations.read_data_retention_policy_choice(org_name) if policy_choice is None: - print(" โœ“ No data retention policy configured") + print(" No data retention policy configured") elif policy_choice.data_retention_policy_delete_older: policy = policy_choice.data_retention_policy_delete_older print( - f" โœ“ Current Policy: Delete Older ({policy.delete_older_than_n_days} days)" + f" Current Policy: Delete Older ({policy.delete_older_than_n_days} days)" ) elif policy_choice.data_retention_policy_dont_delete: - print(" โœ“ Current Policy: Don't Delete") + print(" Current Policy: Don't Delete") # Test legacy conversion if policy_choice and policy_choice.is_populated(): legacy = policy_choice.convert_to_legacy_struct() if legacy: print( - f" โœ“ Legacy representation: {legacy.delete_older_than_n_days} days" + f" Legacy representation: {legacy.delete_older_than_n_days} days" ) except Exception as e: if "not found" in str(e).lower() or "404" in str(e): - print(" โš  Feature not available (Terraform Enterprise only)") + print(" Feature not available (Terraform Enterprise only)") else: - print(f" โœ— Error reading updated policy: {e}") + print(f" Error reading updated policy: {e}") # Test deleting policy print("\n6. Deleting Data Retention Policy:") try: client.organizations.delete_data_retention_policy(org_name) - print(" โœ“ Successfully deleted data retention policy") + print(" Successfully deleted data retention policy") # Verify deletion policy_choice = client.organizations.read_data_retention_policy_choice(org_name) if policy_choice is None or not policy_choice.is_populated(): - print(" โœ“ Verified: No policy configured after deletion") + print(" Verified: No policy configured after deletion") else: - print(" โš  Policy still exists after deletion attempt") + print(" Policy still exists after deletion attempt") except Exception as e: if "not found" in str(e).lower() or "404" in str(e): - print(" โš  Feature not available (Terraform Enterprise only)") + print(" Feature not available (Terraform Enterprise only)") else: - print(f" โœ— Error deleting policy: {e}") + print(f" Error deleting policy: {e}") def test_organization_creation_and_cleanup(client): @@ -239,24 +239,24 @@ def test_organization_creation_and_cleanup(client): name=test_org_name, email="aayush.singh@hashicorp.com" ) new_org = client.organizations.create(create_opts) - print(f" โœ“ Created organization: {new_org.name}") + print(f" Created organization: {new_org.name}") print(f" ID: {new_org.id}") print(f" Email: {new_org.email}") # Test reading the newly created org print("\n2. Reading Newly Created Organization:") read_org = client.organizations.read(test_org_name) - print(f" โœ“ Successfully read organization: {read_org.name}") + print(f" Successfully read organization: {read_org.name}") # Cleanup print("\n3. Cleaning Up Test Organization:") client.organizations.delete(test_org_name) - print(" โœ“ Successfully deleted test organization") + print(" Successfully deleted test organization") return True except Exception as e: - print(f" โš  Organization creation/deletion test skipped: {e}") + print(f" Organization creation/deletion test skipped: {e}") print( " This is normal if you don't have organization management permissions" ) @@ -265,15 +265,15 @@ def test_organization_creation_and_cleanup(client): def main(): """Main function to test all organization functionalities.""" - print("๐Ÿš€ Python TFE Organization Functions Test Suite") + print(" Python TFE Organization Functions Test Suite") print("=" * 60) # Initialize client try: client = TFEClient(TFEConfig.from_env()) - print("โœ“ TFE Client initialized successfully") + print(" TFE Client initialized successfully") except Exception as e: - print(f"โœ— Failed to initialize TFE client: {e}") + print(f" Failed to initialize TFE client: {e}") print( "Please ensure TF_CLOUD_ORGANIZATION and TF_CLOUD_TOKEN environment variables are set" ) @@ -282,7 +282,7 @@ def main(): # Test basic operations test_org_name = test_basic_org_operations(client) if not test_org_name: - print("\nโœ— Cannot continue without a valid organization") + print("\n Cannot continue without a valid organization") return 1 # Test read operations @@ -296,20 +296,20 @@ def main(): # Summary print("\n" + "=" * 60) - print("๐Ÿ“Š Test Summary:") - print("โœ“ Basic organization operations tested") - print("โœ“ Organization read operations tested") - print("โœ“ Data retention policy operations tested") + print(" Test Summary:") + print(" Basic organization operations tested") + print(" Organization read operations tested") + print(" Data retention policy operations tested") if creation_success: - print("โœ“ Organization creation/deletion tested") + print("Organization creation/deletion tested") else: - print("โš  Organization creation/deletion skipped (permissions)") + print("Organization creation/deletion skipped (permissions)") print( - f"\n๐ŸŽฏ All available organization functions have been tested against '{test_org_name}'" + f"\n All available organization functions have been tested against '{test_org_name}'" ) print("Note: Data retention policy features require Terraform Enterprise") - print("\nโœ… Test suite completed successfully!") + print("\nTest suite completed successfully!") return 0 diff --git a/examples/organization_membership.py b/examples/organization_membership.py new file mode 100644 index 0000000..2fcc833 --- /dev/null +++ b/examples/organization_membership.py @@ -0,0 +1,320 @@ +#!/usr/bin/env python3 +""" +Example and test script for organization membership list functionality. + +Requirements: +- TFE_TOKEN environment variable set +- TFE_ADDRESS environment variable set (optional, defaults to Terraform Cloud) +- An organization with members to list + +Usage: + python examples/organization_membership.py +""" + +import sys + +from pytfe import TFEClient +from pytfe.models import ( + OrganizationMembershipListOptions, + OrganizationMembershipReadOptions, + OrganizationMembershipStatus, + OrgMembershipIncludeOpt, +) + + +def main(): + """Demonstrate organization membership list functionality.""" + + organization_name = "aayush-test" + + # Initialize the client (reads TFE_TOKEN and TFE_ADDRESS from environment) + try: + client = TFEClient() + print(" Connected to Terraform Cloud/Enterprise") + except Exception as e: + print(f" Error connecting: {e}") + print("\nMake sure TFE_TOKEN environment variable is set:") + print(" export TFE_TOKEN='your-token-here'") + sys.exit(1) + + print(f"\nTesting Organization Membership List for: {organization_name}") + print("=" * 70) + + # Test 1: List all organization memberships (no options) + print("\n[Test 1] List all organization memberships:") + try: + count = 0 + memberships_list = [] + for membership in client.organization_memberships.list(organization_name): + count += 1 + memberships_list.append(membership) + if count <= 5: # Show first 5 + print( + f" {membership.email} (ID: {membership.id[:8]}..., Status: {membership.status.value})" + ) + + print(memberships_list) + print(f" Total memberships: {count}") + + if count == 0: + print( + " No memberships found - organization may not exist or has no members" + ) + else: + print(f" Success: Retrieved {count} membership(s)") + except ValueError as e: + print(f" Validation Error: {e}") + except Exception as e: + print(f" Error: {type(e).__name__}: {e}") + + # Test 2: Iterate with custom page size + print("\n[Test 2] Iterate with custom page size (3 items per page):") + try: + options = OrganizationMembershipListOptions( + page_size=3, # Fetch 3 items per page + ) + count = 0 + for membership in client.organization_memberships.list( + organization_name, options + ): + count += 1 + if count <= 3: + print(f" {membership.email}") + + print(f" Processed {count} memberships (fetched in batches of 3)") + print(" Success: Pagination working correctly") + except Exception as e: + print(f" Error: {type(e).__name__}: {e}") + + # Test 3: Iterate with user relationships included + print("\n[Test 3] Iterate with user relationships included:") + try: + options = OrganizationMembershipListOptions( + include=[OrgMembershipIncludeOpt.USER], + ) + count = 0 + users_found = 0 + for membership in client.organization_memberships.list( + organization_name, options + ): + count += 1 + if membership.user: + users_found += 1 + if count <= 3: # Show first 3 + user_id = membership.user.id if membership.user else "N/A" + print(f" {membership.email} (User ID: {user_id})") + + print(f" Processed {count} memberships, {users_found} with user data") + print(" Success: Include parameter working") + except Exception as e: + print(f" Error: {type(e).__name__}: {e}") + + # Test 4: Filter by status (invited) + print("\n[Test 4] Filter by status (invited only):") + try: + options = OrganizationMembershipListOptions( + status=OrganizationMembershipStatus.INVITED, + ) + invited = [] + for membership in client.organization_memberships.list( + organization_name, options + ): + invited.append(membership.email) + if membership.status != OrganizationMembershipStatus.INVITED: + print(f" ERROR: Found non-invited member: {membership.email}") + + print(f" Found {len(invited)} invited membership(s)") + for email in invited[:5]: # Show first 5 + print(f" {email}") + + if len(invited) == 0: + print(" No invited members found") + print(" Success: Status filter working") + except Exception as e: + print(f" Error: {type(e).__name__}: {e}") + + # Test 5: Filter by email addresses (using first member found in Test 1) + print("\n[Test 5] Filter by specific email address:") + try: + if count > 0 and len(memberships_list) > 0: + test_email = memberships_list[0].email + print(f" Testing with email: {test_email}") + + options = OrganizationMembershipListOptions( + emails=[test_email], + ) + matching = [] + for membership in client.organization_memberships.list( + organization_name, options + ): + matching.append(membership.email) + + print(f" Found {len(matching)} matching membership(s)") + for email in matching: + print(f" {email}") + + if len(matching) == 1 and matching[0] == test_email: + print(" Success: Email filter working correctly") + else: + print(f" Warning: Expected 1 result with email {test_email}") + else: + print(" Skipped: No memberships available from Test 1") + except ValueError as e: + print(f" Validation Error: {e}") + except Exception as e: + print(f" Error: {type(e).__name__}: {e}") + + # Test 6: Search by query string + print("\n[Test 6] Search memberships by query string:") + try: + if count > 0 and len(memberships_list) > 0: + # Extract domain from first email for testing + test_email = memberships_list[0].email + domain = test_email.split("@")[1] if "@" in test_email else None + + if domain: + print(f" Searching for: {domain}") + options = OrganizationMembershipListOptions( + query=domain, # Searches in user name and email + ) + results = [] + for membership in client.organization_memberships.list( + organization_name, options + ): + results.append(membership.email) + + print(f" Found {len(results)} membership(s) matching query") + for email in results[:5]: # Show first 5 + print(f" {email}") + + if len(results) > 0: + print(" Success: Query filter working") + else: + print(f" Warning: No results found for query '{domain}'") + else: + print(" Skipped: Could not extract domain from email") + else: + print(" Skipped: No memberships available from Test 1") + except Exception as e: + print(f" Error: {type(e).__name__}: {e}") + + # Test 7: Combined filters (active + includes) + print("\n[Test 7] Combined filters: active members with user & teams included:") + try: + options = OrganizationMembershipListOptions( + status=OrganizationMembershipStatus.ACTIVE, + include=[OrgMembershipIncludeOpt.USER, OrgMembershipIncludeOpt.TEAMS], + page_size=5, + ) + active_members = [] + for membership in client.organization_memberships.list( + organization_name, options + ): + team_count = len(membership.teams) if membership.teams else 0 + has_user = membership.user is not None + active_members.append((membership.email, team_count, has_user)) + + print(f" Found {len(active_members)} active membership(s)") + for email, team_count, has_user in active_members[:5]: # Show first 5 + user_str = " User" if has_user else " No User" + print(f" {email} (Teams: {team_count}, {user_str})") + + if len(active_members) > 0: + print(" Success: Combined filters working") + else: + print(" No active members found") + except Exception as e: + print(f" Error: {type(e).__name__}: {e}") + + # Test 8: Read a specific organization membership + print("\n[Test 8] Read a specific organization membership:") + try: + if count > 0 and len(memberships_list) > 0: + test_membership_id = memberships_list[0].id + print(f" Reading membership ID: {test_membership_id}") + + membership = client.organization_memberships.read(test_membership_id) + print(f" Email: {membership.email}") + print(f" Status: {membership.status.value}") + print(f" ID: {membership.id}") + print(" Success: Read membership successfully") + else: + print(" Skipped: No memberships available from Test 1") + except Exception as e: + print(f" Error: {type(e).__name__}: {e}") + + # Test 9: Read with options (include user and teams) + print("\n[Test 9] Read membership with options (include user & teams):") + try: + if count > 0 and len(memberships_list) > 0: + test_membership_id = memberships_list[0].id + print(f" Reading membership ID: {test_membership_id}") + + read_options = OrganizationMembershipReadOptions( + include=[OrgMembershipIncludeOpt.USER, OrgMembershipIncludeOpt.TEAMS] + ) + membership = client.organization_memberships.read_with_options( + test_membership_id, read_options + ) + + print(f" Email: {membership.email}") + print(f" Status: {membership.status.value}") + user_id = membership.user.id if membership.user else "N/A" + print(f" User ID: {user_id}") + team_count = len(membership.teams) if membership.teams else 0 + print(f" Teams: {team_count}") + print(" Success: Read with options working") + else: + print(" Skipped: No memberships available from Test 1") + except Exception as e: + print(f" Error: {type(e).__name__}: {e}") + + # CREATE EXAMPLES + print("\n[Create Example] Add a new organization membership:") + try: + from pytfe.models import OrganizationMembershipCreateOptions, Team + + # Replace with a valid email for your organization + new_member_email = "sivaselvan.i@hashicorp.com" + + # Create membership with teams (uncomment to use) + from pytfe.models import OrganizationAccess + + team = Team( + id="team-dx24FR9xQUuwNTHA", + organization_access=OrganizationAccess(read_workspaces=True), + ) # Replace with actual team ID + create_options = OrganizationMembershipCreateOptions( + email=new_member_email, teams=[team] + ) + + created_membership = client.organization_memberships.create( + organization_name, create_options + ) + print(f" Created membership for: {created_membership.email}") + print(f" ID: {created_membership.id}") + print(f" Status: {created_membership.status.value}") + + except Exception as e: + print(f" Error: {type(e).__name__}: {e}") + + # Delete membership example + print("\n[Delete Example] Delete an organization membership:") + try: + from pytfe.errors import NotFound + + membership_id = "ou-9mG77c6uE5GScg9k" # Replace with actual membership ID + print(f" Attempting to delete membership: {membership_id}") + + client.organization_memberships.delete(membership_id) + print(f" โœ“ Successfully deleted membership {membership_id}") + + except NotFound as e: + print(f" โœ— Membership not found: {e}") + print(" โ„น The membership may have already been deleted or the ID is invalid") + except Exception as e: + print(f" โœ— Error deleting membership: {type(e).__name__}: {e}") + + +if __name__ == "__main__": + main() diff --git a/examples/project.py b/examples/project.py index 6999e4f..3935878 100644 --- a/examples/project.py +++ b/examples/project.py @@ -31,6 +31,7 @@ from pytfe._http import HTTPTransport from pytfe.config import TFEConfig +from pytfe.errors import NotFound from pytfe.models import ( ProjectAddTagBindingsOptions, ProjectCreateOptions, @@ -50,7 +51,7 @@ def integration_client(): if not token: pytest.skip( "TFE_TOKEN environment variable is required. " - "Get your token from HCP Terraform: Settings โ†’ API Tokens" + "Get your token from HCP Terraform: Settings API Tokens" ) if not org: @@ -59,8 +60,8 @@ def integration_client(): "Use your organization name from HCP Terraform URL" ) - print(f"\n๐Ÿ”ง Testing against organization: {org}") - print(f"๐Ÿ”ง Using token: {token[:10]}...") + print(f"\n Testing against organization: {org}") + print(f" Using token: {token[:10]}...") config = TFEConfig() @@ -95,9 +96,9 @@ def test_list_projects_integration(integration_client): try: # Test basic list without options - print("๐Ÿ“‹ Testing LIST operation: basic list") + print(" Testing LIST operation: basic list") project_list = list(projects.list(org)) - print(f"โœ… Found {len(project_list)} projects in organization '{org}'") + print(f"Found {len(project_list)} projects in organization '{org}'") assert isinstance(project_list, list) @@ -111,18 +112,16 @@ def test_list_projects_integration(integration_client): assert hasattr(project, "description"), "Project should have a description" assert hasattr(project, "created_at"), "Project should have created_at" assert hasattr(project, "updated_at"), "Project should have updated_at" - print(f"๐Ÿ“‹ Example project: {project.name} (ID: {project.id})") - print(f"๐Ÿ“‹ Created: {project.created_at}, Updated: {project.updated_at}") + print(f" Example project: {project.name} (ID: {project.id})") + print(f" Created: {project.created_at}, Updated: {project.updated_at}") else: - print("๐Ÿ“‹ No projects found - this is normal for a new organization") + print(" No projects found - this is normal for a new organization") # Test list with options - print("๐Ÿ“‹ Testing LIST operation: with options") + print(" Testing LIST operation: with options") list_options = ProjectListOptions(page_size=5) project_list_with_options = list(projects.list(org, list_options)) - print( - f"โœ… List with options returned {len(project_list_with_options)} projects" - ) + print(f"List with options returned {len(project_list_with_options)} projects") except Exception as e: pytest.fail( @@ -145,7 +144,7 @@ def test_create_project_integration(integration_client): try: # Test CREATE operation - print(f"๐Ÿ”จ Testing CREATE operation: {test_name}") + print(f" Testing CREATE operation: {test_name}") create_options = ProjectCreateOptions( name=test_name, description=test_description ) @@ -169,9 +168,9 @@ def test_create_project_integration(integration_client): ) project_id = created_project.id - print(f"โœ… CREATE successful: {project_id}") + print(f"CREATE successful: {project_id}") print( - f"โœ… Project details: {created_project.name} - {created_project.description}" + f"Project details: {created_project.name} - {created_project.description}" ) except Exception as e: @@ -181,11 +180,11 @@ def test_create_project_integration(integration_client): # Clean up created project if project_id: try: - print(f"๐Ÿ—‘๏ธ Cleaning up created project: {project_id}") + print(f"Cleaning up created project: {project_id}") projects.delete(project_id) - print("โœ… Cleanup successful") + print("Cleanup successful") except Exception as e: - print(f"โŒ Warning: Failed to clean up project {project_id}: {e}") + print(f" Warning: Failed to clean up project {project_id}: {e}") def test_read_project_integration(integration_client): @@ -210,7 +209,7 @@ def test_read_project_integration(integration_client): project_id = created_project.id # Test READ operation - print(f"๐Ÿ“– Testing READ operation: {project_id}") + print(f" Testing READ operation: {project_id}") read_project = projects.read(project_id) # Validate read project @@ -226,11 +225,11 @@ def test_read_project_integration(integration_client): assert hasattr(read_project, "created_at"), "Project should have created_at" assert hasattr(read_project, "updated_at"), "Project should have updated_at" - print(f"โœ… READ successful: {read_project.name}") - print(f"โœ… Project created: {read_project.created_at}") + print(f"READ successful: {read_project.name}") + print(f"Project created: {read_project.created_at}") # Note: Projects API doesn't support include parameters in the current API version - print("โœ… READ operation completed successfully") + print("READ operation completed successfully") except Exception as e: pytest.fail(f"READ operation failed: {e}") @@ -239,11 +238,11 @@ def test_read_project_integration(integration_client): # Clean up created project if project_id: try: - print(f"๐Ÿ—‘๏ธ Cleaning up read test project: {project_id}") + print(f"Cleaning up read test project: {project_id}") projects.delete(project_id) - print("โœ… Cleanup successful") + print("Cleanup successful") except Exception as e: - print(f"โŒ Warning: Failed to clean up project {project_id}: {e}") + print(f" Warning: Failed to clean up project {project_id}: {e}") def test_update_project_integration(integration_client): @@ -263,7 +262,7 @@ def test_update_project_integration(integration_client): try: # Create a project to update - print(f"๐Ÿ”จ Creating project for UPDATE test: {original_name}") + print(f" Creating project for UPDATE test: {original_name}") create_options = ProjectCreateOptions( name=original_name, description=original_description ) @@ -271,7 +270,7 @@ def test_update_project_integration(integration_client): project_id = created_project.id # Test UPDATE operation - name only - print("โœ๏ธ Testing UPDATE operation: name only") + print(" Testing UPDATE operation: name only") update_options = ProjectUpdateOptions(name=updated_name) updated_project = projects.update(project_id, update_options) @@ -284,10 +283,10 @@ def test_update_project_integration(integration_client): assert updated_project.description == original_description, ( "Description should remain unchanged" ) - print(f"โœ… UPDATE name successful: {updated_project.name}") + print(f"UPDATE name successful: {updated_project.name}") # Test UPDATE operation - description only - print("โœ๏ธ Testing UPDATE operation: description only") + print(" Testing UPDATE operation: description only") update_options = ProjectUpdateOptions(description=updated_description) updated_project = projects.update(project_id, update_options) @@ -295,12 +294,12 @@ def test_update_project_integration(integration_client): assert updated_project.description == updated_description, ( f"Expected updated description {updated_description}, got {updated_project.description}" ) - print("โœ… UPDATE description successful") + print("UPDATE description successful") # Test UPDATE operation - both name and description final_name = f"final-{unique_id}" final_description = "Final description for update test" - print("โœ๏ธ Testing UPDATE operation: both name and description") + print(" Testing UPDATE operation: both name and description") update_options = ProjectUpdateOptions( name=final_name, description=final_description ) @@ -312,7 +311,7 @@ def test_update_project_integration(integration_client): assert updated_project.description == final_description, ( f"Expected final description {final_description}, got {updated_project.description}" ) - print(f"โœ… UPDATE both fields successful: {updated_project.name}") + print(f"UPDATE both fields successful: {updated_project.name}") except Exception as e: pytest.fail(f"UPDATE operation failed: {e}") @@ -321,11 +320,11 @@ def test_update_project_integration(integration_client): # Clean up created project if project_id: try: - print(f"๐Ÿ—‘๏ธ Cleaning up update test project: {project_id}") + print(f"Cleaning up update test project: {project_id}") projects.delete(project_id) - print("โœ… Cleanup successful") + print("Cleanup successful") except Exception as e: - print(f"โŒ Warning: Failed to clean up project {project_id}: {e}") + print(f" Warning: Failed to clean up project {project_id}: {e}") def test_delete_project_integration(integration_client): @@ -342,33 +341,33 @@ def test_delete_project_integration(integration_client): try: # Create a project to delete - print(f"๐Ÿ”จ Creating project for DELETE test: {test_name}") + print(f" Creating project for DELETE test: {test_name}") create_options = ProjectCreateOptions( name=test_name, description="Project for delete test" ) created_project = projects.create(org, create_options) project_id = created_project.id - print(f"โœ… Project created for deletion: {project_id}") + print(f"Project created for deletion: {project_id}") # Verify project exists - print("๐Ÿ“– Verifying project exists before deletion") + print(" Verifying project exists before deletion") read_project = projects.read(project_id) assert read_project.id == project_id - print(f"โœ… Project confirmed to exist: {read_project.name}") + print(f"Project confirmed to exist: {read_project.name}") # Test DELETE operation - print(f"๐Ÿ—‘๏ธ Testing DELETE operation: {project_id}") + print(f"Testing DELETE operation: {project_id}") projects.delete(project_id) - print("โœ… DELETE operation completed") + print("DELETE operation completed") # Verify project is deleted - print("๐Ÿ“– Verifying project is deleted") + print(" Verifying project is deleted") try: projects.read(project_id) pytest.fail("Project should not exist after deletion") except Exception as e: if "404" in str(e) or "not found" in str(e).lower(): - print("โœ… Project successfully deleted - confirmed by 404 error") + print("Project successfully deleted - confirmed by 404 error") else: raise e @@ -382,7 +381,7 @@ def test_delete_project_integration(integration_client): # Additional cleanup attempt (should be unnecessary) if project_id: try: - print(f"๐Ÿ—‘๏ธ Additional cleanup attempt: {project_id}") + print(f"Additional cleanup attempt: {project_id}") projects.delete(project_id) except Exception: pass # Project might already be deleted @@ -391,8 +390,8 @@ def test_delete_project_integration(integration_client): def test_comprehensive_crud_integration(integration_client): """Test all CRUD operations in sequence - โš ๏ธ WARNING: This test creates and deletes real resources! - Tests complete workflow: CREATE โ†’ READ โ†’ UPDATE โ†’ LIST โ†’ DELETE + WARNING: This test creates and deletes real resources! + Tests complete workflow: CREATE READ UPDATE LIST DELETE """ projects, org = integration_client @@ -404,10 +403,10 @@ def test_comprehensive_crud_integration(integration_client): project_id = None try: - print(f"๐Ÿ”„ Starting comprehensive CRUD test: {test_name}") + print(f" Starting comprehensive CRUD test: {test_name}") # 1. CREATE - print("1๏ธโƒฃ CREATE: Creating project") + print("1 CREATE: Creating project") create_options = ProjectCreateOptions( name=test_name, description=test_description ) @@ -416,19 +415,19 @@ def test_comprehensive_crud_integration(integration_client): assert created_project.name == test_name assert created_project.description == test_description - print(f"โœ… CREATE: {project_id}") + print(f"CREATE: {project_id}") # 2. READ - print("2๏ธโƒฃ READ: Reading created project") + print("2 READ: Reading created project") read_project = projects.read(project_id) assert read_project.id == project_id assert read_project.name == test_name assert read_project.description == test_description - print(f"โœ… READ: {read_project.name}") + print(f"READ: {read_project.name}") # 3. UPDATE - print("3๏ธโƒฃ UPDATE: Updating project") + print("3 UPDATE: Updating project") update_options = ProjectUpdateOptions( name=updated_name, description=updated_description ) @@ -437,10 +436,10 @@ def test_comprehensive_crud_integration(integration_client): assert updated_project.id == project_id assert updated_project.name == updated_name assert updated_project.description == updated_description - print(f"โœ… UPDATE: {updated_project.name}") + print(f"UPDATE: {updated_project.name}") # 4. LIST (verify updated project appears) - print("4๏ธโƒฃ LIST: Verifying project appears in list") + print("4 LIST: Verifying project appears in list") project_list = list(projects.list(org)) found_project = None for p in project_list: @@ -452,26 +451,26 @@ def test_comprehensive_crud_integration(integration_client): f"Updated project {project_id} should appear in list" ) assert found_project.name == updated_name - print("โœ… LIST: Found updated project in list") + print("LIST: Found updated project in list") # 5. DELETE - print("5๏ธโƒฃ DELETE: Deleting project") + print("5 DELETE: Deleting project") projects.delete(project_id) - print("โœ… DELETE: Project deleted") + print("DELETE: Project deleted") # 6. Verify deletion - print("6๏ธโƒฃ VERIFY: Confirming deletion") + print("6 VERIFY: Confirming deletion") try: projects.read(project_id) pytest.fail("Project should not exist after deletion") except Exception as e: if "404" in str(e) or "not found" in str(e).lower(): - print("โœ… VERIFY: Deletion confirmed") + print("VERIFY: Deletion confirmed") else: raise e project_id = None # Clear since deleted - print("๐ŸŽ‰ Comprehensive CRUD test completed successfully!") + print(" Comprehensive CRUD test completed successfully!") except Exception as e: pytest.fail(f"Comprehensive CRUD test failed: {e}") @@ -479,7 +478,7 @@ def test_comprehensive_crud_integration(integration_client): finally: if project_id: try: - print(f"๐Ÿ—‘๏ธ Final cleanup: {project_id}") + print(f"Final cleanup: {project_id}") projects.delete(project_id) except Exception: pass @@ -492,14 +491,14 @@ def test_validation_integration(integration_client): """ projects, org = integration_client - print("๐Ÿ” Testing validation with real API calls") + print(" Testing validation with real API calls") try: # Test valid project creation unique_id = str(uuid.uuid4())[:8] valid_name = f"validation-test-{unique_id}" - print(f"โœ… Testing valid project creation: {valid_name}") + print(f"Testing valid project creation: {valid_name}") create_options = ProjectCreateOptions( name=valid_name, description="Valid project" ) @@ -507,20 +506,20 @@ def test_validation_integration(integration_client): assert created_project.name == valid_name project_id = created_project.id - print(f"โœ… Valid project created successfully: {project_id}") + print(f"Valid project created successfully: {project_id}") # Test valid project update updated_name = f"validation-updated-{unique_id}" - print(f"โœ… Testing valid project update: {updated_name}") + print(f"Testing valid project update: {updated_name}") update_options = ProjectUpdateOptions(name=updated_name) updated_project = projects.update(project_id, update_options) assert updated_project.name == updated_name - print("โœ… Valid project updated successfully") + print("Valid project updated successfully") # Clean up projects.delete(project_id) - print("โœ… Validation test cleanup completed") + print("Validation test cleanup completed") except Exception as e: pytest.fail(f"Validation integration test failed: {e}") @@ -533,44 +532,42 @@ def test_error_handling_integration(integration_client): """ projects, org = integration_client - print("๐Ÿšซ Testing error handling scenarios") + print(" Testing error handling scenarios") # Test reading a non-existent project - print("๐Ÿšซ Testing read non-existent project") + print(" Testing read non-existent project") fake_project_id = "prj-nonexistent123456789" try: projects.read(fake_project_id) pytest.fail("Should have raised an exception for non-existent project") except Exception as e: - print( - f"โœ… Correctly handled error for non-existent project: {type(e).__name__}" - ) + print(f"Correctly handled error for non-existent project: {type(e).__name__}") assert "404" in str(e) or "not found" in str(e).lower() # Test updating a non-existent project - print("๐Ÿšซ Testing update non-existent project") + print(" Testing update non-existent project") try: update_options = ProjectUpdateOptions(name="should-fail") projects.update(fake_project_id, update_options) pytest.fail("Should have raised an exception for non-existent project") except Exception as e: print( - f"โœ… Correctly handled update error for non-existent project: {type(e).__name__}" + f"Correctly handled update error for non-existent project: {type(e).__name__}" ) assert "404" in str(e) or "not found" in str(e).lower() # Test deleting a non-existent project - print("๐Ÿšซ Testing delete non-existent project") + print(" Testing delete non-existent project") try: projects.delete(fake_project_id) pytest.fail("Should have raised an exception for non-existent project") except Exception as e: print( - f"โœ… Correctly handled delete error for non-existent project: {type(e).__name__}" + f"Correctly handled delete error for non-existent project: {type(e).__name__}" ) assert "404" in str(e) or "not found" in str(e).lower() - print("โœ… All error handling scenarios tested successfully") + print("All error handling scenarios tested successfully") def test_project_tag_bindings_integration(integration_client): @@ -589,42 +586,42 @@ def test_project_tag_bindings_integration(integration_client): try: # Create a test project for tagging operations - print(f"๐Ÿท๏ธ Setting up test project for tagging: {test_name}") + print(f" Setting up test project for tagging: {test_name}") create_options = ProjectCreateOptions( name=test_name, description=test_description ) created_project = projects.create(org, create_options) project_id = created_project.id - print(f"โœ… Created test project: {project_id}") + print(f"Created test project: {project_id}") # Test 1: List tag bindings (this should work) - print("๐Ÿท๏ธ Testing LIST_TAG_BINDINGS") + print(" Testing LIST_TAG_BINDINGS") try: initial_tag_bindings = projects.list_tag_bindings(project_id) assert isinstance(initial_tag_bindings, list), "Should return a list" - print(f"โœ… list_tag_bindings works: {len(initial_tag_bindings)} bindings") + print(f"list_tag_bindings works: {len(initial_tag_bindings)} bindings") list_tag_bindings_available = True except Exception as e: - print(f"โŒ list_tag_bindings not available: {e}") + print(f" list_tag_bindings not available: {e}") list_tag_bindings_available = False # Test 2: List effective tag bindings - print("๐Ÿท๏ธ Testing LIST_EFFECTIVE_TAG_BINDINGS") + print(" Testing LIST_EFFECTIVE_TAG_BINDINGS") try: effective_bindings = projects.list_effective_tag_bindings(project_id) assert isinstance(effective_bindings, list), "Should return a list" print( - f"โœ… list_effective_tag_bindings works: {len(effective_bindings)} bindings" + f"list_effective_tag_bindings works: {len(effective_bindings)} bindings" ) effective_tag_bindings_available = True except Exception as e: - print(f"โŒ list_effective_tag_bindings not available: {e}") + print(f" list_effective_tag_bindings not available: {e}") print(" This feature may require a higher HCP Terraform plan") effective_tag_bindings_available = False # Test 3: Add tag bindings (if basic listing works) if list_tag_bindings_available: - print("๐Ÿท๏ธ Testing ADD_TAG_BINDINGS") + print(" Testing ADD_TAG_BINDINGS") try: test_tags = [ TagBinding(key="environment", value="testing"), @@ -637,9 +634,7 @@ def test_project_tag_bindings_integration(integration_client): assert len(added_bindings) == len(test_tags), ( "Should return all added tags" ) - print( - f"โœ… add_tag_bindings works: added {len(added_bindings)} bindings" - ) + print(f"add_tag_bindings works: added {len(added_bindings)} bindings") # Verify tags were actually added current_bindings = projects.list_tag_bindings(project_id) @@ -648,12 +643,12 @@ def test_project_tag_bindings_integration(integration_client): assert tag.key in added_keys, ( f"Tag {tag.key} not found after adding" ) - print(f"โœ… Verified tags added: {len(current_bindings)} total bindings") + print(f"Verified tags added: {len(current_bindings)} total bindings") add_tag_bindings_available = True # Test 4: Delete tag bindings - print("๐Ÿท๏ธ Testing DELETE_TAG_BINDINGS") + print(" Testing DELETE_TAG_BINDINGS") try: result = projects.delete_tag_bindings(project_id) assert result is None, "Delete should return None" @@ -661,15 +656,15 @@ def test_project_tag_bindings_integration(integration_client): # Verify deletion final_bindings = projects.list_tag_bindings(project_id) print( - f"โœ… delete_tag_bindings works: {len(final_bindings)} bindings remain" + f"delete_tag_bindings works: {len(final_bindings)} bindings remain" ) delete_tag_bindings_available = True except Exception as e: - print(f"โŒ delete_tag_bindings not available: {e}") + print(f" delete_tag_bindings not available: {e}") delete_tag_bindings_available = False except Exception as e: - print(f"โŒ add_tag_bindings not available: {e}") + print(f" add_tag_bindings not available: {e}") print(" This feature may require a higher HCP Terraform plan") add_tag_bindings_available = False delete_tag_bindings_available = False @@ -678,7 +673,7 @@ def test_project_tag_bindings_integration(integration_client): delete_tag_bindings_available = False # Summary - print("\n๐Ÿ“Š Project Tag Bindings API Availability Summary:") + print("\n Project Tag Bindings API Availability Summary:") features = [ ("list_tag_bindings", list_tag_bindings_available), ("list_effective_tag_bindings", effective_tag_bindings_available), @@ -687,20 +682,20 @@ def test_project_tag_bindings_integration(integration_client): ] for feature_name, available in features: - status = "โœ… Available" if available else "โŒ Not Available" + status = "Available" if available else " Not Available" print(f" {feature_name}: {status}") available_count = sum(available for _, available in features) print( - f"\n๐ŸŽฏ {available_count}/4 tag binding features are available in this HCP Terraform organization" + f"\n {available_count}/4 tag binding features are available in this HCP Terraform organization" ) if available_count == 4: - print("๐ŸŽ‰ All project tag binding operations work perfectly!") + print(" All project tag binding operations work perfectly!") elif available_count > 0: - print("โœ… Partial functionality available - basic operations work!") + print("Partial functionality available - basic operations work!") else: - print("โš ๏ธ Tag binding features may require a higher HCP Terraform plan") + print(" Tag binding features may require a higher HCP Terraform plan") except Exception as e: pytest.fail( @@ -714,10 +709,10 @@ def test_project_tag_bindings_integration(integration_client): try: print(f"๐Ÿงน Cleaning up test project: {project_id}") projects.delete(project_id) - print("โœ… Test project deleted successfully") + print("Test project deleted successfully") except Exception as cleanup_error: print( - f"โš ๏ธ Warning: Failed to clean up test project {project_id}: {cleanup_error}" + f" Warning: Failed to clean up test project {project_id}: {cleanup_error}" ) @@ -732,10 +727,10 @@ def test_project_tag_bindings_error_scenarios(integration_client): """ projects, org = integration_client - print("๐Ÿท๏ธ Testing tag binding error scenarios") + print(" Testing tag binding error scenarios") # Test invalid project ID validation - print("๐Ÿšซ Testing invalid project ID scenarios") + print(" Testing invalid project ID scenarios") invalid_project_ids = ["", "x", "invalid-id", None] @@ -746,41 +741,42 @@ def test_project_tag_bindings_error_scenarios(integration_client): try: projects.list_tag_bindings(invalid_id) pytest.fail( - f"Should have raised ValueError for invalid project ID: {invalid_id}" + f"Should have raised ValueError or NotFound for invalid project ID: {invalid_id}" ) - except ValueError as e: - print(f"โœ… Correctly rejected invalid project ID '{invalid_id}': {e}") - assert "Project ID is required and must be valid" in str(e) + except (ValueError, NotFound) as e: + print(f"Correctly rejected invalid project ID '{invalid_id}': {e}") + if isinstance(e, ValueError): + assert "Project ID is required and must be valid" in str(e) try: projects.list_effective_tag_bindings(invalid_id) pytest.fail( - f"Should have raised ValueError for invalid project ID: {invalid_id}" + f"Should have raised ValueError or NotFound for invalid project ID: {invalid_id}" ) - except ValueError as e: - print(f"โœ… Correctly rejected invalid project ID '{invalid_id}': {e}") + except (ValueError, NotFound) as e: + print(f"Correctly rejected invalid project ID '{invalid_id}': {e}") try: projects.delete_tag_bindings(invalid_id) pytest.fail( - f"Should have raised ValueError for invalid project ID: {invalid_id}" + f"Should have raised ValueError or NotFound for invalid project ID: {invalid_id}" ) - except ValueError as e: - print(f"โœ… Correctly rejected invalid project ID '{invalid_id}': {e}") + except (ValueError, NotFound) as e: + print(f"Correctly rejected invalid project ID '{invalid_id}': {e}") # Test empty tag binding list - print("๐Ÿšซ Testing empty tag binding list") + print(" Testing empty tag binding list") try: fake_project_id = "prj-fakefakefake123" empty_options = ProjectAddTagBindingsOptions(tag_bindings=[]) projects.add_tag_bindings(fake_project_id, empty_options) pytest.fail("Should have raised ValueError for empty tag binding list") except ValueError as e: - print(f"โœ… Correctly rejected empty tag binding list: {e}") + print(f"Correctly rejected empty tag binding list: {e}") assert "At least one tag binding is required" in str(e) # Test non-existent project operations - print("๐Ÿšซ Testing operations on non-existent project") + print(" Testing operations on non-existent project") fake_project_id = "prj-doesnotexist123" # These should raise HTTP errors (404) from the API @@ -797,7 +793,7 @@ def test_project_tag_bindings_error_scenarios(integration_client): pytest.fail(f"{operation_name} should have failed for non-existent project") except Exception as e: print( - f"โœ… {operation_name} correctly failed for non-existent project: {type(e).__name__}" + f"{operation_name} correctly failed for non-existent project: {type(e).__name__}" ) # Should be some kind of HTTP error (404, not found, etc.) assert ( @@ -814,7 +810,7 @@ def test_project_tag_bindings_error_scenarios(integration_client): pytest.fail("add_tag_bindings should have failed for non-existent project") except Exception as e: print( - f"โœ… add_tag_bindings correctly failed for non-existent project: {type(e).__name__}" + f"add_tag_bindings correctly failed for non-existent project: {type(e).__name__}" ) assert ( "404" in str(e) @@ -822,7 +818,7 @@ def test_project_tag_bindings_error_scenarios(integration_client): or "does not exist" in str(e).lower() ) - print("โœ… All tag binding error scenarios tested successfully") + print("All tag binding error scenarios tested successfully") if __name__ == "__main__": @@ -839,12 +835,12 @@ def test_project_tag_bindings_error_scenarios(integration_client): org = os.environ.get("TFE_ORG") if not token or not org: - print("โŒ Please set TFE_TOKEN and TFE_ORG environment variables") + print(" Please set TFE_TOKEN and TFE_ORG environment variables") print(" export TFE_TOKEN='your-hcp-terraform-token'") print(" export TFE_ORG='your-organization-name'") sys.exit(1) - print("๐Ÿงช Running integration tests directly...") + print(" Running integration tests directly...") print( " For full pytest features, use: pytest examples/integration_test_example.py -v -s" ) diff --git a/examples/registry_module.py b/examples/registry_module.py index 0be3b99..12bcb46 100644 --- a/examples/registry_module.py +++ b/examples/registry_module.py @@ -83,17 +83,17 @@ def main(): organization_name=organization_name, registry_name=RegistryName.PRIVATE ) modules = list(client.registry_modules.list(organization_name, options)) - print(f" โœ“ Found {len(modules)} registry modules") + print(f" Found {len(modules)} registry modules") for i, module in enumerate(modules[:3], 1): print(f" {i}. {module.name}/{module.provider} (ID: {module.id})") except NotFound: print( - " โœ“ No modules found (organization may not exist or no private modules available)" + " No modules found (organization may not exist or no private modules available)" ) except Exception as e: - print(f" โœ— Error: {e}") + print(f" Error: {e}") # ===================================================== # TEST 2: CREATE REGISTRY MODULE WITH VCS CONNECTION [TESTED - COMMENTED] @@ -131,13 +131,13 @@ def main(): vcs_create_options ) print( - f" โœ“ Created VCS module: {created_module.name}/{created_module.provider}" + f" Created VCS module: {created_module.name}/{created_module.provider}" ) print(f" ID: {created_module.id}") print(f" Status: {created_module.status}") except Exception as e: - print(f" โœ— Error: {e}") + print(f" Error: {e}") # ===================================================== # TEST 3: READ REGISTRY MODULE [TESTED - COMMENTED] @@ -153,12 +153,12 @@ def main(): ) read_module = client.registry_modules.read(module_id) - print(f" โœ“ Read module: {read_module.name}") + print(f" Read module: {read_module.name}") print(f" Status: {read_module.status}") print(f" Created: {read_module.created_at}") except Exception as e: - print(f" โœ— Error: {e}") + print(f" Error: {e}") # ===================================================== # TEST 4: LIST COMMITS [TESTED - COMMENTED] @@ -175,10 +175,10 @@ def main(): commits = client.registry_modules.list_commits(module_id) commit_list = list(commits.items) if hasattr(commits, "items") else [] - print(f" โœ“ Found {len(commit_list)} commits") + print(f" Found {len(commit_list)} commits") except Exception as e: - print(f" โœ— Error: {e}") + print(f" Error: {e}") # ===================================================== # TEST 5: CREATE VERSION [TESTED - COMMENTED] @@ -200,11 +200,11 @@ def main(): version = client.registry_modules.create_version(module_id, version_options) created_version = version.version - print(f" โœ“ Created version: {version.version}") + print(f" Created version: {version.version}") print(f" Status: {version.status}") except Exception as e: - print(f" โœ— Error: {e}") + print(f" Error: {e}") # ===================================================== # TEST 6: READ VERSION [TESTED - COMMENTED] @@ -222,12 +222,12 @@ def main(): read_version = client.registry_modules.read_version( module_id, created_version ) - print(f" โœ“ Read version: {read_version.version}") + print(f" Read version: {read_version.version}") print(f" Status: {read_version.status}") print(f" ID: {read_version.id}") except Exception as e: - print(f" โœ— Error: {e}") + print(f" Error: {e}") # ===================================================== # TEST 7: READ PUBLIC TERRAFORM REGISTRY MODULE @@ -247,14 +247,14 @@ def main(): public_module = client.registry_modules.read_terraform_registry_module( public_module_id, version ) - print(f" โœ“ Read public module: {public_module.name}") + print(f" Read public module: {public_module.name}") print(f" Version: {version}") print(f" Downloads: {getattr(public_module, 'downloads', 'N/A')}") print(f" Verified: {getattr(public_module, 'verified', 'N/A')}") print(f" Source: {getattr(public_module, 'source', 'N/A')}") except Exception as e: - print(f" โœ— Error: {e}") + print(f" Error: {e}") # ===================================================== # TEST 8: CREATE SIMPLE REGISTRY MODULE (Non-VCS) @@ -274,7 +274,7 @@ def main(): organization_name, create_options ) print( - f" โœ“ Created simple module: {created_simple_module.name}/{created_simple_module.provider}" + f" Created simple module: {created_simple_module.name}/{created_simple_module.provider}" ) print(f" ID: {created_simple_module.id}") print( @@ -286,7 +286,7 @@ def main(): created_module = created_simple_module except Exception as e: - print(f" โœ— Error: {e}") + print(f" Error: {e}") # ===================================================== # TEST 8A: LIST VERSIONS @@ -303,13 +303,13 @@ def main(): versions = client.registry_modules.list_versions(module_id) versions_list = list(versions) if hasattr(versions, "__iter__") else [] - print(f" โœ“ Found {len(versions_list)} versions") + print(f" Found {len(versions_list)} versions") for i, version in enumerate(versions_list[:3], 1): print(f" {i}. Version {version.version} (Status: {version.status})") except Exception as e: - print(f" โœ— Error: {e}") + print(f" Error: {e}") # ===================================================== # TEST 8B: UPDATE MODULE @@ -335,12 +335,12 @@ def main(): ) updated_module = client.registry_modules.update(module_id, update_options) - print(f" โœ“ Updated module: {updated_module.name}") + print(f" Updated module: {updated_module.name}") print(f" No Code: {updated_module.no_code}") print(f" Status: {updated_module.status}") except Exception as e: - print(f" โš  Update may not be supported: {e}") + print(f" Update may not be supported: {e}") # ===================================================== # TEST 9: CREATE MODULE FOR UPLOAD TESTING @@ -358,12 +358,12 @@ def main(): created_module = client.registry_modules.create( organization_name, create_options ) - print(f" โœ“ Created test module: {created_module.name}") + print(f" Created test module: {created_module.name}") print(f" Provider: {created_module.provider}") print(f" Status: {created_module.status}") except Exception as e: - print(f" โœ— Error creating module: {e}") + print(f" Error creating module: {e}") return # ===================================================== @@ -387,7 +387,7 @@ def main(): version = client.registry_modules.create_version(module_id, version_options) created_version = version.version version_object = version - print(f" โœ“ Created version: {created_version}") + print(f" Created version: {created_version}") print(f" Status: {version.status}") # Check if upload URL is available @@ -397,7 +397,7 @@ def main(): print(f" Upload URL available: {'Yes' if upload_url else 'No'}") except Exception as e: - print(f" โœ— Error creating version: {e}") + print(f" Error creating version: {e}") # ===================================================== # TEST 11: UPLOAD_TAR_GZIP FUNCTION TESTING @@ -476,7 +476,7 @@ def main(): if upload_url: client.registry_modules.upload_tar_gzip(upload_url, tar_buffer) print( - " โœ“ Successfully uploaded tar.gz content using upload_tar_gzip()" + " Successfully uploaded tar.gz content using upload_tar_gzip()" ) # Wait for processing @@ -496,16 +496,16 @@ def main(): if updated_module.status.value != "pending": print( - f" โœ… SUCCESS: Module status changed from PENDING to {updated_module.status}" + f" SUCCESS: Module status changed from PENDING to {updated_module.status}" ) else: print(" โณ Module still processing - may take longer") else: - print(" โš  No upload URL available in version links") + print(" No upload URL available in version links") except Exception as e: - print(f" โœ— Error in upload_tar_gzip test: {e}") + print(f" Error in upload_tar_gzip test: {e}") # ===================================================== # TEST 12: UPLOAD FUNCTION TESTING @@ -597,7 +597,7 @@ def main(): # Try the upload function try: client.registry_modules.upload(version_object, temp_dir) - print(" โœ“ Successfully uploaded using upload() function") + print(" Successfully uploaded using upload() function") # Wait and check status print(" Waiting 5 seconds for processing...") @@ -614,7 +614,7 @@ def main(): print(f" Updated Module Status: {updated_module.status}") except NotImplementedError as nie: - print(f" โš  upload() function not fully implemented: {nie}") + print(f" upload() function not fully implemented: {nie}") print(" This is expected - the function is a placeholder") # Fallback to upload_tar_gzip @@ -637,17 +637,17 @@ def main(): tar_buffer.seek(0) client.registry_modules.upload_tar_gzip(upload_url, tar_buffer) print( - " โœ“ Successfully uploaded using upload_tar_gzip() as fallback" + " Successfully uploaded using upload_tar_gzip() as fallback" ) except Exception as upload_error: - print(f" โœ— upload() function error: {upload_error}") + print(f" upload() function error: {upload_error}") else: - print(" โš  No upload URL available - cannot test upload function") + print(" No upload URL available - cannot test upload function") except Exception as e: - print(f" โœ— Error in upload() test: {e}") + print(f" Error in upload() test: {e}") # ===================================================== # TEST 13: DELETE VERSION @@ -670,7 +670,7 @@ def main(): test_module_for_deletion = client.registry_modules.create( organization_name, delete_create_options ) - print(f" โœ“ Created test module: {test_module_for_deletion.name}") + print(f" Created test module: {test_module_for_deletion.name}") # Create a version for deletion testing module_id = RegistryModuleID( @@ -684,7 +684,7 @@ def main(): version = client.registry_modules.create_version(module_id, version_options) test_version_for_deletion = version.version - print(f" โœ“ Created test version: {test_version_for_deletion}") + print(f" Created test version: {test_version_for_deletion}") # Now test version deletion print(f" Testing deletion of version {test_version_for_deletion}...") @@ -692,7 +692,7 @@ def main(): # Delete the version client.registry_modules.delete_version(module_id, test_version_for_deletion) print( - f" โœ“ Successfully called delete_version() for version: {test_version_for_deletion}" + f" Successfully called delete_version() for version: {test_version_for_deletion}" ) # Verify deletion by trying to read it @@ -706,13 +706,13 @@ def main(): version=test_version_for_deletion, ) print( - " โš  Warning: Version still exists after deletion (may take time to process)" + " Warning: Version still exists after deletion (may take time to process)" ) except Exception: - print(" โœ“ Confirmed: Version no longer exists") + print(" Confirmed: Version no longer exists") except Exception as e: - print(f" โœ— Error in delete_version test: {e}") + print(f" Error in delete_version test: {e}") # ===================================================== # TEST 14: DELETE BY NAME @@ -737,23 +737,23 @@ def main(): # Delete the module client.registry_modules.delete_by_name(module_id) print( - f" โœ“ Successfully called delete_by_name() for module: {test_module_for_deletion.name}" + f" Successfully called delete_by_name() for module: {test_module_for_deletion.name}" ) # Verify deletion try: client.registry_modules.read(module_id) print( - " โš  Warning: Module still exists after deletion (may take time to process)" + " Warning: Module still exists after deletion (may take time to process)" ) except Exception: - print(" โœ“ Confirmed: Module no longer exists") + print(" Confirmed: Module no longer exists") except Exception as read_error: print(f" Module not found: {read_error}") except Exception as e: - print(f" โœ— Error in delete_by_name test: {e}") + print(f" Error in delete_by_name test: {e}") # ===================================================== # TEST 15: DELETE (Alternative delete method) @@ -768,7 +768,7 @@ def main(): print(f" Testing delete with non-existent module: {test_name}") client.registry_modules.delete(organization_name, test_name) print( - " โœ“ Delete function executed successfully (may return 404 for non-existent module)" + " Delete function executed successfully (may return 404 for non-existent module)" ) except Exception as e: @@ -794,7 +794,7 @@ def main(): test_provider_module = client.registry_modules.create( organization_name, delete_provider_options ) - print(f" โœ“ Created test module with provider: {test_provider_name}") + print(f" Created test module with provider: {test_provider_name}") # Now test delete_provider function test_provider_module_id = RegistryModuleID( @@ -807,20 +807,20 @@ def main(): print(f" Testing delete_provider() for provider: {test_provider_name}") client.registry_modules.delete_provider(test_provider_module_id) print( - f" โœ“ Successfully called delete_provider() for provider: {test_provider_name}" + f" Successfully called delete_provider() for provider: {test_provider_name}" ) # Verify deletion by trying to read the module try: client.registry_modules.read(test_provider_module_id) print( - " โš  Warning: Module still exists after provider deletion (may take time to process)" + " Warning: Module still exists after provider deletion (may take time to process)" ) except Exception: - print(" โœ“ Confirmed: All modules for provider have been deleted") + print(" Confirmed: All modules for provider have been deleted") except Exception as e: - print(f" โœ— Error in delete_provider test: {e}") + print(f" Error in delete_provider test: {e}") # ===================================================== # TESTING SUMMARY @@ -829,26 +829,26 @@ def main(): print("REGISTRY MODULE TESTING COMPLETED!") print("=" * 80) print("Summary of ALL 15 Functions Tested:") - print("โœ“ list() - List registry modules in organization") - print("โœ“ create_with_vcs_connection() - Create module with VCS connection") - print("โœ“ read() - Read module details") - print("โœ“ list_commits() - List VCS commits for module") - print("โœ“ create_version() - Create new module version") - print("โœ“ read_version() - Read specific version details") - print("โœ“ read_terraform_registry_module() - Read public registry module") - print("โœ“ create() - Create simple module") - print("โœ“ list_versions() - List all versions of a module") - print("โœ“ update() - Update module settings") - print("โœ“ upload_tar_gzip() - Upload tar.gz archive to upload URL") - print("โœ“ upload() - Upload from local directory path (placeholder)") - print("โœ“ delete_version() - Delete a specific version") - print("โœ“ delete_by_name() - Delete entire module by name") - print("โœ“ delete() - Delete module by organization and name") - print("โœ“ delete_provider() - Delete all modules for a provider") + print(" list() - List registry modules in organization") + print(" create_with_vcs_connection() - Create module with VCS connection") + print(" read() - Read module details") + print(" list_commits() - List VCS commits for module") + print(" create_version() - Create new module version") + print(" read_version() - Read specific version details") + print(" read_terraform_registry_module() - Read public registry module") + print(" create() - Create simple module") + print(" list_versions() - List all versions of a module") + print(" update() - Update module settings") + print(" upload_tar_gzip() - Upload tar.gz archive to upload URL") + print(" upload() - Upload from local directory path (placeholder)") + print(" delete_version() - Delete a specific version") + print(" delete_by_name() - Delete entire module by name") + print(" delete() - Delete module by organization and name") + print(" delete_provider() - Delete all modules for a provider") if created_module: - print(f"โœ“ Created test module: {created_module.name}") + print(f" Created test module: {created_module.name}") print("=" * 80) - print("๐ŸŽ‰ ALL 15 REGISTRY MODULE FUNCTIONS HAVE BEEN TESTED!") + print(" ALL 15 REGISTRY MODULE FUNCTIONS HAVE BEEN TESTED!") print("=" * 80) diff --git a/examples/registry_provider.py b/examples/registry_provider.py index bcd3e32..cd03dea 100644 --- a/examples/registry_provider.py +++ b/examples/registry_provider.py @@ -49,7 +49,7 @@ def test_list_simple(): try: providers = list(client.registry_providers.list(org)) - print(f"โœ“ Found {len(providers)} providers in organization '{org}'") + print(f" Found {len(providers)} providers in organization '{org}'") for i, provider in enumerate(providers[:5], 1): print(f" {i}. {provider.name}") @@ -62,7 +62,7 @@ def test_list_simple(): return providers except Exception as e: - print(f"โœ— Error: {e}") + print(f" Error: {e}") return [] @@ -79,7 +79,7 @@ def test_list_with_options(): ) providers = list(client.registry_providers.list(org, options)) - print(f"โœ“ Found {len(providers)} providers matching search 'test'") + print(f" Found {len(providers)} providers matching search 'test'") # Test with include include_options = RegistryProviderListOptions( @@ -87,12 +87,12 @@ def test_list_with_options(): ) detailed_providers = list(client.registry_providers.list(org, include_options)) - print(f"โœ“ Found {len(detailed_providers)} providers with version details") + print(f" Found {len(detailed_providers)} providers with version details") return providers except Exception as e: - print(f"โœ— Error: {e}") + print(f" Error: {e}") return [] @@ -112,7 +112,7 @@ def test_create_private(): ) provider = client.registry_providers.create(org, options) - print(f"โœ“ Created private provider: {provider.name}") + print(f" Created private provider: {provider.name}") print(f" ID: {provider.id}") print(f" Namespace: {provider.namespace}") print(f" Registry: {provider.registry_name.value}") @@ -121,7 +121,7 @@ def test_create_private(): return provider except Exception as e: - print(f"โœ— Error creating private provider: {e}") + print(f" Error creating private provider: {e}") return None @@ -142,7 +142,7 @@ def test_create_public(): ) provider = client.registry_providers.create(org, options) - print(f"โœ“ Created public provider: {provider.name}") + print(f" Created public provider: {provider.name}") print(f" ID: {provider.id}") print(f" Namespace: {provider.namespace}") print(f" Registry: {provider.registry_name.value}") @@ -151,7 +151,7 @@ def test_create_public(): return provider except Exception as e: - print(f"โœ— Error creating public provider: {e}") + print(f" Error creating public provider: {e}") return None @@ -162,7 +162,7 @@ def test_read_with_id(provider_data): client, org = get_client_and_org() if not provider_data: - print("โš ๏ธ No provider data provided") + print(" No provider data provided") return None try: @@ -175,7 +175,7 @@ def test_read_with_id(provider_data): # Basic read provider = client.registry_providers.read(provider_id) - print(f"โœ“ Read provider: {provider.name}") + print(f" Read provider: {provider.name}") print(f" ID: {provider.id}") print(f" Namespace: {provider.namespace}") print(f" Registry: {provider.registry_name.value}") @@ -189,7 +189,7 @@ def test_read_with_id(provider_data): ) detailed_provider = client.registry_providers.read(provider_id, options) - print(f"โœ“ Read with options: {detailed_provider.name}") + print(f" Read with options: {detailed_provider.name}") if detailed_provider.registry_provider_versions: print( @@ -201,7 +201,7 @@ def test_read_with_id(provider_data): return provider except Exception as e: - print(f"โœ— Error reading provider: {e}") + print(f" Error reading provider: {e}") return None @@ -212,7 +212,7 @@ def test_delete_by_id(provider_data): client, org = get_client_and_org() if not provider_data: - print("โš ๏ธ No provider data provided") + print(" No provider data provided") return False try: @@ -225,11 +225,11 @@ def test_delete_by_id(provider_data): # Verify provider exists provider = client.registry_providers.read(provider_id) - print(f"โœ“ Found provider to delete: {provider.name}") + print(f" Found provider to delete: {provider.name}") # Delete the provider client.registry_providers.delete(provider_id) - print("โœ“ Successfully called delete() for provider") + print(" Successfully called delete() for provider") # Verify deletion (optional - may take time) import time @@ -238,20 +238,20 @@ def test_delete_by_id(provider_data): try: client.registry_providers.read(provider_id) - print("โš ๏ธ Provider still exists (deletion may take time)") + print(" Provider still exists (deletion may take time)") except Exception: - print("โœ“ Provider successfully deleted") + print(" Provider successfully deleted") return True except Exception as e: - print(f"โœ— Error deleting provider: {e}") + print(f" Error deleting provider: {e}") return False def main(): """Run all tests in sequence.""" - print("๐Ÿš€ REGISTRY PROVIDER INDIVIDUAL TESTS") + print(" REGISTRY PROVIDER INDIVIDUAL TESTS") print("=" * 50) # Test 1: List providers @@ -262,9 +262,9 @@ def main(): test_list_with_options() print() - # โš ๏ธ WARNING: Uncomment the following tests to create/delete providers - print("โš ๏ธ WARNING: Creation and deletion tests are commented out for safety") - print("โš ๏ธ Uncomment them in the code to test creation and deletion") + # WARNING: Uncomment the following tests to create/delete providers + print(" WARNING: Creation and deletion tests are commented out for safety") + print(" Uncomment them in the code to test creation and deletion") print() # UNCOMMENT TO TEST CREATION: @@ -297,8 +297,8 @@ def main(): test_read_with_id(existing_provider) print() - print("โœ… Individual tests completed!") - print("๐Ÿ’ก To test creation/deletion, uncomment the relevant sections in the code") + print("Individual tests completed!") + print(" To test creation/deletion, uncomment the relevant sections in the code") if __name__ == "__main__": diff --git a/examples/reserved_tag_key.py b/examples/reserved_tag_key.py index 8eeb5e7..75b9fb9 100644 --- a/examples/reserved_tag_key.py +++ b/examples/reserved_tag_key.py @@ -36,11 +36,11 @@ def main(): # Validate environment variables if not TFE_TOKEN: - print("โŒ Error: TFE_TOKEN environment variable is required") + print(" Error: TFE_TOKEN environment variable is required") sys.exit(1) if not TFE_ORG: - print("โŒ Error: TFE_ORG environment variable is required") + print(" Error: TFE_ORG environment variable is required") sys.exit(1) # Initialize the TFE client @@ -54,7 +54,7 @@ def main(): # 1. List existing reserved tag keys print("\n1. Listing reserved tag keys...") reserved_tag_keys = client.reserved_tag_key.list(TFE_ORG) - print(f"โœ… Found {len(reserved_tag_keys.items)} reserved tag keys:") + print(f"Found {len(reserved_tag_keys.items)} reserved tag keys:") for rtk in reserved_tag_keys.items: print( f" - ID: {rtk.id}, Key: {rtk.key}, Disable Overrides: {rtk.disable_overrides}" @@ -67,7 +67,7 @@ def main(): ) new_rtk = client.reserved_tag_key.create(TFE_ORG, create_options) - print(f"โœ… Created reserved tag key: {new_rtk.id} - {new_rtk.key}") + print(f"Created reserved tag key: {new_rtk.id} - {new_rtk.key}") print(f" Disable Overrides: {new_rtk.disable_overrides}") # 3. Update the reserved tag key @@ -77,48 +77,46 @@ def main(): ) updated_rtk = client.reserved_tag_key.update(new_rtk.id, update_options) - print(f"โœ… Updated reserved tag key: {updated_rtk.id} - {updated_rtk.key}") + print(f"Updated reserved tag key: {updated_rtk.id} - {updated_rtk.key}") print(f" Disable Overrides: {updated_rtk.disable_overrides}") # 4. Delete the reserved tag key print("\n4. Deleting the reserved tag key...") client.reserved_tag_key.delete(new_rtk.id) - print(f"โœ… Deleted reserved tag key: {new_rtk.id}") + print(f"Deleted reserved tag key: {new_rtk.id}") # 5. Verify deletion by listing again print("\n5. Verifying deletion...") reserved_tag_keys_after = client.reserved_tag_key.list(TFE_ORG) - print( - f"โœ… Reserved tag keys after deletion: {len(reserved_tag_keys_after.items)}" - ) + print(f"Reserved tag keys after deletion: {len(reserved_tag_keys_after.items)}") # 6. Demonstrate pagination with options print("\n6. Demonstrating pagination options...") list_options = ReservedTagKeyListOptions(page_size=5, page_number=1) paginated_rtks = client.reserved_tag_key.list(TFE_ORG, list_options) - print(f"โœ… Page 1 with page size 5: {len(paginated_rtks.items)} keys") + print(f"Page 1 with page size 5: {len(paginated_rtks.items)} keys") print(f" Total pages: {paginated_rtks.total_pages}") print(f" Total count: {paginated_rtks.total_count}") - print("\n๐ŸŽ‰ Reserved Tag Keys API example completed successfully!") + print("\n Reserved Tag Keys API example completed successfully!") except NotImplementedError as e: - print(f"\nโš ๏ธ Note: {e}") + print(f"\n Note: {e}") print("This is expected - the read operation is not supported by the API.") except TFEError as e: - print(f"\nโŒ TFE API Error: {e}") + print(f"\n TFE API Error: {e}") if hasattr(e, "status"): if e.status == 403: - print("๐Ÿ’ก Permission denied - check token permissions") + print(" Permission denied - check token permissions") elif e.status == 401: - print("๐Ÿ’ก Authentication failed - check token validity") + print(" Authentication failed - check token validity") elif e.status == 422: - print("๐Ÿ’ก Validation error - check reserved tag key format") + print(" Validation error - check reserved tag key format") sys.exit(1) except Exception as e: - print(f"\nโŒ Unexpected error: {e}") + print(f"\n Unexpected error: {e}") sys.exit(1) diff --git a/examples/run_task.py b/examples/run_task.py index 102874d..e62a7da 100644 --- a/examples/run_task.py +++ b/examples/run_task.py @@ -112,7 +112,7 @@ def main(): if count >= args.page_size * 2: # Safety limit based on page size break - print(f"โœ“ Found {len(run_task_list)} run tasks") + print(f" Found {len(run_task_list)} run tasks") print() if not run_task_list: @@ -128,7 +128,7 @@ def main(): print(f" Description: {task.description}") print() except Exception as e: - print(f"โœ— Error listing run tasks: {e}") + print(f" Error listing run tasks: {e}") return # 2) Create a new run task if requested @@ -149,7 +149,7 @@ def main(): print(f"Creating run task '{task_name}' in organization '{args.org}'...") run_task = client.run_tasks.create(args.org, create_options) - print("โœ“ Successfully created run task!") + print(" Successfully created run task!") print(f" Name: {run_task.name}") print(f" ID: {run_task.id}") print(f" URL: {run_task.url}") @@ -161,7 +161,7 @@ def main(): args.task_id = run_task.id # Use the created task for other operations except Exception as e: - print(f"โœ— Error creating run task: {e}") + print(f" Error creating run task: {e}") return # 3) Read run task details if task ID is provided @@ -180,7 +180,7 @@ def main(): run_task = client.run_tasks.read(args.task_id) print("Reading run task details...") - print("โœ“ Successfully read run task!") + print(" Successfully read run task!") print(f" Name: {run_task.name}") print(f" ID: {run_task.id}") print(f" URL: {run_task.url}") @@ -199,7 +199,7 @@ def main(): print() except Exception as e: - print(f"โœ— Error reading run task: {e}") + print(f" Error reading run task: {e}") return # 4) Update run task if requested @@ -214,14 +214,14 @@ def main(): ) print(f"Updating run task '{args.task_id}'...") updated_task = client.run_tasks.update(args.task_id, update_options) - print("โœ“ Successfully updated run task!") + print(" Successfully updated run task!") print(f" Name: {updated_task.name}") print(f" Description: {updated_task.description}") print(f" URL: {updated_task.url}") print(f" Enabled: {updated_task.enabled}") print() except Exception as e: - print(f"โœ— Error updating run task: {e}") + print(f" Error updating run task: {e}") return # 5) Delete run task if requested (should be last operation) @@ -230,10 +230,10 @@ def main(): try: print(f"Deleting run task '{args.task_id}'...") client.run_tasks.delete(args.task_id) - print(f"โœ“ Successfully deleted run task: {args.task_id}") + print(f" Successfully deleted run task: {args.task_id}") print() except Exception as e: - print(f"โœ— Error deleting run task: {e}") + print(f" Error deleting run task: {e}") return diff --git a/examples/run_trigger.py b/examples/run_trigger.py index c651210..35dba80 100644 --- a/examples/run_trigger.py +++ b/examples/run_trigger.py @@ -140,7 +140,7 @@ def main(): if count >= args.page_size * 2: # Safety limit based on page size break - print(f"โœ“ Found {len(run_trigger_list)} run triggers") + print(f" Found {len(run_trigger_list)} run triggers") print() if not run_trigger_list: @@ -158,7 +158,7 @@ def main(): print(f" Target Workspace ID: {trigger.workspace.id}") print() except Exception as e: - print(f"โœ— Error listing run triggers: {e}") + print(f" Error listing run triggers: {e}") return # 2) Create a new run trigger if requested @@ -178,7 +178,7 @@ def main(): f"Creating run trigger from workspace '{args.source_workspace_id}' to '{args.workspace_id}'..." ) run_trigger = client.run_triggers.create(args.workspace_id, create_options) - print("โœ“ Successfully created run trigger!") + print(" Successfully created run trigger!") print(f" ID: {run_trigger.id}") print(f" Source: {run_trigger.sourceable_name}") print(f" Target: {run_trigger.workspace_name}") @@ -198,12 +198,10 @@ def main(): run_trigger.id ) # Use the created trigger for other operations except Exception as e: - print(f"โœ— Error creating run trigger: {e}") + print(f" Error creating run trigger: {e}") return elif args.create: - print( - "โœ— Error: --create requires both --workspace-id and --source-workspace-id" - ) + print(" Error: --create requires both --workspace-id and --source-workspace-id") return # 3) Read run trigger details if trigger ID is provided @@ -213,7 +211,7 @@ def main(): print("Reading run trigger details...") run_trigger = client.run_triggers.read(args.trigger_id) - print("โœ“ Successfully read run trigger!") + print(" Successfully read run trigger!") print(f" ID: {run_trigger.id}") print(f" Type: {run_trigger.type}") print(f" Source: {run_trigger.sourceable_name}") @@ -243,7 +241,7 @@ def main(): print() except Exception as e: - print(f"โœ— Error reading run trigger: {e}") + print(f" Error reading run trigger: {e}") return # 4) Delete run trigger if requested (should be last operation) @@ -252,10 +250,10 @@ def main(): try: print(f"Deleting run trigger '{args.trigger_id}'...") client.run_triggers.delete(args.trigger_id) - print(f"โœ“ Successfully deleted run trigger: {args.trigger_id}") + print(f" Successfully deleted run trigger: {args.trigger_id}") print() except Exception as e: - print(f"โœ— Error deleting run trigger: {e}") + print(f" Error deleting run trigger: {e}") return diff --git a/examples/ssh_keys.py b/examples/ssh_keys.py index 7d2efa3..43e4a8a 100644 --- a/examples/ssh_keys.py +++ b/examples/ssh_keys.py @@ -9,9 +9,9 @@ 5. Delete an SSH key IMPORTANT: SSH Keys API has special authentication requirements: -- โŒ CANNOT use Organization Tokens (AT-*) -- โœ… MUST use User Tokens or Team Tokens -- โœ… MUST have 'manage VCS settings' permission +- CANNOT use Organization Tokens (AT-*) +- MUST use User Tokens or Team Tokens +- MUST have 'manage VCS settings' permission Before running this script: 1. Create a User Token in Terraform Cloud: @@ -42,28 +42,28 @@ def check_token_type(token): """Check and validate token type for SSH Keys API.""" - print("๐Ÿ” Token Analysis:") + print(" Token Analysis:") if token.startswith("AT-"): print(" Token Type: Organization Token (AT-*)") - print(" โŒ SSH Keys API does NOT support Organization Tokens") - print(" ๐Ÿ’ก Please create a User Token instead") + print(" SSH Keys API does NOT support Organization Tokens") + print(" Please create a User Token instead") print("") - print("๐Ÿ”ง To create a User Token:") + print(" To create a User Token:") print(" 1. Go to Terraform Cloud โ†’ User Settings โ†’ Tokens") print(" 2. Create new token with VCS management permissions") print(" 3. Replace TFE_TOKEN environment variable") return False elif token.startswith("TF-"): print(" Token Type: User Token (TF-*)") - print(" โœ… SSH Keys API supports User Tokens") + print(" SSH Keys API supports User Tokens") return True elif ".atlasv1." in token: print(" Token Type: User/Team Token (.atlasv1. format)") - print(" โœ… SSH Keys API supports User/Team Tokens") + print(" SSH Keys API supports User/Team Tokens") return True else: print(f" Token Type: Unknown format ({token[:10]}...)") - print(" ๐Ÿ’ก Expected User Token (TF-*) or Team Token") + print(" Expected User Token (TF-*) or Team Token") return True # Allow unknown formats to try @@ -72,17 +72,17 @@ def main(): # Validate environment variables if not TFE_TOKEN: - print("โŒ Error: TFE_TOKEN environment variable is required") - print("๐Ÿ’ก Create a User Token (not Organization Token) in Terraform Cloud") + print(" Error: TFE_TOKEN environment variable is required") + print(" Create a User Token (not Organization Token) in Terraform Cloud") sys.exit(1) if not TFE_ORG: - print("โŒ Error: TFE_ORG environment variable is required") + print(" Error: TFE_ORG environment variable is required") sys.exit(1) if not SSH_KEY_VALUE: - print("โŒ Error: SSH_PRIVATE_KEY environment variable is required") - print("๐Ÿ’ก Provide a valid SSH private key for testing") + print(" Error: SSH_PRIVATE_KEY environment variable is required") + print(" Provide a valid SSH private key for testing") sys.exit(1) # Check token type first @@ -100,7 +100,7 @@ def main(): # 1. List existing SSH keys print("\n1. Listing SSH keys...") ssh_keys = client.ssh_keys.list(TFE_ORG) - print(f"โœ… Found {len(ssh_keys.items)} SSH keys:") + print(f" Found {len(ssh_keys.items)} SSH keys:") for key in ssh_keys.items: print(f" - ID: {key.id}, Name: {key.name}") @@ -111,62 +111,62 @@ def main(): ) new_key = client.ssh_keys.create(TFE_ORG, create_options) - print(f"โœ… Created SSH key: {new_key.id} - {new_key.name}") + print(f" Created SSH key: {new_key.id} - {new_key.name}") # 3. Read the SSH key we just created print("\n3. Reading the SSH key...") read_key = client.ssh_keys.read(new_key.id) - print(f"โœ… Read SSH key: {read_key.id} - {read_key.name}") + print(f" Read SSH key: {read_key.id} - {read_key.name}") # 4. Update the SSH key print("\n4. Updating the SSH key...") update_options = SSHKeyUpdateOptions(name="Updated Python TFE Example SSH Key") updated_key = client.ssh_keys.update(new_key.id, update_options) - print(f"โœ… Updated SSH key: {updated_key.id} - {updated_key.name}") + print(f" Updated SSH key: {updated_key.id} - {updated_key.name}") # 5. Delete the SSH key print("\n5. Deleting the SSH key...") client.ssh_keys.delete(new_key.id) - print(f"โœ… Deleted SSH key: {new_key.id}") + print(f" Deleted SSH key: {new_key.id}") # 6. Verify deletion by listing again print("\n6. Verifying deletion...") ssh_keys_after = client.ssh_keys.list(TFE_ORG) - print(f"โœ… SSH keys after deletion: {len(ssh_keys_after.items)}") + print(f" SSH keys after deletion: {len(ssh_keys_after.items)}") # 7. Demonstrate pagination with options print("\n7. Demonstrating pagination options...") list_options = SSHKeyListOptions(page_size=5, page_number=1) paginated_keys = client.ssh_keys.list(TFE_ORG, list_options) - print(f"โœ… Page 1 with page size 5: {len(paginated_keys.items)} keys") + print(f" Page 1 with page size 5: {len(paginated_keys.items)} keys") print(f" Total pages: {paginated_keys.total_pages}") print(f" Total count: {paginated_keys.total_count}") - print("\n๐ŸŽ‰ SSH Keys API example completed successfully!") + print("\n SSH Keys API example completed successfully!") except NotFound as e: - print(f"\nโŒ SSH Keys API Error: {e}") - print("\n๐Ÿ’ก This error usually means:") + print(f"\n SSH Keys API Error: {e}") + print("\n This error usually means:") print(" - Using Organization Token (not allowed)") print(" - SSH Keys feature not available") print(" - Insufficient permissions") - print("\n๐Ÿ”ง Try using a User Token instead of Organization Token") + print("\n Try using a User Token instead of Organization Token") sys.exit(1) except TFEError as e: - print(f"\nโŒ TFE API Error: {e}") + print(f"\n TFE API Error: {e}") if hasattr(e, "status"): if e.status == 403: - print("๐Ÿ’ก Permission denied - check token type and permissions") + print(" Permission denied - check token type and permissions") elif e.status == 401: - print("๐Ÿ’ก Authentication failed - check token validity") + print(" Authentication failed - check token validity") elif e.status == 422: - print("๐Ÿ’ก Validation error - check SSH key format") + print(" Validation error - check SSH key format") sys.exit(1) except Exception as e: - print(f"\nโŒ Unexpected error: {e}") + print(f"\n Unexpected error: {e}") sys.exit(1) diff --git a/examples/variables.py b/examples/variables.py index bc5227e..ba47f09 100644 --- a/examples/variables.py +++ b/examples/variables.py @@ -48,10 +48,10 @@ def main(): try: variable = client.variables.create(workspace_id, terraform_var) created_variables.append(variable.id) - print(f"โœ“ Created Terraform variable: {variable.key} = {variable.value}") + print(f" Created Terraform variable: {variable.key} = {variable.value}") print(f" ID: {variable.id}, Category: {variable.category}") except Exception as e: - print(f"โœ— Error creating Terraform variable: {e}") + print(f" Error creating Terraform variable: {e}") # Create an environment variable env_var = VariableCreateOptions( @@ -66,10 +66,10 @@ def main(): try: variable = client.variables.create(workspace_id, env_var) created_variables.append(variable.id) - print(f"โœ“ Created environment variable: {variable.key} = {variable.value}") + print(f" Created environment variable: {variable.key} = {variable.value}") print(f" ID: {variable.id}, Category: {variable.category}") except Exception as e: - print(f"โœ— Error creating environment variable: {e}") + print(f" Error creating environment variable: {e}") # Create a sensitive variable secret_var = VariableCreateOptions( @@ -84,10 +84,10 @@ def main(): try: variable = client.variables.create(workspace_id, secret_var) created_variables.append(variable.id) - print(f"โœ“ Created sensitive variable: {variable.key} = ***HIDDEN***") + print(f" Created sensitive variable: {variable.key} = ***HIDDEN***") print(f" ID: {variable.id}, Category: {variable.category}") except Exception as e: - print(f"โœ— Error creating sensitive variable: {e}") + print(f" Error creating sensitive variable: {e}") # Small delay to ensure variables are created time.sleep(1) @@ -105,7 +105,7 @@ def main(): f" โ€ข {var.key} = {value_display} ({var.category}) [ID: {var.id}]" ) except Exception as e: - print(f"โœ— Error listing variables: {e}") + print(f" Error listing variables: {e}") # 3. Test LIST_ALL function (includes inherited variables from variable sets) print("\n3. Testing LIST_ALL operation (includes variable sets):") @@ -120,7 +120,7 @@ def main(): f" โ€ข {var.key} = {value_display} ({var.category}) [ID: {var.id}]" ) except Exception as e: - print(f"โœ— Error listing all variables: {e}") + print(f" Error listing all variables: {e}") # Test READ function with specific variable ID - COMMENTED OUT print("\n4. Testing READ operation with specific variable ID:") @@ -134,9 +134,9 @@ def main(): variable = client.variables.read(workspace_id, test_variable_id) # For testing, show actual values even for sensitive variables if variable.sensitive: - print(f"โœ“ Read variable: {variable.key} = {variable.value} (SENSITIVE)") + print(f" Read variable: {variable.key} = {variable.value} (SENSITIVE)") else: - print(f"โœ“ Read variable: {variable.key} = {variable.value}") + print(f" Read variable: {variable.key} = {variable.value}") print(f" ID: {variable.id}") print(f" Description: {variable.description}") print(f" Category: {variable.category}") @@ -145,7 +145,7 @@ def main(): if hasattr(variable, "version_id"): print(f" Version ID: {variable.version_id}") except Exception as e: - print(f"โœ— Error reading variable {test_variable_id}: {e}") + print(f" Error reading variable {test_variable_id}: {e}") # Test UPDATE function with specific variable ID - COMMENTED OUT print("\n5. Testing UPDATE operation with specific variable ID:") @@ -175,7 +175,7 @@ def main(): workspace_id, test_variable_id, update_options ) print( - f"โœ“ Updated variable: {updated_variable.key} = {updated_variable.value}" + f" Updated variable: {updated_variable.key} = {updated_variable.value}" ) print(f" Description: {updated_variable.description}") print(f" Category: {updated_variable.category}") @@ -183,7 +183,7 @@ def main(): print(f" Sensitive: {updated_variable.sensitive}") print(f" ID: {updated_variable.id}") except Exception as e: - print(f"โœ— Error updating variable {test_variable_id}: {e}") + print(f" Error updating variable {test_variable_id}: {e}") # Test DELETE function with specific variable ID print("\n6. Testing DELETE operation with specific variable ID:") @@ -201,21 +201,21 @@ def main(): # Delete the variable client.variables.delete(workspace_id, test_variable_id) - print(f"โœ“ Successfully deleted variable with ID: {test_variable_id}") + print(f" Successfully deleted variable with ID: {test_variable_id}") # Try to read it again to verify deletion print("Verifying deletion...") try: client.variables.read(workspace_id, test_variable_id) - print("โœ— Warning: Variable still exists after deletion!") + print(" Warning: Variable still exists after deletion!") except Exception as read_error: if "not found" in str(read_error).lower() or "404" in str(read_error): - print("โœ“ Confirmed: Variable no longer exists") + print(" Confirmed: Variable no longer exists") else: - print(f"โœ— Unexpected error verifying deletion: {read_error}") + print(f" Unexpected error verifying deletion: {read_error}") except Exception as e: - print(f"โœ— Error deleting variable {test_variable_id}: {e}") + print(f" Error deleting variable {test_variable_id}: {e}") # 4. Test READ function print("\n4. Testing READ operation:") @@ -228,14 +228,14 @@ def main(): value_display = ( "***SENSITIVE***" if variable.sensitive else variable.value ) - print(f"โœ“ Read variable: {variable.key} = {value_display}") + print(f" Read variable: {variable.key} = {value_display}") print(f" ID: {variable.id}") print(f" Description: {variable.description}") print(f" Category: {variable.category}") print(f" HCL: {variable.hcl}") print(f" Sensitive: {variable.sensitive}") except Exception as e: - print(f"โœ— Error reading variable {test_var_id}: {e}") + print(f" Error reading variable {test_var_id}: {e}") else: print("No variables available to read") @@ -262,12 +262,12 @@ def main(): workspace_id, test_var_id, update_options ) print( - f"โœ“ Updated variable: {updated_variable.key} = {updated_variable.value}" + f" Updated variable: {updated_variable.key} = {updated_variable.value}" ) print(f" New description: {updated_variable.description}") print(f" ID: {updated_variable.id}") except Exception as e: - print(f"โœ— Error updating variable {test_var_id}: {e}") + print(f" Error updating variable {test_var_id}: {e}") else: print("No variables available to update") @@ -279,9 +279,9 @@ def main(): for var_id in created_variables: try: client.variables.delete(workspace_id, var_id) - print(f"โœ“ Deleted variable with ID: {var_id}") + print(f" Deleted variable with ID: {var_id}") except Exception as e: - print(f"โœ— Error deleting variable {var_id}: {e}") + print(f" Error deleting variable {var_id}: {e}") # Verify deletion by listing variables again print("\nVerifying deletion - listing variables after cleanup:") @@ -300,12 +300,12 @@ def main(): for var in remaining_test_vars: print(f" โ€ข {var.key} [ID: {var.id}]") else: - print("โœ“ All test variables successfully deleted") + print(" All test variables successfully deleted") except Exception as e: - print(f"โœ— Error verifying deletion: {e}") + print(f" Error verifying deletion: {e}") except Exception as e: - print(f"โœ— Unexpected error during testing: {e}") + print(f" Unexpected error during testing: {e}") print("\n" + "=" * 60) print("Variable testing complete!") diff --git a/examples/workspace.py b/examples/workspace.py index 3f55a94..dca9b11 100644 --- a/examples/workspace.py +++ b/examples/workspace.py @@ -7,7 +7,7 @@ integration, SSH keys, remote state, data retention, and filtering capabilities. API Coverage: 38/38 workspace methods (100% coverage) -Testing Status: โœ… All operations tested and validated +Testing Status: All operations tested and validated Organization: Logically grouped into 16 sections for easy navigation Prerequisites: @@ -176,7 +176,7 @@ def main(): if count >= args.page_size * 2: # Safety limit based on page size break - print(f"โœ“ Found {len(workspace_list)} workspaces") + print(f" Found {len(workspace_list)} workspaces") print() if not workspace_list: @@ -189,7 +189,7 @@ def main(): print(f" Auto Apply: {ws.auto_apply}") print() except Exception as e: - print(f"โœ— Error listing workspaces: {e}") + print(f" Error listing workspaces: {e}") return # 2) Create a new workspace if requested @@ -216,7 +216,7 @@ def main(): f"Creating workspace '{workspace_name}' in organization '{args.org}'..." ) workspace = client.workspaces.create(args.org, create_options) - print("โœ“ Successfully created workspace!") + print(" Successfully created workspace!") print(f" Name: {workspace.name}") print(f" ID: {workspace.id}") print(f" Description: {workspace.description}") @@ -230,7 +230,7 @@ def main(): ) # Use the created workspace for other operations args.workspace_id = workspace.id except Exception as e: - print(f"โœ— Error creating workspace: {e}") + print(f" Error creating workspace: {e}") return # 3a) Read workspace details using read_with_options @@ -246,7 +246,7 @@ def main(): workspace = client.workspaces.read_with_options( args.workspace, read_options, organization=args.org ) - print(f"โœ“ read_with_options: {workspace.name}") + print(f" read_with_options: {workspace.name}") print(f" ID: {workspace.id}") print(f" Description: {workspace.description}") print(f" Execution Mode: {workspace.execution_mode}") @@ -259,7 +259,7 @@ def main(): if not args.workspace_id: args.workspace_id = workspace.id except Exception as e: - print(f"โœ— read_with_options error: {e}") + print(f" read_with_options error: {e}") # Test basic read method (when testing all read methods) if args.read_all or args.all_tests: @@ -268,11 +268,11 @@ def main(): workspace = client.workspaces.read( args.workspace, organization=args.org ) - print(f"โœ“ read: {workspace.name} (ID: {workspace.id})") + print(f" read: {workspace.name} (ID: {workspace.id})") print(f" Description: {workspace.description}") print(f" Execution Mode: {workspace.execution_mode}") except Exception as e: - print(f"โœ— read error: {e}") + print(f" read error: {e}") # 3b) Read workspace by ID methods (comprehensive testing) if args.workspace_id and (args.read_all or args.all_tests): @@ -283,9 +283,9 @@ def main(): try: print("Testing read_by_id()...") workspace = client.workspaces.read_by_id(args.workspace_id) - print(f"โœ“ read_by_id: {workspace.name} (ID: {workspace.id})") + print(f" read_by_id: {workspace.name} (ID: {workspace.id})") except Exception as e: - print(f"โœ— read_by_id error: {e}") + print(f" read_by_id error: {e}") # Test read_by_id_with_options try: @@ -295,10 +295,10 @@ def main(): args.workspace_id, options ) print( - f"โœ“ read_by_id_with_options: {workspace.name} with organization included" + f" read_by_id_with_options: {workspace.name} with organization included" ) except Exception as e: - print(f"โœ— read_by_id_with_options error: {e}") + print(f" read_by_id_with_options error: {e}") # 4a) Update workspace by name if args.update and args.workspace or args.update_all or args.all_tests: @@ -317,14 +317,14 @@ def main(): updated_workspace = client.workspaces.update( args.workspace, update_options, organization=args.org ) - print("โœ“ update: Successfully updated workspace!") + print(" update: Successfully updated workspace!") print(f" Name: {updated_workspace.name}") print(f" Description: {updated_workspace.description}") print(f" Auto Apply: {updated_workspace.auto_apply}") print(f" Terraform Version: {updated_workspace.terraform_version}") print() except Exception as e: - print(f"โœ— update error: {e}") + print(f" update error: {e}") # 4b) Update workspace by ID if args.workspace_id and (args.update_all or args.all_tests): @@ -340,10 +340,10 @@ def main(): args.workspace_id, update_options ) print( - f"โœ“ update_by_id: Updated description to '{updated_workspace.description}'" + f" update_by_id: Updated description to '{updated_workspace.description}'" ) except Exception as e: - print(f"โœ— update_by_id error: {e}") + print(f" update_by_id error: {e}") # 5) Lock workspace if requested if args.lock and args.workspace_id: @@ -371,11 +371,11 @@ def main(): workspace = client.workspaces.remove_vcs_connection( args.workspace, organization=args.org ) - print("โœ“ Successfully removed VCS connection from workspace!") + print(" Successfully removed VCS connection from workspace!") print(f" Workspace: {workspace.name}") print() except Exception as e: - print(f"โœ— Error removing VCS connection: {e}") + print(f" Error removing VCS connection: {e}") # 8) Demonstrate tag operations if args.workspace_id: @@ -423,7 +423,7 @@ def main(): try: print("Testing force_unlock()...") workspace = client.workspaces.force_unlock(args.workspace_id) - print(f"โœ“ force_unlock: Workspace {workspace.name} force unlocked") + print(f" force_unlock: Workspace {workspace.name} force unlocked") except Exception as e: print(f" force_unlock result: {e}") print(" (Expected if workspace wasn't locked)") @@ -446,15 +446,15 @@ def main(): workspace = client.workspaces.assign_ssh_key( args.workspace_id, ssh_key.id ) - print(f"โœ“ assign_ssh_key: Assigned key to {workspace.name}") + print(f" assign_ssh_key: Assigned key to {workspace.name}") # Test unassign SSH key print("Testing unassign_ssh_key()...") workspace = client.workspaces.unassign_ssh_key(args.workspace_id) - print(f"โœ“ unassign_ssh_key: Removed key from {workspace.name}") + print(f" unassign_ssh_key: Removed key from {workspace.name}") except Exception as e: - print(f"โœ— SSH key assignment error: {e}") + print(f" SSH key assignment error: {e}") else: print("No SSH keys available for testing") print( @@ -462,7 +462,7 @@ def main(): ) except Exception as e: - print(f"โœ— SSH key listing error: {e}") + print(f" SSH key listing error: {e}") # 12) Test advanced tag operations if (args.all_tests or args.tag_ops) and args.workspace_id: @@ -473,7 +473,7 @@ def main(): print("Testing remove_tags()...") remove_options = WorkspaceRemoveTagsOptions(tags=[Tag(name="demo")]) client.workspaces.remove_tags(args.workspace_id, remove_options) - print("โœ“ remove_tags: Removed 'demo' tag") + print(" remove_tags: Removed 'demo' tag") except Exception as e: print(f" remove_tags: {e}") @@ -481,9 +481,9 @@ def main(): # Test list_tag_bindings print("Testing list_tag_bindings()...") bindings = list(client.workspaces.list_tag_bindings(args.workspace_id)) - print(f"โœ“ list_tag_bindings: Found {len(bindings)} tag bindings") + print(f" list_tag_bindings: Found {len(bindings)} tag bindings") except Exception as e: - print(f"โœ— list_tag_bindings error: {e}") + print(f" list_tag_bindings error: {e}") try: # Test list_effective_tag_bindings @@ -492,17 +492,17 @@ def main(): client.workspaces.list_effective_tag_bindings(args.workspace_id) ) print( - f"โœ“ list_effective_tag_bindings: Found {len(effective_bindings)} effective bindings" + f" list_effective_tag_bindings: Found {len(effective_bindings)} effective bindings" ) except Exception as e: - print(f"โœ— list_effective_tag_bindings error: {e}") + print(f" list_effective_tag_bindings error: {e}") # 13) Test additional remote state operations if (args.all_tests or args.remote_state) and args.workspace_id: _print_header("Testing additional remote state operations") print("Available remote state methods:") - print("โœ“ list_remote_state_consumers() - Already tested above") + print(" list_remote_state_consumers() - Already tested above") print(" add_remote_state_consumers() - Requires consumer workspace IDs") print(" update_remote_state_consumers() - Requires specific setup") print(" remove_remote_state_consumers() - Requires existing consumers") @@ -514,7 +514,7 @@ def main(): try: print("Testing read_data_retention_policy()...") policy = client.workspaces.read_data_retention_policy(args.workspace_id) - print(f"โœ“ read_data_retention_policy: {policy}") + print(f" read_data_retention_policy: {policy}") except Exception as e: print(f" read_data_retention_policy: {e}") print(" (Expected if no policy is set)") @@ -524,7 +524,7 @@ def main(): choice = client.workspaces.read_data_retention_policy_choice( args.workspace_id ) - print(f"โœ“ read_data_retention_policy_choice: {choice}") + print(f" read_data_retention_policy_choice: {choice}") except Exception as e: print(f" read_data_retention_policy_choice: {e}") @@ -543,7 +543,7 @@ def main(): print("Testing readme()...") readme = client.workspaces.readme(args.workspace_id) if readme: - print(f"โœ“ readme: Found README content ({len(readme)} characters)") + print(f" readme: Found README content ({len(readme)} characters)") print( f" Preview: {readme[:100]}..." if len(readme) > 100 diff --git a/src/pytfe/client.py b/src/pytfe/client.py index 3894886..c814eea 100644 --- a/src/pytfe/client.py +++ b/src/pytfe/client.py @@ -9,6 +9,7 @@ from .resources.notification_configuration import NotificationConfigurations from .resources.oauth_client import OAuthClients from .resources.oauth_token import OAuthTokens +from .resources.organization_membership import OrganizationMemberships from .resources.organizations import Organizations from .resources.plan import Plans from .resources.policy import Policies @@ -64,6 +65,7 @@ def __init__(self, config: TFEConfig | None = None): self.applies = Applies(self._transport) self.plans = Plans(self._transport) self.organizations = Organizations(self._transport) + self.organization_memberships = OrganizationMemberships(self._transport) self.projects = Projects(self._transport) self.variables = Variables(self._transport) self.variable_sets = VariableSets(self._transport) diff --git a/src/pytfe/errors.py b/src/pytfe/errors.py index 3eac2be..ffe7a25 100644 --- a/src/pytfe/errors.py +++ b/src/pytfe/errors.py @@ -55,11 +55,19 @@ class RequiredFieldMissing(TFEError): ... class ErrStateVersionUploadNotSupported(TFEError): ... +# Generic error constants +ERR_UNAUTHORIZED = "unauthorized" +ERR_RESOURCE_NOT_FOUND = "resource not found" +ERR_MISSING_DIRECTORY = "path needs to be an existing directory" +ERR_NAMESPACE_NOT_AUTHORIZED = "namespace not authorized" + # Error message constants ERR_INVALID_NAME = "invalid value for name" ERR_REQUIRED_NAME = "name is required" ERR_INVALID_ORG = "invalid organization name" ERR_REQUIRED_EMAIL = "email is required" +ERR_INVALID_EMAIL = "invalid email format" +ERR_INVALID_MEMBERSHIP_ID = "invalid value for organization membership ID" # Registry Module Error Constants ERR_REQUIRED_PROVIDER = "provider is required" diff --git a/src/pytfe/models/__init__.py b/src/pytfe/models/__init__.py index f3eb33b..68286ff 100644 --- a/src/pytfe/models/__init__.py +++ b/src/pytfe/models/__init__.py @@ -86,6 +86,14 @@ ReadRunQueueOptions, RunQueue, ) +from .organization_membership import ( + OrganizationMembership, + OrganizationMembershipCreateOptions, + OrganizationMembershipListOptions, + OrganizationMembershipReadOptions, + OrganizationMembershipStatus, + OrgMembershipIncludeOpt, +) from .policy import ( Policy, PolicyCreateOptions, @@ -274,6 +282,11 @@ SSHKeyListOptions, SSHKeyUpdateOptions, ) +from .team import ( + OrganizationAccess, + Team, + TeamPermissions, +) # Variables from .variable import ( @@ -449,6 +462,15 @@ "Organization", "OrganizationCreateOptions", "OrganizationUpdateOptions", + "OrganizationMembership", + "OrganizationMembershipCreateOptions", + "OrganizationMembershipListOptions", + "OrganizationMembershipReadOptions", + "OrganizationMembershipStatus", + "OrgMembershipIncludeOpt", + "OrganizationAccess", + "Team", + "TeamPermissions", "Project", "ProjectAddTagBindingsOptions", "ProjectCreateOptions", diff --git a/src/pytfe/models/organization_membership.py b/src/pytfe/models/organization_membership.py new file mode 100644 index 0000000..a588e9c --- /dev/null +++ b/src/pytfe/models/organization_membership.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +from enum import Enum +from typing import TYPE_CHECKING + +from pydantic import BaseModel, ConfigDict, Field + +if TYPE_CHECKING: + from .organization import Organization + from .team import Team + from .user import User + + +class OrganizationMembershipStatus(str, Enum): + """Organization membership status enum.""" + + ACTIVE = "active" + INVITED = "invited" + + +class OrgMembershipIncludeOpt(str, Enum): + """Include options for organization membership queries.""" + + USER = "user" + TEAMS = "teams" + + +class OrganizationMembership(BaseModel): + """Represents a Terraform Enterprise organization membership.""" + + model_config = ConfigDict(populate_by_name=True) + + id: str + status: OrganizationMembershipStatus + email: str + + # Relations + organization: Organization | None = None + user: User | None = None + teams: list[Team] | None = None + + +class OrganizationMembershipListOptions(BaseModel): + """Options for listing organization memberships.""" + + model_config = ConfigDict(populate_by_name=True) + + # Pagination + page_number: int | None = Field(None, alias="page[number]") + page_size: int | None = Field(None, alias="page[size]") + + # Include related resources + include: list[OrgMembershipIncludeOpt] | None = None + + # Filters + emails: list[str] | None = Field(None, alias="filter[email]") + status: OrganizationMembershipStatus | None = Field(None, alias="filter[status]") + query: str | None = Field(None, alias="q") + + +class OrganizationMembershipReadOptions(BaseModel): + """Options for reading an organization membership.""" + + model_config = ConfigDict(populate_by_name=True) + + # Include related resources + include: list[OrgMembershipIncludeOpt] | None = None + + +class OrganizationMembershipCreateOptions(BaseModel): + """Options for creating an organization membership.""" + + model_config = ConfigDict(populate_by_name=True) + + # Required + email: str + + # Optional: A list of teams to add the user to + teams: list[Team] | None = None + + +# Rebuild models after all definitions to resolve forward references +def _rebuild_models() -> None: + """Rebuild models to resolve forward references.""" + try: + from .organization import Organization # noqa: F401 + from .team import Team # noqa: F401 + from .user import User # noqa: F401 + + OrganizationMembership.model_rebuild() + OrganizationMembershipCreateOptions.model_rebuild() + except Exception: + # If rebuild fails, models will still work at runtime + pass + + +_rebuild_models() diff --git a/src/pytfe/models/team.py b/src/pytfe/models/team.py new file mode 100644 index 0000000..c19b007 --- /dev/null +++ b/src/pytfe/models/team.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from pydantic import BaseModel, ConfigDict + +if TYPE_CHECKING: + from .organization_membership import OrganizationMembership + from .user import User + + +class OrganizationAccess(BaseModel): + """Organization access permissions for a team.""" + + model_config = ConfigDict(populate_by_name=True) + + manage_policies: bool = False + manage_policy_overrides: bool = False + manage_workspaces: bool = False + manage_vcs_settings: bool = False + manage_providers: bool = False + manage_modules: bool = False + manage_run_tasks: bool = False + manage_projects: bool = False + read_workspaces: bool = False + read_projects: bool = False + manage_membership: bool = False + manage_teams: bool = False + manage_organization_access: bool = False + access_secret_teams: bool = False + manage_agent_pools: bool = False + + +class TeamPermissions(BaseModel): + """Team permissions for the current user.""" + + model_config = ConfigDict(populate_by_name=True) + + can_destroy: bool = False + can_update_membership: bool = False + + +class Team(BaseModel): + """Represents a Terraform Enterprise team.""" + + model_config = ConfigDict(populate_by_name=True) + + id: str + name: str | None = None + is_unified: bool = False + organization_access: OrganizationAccess | None = None + visibility: str | None = None + permissions: TeamPermissions | None = None + user_count: int = 0 + sso_team_id: str | None = None + allow_member_token_management: bool = False + + # Relations + users: list[User] | None = None + organization_memberships: list[OrganizationMembership] | None = None + + +def _rebuild_models() -> None: + """Rebuild models to resolve forward references.""" + from .organization import Organization # noqa: F401 + from .organization_membership import OrganizationMembership # noqa: F401 + from .user import User # noqa: F401 + + Team.model_rebuild() + + +_rebuild_models() diff --git a/src/pytfe/models/user.py b/src/pytfe/models/user.py index 69fe53d..26b902e 100644 --- a/src/pytfe/models/user.py +++ b/src/pytfe/models/user.py @@ -7,17 +7,17 @@ class User(BaseModel): model_config = ConfigDict(populate_by_name=True, validate_by_name=True) id: str = Field(..., alias="id") - avatar_url: str = Field(..., alias="avatar-url") - email: str = Field(..., alias="email") - is_service_account: bool = Field(..., alias="is-service-account") - two_factor: dict = Field(..., alias="two-factor") - unconfirmed_email: str = Field(..., alias="unconfirmed-email") - username: str = Field(..., alias="username") - v2_only: bool = Field(..., alias="v2-only") - is_site_admin: bool = Field(..., alias="is-site-admin") # Deprecated - is_admin: bool = Field(..., alias="is-admin") - is_sso_login: bool = Field(..., alias="is-sso-login") - permissions: dict = Field(..., alias="permissions") + avatar_url: str = Field(default="", alias="avatar-url") + email: str = Field(default="", alias="email") + is_service_account: bool = Field(default=False, alias="is-service-account") + two_factor: dict = Field(default_factory=dict, alias="two-factor") + unconfirmed_email: str = Field(default="", alias="unconfirmed-email") + username: str = Field(default="", alias="username") + v2_only: bool = Field(default=False, alias="v2-only") + is_site_admin: bool = Field(default=False, alias="is-site-admin") # Deprecated + is_admin: bool = Field(default=False, alias="is-admin") + is_sso_login: bool = Field(default=False, alias="is-sso-login") + permissions: dict = Field(default_factory=dict, alias="permissions") # Relations # authentication_tokens: AuthenticationTokens = Field(..., alias="authentication-tokens") diff --git a/src/pytfe/resources/organization_membership.py b/src/pytfe/resources/organization_membership.py new file mode 100644 index 0000000..11d7eb2 --- /dev/null +++ b/src/pytfe/resources/organization_membership.py @@ -0,0 +1,284 @@ +from __future__ import annotations + +import re +from collections.abc import Iterator +from typing import Any + +from ..errors import ERR_INVALID_EMAIL, ERR_INVALID_ORG +from ..models.organization import Organization +from ..models.organization_membership import ( + OrganizationMembership, + OrganizationMembershipCreateOptions, + OrganizationMembershipListOptions, + OrganizationMembershipReadOptions, +) +from ..models.team import Team +from ..models.user import User +from ..utils import valid_string_id +from ._base import _Service + + +def _valid_email(email: str) -> bool: + """Validate email format.""" + if not email or not isinstance(email, str): + return False + # Simple email validation pattern + pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$" + return re.match(pattern, email) is not None + + +def _validate_email_params(emails: list[str] | None) -> None: + """Validate a list of email parameters.""" + if not emails: + return + for email in emails: + if not _valid_email(email): + raise ValueError(ERR_INVALID_EMAIL) + + +class OrganizationMemberships(_Service): + """Organization memberships service for managing organization members.""" + + def create( + self, + organization: str, + options: OrganizationMembershipCreateOptions, + ) -> OrganizationMembership: + """Create an organization membership with the given options. + + Args: + organization: The name of the organization + options: The options for creating the organization membership + + Returns: + The created OrganizationMembership + + Raises: + ValueError: If organization name is invalid or options are invalid + """ + if not valid_string_id(organization): + raise ValueError(ERR_INVALID_ORG) + + # Validate email is provided + if not options.email: + raise ValueError("email is required") + + # Validate email format + if not _valid_email(options.email): + raise ValueError(ERR_INVALID_EMAIL) + + # Build the URL path + path = f"/api/v2/organizations/{organization}/organization-memberships" + + # Build the request body + body = { + "data": { + "type": "organization-memberships", + "attributes": { + "email": options.email, + }, + } + } + + # Add teams relationship if provided + if options.teams: + body["data"]["relationships"] = { + "teams": { + "data": [{"type": "teams", "id": team.id} for team in options.teams] + } + } + + # Make the POST request + response = self.t.request("POST", path, json_body=body) + data = response.json() + + return self._parse_membership(data["data"]) + + def list( + self, + organization: str, + options: OrganizationMembershipListOptions | None = None, + ) -> Iterator[OrganizationMembership]: + """List all the organization memberships of the given organization. + + Args: + organization: The name of the organization + options: Optional filters and pagination options + + Yields: + OrganizationMembership instances one at a time + + Raises: + ValueError: If organization name is invalid or email filters are invalid + """ + if not valid_string_id(organization): + raise ValueError(ERR_INVALID_ORG) + + # Validate options if provided + if options and options.emails: + _validate_email_params(options.emails) + + # Build the URL path + path = f"/api/v2/organizations/{organization}/organization-memberships" + + # Build query parameters from options + params: dict[str, Any] = {} + if options: + options_dict = options.model_dump(by_alias=True, exclude_none=True) + + # Handle include parameter - convert list to comma-separated string + if "include" in options_dict and isinstance(options_dict["include"], list): + options_dict["include"] = ",".join( + opt.value if hasattr(opt, "value") else str(opt) + for opt in options.include or [] + ) + + # Handle emails filter - convert list to comma-separated string + if "filter[email]" in options_dict and isinstance( + options_dict["filter[email]"], list + ): + options_dict["filter[email]"] = ",".join(options_dict["filter[email]"]) + + # Handle status filter - extract value from enum + if "filter[status]" in options_dict: + status_value = options_dict["filter[status]"] + if hasattr(status_value, "value"): + options_dict["filter[status]"] = status_value.value + + params.update(options_dict) + + # Use the _list helper for automatic pagination + for item in self._list(path, params=params): + yield self._parse_membership(item) + + def read(self, organization_membership_id: str) -> OrganizationMembership: + """Read an organization membership by its ID. + + Args: + organization_membership_id: The ID of the organization membership to read + + Returns: + The OrganizationMembership + + Raises: + ValueError: If organization membership ID is invalid + NotFound: If the resource is not found + """ + return self.read_with_options( + organization_membership_id, OrganizationMembershipReadOptions() + ) + + def read_with_options( + self, + organization_membership_id: str, + options: OrganizationMembershipReadOptions, + ) -> OrganizationMembership: + """Read an organization membership by ID with options. + + Args: + organization_membership_id: The ID of the organization membership to read + options: Read options including include parameters + + Returns: + The OrganizationMembership with requested included data + + Raises: + ValueError: If organization membership ID is invalid + NotFound: If the resource is not found + """ + if not valid_string_id(organization_membership_id): + raise ValueError("invalid organization membership ID") + + # Build the URL path + path = f"/api/v2/organization-memberships/{organization_membership_id}" + + # Build query parameters from options + params: dict[str, Any] = {} + if options: + options_dict = options.model_dump(by_alias=True, exclude_none=True) + + # Handle include parameter - convert list to comma-separated string + if "include" in options_dict and isinstance(options_dict["include"], list): + options_dict["include"] = ",".join( + opt.value if hasattr(opt, "value") else str(opt) + for opt in options.include or [] + ) + + params.update(options_dict) + + # Make the GET request + # NotFound exception will be raised by self.t.request if resource doesn't exist + response = self.t.request("GET", path, params=params) + data = response.json() + return self._parse_membership(data["data"]) + + def delete(self, organization_membership_id: str) -> None: + """Delete an organization membership by its ID. + + Args: + organization_membership_id: The ID of the organization membership to delete + + Raises: + ValueError: If organization membership ID is invalid + """ + if not valid_string_id(organization_membership_id): + raise ValueError("invalid organization membership ID") + + # Build the URL path + path = f"/api/v2/organization-memberships/{organization_membership_id}" + + # Make the DELETE request + self.t.request("DELETE", path) + + def _parse_membership(self, data: dict[str, Any]) -> OrganizationMembership: + """Parse a membership from API response data. + + Args: + data: The raw API response data for a membership + + Returns: + OrganizationMembership instance + """ + membership_id = data.get("id", "") + attributes = data.get("attributes", {}) + + # Extract basic attributes + status = attributes.get("status", "active") + email = attributes.get("email", "") + + # Extract relationships if present + relationships = data.get("relationships", {}) + + # Parse organization relationship + organization = None + if "organization" in relationships: + org_data = relationships["organization"].get("data") + if org_data: + organization = Organization(id=org_data.get("id")) + + # Parse user relationship + user = None + if "user" in relationships: + user_data = relationships["user"].get("data") + if user_data: + user = User(id=user_data.get("id")) + + # Parse teams relationship + teams = None + if "teams" in relationships: + teams_data = relationships["teams"].get("data", []) + if teams_data: + teams = [Team(id=team.get("id")) for team in teams_data] + + # Handle included data if present (for full user/org objects) + # This would be populated when include options are used + # For now, keeping it simple with just IDs + + return OrganizationMembership( + id=membership_id, + status=status, + email=email, + organization=organization, + user=user, + teams=teams, + ) diff --git a/src/pytfe/resources/policy_check.py b/src/pytfe/resources/policy_check.py index 7529f61..affae77 100644 --- a/src/pytfe/resources/policy_check.py +++ b/src/pytfe/resources/policy_check.py @@ -97,7 +97,7 @@ def logs(self, policy_check_id: str) -> str: # Continue polling if the policy check is still pending or queued if pc.status in (PolicyStatus.POLICY_PENDING, PolicyStatus.POLICY_QUEUED): - time.sleep(0.5) # 500ms wait, equivalent to Go's 500 * time.Millisecond + time.sleep(0.5) # 500ms wait continue # Policy check is finished, get the logs diff --git a/src/pytfe/resources/projects.py b/src/pytfe/resources/projects.py index 9011a3c..e64cb34 100644 --- a/src/pytfe/resources/projects.py +++ b/src/pytfe/resources/projects.py @@ -105,7 +105,7 @@ def list( self, organization: str, options: ProjectListOptions | None = None ) -> Iterator[Project]: """List projects in an organization""" - # Validate inputs following Go patterns + # Validate inputs validate_project_list_options(organization) path = f"/api/v2/organizations/{organization}/projects" @@ -129,7 +129,7 @@ def list( items_iter = self._list(path) for item in items_iter: - # Extract project data following Go patterns + # Extract project data attr = item.get("attributes", {}) or {} project_data = { "id": _safe_str(item.get("id")), @@ -147,7 +147,7 @@ def list( def create(self, organization: str, options: ProjectCreateOptions) -> Project: """Create a new project in an organization""" - # Validate inputs following Go patterns + # Validate inputs validate_project_create_options(organization, options.name, options.description) path = f"/api/v2/organizations/{organization}/projects" @@ -160,7 +160,7 @@ def create(self, organization: str, options: ProjectCreateOptions) -> Project: response = self.t.request("POST", path, json_body=payload) data = response.json()["data"] - # Extract project data following Go patterns + # Extract project data attr = data.get("attributes", {}) or {} project_data = { "id": _safe_str(data.get("id")), @@ -180,7 +180,7 @@ def read( self, project_id: str, include: builtins.list[str] | None = None ) -> Project: """Get a specific project by ID""" - # Validate inputs following Go patterns + # Validate inputs if not valid_string_id(project_id): raise ValueError("Project ID is required and must be valid") @@ -201,7 +201,7 @@ def read( org_data = relationships.get("organization", {}).get("data", {}) organization = _safe_str(org_data.get("id")) - # Extract project data following Go patterns + # Extract project data attr = data.get("attributes", {}) or {} project_data = { "id": _safe_str(data.get("id")), @@ -219,7 +219,7 @@ def read( def update(self, project_id: str, options: ProjectUpdateOptions) -> Project: """Update a project's name and/or description""" - # Validate inputs following Go patterns + # Validate inputs validate_project_update_options(project_id, options.name, options.description) path = f"/api/v2/projects/{project_id}" @@ -242,7 +242,7 @@ def update(self, project_id: str, options: ProjectUpdateOptions) -> Project: org_data = relationships.get("organization", {}).get("data", {}) organization = _safe_str(org_data.get("id")) - # Extract project data following Go patterns + # Extract project data attr = data.get("attributes", {}) or {} project_data = { "id": _safe_str(data.get("id")), @@ -260,7 +260,7 @@ def update(self, project_id: str, options: ProjectUpdateOptions) -> Project: def delete(self, project_id: str) -> None: """Delete a project""" - # Validate inputs following Go patterns + # Validate inputs if not valid_string_id(project_id): raise ValueError("Project ID is required and must be valid") diff --git a/tests/units/test_organization_membership.py b/tests/units/test_organization_membership.py new file mode 100644 index 0000000..11888d5 --- /dev/null +++ b/tests/units/test_organization_membership.py @@ -0,0 +1,835 @@ +""" +Comprehensive unit tests for organization membership operations in the Python TFE SDK. + +This test suite covers all organization membership methods including list, create, read, +read with options, and delete operations. +""" + +from unittest.mock import Mock + +import pytest + +from src.pytfe.errors import ( + ERR_INVALID_EMAIL, + ERR_INVALID_ORG, + ERR_REQUIRED_EMAIL, + NotFound, +) +from src.pytfe.models.organization_membership import ( + OrganizationMembershipCreateOptions, + OrganizationMembershipListOptions, + OrganizationMembershipReadOptions, + OrganizationMembershipStatus, + OrgMembershipIncludeOpt, +) +from src.pytfe.models.team import OrganizationAccess, Team +from src.pytfe.resources.organization_membership import OrganizationMemberships + + +class TestOrganizationMembershipList: + """Test suite for organization membership list operations.""" + + @pytest.fixture + def mock_transport(self): + """Mock HTTP transport.""" + transport = Mock() + return transport + + @pytest.fixture + def membership_service(self, mock_transport): + """Create organization membership service with mocked transport.""" + return OrganizationMemberships(mock_transport) + + @pytest.fixture + def sample_membership_response(self): + """Sample JSON:API organization membership response.""" + return { + "data": [ + { + "type": "organization-memberships", + "id": "ou-abc123def456", + "attributes": {"status": "active"}, + "relationships": { + "teams": { + "data": [{"type": "teams", "id": "team-yUrEehvfG4pdmSjc"}] + }, + "user": {"data": {"type": "users", "id": "user-123"}}, + "organization": { + "data": {"type": "organizations", "id": "org-test"} + }, + }, + }, + { + "type": "organization-memberships", + "id": "ou-xyz789ghi012", + "attributes": {"status": "invited"}, + "relationships": { + "teams": { + "data": [{"type": "teams", "id": "team-yUrEehvfG4pdmSjc"}] + }, + "user": {"data": {"type": "users", "id": "user-456"}}, + "organization": { + "data": {"type": "organizations", "id": "org-test"} + }, + }, + }, + ], + "meta": {"pagination": {"current-page": 1, "total-count": 2}}, + } + + def test_list_without_options( + self, membership_service, mock_transport, sample_membership_response + ): + """Test listing organization memberships without options.""" + mock_response = Mock() + mock_response.json.return_value = sample_membership_response + mock_transport.request.return_value = mock_response + + memberships = list(membership_service.list("test-org")) + + assert len(memberships) == 2 + assert memberships[0].status == OrganizationMembershipStatus.ACTIVE + assert memberships[0].id == "ou-abc123def456" + assert memberships[1].status == OrganizationMembershipStatus.INVITED + assert memberships[1].id == "ou-xyz789ghi012" + mock_transport.request.assert_called_once() + + def test_list_with_pagination_options(self, membership_service, mock_transport): + """Test listing with pagination options.""" + mock_response = Mock() + mock_response.json.return_value = { + "data": [], + "meta": {"pagination": {"current-page": 999, "total-count": 2}}, + } + mock_transport.request.return_value = mock_response + + options = OrganizationMembershipListOptions(page_number=999, page_size=100) + memberships = list(membership_service.list("test-org", options)) + + assert len(memberships) == 0 + # Verify pagination params are passed + call_args = mock_transport.request.call_args + assert call_args is not None + + def test_list_with_include_options( + self, membership_service, mock_transport, sample_membership_response + ): + """Test listing with include options for user and teams.""" + mock_response = Mock() + mock_response.json.return_value = sample_membership_response + mock_transport.request.return_value = mock_response + + options = OrganizationMembershipListOptions( + include=[OrgMembershipIncludeOpt.USER, OrgMembershipIncludeOpt.TEAMS] + ) + memberships = list(membership_service.list("test-org", options)) + + assert len(memberships) == 2 + mock_transport.request.assert_called_once() + + def test_list_with_email_filter(self, membership_service, mock_transport): + """Test listing with email filter option.""" + mock_response = Mock() + mock_response.json.return_value = { + "data": [ + { + "type": "organization-memberships", + "id": "ou-abc123", + "attributes": {"status": "active"}, + "relationships": { + "teams": {"data": [{"type": "teams", "id": "team-xyz"}]}, + "user": {"data": {"type": "users", "id": "user-abc"}}, + "organization": { + "data": {"type": "organizations", "id": "test-org"} + }, + }, + } + ], + "meta": {"pagination": {"current-page": 1, "total-count": 1}}, + } + mock_transport.request.return_value = mock_response + + options = OrganizationMembershipListOptions(emails=["specific@example.com"]) + memberships = list(membership_service.list("test-org", options)) + + assert len(memberships) == 1 + assert memberships[0].status == OrganizationMembershipStatus.ACTIVE + + def test_list_with_status_filter(self, membership_service, mock_transport): + """Test listing with status filter option.""" + mock_response = Mock() + mock_response.json.return_value = { + "data": [ + { + "type": "organization-memberships", + "id": "ou-abc123", + "attributes": {"status": "invited"}, + "relationships": { + "teams": {"data": [{"type": "teams", "id": "team-xyz"}]}, + "user": {"data": {"type": "users", "id": "user-abc"}}, + "organization": { + "data": {"type": "organizations", "id": "test-org"} + }, + }, + } + ], + "meta": {"pagination": {"current-page": 1, "total-count": 1}}, + } + mock_transport.request.return_value = mock_response + + options = OrganizationMembershipListOptions( + status=OrganizationMembershipStatus.INVITED + ) + memberships = list(membership_service.list("test-org", options)) + + assert len(memberships) == 1 + assert memberships[0].status == OrganizationMembershipStatus.INVITED + + def test_list_with_query_string(self, membership_service, mock_transport): + """Test listing with search query string.""" + mock_response = Mock() + mock_response.json.return_value = { + "data": [ + { + "type": "organization-memberships", + "id": "ou-abc123", + "attributes": {"status": "active"}, + "relationships": { + "teams": {"data": [{"type": "teams", "id": "team-xyz"}]}, + "user": {"data": {"type": "users", "id": "user-abc"}}, + "organization": { + "data": {"type": "organizations", "id": "test-org"} + }, + }, + } + ], + "meta": {"pagination": {"current-page": 1, "total-count": 1}}, + } + mock_transport.request.return_value = mock_response + + options = OrganizationMembershipListOptions(query="example.com") + memberships = list(membership_service.list("test-org", options)) + + assert len(memberships) == 1 + + def test_list_with_invalid_organization(self, membership_service): + """Test listing with invalid organization name.""" + with pytest.raises(ValueError, match=ERR_INVALID_ORG): + list(membership_service.list("")) + + +class TestOrganizationMembershipCreate: + """Test suite for organization membership create operations.""" + + @pytest.fixture + def mock_transport(self): + """Mock HTTP transport.""" + transport = Mock() + return transport + + @pytest.fixture + def membership_service(self, mock_transport): + """Create organization membership service with mocked transport.""" + return OrganizationMemberships(mock_transport) + + @pytest.fixture + def sample_create_response(self): + """Sample JSON:API create response.""" + return { + "data": { + "type": "organization-memberships", + "id": "ou-newmember123", + "attributes": {"status": "invited"}, + "relationships": { + "teams": { + "data": [{"type": "teams", "id": "team-GeLZkdnK6xAVjA5H"}] + }, + "user": {"data": {"type": "users", "id": "user-J8oxGmRk5eC2WLfX"}}, + "organization": { + "data": {"type": "organizations", "id": "my-organization"} + }, + }, + }, + "included": [ + { + "id": "user-J8oxGmRk5eC2WLfX", + "type": "users", + "attributes": { + "username": None, + "is-service-account": False, + "auth-method": "hcp_sso", + "avatar-url": "https://www.gravatar.com/avatar/55502f40dc8b7c769880b10874abc9d0?s=100&d=mm", + "two-factor": {"enabled": False, "verified": False}, + "email": "newuser@example.com", + "permissions": { + "can-create-organizations": True, + "can-change-email": True, + "can-change-username": True, + "can-manage-user-tokens": False, + }, + }, + "relationships": { + "authentication-tokens": { + "links": { + "related": "/api/v2/users/user-J8oxGmRk5eC2WLfX/authentication-tokens" + } + } + }, + "links": {"self": "/api/v2/users/user-J8oxGmRk5eC2WLfX"}, + } + ], + } + + def test_create_with_valid_options( + self, membership_service, mock_transport, sample_create_response + ): + """Test creating organization membership with valid options.""" + mock_response = Mock() + mock_response.json.return_value = sample_create_response + mock_transport.request.return_value = mock_response + + options = OrganizationMembershipCreateOptions(email="newuser@example.com") + membership = membership_service.create("test-org", options) + + assert membership.status == OrganizationMembershipStatus.INVITED + assert membership.id == "ou-newmember123" + assert membership.user is not None + # User is parsed as a User object with id + assert membership.user.id == "user-J8oxGmRk5eC2WLfX" + mock_transport.request.assert_called_once() + + def test_create_with_teams(self, membership_service, mock_transport): + """Test creating organization membership with initial teams.""" + mock_response = Mock() + mock_response.json.return_value = { + "data": { + "type": "organization-memberships", + "id": "ou-withteams123", + "attributes": {"status": "invited"}, + "relationships": { + "teams": { + "data": [ + {"type": "teams", "id": "team-123"}, + {"type": "teams", "id": "team-456"}, + ] + }, + "user": {"data": {"type": "users", "id": "user-xyz"}}, + "organization": { + "data": {"type": "organizations", "id": "test-org"} + }, + }, + } + } + mock_transport.request.return_value = mock_response + + team1 = Team(id="team-123") + team2 = Team(id="team-456") + options = OrganizationMembershipCreateOptions( + email="teamuser@example.com", teams=[team1, team2] + ) + membership = membership_service.create("test-org", options) + + assert membership.status == OrganizationMembershipStatus.INVITED + assert membership.teams is not None + assert len(membership.teams) == 2 + + def test_create_with_organization_access(self, membership_service, mock_transport): + """Test creating membership with team that has organization access.""" + mock_response = Mock() + mock_response.json.return_value = { + "data": { + "type": "organization-memberships", + "id": "ou-orgaccess123", + "attributes": {"status": "invited"}, + "relationships": { + "teams": {"data": [{"type": "teams", "id": "team-123"}]}, + "user": {"data": {"type": "users", "id": "user-abc"}}, + "organization": { + "data": {"type": "organizations", "id": "test-org"} + }, + }, + } + } + mock_transport.request.return_value = mock_response + + team = Team( + id="team-123", organization_access=OrganizationAccess(read_workspaces=True) + ) + options = OrganizationMembershipCreateOptions( + email="orgaccess@example.com", teams=[team] + ) + membership = membership_service.create("test-org", options) + + assert membership.status == OrganizationMembershipStatus.INVITED + assert membership.id == "ou-orgaccess123" + + def test_create_with_invalid_organization(self, membership_service): + """Test creating with invalid organization name.""" + options = OrganizationMembershipCreateOptions(email="user@example.com") + with pytest.raises(ValueError, match=ERR_INVALID_ORG): + membership_service.create("", options) + + def test_create_with_missing_email(self, membership_service): + """Test creating without required email.""" + options = OrganizationMembershipCreateOptions(email="") + with pytest.raises(ValueError, match=ERR_REQUIRED_EMAIL): + membership_service.create("test-org", options) + + def test_create_with_invalid_email(self, membership_service): + """Test creating with invalid email format.""" + options = OrganizationMembershipCreateOptions(email="not-an-email") + with pytest.raises(ValueError, match=ERR_INVALID_EMAIL): + membership_service.create("test-org", options) + + +class TestOrganizationMembershipRead: + """Test suite for organization membership read operations.""" + + @pytest.fixture + def mock_transport(self): + """Mock HTTP transport.""" + transport = Mock() + return transport + + @pytest.fixture + def membership_service(self, mock_transport): + """Create organization membership service with mocked transport.""" + return OrganizationMemberships(mock_transport) + + @pytest.fixture + def sample_read_response(self): + """Sample JSON:API read response.""" + return { + "data": { + "type": "organization-memberships", + "id": "ou-abc123def456", + "attributes": {"status": "active"}, + "relationships": { + "teams": { + "data": [{"type": "teams", "id": "team-97LkM7QciNkwb2nh"}] + }, + "user": {"data": {"type": "users", "id": "user-123"}}, + "organization": { + "data": {"type": "organizations", "id": "org-test"} + }, + }, + } + } + + def test_read_when_membership_exists( + self, membership_service, mock_transport, sample_read_response + ): + """Test reading organization membership when it exists.""" + mock_response = Mock() + mock_response.json.return_value = sample_read_response + mock_transport.request.return_value = mock_response + + membership = membership_service.read("ou-abc123def456") + + assert membership is not None + assert membership.id == "ou-abc123def456" + assert membership.status == OrganizationMembershipStatus.ACTIVE + mock_transport.request.assert_called_once() + + def test_read_when_membership_not_found(self, membership_service, mock_transport): + """Test reading when membership does not exist.""" + mock_transport.request.side_effect = NotFound("not found", status=404) + + with pytest.raises(NotFound): + membership_service.read("ou-nonexisting") + + def test_read_with_invalid_membership_id(self, membership_service): + """Test reading with invalid membership ID.""" + with pytest.raises(ValueError, match="invalid organization membership ID"): + membership_service.read("") + + +class TestOrganizationMembershipReadWithOptions: + """Test suite for organization membership read with options operations.""" + + @pytest.fixture + def mock_transport(self): + """Mock HTTP transport.""" + transport = Mock() + return transport + + @pytest.fixture + def membership_service(self, mock_transport): + """Create organization membership service with mocked transport.""" + return OrganizationMemberships(mock_transport) + + @pytest.fixture + def sample_read_with_user_response(self): + """Sample JSON:API read response with user included.""" + return { + "data": { + "type": "organization-memberships", + "id": "ou-abc123def456", + "attributes": {"status": "active"}, + "relationships": { + "teams": { + "data": [{"type": "teams", "id": "team-97LkM7QciNkwb2nh"}] + }, + "user": {"data": {"type": "users", "id": "user-123"}}, + "organization": { + "data": {"type": "organizations", "id": "org-test"} + }, + }, + }, + "included": [ + { + "type": "users", + "id": "user-123", + "attributes": { + "username": "testuser", + "is-service-account": False, + "avatar-url": "https://www.gravatar.com/avatar/test?s=100&d=mm", + "two-factor": {"enabled": False, "verified": False}, + "email": "user@example.com", + "permissions": { + "can-create-organizations": True, + "can-change-email": True, + }, + }, + "relationships": { + "authentication-tokens": { + "links": { + "related": "/api/v2/users/user-123/authentication-tokens" + } + } + }, + "links": {"self": "/api/v2/users/user-123"}, + } + ], + } + + def test_read_with_options_include_user( + self, membership_service, mock_transport, sample_read_with_user_response + ): + """Test reading with include user option.""" + mock_response = Mock() + mock_response.json.return_value = sample_read_with_user_response + mock_transport.request.return_value = mock_response + + options = OrganizationMembershipReadOptions( + include=[OrgMembershipIncludeOpt.USER] + ) + membership = membership_service.read_with_options("ou-abc123def456", options) + + assert membership is not None + assert membership.id == "ou-abc123def456" + assert membership.user is not None + + def test_read_with_options_include_teams(self, membership_service, mock_transport): + """Test reading with include teams option.""" + mock_response = Mock() + mock_response.json.return_value = { + "data": { + "type": "organization-memberships", + "id": "ou-abc123def456", + "attributes": {"status": "active"}, + "relationships": { + "teams": { + "data": [ + {"type": "teams", "id": "team-123"}, + {"type": "teams", "id": "team-456"}, + ] + }, + "user": {"data": {"type": "users", "id": "user-123"}}, + "organization": { + "data": {"type": "organizations", "id": "org-test"} + }, + }, + } + } + mock_transport.request.return_value = mock_response + + options = OrganizationMembershipReadOptions( + include=[OrgMembershipIncludeOpt.TEAMS] + ) + membership = membership_service.read_with_options("ou-abc123def456", options) + + assert membership is not None + assert membership.teams is not None + assert len(membership.teams) == 2 + + def test_read_with_options_without_options( + self, membership_service, mock_transport + ): + """Test reading with empty options.""" + mock_response = Mock() + mock_response.json.return_value = { + "data": { + "type": "organization-memberships", + "id": "ou-abc123def456", + "attributes": {"status": "active"}, + "relationships": { + "teams": { + "data": [{"type": "teams", "id": "team-97LkM7QciNkwb2nh"}] + }, + "user": {"data": {"type": "users", "id": "user-123"}}, + "organization": { + "data": {"type": "organizations", "id": "org-test"} + }, + }, + } + } + mock_transport.request.return_value = mock_response + + options = OrganizationMembershipReadOptions() + membership = membership_service.read_with_options("ou-abc123def456", options) + + assert membership is not None + assert membership.id == "ou-abc123def456" + + def test_read_with_options_not_found(self, membership_service, mock_transport): + """Test reading with options when membership doesn't exist.""" + mock_transport.request.side_effect = NotFound("not found", status=404) + + options = OrganizationMembershipReadOptions( + include=[OrgMembershipIncludeOpt.USER] + ) + with pytest.raises(NotFound): + membership_service.read_with_options("ou-nonexisting", options) + + def test_read_with_options_invalid_id(self, membership_service): + """Test reading with options with invalid membership ID.""" + options = OrganizationMembershipReadOptions() + with pytest.raises(ValueError, match="invalid organization membership ID"): + membership_service.read_with_options("", options) + + +class TestOrganizationMembershipDelete: + """Test suite for organization membership delete operations.""" + + @pytest.fixture + def mock_transport(self): + """Mock HTTP transport.""" + transport = Mock() + return transport + + @pytest.fixture + def membership_service(self, mock_transport): + """Create organization membership service with mocked transport.""" + return OrganizationMemberships(mock_transport) + + def test_delete_with_valid_id(self, membership_service, mock_transport): + """Test deleting organization membership with valid ID.""" + mock_response = Mock() + mock_response.status_code = 204 + mock_transport.request.return_value = mock_response + + membership_service.delete("ou-abc123def456") + + mock_transport.request.assert_called_once() + call_args = mock_transport.request.call_args + assert call_args[0][0] == "DELETE" + + def test_delete_with_invalid_id(self, membership_service): + """Test deleting with invalid membership ID.""" + with pytest.raises(ValueError, match="invalid organization membership ID"): + membership_service.delete("") + + def test_delete_nonexistent_membership(self, membership_service, mock_transport): + """Test deleting a membership that doesn't exist.""" + mock_transport.request.side_effect = NotFound("not found", status=404) + + with pytest.raises(NotFound): + membership_service.delete("ou-nonexisting") + + +class TestOrganizationMembershipValidation: + """Test suite for organization membership validation.""" + + @pytest.fixture + def mock_transport(self): + """Mock HTTP transport.""" + transport = Mock() + return transport + + @pytest.fixture + def membership_service(self, mock_transport): + """Create organization membership service with mocked transport.""" + return OrganizationMemberships(mock_transport) + + def test_validate_email_format(self, membership_service): + """Test email validation with invalid formats.""" + invalid_emails = [ + "not-an-email", + "@example.com", + "user@", + "user", + "", + ] + + for email in invalid_emails: + options = OrganizationMembershipCreateOptions(email=email) + with pytest.raises(ValueError): + membership_service.create("test-org", options) + + def test_validate_valid_email_format(self, membership_service, mock_transport): + """Test email validation with valid formats.""" + mock_response = Mock() + mock_response.json.return_value = { + "data": { + "type": "organization-memberships", + "id": "ou-test", + "attributes": {"status": "invited"}, + "relationships": { + "teams": {"data": [{"type": "teams", "id": "team-abc"}]}, + "user": {"data": {"type": "users", "id": "user-xyz"}}, + "organization": { + "data": {"type": "organizations", "id": "test-org"} + }, + }, + } + } + mock_transport.request.return_value = mock_response + + valid_emails = [ + "user@example.com", + "user.name@example.com", + "user+tag@example.co.uk", + ] + + for email in valid_emails: + options = OrganizationMembershipCreateOptions(email=email) + membership = membership_service.create("test-org", options) + assert membership is not None + + +class TestOrganizationMembershipIntegration: + """Integration tests for complete workflows.""" + + @pytest.fixture + def mock_transport(self): + """Mock HTTP transport.""" + transport = Mock() + return transport + + @pytest.fixture + def membership_service(self, mock_transport): + """Create organization membership service with mocked transport.""" + return OrganizationMemberships(mock_transport) + + def test_create_read_delete_workflow(self, membership_service, mock_transport): + """Test complete workflow: create, read, then delete.""" + # Mock create response + create_response = Mock() + create_response.json.return_value = { + "data": { + "type": "organization-memberships", + "id": "ou-workflow123", + "attributes": {"status": "invited"}, + "relationships": { + "teams": {"data": [{"type": "teams", "id": "team-abc"}]}, + "user": {"data": {"type": "users", "id": "user-xyz"}}, + "organization": { + "data": {"type": "organizations", "id": "test-org"} + }, + }, + } + } + + # Mock read response + read_response = Mock() + read_response.json.return_value = { + "data": { + "type": "organization-memberships", + "id": "ou-workflow123", + "attributes": {"status": "invited"}, + "relationships": { + "teams": {"data": [{"type": "teams", "id": "team-abc"}]}, + "user": {"data": {"type": "users", "id": "user-xyz"}}, + "organization": { + "data": {"type": "organizations", "id": "test-org"} + }, + }, + } + } + + # Mock delete response + delete_response = Mock() + delete_response.status_code = 204 + + mock_transport.request.side_effect = [ + create_response, + read_response, + delete_response, + ] + + # Create + options = OrganizationMembershipCreateOptions(email="workflow@example.com") + created = membership_service.create("test-org", options) + assert created.id == "ou-workflow123" + + # Read + read_membership = membership_service.read("ou-workflow123") + assert read_membership.id == created.id + + # Delete + membership_service.delete("ou-workflow123") + + assert mock_transport.request.call_count == 3 + + def test_list_filter_and_read_workflow(self, membership_service, mock_transport): + """Test workflow: list with filters, then read specific member.""" + # Mock list response + list_response = Mock() + list_response.json.return_value = { + "data": [ + { + "type": "organization-memberships", + "id": "ou-member1", + "attributes": {"status": "active"}, + "relationships": { + "teams": { + "data": [{"type": "teams", "id": "team-yUrEehvfG4pdmSjc"}] + }, + "user": {"data": {"type": "users", "id": "user-123"}}, + "organization": { + "data": {"type": "organizations", "id": "test-org"} + }, + }, + } + ], + "meta": {"pagination": {"current-page": 1, "total-count": 1}}, + } + + # Mock read response + read_response = Mock() + read_response.json.return_value = { + "data": { + "type": "organization-memberships", + "id": "ou-member1", + "attributes": {"status": "active"}, + "relationships": { + "teams": { + "data": [{"type": "teams", "id": "team-yUrEehvfG4pdmSjc"}] + }, + "user": {"data": {"type": "users", "id": "user-123"}}, + "organization": { + "data": {"type": "organizations", "id": "test-org"} + }, + }, + } + } + + mock_transport.request.side_effect = [list_response, read_response] + + # List with filter + options = OrganizationMembershipListOptions( + status=OrganizationMembershipStatus.ACTIVE + ) + memberships = list(membership_service.list("test-org", options)) + assert len(memberships) == 1 + assert memberships[0].status == OrganizationMembershipStatus.ACTIVE + + # Read specific member with options + read_options = OrganizationMembershipReadOptions( + include=[OrgMembershipIncludeOpt.USER] + ) + member = membership_service.read_with_options(memberships[0].id, read_options) + assert member.user is not None + + assert mock_transport.request.call_count == 2 From 6d3f2d0ec4ff81f003cb81cd47196ed887d9fba9 Mon Sep 17 00:00:00 2001 From: Sivaselvan32 Date: Fri, 28 Nov 2025 14:44:46 +0530 Subject: [PATCH 07/18] feat(policy set parameters): modified the example --- examples/policy_set_parameter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/policy_set_parameter.py b/examples/policy_set_parameter.py index c2ffea3..9d6f662 100644 --- a/examples/policy_set_parameter.py +++ b/examples/policy_set_parameter.py @@ -158,7 +158,7 @@ def main(): # Delete the parameter client.policy_set_parameters.delete(args.policy_set_id, args.parameter_id) - print(f"\nโœ“ Successfully deleted parameter: {args.parameter_id}") + print(f"\n Successfully deleted parameter: {args.parameter_id}") # List remaining parameters _print_header("Listing parameters after deletion") From cf5a61d9917a8f4ed87740efd15b2fb9819f6468 Mon Sep 17 00:00:00 2001 From: aayushsingh2502 Date: Mon, 1 Dec 2025 11:42:17 +0530 Subject: [PATCH 08/18] code cleanup --- examples/organization_membership.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/organization_membership.py b/examples/organization_membership.py index 2fcc833..921c6d4 100644 --- a/examples/organization_membership.py +++ b/examples/organization_membership.py @@ -307,13 +307,13 @@ def main(): print(f" Attempting to delete membership: {membership_id}") client.organization_memberships.delete(membership_id) - print(f" โœ“ Successfully deleted membership {membership_id}") + print(f" Successfully deleted membership {membership_id}") except NotFound as e: - print(f" โœ— Membership not found: {e}") - print(" โ„น The membership may have already been deleted or the ID is invalid") + print(f" Membership not found: {e}") + print(" The membership may have already been deleted or the ID is invalid") except Exception as e: - print(f" โœ— Error deleting membership: {type(e).__name__}: {e}") + print(f" Error deleting membership: {type(e).__name__}: {e}") if __name__ == "__main__": From 1b1e38c7c00d294ce154095431934d8196561ab8 Mon Sep 17 00:00:00 2001 From: aayushsingh2502 Date: Mon, 1 Dec 2025 15:44:31 +0530 Subject: [PATCH 09/18] read with options update --- examples/organization_membership.py | 4 ---- src/pytfe/resources/organization_membership.py | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/examples/organization_membership.py b/examples/organization_membership.py index 921c6d4..0c8e1c0 100644 --- a/examples/organization_membership.py +++ b/examples/organization_membership.py @@ -82,7 +82,6 @@ def main(): print(f" {membership.email}") print(f" Processed {count} memberships (fetched in batches of 3)") - print(" Success: Pagination working correctly") except Exception as e: print(f" Error: {type(e).__name__}: {e}") @@ -105,7 +104,6 @@ def main(): print(f" {membership.email} (User ID: {user_id})") print(f" Processed {count} memberships, {users_found} with user data") - print(" Success: Include parameter working") except Exception as e: print(f" Error: {type(e).__name__}: {e}") @@ -129,7 +127,6 @@ def main(): if len(invited) == 0: print(" No invited members found") - print(" Success: Status filter working") except Exception as e: print(f" Error: {type(e).__name__}: {e}") @@ -263,7 +260,6 @@ def main(): print(f" User ID: {user_id}") team_count = len(membership.teams) if membership.teams else 0 print(f" Teams: {team_count}") - print(" Success: Read with options working") else: print(" Skipped: No memberships available from Test 1") except Exception as e: diff --git a/src/pytfe/resources/organization_membership.py b/src/pytfe/resources/organization_membership.py index 11d7eb2..659608b 100644 --- a/src/pytfe/resources/organization_membership.py +++ b/src/pytfe/resources/organization_membership.py @@ -171,7 +171,7 @@ def read(self, organization_membership_id: str) -> OrganizationMembership: def read_with_options( self, organization_membership_id: str, - options: OrganizationMembershipReadOptions, + options: OrganizationMembershipReadOptions | None = None, ) -> OrganizationMembership: """Read an organization membership by ID with options. From d44c4428cd2d95a8f4137b2da4d27bd811156e70 Mon Sep 17 00:00:00 2001 From: KshitijaChoudhari Date: Wed, 3 Dec 2025 15:53:26 +0530 Subject: [PATCH 10/18] feat: add workspace resources functionality - Add WorkspaceResource model and WorkspaceResourceListOptions - Add WorkspaceResourcesService for listing workspace resources - Add workspace_resources.py example CLI tool with flag-based interface - Add comprehensive unit tests for workspace resources - Update client.py to include workspace_resources service - Update models/__init__.py with WorkspaceResource exports --- examples/workspace_resources.py | 130 +++++++++ src/pytfe/client.py | 2 + src/pytfe/models/__init__.py | 9 + src/pytfe/models/workspace_resource.py | 29 ++ src/pytfe/resources/workspace_resources.py | 69 +++++ tests/units/test_workspace_resources.py | 297 +++++++++++++++++++++ 6 files changed, 536 insertions(+) create mode 100644 examples/workspace_resources.py create mode 100644 src/pytfe/models/workspace_resource.py create mode 100644 src/pytfe/resources/workspace_resources.py create mode 100644 tests/units/test_workspace_resources.py diff --git a/examples/workspace_resources.py b/examples/workspace_resources.py new file mode 100644 index 0000000..730e24d --- /dev/null +++ b/examples/workspace_resources.py @@ -0,0 +1,130 @@ +"""Example script for working with workspace resources in Terraform Enterprise. + +This script demonstrates how to list resources within a workspace. +""" + +import argparse +import sys + +from pytfe import TFEClient +from pytfe.models import WorkspaceResourceListOptions + + +def list_workspace_resources( + client: TFEClient, + workspace_id: str, + page_number: int | None = None, + page_size: int | None = None, +) -> None: + """List all resources in a workspace.""" + try: + print(f"Listing resources for workspace: {workspace_id}") + + # Prepare list options + options = None + if page_number or page_size: + options = WorkspaceResourceListOptions() + if page_number: + options.page_number = page_number + if page_size: + options.page_size = page_size + + # List workspace resources (returns an iterator) + resources = list(client.workspace_resources.list(workspace_id, options)) + + if not resources: + print("No resources found in this workspace.") + return + + print(f"\nFound {len(resources)} resource(s):") + print("-" * 80) + + for resource in resources: + print(f"ID: {resource.id}") + print(f"Address: {resource.address}") + print(f"Name: {resource.name}") + print(f"Module: {resource.module}") + print(f"Provider: {resource.provider}") + print(f"Provider Type: {resource.provider_type}") + print(f"Created At: {resource.created_at}") + print(f"Updated At: {resource.updated_at}") + print(f"Modified By State Version: {resource.modified_by_state_version_id}") + if resource.name_index: + print(f"Name Index: {resource.name_index}") + print("-" * 80) + + except Exception as e: + print(f"Error listing workspace resources: {e}", file=sys.stderr) + sys.exit(1) + + +def main(): + """Main function to handle command line arguments and execute operations.""" + parser = argparse.ArgumentParser( + description="Manage workspace resources in Terraform Enterprise", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # List all resources in a workspace + python workspace_resources.py --list --workspace-id ws-abc123 + + # List with pagination + python workspace_resources.py --list --workspace-id ws-abc123 --page-number 2 --page-size 50 + +Environment variables: + TFE_TOKEN: Your Terraform Enterprise API token + TFE_URL: Your Terraform Enterprise URL (default: https://app.terraform.io) + TFE_ORG: Your Terraform Enterprise organization name + """, + ) + + # Add command flags + parser.add_argument( + "--list", + action="store_true", + help="List workspace resources" + ) + parser.add_argument( + "--workspace-id", + required=True, + help="ID of the workspace (required, e.g., ws-abc123)" + ) + parser.add_argument( + "--page-number", + type=int, + help="Page number for pagination" + ) + parser.add_argument( + "--page-size", + type=int, + help="Page size for pagination" + ) + + args = parser.parse_args() + + if not args.list: + parser.print_help() + sys.exit(1) + + # Initialize TFE client + try: + client = TFEClient() + except Exception as e: + print(f"Error initializing TFE client: {e}", file=sys.stderr) + print( + "Make sure TFE_TOKEN and TFE_URL environment variables are set.", + file=sys.stderr, + ) + sys.exit(1) + + # Execute the list command + list_workspace_resources( + client, + args.workspace_id, + args.page_number, + args.page_size, + ) + + +if __name__ == "__main__": + main() diff --git a/src/pytfe/client.py b/src/pytfe/client.py index 7aa1bd8..f50cdfe 100644 --- a/src/pytfe/client.py +++ b/src/pytfe/client.py @@ -33,6 +33,7 @@ from .resources.state_versions import StateVersions from .resources.variable import Variables from .resources.variable_sets import VariableSets, VariableSetVariables +from .resources.workspace_resources import WorkspaceResourcesService from .resources.workspaces import Workspaces @@ -72,6 +73,7 @@ def __init__(self, config: TFEConfig | None = None): self.variable_sets = VariableSets(self._transport) self.variable_set_variables = VariableSetVariables(self._transport) self.workspaces = Workspaces(self._transport) + self.workspace_resources = WorkspaceResourcesService(self._transport) self.registry_modules = RegistryModules(self._transport) self.registry_providers = RegistryProviders(self._transport) diff --git a/src/pytfe/models/__init__.py b/src/pytfe/models/__init__.py index a3cc71f..c70dd05 100644 --- a/src/pytfe/models/__init__.py +++ b/src/pytfe/models/__init__.py @@ -353,6 +353,12 @@ WorkspaceUpdateRemoteStateConsumersOptions, ) +# โ”€โ”€ Workspace Resources โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +from .workspace_resource import ( + WorkspaceResource, + WorkspaceResourceListOptions, +) + # โ”€โ”€ Public surface โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ __all__ = [ # OAuth @@ -524,6 +530,9 @@ "WorkspaceTagListOptions", "WorkspaceUpdateOptions", "WorkspaceUpdateRemoteStateConsumersOptions", + # Workspace Resources + "WorkspaceResource", + "WorkspaceResourceListOptions", "RunQueue", "ReadRunQueueOptions", # Runs diff --git a/src/pytfe/models/workspace_resource.py b/src/pytfe/models/workspace_resource.py new file mode 100644 index 0000000..78eaa40 --- /dev/null +++ b/src/pytfe/models/workspace_resource.py @@ -0,0 +1,29 @@ +"""Workspace resources models for Terraform Enterprise.""" + +from pydantic import BaseModel + + +class WorkspaceResource(BaseModel): + """Represents a Terraform Enterprise workspace resource. + + These are resources managed by Terraform in a workspace's state. + """ + + id: str + address: str + name: str + created_at: str + updated_at: str + module: str + provider: str + provider_type: str + modified_by_state_version_id: str + name_index: str | None = None + + +class WorkspaceResourceListOptions(BaseModel): + """Options for listing workspace resources.""" + + # Pagination + page_number: int | None = None + page_size: int | None = None diff --git a/src/pytfe/resources/workspace_resources.py b/src/pytfe/resources/workspace_resources.py new file mode 100644 index 0000000..ff3e0ee --- /dev/null +++ b/src/pytfe/resources/workspace_resources.py @@ -0,0 +1,69 @@ +"""Workspace resources service for Terraform Enterprise.""" + +import urllib.parse +from collections.abc import Iterator +from typing import Any + +from pytfe.models import ( + WorkspaceResource, + WorkspaceResourceListOptions, +) + +from ._base import _Service + + +def _workspace_resource_from(data: dict[str, Any]) -> WorkspaceResource: + """Convert API response data to WorkspaceResource model.""" + attributes = data.get("attributes", {}) + + return WorkspaceResource( + id=data.get("id", ""), + address=attributes.get("address", ""), + name=attributes.get("name", ""), + created_at=attributes.get("created-at", ""), + updated_at=attributes.get("updated-at", ""), + module=attributes.get("module", ""), + provider=attributes.get("provider", ""), + provider_type=attributes.get("provider-type", ""), + modified_by_state_version_id=attributes.get("modified-by-state-version-id", ""), + name_index=attributes.get("name-index"), + ) + + +class WorkspaceResourcesService(_Service): + """Service for managing workspace resources in Terraform Enterprise. + + Workspace resources represent the infrastructure resources + managed by Terraform in a workspace's state file. + """ + + def list( + self, workspace_id: str, options: WorkspaceResourceListOptions | None = None + ) -> Iterator[WorkspaceResource]: + """List workspace resources for a given workspace. + + Args: + workspace_id: The ID of the workspace to list resources for + options: Optional query parameters for filtering and pagination + + Yields: + WorkspaceResource objects + """ + if not workspace_id or not workspace_id.strip(): + raise ValueError("workspace_id is required") + + # URL encode the workspace ID and construct URL + encoded_workspace_id = urllib.parse.quote(workspace_id, safe="") + url = f"/api/v2/workspaces/{encoded_workspace_id}/resources" + + # Handle parameters + params: dict[str, int] = {} + if options: + if options.page_number is not None: + params["page[number]"] = options.page_number + if options.page_size is not None: + params["page[size]"] = options.page_size + + # Use the _list method from base service to handle pagination + for item in self._list(url, params=params): + yield _workspace_resource_from(item) diff --git a/tests/units/test_workspace_resources.py b/tests/units/test_workspace_resources.py new file mode 100644 index 0000000..05f7c15 --- /dev/null +++ b/tests/units/test_workspace_resources.py @@ -0,0 +1,297 @@ +"""Unit tests for workspace resources service.""" + +from unittest.mock import Mock + +import pytest + +from pytfe.models.workspace_resource import ( + WorkspaceResource, + WorkspaceResourceListOptions, +) +from pytfe.resources.workspace_resources import WorkspaceResourcesService + + +class TestWorkspaceResourcesService: + """Test suite for WorkspaceResourcesService.""" + + @pytest.fixture + def mock_transport(self): + """Create a mock transport for testing.""" + return Mock() + + @pytest.fixture + def service(self, mock_transport): + """Create a WorkspaceResourcesService instance for testing.""" + return WorkspaceResourcesService(mock_transport) + + @pytest.fixture + def sample_workspace_resource_response(self): + """Sample API response for workspace resources list.""" + return { + "data": [ + { + "id": "resource-1", + "type": "resources", + "attributes": { + "address": "media_bucket.aws_s3_bucket_public_access_block.this[0]", + "name": "this", + "created-at": "2023-01-01T00:00:00Z", + "updated-at": "2023-01-01T00:00:00Z", + "module": "media_bucket", + "provider": "hashicorp/aws", + "provider-type": "aws", + "modified-by-state-version-id": "sv-abc123", + "name-index": "0", + }, + }, + { + "id": "resource-2", + "type": "resources", + "attributes": { + "address": "aws_instance.example", + "name": "example", + "created-at": "2023-01-02T00:00:00Z", + "updated-at": "2023-01-02T00:00:00Z", + "module": "root", + "provider": "hashicorp/aws", + "provider-type": "aws", + "modified-by-state-version-id": "sv-def456", + "name-index": None, + }, + }, + ], + "meta": { + "pagination": { + "current_page": 1, + "total_pages": 1, + "total_count": 2, + "page_size": 20, + } + }, + } + + @pytest.fixture + def sample_empty_response(self): + """Sample API response for empty workspace resources list.""" + return { + "data": [], + "meta": { + "pagination": { + "current_page": 1, + "total_pages": 1, + "total_count": 0, + "page_size": 20, + } + }, + } + + def test_list_workspace_resources_success( + self, service, mock_transport, sample_workspace_resource_response + ): + """Test successful listing of workspace resources.""" + # Mock the transport response + mock_response = Mock() + mock_response.json.return_value = sample_workspace_resource_response + mock_transport.request.return_value = mock_response + + # Call the service + result = list(service.list("ws-abc123")) + + # Verify request was made correctly + mock_transport.request.assert_called_once_with( + "GET", + "/api/v2/workspaces/ws-abc123/resources", + params={"page[number]": 1, "page[size]": 100}, + ) + + # Verify response parsing + assert isinstance(result, list) + assert len(result) == 2 + + # Check first resource + resource1 = result[0] + assert isinstance(resource1, WorkspaceResource) + assert resource1.id == "resource-1" + assert ( + resource1.address + == "media_bucket.aws_s3_bucket_public_access_block.this[0]" + ) + assert resource1.name == "this" + assert resource1.module == "media_bucket" + assert resource1.provider == "hashicorp/aws" + assert resource1.provider_type == "aws" + assert resource1.modified_by_state_version_id == "sv-abc123" + assert resource1.name_index == "0" + assert resource1.created_at == "2023-01-01T00:00:00Z" + assert resource1.updated_at == "2023-01-01T00:00:00Z" + + # Check second resource + resource2 = result[1] + assert resource2.id == "resource-2" + assert resource2.address == "aws_instance.example" + assert resource2.name == "example" + assert resource2.module == "root" + assert resource2.name_index is None + + def test_list_workspace_resources_with_options( + self, service, mock_transport, sample_workspace_resource_response + ): + """Test listing workspace resources with pagination options.""" + # Mock the transport response + mock_response = Mock() + mock_response.json.return_value = sample_workspace_resource_response + mock_transport.request.return_value = mock_response + + # Create options + options = WorkspaceResourceListOptions(page_number=2, page_size=50) + + # Call the service + result = list(service.list("ws-abc123", options)) + + # Verify request was made correctly + mock_transport.request.assert_called_once_with( + "GET", + "/api/v2/workspaces/ws-abc123/resources", + params={"page[number]": 2, "page[size]": 50}, + ) + + # Verify response + assert isinstance(result, list) + assert len(result) == 2 + + def test_list_workspace_resources_empty( + self, service, mock_transport, sample_empty_response + ): + """Test listing workspace resources when no resources exist.""" + # Mock the transport response + mock_response = Mock() + mock_response.json.return_value = sample_empty_response + mock_transport.request.return_value = mock_response + + # Call the service + result = list(service.list("ws-abc123")) + + # Verify request was made correctly + mock_transport.request.assert_called_once_with( + "GET", + "/api/v2/workspaces/ws-abc123/resources", + params={"page[number]": 1, "page[size]": 100}, + ) + + # Verify response + assert isinstance(result, list) + assert len(result) == 0 + + def test_list_workspace_resources_invalid_workspace_id(self, service): + """Test listing workspace resources with invalid workspace ID.""" + with pytest.raises(ValueError, match="workspace_id is required"): + list(service.list("")) + + with pytest.raises(ValueError, match="workspace_id is required"): + list(service.list(None)) + + def test_list_workspace_resources_url_encoding( + self, service, mock_transport, sample_workspace_resource_response + ): + """Test that workspace ID is properly URL encoded.""" + # Mock the transport response + mock_response = Mock() + mock_response.json.return_value = sample_workspace_resource_response + mock_transport.request.return_value = mock_response + + # Call with workspace ID that needs encoding + list(service.list("ws-abc/123")) + + # Verify the URL was properly encoded + mock_transport.request.assert_called_once_with( + "GET", + "/api/v2/workspaces/ws-abc%2F123/resources", + params={"page[number]": 1, "page[size]": 100}, + ) + + def test_list_workspace_resources_malformed_response(self, service, mock_transport): + """Test handling of malformed API response.""" + # Mock malformed response + mock_response = Mock() + mock_response.json.return_value = {"invalid": "response"} + mock_transport.request.return_value = mock_response + + # Call the service + result = list(service.list("ws-abc123")) + + # Should handle gracefully and return empty list + assert isinstance(result, list) + assert len(result) == 0 + + def test_list_workspace_resources_api_error(self, service, mock_transport): + """Test handling of API errors.""" + # Mock API error + mock_transport.request.side_effect = Exception("API Error") + + # Should propagate the exception + with pytest.raises(Exception, match="API Error"): + list(service.list("ws-abc123")) + + +class TestWorkspaceResourceModel: + """Test suite for WorkspaceResource model.""" + + def test_workspace_resource_creation(self): + """Test creating a WorkspaceResource instance.""" + resource = WorkspaceResource( + id="resource-1", + address="aws_instance.example", + name="example", + created_at="2023-01-01T00:00:00Z", + updated_at="2023-01-01T00:00:00Z", + module="root", + provider="hashicorp/aws", + provider_type="aws", + modified_by_state_version_id="sv-abc123", + name_index="0", + ) + + assert resource.id == "resource-1" + assert resource.address == "aws_instance.example" + assert resource.name == "example" + assert resource.module == "root" + assert resource.provider == "hashicorp/aws" + assert resource.provider_type == "aws" + assert resource.modified_by_state_version_id == "sv-abc123" + assert resource.name_index == "0" + + def test_workspace_resource_optional_fields(self): + """Test WorkspaceResource with optional fields.""" + resource = WorkspaceResource( + id="resource-1", + address="aws_instance.example", + name="example", + created_at="2023-01-01T00:00:00Z", + updated_at="2023-01-01T00:00:00Z", + module="root", + provider="hashicorp/aws", + provider_type="aws", + modified_by_state_version_id="sv-abc123", + # name_index is optional + ) + + assert resource.name_index is None + + +class TestWorkspaceResourceListOptions: + """Test suite for WorkspaceResourceListOptions model.""" + + def test_workspace_resource_list_options_creation(self): + """Test creating WorkspaceResourceListOptions.""" + options = WorkspaceResourceListOptions(page_number=2, page_size=50) + + assert options.page_number == 2 + assert options.page_size == 50 + + def test_workspace_resource_list_options_defaults(self): + """Test WorkspaceResourceListOptions with defaults.""" + options = WorkspaceResourceListOptions() + + # Should use default values from BaseListOptions + assert options.page_number is None + assert options.page_size is None From 463134d0ecee62419faa319a29072382f2061d4f Mon Sep 17 00:00:00 2001 From: KshitijaChoudhari Date: Wed, 3 Dec 2025 16:06:48 +0530 Subject: [PATCH 11/18] style: format workspace_resources.py with ruff --- examples/workspace_resources.py | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/examples/workspace_resources.py b/examples/workspace_resources.py index 730e24d..15fbf1b 100644 --- a/examples/workspace_resources.py +++ b/examples/workspace_resources.py @@ -79,26 +79,14 @@ def main(): ) # Add command flags - parser.add_argument( - "--list", - action="store_true", - help="List workspace resources" - ) + parser.add_argument("--list", action="store_true", help="List workspace resources") parser.add_argument( "--workspace-id", required=True, - help="ID of the workspace (required, e.g., ws-abc123)" - ) - parser.add_argument( - "--page-number", - type=int, - help="Page number for pagination" - ) - parser.add_argument( - "--page-size", - type=int, - help="Page size for pagination" + help="ID of the workspace (required, e.g., ws-abc123)", ) + parser.add_argument("--page-number", type=int, help="Page number for pagination") + parser.add_argument("--page-size", type=int, help="Page size for pagination") args = parser.parse_args() From 446a4d0c0aaf551e780771e31c30f65fcb6f3b23 Mon Sep 17 00:00:00 2001 From: KshitijaChoudhari Date: Wed, 3 Dec 2025 16:34:12 +0530 Subject: [PATCH 12/18] refactor: remove unnecessary URL encoding from workspace_resources - Remove urllib.parse import - Remove URL encoding for workspace_id parameter - Workspace IDs are alphanumeric with hyphens, no encoding needed - Consistent with other services in the codebase --- src/pytfe/resources/workspace_resources.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/pytfe/resources/workspace_resources.py b/src/pytfe/resources/workspace_resources.py index ff3e0ee..617b5f7 100644 --- a/src/pytfe/resources/workspace_resources.py +++ b/src/pytfe/resources/workspace_resources.py @@ -1,6 +1,5 @@ """Workspace resources service for Terraform Enterprise.""" -import urllib.parse from collections.abc import Iterator from typing import Any @@ -52,9 +51,7 @@ def list( if not workspace_id or not workspace_id.strip(): raise ValueError("workspace_id is required") - # URL encode the workspace ID and construct URL - encoded_workspace_id = urllib.parse.quote(workspace_id, safe="") - url = f"/api/v2/workspaces/{encoded_workspace_id}/resources" + url = f"/api/v2/workspaces/{workspace_id}/resources" # Handle parameters params: dict[str, int] = {} From 2e9847eea31efc987c2703c6d3c8fbb4d2c63abe Mon Sep 17 00:00:00 2001 From: KshitijaChoudhari Date: Wed, 3 Dec 2025 16:36:58 +0530 Subject: [PATCH 13/18] test: remove URL encoding test from workspace_resources - Remove test_list_workspace_resources_url_encoding test - URL encoding is not needed for workspace IDs - All 376 tests now passing --- tests/units/test_workspace_resources.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/tests/units/test_workspace_resources.py b/tests/units/test_workspace_resources.py index 05f7c15..f685d84 100644 --- a/tests/units/test_workspace_resources.py +++ b/tests/units/test_workspace_resources.py @@ -190,25 +190,6 @@ def test_list_workspace_resources_invalid_workspace_id(self, service): with pytest.raises(ValueError, match="workspace_id is required"): list(service.list(None)) - def test_list_workspace_resources_url_encoding( - self, service, mock_transport, sample_workspace_resource_response - ): - """Test that workspace ID is properly URL encoded.""" - # Mock the transport response - mock_response = Mock() - mock_response.json.return_value = sample_workspace_resource_response - mock_transport.request.return_value = mock_response - - # Call with workspace ID that needs encoding - list(service.list("ws-abc/123")) - - # Verify the URL was properly encoded - mock_transport.request.assert_called_once_with( - "GET", - "/api/v2/workspaces/ws-abc%2F123/resources", - params={"page[number]": 1, "page[size]": 100}, - ) - def test_list_workspace_resources_malformed_response(self, service, mock_transport): """Test handling of malformed API response.""" # Mock malformed response From 67b127614cc278e8b176e846790eefd7e38d8176 Mon Sep 17 00:00:00 2001 From: aayushsingh2502 Date: Mon, 15 Dec 2025 00:06:27 +0530 Subject: [PATCH 14/18] extra spaces removed --- examples/agent.py | 30 +- examples/agent_pool.py | 14 +- examples/apply.py | 6 +- examples/configuration_version.py | 366 ++++++++++++------------- examples/notification_configuration.py | 76 +++-- examples/oauth_client.py | 110 ++++---- examples/oauth_token.py | 90 +++--- examples/org.py | 164 ++++++----- examples/organization_membership.py | 130 +++++---- examples/policy.py | 12 +- examples/policy_check.py | 40 +-- examples/policy_evaluation.py | 32 +-- examples/policy_set.py | 4 +- examples/policy_set_parameter.py | 44 +-- examples/project.py | 94 +++---- examples/registry_module.py | 194 +++++++------ examples/registry_provider.py | 90 +++--- examples/reserved_tag_key.py | 18 +- examples/run.py | 42 ++- examples/run_events.py | 6 +- examples/run_task.py | 72 +++-- examples/run_trigger.py | 60 ++-- examples/ssh_keys.py | 70 ++--- examples/variable_sets.py | 30 +- examples/variables.py | 104 ++++--- examples/workspace.py | 148 +++++----- 26 files changed, 998 insertions(+), 1048 deletions(-) diff --git a/examples/agent.py b/examples/agent.py index 5b0990c..c80eb62 100644 --- a/examples/agent.py +++ b/examples/agent.py @@ -36,11 +36,11 @@ def main(): address = os.environ.get("TFE_ADDRESS", "https://app.terraform.io") if not token: - print(" TFE_TOKEN environment variable is required") + print("TFE_TOKEN environment variable is required") return 1 if not org: - print(" TFE_ORG environment variable is required") + print("TFE_ORG environment variable is required") return 1 # Create TFE client @@ -58,7 +58,7 @@ def main(): # Convert to list to check if empty and get count pool_list = list(agent_pools) if not pool_list: - print(" No agent pools found. Create an agent pool first.") + print("No agent pools found. Create an agent pool first.") return 1 print(f"Found {len(pool_list)} agent pools:") @@ -81,27 +81,27 @@ def main(): if agent_list: total_agents += len(agent_list) for agent in agent_list: - print(f" - Agent {agent.id}") - print(f" Name: {agent.name or 'Unnamed'}") - print(f" Status: {agent.status}") - print(f" Version: {agent.version or 'Unknown'}") - print(f" IP: {agent.ip_address or 'Unknown'}") - print(f" Last Ping: {agent.last_ping_at or 'Never'}") + print(f"Agent {agent.id}") + print(f"Name: {agent.name or 'Unnamed'}") + print(f"Status: {agent.status}") + print(f"Version: {agent.version or 'Unknown'}") + print(f"IP: {agent.ip_address or 'Unknown'}") + print(f"Last Ping: {agent.last_ping_at or 'Never'}") # Example 3: Read detailed agent information try: agent_details = client.agents.read(agent.id) - print(" Agent details retrieved successfully") - print(f" Full name: {agent_details.name or 'Unnamed'}") - print(f" Current status: {agent_details.status}") + print("Agent details retrieved successfully") + print(f"Full name: {agent_details.name or 'Unnamed'}") + print(f"Current status: {agent_details.status}") except NotFound: - print(" Agent details not accessible") + print("Agent details not accessible") except Exception as e: - print(f" Error reading agent details: {e}") + print(f"Error reading agent details: {e}") print("") else: - print(" No agents found in this pool") + print("No agents found in this pool") if total_agents == 0: print("\n No agents found in any pools.") diff --git a/examples/agent_pool.py b/examples/agent_pool.py index acc9750..bbaf14e 100644 --- a/examples/agent_pool.py +++ b/examples/agent_pool.py @@ -39,11 +39,11 @@ def main(): address = os.environ.get("TFE_ADDRESS", "https://app.terraform.io") if not token: - print(" TFE_TOKEN environment variable is required") + print("TFE_TOKEN environment variable is required") return 1 if not org: - print(" TFE_ORG environment variable is required") + print("TFE_ORG environment variable is required") return 1 # Create TFE client @@ -81,10 +81,10 @@ def main(): # Example 3: Read the agent pool print("\n Reading agent pool details...") pool_details = client.agent_pools.read(new_pool.id) - print(f" Name: {pool_details.name}") - print(f" Organization Scoped: {pool_details.organization_scoped}") - print(f" Policy: {pool_details.allowed_workspace_policy}") - print(f" Agent Count: {pool_details.agent_count}") + print(f"Name: {pool_details.name}") + print(f"Organization Scoped: {pool_details.organization_scoped}") + print(f"Policy: {pool_details.allowed_workspace_policy}") + print(f"Agent Count: {pool_details.agent_count}") # Example 4: Update the agent pool print("\n Updating agent pool...") @@ -118,7 +118,7 @@ def main(): print(f" - {token.description or 'No description'} (ID: {token.id})") # Example 7: Clean up - delete the token and pool - print("\n๐Ÿงน Cleaning up...") + print("\n Cleaning up...") client.agent_tokens.delete(agent_token.id) print("Deleted agent token") diff --git a/examples/apply.py b/examples/apply.py index cf280ef..44fd443 100644 --- a/examples/apply.py +++ b/examples/apply.py @@ -39,9 +39,9 @@ def main(): # Display timestamp details if available if apply.status_timestamps: - print(f" Queued At: {apply.status_timestamps.queued_at}") - print(f" Started At: {apply.status_timestamps.started_at}") - print(f" Finished At: {apply.status_timestamps.finished_at}") + print(f"Queued At: {apply.status_timestamps.queued_at}") + print(f"Started At: {apply.status_timestamps.started_at}") + print(f"Finished At: {apply.status_timestamps.finished_at}") except Exception as e: print(f"Error reading apply: {e}") return 1 diff --git a/examples/configuration_version.py b/examples/configuration_version.py index 92840b4..87fa6d1 100644 --- a/examples/configuration_version.py +++ b/examples/configuration_version.py @@ -191,33 +191,31 @@ def main(): try: # Basic list without options cv_list = list(client.configuration_versions.list(workspace_id)) - print(f" Found {len(cv_list)} configuration versions") + print(f"Found {len(cv_list)} configuration versions") if cv_list: - print(" Recent configuration versions:") + print("Recent configuration versions:") for i, cv in enumerate(cv_list[:5], 1): - print(f" {i}. {cv.id}") - print(f" Status: {cv.status}") - print(f" Source: {cv.source}") + print(f"{i}. {cv.id}") + print(f"Status: {cv.status}") + print(f"Source: {cv.source}") if cv.status_timestamps and "queued-at" in cv.status_timestamps: - print(f" Queued at: {cv.status_timestamps['queued-at']}") + print(f"Queued at: {cv.status_timestamps['queued-at']}") elif cv.status_timestamps: first_timestamp = list(cv.status_timestamps.keys())[0] - print( - f" {first_timestamp}: {cv.status_timestamps[first_timestamp]}" - ) + print(f"{first_timestamp}: {cv.status_timestamps[first_timestamp]}") else: - print(" No timestamps available") + print("No timestamps available") # Test with options - print("\n Testing list with options:") + print("\nTesting list with options:") try: list_options = ConfigurationVersionListOptions( include=[ConfigVerIncludeOpt.INGRESS_ATTRIBUTES], page_size=5, # Reduced page size page_number=1, ) - print(f" Making request with include: {list_options.include[0].value}") + print(f"Making request with include: {list_options.include[0].value}") # Add timeout protection by limiting the iterator cv_list_opts = [] @@ -228,18 +226,16 @@ def main(): if count >= 10: # Limit to prevent infinite loop break - print(f" Found {len(cv_list_opts)} configuration versions with options") - print( - f" Include options: {[opt.value for opt in list_options.include]}" - ) + print(f"Found {len(cv_list_opts)} configuration versions with options") + print(f"Include options: {[opt.value for opt in list_options.include]}") except Exception as opts_error: - print(f" Error with options: {opts_error}") - print(" This may be expected if the API doesn't support these options") - print(" Basic list functionality still works") + print(f"Error with options: {opts_error}") + print("This may be expected if the API doesn't support these options") + print("Basic list functionality still works") except Exception as e: - print(f" Error: {e}") + print(f"Error: {e}") import traceback traceback.print_exc() @@ -250,7 +246,7 @@ def main(): print("\n2. Testing create() function:") try: # Test 2a: Create and upload a REAL configuration version that will show in runs - print(" 2a. Creating REAL NON-SPECULATIVE configuration version:") + print("2a. Creating REAL NON-SPECULATIVE configuration version:") create_options = ConfigurationVersionCreateOptions( auto_queue_runs=True, # This will create a run automatically speculative=False, # This will make it appear in workspace runs @@ -258,23 +254,23 @@ def main(): new_cv = client.configuration_versions.create(workspace_id, create_options) created_cv_id = new_cv.id - print(f" Created NON-SPECULATIVE CV: {created_cv_id}") - print(f" Status: {new_cv.status}") - print(f" Speculative: {new_cv.speculative} (will show in runs)") - print(f" Auto-queue runs: {new_cv.auto_queue_runs} (will create run)") - print(f" Upload URL available: {bool(new_cv.upload_url)}") + print(f"Created NON-SPECULATIVE CV: {created_cv_id}") + print(f"Status: {new_cv.status}") + print(f"Speculative: {new_cv.speculative} (will show in runs)") + print(f"Auto-queue runs: {new_cv.auto_queue_runs} (will create run)") + print(f"Upload URL available: {bool(new_cv.upload_url)}") # UPLOAD REAL TERRAFORM CODE IMMEDIATELY if new_cv.upload_url: - print("\n Uploading real Terraform configuration...") + print("\nUploading real Terraform configuration...") with tempfile.TemporaryDirectory() as temp_dir: - print(f" Creating Terraform files in: {temp_dir}") + print(f"Creating Terraform files in: {temp_dir}") create_test_terraform_configuration(temp_dir) # List created files files = os.listdir(temp_dir) - print(f" Created {len(files)} Terraform files:") + print(f"Created {len(files)} Terraform files:") for filename in sorted(files): filepath = os.path.join(temp_dir, filename) size = os.path.getsize(filepath) @@ -282,7 +278,7 @@ def main(): try: # Create tar.gz archive manually since go-slug isn't available - print(" Creating tar.gz archive manually...") + print("Creating tar.gz archive manually...") import tarfile @@ -296,41 +292,41 @@ def main(): archive_buffer.seek(0) archive_bytes = archive_buffer.getvalue() - print(f" Created archive: {len(archive_bytes)} bytes") + print(f"Created archive: {len(archive_bytes)} bytes") # Use the SDK's upload_tar_gzip method instead of direct HTTP calls - print(" Uploading archive using SDK method...") + print("Uploading archive using SDK method...") archive_buffer.seek(0) # Reset buffer position client.configuration_versions.upload_tar_gzip( new_cv.upload_url, archive_buffer ) - print(" Terraform configuration uploaded successfully!") + print("Terraform configuration uploaded successfully!") # Wait and check status - print("\n Checking status after upload...") + print("\nChecking status after upload...") time.sleep(5) # Give TFE time to process updated_cv = client.configuration_versions.read(created_cv_id) - print(f" Status after upload: {updated_cv.status}") + print(f"Status after upload: {updated_cv.status}") if updated_cv.status.value in ["uploaded", "fetching"]: - print(" REAL configuration version created successfully!") - print(" This CV now contains actual Terraform code") + print("REAL configuration version created successfully!") + print("This CV now contains actual Terraform code") print( - " You can now see this CV in your Terraform Cloud workspace!" + "You can now see this CV in your Terraform Cloud workspace!" ) else: - print(f" Status is still: {updated_cv.status.value}") - print(" (Upload may still be processing)") + print(f"Status is still: {updated_cv.status.value}") + print("(Upload may still be processing)") except Exception as e: - print(f" Upload failed: {type(e).__name__}: {e}") - print(" CV created but no configuration uploaded") + print(f"Upload failed: {type(e).__name__}: {e}") + print("CV created but no configuration uploaded") else: - print(" No upload URL - cannot upload Terraform code") + print("No upload URL - cannot upload Terraform code") # Test 2b: Create standard configuration version for upload testing - print("\n 2b. Creating standard configuration version for upload tests:") + print("\n 2b. Creating standard configuration version for upload tests:") standard_options = ConfigurationVersionCreateOptions( auto_queue_runs=False, speculative=False ) @@ -339,24 +335,24 @@ def main(): workspace_id, standard_options ) uploadable_cv_id = standard_cv.id # Save for summary display - print(f" Created standard CV: {standard_cv.id}") - print(f" Status: {standard_cv.status}") - print(f" Speculative: {standard_cv.speculative}") - print(f" Auto-queue runs: {standard_cv.auto_queue_runs}") + print(f"Created standard CV: {standard_cv.id}") + print(f"Status: {standard_cv.status}") + print(f"Speculative: {standard_cv.speculative}") + print(f"Auto-queue runs: {standard_cv.auto_queue_runs}") # Test 2c: Create with auto-queue runs (will trigger run when uploaded) - print("\n 2c. Creating configuration version with auto-queue:") + print("\n 2c. Creating configuration version with auto-queue:") auto_options = ConfigurationVersionCreateOptions( auto_queue_runs=True, speculative=False ) auto_cv = client.configuration_versions.create(workspace_id, auto_options) - print(f" Created auto-queue CV: {auto_cv.id}") - print(f" Auto-queue runs: {auto_cv.auto_queue_runs}") - print(" This will trigger a Terraform run when code is uploaded") + print(f"Created auto-queue CV: {auto_cv.id}") + print(f"Auto-queue runs: {auto_cv.auto_queue_runs}") + print("This will trigger a Terraform run when code is uploaded") except Exception as e: - print(f" Error: {e}") + print(f"Error: {e}") import traceback traceback.print_exc() @@ -369,29 +365,25 @@ def main(): try: cv_details = client.configuration_versions.read(created_cv_id) - print(f" Read configuration version: {cv_details.id}") - print(f" Status: {cv_details.status}") - print(f" Source: {cv_details.source}") + print(f"Read configuration version: {cv_details.id}") + print(f"Status: {cv_details.status}") + print(f"Source: {cv_details.source}") if cv_details.status_timestamps: - print( - f" Status timestamps: {list(cv_details.status_timestamps.keys())}" - ) + print(f"Status timestamps: {list(cv_details.status_timestamps.keys())}") if "queued-at" in cv_details.status_timestamps: - print( - f" Queued at: {cv_details.status_timestamps['queued-at']}" - ) + print(f"Queued at: {cv_details.status_timestamps['queued-at']}") else: - print(" No status timestamps available") - print(f" Auto-queue runs: {cv_details.auto_queue_runs}") - print(f" Speculative: {cv_details.speculative}") + print("No status timestamps available") + print(f"Auto-queue runs: {cv_details.auto_queue_runs}") + print(f"Speculative: {cv_details.speculative}") if cv_details.upload_url: - print(f" Upload URL: {cv_details.upload_url[:60]}...") + print(f"Upload URL: {cv_details.upload_url[:60]}...") else: - print(" Upload URL: None") + print("Upload URL: None") # Test field validation - print("\n Field validation:") + print("\n Field validation:") required_fields = [ "id", "status", @@ -403,12 +395,12 @@ def main(): for field in required_fields: if hasattr(cv_details, field): value = getattr(cv_details, field) - print(f" {field}: {type(value).__name__}") + print(f"{field}: {type(value).__name__}") else: - print(f" {field}: Missing") + print(f"{field}: Missing") except Exception as e: - print(f" Error: {e}") + print(f"Error: {e}") import traceback traceback.print_exc() @@ -426,59 +418,59 @@ def main(): ) fresh_cv = client.configuration_versions.create(workspace_id, upload_options) - print(f" Created fresh CV for upload: {fresh_cv.id}") + print(f"Created fresh CV for upload: {fresh_cv.id}") upload_url = fresh_cv.upload_url if not upload_url: - print(" No upload URL available for this configuration version") - print(" Configuration version may not be in uploadable state") + print("No upload URL available for this configuration version") + print("Configuration version may not be in uploadable state") else: with tempfile.TemporaryDirectory() as temp_dir: - print(f" Creating test configuration in: {temp_dir}") + print(f"Creating test configuration in: {temp_dir}") create_test_terraform_configuration(temp_dir) # List created files files = os.listdir(temp_dir) - print(f" Created {len(files)} files:") + print(f"Created {len(files)} files:") for filename in sorted(files): filepath = os.path.join(temp_dir, filename) size = os.path.getsize(filepath) print(f" - {filename} ({size} bytes)") - print(f"\n Uploading configuration to CV: {fresh_cv.id}") - print(f" Upload URL: {upload_url[:60]}...") + print(f"\n Uploading configuration to CV: {fresh_cv.id}") + print(f"Upload URL: {upload_url[:60]}...") try: client.configuration_versions.upload(upload_url, temp_dir) - print(" Configuration uploaded successfully!") + print("Configuration uploaded successfully!") # Check status after upload - print("\n Checking status after upload:") + print("\n Checking status after upload:") time.sleep(3) # Give TFE time to process updated_cv = client.configuration_versions.read(fresh_cv.id) - print(f" Status after upload: {updated_cv.status}") + print(f"Status after upload: {updated_cv.status}") if updated_cv.status.value != "pending": - print(" Status changed (upload processed)") + print("Status changed (upload processed)") else: - print(" Status still pending (may need more time)") + print("Status still pending (may need more time)") except ImportError as e: if "go-slug" in str(e): - print(" go-slug package not available") - print(" Install with: pip install go-slug") + print("go-slug package not available") + print("Install with: pip install go-slug") print( - " Upload function exists but requires go-slug for packaging" + "Upload function exists but requires go-slug for packaging" ) print( - " Function correctly raises ImportError when go-slug unavailable" + "Function correctly raises ImportError when go-slug unavailable" ) else: raise except Exception as e: - print(f" Error: {e}") + print(f"Error: {e}") import traceback traceback.print_exc() @@ -492,7 +484,7 @@ def main(): cv_generator = client.configuration_versions.list(workspace_id) downloadable_cvs = [] - print(" Scanning for downloadable configuration versions:") + print("Scanning for downloadable configuration versions:") # Convert generator to list and limit to avoid infinite loop cv_list = [] count = 0 @@ -503,46 +495,46 @@ def main(): break for cv in cv_list: - print(f" CV {cv.id}: Status = {cv.status}") + print(f"CV {cv.id}: Status = {cv.status}") if cv.status.value in ["uploaded", "archived"]: downloadable_cvs.append(cv) if not downloadable_cvs: - print(" No uploaded configuration versions found to download") - print(" This is not a test failure - upload a configuration first") + print("No uploaded configuration versions found to download") + print("This is not a test failure - upload a configuration first") else: downloadable_cv = downloadable_cvs[0] - print(f"\n Downloading CV: {downloadable_cv.id}") - print(f" Status: {downloadable_cv.status}") + print(f"\n Downloading CV: {downloadable_cv.id}") + print(f"Status: {downloadable_cv.status}") archive_data = client.configuration_versions.download(downloadable_cv.id) - print(f" Downloaded {len(archive_data)} bytes") + print(f"Downloaded {len(archive_data)} bytes") # Validate downloaded data - print("\n Validating downloaded data:") + print("\n Validating downloaded data:") if len(archive_data) > 0: - print(" Archive data is non-empty") + print("Archive data is non-empty") # Basic format check if archive_data[:2] == b"\x1f\x8b": - print(" Data appears to be gzip format") + print("Data appears to be gzip format") else: - print(" Data may not be gzip format (could still be valid)") + print("Data may not be gzip format (could still be valid)") else: - print(" Archive data is empty") + print("Archive data is empty") # Test multiple downloads if available if len(downloadable_cvs) > 1: - print("\n Testing multiple downloads:") + print("\n Testing multiple downloads:") for i, cv in enumerate(downloadable_cvs[1:3], 2): try: data = client.configuration_versions.download(cv.id) - print(f" CV {i}: {cv.id} - {len(data)} bytes") + print(f"CV {i}: {cv.id} - {len(data)} bytes") except Exception as e: - print(f" CV {i}: {cv.id} - Failed: {type(e).__name__}") + print(f"CV {i}: {cv.id} - Failed: {type(e).__name__}") except Exception as e: - print(f" Error: {e}") + print(f"Error: {e}") import traceback traceback.print_exc() @@ -566,19 +558,19 @@ def main(): if len(cv_list) < 2: print( - " Need at least 2 configuration versions to test archive functionality" + "Need at least 2 configuration versions to test archive functionality" ) print( - " This is not a test failure - create more configuration versions first" + "This is not a test failure - create more configuration versions first" ) else: # Find suitable candidates for archiving archivable_cvs = [] already_archived = [] - print(" Scanning configuration versions for archiving:") + print("Scanning configuration versions for archiving:") for cv in cv_list: - print(f" CV {cv.id}: Status = {cv.status}") + print(f"CV {cv.id}: Status = {cv.status}") if cv.status.value == "archived": already_archived.append(cv) elif cv.status.value in ["uploaded", "errored", "pending"]: @@ -596,59 +588,53 @@ def main(): if candidates: cv_to_archive = candidates[0] # Pick an older uploaded CV - print(f"\n Attempting to archive CV: {cv_to_archive.id}") - print(f" Current status: {cv_to_archive.status}") - print(" (Skipping most recent uploaded CV to avoid 'current' error)") + print(f"\n Attempting to archive CV: {cv_to_archive.id}") + print(f"Current status: {cv_to_archive.status}") + print("(Skipping most recent uploaded CV to avoid 'current' error)") try: client.configuration_versions.archive(cv_to_archive.id) - print(" Archive request sent successfully") + print("Archive request sent successfully") # Check status after archive request - print("\n Checking status after archive request:") + print("\n Checking status after archive request:") time.sleep(3) try: updated_cv = client.configuration_versions.read( cv_to_archive.id ) - print(f" Status after archive: {updated_cv.status}") + print(f"Status after archive: {updated_cv.status}") if updated_cv.status.value == "archived": - print(" Successfully archived") + print("Successfully archived") else: - print(" Still processing (archive may take time)") + print("Still processing (archive may take time)") except Exception: - print( - " Could not read status after archive (may be expected)" - ) + print("Could not read status after archive (may be expected)") except Exception as e: if "404" in str(e) or "not found" in str(e).lower(): - print(" CV may have been auto-archived or removed") + print("CV may have been auto-archived or removed") elif "current" in str(e).lower(): - print(" Cannot archive current configuration version") - print( - " Function correctly handles 'current' CV restriction" - ) + print("Cannot archive current configuration version") + print("Function correctly handles 'current' CV restriction") else: - print(f" Archive failed: {type(e).__name__}: {e}") + print(f"Archive failed: {type(e).__name__}: {e}") else: - print("\n No suitable configuration versions found for archiving") - print( - " Need at least 2 uploaded CVs (to avoid archiving current one)" - ) - print(" Function correctly validates archivable CVs") + print("\n No suitable configuration versions found for archiving") + print("Need at least 2 uploaded CVs (to avoid archiving current one)") + print("Function correctly validates archivable CVs") # Test archiving already archived CV if already_archived: - print("\n Testing archive of already archived CV:") + print("\n Testing archive of already archived CV:") already_archived_cv = already_archived[0] - print(f" CV ID: {already_archived_cv.id} (already archived)") + print(f"CV ID: {already_archived_cv.id} (already archived)") try: client.configuration_versions.archive(already_archived_cv.id) - print(" Handled gracefully (no-op for already archived)") + print("Handled gracefully (no-op for already archived)") except Exception as e: - print(f" Correctly rejected: {type(e).__name__}") + print(f"Correctly rejected: {type(e).__name__}") except Exception as e: print(f" Error: {e}") @@ -671,33 +657,31 @@ def main(): created_cv_id, read_options ) - print(f" Read configuration version with options: {cv_with_options.id}") - print(f" Status: {cv_with_options.status}") - print(f" Source: {cv_with_options.source}") + print(f"Read configuration version with options: {cv_with_options.id}") + print(f"Status: {cv_with_options.status}") + print(f"Source: {cv_with_options.source}") if ( hasattr(cv_with_options, "ingress_attributes") and cv_with_options.ingress_attributes ): - print(" Ingress attributes included in response") + print("Ingress attributes included in response") if hasattr(cv_with_options.ingress_attributes, "branch"): - print(f" Branch: {cv_with_options.ingress_attributes.branch}") + print(f"Branch: {cv_with_options.ingress_attributes.branch}") if hasattr(cv_with_options.ingress_attributes, "clone_url"): - print( - f" Clone URL: {cv_with_options.ingress_attributes.clone_url}" - ) + print(f"Clone URL: {cv_with_options.ingress_attributes.clone_url}") else: - print(" No ingress attributes (expected for API-created CVs)") - print(" Ingress attributes are only present for VCS-connected CVs") + print("No ingress attributes (expected for API-created CVs)") + print("Ingress attributes are only present for VCS-connected CVs") except Exception as e: - print(f" Error: {e}") + print(f"Error: {e}") import traceback traceback.print_exc() else: print("\n7. Testing read_with_options() function:") - print(" Skipped - no configuration version created for testing") + print("Skipped - no configuration version created for testing") # ===================================================== # TEST 8: CREATE FOR REGISTRY MODULE (BETA) @@ -714,37 +698,33 @@ def main(): "provider": "aws", } - print(" Testing registry module configuration version creation:") - print(f" Module ID: {module_id}") + print("Testing registry module configuration version creation:") + print(f"Module ID: {module_id}") try: registry_cv = client.configuration_versions.create_for_registry_module( module_id ) - print(f" Created registry module CV: {registry_cv.id}") - print(f" Status: {registry_cv.status}") - print(f" Source: {registry_cv.source}") + print(f"Created registry module CV: {registry_cv.id}") + print(f"Status: {registry_cv.status}") + print(f"Source: {registry_cv.source}") except Exception as e: if "404" in str(e) or "not found" in str(e).lower(): - print( - " Registry module not found (expected - requires actual module)" - ) - print(" Function exists and properly handles missing modules") + print("Registry module not found (expected - requires actual module)") + print("Function exists and properly handles missing modules") elif "403" in str(e) or "forbidden" in str(e).lower(): - print(" No permission to access registry modules (expected)") - print(" Function exists and properly handles permission errors") + print("No permission to access registry modules (expected)") + print("Function exists and properly handles permission errors") elif "AttributeError" in str(e): - print(f" Function parameter error: {e}") - print(" Function exists but may need parameter adjustment") + print(f"Function parameter error: {e}") + print("Function exists but may need parameter adjustment") else: - print( - f" Registry module CV creation failed: {type(e).__name__}: {e}" - ) - print(" This may be expected if no registry modules exist") + print(f"Registry module CV creation failed: {type(e).__name__}: {e}") + print("This may be expected if no registry modules exist") except Exception as e: - print(f" Error: {e}") + print(f"Error: {e}") import traceback traceback.print_exc() @@ -766,8 +746,8 @@ def main(): upload_url = upload_test_cv.upload_url if upload_url: - print(f" Created CV for upload test: {upload_test_cv_id}") - print(f" Upload URL available: {bool(upload_url)}") + print(f"Created CV for upload test: {upload_test_cv_id}") + print(f"Upload URL available: {bool(upload_url)}") # Create a simple tar.gz archive in memory for testing import tarfile @@ -784,32 +764,30 @@ def main(): tar.add(test_file, arcname="main.tf") archive_buffer.seek(0) - print( - f" Created test archive: {len(archive_buffer.getvalue())} bytes" - ) + print(f"Created test archive: {len(archive_buffer.getvalue())} bytes") # Test direct tar.gz upload try: client.configuration_versions.upload_tar_gzip( upload_url, archive_buffer ) - print(" Direct tar.gz upload successful!") + print("Direct tar.gz upload successful!") # Check status after upload time.sleep(2) updated_upload_cv = client.configuration_versions.read( upload_test_cv_id ) - print(f" Status after upload: {updated_upload_cv.status}") + print(f"Status after upload: {updated_upload_cv.status}") except Exception as e: - print(f" Upload failed: {type(e).__name__}: {e}") - print(" This may be expected depending on TFE configuration") + print(f"Upload failed: {type(e).__name__}: {e}") + print("This may be expected depending on TFE configuration") else: - print(" No upload URL available - cannot test upload_tar_gzip") + print("No upload URL available - cannot test upload_tar_gzip") except Exception as e: - print(f" Error: {e}") + print(f"Error: {e}") import traceback traceback.print_exc() @@ -823,38 +801,38 @@ def main(): # on non-Enterprise installations, but we test that the functions exist if created_cv_id: - print(f" Testing with CV: {created_cv_id}") + print(f"Testing with CV: {created_cv_id}") # Test soft delete backing data - print("\n 10a. Testing soft_delete_backing_data():") + print("\n 10a. Testing soft_delete_backing_data():") try: client.configuration_versions.soft_delete_backing_data(created_cv_id) - print(" Soft delete backing data request sent successfully") + print("Soft delete backing data request sent successfully") except Exception as e: if "404" in str(e) or "not found" in str(e).lower(): - print(" CV not found for backing data operation") + print("CV not found for backing data operation") elif "403" in str(e) or "forbidden" in str(e).lower(): - print(" Enterprise feature - not available (expected)") + print("Enterprise feature - not available (expected)") else: - print(f" Soft delete failed: {type(e).__name__}: {e}") - print(" Function exists and properly handles Enterprise restrictions") + print(f"Soft delete failed: {type(e).__name__}: {e}") + print("Function exists and properly handles Enterprise restrictions") # Test restore backing data - print("\n 10b. Testing restore_backing_data():") + print("\n 10b. Testing restore_backing_data():") try: client.configuration_versions.restore_backing_data(created_cv_id) - print(" Restore backing data request sent successfully") + print("Restore backing data request sent successfully") except Exception as e: if "404" in str(e) or "not found" in str(e).lower(): - print(" CV not found for backing data operation") + print("CV not found for backing data operation") elif "403" in str(e) or "forbidden" in str(e).lower(): - print(" Enterprise feature - not available (expected)") + print("Enterprise feature - not available (expected)") else: - print(f" Restore failed: {type(e).__name__}: {e}") - print(" Function exists and properly handles Enterprise restrictions") + print(f"Restore failed: {type(e).__name__}: {e}") + print("Function exists and properly handles Enterprise restrictions") # Test permanently delete backing data - print("\n 10c. Testing permanently_delete_backing_data():") + print("\n 10c. Testing permanently_delete_backing_data():") try: # Create a separate CV for this destructive test perm_delete_options = ConfigurationVersionCreateOptions( @@ -869,15 +847,15 @@ def main(): client.configuration_versions.permanently_delete_backing_data( perm_delete_cv_id ) - print(" Permanent delete backing data request sent successfully") + print("Permanent delete backing data request sent successfully") except Exception as e: if "404" in str(e) or "not found" in str(e).lower(): - print(" CV not found for backing data operation") + print("CV not found for backing data operation") elif "403" in str(e) or "forbidden" in str(e).lower(): - print(" Enterprise feature - not available (expected)") + print("Enterprise feature - not available (expected)") else: - print(f" Permanent delete failed: {type(e).__name__}: {e}") - print(" Function exists and properly handles Enterprise restrictions") + print(f"Permanent delete failed: {type(e).__name__}: {e}") + print(" sFunction exists and properly handles Enterprise restrictions") # ===================================================== # TEST SUMMARY diff --git a/examples/notification_configuration.py b/examples/notification_configuration.py index ea20380..789367f 100644 --- a/examples/notification_configuration.py +++ b/examples/notification_configuration.py @@ -54,9 +54,9 @@ def main(): f"Found {len(workspace_notifications.items)} notification configurations" ) for nc in workspace_notifications.items: - print(f" - {nc.name} (ID: {nc.id}, Enabled: {nc.enabled})") + print(f"- {nc.name} (ID: {nc.id}, Enabled: {nc.enabled})") except Exception as e: - print(f" Error listing workspace notifications: {e}") + print(f"Error listing workspace notifications: {e}") print() @@ -76,14 +76,14 @@ def main(): f"Found {len(team_notifications.items)} team notification configurations" ) for nc in team_notifications.items: - print(f" - {nc.name} (ID: {nc.id}, Enabled: {nc.enabled})") + print(f"- {nc.name} (ID: {nc.id}, Enabled: {nc.enabled})") except Exception as e: error_msg = str(e).lower() if "not found" in error_msg: - print(f" Team not found (expected with fake team ID): {team_id}") - print(" Teams are not available in HCP Terraform free plan") + print(f"Team not found (expected with fake team ID): {team_id}") + print("Teams are not available in HCP Terraform free plan") else: - print(f" Error listing team notifications: {e}") + print(f"Error listing team notifications: {e}") print() @@ -113,7 +113,7 @@ def main(): workspace_id, create_options ) print( - f" Created notification: {new_notification.name} (ID: {new_notification.id})" + f"Created notification: {new_notification.name} (ID: {new_notification.id})" ) notification_id = new_notification.id @@ -123,10 +123,10 @@ def main(): read_notification = client.notification_configurations.read( notification_config_id=notification_id ) - print(f" Read notification: {read_notification.name}") - print(f" Destination type: {read_notification.destination_type}") - print(f" Enabled: {read_notification.enabled}") - print(f" Triggers: {read_notification.triggers}") + print(f"Read notification: {read_notification.name}") + print(f"Destination type: {read_notification.destination_type}") + print(f"Enabled: {read_notification.enabled}") + print(f"Triggers: {read_notification.triggers}") # ===== Update the notification configuration ===== print("\n5. Updating the notification configuration...") @@ -139,24 +139,22 @@ def main(): updated_notification = client.notification_configurations.update( notification_config_id=notification_id, options=update_options ) - print(f" Updated notification: {updated_notification.name}") - print(f" Enabled: {updated_notification.enabled}") + print(f"Updated notification: {updated_notification.name}") + print(f"Enabled: {updated_notification.enabled}") # ===== Verify the notification configuration ===== print("\n6. Verifying the notification configuration...") - print(" Note: This will fail with fake URLs - that's expected!") + print("Note: This will fail with fake URLs - that's expected!") try: client.notification_configurations.verify( notification_config_id=notification_id ) - print( - f" Verification successful for notification ID: {notification_id}" - ) - print(" Note: Verification sends a test payload to the configured URL") + print(f"Verification successful for notification ID: {notification_id}") + print("Note: Verification sends a test payload to the configured URL") except Exception as e: - print(f" Verification failed (expected with fake URL): {e}") + print(f"Verification failed (expected with fake URL): {e}") print( - " To test verification, use a real webhook URL from Slack, Teams, or Discord" + "To test verification, use a real webhook URL from Slack, Teams, or Discord" ) # ===== Delete the notification configuration ===== @@ -164,27 +162,27 @@ def main(): client.notification_configurations.delete( notification_config_id=notification_id ) - print(f" Deleted notification configuration: {notification_id}") + print(f"Deleted notification configuration: {notification_id}") # Verify deletion try: client.notification_configurations.read( notification_config_id=notification_id ) - print(" ERROR: Notification still exists after deletion!") + print("ERROR: Notification still exists after deletion!") except Exception: - print(" Confirmed: Notification configuration has been deleted") + print("Confirmed: Notification configuration has been deleted") except Exception as e: error_msg = str(e).lower() if "verification failed" in error_msg and "404" in error_msg: - print(" Webhook verification failed (expected with fake URL)") - print(" The fake Slack URL returns 404 - this is normal for testing") - print(" To test real verification, use a webhook from:") - print(" โ€ข webhook.site (instant test URL)") - print(" โ€ข Slack, Teams, or Discord webhook") + print(" Webhook verification failed (expected with fake URL)") + print("The fake Slack URL returns 404 - this is normal for testing") + print("To test real verification, use a webhook from:") + print("webhook.site (instant test URL)") + print("Slack, Teams, or Discord webhook") else: - print(f" Error in workspace notification operations: {e}") + print(f" Error in workspace notification operations: {e}") print() @@ -213,28 +211,28 @@ def main(): team_id, team_create_options ) print( - f" Created team notification: {team_notification.name} (ID: {team_notification.id})" + f"Created team notification: {team_notification.name} (ID: {team_notification.id})" ) # Clean up team notification client.notification_configurations.delete( notification_config_id=team_notification.id ) - print(f" Cleaned up team notification: {team_notification.id}") + print(f"Cleaned up team notification: {team_notification.id}") else: print( - f" Skipping team notifications - no real team ID available (using: {team_id})" + f"Skipping team notifications - no real team ID available (using: {team_id})" ) except Exception as e: - print(f" Error in team notification operations: {e}") + print(f" Error in team notification operations: {e}") error_msg = str(e).lower() if "not found" in error_msg: - print(" Team may not exist or token lacks team permissions") + print("Team may not exist or token lacks team permissions") elif "forbidden" in error_msg or "unauthorized" in error_msg: - print(" Token may lack team notification permissions") + print("Token may lack team notification permissions") elif "team" in error_msg: - print(" Team-specific error - check team settings or plan level") + print("Team-specific error - check team settings or plan level") print() @@ -263,17 +261,17 @@ def main(): workspace_id, teams_create_options ) print( - f" Created Teams notification: {teams_notification.name} (ID: {teams_notification.id})" + f"Created Teams notification: {teams_notification.name} (ID: {teams_notification.id})" ) # Clean up Teams notification client.notification_configurations.delete( notification_config_id=teams_notification.id ) - print(f" Cleaned up Teams notification: {teams_notification.id}") + print(f"Cleaned up Teams notification: {teams_notification.id}") except Exception as e: - print(f" Error in Teams notification operations: {e}") + print(f"Error in Teams notification operations: {e}") except Exception as e: print(f"Error: {e}") diff --git a/examples/oauth_client.py b/examples/oauth_client.py index b4c7aa2..e9cf62a 100644 --- a/examples/oauth_client.py +++ b/examples/oauth_client.py @@ -89,13 +89,13 @@ def main(): # Test basic list without options oauth_clients = list(client.oauth_clients.list(organization_name)) - print(f" Found {len(oauth_clients)} OAuth clients") + print(f"Found {len(oauth_clients)} OAuth clients") for i, oauth_client in enumerate(oauth_clients[:3], 1): - print(f" {i}. {oauth_client.id} - {oauth_client.service_provider}") + print(f"{i}. {oauth_client.id} - {oauth_client.service_provider}") if oauth_client.name: - print(f" Name: {oauth_client.name}") - print(f" Service Provider: {oauth_client.service_provider_name}") + print(f"Name: {oauth_client.name}") + print(f"Service Provider: {oauth_client.service_provider_name}") # Test list with options if len(oauth_clients) > 0: @@ -110,21 +110,19 @@ def main(): oauth_clients_with_options = list( client.oauth_clients.list(organization_name, options) ) - print( - f" Found {len(oauth_clients_with_options)} OAuth clients with options" - ) + print(f"Found {len(oauth_clients_with_options)} OAuth clients with options") if oauth_clients_with_options: first_client = oauth_clients_with_options[0] print( - f" First client includes - OAuth Tokens: {len(first_client.oauth_tokens or [])}" + f"First client includes - OAuth Tokens: {len(first_client.oauth_tokens or [])}" ) print( f" - Projects: {len(first_client.projects or [])}" ) except Exception as e: - print(f" Error listing OAuth clients: {e}") + print(f"Error listing OAuth clients: {e}") # ===================================================== # TEST 2: CREATE OAUTH CLIENT @@ -152,19 +150,17 @@ def main(): created_oauth_client = client.oauth_clients.create( organization_name, create_options ) - print(f" Created OAuth client: {created_oauth_client.id}") - print(f" Name: {created_oauth_client.name}") - print(f" Service Provider: {created_oauth_client.service_provider}") - print(f" API URL: {created_oauth_client.api_url}") - print(f" HTTP URL: {created_oauth_client.http_url}") - print( - f" Organization Scoped: {created_oauth_client.organization_scoped}" - ) + print(f"Created OAuth client: {created_oauth_client.id}") + print(f"Name: {created_oauth_client.name}") + print(f"Service Provider: {created_oauth_client.service_provider}") + print(f"API URL: {created_oauth_client.api_url}") + print(f"HTTP URL: {created_oauth_client.http_url}") + print(f"Organization Scoped: {created_oauth_client.organization_scoped}") except Exception as e: - print(f" Error creating OAuth client: {e}") + print(f"Error creating OAuth client: {e}") else: - print(" Skipped - OAUTH_CLIENT_GITHUB_TOKEN not set") + print("Skipped - OAUTH_CLIENT_GITHUB_TOKEN not set") # ===================================================== # TEST 3: READ OAUTH CLIENT @@ -178,15 +174,15 @@ def main(): print(f"Reading OAuth client: {created_oauth_client.id}") read_oauth_client = client.oauth_clients.read(created_oauth_client.id) - print(f" Read OAuth client: {read_oauth_client.id}") - print(f" Name: {read_oauth_client.name}") - print(f" Service Provider: {read_oauth_client.service_provider}") - print(f" Created At: {read_oauth_client.created_at}") - print(f" Callback URL: {read_oauth_client.callback_url}") - print(f" Connect Path: {read_oauth_client.connect_path}") + print(f"Read OAuth client: {read_oauth_client.id}") + print(f"Name: {read_oauth_client.name}") + print(f"Service Provider: {read_oauth_client.service_provider}") + print(f"Created At: {read_oauth_client.created_at}") + print(f"Callback URL: {read_oauth_client.callback_url}") + print(f"Connect Path: {read_oauth_client.connect_path}") except Exception as e: - print(f" Error reading OAuth client: {e}") + print(f"Error reading OAuth client: {e}") else: # Try to read an existing OAuth client if no client was created try: @@ -196,12 +192,12 @@ def main(): print(f"Reading existing OAuth client: {test_client.id}") read_oauth_client = client.oauth_clients.read(test_client.id) - print(f" Read existing OAuth client: {read_oauth_client.id}") - print(f" Service Provider: {read_oauth_client.service_provider}") + print(f"Read existing OAuth client: {read_oauth_client.id}") + print(f"Service Provider: {read_oauth_client.service_provider}") else: - print(" No existing OAuth clients found to test read()") + print("No existing OAuth clients found to test read()") except Exception as e: - print(f" Error reading existing OAuth client: {e}") + print(f"Error reading existing OAuth client: {e}") # ===================================================== # TEST 4: READ OAUTH CLIENT WITH OPTIONS @@ -234,20 +230,20 @@ def main(): read_oauth_client = client.oauth_clients.read_with_options( target_client.id, read_options ) - print(f" Read OAuth client with options: {read_oauth_client.id}") - print(f" OAuth Tokens: {len(read_oauth_client.oauth_tokens or [])}") - print(f" Projects: {len(read_oauth_client.projects or [])}") + print(f"Read OAuth client with options: {read_oauth_client.id}") + print(f"OAuth Tokens: {len(read_oauth_client.oauth_tokens or [])}") + print(f"Projects: {len(read_oauth_client.projects or [])}") if read_oauth_client.oauth_tokens: - print(" OAuth Token details:") + print(" OAuth Token details:") for i, token in enumerate(read_oauth_client.oauth_tokens[:2], 1): if isinstance(token, dict): - print(f" {i}. Token ID: {token.get('id', 'N/A')}") + print(f"{i}. Token ID: {token.get('id', 'N/A')}") except Exception as e: - print(f" Error reading OAuth client with options: {e}") + print(f"Error reading OAuth client with options: {e}") else: - print(" No OAuth client available to test read_with_options()") + print("No OAuth client available to test read_with_options()") # ===================================================== # TEST 5: UPDATE OAUTH CLIENT @@ -268,8 +264,8 @@ def main(): updated_oauth_client = client.oauth_clients.update( created_oauth_client.id, update_options ) - print(f" Updated OAuth client: {updated_oauth_client.id}") - print(f" Updated Name: {updated_oauth_client.name}") + print(f"Updated OAuth client: {updated_oauth_client.id}") + print(f"Updated Name: {updated_oauth_client.name}") print( f" Updated Organization Scoped: {updated_oauth_client.organization_scoped}" ) @@ -278,9 +274,9 @@ def main(): created_oauth_client = updated_oauth_client except Exception as e: - print(f" Error updating OAuth client: {e}") + print(f"Error updating OAuth client: {e}") else: - print(" No OAuth client created to test update()") + print("No OAuth client created to test update()") # ===================================================== # TEST 6: PREPARE TEST PROJECTS (for project operations) @@ -306,10 +302,10 @@ def main(): f" {i}. {corresponding_project.name} (ID: {project_ref['id']})" ) else: - print(" No projects found - project operations tests will be skipped") + print("No projects found - project operations tests will be skipped") except Exception as e: - print(f" Error getting projects: {e}") + print(f"Error getting projects: {e}") # ===================================================== # TEST 7: ADD PROJECTS TO OAUTH CLIENT @@ -342,12 +338,12 @@ def main(): ) except Exception as e: - print(f" Error adding projects to OAuth client: {e}") + print(f"Error adding projects to OAuth client: {e}") else: if not created_oauth_client: - print(" No OAuth client created to test add_projects()") + print("No OAuth client created to test add_projects()") if not test_projects: - print(" No projects available to test add_projects()") + print("No projects available to test add_projects()") # ===================================================== # TEST 8: REMOVE PROJECTS FROM OAUTH CLIENT @@ -382,12 +378,12 @@ def main(): ) except Exception as e: - print(f" Error removing projects from OAuth client: {e}") + print(f"Error removing projects from OAuth client: {e}") else: if not created_oauth_client: - print(" No OAuth client created to test remove_projects()") + print("No OAuth client created to test remove_projects()") if not test_projects: - print(" No projects available to test remove_projects()") + print("No projects available to test remove_projects()") # ===================================================== # TEST 9: DELETE OAUTH CLIENT @@ -403,27 +399,27 @@ def main(): # First, let's confirm it exists try: client.oauth_clients.read(created_oauth_client.id) - print(" Confirmed OAuth client exists before deletion") + print("Confirmed OAuth client exists before deletion") except NotFound: - print(" OAuth client not found before deletion attempt") + print("OAuth client not found before deletion attempt") # Delete the OAuth client client.oauth_clients.delete(created_oauth_client.id) - print(f" Successfully deleted OAuth client: {created_oauth_client.id}") + print(f"Successfully deleted OAuth client: {created_oauth_client.id}") # Verify deletion by trying to read it try: client.oauth_clients.read(created_oauth_client.id) - print(" Warning: OAuth client still exists after deletion") + print("Warning: OAuth client still exists after deletion") except NotFound: - print(" Verification: OAuth client successfully deleted (not found)") + print("Verification: OAuth client successfully deleted (not found)") except Exception as e: - print(f" ? Verification error: {e}") + print(f"? Verification error: {e}") except Exception as e: - print(f" Error deleting OAuth client: {e}") + print(f"Error deleting OAuth client: {e}") else: - print(" No OAuth client created to test delete()") + print("No OAuth client created to test delete()") # ===================================================== # SUMMARY diff --git a/examples/oauth_token.py b/examples/oauth_token.py index 5a0ae1d..16b29df 100644 --- a/examples/oauth_token.py +++ b/examples/oauth_token.py @@ -57,17 +57,17 @@ def main(): try: # Test basic list without options token_list = client.oauth_tokens.list(organization_name) - print(f" Found {len(token_list.items)} OAuth tokens") + print(f"Found {len(token_list.items)} OAuth tokens") # Show token details for i, token in enumerate(token_list.items[:3], 1): # Show first 3 - print(f" {i}. Token ID: {token.id}") - print(f" UID: {token.uid}") - print(f" Service Provider User: {token.service_provider_user}") - print(f" Has SSH Key: {token.has_ssh_key}") - print(f" Created: {token.created_at}") + print(f"{i}. Token ID: {token.id}") + print(f"UID: {token.uid}") + print(f"Service Provider User: {token.service_provider_user}") + print(f"Has SSH Key: {token.has_ssh_key}") + print(f"Created: {token.created_at}") if token.oauth_client: - print(f" OAuth Client: {token.oauth_client.id}") + print(f"OAuth Client: {token.oauth_client.id}") # Store first token for subsequent tests if token_list.items: @@ -75,21 +75,21 @@ def main(): print(f"\n Using token {test_token_id} for subsequent tests") # Test list with options - print("\n Testing list() with pagination options:") + print("\nTesting list() with pagination options:") options = OAuthTokenListOptions(page_size=10, page_number=1) token_list_with_options = client.oauth_tokens.list(organization_name, options) - print(f" Found {len(token_list_with_options.items)} tokens with options") + print(f"Found {len(token_list_with_options.items)} tokens with options") if token_list_with_options.current_page: - print(f" Current page: {token_list_with_options.current_page}") + print(f"Current page: {token_list_with_options.current_page}") if token_list_with_options.total_count: - print(f" Total count: {token_list_with_options.total_count}") + print(f"Total count: {token_list_with_options.total_count}") except NotFound: print( - " No OAuth tokens found (organization may not exist or no tokens available)" + "No OAuth tokens found (organization may not exist or no tokens available)" ) except Exception as e: - print(f" Error: {e}") + print(f"Error: {e}") # ===================================================== # TEST 2: READ OAUTH TOKEN @@ -98,19 +98,19 @@ def main(): print("\n2. Testing read() function:") try: token = client.oauth_tokens.read(test_token_id) - print(f" Read OAuth token: {token.id}") - print(f" UID: {token.uid}") - print(f" Service Provider User: {token.service_provider_user}") - print(f" Has SSH Key: {token.has_ssh_key}") - print(f" Created: {token.created_at}") + print(f"Read OAuth token: {token.id}") + print(f"UID: {token.uid}") + print(f"Service Provider User: {token.service_provider_user}") + print(f"Has SSH Key: {token.has_ssh_key}") + print(f"Created: {token.created_at}") if token.oauth_client: - print(f" OAuth Client: {token.oauth_client.id}") + print(f"OAuth Client: {token.oauth_client.id}") except Exception as e: - print(f" Error: {e}") + print(f"Error: {e}") else: print("\n2. Testing read() function:") - print(" Skipped - No OAuth token available to read") + print("Skipped - No OAuth token available to read") # ===================================================== # TEST 3: UPDATE OAUTH TOKEN @@ -119,29 +119,29 @@ def main(): print("\n3. Testing update() function:") try: # Test updating with SSH key - print(" Testing update with SSH key...") + print("Testing update with SSH key...") ssh_key = """-----BEGIN RSA PRIVATE KEY----- -----END RSA PRIVATE KEY-----""" options = OAuthTokenUpdateOptions(private_ssh_key=ssh_key) updated_token = client.oauth_tokens.update(test_token_id, options) - print(f" Updated OAuth token: {updated_token.id}") - print(f" Has SSH Key after update: {updated_token.has_ssh_key}") + print(f"Updated OAuth token: {updated_token.id}") + print(f"Has SSH Key after update: {updated_token.has_ssh_key}") # Test updating without SSH key (no changes) print("\n Testing update without changes...") options_empty = OAuthTokenUpdateOptions() updated_token_2 = client.oauth_tokens.update(test_token_id, options_empty) - print(f" Updated OAuth token (no changes): {updated_token_2.id}") + print(f"Updated OAuth token (no changes): {updated_token_2.id}") except Exception as e: - print(f" Error: {e}") + print(f"Error: {e}") print( - " Note: This may fail if the SSH key format is invalid or constraints apply" + "Note: This may fail if the SSH key format is invalid or constraints apply" ) else: print("\n3. Testing update() function:") - print(" Skipped - No OAuth token available to update") + print("Skipped - No OAuth token available to update") # ===================================================== # TEST 4: DELETE OAUTH TOKEN @@ -152,44 +152,44 @@ def main(): delete_token_id = "ot-WQf5ARHA1Qxzo9d4" try: - print(f" Attempting to delete OAuth token: {delete_token_id}") + print(f"Attempting to delete OAuth token: {delete_token_id}") client.oauth_tokens.delete(delete_token_id) - print(f" Successfully deleted OAuth token: {delete_token_id}") + print(f"Successfully deleted OAuth token: {delete_token_id}") # Verify deletion by trying to read the token try: client.oauth_tokens.read(delete_token_id) - print(" Token still exists after deletion!") + print("Token still exists after deletion!") except NotFound: - print(" Confirmed token was deleted - no longer accessible") + print("Confirmed token was deleted - no longer accessible") except Exception as e: - print(f" ? Verification failed: {e}") + print(f"? Verification failed: {e}") except Exception as e: - print(f" Error deleting token: {e}") + print(f"Error deleting token: {e}") # Uncomment the following section ONLY if you have a disposable OAuth token # WARNING: This will permanently delete the OAuth token! """ if test_token_id: try: - print(f" Attempting to delete OAuth token: {test_token_id}") + print(f"Attempting to delete OAuth token: {test_token_id}") client.oauth_tokens.delete(test_token_id) - print(f" Successfully deleted OAuth token: {test_token_id}") + print(f"Successfully deleted OAuth token: {test_token_id}") # Verify deletion by trying to read the token try: client.oauth_tokens.read(test_token_id) - print(f" Token still exists after deletion!") + print(f"Token still exists after deletion!") except NotFound: - print(f" Confirmed token was deleted - no longer accessible") + print(f"Confirmed token was deleted - no longer accessible") except Exception as e: - print(f" ? Verification failed: {e}") + print(f"? Verification failed: {e}") except Exception as e: - print(f" Error deleting token: {e}") + print(f"Error deleting token: {e}") else: - print(" Skipped - No OAuth token available to delete") + print("Skipped - No OAuth token available to delete") """ # ===================================================== @@ -199,10 +199,10 @@ def main(): print("OAUTH TOKEN TESTING COMPLETE") print("=" * 80) print("Functions tested:") - print(" 1. list() - List OAuth tokens for organization") - print(" 2. read() - Read OAuth token by ID") - print(" 3. update() - Update existing OAuth token") - print(" 4. delete() - Delete OAuth token (testing with ot-WQf5ARHA1Qxzo9d4)") + print("1. list() - List OAuth tokens for organization") + print("2. read() - Read OAuth token by ID") + print("3. update() - Update existing OAuth token") + print("4. delete() - Delete OAuth token (testing with ot-WQf5ARHA1Qxzo9d4)") print("") print("All OAuth token functions have been tested!") print("Check the output above for any errors or warnings.") diff --git a/examples/org.py b/examples/org.py index eb767b2..6538f8d 100644 --- a/examples/org.py +++ b/examples/org.py @@ -16,21 +16,21 @@ def test_basic_org_operations(client): try: org_list = client.organizations.list() orgs = list(org_list) - print(f" Found {len(orgs)} organizations") + print(f"Found {len(orgs)} organizations") # Show first few organizations for i, org in enumerate(orgs[:5], 1): - print(f" {i:2d}. {org.name} (ID: {org.id})") + print(f"{i:2d}. {org.name} (ID: {org.id})") if org.email: - print(f" Email: {org.email}") + print(f"Email: {org.email}") if len(orgs) > 5: - print(f" ... and {len(orgs) - 5} more") + print(f"... and {len(orgs) - 5} more") return orgs[0].name if orgs else None # Return first org name for testing except Exception as e: - print(f" Error listing organizations: {e}") + print(f"Error listing organizations: {e}") return None @@ -42,63 +42,63 @@ def test_org_read_operations(client, org_name): print("\n1. Reading Organization Details:") try: org = client.organizations.read(org_name) - print(f" Organization: {org.name}") - print(f" ID: {org.id}") - print(f" Email: {org.email or 'Not set'}") - print(f" Created: {org.created_at or 'Unknown'}") - print(f" Execution Mode: {org.default_execution_mode or 'Not set'}") - print(f" Two-Factor: {org.two_factor_conformant}") + print(f"Organization: {org.name}") + print(f"ID: {org.id}") + print(f"Email: {org.email or 'Not set'}") + print(f"Created: {org.created_at or 'Unknown'}") + print(f"Execution Mode: {org.default_execution_mode or 'Not set'}") + print(f"Two-Factor: {org.two_factor_conformant}") except Exception as e: - print(f" Error reading organization: {e}") + print(f"Error reading organization: {e}") # Test capacity print("\n2. Reading Organization Capacity:") try: capacity = client.organizations.read_capacity(org_name) - print(" Capacity:") - print(f" Pending runs: {capacity.pending}") - print(f" Running runs: {capacity.running}") - print(f" Total active: {capacity.pending + capacity.running}") + print("Capacity:") + print(f"Pending runs: {capacity.pending}") + print(f"Running runs: {capacity.running}") + print(f"Total active: {capacity.pending + capacity.running}") except Exception as e: - print(f" Error reading capacity: {e}") + print(f"Error reading capacity: {e}") # Test entitlements print("\n3. Reading Organization Entitlements:") try: entitlements = client.organizations.read_entitlements(org_name) - print(" Entitlements:") - print(f" Operations: {entitlements.operations}") - print(f" Teams: {entitlements.teams}") - print(f" State Storage: {entitlements.state_storage}") - print(f" VCS Integrations: {entitlements.vcs_integrations}") - print(f" Cost Estimation: {entitlements.cost_estimation}") - print(f" Sentinel: {entitlements.sentinel}") - print(f" Private Module Registry: {entitlements.private_module_registry}") - print(f" SSO: {entitlements.sso}") + print("Entitlements:") + print(f"Operations: {entitlements.operations}") + print(f"Teams: {entitlements.teams}") + print(f"State Storage: {entitlements.state_storage}") + print(f"VCS Integrations: {entitlements.vcs_integrations}") + print(f"Cost Estimation: {entitlements.cost_estimation}") + print(f"Sentinel: {entitlements.sentinel}") + print(f"Private Module Registry: {entitlements.private_module_registry}") + print(f"SSO: {entitlements.sso}") except Exception as e: - print(f" Error reading entitlements: {e}") + print(f"Error reading entitlements: {e}") # Test run queue print("\n4. Reading Organization Run Queue:") try: queue_options = ReadRunQueueOptions(page_number=1, page_size=10) run_queue = client.organizations.read_run_queue(org_name, queue_options) - print(" Run Queue:") - print(f" Items in queue: {len(run_queue.items)}") + print("Run Queue:") + print(f"Items in queue: {len(run_queue.items)}") if run_queue.pagination: - print(f" Current page: {run_queue.pagination.current_page}") - print(f" Total count: {run_queue.pagination.total_count}") + print(f"Current page: {run_queue.pagination.current_page}") + print(f"Total count: {run_queue.pagination.total_count}") # Show details of first few runs for i, run in enumerate(run_queue.items[:3], 1): - print(f" Run {i}: ID={run.id}, Status={run.status}") + print(f"Run {i}: ID={run.id}, Status={run.status}") if len(run_queue.items) > 3: - print(f" ... and {len(run_queue.items) - 3} more runs") + print(f"... and {len(run_queue.items) - 3} more runs") except Exception as e: - print(f" Error reading run queue: {e}") + print(f"Error reading run queue: {e}") def test_data_retention_policies(client, org_name): @@ -111,27 +111,27 @@ def test_data_retention_policies(client, org_name): try: policy_choice = client.organizations.read_data_retention_policy_choice(org_name) if policy_choice is None: - print(" No data retention policy currently configured") + print("No data retention policy currently configured") elif policy_choice.data_retention_policy_delete_older: policy = policy_choice.data_retention_policy_delete_older print( - f" Delete Older Policy: {policy.delete_older_than_n_days} days (ID: {policy.id})" + f"Delete Older Policy: {policy.delete_older_than_n_days} days (ID: {policy.id})" ) elif policy_choice.data_retention_policy_dont_delete: policy = policy_choice.data_retention_policy_dont_delete - print(f" Don't Delete Policy (ID: {policy.id})") + print(f"Don't Delete Policy (ID: {policy.id})") elif policy_choice.data_retention_policy: policy = policy_choice.data_retention_policy print( - f" Legacy Policy: {policy.delete_older_than_n_days} days (ID: {policy.id})" + f"Legacy Policy: {policy.delete_older_than_n_days} days (ID: {policy.id})" ) except Exception as e: if "not found" in str(e).lower() or "404" in str(e): print( - " Data retention policies not available (Terraform Enterprise feature)" + "Data retention policies not available (Terraform Enterprise feature)" ) else: - print(f" Error reading data retention policy: {e}") + print(f"Error reading data retention policy: {e}") # Test setting delete older policy print("\n2. Setting Delete Older Data Retention Policy (30 days):") @@ -140,14 +140,14 @@ def test_data_retention_policies(client, org_name): policy = client.organizations.set_data_retention_policy_delete_older( org_name, options ) - print(" Created Delete Older Policy:") - print(f" ID: {policy.id}") - print(f" Delete after: {policy.delete_older_than_n_days} days") + print("Created Delete Older Policy:") + print(f"ID: {policy.id}") + print(f"Delete after: {policy.delete_older_than_n_days} days") except Exception as e: if "not found" in str(e).lower() or "404" in str(e): - print(" Feature not available (Terraform Enterprise only)") + print("Feature not available (Terraform Enterprise only)") else: - print(f" Error setting delete older policy: {e}") + print(f"Error setting delete older policy: {e}") # Test updating delete older policy print("\n3. Updating Delete Older Policy (15 days):") @@ -156,14 +156,14 @@ def test_data_retention_policies(client, org_name): policy = client.organizations.set_data_retention_policy_delete_older( org_name, options ) - print(" Updated Delete Older Policy:") - print(f" ID: {policy.id}") - print(f" Delete after: {policy.delete_older_than_n_days} days") + print("Updated Delete Older Policy:") + print(f"ID: {policy.id}") + print(f"Delete after: {policy.delete_older_than_n_days} days") except Exception as e: if "not found" in str(e).lower() or "404" in str(e): - print(" Feature not available (Terraform Enterprise only)") + print("Feature not available (Terraform Enterprise only)") else: - print(f" Error updating delete older policy: {e}") + print(f"Error updating delete older policy: {e}") # Test setting don't delete policy print("\n4. Setting Don't Delete Data Retention Policy:") @@ -172,59 +172,57 @@ def test_data_retention_policies(client, org_name): policy = client.organizations.set_data_retention_policy_dont_delete( org_name, options ) - print(" Created Don't Delete Policy:") - print(f" ID: {policy.id}") - print(" Data will never be automatically deleted") + print("Created Don't Delete Policy:") + print(f"ID: {policy.id}") + print("Data will never be automatically deleted") except Exception as e: if "not found" in str(e).lower() or "404" in str(e): - print(" Feature not available (Terraform Enterprise only)") + print("Feature not available (Terraform Enterprise only)") else: - print(f" Error setting don't delete policy: {e}") + print(f"Error setting don't delete policy: {e}") # Test reading policy after changes print("\n5. Reading Data Retention Policy After Changes:") try: policy_choice = client.organizations.read_data_retention_policy_choice(org_name) if policy_choice is None: - print(" No data retention policy configured") + print("No data retention policy configured") elif policy_choice.data_retention_policy_delete_older: policy = policy_choice.data_retention_policy_delete_older print( - f" Current Policy: Delete Older ({policy.delete_older_than_n_days} days)" + f"Current Policy: Delete Older ({policy.delete_older_than_n_days} days)" ) elif policy_choice.data_retention_policy_dont_delete: - print(" Current Policy: Don't Delete") + print("Current Policy: Don't Delete") # Test legacy conversion if policy_choice and policy_choice.is_populated(): legacy = policy_choice.convert_to_legacy_struct() if legacy: - print( - f" Legacy representation: {legacy.delete_older_than_n_days} days" - ) + print(f"Legacy representation: {legacy.delete_older_than_n_days} days") except Exception as e: if "not found" in str(e).lower() or "404" in str(e): - print(" Feature not available (Terraform Enterprise only)") + print("Feature not available (Terraform Enterprise only)") else: - print(f" Error reading updated policy: {e}") + print(f"Error reading updated policy: {e}") # Test deleting policy print("\n6. Deleting Data Retention Policy:") try: client.organizations.delete_data_retention_policy(org_name) - print(" Successfully deleted data retention policy") + print("Successfully deleted data retention policy") # Verify deletion policy_choice = client.organizations.read_data_retention_policy_choice(org_name) if policy_choice is None or not policy_choice.is_populated(): - print(" Verified: No policy configured after deletion") + print("Verified: No policy configured after deletion") else: - print(" Policy still exists after deletion attempt") + print("Policy still exists after deletion attempt") except Exception as e: if "not found" in str(e).lower() or "404" in str(e): - print(" Feature not available (Terraform Enterprise only)") + print("Feature not available (Terraform Enterprise only)") else: - print(f" Error deleting policy: {e}") + print(f"Error deleting policy: {e}") def test_organization_creation_and_cleanup(client): @@ -239,41 +237,39 @@ def test_organization_creation_and_cleanup(client): name=test_org_name, email="aayush.singh@hashicorp.com" ) new_org = client.organizations.create(create_opts) - print(f" Created organization: {new_org.name}") - print(f" ID: {new_org.id}") - print(f" Email: {new_org.email}") + print(f"Created organization: {new_org.name}") + print(f"ID: {new_org.id}") + print(f"Email: {new_org.email}") # Test reading the newly created org print("\n2. Reading Newly Created Organization:") read_org = client.organizations.read(test_org_name) - print(f" Successfully read organization: {read_org.name}") + print(f"Successfully read organization: {read_org.name}") # Cleanup print("\n3. Cleaning Up Test Organization:") client.organizations.delete(test_org_name) - print(" Successfully deleted test organization") + print("Successfully deleted test organization") return True except Exception as e: - print(f" Organization creation/deletion test skipped: {e}") - print( - " This is normal if you don't have organization management permissions" - ) + print(f"Organization creation/deletion test skipped: {e}") + print("This is normal if you don't have organization management permissions") return False def main(): """Main function to test all organization functionalities.""" - print(" Python TFE Organization Functions Test Suite") + print("Python TFE Organization Functions Test Suite") print("=" * 60) # Initialize client try: client = TFEClient(TFEConfig.from_env()) - print(" TFE Client initialized successfully") + print("TFE Client initialized successfully") except Exception as e: - print(f" Failed to initialize TFE client: {e}") + print(f"Failed to initialize TFE client: {e}") print( "Please ensure TF_CLOUD_ORGANIZATION and TF_CLOUD_TOKEN environment variables are set" ) @@ -296,10 +292,10 @@ def main(): # Summary print("\n" + "=" * 60) - print(" Test Summary:") - print(" Basic organization operations tested") - print(" Organization read operations tested") - print(" Data retention policy operations tested") + print("Test Summary:") + print("Basic organization operations tested") + print("Organization read operations tested") + print("Data retention policy operations tested") if creation_success: print("Organization creation/deletion tested") else: diff --git a/examples/organization_membership.py b/examples/organization_membership.py index 0c8e1c0..da6d45d 100644 --- a/examples/organization_membership.py +++ b/examples/organization_membership.py @@ -30,11 +30,11 @@ def main(): # Initialize the client (reads TFE_TOKEN and TFE_ADDRESS from environment) try: client = TFEClient() - print(" Connected to Terraform Cloud/Enterprise") + print("Connected to Terraform Cloud/Enterprise") except Exception as e: - print(f" Error connecting: {e}") + print(f"Error connecting: {e}") print("\nMake sure TFE_TOKEN environment variable is set:") - print(" export TFE_TOKEN='your-token-here'") + print("export TFE_TOKEN='your-token-here'") sys.exit(1) print(f"\nTesting Organization Membership List for: {organization_name}") @@ -50,22 +50,20 @@ def main(): memberships_list.append(membership) if count <= 5: # Show first 5 print( - f" {membership.email} (ID: {membership.id[:8]}..., Status: {membership.status.value})" + f"{membership.email} (ID: {membership.id[:8]}..., Status: {membership.status.value})" ) print(memberships_list) - print(f" Total memberships: {count}") + print(f"Total memberships: {count}") if count == 0: - print( - " No memberships found - organization may not exist or has no members" - ) + print("No memberships found - organization may not exist or has no members") else: - print(f" Success: Retrieved {count} membership(s)") + print(f"Success: Retrieved {count} membership(s)") except ValueError as e: - print(f" Validation Error: {e}") + print(f"Validation Error: {e}") except Exception as e: - print(f" Error: {type(e).__name__}: {e}") + print(f"Error: {type(e).__name__}: {e}") # Test 2: Iterate with custom page size print("\n[Test 2] Iterate with custom page size (3 items per page):") @@ -79,11 +77,11 @@ def main(): ): count += 1 if count <= 3: - print(f" {membership.email}") + print(f"{membership.email}") - print(f" Processed {count} memberships (fetched in batches of 3)") + print(f"Processed {count} memberships (fetched in batches of 3)") except Exception as e: - print(f" Error: {type(e).__name__}: {e}") + print(f"Error: {type(e).__name__}: {e}") # Test 3: Iterate with user relationships included print("\n[Test 3] Iterate with user relationships included:") @@ -101,11 +99,11 @@ def main(): users_found += 1 if count <= 3: # Show first 3 user_id = membership.user.id if membership.user else "N/A" - print(f" {membership.email} (User ID: {user_id})") + print(f"{membership.email} (User ID: {user_id})") - print(f" Processed {count} memberships, {users_found} with user data") + print(f"Processed {count} memberships, {users_found} with user data") except Exception as e: - print(f" Error: {type(e).__name__}: {e}") + print(f"Error: {type(e).__name__}: {e}") # Test 4: Filter by status (invited) print("\n[Test 4] Filter by status (invited only):") @@ -119,23 +117,23 @@ def main(): ): invited.append(membership.email) if membership.status != OrganizationMembershipStatus.INVITED: - print(f" ERROR: Found non-invited member: {membership.email}") + print(f"ERROR: Found non-invited member: {membership.email}") - print(f" Found {len(invited)} invited membership(s)") + print(f"Found {len(invited)} invited membership(s)") for email in invited[:5]: # Show first 5 - print(f" {email}") + print(f"{email}") if len(invited) == 0: - print(" No invited members found") + print("No invited members found") except Exception as e: - print(f" Error: {type(e).__name__}: {e}") + print(f"Error: {type(e).__name__}: {e}") # Test 5: Filter by email addresses (using first member found in Test 1) print("\n[Test 5] Filter by specific email address:") try: if count > 0 and len(memberships_list) > 0: test_email = memberships_list[0].email - print(f" Testing with email: {test_email}") + print(f"Testing with email: {test_email}") options = OrganizationMembershipListOptions( emails=[test_email], @@ -146,20 +144,20 @@ def main(): ): matching.append(membership.email) - print(f" Found {len(matching)} matching membership(s)") + print(f"Found {len(matching)} matching membership(s)") for email in matching: - print(f" {email}") + print(f"{email}") if len(matching) == 1 and matching[0] == test_email: - print(" Success: Email filter working correctly") + print("Success: Email filter working correctly") else: - print(f" Warning: Expected 1 result with email {test_email}") + print(f"Warning: Expected 1 result with email {test_email}") else: - print(" Skipped: No memberships available from Test 1") + print("Skipped: No memberships available from Test 1") except ValueError as e: - print(f" Validation Error: {e}") + print(f"Validation Error: {e}") except Exception as e: - print(f" Error: {type(e).__name__}: {e}") + print(f"Error: {type(e).__name__}: {e}") # Test 6: Search by query string print("\n[Test 6] Search memberships by query string:") @@ -170,7 +168,7 @@ def main(): domain = test_email.split("@")[1] if "@" in test_email else None if domain: - print(f" Searching for: {domain}") + print(f"Searching for: {domain}") options = OrganizationMembershipListOptions( query=domain, # Searches in user name and email ) @@ -180,20 +178,20 @@ def main(): ): results.append(membership.email) - print(f" Found {len(results)} membership(s) matching query") + print(f"Found {len(results)} membership(s) matching query") for email in results[:5]: # Show first 5 - print(f" {email}") + print(f"{email}") if len(results) > 0: - print(" Success: Query filter working") + print("Success: Query filter working") else: - print(f" Warning: No results found for query '{domain}'") + print(f"Warning: No results found for query '{domain}'") else: - print(" Skipped: Could not extract domain from email") + print("Skipped: Could not extract domain from email") else: - print(" Skipped: No memberships available from Test 1") + print("Skipped: No memberships available from Test 1") except Exception as e: - print(f" Error: {type(e).__name__}: {e}") + print(f"Error: {type(e).__name__}: {e}") # Test 7: Combined filters (active + includes) print("\n[Test 7] Combined filters: active members with user & teams included:") @@ -211,41 +209,41 @@ def main(): has_user = membership.user is not None active_members.append((membership.email, team_count, has_user)) - print(f" Found {len(active_members)} active membership(s)") + print(f"Found {len(active_members)} active membership(s)") for email, team_count, has_user in active_members[:5]: # Show first 5 user_str = " User" if has_user else " No User" - print(f" {email} (Teams: {team_count}, {user_str})") + print(f"{email} (Teams: {team_count}, {user_str})") if len(active_members) > 0: - print(" Success: Combined filters working") + print("Success: Combined filters working") else: - print(" No active members found") + print("No active members found") except Exception as e: - print(f" Error: {type(e).__name__}: {e}") + print(f"Error: {type(e).__name__}: {e}") # Test 8: Read a specific organization membership print("\n[Test 8] Read a specific organization membership:") try: if count > 0 and len(memberships_list) > 0: test_membership_id = memberships_list[0].id - print(f" Reading membership ID: {test_membership_id}") + print(f"Reading membership ID: {test_membership_id}") membership = client.organization_memberships.read(test_membership_id) - print(f" Email: {membership.email}") - print(f" Status: {membership.status.value}") - print(f" ID: {membership.id}") - print(" Success: Read membership successfully") + print(f"Email: {membership.email}") + print(f"Status: {membership.status.value}") + print(f"ID: {membership.id}") + print("Success: Read membership successfully") else: - print(" Skipped: No memberships available from Test 1") + print("Skipped: No memberships available from Test 1") except Exception as e: - print(f" Error: {type(e).__name__}: {e}") + print(f"Error: {type(e).__name__}: {e}") # Test 9: Read with options (include user and teams) print("\n[Test 9] Read membership with options (include user & teams):") try: if count > 0 and len(memberships_list) > 0: test_membership_id = memberships_list[0].id - print(f" Reading membership ID: {test_membership_id}") + print(f"Reading membership ID: {test_membership_id}") read_options = OrganizationMembershipReadOptions( include=[OrgMembershipIncludeOpt.USER, OrgMembershipIncludeOpt.TEAMS] @@ -254,16 +252,16 @@ def main(): test_membership_id, read_options ) - print(f" Email: {membership.email}") - print(f" Status: {membership.status.value}") + print(f"Email: {membership.email}") + print(f"Status: {membership.status.value}") user_id = membership.user.id if membership.user else "N/A" - print(f" User ID: {user_id}") + print(f"User ID: {user_id}") team_count = len(membership.teams) if membership.teams else 0 - print(f" Teams: {team_count}") + print(f"Teams: {team_count}") else: - print(" Skipped: No memberships available from Test 1") + print("Skipped: No memberships available from Test 1") except Exception as e: - print(f" Error: {type(e).__name__}: {e}") + print(f"Error: {type(e).__name__}: {e}") # CREATE EXAMPLES print("\n[Create Example] Add a new organization membership:") @@ -287,12 +285,12 @@ def main(): created_membership = client.organization_memberships.create( organization_name, create_options ) - print(f" Created membership for: {created_membership.email}") - print(f" ID: {created_membership.id}") - print(f" Status: {created_membership.status.value}") + print(f"Created membership for: {created_membership.email}") + print(f"ID: {created_membership.id}") + print(f"Status: {created_membership.status.value}") except Exception as e: - print(f" Error: {type(e).__name__}: {e}") + print(f"Error: {type(e).__name__}: {e}") # Delete membership example print("\n[Delete Example] Delete an organization membership:") @@ -300,16 +298,16 @@ def main(): from pytfe.errors import NotFound membership_id = "ou-9mG77c6uE5GScg9k" # Replace with actual membership ID - print(f" Attempting to delete membership: {membership_id}") + print(f"Attempting to delete membership: {membership_id}") client.organization_memberships.delete(membership_id) - print(f" Successfully deleted membership {membership_id}") + print(f"Successfully deleted membership {membership_id}") except NotFound as e: - print(f" Membership not found: {e}") - print(" The membership may have already been deleted or the ID is invalid") + print(f"Membership not found: {e}") + print("The membership may have already been deleted or the ID is invalid") except Exception as e: - print(f" Error deleting membership: {type(e).__name__}: {e}") + print(f"Error deleting membership: {type(e).__name__}: {e}") if __name__ == "__main__": diff --git a/examples/policy.py b/examples/policy.py index d196597..74352f4 100644 --- a/examples/policy.py +++ b/examples/policy.py @@ -137,11 +137,11 @@ def main(): try: policy = client.policies.create(args.org, create_options) print(f"Created policy: {policy.id}") - print(f" Name: {policy.name}") - print(f" Kind: {policy.kind}") - print(f" Enforcement: {policy.enforcement_level}") + print(f"Name: {policy.name}") + print(f"Kind: {policy.kind}") + print(f"Enforcement: {policy.enforcement_level}") if policy.query: - print(f" Query: {policy.query}") + print(f"Query: {policy.query}") existing_policy = policy except Exception as e: print(f"Error creating policy: {e}") @@ -240,8 +240,8 @@ def main(): updated_policy = client.policies.update(existing_policy.id, update_options) print(f"Updated policy: {updated_policy.id}") - print(f" New description: {updated_policy.description}") - print(f" Enforcement level: {updated_policy.enforcement_level}") + print(f"New description: {updated_policy.description}") + print(f"Enforcement level: {updated_policy.enforcement_level}") except Exception as e: print(f"Error updating policy: {e}") diff --git a/examples/policy_check.py b/examples/policy_check.py index 771a576..67f2081 100644 --- a/examples/policy_check.py +++ b/examples/policy_check.py @@ -66,17 +66,17 @@ def main(): else: for pc in pc_list.items: print(f"- ID: {pc.id}") - print(f" Status: {pc.status}") - print(f" Scope: {pc.scope}") + print(f"Status: {pc.status}") + print(f"Scope: {pc.scope}") if pc.result: print( - f" Result: passed={pc.result.passed}, failed={pc.result.total_failed}" + f"Result: passed={pc.result.passed}, failed={pc.result.total_failed}" ) - print(f" Duration: {pc.result.duration}ms") + print(f"Duration: {pc.result.duration}ms") if pc.actions: - print(f" Can Override: {pc.actions.is_overridable}") + print(f"Can Override: {pc.actions.is_overridable}") if pc.permissions: - print(f" Has Override Permission: {pc.permissions.can_override}") + print(f"Has Override Permission: {pc.permissions.can_override}") print() except Exception as e: @@ -96,34 +96,34 @@ def main(): if pc.result: print("Result Summary:") - print(f" - Passed: {pc.result.passed}") - print(f" - Hard Failed: {pc.result.hard_failed}") - print(f" - Soft Failed: {pc.result.soft_failed}") - print(f" - Advisory Failed: {pc.result.advisory_failed}") - print(f" - Total Failed: {pc.result.total_failed}") - print(f" - Duration: {pc.result.duration}ms") - print(f" - Overall Result: {pc.result.result}") + print(f"- Passed: {pc.result.passed}") + print(f"- Hard Failed: {pc.result.hard_failed}") + print(f"- Soft Failed: {pc.result.soft_failed}") + print(f"- Advisory Failed: {pc.result.advisory_failed}") + print(f"- Total Failed: {pc.result.total_failed}") + print(f"- Duration: {pc.result.duration}ms") + print(f"- Overall Result: {pc.result.result}") if pc.actions: print("Actions:") - print(f" - Is Overridable: {pc.actions.is_overridable}") + print(f"- Is Overridable: {pc.actions.is_overridable}") if pc.permissions: print("Permissions:") - print(f" - Can Override: {pc.permissions.can_override}") + print(f"- Can Override: {pc.permissions.can_override}") if pc.status_timestamps: print("Status Timestamps:") if pc.status_timestamps.queued_at: - print(f" - Queued At: {pc.status_timestamps.queued_at}") + print(f"- Queued At: {pc.status_timestamps.queued_at}") if pc.status_timestamps.passed_at: - print(f" - Passed At: {pc.status_timestamps.passed_at}") + print(f"- Passed At: {pc.status_timestamps.passed_at}") if pc.status_timestamps.soft_failed_at: - print(f" - Soft Failed At: {pc.status_timestamps.soft_failed_at}") + print(f"- Soft Failed At: {pc.status_timestamps.soft_failed_at}") if pc.status_timestamps.hard_failed_at: - print(f" - Hard Failed At: {pc.status_timestamps.hard_failed_at}") + print(f"- Hard Failed At: {pc.status_timestamps.hard_failed_at}") if pc.status_timestamps.errored_at: - print(f" - Errored At: {pc.status_timestamps.errored_at}") + print(f"- Errored At: {pc.status_timestamps.errored_at}") except Exception as e: print(f"Error reading policy check: {e}") diff --git a/examples/policy_evaluation.py b/examples/policy_evaluation.py index 9a0bd05..fecf0c5 100644 --- a/examples/policy_evaluation.py +++ b/examples/policy_evaluation.py @@ -57,44 +57,40 @@ def main(): else: for pe in pe_list.items: print(f"- ID: {pe.id}") - print(f" Status: {pe.status}") - print(f" Policy Kind: {pe.policy_kind}") + print(f"Status: {pe.status}") + print(f"Policy Kind: {pe.policy_kind}") if pe.result_count: print(" Result Count:") if pe.result_count.passed is not None: - print(f" - Passed: {pe.result_count.passed}") + print(f"- Passed: {pe.result_count.passed}") if pe.result_count.advisory_failed is not None: - print( - f" - Advisory Failed: {pe.result_count.advisory_failed}" - ) + print(f"- Advisory Failed: {pe.result_count.advisory_failed}") if pe.result_count.mandatory_failed is not None: - print( - f" - Mandatory Failed: {pe.result_count.mandatory_failed}" - ) + print(f"- Mandatory Failed: {pe.result_count.mandatory_failed}") if pe.result_count.errored is not None: - print(f" - Errored: {pe.result_count.errored}") + print(f"- Errored: {pe.result_count.errored}") if pe.status_timestamp: print(" Status Timestamps:") if pe.status_timestamp.passed_at: - print(f" - Passed At: {pe.status_timestamp.passed_at}") + print(f"- Passed At: {pe.status_timestamp.passed_at}") if pe.status_timestamp.failed_at: - print(f" - Failed At: {pe.status_timestamp.failed_at}") + print(f"- Failed At: {pe.status_timestamp.failed_at}") if pe.status_timestamp.running_at: - print(f" - Running At: {pe.status_timestamp.running_at}") + print(f"- Running At: {pe.status_timestamp.running_at}") if pe.status_timestamp.canceled_at: - print(f" - Canceled At: {pe.status_timestamp.canceled_at}") + print(f"- Canceled At: {pe.status_timestamp.canceled_at}") if pe.status_timestamp.errored_at: - print(f" - Errored At: {pe.status_timestamp.errored_at}") + print(f"- Errored At: {pe.status_timestamp.errored_at}") if pe.task_stage: - print(f" Task Stage: {pe.task_stage.id} ({pe.task_stage.type})") + print(f"Task Stage: {pe.task_stage.id} ({pe.task_stage.type})") if pe.created_at: - print(f" Created At: {pe.created_at}") + print(f"Created At: {pe.created_at}") if pe.updated_at: - print(f" Updated At: {pe.updated_at}") + print(f"Updated At: {pe.updated_at}") print() diff --git a/examples/policy_set.py b/examples/policy_set.py index f6da973..1808d80 100644 --- a/examples/policy_set.py +++ b/examples/policy_set.py @@ -177,9 +177,9 @@ def main(): f"- ID: {ps.id} | Name: {ps.name} | Kind: {ps.kind} | Global: {ps.Global}" ) print( - f" Policy Count: {ps.policy_count} | Workspace Count: {ps.workspace_count}" + f"Policy Count: {ps.policy_count} | Workspace Count: {ps.workspace_count}" ) - print(f" Created: {ps.created_at}") + print(f"Created: {ps.created_at}") print() except Exception as e: diff --git a/examples/policy_set_parameter.py b/examples/policy_set_parameter.py index 9d6f662..9ffdf71 100644 --- a/examples/policy_set_parameter.py +++ b/examples/policy_set_parameter.py @@ -62,10 +62,10 @@ def main(): # Sensitive parameters will have masked values value_display = "***SENSITIVE***" if param.sensitive else param.value print(f"- {param.id}") - print(f" Key: {param.key}") - print(f" Value: {value_display}") - print(f" Category: {param.category.value}") - print(f" Sensitive: {param.sensitive}") + print(f"Key: {param.key}") + print(f"Value: {value_display}") + print(f"Category: {param.category.value}") + print(f"Sensitive: {param.sensitive}") print() if param_count == 0: @@ -84,11 +84,11 @@ def main(): param = client.policy_set_parameters.read(args.policy_set_id, args.parameter_id) print(f"Parameter ID: {param.id}") - print(f" Key: {param.key}") + print(f"Key: {param.key}") value_display = "***SENSITIVE***" if param.sensitive else param.value - print(f" Value: {value_display}") - print(f" Category: {param.category.value}") - print(f" Sensitive: {param.sensitive}") + print(f"Value: {value_display}") + print(f"Category: {param.category.value}") + print(f"Sensitive: {param.sensitive}") # 3) Update a parameter (if --update flag is provided) if args.update: @@ -103,12 +103,12 @@ def main(): args.policy_set_id, args.parameter_id ) print("Before update:") - print(f" Key: {current_param.key}") + print(f"Key: {current_param.key}") value_display = ( "***SENSITIVE***" if current_param.sensitive else current_param.value ) - print(f" Value: {value_display}") - print(f" Sensitive: {current_param.sensitive}") + print(f"Value: {value_display}") + print(f"Sensitive: {current_param.sensitive}") # Update the parameter update_options = PolicySetParameterUpdateOptions( @@ -122,12 +122,12 @@ def main(): ) print("\nAfter update:") - print(f" Key: {updated_param.key}") + print(f"Key: {updated_param.key}") value_display = ( "***SENSITIVE***" if updated_param.sensitive else updated_param.value ) - print(f" Value: {value_display}") - print(f" Sensitive: {updated_param.sensitive}") + print(f"Value: {value_display}") + print(f"Sensitive: {updated_param.sensitive}") # 4) Delete a parameter (if --delete flag is provided) if args.delete: @@ -143,15 +143,15 @@ def main(): args.policy_set_id, args.parameter_id ) print("Parameter to delete:") - print(f" ID: {param_to_delete.id}") - print(f" Key: {param_to_delete.key}") + print(f"ID: {param_to_delete.id}") + print(f"Key: {param_to_delete.key}") value_display = ( "***SENSITIVE***" if param_to_delete.sensitive else param_to_delete.value ) - print(f" Value: {value_display}") - print(f" Sensitive: {param_to_delete.sensitive}") + print(f"Value: {value_display}") + print(f"Sensitive: {param_to_delete.sensitive}") except Exception as e: print(f"Error reading parameter: {e}") return @@ -193,11 +193,11 @@ def main(): ) print(f"Created parameter: {new_param.id}") - print(f" Key: {new_param.key}") + print(f"Key: {new_param.key}") value_display = "***SENSITIVE***" if new_param.sensitive else new_param.value - print(f" Value: {value_display}") - print(f" Category: {new_param.category.value}") - print(f" Sensitive: {new_param.sensitive}") + print(f"Value: {value_display}") + print(f"Category: {new_param.category.value}") + print(f"Sensitive: {new_param.sensitive}") # List again to show the new parameter _print_header("Listing parameters after creation") diff --git a/examples/project.py b/examples/project.py index 3935878..7702b09 100644 --- a/examples/project.py +++ b/examples/project.py @@ -61,7 +61,7 @@ def integration_client(): ) print(f"\n Testing against organization: {org}") - print(f" Using token: {token[:10]}...") + print(f"Using token: {token[:10]}...") config = TFEConfig() @@ -96,7 +96,7 @@ def test_list_projects_integration(integration_client): try: # Test basic list without options - print(" Testing LIST operation: basic list") + print("Testing LIST operation: basic list") project_list = list(projects.list(org)) print(f"Found {len(project_list)} projects in organization '{org}'") @@ -112,13 +112,13 @@ def test_list_projects_integration(integration_client): assert hasattr(project, "description"), "Project should have a description" assert hasattr(project, "created_at"), "Project should have created_at" assert hasattr(project, "updated_at"), "Project should have updated_at" - print(f" Example project: {project.name} (ID: {project.id})") - print(f" Created: {project.created_at}, Updated: {project.updated_at}") + print(f"Example project: {project.name} (ID: {project.id})") + print(f"Created: {project.created_at}, Updated: {project.updated_at}") else: - print(" No projects found - this is normal for a new organization") + print("No projects found - this is normal for a new organization") # Test list with options - print(" Testing LIST operation: with options") + print("Testing LIST operation: with options") list_options = ProjectListOptions(page_size=5) project_list_with_options = list(projects.list(org, list_options)) print(f"List with options returned {len(project_list_with_options)} projects") @@ -144,7 +144,7 @@ def test_create_project_integration(integration_client): try: # Test CREATE operation - print(f" Testing CREATE operation: {test_name}") + print(f"Testing CREATE operation: {test_name}") create_options = ProjectCreateOptions( name=test_name, description=test_description ) @@ -184,7 +184,7 @@ def test_create_project_integration(integration_client): projects.delete(project_id) print("Cleanup successful") except Exception as e: - print(f" Warning: Failed to clean up project {project_id}: {e}") + print(f"Warning: Failed to clean up project {project_id}: {e}") def test_read_project_integration(integration_client): @@ -209,7 +209,7 @@ def test_read_project_integration(integration_client): project_id = created_project.id # Test READ operation - print(f" Testing READ operation: {project_id}") + print(f"Testing READ operation: {project_id}") read_project = projects.read(project_id) # Validate read project @@ -242,7 +242,7 @@ def test_read_project_integration(integration_client): projects.delete(project_id) print("Cleanup successful") except Exception as e: - print(f" Warning: Failed to clean up project {project_id}: {e}") + print(f"Warning: Failed to clean up project {project_id}: {e}") def test_update_project_integration(integration_client): @@ -262,7 +262,7 @@ def test_update_project_integration(integration_client): try: # Create a project to update - print(f" Creating project for UPDATE test: {original_name}") + print(f"Creating project for UPDATE test: {original_name}") create_options = ProjectCreateOptions( name=original_name, description=original_description ) @@ -270,7 +270,7 @@ def test_update_project_integration(integration_client): project_id = created_project.id # Test UPDATE operation - name only - print(" Testing UPDATE operation: name only") + print("Testing UPDATE operation: name only") update_options = ProjectUpdateOptions(name=updated_name) updated_project = projects.update(project_id, update_options) @@ -286,7 +286,7 @@ def test_update_project_integration(integration_client): print(f"UPDATE name successful: {updated_project.name}") # Test UPDATE operation - description only - print(" Testing UPDATE operation: description only") + print("Testing UPDATE operation: description only") update_options = ProjectUpdateOptions(description=updated_description) updated_project = projects.update(project_id, update_options) @@ -299,7 +299,7 @@ def test_update_project_integration(integration_client): # Test UPDATE operation - both name and description final_name = f"final-{unique_id}" final_description = "Final description for update test" - print(" Testing UPDATE operation: both name and description") + print("Testing UPDATE operation: both name and description") update_options = ProjectUpdateOptions( name=final_name, description=final_description ) @@ -324,7 +324,7 @@ def test_update_project_integration(integration_client): projects.delete(project_id) print("Cleanup successful") except Exception as e: - print(f" Warning: Failed to clean up project {project_id}: {e}") + print(f"Warning: Failed to clean up project {project_id}: {e}") def test_delete_project_integration(integration_client): @@ -341,7 +341,7 @@ def test_delete_project_integration(integration_client): try: # Create a project to delete - print(f" Creating project for DELETE test: {test_name}") + print(f"Creating project for DELETE test: {test_name}") create_options = ProjectCreateOptions( name=test_name, description="Project for delete test" ) @@ -350,7 +350,7 @@ def test_delete_project_integration(integration_client): print(f"Project created for deletion: {project_id}") # Verify project exists - print(" Verifying project exists before deletion") + print("Verifying project exists before deletion") read_project = projects.read(project_id) assert read_project.id == project_id print(f"Project confirmed to exist: {read_project.name}") @@ -361,7 +361,7 @@ def test_delete_project_integration(integration_client): print("DELETE operation completed") # Verify project is deleted - print(" Verifying project is deleted") + print("Verifying project is deleted") try: projects.read(project_id) pytest.fail("Project should not exist after deletion") @@ -403,7 +403,7 @@ def test_comprehensive_crud_integration(integration_client): project_id = None try: - print(f" Starting comprehensive CRUD test: {test_name}") + print(f"Starting comprehensive CRUD test: {test_name}") # 1. CREATE print("1 CREATE: Creating project") @@ -470,7 +470,7 @@ def test_comprehensive_crud_integration(integration_client): raise e project_id = None # Clear since deleted - print(" Comprehensive CRUD test completed successfully!") + print("Comprehensive CRUD test completed successfully!") except Exception as e: pytest.fail(f"Comprehensive CRUD test failed: {e}") @@ -491,7 +491,7 @@ def test_validation_integration(integration_client): """ projects, org = integration_client - print(" Testing validation with real API calls") + print("Testing validation with real API calls") try: # Test valid project creation @@ -532,10 +532,10 @@ def test_error_handling_integration(integration_client): """ projects, org = integration_client - print(" Testing error handling scenarios") + print("Testing error handling scenarios") # Test reading a non-existent project - print(" Testing read non-existent project") + print("Testing read non-existent project") fake_project_id = "prj-nonexistent123456789" try: projects.read(fake_project_id) @@ -545,7 +545,7 @@ def test_error_handling_integration(integration_client): assert "404" in str(e) or "not found" in str(e).lower() # Test updating a non-existent project - print(" Testing update non-existent project") + print("Testing update non-existent project") try: update_options = ProjectUpdateOptions(name="should-fail") projects.update(fake_project_id, update_options) @@ -557,7 +557,7 @@ def test_error_handling_integration(integration_client): assert "404" in str(e) or "not found" in str(e).lower() # Test deleting a non-existent project - print(" Testing delete non-existent project") + print("Testing delete non-existent project") try: projects.delete(fake_project_id) pytest.fail("Should have raised an exception for non-existent project") @@ -586,7 +586,7 @@ def test_project_tag_bindings_integration(integration_client): try: # Create a test project for tagging operations - print(f" Setting up test project for tagging: {test_name}") + print(f"Setting up test project for tagging: {test_name}") create_options = ProjectCreateOptions( name=test_name, description=test_description ) @@ -595,18 +595,18 @@ def test_project_tag_bindings_integration(integration_client): print(f"Created test project: {project_id}") # Test 1: List tag bindings (this should work) - print(" Testing LIST_TAG_BINDINGS") + print("Testing LIST_TAG_BINDINGS") try: initial_tag_bindings = projects.list_tag_bindings(project_id) assert isinstance(initial_tag_bindings, list), "Should return a list" print(f"list_tag_bindings works: {len(initial_tag_bindings)} bindings") list_tag_bindings_available = True except Exception as e: - print(f" list_tag_bindings not available: {e}") + print(f"list_tag_bindings not available: {e}") list_tag_bindings_available = False # Test 2: List effective tag bindings - print(" Testing LIST_EFFECTIVE_TAG_BINDINGS") + print("Testing LIST_EFFECTIVE_TAG_BINDINGS") try: effective_bindings = projects.list_effective_tag_bindings(project_id) assert isinstance(effective_bindings, list), "Should return a list" @@ -615,13 +615,13 @@ def test_project_tag_bindings_integration(integration_client): ) effective_tag_bindings_available = True except Exception as e: - print(f" list_effective_tag_bindings not available: {e}") - print(" This feature may require a higher HCP Terraform plan") + print(f"list_effective_tag_bindings not available: {e}") + print("This feature may require a higher HCP Terraform plan") effective_tag_bindings_available = False # Test 3: Add tag bindings (if basic listing works) if list_tag_bindings_available: - print(" Testing ADD_TAG_BINDINGS") + print("Testing ADD_TAG_BINDINGS") try: test_tags = [ TagBinding(key="environment", value="testing"), @@ -648,7 +648,7 @@ def test_project_tag_bindings_integration(integration_client): add_tag_bindings_available = True # Test 4: Delete tag bindings - print(" Testing DELETE_TAG_BINDINGS") + print("Testing DELETE_TAG_BINDINGS") try: result = projects.delete_tag_bindings(project_id) assert result is None, "Delete should return None" @@ -660,12 +660,12 @@ def test_project_tag_bindings_integration(integration_client): ) delete_tag_bindings_available = True except Exception as e: - print(f" delete_tag_bindings not available: {e}") + print(f"delete_tag_bindings not available: {e}") delete_tag_bindings_available = False except Exception as e: - print(f" add_tag_bindings not available: {e}") - print(" This feature may require a higher HCP Terraform plan") + print(f"add_tag_bindings not available: {e}") + print("This feature may require a higher HCP Terraform plan") add_tag_bindings_available = False delete_tag_bindings_available = False else: @@ -683,7 +683,7 @@ def test_project_tag_bindings_integration(integration_client): for feature_name, available in features: status = "Available" if available else " Not Available" - print(f" {feature_name}: {status}") + print(f"{feature_name}: {status}") available_count = sum(available for _, available in features) print( @@ -691,11 +691,11 @@ def test_project_tag_bindings_integration(integration_client): ) if available_count == 4: - print(" All project tag binding operations work perfectly!") + print("All project tag binding operations work perfectly!") elif available_count > 0: print("Partial functionality available - basic operations work!") else: - print(" Tag binding features may require a higher HCP Terraform plan") + print("Tag binding features may require a higher HCP Terraform plan") except Exception as e: pytest.fail( @@ -727,10 +727,10 @@ def test_project_tag_bindings_error_scenarios(integration_client): """ projects, org = integration_client - print(" Testing tag binding error scenarios") + print("Testing tag binding error scenarios") # Test invalid project ID validation - print(" Testing invalid project ID scenarios") + print("Testing invalid project ID scenarios") invalid_project_ids = ["", "x", "invalid-id", None] @@ -765,7 +765,7 @@ def test_project_tag_bindings_error_scenarios(integration_client): print(f"Correctly rejected invalid project ID '{invalid_id}': {e}") # Test empty tag binding list - print(" Testing empty tag binding list") + print("Testing empty tag binding list") try: fake_project_id = "prj-fakefakefake123" empty_options = ProjectAddTagBindingsOptions(tag_bindings=[]) @@ -776,7 +776,7 @@ def test_project_tag_bindings_error_scenarios(integration_client): assert "At least one tag binding is required" in str(e) # Test non-existent project operations - print(" Testing operations on non-existent project") + print("Testing operations on non-existent project") fake_project_id = "prj-doesnotexist123" # These should raise HTTP errors (404) from the API @@ -835,12 +835,12 @@ def test_project_tag_bindings_error_scenarios(integration_client): org = os.environ.get("TFE_ORG") if not token or not org: - print(" Please set TFE_TOKEN and TFE_ORG environment variables") - print(" export TFE_TOKEN='your-hcp-terraform-token'") - print(" export TFE_ORG='your-organization-name'") + print("Please set TFE_TOKEN and TFE_ORG environment variables") + print("export TFE_TOKEN='your-hcp-terraform-token'") + print("export TFE_ORG='your-organization-name'") sys.exit(1) - print(" Running integration tests directly...") + print("Running integration tests directly...") print( " For full pytest features, use: pytest examples/integration_test_example.py -v -s" ) diff --git a/examples/registry_module.py b/examples/registry_module.py index 12bcb46..cc53edf 100644 --- a/examples/registry_module.py +++ b/examples/registry_module.py @@ -83,17 +83,17 @@ def main(): organization_name=organization_name, registry_name=RegistryName.PRIVATE ) modules = list(client.registry_modules.list(organization_name, options)) - print(f" Found {len(modules)} registry modules") + print(f"Found {len(modules)} registry modules") for i, module in enumerate(modules[:3], 1): - print(f" {i}. {module.name}/{module.provider} (ID: {module.id})") + print(f"{i}. {module.name}/{module.provider} (ID: {module.id})") except NotFound: print( " No modules found (organization may not exist or no private modules available)" ) except Exception as e: - print(f" Error: {e}") + print(f"Error: {e}") # ===================================================== # TEST 2: CREATE REGISTRY MODULE WITH VCS CONNECTION [TESTED - COMMENTED] @@ -133,11 +133,11 @@ def main(): print( f" Created VCS module: {created_module.name}/{created_module.provider}" ) - print(f" ID: {created_module.id}") - print(f" Status: {created_module.status}") + print(f"ID: {created_module.id}") + print(f"Status: {created_module.status}") except Exception as e: - print(f" Error: {e}") + print(f"Error: {e}") # ===================================================== # TEST 3: READ REGISTRY MODULE [TESTED - COMMENTED] @@ -153,12 +153,12 @@ def main(): ) read_module = client.registry_modules.read(module_id) - print(f" Read module: {read_module.name}") - print(f" Status: {read_module.status}") - print(f" Created: {read_module.created_at}") + print(f"Read module: {read_module.name}") + print(f"Status: {read_module.status}") + print(f"Created: {read_module.created_at}") except Exception as e: - print(f" Error: {e}") + print(f"Error: {e}") # ===================================================== # TEST 4: LIST COMMITS [TESTED - COMMENTED] @@ -175,10 +175,10 @@ def main(): commits = client.registry_modules.list_commits(module_id) commit_list = list(commits.items) if hasattr(commits, "items") else [] - print(f" Found {len(commit_list)} commits") + print(f"Found {len(commit_list)} commits") except Exception as e: - print(f" Error: {e}") + print(f"Error: {e}") # ===================================================== # TEST 5: CREATE VERSION [TESTED - COMMENTED] @@ -200,11 +200,11 @@ def main(): version = client.registry_modules.create_version(module_id, version_options) created_version = version.version - print(f" Created version: {version.version}") - print(f" Status: {version.status}") + print(f"Created version: {version.version}") + print(f"Status: {version.status}") except Exception as e: - print(f" Error: {e}") + print(f"Error: {e}") # ===================================================== # TEST 6: READ VERSION [TESTED - COMMENTED] @@ -222,12 +222,12 @@ def main(): read_version = client.registry_modules.read_version( module_id, created_version ) - print(f" Read version: {read_version.version}") - print(f" Status: {read_version.status}") - print(f" ID: {read_version.id}") + print(f"Read version: {read_version.version}") + print(f"Status: {read_version.status}") + print(f"ID: {read_version.id}") except Exception as e: - print(f" Error: {e}") + print(f"Error: {e}") # ===================================================== # TEST 7: READ PUBLIC TERRAFORM REGISTRY MODULE @@ -247,20 +247,20 @@ def main(): public_module = client.registry_modules.read_terraform_registry_module( public_module_id, version ) - print(f" Read public module: {public_module.name}") - print(f" Version: {version}") - print(f" Downloads: {getattr(public_module, 'downloads', 'N/A')}") - print(f" Verified: {getattr(public_module, 'verified', 'N/A')}") - print(f" Source: {getattr(public_module, 'source', 'N/A')}") + print(f"Read public module: {public_module.name}") + print(f"Version: {version}") + print(f"Downloads: {getattr(public_module, 'downloads', 'N/A')}") + print(f"Verified: {getattr(public_module, 'verified', 'N/A')}") + print(f"Source: {getattr(public_module, 'source', 'N/A')}") except Exception as e: - print(f" Error: {e}") + print(f"Error: {e}") # ===================================================== # TEST 8: CREATE SIMPLE REGISTRY MODULE (Non-VCS) # ===================================================== print("\n8. Testing create() function (non-VCS module):") - print(" NOTE: Non-VCS modules start in PENDING status until content is uploaded") + print("NOTE: Non-VCS modules start in PENDING status until content is uploaded") try: unique_suffix = f"{int(time.time())}-{random.randint(1000, 9999)}" @@ -274,19 +274,19 @@ def main(): organization_name, create_options ) print( - f" Created simple module: {created_simple_module.name}/{created_simple_module.provider}" + f"Created simple module: {created_simple_module.name}/{created_simple_module.provider}" ) - print(f" ID: {created_simple_module.id}") + print(f"ID: {created_simple_module.id}") print( - f" Status: {created_simple_module.status} (PENDING until content uploaded)" + f"Status: {created_simple_module.status} (PENDING until content uploaded)" ) - print(f" No Code: {created_simple_module.no_code}") + print(f"No Code: {created_simple_module.no_code}") # Store for later tests (will be overridden by upload test module) created_module = created_simple_module except Exception as e: - print(f" Error: {e}") + print(f"Error: {e}") # ===================================================== # TEST 8A: LIST VERSIONS @@ -303,20 +303,20 @@ def main(): versions = client.registry_modules.list_versions(module_id) versions_list = list(versions) if hasattr(versions, "__iter__") else [] - print(f" Found {len(versions_list)} versions") + print(f"Found {len(versions_list)} versions") for i, version in enumerate(versions_list[:3], 1): - print(f" {i}. Version {version.version} (Status: {version.status})") + print(f"{i}. Version {version.version} (Status: {version.status})") except Exception as e: - print(f" Error: {e}") + print(f"Error: {e}") # ===================================================== # TEST 8B: UPDATE MODULE # ===================================================== if created_module: print("\n8B. Testing update() function:") - print(" NOTE: Update functionality may vary by TFE version") + print("NOTE: Update functionality may vary by TFE version") try: module_id = RegistryModuleID( organization=organization_name, @@ -327,7 +327,7 @@ def main(): # First check current module status current_module = client.registry_modules.read(module_id) - print(f" Current module no_code setting: {current_module.no_code}") + print(f"Current module no_code setting: {current_module.no_code}") # Try to update no_code setting update_options = RegistryModuleUpdateOptions( @@ -335,12 +335,12 @@ def main(): ) updated_module = client.registry_modules.update(module_id, update_options) - print(f" Updated module: {updated_module.name}") - print(f" No Code: {updated_module.no_code}") - print(f" Status: {updated_module.status}") + print(f"Updated module: {updated_module.name}") + print(f"No Code: {updated_module.no_code}") + print(f"Status: {updated_module.status}") except Exception as e: - print(f" Update may not be supported: {e}") + print(f"Update may not be supported: {e}") # ===================================================== # TEST 9: CREATE MODULE FOR UPLOAD TESTING @@ -358,12 +358,12 @@ def main(): created_module = client.registry_modules.create( organization_name, create_options ) - print(f" Created test module: {created_module.name}") - print(f" Provider: {created_module.provider}") - print(f" Status: {created_module.status}") + print(f"Created test module: {created_module.name}") + print(f"Provider: {created_module.provider}") + print(f"Status: {created_module.status}") except Exception as e: - print(f" Error creating module: {e}") + print(f"Error creating module: {e}") return # ===================================================== @@ -387,24 +387,24 @@ def main(): version = client.registry_modules.create_version(module_id, version_options) created_version = version.version version_object = version - print(f" Created version: {created_version}") - print(f" Status: {version.status}") + print(f"Created version: {created_version}") + print(f"Status: {version.status}") # Check if upload URL is available upload_url = ( version.links.get("upload") if hasattr(version, "links") else None ) - print(f" Upload URL available: {'Yes' if upload_url else 'No'}") + print(f"Upload URL available: {'Yes' if upload_url else 'No'}") except Exception as e: - print(f" Error creating version: {e}") + print(f"Error creating version: {e}") # ===================================================== # TEST 11: UPLOAD_TAR_GZIP FUNCTION TESTING # ===================================================== if created_module and created_version and version_object: print("\n11. Testing upload_tar_gzip() function:") - print(" This will change module status from PENDING to SETUP_COMPLETE") + print("This will change module status from PENDING to SETUP_COMPLETE") try: # Create a simple module structure in memory tar_buffer = io.BytesIO() @@ -475,12 +475,10 @@ def main(): if upload_url: client.registry_modules.upload_tar_gzip(upload_url, tar_buffer) - print( - " Successfully uploaded tar.gz content using upload_tar_gzip()" - ) + print("Successfully uploaded tar.gz content using upload_tar_gzip()") # Wait for processing - print(" Waiting 5 seconds for processing...") + print("Waiting 5 seconds for processing...") time.sleep(5) # Check module status after upload @@ -492,27 +490,27 @@ def main(): ) updated_module = client.registry_modules.read(module_id) - print(f" Updated Module Status: {updated_module.status}") + print(f"Updated Module Status: {updated_module.status}") if updated_module.status.value != "pending": print( - f" SUCCESS: Module status changed from PENDING to {updated_module.status}" + f"SUCCESS: Module status changed from PENDING to {updated_module.status}" ) else: - print(" โณ Module still processing - may take longer") + print("Module still processing - may take longer") else: - print(" No upload URL available in version links") + print(" No upload URL available in version links") except Exception as e: - print(f" Error in upload_tar_gzip test: {e}") + print(f"Error in upload_tar_gzip test: {e}") # ===================================================== # TEST 12: UPLOAD FUNCTION TESTING # ===================================================== if created_module and created_version and version_object: print("\n12. Testing upload() function:") - print(" NOTE: This function uploads from a local file path") + print("NOTE: This function uploads from a local file path") try: # Create a temporary directory with module structure with tempfile.TemporaryDirectory() as temp_dir: @@ -582,8 +580,8 @@ def main(): """.strip() ) - print(f" Created temporary module files in: {temp_dir}") - print(f" Files: {os.listdir(temp_dir)}") + print(f"Created temporary module files in: {temp_dir}") + print(f"Files: {os.listdir(temp_dir)}") # Check if upload URL is available upload_url = ( @@ -592,15 +590,15 @@ def main(): else None ) if upload_url: - print(" Upload URL available: Yes") + print("Upload URL available: Yes") # Try the upload function try: client.registry_modules.upload(version_object, temp_dir) - print(" Successfully uploaded using upload() function") + print("Successfully uploaded using upload() function") # Wait and check status - print(" Waiting 5 seconds for processing...") + print("Waiting 5 seconds for processing...") time.sleep(5) module_id = RegistryModuleID( @@ -611,14 +609,14 @@ def main(): ) updated_module = client.registry_modules.read(module_id) - print(f" Updated Module Status: {updated_module.status}") + print(f"Updated Module Status: {updated_module.status}") except NotImplementedError as nie: - print(f" upload() function not fully implemented: {nie}") - print(" This is expected - the function is a placeholder") + print(f"upload() function not fully implemented: {nie}") + print("This is expected - the function is a placeholder") # Fallback to upload_tar_gzip - print(" Trying fallback: upload_tar_gzip()...") + print("Trying fallback: upload_tar_gzip()...") tar_buffer = io.BytesIO() with tarfile.open(fileobj=tar_buffer, mode="w:gz") as tar: @@ -637,24 +635,24 @@ def main(): tar_buffer.seek(0) client.registry_modules.upload_tar_gzip(upload_url, tar_buffer) print( - " Successfully uploaded using upload_tar_gzip() as fallback" + "Successfully uploaded using upload_tar_gzip() as fallback" ) except Exception as upload_error: - print(f" upload() function error: {upload_error}") + print(f"upload() function error: {upload_error}") else: - print(" No upload URL available - cannot test upload function") + print(" No upload URL available - cannot test upload function") except Exception as e: - print(f" Error in upload() test: {e}") + print(f"Error in upload() test: {e}") # ===================================================== # TEST 13: DELETE VERSION # ===================================================== # Create a test module and version for delete testing print("\n13. Testing delete_version() function:") - print(" Creating test module and version for deletion...") + print("Creating test module and version for deletion...") test_module_for_deletion = None test_version_for_deletion = None @@ -670,7 +668,7 @@ def main(): test_module_for_deletion = client.registry_modules.create( organization_name, delete_create_options ) - print(f" Created test module: {test_module_for_deletion.name}") + print(f"Created test module: {test_module_for_deletion.name}") # Create a version for deletion testing module_id = RegistryModuleID( @@ -684,15 +682,15 @@ def main(): version = client.registry_modules.create_version(module_id, version_options) test_version_for_deletion = version.version - print(f" Created test version: {test_version_for_deletion}") + print(f"Created test version: {test_version_for_deletion}") # Now test version deletion - print(f" Testing deletion of version {test_version_for_deletion}...") + print(f"Testing deletion of version {test_version_for_deletion}...") # Delete the version client.registry_modules.delete_version(module_id, test_version_for_deletion) print( - f" Successfully called delete_version() for version: {test_version_for_deletion}" + f"Successfully called delete_version() for version: {test_version_for_deletion}" ) # Verify deletion by trying to read it @@ -706,13 +704,13 @@ def main(): version=test_version_for_deletion, ) print( - " Warning: Version still exists after deletion (may take time to process)" + "Warning: Version still exists after deletion (may take time to process)" ) except Exception: - print(" Confirmed: Version no longer exists") + print(" Confirmed: Version no longer exists") except Exception as e: - print(f" Error in delete_version test: {e}") + print(f"Error in delete_version test: {e}") # ===================================================== # TEST 14: DELETE BY NAME @@ -731,54 +729,54 @@ def main(): try: client.registry_modules.read(module_id) print( - f" Module {test_module_for_deletion.name}/{test_module_for_deletion.provider} exists" + f"Module {test_module_for_deletion.name}/{test_module_for_deletion.provider} exists" ) # Delete the module client.registry_modules.delete_by_name(module_id) print( - f" Successfully called delete_by_name() for module: {test_module_for_deletion.name}" + f"Successfully called delete_by_name() for module: {test_module_for_deletion.name}" ) # Verify deletion try: client.registry_modules.read(module_id) print( - " Warning: Module still exists after deletion (may take time to process)" + "Warning: Module still exists after deletion (may take time to process)" ) except Exception: - print(" Confirmed: Module no longer exists") + print("Confirmed: Module no longer exists") except Exception as read_error: - print(f" Module not found: {read_error}") + print(f"Module not found: {read_error}") except Exception as e: - print(f" Error in delete_by_name test: {e}") + print(f"Error in delete_by_name test: {e}") # ===================================================== # TEST 15: DELETE (Alternative delete method) # ===================================================== print("\n15. Testing delete() function:") - print(" NOTE: Testing with non-existent module to avoid conflicts") + print("NOTE: Testing with non-existent module to avoid conflicts") try: # This function takes organization and name directly # We'll test with a non-existent module to avoid conflicts test_name = "non-existent-module-for-testing" - print(f" Testing delete with non-existent module: {test_name}") + print(f"Testing delete with non-existent module: {test_name}") client.registry_modules.delete(organization_name, test_name) print( - " Delete function executed successfully (may return 404 for non-existent module)" + "Delete function executed successfully (may return 404 for non-existent module)" ) except Exception as e: - print(f" Expected error for non-existent module: {e}") + print(f"Expected error for non-existent module: {e}") # ===================================================== # TEST 16: DELETE PROVIDER (SAFE VERSION - CREATES TEST PROVIDER) # ===================================================== print("\n16. Testing delete_provider() function:") - print(" Creating a test provider specifically for deletion testing...") + print("Creating a test provider specifically for deletion testing...") try: # Create a test module with a valid provider for deletion testing @@ -794,7 +792,7 @@ def main(): test_provider_module = client.registry_modules.create( organization_name, delete_provider_options ) - print(f" Created test module with provider: {test_provider_name}") + print(f"Created test module with provider: {test_provider_name}") # Now test delete_provider function test_provider_module_id = RegistryModuleID( @@ -804,23 +802,23 @@ def main(): registry_name=RegistryName.PRIVATE, ) - print(f" Testing delete_provider() for provider: {test_provider_name}") + print(f"Testing delete_provider() for provider: {test_provider_name}") client.registry_modules.delete_provider(test_provider_module_id) print( - f" Successfully called delete_provider() for provider: {test_provider_name}" + f"Successfully called delete_provider() for provider: {test_provider_name}" ) # Verify deletion by trying to read the module try: client.registry_modules.read(test_provider_module_id) print( - " Warning: Module still exists after provider deletion (may take time to process)" + "Warning: Module still exists after provider deletion (may take time to process)" ) except Exception: - print(" Confirmed: All modules for provider have been deleted") + print("Confirmed: All modules for provider have been deleted") except Exception as e: - print(f" Error in delete_provider test: {e}") + print(f"Error in delete_provider test: {e}") # ===================================================== # TESTING SUMMARY @@ -846,7 +844,7 @@ def main(): print(" delete() - Delete module by organization and name") print(" delete_provider() - Delete all modules for a provider") if created_module: - print(f" Created test module: {created_module.name}") + print(f"Created test module: {created_module.name}") print("=" * 80) print(" ALL 15 REGISTRY MODULE FUNCTIONS HAVE BEEN TESTED!") print("=" * 80) diff --git a/examples/registry_provider.py b/examples/registry_provider.py index cd03dea..d2b7c4b 100644 --- a/examples/registry_provider.py +++ b/examples/registry_provider.py @@ -49,20 +49,20 @@ def test_list_simple(): try: providers = list(client.registry_providers.list(org)) - print(f" Found {len(providers)} providers in organization '{org}'") + print(f"Found {len(providers)} providers in organization '{org}'") for i, provider in enumerate(providers[:5], 1): - print(f" {i}. {provider.name}") - print(f" Namespace: {provider.namespace}") - print(f" Registry: {provider.registry_name.value}") - print(f" ID: {provider.id}") - print(f" Can Delete: {provider.permissions.can_delete}") + print(f"{i}. {provider.name}") + print(f"Namespace: {provider.namespace}") + print(f"Registry: {provider.registry_name.value}") + print(f"ID: {provider.id}") + print(f"Can Delete: {provider.permissions.can_delete}") print() return providers except Exception as e: - print(f" Error: {e}") + print(f"Error: {e}") return [] @@ -79,7 +79,7 @@ def test_list_with_options(): ) providers = list(client.registry_providers.list(org, options)) - print(f" Found {len(providers)} providers matching search 'test'") + print(f"Found {len(providers)} providers matching search 'test'") # Test with include include_options = RegistryProviderListOptions( @@ -87,12 +87,12 @@ def test_list_with_options(): ) detailed_providers = list(client.registry_providers.list(org, include_options)) - print(f" Found {len(detailed_providers)} providers with version details") + print(f"Found {len(detailed_providers)} providers with version details") return providers except Exception as e: - print(f" Error: {e}") + print(f"Error: {e}") return [] @@ -112,16 +112,16 @@ def test_create_private(): ) provider = client.registry_providers.create(org, options) - print(f" Created private provider: {provider.name}") - print(f" ID: {provider.id}") - print(f" Namespace: {provider.namespace}") - print(f" Registry: {provider.registry_name.value}") - print(f" Created: {provider.created_at}") + print(f"Created private provider: {provider.name}") + print(f"ID: {provider.id}") + print(f"Namespace: {provider.namespace}") + print(f"Registry: {provider.registry_name.value}") + print(f"Created: {provider.created_at}") return provider except Exception as e: - print(f" Error creating private provider: {e}") + print(f"Error creating private provider: {e}") return None @@ -142,16 +142,16 @@ def test_create_public(): ) provider = client.registry_providers.create(org, options) - print(f" Created public provider: {provider.name}") - print(f" ID: {provider.id}") - print(f" Namespace: {provider.namespace}") - print(f" Registry: {provider.registry_name.value}") - print(f" Created: {provider.created_at}") + print(f"Created public provider: {provider.name}") + print(f"ID: {provider.id}") + print(f"Namespace: {provider.namespace}") + print(f"Registry: {provider.registry_name.value}") + print(f"Created: {provider.created_at}") return provider except Exception as e: - print(f" Error creating public provider: {e}") + print(f"Error creating public provider: {e}") return None @@ -162,7 +162,7 @@ def test_read_with_id(provider_data): client, org = get_client_and_org() if not provider_data: - print(" No provider data provided") + print("No provider data provided") return None try: @@ -175,13 +175,13 @@ def test_read_with_id(provider_data): # Basic read provider = client.registry_providers.read(provider_id) - print(f" Read provider: {provider.name}") - print(f" ID: {provider.id}") - print(f" Namespace: {provider.namespace}") - print(f" Registry: {provider.registry_name.value}") - print(f" Created: {provider.created_at}") - print(f" Updated: {provider.updated_at}") - print(f" Can Delete: {provider.permissions.can_delete}") + print(f"Read provider: {provider.name}") + print(f"ID: {provider.id}") + print(f"Namespace: {provider.namespace}") + print(f"Registry: {provider.registry_name.value}") + print(f"Created: {provider.created_at}") + print(f"Updated: {provider.updated_at}") + print(f"Can Delete: {provider.permissions.can_delete}") # Read with options options = RegistryProviderReadOptions( @@ -189,19 +189,17 @@ def test_read_with_id(provider_data): ) detailed_provider = client.registry_providers.read(provider_id, options) - print(f" Read with options: {detailed_provider.name}") + print(f"Read with options: {detailed_provider.name}") if detailed_provider.registry_provider_versions: - print( - f" Found {len(detailed_provider.registry_provider_versions)} versions" - ) + print(f"Found {len(detailed_provider.registry_provider_versions)} versions") else: - print(" No versions found") + print("No versions found") return provider except Exception as e: - print(f" Error reading provider: {e}") + print(f"Error reading provider: {e}") return None @@ -212,7 +210,7 @@ def test_delete_by_id(provider_data): client, org = get_client_and_org() if not provider_data: - print(" No provider data provided") + print("No provider data provided") return False try: @@ -225,11 +223,11 @@ def test_delete_by_id(provider_data): # Verify provider exists provider = client.registry_providers.read(provider_id) - print(f" Found provider to delete: {provider.name}") + print(f"Found provider to delete: {provider.name}") # Delete the provider client.registry_providers.delete(provider_id) - print(" Successfully called delete() for provider") + print("Successfully called delete() for provider") # Verify deletion (optional - may take time) import time @@ -238,20 +236,20 @@ def test_delete_by_id(provider_data): try: client.registry_providers.read(provider_id) - print(" Provider still exists (deletion may take time)") + print("Provider still exists (deletion may take time)") except Exception: - print(" Provider successfully deleted") + print("Provider successfully deleted") return True except Exception as e: - print(f" Error deleting provider: {e}") + print(f"Error deleting provider: {e}") return False def main(): """Run all tests in sequence.""" - print(" REGISTRY PROVIDER INDIVIDUAL TESTS") + print("REGISTRY PROVIDER INDIVIDUAL TESTS") print("=" * 50) # Test 1: List providers @@ -263,8 +261,8 @@ def main(): print() # WARNING: Uncomment the following tests to create/delete providers - print(" WARNING: Creation and deletion tests are commented out for safety") - print(" Uncomment them in the code to test creation and deletion") + print("WARNING: Creation and deletion tests are commented out for safety") + print("Uncomment them in the code to test creation and deletion") print() # UNCOMMENT TO TEST CREATION: @@ -298,7 +296,7 @@ def main(): print() print("Individual tests completed!") - print(" To test creation/deletion, uncomment the relevant sections in the code") + print("To test creation/deletion, uncomment the relevant sections in the code") if __name__ == "__main__": diff --git a/examples/reserved_tag_key.py b/examples/reserved_tag_key.py index 75b9fb9..b0056c0 100644 --- a/examples/reserved_tag_key.py +++ b/examples/reserved_tag_key.py @@ -36,11 +36,11 @@ def main(): # Validate environment variables if not TFE_TOKEN: - print(" Error: TFE_TOKEN environment variable is required") + print("Error: TFE_TOKEN environment variable is required") sys.exit(1) if not TFE_ORG: - print(" Error: TFE_ORG environment variable is required") + print("Error: TFE_ORG environment variable is required") sys.exit(1) # Initialize the TFE client @@ -68,7 +68,7 @@ def main(): new_rtk = client.reserved_tag_key.create(TFE_ORG, create_options) print(f"Created reserved tag key: {new_rtk.id} - {new_rtk.key}") - print(f" Disable Overrides: {new_rtk.disable_overrides}") + print(f"Disable Overrides: {new_rtk.disable_overrides}") # 3. Update the reserved tag key print("\n3. Updating the reserved tag key...") @@ -78,7 +78,7 @@ def main(): updated_rtk = client.reserved_tag_key.update(new_rtk.id, update_options) print(f"Updated reserved tag key: {updated_rtk.id} - {updated_rtk.key}") - print(f" Disable Overrides: {updated_rtk.disable_overrides}") + print(f"Disable Overrides: {updated_rtk.disable_overrides}") # 4. Delete the reserved tag key print("\n4. Deleting the reserved tag key...") @@ -95,8 +95,8 @@ def main(): list_options = ReservedTagKeyListOptions(page_size=5, page_number=1) paginated_rtks = client.reserved_tag_key.list(TFE_ORG, list_options) print(f"Page 1 with page size 5: {len(paginated_rtks.items)} keys") - print(f" Total pages: {paginated_rtks.total_pages}") - print(f" Total count: {paginated_rtks.total_count}") + print(f"Total pages: {paginated_rtks.total_pages}") + print(f"Total count: {paginated_rtks.total_count}") print("\n Reserved Tag Keys API example completed successfully!") @@ -108,11 +108,11 @@ def main(): print(f"\n TFE API Error: {e}") if hasattr(e, "status"): if e.status == 403: - print(" Permission denied - check token permissions") + print("Permission denied - check token permissions") elif e.status == 401: - print(" Authentication failed - check token validity") + print("Authentication failed - check token validity") elif e.status == 422: - print(" Validation error - check reserved tag key format") + print("Validation error - check reserved tag key format") sys.exit(1) except Exception as e: diff --git a/examples/run.py b/examples/run.py index 79cbca9..d95b6e9 100644 --- a/examples/run.py +++ b/examples/run.py @@ -82,10 +82,8 @@ def main(): for run in run_list.items: print(f"- {run.id} | status={run.status} | created={run.created_at}") - print(f" message: {run.message}") - print( - f" has_changes: {run.has_changes} | is_destroy: {run.is_destroy}" - ) + print(f"message: {run.message}") + print(f"has_changes: {run.has_changes} | is_destroy: {run.is_destroy}") if not run_list.items: print("No runs found.") @@ -119,11 +117,11 @@ def main(): if detailed_run.actions: print("\nAvailable Actions:") - print(f" Can Apply: {detailed_run.actions.is_confirmable}") - print(f" Can Cancel: {detailed_run.actions.is_cancelable}") - print(f" Can Discard: {detailed_run.actions.is_discardable}") + print(f"Can Apply: {detailed_run.actions.is_confirmable}") + print(f"Can Cancel: {detailed_run.actions.is_cancelable}") + print(f"Can Discard: {detailed_run.actions.is_discardable}") print( - f" Can Force Cancel: {detailed_run.actions.is_force_cancelable}" + f"Can Force Cancel: {detailed_run.actions.is_force_cancelable}" ) if detailed_run.created_by: @@ -196,7 +194,7 @@ def main(): for run in org_runs.items[:3]: # Show first 3 print(f"- {run.id} | status={run.status}") if run.workspace: - print(f" workspace: {run.workspace.name}") + print(f"workspace: {run.workspace.name}") except Exception as e: print(f"Error listing organization runs: {e}") @@ -226,37 +224,37 @@ def main(): print("\n1. Basic read():") try: basic_run = client.runs.read(demo_run.id) - print(f" Read run {basic_run.id} - status: {basic_run.status}") + print(f"Read run {basic_run.id} - status: {basic_run.status}") except Exception as e: - print(f" Error: {e}") + print(f"Error: {e}") # Show action methods (but don't execute them for safety) print("\n2. Available action methods (not executed):") - print(" # Apply run:") + print("# Apply run:") print( - f" # client.runs.apply('{demo_run.id}', RunApplyOptions(comment='Applied via SDK'))" + f"# client.runs.apply('{demo_run.id}', RunApplyOptions(comment='Applied via SDK'))" ) - print(" # Cancel run:") + print("# Cancel run:") print( - f" # client.runs.cancel('{demo_run.id}', RunCancelOptions(comment='Canceled via SDK'))" + f"# client.runs.cancel('{demo_run.id}', RunCancelOptions(comment='Canceled via SDK'))" ) - print(" # Force cancel run:") + print("# Force cancel run:") print( - f" # client.runs.force_cancel('{demo_run.id}', RunForceCancelOptions(comment='Force canceled'))" + f"# client.runs.force_cancel('{demo_run.id}', RunForceCancelOptions(comment='Force canceled'))" ) - print(" # Discard run:") + print("# Discard run:") print( - f" # client.runs.discard('{demo_run.id}', RunDiscardOptions(comment='Discarded via SDK'))" + f"# client.runs.discard('{demo_run.id}', RunDiscardOptions(comment='Discarded via SDK'))" ) - print(" # Force execute run:") - print(f" # client.runs.force_execute('{demo_run.id}')") + print("# Force execute run:") + print(f"# client.runs.force_execute('{demo_run.id}')") print("\n Note: These actions are commented out for safety.") - print(" Uncomment and use them carefully in your own code.") + print("Uncomment and use them carefully in your own code.") if __name__ == "__main__": diff --git a/examples/run_events.py b/examples/run_events.py index 75033c2..a648c5b 100644 --- a/examples/run_events.py +++ b/examples/run_events.py @@ -106,9 +106,9 @@ def main(): else: for event in event_list.items: print(f"Event ID: {event.id}") - print(f" Action: {event.action or 'N/A'}") - print(f" Description: {event.description or 'N/A'}") - print(f" Created At: {event.created_at or 'N/A'}") + print(f"Action: {event.action or 'N/A'}") + print(f"Description: {event.description or 'N/A'}") + print(f"Created At: {event.created_at or 'N/A'}") print() diff --git a/examples/run_task.py b/examples/run_task.py index e62a7da..8331941 100644 --- a/examples/run_task.py +++ b/examples/run_task.py @@ -112,7 +112,7 @@ def main(): if count >= args.page_size * 2: # Safety limit based on page size break - print(f" Found {len(run_task_list)} run tasks") + print(f"Found {len(run_task_list)} run tasks") print() if not run_task_list: @@ -120,15 +120,15 @@ def main(): else: for i, task in enumerate(run_task_list, 1): print(f"{i:2d}. {task.name}") - print(f" ID: {task.id}") - print(f" URL: {task.url}") - print(f" Category: {task.category}") - print(f" Enabled: {task.enabled}") + print(f"ID: {task.id}") + print(f"URL: {task.url}") + print(f"Category: {task.category}") + print(f"Enabled: {task.enabled}") if task.description: - print(f" Description: {task.description}") + print(f"Description: {task.description}") print() except Exception as e: - print(f" Error listing run tasks: {e}") + print(f"Error listing run tasks: {e}") return # 2) Create a new run task if requested @@ -149,19 +149,19 @@ def main(): print(f"Creating run task '{task_name}' in organization '{args.org}'...") run_task = client.run_tasks.create(args.org, create_options) - print(" Successfully created run task!") - print(f" Name: {run_task.name}") - print(f" ID: {run_task.id}") - print(f" URL: {run_task.url}") - print(f" Category: {run_task.category}") - print(f" Enabled: {run_task.enabled}") - print(f" Description: {run_task.description}") - print(f" HMAC Key: {'[CONFIGURED]' if run_task.hmac_key else 'None'}") + print("Successfully created run task!") + print(f"Name: {run_task.name}") + print(f"ID: {run_task.id}") + print(f"URL: {run_task.url}") + print(f"Category: {run_task.category}") + print(f"Enabled: {run_task.enabled}") + print(f"Description: {run_task.description}") + print(f"HMAC Key: {'[CONFIGURED]' if run_task.hmac_key else 'None'}") print() args.task_id = run_task.id # Use the created task for other operations except Exception as e: - print(f" Error creating run task: {e}") + print(f"Error creating run task: {e}") return # 3) Read run task details if task ID is provided @@ -180,26 +180,24 @@ def main(): run_task = client.run_tasks.read(args.task_id) print("Reading run task details...") - print(" Successfully read run task!") - print(f" Name: {run_task.name}") - print(f" ID: {run_task.id}") - print(f" URL: {run_task.url}") - print(f" Category: {run_task.category}") - print(f" Enabled: {run_task.enabled}") - print(f" Description: {run_task.description or 'None'}") - print(f" HMAC Key: {'[SET]' if run_task.hmac_key else 'None'}") + print("Successfully read run task!") + print(f"Name: {run_task.name}") + print(f"ID: {run_task.id}") + print(f"URL: {run_task.url}") + print(f"Category: {run_task.category}") + print(f"Enabled: {run_task.enabled}") + print(f"Description: {run_task.description or 'None'}") + print(f"HMAC Key: {'[SET]' if run_task.hmac_key else 'None'}") if run_task.organization: - print(f" Organization: {run_task.organization.id}") + print(f"Organization: {run_task.organization.id}") if run_task.workspace_run_tasks: - print( - f" Workspace Run Tasks: {len(run_task.workspace_run_tasks)} items" - ) + print(f"Workspace Run Tasks: {len(run_task.workspace_run_tasks)} items") print() except Exception as e: - print(f" Error reading run task: {e}") + print(f"Error reading run task: {e}") return # 4) Update run task if requested @@ -214,14 +212,14 @@ def main(): ) print(f"Updating run task '{args.task_id}'...") updated_task = client.run_tasks.update(args.task_id, update_options) - print(" Successfully updated run task!") - print(f" Name: {updated_task.name}") - print(f" Description: {updated_task.description}") - print(f" URL: {updated_task.url}") - print(f" Enabled: {updated_task.enabled}") + print("Successfully updated run task!") + print(f"Name: {updated_task.name}") + print(f"Description: {updated_task.description}") + print(f"URL: {updated_task.url}") + print(f"Enabled: {updated_task.enabled}") print() except Exception as e: - print(f" Error updating run task: {e}") + print(f"Error updating run task: {e}") return # 5) Delete run task if requested (should be last operation) @@ -230,10 +228,10 @@ def main(): try: print(f"Deleting run task '{args.task_id}'...") client.run_tasks.delete(args.task_id) - print(f" Successfully deleted run task: {args.task_id}") + print(f"Successfully deleted run task: {args.task_id}") print() except Exception as e: - print(f" Error deleting run task: {e}") + print(f"Error deleting run task: {e}") return diff --git a/examples/run_trigger.py b/examples/run_trigger.py index 35dba80..c6fed59 100644 --- a/examples/run_trigger.py +++ b/examples/run_trigger.py @@ -140,7 +140,7 @@ def main(): if count >= args.page_size * 2: # Safety limit based on page size break - print(f" Found {len(run_trigger_list)} run triggers") + print(f"Found {len(run_trigger_list)} run triggers") print() if not run_trigger_list: @@ -150,15 +150,15 @@ def main(): print( f"{i:2d}. {trigger.sourceable_name} โ†’ {trigger.workspace_name}" ) - print(f" ID: {trigger.id}") - print(f" Created: {trigger.created_at}") + print(f"ID: {trigger.id}") + print(f"Created: {trigger.created_at}") if trigger.sourceable and hasattr(trigger.sourceable, "id"): - print(f" Source Workspace ID: {trigger.sourceable.id}") + print(f"Source Workspace ID: {trigger.sourceable.id}") if trigger.workspace and hasattr(trigger.workspace, "id"): - print(f" Target Workspace ID: {trigger.workspace.id}") + print(f"Target Workspace ID: {trigger.workspace.id}") print() except Exception as e: - print(f" Error listing run triggers: {e}") + print(f"Error listing run triggers: {e}") return # 2) Create a new run trigger if requested @@ -178,11 +178,11 @@ def main(): f"Creating run trigger from workspace '{args.source_workspace_id}' to '{args.workspace_id}'..." ) run_trigger = client.run_triggers.create(args.workspace_id, create_options) - print(" Successfully created run trigger!") - print(f" ID: {run_trigger.id}") - print(f" Source: {run_trigger.sourceable_name}") - print(f" Target: {run_trigger.workspace_name}") - print(f" Created: {run_trigger.created_at}") + print("Successfully created run trigger!") + print(f"ID: {run_trigger.id}") + print(f"Source: {run_trigger.sourceable_name}") + print(f"Target: {run_trigger.workspace_name}") + print(f"Created: {run_trigger.created_at}") if run_trigger.sourceable: print( @@ -198,10 +198,10 @@ def main(): run_trigger.id ) # Use the created trigger for other operations except Exception as e: - print(f" Error creating run trigger: {e}") + print(f"Error creating run trigger: {e}") return elif args.create: - print(" Error: --create requires both --workspace-id and --source-workspace-id") + print("Error: --create requires both --workspace-id and --source-workspace-id") return # 3) Read run trigger details if trigger ID is provided @@ -211,37 +211,37 @@ def main(): print("Reading run trigger details...") run_trigger = client.run_triggers.read(args.trigger_id) - print(" Successfully read run trigger!") - print(f" ID: {run_trigger.id}") - print(f" Type: {run_trigger.type}") - print(f" Source: {run_trigger.sourceable_name}") - print(f" Target: {run_trigger.workspace_name}") - print(f" Created: {run_trigger.created_at}") + print("Successfully read run trigger!") + print(f"ID: {run_trigger.id}") + print(f"Type: {run_trigger.type}") + print(f"Source: {run_trigger.sourceable_name}") + print(f"Target: {run_trigger.workspace_name}") + print(f"Created: {run_trigger.created_at}") # Show detailed workspace information if run_trigger.sourceable: - print(" Source Workspace Details:") - print(f" - Name: {run_trigger.sourceable.name}") - print(f" - ID: {run_trigger.sourceable.id}") + print("Source Workspace Details:") + print(f"- Name: {run_trigger.sourceable.name}") + print(f"- ID: {run_trigger.sourceable.id}") if ( hasattr(run_trigger.sourceable, "organization") and run_trigger.sourceable.organization ): - print(f" - Organization: {run_trigger.sourceable.organization}") + print(f"- Organization: {run_trigger.sourceable.organization}") if run_trigger.workspace: - print(" Target Workspace Details:") - print(f" - Name: {run_trigger.workspace.name}") - print(f" - ID: {run_trigger.workspace.id}") + print("Target Workspace Details:") + print(f"- Name: {run_trigger.workspace.name}") + print(f"- ID: {run_trigger.workspace.id}") if ( hasattr(run_trigger.workspace, "organization") and run_trigger.workspace.organization ): - print(f" - Organization: {run_trigger.workspace.organization}") + print(f"- Organization: {run_trigger.workspace.organization}") print() except Exception as e: - print(f" Error reading run trigger: {e}") + print(f"Error reading run trigger: {e}") return # 4) Delete run trigger if requested (should be last operation) @@ -250,10 +250,10 @@ def main(): try: print(f"Deleting run trigger '{args.trigger_id}'...") client.run_triggers.delete(args.trigger_id) - print(f" Successfully deleted run trigger: {args.trigger_id}") + print(f"Successfully deleted run trigger: {args.trigger_id}") print() except Exception as e: - print(f" Error deleting run trigger: {e}") + print(f"Error deleting run trigger: {e}") return diff --git a/examples/ssh_keys.py b/examples/ssh_keys.py index 43e4a8a..743bd98 100644 --- a/examples/ssh_keys.py +++ b/examples/ssh_keys.py @@ -42,28 +42,28 @@ def check_token_type(token): """Check and validate token type for SSH Keys API.""" - print(" Token Analysis:") + print("Token Analysis:") if token.startswith("AT-"): - print(" Token Type: Organization Token (AT-*)") - print(" SSH Keys API does NOT support Organization Tokens") - print(" Please create a User Token instead") + print("Token Type: Organization Token (AT-*)") + print("SSH Keys API does NOT support Organization Tokens") + print("Please create a User Token instead") print("") - print(" To create a User Token:") - print(" 1. Go to Terraform Cloud โ†’ User Settings โ†’ Tokens") - print(" 2. Create new token with VCS management permissions") - print(" 3. Replace TFE_TOKEN environment variable") + print("To create a User Token:") + print("1. Go to Terraform Cloud โ†’ User Settings โ†’ Tokens") + print("2. Create new token with VCS management permissions") + print("3. Replace TFE_TOKEN environment variable") return False elif token.startswith("TF-"): - print(" Token Type: User Token (TF-*)") - print(" SSH Keys API supports User Tokens") + print("Token Type: User Token (TF-*)") + print("SSH Keys API supports User Tokens") return True elif ".atlasv1." in token: - print(" Token Type: User/Team Token (.atlasv1. format)") - print(" SSH Keys API supports User/Team Tokens") + print("Token Type: User/Team Token (.atlasv1. format)") + print("SSH Keys API supports User/Team Tokens") return True else: - print(f" Token Type: Unknown format ({token[:10]}...)") - print(" Expected User Token (TF-*) or Team Token") + print(f"Token Type: Unknown format ({token[:10]}...)") + print("Expected User Token (TF-*) or Team Token") return True # Allow unknown formats to try @@ -72,17 +72,17 @@ def main(): # Validate environment variables if not TFE_TOKEN: - print(" Error: TFE_TOKEN environment variable is required") - print(" Create a User Token (not Organization Token) in Terraform Cloud") + print("Error: TFE_TOKEN environment variable is required") + print("Create a User Token (not Organization Token) in Terraform Cloud") sys.exit(1) if not TFE_ORG: - print(" Error: TFE_ORG environment variable is required") + print("Error: TFE_ORG environment variable is required") sys.exit(1) if not SSH_KEY_VALUE: - print(" Error: SSH_PRIVATE_KEY environment variable is required") - print(" Provide a valid SSH private key for testing") + print("Error: SSH_PRIVATE_KEY environment variable is required") + print("Provide a valid SSH private key for testing") sys.exit(1) # Check token type first @@ -100,9 +100,9 @@ def main(): # 1. List existing SSH keys print("\n1. Listing SSH keys...") ssh_keys = client.ssh_keys.list(TFE_ORG) - print(f" Found {len(ssh_keys.items)} SSH keys:") + print(f"Found {len(ssh_keys.items)} SSH keys:") for key in ssh_keys.items: - print(f" - ID: {key.id}, Name: {key.name}") + print(f"- ID: {key.id}, Name: {key.name}") # 2. Create a new SSH key print("\n2. Creating a new SSH key...") @@ -111,46 +111,46 @@ def main(): ) new_key = client.ssh_keys.create(TFE_ORG, create_options) - print(f" Created SSH key: {new_key.id} - {new_key.name}") + print(f"Created SSH key: {new_key.id} - {new_key.name}") # 3. Read the SSH key we just created print("\n3. Reading the SSH key...") read_key = client.ssh_keys.read(new_key.id) - print(f" Read SSH key: {read_key.id} - {read_key.name}") + print(f"Read SSH key: {read_key.id} - {read_key.name}") # 4. Update the SSH key print("\n4. Updating the SSH key...") update_options = SSHKeyUpdateOptions(name="Updated Python TFE Example SSH Key") updated_key = client.ssh_keys.update(new_key.id, update_options) - print(f" Updated SSH key: {updated_key.id} - {updated_key.name}") + print(f"Updated SSH key: {updated_key.id} - {updated_key.name}") # 5. Delete the SSH key print("\n5. Deleting the SSH key...") client.ssh_keys.delete(new_key.id) - print(f" Deleted SSH key: {new_key.id}") + print(f"Deleted SSH key: {new_key.id}") # 6. Verify deletion by listing again print("\n6. Verifying deletion...") ssh_keys_after = client.ssh_keys.list(TFE_ORG) - print(f" SSH keys after deletion: {len(ssh_keys_after.items)}") + print(f"SSH keys after deletion: {len(ssh_keys_after.items)}") # 7. Demonstrate pagination with options print("\n7. Demonstrating pagination options...") list_options = SSHKeyListOptions(page_size=5, page_number=1) paginated_keys = client.ssh_keys.list(TFE_ORG, list_options) - print(f" Page 1 with page size 5: {len(paginated_keys.items)} keys") - print(f" Total pages: {paginated_keys.total_pages}") - print(f" Total count: {paginated_keys.total_count}") + print(f"Page 1 with page size 5: {len(paginated_keys.items)} keys") + print(f"Total pages: {paginated_keys.total_pages}") + print(f"Total count: {paginated_keys.total_count}") print("\n SSH Keys API example completed successfully!") except NotFound as e: print(f"\n SSH Keys API Error: {e}") print("\n This error usually means:") - print(" - Using Organization Token (not allowed)") - print(" - SSH Keys feature not available") - print(" - Insufficient permissions") + print("- Using Organization Token (not allowed)") + print("- SSH Keys feature not available") + print("- Insufficient permissions") print("\n Try using a User Token instead of Organization Token") sys.exit(1) @@ -158,11 +158,11 @@ def main(): print(f"\n TFE API Error: {e}") if hasattr(e, "status"): if e.status == 403: - print(" Permission denied - check token type and permissions") + print("Permission denied - check token type and permissions") elif e.status == 401: - print(" Authentication failed - check token validity") + print("Authentication failed - check token validity") elif e.status == 422: - print(" Validation error - check SSH key format") + print("Validation error - check SSH key format") sys.exit(1) except Exception as e: diff --git a/examples/variable_sets.py b/examples/variable_sets.py index 4a6ca50..0b41084 100644 --- a/examples/variable_sets.py +++ b/examples/variable_sets.py @@ -68,7 +68,7 @@ def variable_set_example(): print(f"Found {len(variable_sets)} existing variable sets") for vs in variable_sets[:3]: # Show first 3 - print(f" - {vs.name} (ID: {vs.id}, Global: {vs.global_})") + print(f"- {vs.name} (ID: {vs.id}, Global: {vs.global_})") print() # 2. Create a new variable set @@ -87,9 +87,9 @@ def variable_set_example(): print( f"Created variable set: {new_variable_set.name} (ID: {new_variable_set.id})" ) - print(f" Description: {new_variable_set.description}") - print(f" Global: {new_variable_set.global_}") - print(f" Priority: {new_variable_set.priority}") + print(f"Description: {new_variable_set.description}") + print(f"Global: {new_variable_set.global_}") + print(f"Priority: {new_variable_set.priority}") print() # 3. Create variables in the variable set @@ -155,8 +155,8 @@ def variable_set_example(): for var in variables: sensitive_note = " (sensitive)" if var.sensitive else "" hcl_note = " (HCL)" if var.hcl else "" - print(f" - {var.key}: {var.category.value}{sensitive_note}{hcl_note}") - print(f" Description: {var.description}") + print(f"- {var.key}: {var.category.value}{sensitive_note}{hcl_note}") + print(f"Description: {var.description}") print() # 5. Update a variable @@ -171,7 +171,7 @@ def variable_set_example(): created_variable_set_id, tf_variable.id, update_var_options ) print(f"Updated variable: {updated_variable.key} = {updated_variable.value}") - print(f" New description: {updated_variable.description}") + print(f"New description: {updated_variable.description}") print() # 6. Update the variable set itself @@ -186,8 +186,8 @@ def variable_set_example(): created_variable_set_id, update_set_options ) print(f"Updated variable set: {updated_variable_set.name}") - print(f" New description: {updated_variable_set.description}") - print(f" Priority: {updated_variable_set.priority}") + print(f"New description: {updated_variable_set.description}") + print(f"Priority: {updated_variable_set.priority}") print() # 7. Example: Apply to workspaces (if any exist) @@ -279,8 +279,8 @@ def variable_set_example(): created_variable_set_id, read_options ) print(f"Variable set: {detailed_varset.name}") - print(f" Variables count: {len(detailed_varset.vars or [])}") - print(f" Workspaces count: {len(detailed_varset.workspaces or [])}") + print(f"Variables count: {len(detailed_varset.vars or [])}") + print(f"Workspaces count: {len(detailed_varset.workspaces or [])}") print() print("=== Variable Set Operations Completed Successfully ===") @@ -347,8 +347,8 @@ def global_variable_set_example(): global_varset = client.variable_sets.create(org_name, global_create_options) created_variable_set_id = global_varset.id print(f"Created global variable set: {global_varset.name}") - print(f" Global: {global_varset.global_}") - print(f" Priority: {global_varset.priority}") + print(f"Global: {global_varset.global_}") + print(f"Priority: {global_varset.priority}") # Add some common variables print("\nAdding common variables...") @@ -376,7 +376,7 @@ def global_variable_set_example(): variable = client.variable_set_variables.create( created_variable_set_id, var_options ) - print(f" Added {variable.category.value} variable: {variable.key}") + print(f"Added {variable.category.value} variable: {variable.key}") print(f"\nGlobal variable set is now available to all workspaces in {org_name}") @@ -463,7 +463,7 @@ def project_scoped_variable_set_example(): variable = client.variable_set_variables.create( created_variable_set_id, var_options ) - print(f" Added variable: {variable.key}") + print(f"Added variable: {variable.key}") print( f"\nProject-scoped variable set is available to workspaces in project: {target_project.name}" diff --git a/examples/variables.py b/examples/variables.py index ba47f09..0b3fe34 100644 --- a/examples/variables.py +++ b/examples/variables.py @@ -48,10 +48,10 @@ def main(): try: variable = client.variables.create(workspace_id, terraform_var) created_variables.append(variable.id) - print(f" Created Terraform variable: {variable.key} = {variable.value}") - print(f" ID: {variable.id}, Category: {variable.category}") + print(f"Created Terraform variable: {variable.key} = {variable.value}") + print(f"ID: {variable.id}, Category: {variable.category}") except Exception as e: - print(f" Error creating Terraform variable: {e}") + print(f"Error creating Terraform variable: {e}") # Create an environment variable env_var = VariableCreateOptions( @@ -66,10 +66,10 @@ def main(): try: variable = client.variables.create(workspace_id, env_var) created_variables.append(variable.id) - print(f" Created environment variable: {variable.key} = {variable.value}") - print(f" ID: {variable.id}, Category: {variable.category}") + print(f"Created environment variable: {variable.key} = {variable.value}") + print(f"ID: {variable.id}, Category: {variable.category}") except Exception as e: - print(f" Error creating environment variable: {e}") + print(f"Error creating environment variable: {e}") # Create a sensitive variable secret_var = VariableCreateOptions( @@ -84,10 +84,10 @@ def main(): try: variable = client.variables.create(workspace_id, secret_var) created_variables.append(variable.id) - print(f" Created sensitive variable: {variable.key} = ***HIDDEN***") - print(f" ID: {variable.id}, Category: {variable.category}") + print(f"Created sensitive variable: {variable.key} = ***HIDDEN***") + print(f"ID: {variable.id}, Category: {variable.category}") except Exception as e: - print(f" Error creating sensitive variable: {e}") + print(f"Error creating sensitive variable: {e}") # Small delay to ensure variables are created time.sleep(1) @@ -101,11 +101,9 @@ def main(): print(f"Found {len(variables)} workspace variables:") for var in variables: value_display = "***SENSITIVE***" if var.sensitive else var.value - print( - f" โ€ข {var.key} = {value_display} ({var.category}) [ID: {var.id}]" - ) + print(f"{var.key} = {value_display} ({var.category}) [ID: {var.id}]") except Exception as e: - print(f" Error listing variables: {e}") + print(f"Error listing variables: {e}") # 3. Test LIST_ALL function (includes inherited variables from variable sets) print("\n3. Testing LIST_ALL operation (includes variable sets):") @@ -116,11 +114,9 @@ def main(): print(f"Found {len(all_variables)} total variables (including inherited):") for var in all_variables: value_display = "***SENSITIVE***" if var.sensitive else var.value - print( - f" โ€ข {var.key} = {value_display} ({var.category}) [ID: {var.id}]" - ) + print(f"{var.key} = {value_display} ({var.category}) [ID: {var.id}]") except Exception as e: - print(f" Error listing all variables: {e}") + print(f"Error listing all variables: {e}") # Test READ function with specific variable ID - COMMENTED OUT print("\n4. Testing READ operation with specific variable ID:") @@ -134,18 +130,18 @@ def main(): variable = client.variables.read(workspace_id, test_variable_id) # For testing, show actual values even for sensitive variables if variable.sensitive: - print(f" Read variable: {variable.key} = {variable.value} (SENSITIVE)") + print(f"Read variable: {variable.key} = {variable.value} (SENSITIVE)") else: - print(f" Read variable: {variable.key} = {variable.value}") - print(f" ID: {variable.id}") - print(f" Description: {variable.description}") - print(f" Category: {variable.category}") - print(f" HCL: {variable.hcl}") - print(f" Sensitive: {variable.sensitive}") + print(f"Read variable: {variable.key} = {variable.value}") + print(f"ID: {variable.id}") + print(f"Description: {variable.description}") + print(f"Category: {variable.category}") + print(f"HCL: {variable.hcl}") + print(f"Sensitive: {variable.sensitive}") if hasattr(variable, "version_id"): - print(f" Version ID: {variable.version_id}") + print(f"Version ID: {variable.version_id}") except Exception as e: - print(f" Error reading variable {test_variable_id}: {e}") + print(f"Error reading variable {test_variable_id}: {e}") # Test UPDATE function with specific variable ID - COMMENTED OUT print("\n5. Testing UPDATE operation with specific variable ID:") @@ -177,13 +173,13 @@ def main(): print( f" Updated variable: {updated_variable.key} = {updated_variable.value}" ) - print(f" Description: {updated_variable.description}") - print(f" Category: {updated_variable.category}") - print(f" HCL: {updated_variable.hcl}") - print(f" Sensitive: {updated_variable.sensitive}") - print(f" ID: {updated_variable.id}") + print(f"Description: {updated_variable.description}") + print(f"Category: {updated_variable.category}") + print(f"HCL: {updated_variable.hcl}") + print(f"Sensitive: {updated_variable.sensitive}") + print(f"ID: {updated_variable.id}") except Exception as e: - print(f" Error updating variable {test_variable_id}: {e}") + print(f"Error updating variable {test_variable_id}: {e}") # Test DELETE function with specific variable ID print("\n6. Testing DELETE operation with specific variable ID:") @@ -197,25 +193,25 @@ def main(): # First read the variable to confirm it exists before deletion variable = client.variables.read(workspace_id, test_variable_id) print(f"Variable to delete: {variable.key} = {variable.value}") - print(f" ID: {variable.id}") + print(f"ID: {variable.id}") # Delete the variable client.variables.delete(workspace_id, test_variable_id) - print(f" Successfully deleted variable with ID: {test_variable_id}") + print(f"Successfully deleted variable with ID: {test_variable_id}") # Try to read it again to verify deletion print("Verifying deletion...") try: client.variables.read(workspace_id, test_variable_id) - print(" Warning: Variable still exists after deletion!") + print("Warning: Variable still exists after deletion!") except Exception as read_error: if "not found" in str(read_error).lower() or "404" in str(read_error): - print(" Confirmed: Variable no longer exists") + print("Confirmed: Variable no longer exists") else: - print(f" Unexpected error verifying deletion: {read_error}") + print(f"Unexpected error verifying deletion: {read_error}") except Exception as e: - print(f" Error deleting variable {test_variable_id}: {e}") + print(f"Error deleting variable {test_variable_id}: {e}") # 4. Test READ function print("\n4. Testing READ operation:") @@ -228,14 +224,14 @@ def main(): value_display = ( "***SENSITIVE***" if variable.sensitive else variable.value ) - print(f" Read variable: {variable.key} = {value_display}") - print(f" ID: {variable.id}") - print(f" Description: {variable.description}") - print(f" Category: {variable.category}") - print(f" HCL: {variable.hcl}") - print(f" Sensitive: {variable.sensitive}") + print(f"Read variable: {variable.key} = {value_display}") + print(f"ID: {variable.id}") + print(f"Description: {variable.description}") + print(f"Category: {variable.category}") + print(f"HCL: {variable.hcl}") + print(f"Sensitive: {variable.sensitive}") except Exception as e: - print(f" Error reading variable {test_var_id}: {e}") + print(f"Error reading variable {test_var_id}: {e}") else: print("No variables available to read") @@ -264,10 +260,10 @@ def main(): print( f" Updated variable: {updated_variable.key} = {updated_variable.value}" ) - print(f" New description: {updated_variable.description}") - print(f" ID: {updated_variable.id}") + print(f"New description: {updated_variable.description}") + print(f"ID: {updated_variable.id}") except Exception as e: - print(f" Error updating variable {test_var_id}: {e}") + print(f"Error updating variable {test_var_id}: {e}") else: print("No variables available to update") @@ -279,9 +275,9 @@ def main(): for var_id in created_variables: try: client.variables.delete(workspace_id, var_id) - print(f" Deleted variable with ID: {var_id}") + print(f"Deleted variable with ID: {var_id}") except Exception as e: - print(f" Error deleting variable {var_id}: {e}") + print(f"Error deleting variable {var_id}: {e}") # Verify deletion by listing variables again print("\nVerifying deletion - listing variables after cleanup:") @@ -298,14 +294,14 @@ def main(): f"Warning: {len(remaining_test_vars)} test variables still exist:" ) for var in remaining_test_vars: - print(f" โ€ข {var.key} [ID: {var.id}]") + print(f"โ€ข {var.key} [ID: {var.id}]") else: - print(" All test variables successfully deleted") + print("All test variables successfully deleted") except Exception as e: - print(f" Error verifying deletion: {e}") + print(f"Error verifying deletion: {e}") except Exception as e: - print(f" Unexpected error during testing: {e}") + print(f"Unexpected error during testing: {e}") print("\n" + "=" * 60) print("Variable testing complete!") diff --git a/examples/workspace.py b/examples/workspace.py index dca9b11..4dfb643 100644 --- a/examples/workspace.py +++ b/examples/workspace.py @@ -176,7 +176,7 @@ def main(): if count >= args.page_size * 2: # Safety limit based on page size break - print(f" Found {len(workspace_list)} workspaces") + print(f"Found {len(workspace_list)} workspaces") print() if not workspace_list: @@ -184,12 +184,12 @@ def main(): else: for i, ws in enumerate(workspace_list, 1): print(f"{i:2d}. {ws.name}") - print(f" ID: {ws.id}") - print(f" Execution Mode: {ws.execution_mode}") - print(f" Auto Apply: {ws.auto_apply}") + print(f"ID: {ws.id}") + print(f"Execution Mode: {ws.execution_mode}") + print(f"Auto Apply: {ws.auto_apply}") print() except Exception as e: - print(f" Error listing workspaces: {e}") + print(f"Error listing workspaces: {e}") return # 2) Create a new workspace if requested @@ -216,13 +216,13 @@ def main(): f"Creating workspace '{workspace_name}' in organization '{args.org}'..." ) workspace = client.workspaces.create(args.org, create_options) - print(" Successfully created workspace!") - print(f" Name: {workspace.name}") - print(f" ID: {workspace.id}") - print(f" Description: {workspace.description}") - print(f" Execution Mode: {workspace.execution_mode}") - print(f" Auto Apply: {workspace.auto_apply}") - print(f" Terraform Version: {workspace.terraform_version}") + print("Successfully created workspace!") + print(f"Name: {workspace.name}") + print(f"ID: {workspace.id}") + print(f"Description: {workspace.description}") + print(f"Execution Mode: {workspace.execution_mode}") + print(f"Auto Apply: {workspace.auto_apply}") + print(f"Terraform Version: {workspace.terraform_version}") print() args.workspace = ( @@ -230,7 +230,7 @@ def main(): ) # Use the created workspace for other operations args.workspace_id = workspace.id except Exception as e: - print(f" Error creating workspace: {e}") + print(f"Error creating workspace: {e}") return # 3a) Read workspace details using read_with_options @@ -246,20 +246,20 @@ def main(): workspace = client.workspaces.read_with_options( args.workspace, read_options, organization=args.org ) - print(f" read_with_options: {workspace.name}") - print(f" ID: {workspace.id}") - print(f" Description: {workspace.description}") - print(f" Execution Mode: {workspace.execution_mode}") - print(f" Auto Apply: {workspace.auto_apply}") - print(f" Locked: {workspace.locked}") - print(f" Terraform Version: {workspace.terraform_version}") - print(f" Working Directory: {workspace.working_directory}") + print(f"read_with_options: {workspace.name}") + print(f"ID: {workspace.id}") + print(f"Description: {workspace.description}") + print(f"Execution Mode: {workspace.execution_mode}") + print(f"Auto Apply: {workspace.auto_apply}") + print(f"Locked: {workspace.locked}") + print(f"Terraform Version: {workspace.terraform_version}") + print(f"Working Directory: {workspace.working_directory}") # Set workspace_id for further operations if not args.workspace_id: args.workspace_id = workspace.id except Exception as e: - print(f" read_with_options error: {e}") + print(f"read_with_options error: {e}") # Test basic read method (when testing all read methods) if args.read_all or args.all_tests: @@ -268,11 +268,11 @@ def main(): workspace = client.workspaces.read( args.workspace, organization=args.org ) - print(f" read: {workspace.name} (ID: {workspace.id})") - print(f" Description: {workspace.description}") - print(f" Execution Mode: {workspace.execution_mode}") + print(f"read: {workspace.name} (ID: {workspace.id})") + print(f"Description: {workspace.description}") + print(f"Execution Mode: {workspace.execution_mode}") except Exception as e: - print(f" read error: {e}") + print(f"read error: {e}") # 3b) Read workspace by ID methods (comprehensive testing) if args.workspace_id and (args.read_all or args.all_tests): @@ -283,9 +283,9 @@ def main(): try: print("Testing read_by_id()...") workspace = client.workspaces.read_by_id(args.workspace_id) - print(f" read_by_id: {workspace.name} (ID: {workspace.id})") + print(f"read_by_id: {workspace.name} (ID: {workspace.id})") except Exception as e: - print(f" read_by_id error: {e}") + print(f"read_by_id error: {e}") # Test read_by_id_with_options try: @@ -295,10 +295,10 @@ def main(): args.workspace_id, options ) print( - f" read_by_id_with_options: {workspace.name} with organization included" + f"read_by_id_with_options: {workspace.name} with organization included" ) except Exception as e: - print(f" read_by_id_with_options error: {e}") + print(f"read_by_id_with_options error: {e}") # 4a) Update workspace by name if args.update and args.workspace or args.update_all or args.all_tests: @@ -317,14 +317,14 @@ def main(): updated_workspace = client.workspaces.update( args.workspace, update_options, organization=args.org ) - print(" update: Successfully updated workspace!") - print(f" Name: {updated_workspace.name}") - print(f" Description: {updated_workspace.description}") - print(f" Auto Apply: {updated_workspace.auto_apply}") - print(f" Terraform Version: {updated_workspace.terraform_version}") + print("update: Successfully updated workspace!") + print(f"Name: {updated_workspace.name}") + print(f"Description: {updated_workspace.description}") + print(f"Auto Apply: {updated_workspace.auto_apply}") + print(f"Terraform Version: {updated_workspace.terraform_version}") print() except Exception as e: - print(f" update error: {e}") + print(f"update error: {e}") # 4b) Update workspace by ID if args.workspace_id and (args.update_all or args.all_tests): @@ -340,10 +340,10 @@ def main(): args.workspace_id, update_options ) print( - f" update_by_id: Updated description to '{updated_workspace.description}'" + f"update_by_id: Updated description to '{updated_workspace.description}'" ) except Exception as e: - print(f" update_by_id error: {e}") + print(f"update_by_id error: {e}") # 5) Lock workspace if requested if args.lock and args.workspace_id: @@ -371,11 +371,11 @@ def main(): workspace = client.workspaces.remove_vcs_connection( args.workspace, organization=args.org ) - print(" Successfully removed VCS connection from workspace!") - print(f" Workspace: {workspace.name}") + print("Successfully removed VCS connection from workspace!") + print(f"Workspace: {workspace.name}") print() except Exception as e: - print(f" Error removing VCS connection: {e}") + print(f"Error removing VCS connection: {e}") # 8) Demonstrate tag operations if args.workspace_id: @@ -423,10 +423,10 @@ def main(): try: print("Testing force_unlock()...") workspace = client.workspaces.force_unlock(args.workspace_id) - print(f" force_unlock: Workspace {workspace.name} force unlocked") + print(f"force_unlock: Workspace {workspace.name} force unlocked") except Exception as e: - print(f" force_unlock result: {e}") - print(" (Expected if workspace wasn't locked)") + print(f"force_unlock result: {e}") + print("(Expected if workspace wasn't locked)") # 11) Test SSH key operations if (args.all_tests or args.ssh_keys) and args.workspace_id: @@ -446,15 +446,15 @@ def main(): workspace = client.workspaces.assign_ssh_key( args.workspace_id, ssh_key.id ) - print(f" assign_ssh_key: Assigned key to {workspace.name}") + print(f"assign_ssh_key: Assigned key to {workspace.name}") # Test unassign SSH key print("Testing unassign_ssh_key()...") workspace = client.workspaces.unassign_ssh_key(args.workspace_id) - print(f" unassign_ssh_key: Removed key from {workspace.name}") + print(f"unassign_ssh_key: Removed key from {workspace.name}") except Exception as e: - print(f" SSH key assignment error: {e}") + print(f"SSH key assignment error: {e}") else: print("No SSH keys available for testing") print( @@ -462,7 +462,7 @@ def main(): ) except Exception as e: - print(f" SSH key listing error: {e}") + print(f"SSH key listing error: {e}") # 12) Test advanced tag operations if (args.all_tests or args.tag_ops) and args.workspace_id: @@ -473,17 +473,17 @@ def main(): print("Testing remove_tags()...") remove_options = WorkspaceRemoveTagsOptions(tags=[Tag(name="demo")]) client.workspaces.remove_tags(args.workspace_id, remove_options) - print(" remove_tags: Removed 'demo' tag") + print("remove_tags: Removed 'demo' tag") except Exception as e: - print(f" remove_tags: {e}") + print(f"remove_tags: {e}") try: # Test list_tag_bindings print("Testing list_tag_bindings()...") bindings = list(client.workspaces.list_tag_bindings(args.workspace_id)) - print(f" list_tag_bindings: Found {len(bindings)} tag bindings") + print(f"list_tag_bindings: Found {len(bindings)} tag bindings") except Exception as e: - print(f" list_tag_bindings error: {e}") + print(f"list_tag_bindings error: {e}") try: # Test list_effective_tag_bindings @@ -492,20 +492,20 @@ def main(): client.workspaces.list_effective_tag_bindings(args.workspace_id) ) print( - f" list_effective_tag_bindings: Found {len(effective_bindings)} effective bindings" + f"list_effective_tag_bindings: Found {len(effective_bindings)} effective bindings" ) except Exception as e: - print(f" list_effective_tag_bindings error: {e}") + print(f"list_effective_tag_bindings error: {e}") # 13) Test additional remote state operations if (args.all_tests or args.remote_state) and args.workspace_id: _print_header("Testing additional remote state operations") print("Available remote state methods:") - print(" list_remote_state_consumers() - Already tested above") - print(" add_remote_state_consumers() - Requires consumer workspace IDs") - print(" update_remote_state_consumers() - Requires specific setup") - print(" remove_remote_state_consumers() - Requires existing consumers") + print("list_remote_state_consumers() - Already tested above") + print("add_remote_state_consumers() - Requires consumer workspace IDs") + print("update_remote_state_consumers() - Requires specific setup") + print("remove_remote_state_consumers() - Requires existing consumers") # 14) Test data retention policies if (args.all_tests or args.retention) and args.workspace_id: @@ -514,26 +514,26 @@ def main(): try: print("Testing read_data_retention_policy()...") policy = client.workspaces.read_data_retention_policy(args.workspace_id) - print(f" read_data_retention_policy: {policy}") + print(f"read_data_retention_policy: {policy}") except Exception as e: - print(f" read_data_retention_policy: {e}") - print(" (Expected if no policy is set)") + print(f"read_data_retention_policy: {e}") + print("(Expected if no policy is set)") try: print("Testing read_data_retention_policy_choice()...") choice = client.workspaces.read_data_retention_policy_choice( args.workspace_id ) - print(f" read_data_retention_policy_choice: {choice}") + print(f"read_data_retention_policy_choice: {choice}") except Exception as e: - print(f" read_data_retention_policy_choice: {e}") + print(f"read_data_retention_policy_choice: {e}") print("Available policy setting methods:") - print(" set_data_retention_policy() - Set custom retention policy") - print(" set_data_retention_policy_delete_older() - Delete older runs") - print(" set_data_retention_policy_dont_delete() - Keep all runs") - print(" delete_data_retention_policy() - Remove retention policy") - print(" (Not executed to preserve workspace settings)") + print("set_data_retention_policy() - Set custom retention policy") + print("set_data_retention_policy_delete_older() - Delete older runs") + print("set_data_retention_policy_dont_delete() - Keep all runs") + print("delete_data_retention_policy() - Remove retention policy") + print("(Not executed to preserve workspace settings)") # 15) Test readme functionality if (args.all_tests or args.readme) and args.workspace_id: @@ -543,17 +543,17 @@ def main(): print("Testing readme()...") readme = client.workspaces.readme(args.workspace_id) if readme: - print(f" readme: Found README content ({len(readme)} characters)") + print(f"readme: Found README content ({len(readme)} characters)") print( - f" Preview: {readme[:100]}..." + f"Preview: {readme[:100]}..." if len(readme) > 100 - else f" Content: {readme}" + else f"Content: {readme}" ) else: - print(" readme: No README content found") + print("readme: No README content found") except Exception as e: - print(f" readme result: {e}") - print(" (Expected if workspace has no README)") + print(f"readme result: {e}") + print("(Expected if workspace has no README)") # 16) Delete workspace if requested (should be last operation) if args.delete and args.workspace: From 89a77050ba296ddf139af5a8318637e880ac2175 Mon Sep 17 00:00:00 2001 From: aayushsingh2502 Date: Sun, 4 Jan 2026 12:08:57 +0530 Subject: [PATCH 15/18] contributing doc, test.md and changelog added --- CHANGELOG.md | 142 +++++++++++ docs/CONTRIBUTING.md | 583 ++++++++++++++++++++++++++++++++++++++++++- docs/TESTS.md | 478 ++++++++++++++++++++++++++++++++++- 3 files changed, 1201 insertions(+), 2 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..77af2dd --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,142 @@ +# Unreleased + +# v0.1.1 + +## Features + +### Organization Management +* Added organization membership list functionality with flexible filtering and pagination by @aayushsingh2502 [#54](https://github.com/hashicorp/python-tfe/pull/54) +* Added organization membership read functionality by @aayushsingh2502 [#54](https://github.com/hashicorp/python-tfe/pull/54) +* Added organization membership read with relationship includes by @aayushsingh2502 [#54](https://github.com/hashicorp/python-tfe/pull/54) +* Added organization membership create functionality to invite users via email with optional team assignments by @aayushsingh2502 [#54](https://github.com/hashicorp/python-tfe/pull/54) +* Added organization membership delete functionality by @aayushsingh2502 [#54](https://github.com/hashicorp/python-tfe/pull/54) + +### Workspace Management +* Added workspace resources list functionality with pagination support by @KshitijaChoudhari [#58](https://github.com/hashicorp/python-tfe/pull/58) +* Added robust data models with Pydantic validation for workspace resources by @KshitijaChoudhari [#58](https://github.com/hashicorp/python-tfe/pull/58) +* Added comprehensive filtering options for workspace resources by @KshitijaChoudhari [#58](https://github.com/hashicorp/python-tfe/pull/58) + +### Policy Management +* Added policy set parameter list functionality by @isivaselvan [#53](https://github.com/hashicorp/python-tfe/pull/53) +* Added policy set parameter create functionality by @isivaselvan [#53](https://github.com/hashicorp/python-tfe/pull/53) +* Added policy set parameter read functionality by @isivaselvan [#53](https://github.com/hashicorp/python-tfe/pull/53) +* Added policy set parameter update functionality by @isivaselvan [#53](https://github.com/hashicorp/python-tfe/pull/53) +* Added policy set parameter delete functionality by @isivaselvan [#53](https://github.com/hashicorp/python-tfe/pull/53) + +## Enhancements +* Code cleanup and improvements across example files by @aayushsingh2502 [#54](https://github.com/hashicorp/python-tfe/pull/54) + +# v0.1.0 + +## Features + +### Core Infrastructure & Foundation +* Established base client architecture, HTTP transport layer, pagination and response handling with retries by @iam404 [#9](https://github.com/hashicorp/python-tfe/pull/9) +* Implemented configuration management and authentication patterns by @iam404 [#9](https://github.com/hashicorp/python-tfe/pull/9) +* Added comprehensive error handling and logging infrastructure by @iam404 [#9](https://github.com/hashicorp/python-tfe/pull/9) + +### Organization Management +* Added full CRUD operations for organizations by @aayushsingh2502 +* Added organization membership and user management by @aayushsingh2502 +* Added organization settings and feature toggles by @aayushsingh2502 + +### Workspace Management +* Added comprehensive workspace lifecycle management by @isivaselvan [#16](https://github.com/hashicorp/python-tfe/pull/16) +* Added VCS integration support for GitHub, GitLab, Bitbucket, Azure DevOps by @isivaselvan [#16](https://github.com/hashicorp/python-tfe/pull/16) +* Added workspace settings, tags, and remote state consumers by @isivaselvan [#16](https://github.com/hashicorp/python-tfe/pull/16) +* Added workspace variable management functionality by @aayushsingh2502 [#16](https://github.com/hashicorp/python-tfe/pull/16) +* Added variable sets integration by @aayushsingh2502 [#16](https://github.com/hashicorp/python-tfe/pull/16) +* Added sensitive variable handling with encryption by @aayushsingh2502 [#16](https://github.com/hashicorp/python-tfe/pull/16) + +### Project Management +* Added project creation, configuration, and management by @KshitijaChoudhari [#23](https://github.com/hashicorp/python-tfe/pull/23) +* Added project tagging and organization by @KshitijaChoudhari [#25](https://github.com/hashicorp/python-tfe/pull/25) +* Added tag binding functionality for improved project organization by @KshitijaChoudhari [#25](https://github.com/hashicorp/python-tfe/pull/25) + +### State Management +* Added state version listing, downloading, and rollback capabilities by @iam404 [#22](https://github.com/hashicorp/python-tfe/pull/22) +* Added state output retrieval and management by @iam404 [#22](https://github.com/hashicorp/python-tfe/pull/22) +* Added secure state file operations with locking mechanisms by @iam404 [#22](https://github.com/hashicorp/python-tfe/pull/22) + +### Variable Sets +* Added variable set creation and management by @KshitijaChoudhari [#27](https://github.com/hashicorp/python-tfe/pull/27) +* Added workspace association and inheritance by @KshitijaChoudhari [#27](https://github.com/hashicorp/python-tfe/pull/27) +* Added global and workspace-specific variable sets by @KshitijaChoudhari [#27](https://github.com/hashicorp/python-tfe/pull/27) + +### Registry Management +* Added private module registry implementation by @aayushsingh2502 [#24](https://github.com/hashicorp/python-tfe/pull/24) +* Added module publishing and version management by @aayushsingh2502 [#24](https://github.com/hashicorp/python-tfe/pull/24) +* Added VCS integration for automated module updates by @aayushsingh2502 [#24](https://github.com/hashicorp/python-tfe/pull/24) +* Added dependency management and semantic versioning by @aayushsingh2502 [#24](https://github.com/hashicorp/python-tfe/pull/24) +* Added custom and community provider management by @aayushsingh2502 [#28](https://github.com/hashicorp/python-tfe/pull/28) +* Added provider version publishing and distribution by @aayushsingh2502 [#28](https://github.com/hashicorp/python-tfe/pull/28) +* Added GPG signature verification support by @aayushsingh2502 [#28](https://github.com/hashicorp/python-tfe/pull/28) + +### Run Management +* Added run creation, execution, and monitoring by @isivaselvan [#30](https://github.com/hashicorp/python-tfe/pull/30) +* Added run status tracking with real-time updates by @isivaselvan [#30](https://github.com/hashicorp/python-tfe/pull/30) +* Added run cancellation and force-cancellation capabilities by @isivaselvan [#30](https://github.com/hashicorp/python-tfe/pull/30) +* Added detailed plan analysis and review by @isivaselvan [#33](https://github.com/hashicorp/python-tfe/pull/33) +* Added apply operations with confirmation workflows by @isivaselvan [#33](https://github.com/hashicorp/python-tfe/pull/33) +* Added plan output parsing and visualization by @isivaselvan [#33](https://github.com/hashicorp/python-tfe/pull/33) +* Added run task creation and execution by @isivaselvan [#26](https://github.com/hashicorp/python-tfe/pull/26) +* Added trigger-based automated runs by @isivaselvan [#26](https://github.com/hashicorp/python-tfe/pull/26) +* Added webhook integration for external triggers by @isivaselvan [#26](https://github.com/hashicorp/python-tfe/pull/26) +* Added comprehensive run event logging by @isivaselvan [#36](https://github.com/hashicorp/python-tfe/pull/36) +* Added event filtering and querying capabilities by @isivaselvan [#36](https://github.com/hashicorp/python-tfe/pull/36) +* Added real-time event streaming support by @isivaselvan [#36](https://github.com/hashicorp/python-tfe/pull/36) + +### Configuration Management +* Added configuration version creation and upload by @aayushsingh2502 [#32](https://github.com/hashicorp/python-tfe/pull/32) +* Added tar.gz archive support for configuration bundles by @aayushsingh2502 [#32](https://github.com/hashicorp/python-tfe/pull/32) +* Added VCS-triggered configuration updates by @aayushsingh2502 [#32](https://github.com/hashicorp/python-tfe/pull/32) + +### Query and Search +* Added complex run filtering and search by @KshitijaChoudhari [#35](https://github.com/hashicorp/python-tfe/pull/35) +* Added historical run data analysis by @KshitijaChoudhari [#35](https://github.com/hashicorp/python-tfe/pull/35) +* Added performance metrics and statistics by @KshitijaChoudhari [#35](https://github.com/hashicorp/python-tfe/pull/35) + +### Agent Management +* Added agent pool creation and configuration by @KshitijaChoudhari [#31](https://github.com/hashicorp/python-tfe/pull/31) +* Added agent registration and lifecycle management by @KshitijaChoudhari [#31](https://github.com/hashicorp/python-tfe/pull/31) +* Added health monitoring and capacity management by @KshitijaChoudhari [#31](https://github.com/hashicorp/python-tfe/pull/31) + +### Authentication & Security +* Added OAuth client creation and configuration by @aayushsingh2502 [#37](https://github.com/hashicorp/python-tfe/pull/37) +* Added VCS provider authentication setup by @aayushsingh2502 [#37](https://github.com/hashicorp/python-tfe/pull/37) +* Added OAuth token refresh and management by @aayushsingh2502 [#37](https://github.com/hashicorp/python-tfe/pull/37) +* Added OAuth token creation and renewal by @aayushsingh2502 [#40](https://github.com/hashicorp/python-tfe/pull/40) +* Added secure token storage and retrieval by @aayushsingh2502 [#40](https://github.com/hashicorp/python-tfe/pull/40) +* Added token scope and permission management by @aayushsingh2502 [#40](https://github.com/hashicorp/python-tfe/pull/40) +* Added SSH key upload and management by @KshitijaChoudhari [#38](https://github.com/hashicorp/python-tfe/pull/38) +* Added key validation and security checks by @KshitijaChoudhari [#38](https://github.com/hashicorp/python-tfe/pull/38) +* Added repository access configuration by @KshitijaChoudhari [#38](https://github.com/hashicorp/python-tfe/pull/38) + +### Tagging & Organization +* Added reserved tag key creation and enforcement by @KshitijaChoudhari [#39](https://github.com/hashicorp/python-tfe/pull/39) +* Added tag validation and naming conventions by @KshitijaChoudhari [#39](https://github.com/hashicorp/python-tfe/pull/39) +* Added organizational tag policies by @KshitijaChoudhari [#39](https://github.com/hashicorp/python-tfe/pull/39) + +### Policy Management +* Added Sentinel policy creation and enforcement by @isivaselvan [#41](https://github.com/hashicorp/python-tfe/pull/41) +* Added policy version management by @isivaselvan [#41](https://github.com/hashicorp/python-tfe/pull/41) +* Added policy evaluation and reporting by @isivaselvan [#41](https://github.com/hashicorp/python-tfe/pull/41) +* Added policy check execution and results by @isivaselvan [#42](https://github.com/hashicorp/python-tfe/pull/42) +* Added override capabilities for policy failures by @isivaselvan [#42](https://github.com/hashicorp/python-tfe/pull/42) +* Added detailed policy violation reporting by @isivaselvan [#42](https://github.com/hashicorp/python-tfe/pull/42) +* Added policy set creation and configuration by @isivaselvan [#45](https://github.com/hashicorp/python-tfe/pull/45) +* Added workspace and organization policy assignment by @isivaselvan [#45](https://github.com/hashicorp/python-tfe/pull/45) +* Added policy set versioning and rollback by @isivaselvan [#45](https://github.com/hashicorp/python-tfe/pull/45) +* Added policy set version management by @isivaselvan [#46](https://github.com/hashicorp/python-tfe/pull/46) +* Added policy set outcome tracking by @isivaselvan [#46](https://github.com/hashicorp/python-tfe/pull/46) +* Added comprehensive evaluation reporting by @isivaselvan [#46](https://github.com/hashicorp/python-tfe/pull/46) + +### Notification Management +* Added notification configuration and management by @KshitijaChoudhari [#43](https://github.com/hashicorp/python-tfe/pull/43) +* Added multi-channel notification support for Slack, email, and webhooks by @KshitijaChoudhari [#43](https://github.com/hashicorp/python-tfe/pull/43) +* Added event-driven notification triggers by @KshitijaChoudhari [#43](https://github.com/hashicorp/python-tfe/pull/43) +* Added custom notification templates and formatting by @KshitijaChoudhari [#43](https://github.com/hashicorp/python-tfe/pull/43) + +## Notes +* Requires Python 3.10 or higher +* Compatible with HCP Terraform and Terraform Enterprise v2 and later diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 36ac46a..da977f1 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -1 +1,582 @@ -# Contributing to pytfe \ No newline at end of file +# Contributing to python-tfe + +If you find an issue with this package, please create an issue in GitHub. If you'd like, we welcome any contributions. Fork this repository and submit a pull request. + +## Adding New Functionality or Fixing Bugs + +If you are adding a new endpoint, make sure to update the API coverage list where we keep track of the HCP Terraform APIs that this SDK supports. + +If you are making relevant changes worth communicating to our users, please include a note about it in our `CHANGELOG.md`. You can include it as part of the PR where you are submitting your changes. + +`CHANGELOG.md` should have the next minor version listed as `# v0.X.0 (Unreleased)` and any changes can go under there. But if you feel that your changes are better suited for a patch version (like a critical bug fix), you may list a new section for this version. You should repeat the same formatting style introduced by previous versions. + +### Scoping Pull Requests That Add New Resources + +There are instances where several new resources being added (i.e., Workspace Run Tasks and Organization Run Tasks) are coalesced into one PR. In order to keep the review process as efficient and least error-prone as possible, we ask that you please scope each PR to an individual resource even if the multiple resources you're adding share similarities. If joining multiple related PRs into one single PR makes more sense logistically, we'd ask that you organize your commit history by resource. A general convention for this repository is one commit for the implementation of the resource's methods, one for tests, and one for cleanup and housekeeping (e.g., modifying the changelog/docs, updating examples, etc.). + +**Note HashiCorp Employees Only:** When submitting a new set of endpoints please ensure that one of your respective team members approves the changes as well before merging. + +## Linting + +After opening a PR, our CI system will perform a series of code checks, one of which is linting. Linting is not strictly required for a change to be merged, but it helps smooth the review process and catch common mistakes early. If you'd like to run the linters manually, follow these steps: + +1. Install development dependencies: `make dev-install` +2. Format your code: `make fmt` +3. Run lint checks: `make lint` + +We use [ruff](https://docs.astral.sh/ruff/) for both formatting and linting, and [mypy](https://mypy.readthedocs.io/) for type checking. + +## Writing Tests + +The test suite contains unit tests with mocked API responses. You can read more about running the tests in [TESTS.md](TESTS.md). Our CI system (GitHub Actions) will not test your fork until a one-time approval takes place. + +To run tests: +```bash +make test +``` + +## Adding New Endpoints + +### Guidelines for Adding New Endpoints + +* A resource class should cover one RESTful resource, which sometimes involves two or more endpoints. +* Each resource class must be registered in the `TFEClient` class in `client.py`. +* You'll need to add unit tests that cover each method of the resource class with mocked responses. +* Option classes serve as a proxy for either passing query params or request bodies: + - `ListOptions` and `ReadOptions` are values passed as query parameters. + - `CreateOptions` and `UpdateOptions` represent the request body. +* URL parameters should be defined as method parameters. +* Any resource-specific errors must be defined in `errors.py`. + +Here is a comprehensive example of what a resource looks like when implemented: + +#### 1. Create the Model (`src/pytfe/models/example.py`) + +```python +"""Models for example resources.""" + +from __future__ import annotations + +from datetime import datetime +from enum import Enum + +from pydantic import BaseModel, ConfigDict, Field + + +class ExampleStatus(str, Enum): + """Status of an example.""" + + PENDING = "pending" + ACTIVE = "active" + COMPLETED = "completed" + + +class Example(BaseModel): + """Represents an example resource.""" + + model_config = ConfigDict(populate_by_name=True) + + id: str = Field(..., description="The unique identifier") + name: str | None = Field(None, description="The name of the example") + status: ExampleStatus | None = Field(None, description="The current status") + url: str | None = Field(None, description="The URL") + optional_value: str | None = Field( + None, alias="optional-value", description="An optional value" + ) + created_at: datetime | None = Field( + None, alias="created-at", description="When this was created" + ) + + # Relationships + organization_name: str | None = Field( + None, description="The organization this belongs to" + ) + + +class ExampleList(BaseModel): + """Represents a paginated list of examples.""" + + model_config = ConfigDict(populate_by_name=True) + + items: list[Example] = Field(default_factory=list, description="List of examples") + current_page: int | None = Field(None, description="Current page number") + total_pages: int | None = Field(None, description="Total number of pages") + prev_page: int | str | None = Field(None, description="Previous page number") + next_page: int | str | None = Field(None, description="Next page number") + total_count: int | None = Field(None, description="Total number of items") + + +class ExampleListOptions(BaseModel): + """Options for listing examples.""" + + model_config = ConfigDict(populate_by_name=True) + + page_number: int | None = Field( + None, alias="page[number]", description="Page number", ge=1 + ) + page_size: int | None = Field( + None, alias="page[size]", description="Items per page", ge=1, le=100 + ) + + +class ExampleCreateOptions(BaseModel): + """Options for creating an example.""" + + model_config = ConfigDict(populate_by_name=True) + + name: str = Field(..., description="The name of the example") + url: str = Field(..., description="The URL") + optional_value: str | None = Field( + None, alias="optional-value", description="An optional value" + ) + + +class ExampleUpdateOptions(BaseModel): + """Options for updating an example.""" + + model_config = ConfigDict(populate_by_name=True) + + name: str | None = Field(None, description="The name") + url: str | None = Field(None, description="The URL") + optional_value: str | None = Field( + None, alias="optional-value", description="An optional value" + ) +``` + +#### 2. Create the Resource Class (`src/pytfe/resources/example.py`) + +```python +"""Example API resource.""" + +from __future__ import annotations + +from collections.abc import Iterator +from typing import Any + +from ..errors import InvalidExampleIDError, InvalidOrgError +from ..models.example import ( + Example, + ExampleCreateOptions, + ExampleListOptions, + ExampleUpdateOptions, +) +from ..utils import valid_string_id +from ._base import _Service + + +class Examples(_Service): + """Example API for Terraform Enterprise.""" + + def list( + self, organization: str, options: ExampleListOptions | None = None + ) -> Iterator[Example]: + """Iterate through all examples in an organization. + + This method automatically handles pagination. + + Args: + organization: The name of the organization + options: Optional list options (page_size, page_number) + + Yields: + Example objects one at a time + """ + if not valid_string_id(organization): + raise InvalidOrgError() + + params: dict[str, Any] = {} + if options: + params = options.model_dump(by_alias=True, exclude_none=True) + + path = f"/api/v2/organizations/{organization}/examples" + for item in self._list(path, params=params): + attrs = item.get("attributes", {}) + attrs["id"] = item.get("id") + + # Extract relationships if needed + relationships = item.get("relationships", {}) + org_rel = relationships.get("organization", {}) + org_data = org_rel.get("data", {}) + if org_data and isinstance(org_data, dict): + attrs["organization_name"] = org_data.get("id") + + yield Example.model_validate(attrs) + + def create( + self, organization: str, options: ExampleCreateOptions + ) -> Example: + """Create a new example. + + Args: + organization: The name of the organization + options: Options for creating the example + + Returns: + The created Example object + """ + if not valid_string_id(organization): + raise InvalidOrgError() + + path = f"/api/v2/organizations/{organization}/examples" + body = { + "data": { + "type": "examples", + "attributes": options.model_dump(by_alias=True, exclude_none=True), + } + } + + response = self.t.request("POST", path, json_body=body) + data = response.json()["data"] + attrs = data.get("attributes", {}) + attrs["id"] = data.get("id") + return Example.model_validate(attrs) + + def read(self, example_id: str) -> Example: + """Read an example by ID. + + Args: + example_id: The ID of the example + + Returns: + The Example object + """ + if not valid_string_id(example_id): + raise InvalidExampleIDError() + + path = f"/api/v2/examples/{example_id}" + response = self.t.request("GET", path) + data = response.json()["data"] + attrs = data.get("attributes", {}) + attrs["id"] = data.get("id") + return Example.model_validate(attrs) + + def update( + self, example_id: str, options: ExampleUpdateOptions + ) -> Example: + """Update an example. + + Args: + example_id: The ID of the example + options: Options for updating the example + + Returns: + The updated Example object + """ + if not valid_string_id(example_id): + raise InvalidExampleIDError() + + path = f"/api/v2/examples/{example_id}" + body = { + "data": { + "type": "examples", + "id": example_id, + "attributes": options.model_dump(by_alias=True, exclude_none=True), + } + } + + response = self.t.request("PATCH", path, json_body=body) + data = response.json()["data"] + attrs = data.get("attributes", {}) + attrs["id"] = data.get("id") + return Example.model_validate(attrs) + + def delete(self, example_id: str) -> None: + """Delete an example. + + Args: + example_id: The ID of the example + + Returns: + None (204 No Content on success) + """ + if not valid_string_id(example_id): + raise InvalidExampleIDError() + + path = f"/api/v2/examples/{example_id}" + self.t.request("DELETE", path) +``` + +#### 3. Add Custom Errors (`src/pytfe/errors.py`) + +```python +class InvalidExampleIDError(InvalidValues): + """Raised when an invalid example ID is provided.""" + + def __init__(self, message: str = "invalid value for example ID") -> None: + super().__init__(message) +``` + +#### 4. Register in Client (`src/pytfe/client.py`) + +```python +from .resources.example import Examples + +class TFEClient: + def __init__(self, config: TFEConfig | None = None): + # ... existing code ... + self.examples = Examples(self._transport) +``` + +#### 5. Export Models (`src/pytfe/models/__init__.py`) + +```python +from .example import ( + Example, + ExampleCreateOptions, + ExampleList, + ExampleListOptions, + ExampleStatus, + ExampleUpdateOptions, +) + +__all__ = [ + # ... existing exports ... + "Example", + "ExampleCreateOptions", + "ExampleList", + "ExampleListOptions", + "ExampleStatus", + "ExampleUpdateOptions", +] +``` + +#### 6. Create Tests (`tests/units/test_example.py`) + +```python +from unittest.mock import MagicMock, Mock + +import pytest + +from pytfe import TFEClient, TFEConfig +from pytfe.errors import InvalidExampleIDError, InvalidOrgError +from pytfe.models.example import ( + Example, + ExampleCreateOptions, + ExampleListOptions, + ExampleStatus, + ExampleUpdateOptions, +) + + +class TestExampleModels: + """Test example models and validation.""" + + def test_example_model_basic(self): + """Test basic Example model creation.""" + example = Example( + id="ex-123", + name="test-example", + status=ExampleStatus.ACTIVE, + ) + assert example.id == "ex-123" + assert example.name == "test-example" + assert example.status == ExampleStatus.ACTIVE + + +class TestExampleOperations: + """Test example operations.""" + + @pytest.fixture + def client(self): + """Create a test client.""" + config = TFEConfig(address="https://test.terraform.io", token="test-token") + return TFEClient(config) + + @pytest.fixture + def mock_list_response(self): + """Create a mock list response.""" + mock = Mock() + mock.json.return_value = { + "data": [ + { + "id": "ex-123", + "type": "examples", + "attributes": { + "name": "example1", + "status": "active", + "url": "https://example.com", + }, + } + ], + "meta": { + "pagination": { + "current-page": 1, + "total-pages": 1, + "prev-page": None, + "next-page": None, + "total-count": 1, + } + }, + } + return mock + + def test_list_examples(self, client, mock_list_response): + """Test listing examples.""" + client._transport.request = MagicMock(return_value=mock_list_response) + + examples = list(client.examples.list("test-org")) + + assert len(examples) == 1 + assert examples[0].id == "ex-123" + assert examples[0].name == "example1" + + client._transport.request.assert_called_once_with( + "GET", + "/api/v2/organizations/test-org/examples", + params={"page[number]": 1, "page[size]": 100}, + ) + + def test_list_examples_invalid_org(self, client): + """Test listing examples with invalid organization.""" + with pytest.raises(InvalidOrgError): + list(client.examples.list("")) + + def test_create_example(self, client): + """Test creating an example.""" + mock_response = Mock() + mock_response.json.return_value = { + "data": { + "id": "ex-new", + "type": "examples", + "attributes": { + "name": "new-example", + "url": "https://new.example.com", + }, + } + } + client._transport.request = MagicMock(return_value=mock_response) + + options = ExampleCreateOptions( + name="new-example", url="https://new.example.com" + ) + example = client.examples.create("test-org", options) + + assert example.id == "ex-new" + assert example.name == "new-example" + + def test_read_example_invalid_id(self, client): + """Test reading example with invalid ID.""" + with pytest.raises(InvalidExampleIDError): + client.examples.read("") +``` + +#### 7. Create Example File (`examples/example.py`) + +```python +#!/usr/bin/env python3 +""" +Example Resource Management + +This example demonstrates all available example operations in the Python TFE SDK. +""" + +import os + +from pytfe import TFEClient, TFEConfig +from pytfe.models import ExampleCreateOptions, ExampleListOptions + + +def main(): + """Main function to demonstrate example operations.""" + print("\n" + "=" * 70) + print("Example Resource Management") + print("=" * 70) + + # Initialize client + token = os.getenv("TFE_TOKEN") + if not token: + print("\nError: TFE_TOKEN environment variable not set") + return + + address = os.getenv("TFE_ADDRESS", "https://app.terraform.io") + config = TFEConfig(address=address, token=token) + client = TFEClient(config) + + organization_name = os.getenv("TFE_ORGANIZATION", "your-org-name") + print(f"\nOrganization: {organization_name}") + print(f"API Address: {address}") + print("-" * 70) + + # List examples + print("\n1. Listing Examples:") + try: + examples = list(client.examples.list(organization_name)) + print(f" Found {len(examples)} examples") + for example in examples[:5]: + print(f" - {example.name} (ID: {example.id})") + except Exception as e: + print(f" Error: {e}") + + print("\n" + "=" * 70) + print("Example Resource Management Complete") + print("=" * 70 + "\n") + + +if __name__ == "__main__": + main() +``` + +### Key Conventions + +1. **Models**: Use Pydantic with `Field` for validation and JSON:API alias mapping +2. **Resources**: Inherit from `_Service`, use `self.t.request()` for HTTP calls +3. **Validation**: Use `valid_string_id()` utility and raise appropriate errors +4. **Iterator Pattern**: For list operations, use `self._list()` for auto-pagination +5. **JSON:API Format**: Request/response bodies use `{"data": {"type": "...", "attributes": {...}}}` +6. **Tests**: Mock `client._transport.request`, test all methods and error conditions +7. **Documentation**: Add docstrings with Args/Returns/Yields sections + +## Adding API Changes That Are Not Generally Available + +In general, beta features should not be merged/released until generally available (GA). However, the maintainers recognize almost any reason to release beta features on a case-by-case basis. These could include: partial customer availability, software dependency, or any reason short of feature completeness. + +Beta features, if released, should be clearly documented: + +```python +class Example(BaseModel): + """Represents an example resource.""" + + # Note: This field is still in BETA and subject to change. + example_new_field: bool | None = Field( + None, alias="example-new-field", description="Beta feature" + ) +``` + +When adding test cases, you can temporarily skip beta features to omit them from running in CI: + +```python +@pytest.mark.skip(reason="Beta feature - skip until GA") +def test_beta_feature(self, client): + """Test beta feature.""" + # test logic here +``` + +**Note**: After your PR has been merged, and the feature either reaches general availability, you should remove the skip decorator. + +## Code Style + +- Follow [PEP 8](https://peps.python.org/pep-0008/) style guidelines +- Use type hints throughout (enforced by mypy) +- Use descriptive variable names +- Keep functions focused and single-purpose +- Add docstrings to all public classes and methods +- Use f-strings for string formatting +- Prefer list comprehensions over map/filter when readable + +## Pull Request Checklist + +Before submitting a PR, ensure: + +- [ ] Code is formatted (`make fmt`) +- [ ] Linting passes (`make lint`) +- [ ] Type checking passes (`make type-check`) +- [ ] All tests pass (`make test`) +- [ ] New functionality has unit tests +- [ ] CHANGELOG.md is updated +- [ ] API coverage list is updated (if adding endpoints) +- [ ] Example file is added/updated (if adding resource) +- [ ] Docstrings are added to new classes/methods + +## Questions? + +Feel free to open an issue for questions about contributing, or reach out to the maintainers for guidance on larger changes. diff --git a/docs/TESTS.md b/docs/TESTS.md index a95f37f..9ddc2af 100644 --- a/docs/TESTS.md +++ b/docs/TESTS.md @@ -1 +1,477 @@ -# Running tests \ No newline at end of file +# Running Tests + +python-tfe includes a comprehensive test suite with unit tests that use mocked API responses. The tests are designed to run quickly without requiring a live HCP Terraform or Terraform Enterprise instance. + +## Quick Start + +```bash +# Install dependencies +make dev-install + +# Run all tests +make test + +# Run with verbose output +python -m pytest -v + +# Run specific test file +python -m pytest tests/units/test_workspaces.py -v + +# Run specific test class or function +python -m pytest tests/units/test_workspaces.py::TestWorkspaceOperations::test_create_workspace_basic -v +``` + +## Test Structure + +Tests are organized in the `tests/units/` directory, with one test file per resource: + +``` +tests/ +โ”œโ”€โ”€ units/ +โ”‚ โ”œโ”€โ”€ test_workspaces.py # Workspace tests +โ”‚ โ”œโ”€โ”€ test_runs.py # Run tests +โ”‚ โ”œโ”€โ”€ test_variables.py # Variable tests +โ”‚ โ”œโ”€โ”€ test_organization_tags.py # Organization tags tests +โ”‚ โ””โ”€โ”€ ... +``` + +Each test file typically contains: +- **Model tests**: Validate Pydantic models and enums +- **Operation tests**: Test CRUD operations with mocked responses +- **Error handling tests**: Validate error conditions +- **Integration tests**: Test complete workflows + +## Test Organization + +Tests follow a consistent structure using pytest classes: + +```python +class TestResourceModels: + """Test model validation and creation.""" + + def test_model_basic(self): + """Test basic model creation.""" + # Test model instantiation and validation + +class TestResourceOperations: + """Test resource operations.""" + + @pytest.fixture + def client(self): + """Create a test client.""" + config = TFEConfig(address="https://test.terraform.io", token="test-token") + return TFEClient(config) + + @pytest.fixture + def mock_response(self): + """Create mock API response.""" + # Return mock response structure + + def test_list_resources(self, client, mock_response): + """Test listing resources.""" + client._transport.request = MagicMock(return_value=mock_response) + # Test the operation + +class TestResourceErrorHandling: + """Test error conditions.""" + + def test_invalid_id_error(self, client): + """Test error handling for invalid IDs.""" + with pytest.raises(InvalidResourceIDError): + client.resources.read("") +``` + +## Writing Tests + +### 1. Create Mock Responses + +Mock API responses follow the JSON:API format: + +```python +@pytest.fixture +def mock_list_response(self): + """Create a mock list response.""" + mock = Mock() + mock.json.return_value = { + "data": [ + { + "id": "ws-123", + "type": "workspaces", + "attributes": { + "name": "my-workspace", + "created-at": "2023-01-01T00:00:00Z", + }, + } + ], + "meta": { + "pagination": { + "current-page": 1, + "total-pages": 1, + "prev-page": None, + "next-page": None, + "total-count": 1, + } + }, + } + return mock +``` + +### 2. Mock the Transport Layer + +Use `MagicMock` to mock the HTTP transport: + +```python +def test_create_workspace(self, client): + """Test creating a workspace.""" + mock_response = Mock() + mock_response.json.return_value = { + "data": { + "id": "ws-new", + "type": "workspaces", + "attributes": {"name": "new-workspace"}, + } + } + + # Mock the transport request method + client._transport.request = MagicMock(return_value=mock_response) + + # Execute the operation + options = WorkspaceCreateOptions(name="new-workspace", organization="test-org") + workspace = client.workspaces.create(options) + + # Assertions + assert workspace.id == "ws-new" + assert workspace.name == "new-workspace" + + # Verify the request was made correctly + client._transport.request.assert_called_once() + call_args = client._transport.request.call_args + assert call_args[0][0] == "POST" # HTTP method + assert "/workspaces" in call_args[0][1] # URL path +``` + +### 3. Test Error Conditions + +Always test validation and error handling: + +```python +def test_create_workspace_invalid_org(self, client): + """Test creating workspace with invalid organization.""" + with pytest.raises(InvalidOrgError): + options = WorkspaceCreateOptions(name="test", organization="") + client.workspaces.create(options) + +def test_read_workspace_invalid_id(self, client): + """Test reading workspace with invalid ID.""" + with pytest.raises(InvalidWorkspaceIDError): + client.workspaces.read(workspace_id="") +``` + +### 4. Test Pagination + +For list operations that use the iterator pattern: + +```python +def test_list_with_pagination(self, client): + """Test listing with pagination.""" + # Setup two pages of responses + page1 = Mock() + page1.json.return_value = { + "data": [{"id": "ws-1", "type": "workspaces", "attributes": {"name": "ws1"}}], + "meta": {"pagination": {"current-page": 1, "total-pages": 2}}, + } + + page2 = Mock() + page2.json.return_value = { + "data": [{"id": "ws-2", "type": "workspaces", "attributes": {"name": "ws2"}}], + "meta": {"pagination": {"current-page": 2, "total-pages": 2}}, + } + + client._transport.request = MagicMock(side_effect=[page1, page2]) + + # List returns an iterator, so convert to list + workspaces = list(client.workspaces.list("test-org")) + + # Should have called request twice (once per page) + assert len(workspaces) == 2 + assert client._transport.request.call_count == 2 +``` + +## Running Tests + +### Run All Tests + +```bash +# Using Makefile +make test + +# Using pytest directly +python -m pytest + +# With verbose output +python -m pytest -v + +# With coverage +python -m pytest --cov=src/pytfe --cov-report=html +``` + +### Run Specific Tests + +```bash +# Run specific file +python -m pytest tests/units/test_workspaces.py + +# Run specific class +python -m pytest tests/units/test_workspaces.py::TestWorkspaceOperations + +# Run specific test +python -m pytest tests/units/test_workspaces.py::TestWorkspaceOperations::test_create_workspace_basic + +# Run tests matching pattern +python -m pytest -k "workspace" -v + +# Run tests matching multiple patterns +python -m pytest -k "create or update" -v +``` + +### Run Tests with Options + +```bash +# Stop on first failure +python -m pytest -x + +# Show local variables in tracebacks +python -m pytest -l + +# Run last failed tests +python -m pytest --lf + +# Run failed tests first, then others +python -m pytest --ff + +# Show test durations +python -m pytest --durations=10 + +# Parallel execution (requires pytest-xdist) +python -m pytest -n auto +``` + +## Test Coverage + +Check test coverage to ensure new code is tested: + +```bash +# Run tests with coverage +python -m pytest --cov=src/pytfe --cov-report=term-missing + +# Generate HTML coverage report +python -m pytest --cov=src/pytfe --cov-report=html + +# Open the HTML report +open htmlcov/index.html +``` + +## Debugging Tests + +### Using Print Statements + +```python +def test_something(self, client): + """Test something.""" + # Use -s flag to see print output + print("Debug info:", some_variable) + assert some_variable == expected +``` + +Run with: `python -m pytest -s tests/units/test_file.py` + +### Using pdb Debugger + +```python +def test_something(self, client): + """Test something.""" + import pdb; pdb.set_trace() # Debugger will stop here + result = client.some_operation() + assert result == expected +``` + +### Using pytest's Built-in Debugger + +```bash +# Drop into debugger on failure +python -m pytest --pdb + +# Drop into debugger at start of each test +python -m pytest --trace +``` + +## Continuous Integration + +Tests run automatically on GitHub Actions for: +- Every push to main branches +- Every pull request +- Scheduled daily runs + +The CI pipeline: +1. Sets up Python 3.11+ environment +2. Installs dependencies +3. Runs linting (ruff, mypy) +4. Runs full test suite +5. Reports coverage + +## Test Best Practices + +### DO: +- Mock all HTTP requests - tests should not hit real APIs +- Test both success and error conditions +- Use descriptive test names that explain what is being tested +- Keep tests independent - each test should be able to run alone +- Use fixtures for common setup code +- Test edge cases and boundary conditions +- Verify request parameters (method, URL, body) in assertions +- Follow the existing test patterns in the codebase + +### DON'T: +- Don't make real API calls in tests +- Don't depend on test execution order +- Don't share state between tests +- Don't use sleep() or time delays +- Don't test implementation details, test behavior +- Don't write overly complex tests - keep them simple and readable + +## Testing Checklist for New Features + +When adding a new resource or endpoint, ensure you have: + +- [ ] Model tests validating all fields and enums +- [ ] Tests for each CRUD operation (Create, Read, Update, Delete, List) +- [ ] Tests for optional parameters and filtering +- [ ] Tests for pagination (if list operation) +- [ ] Tests for all error conditions (invalid IDs, missing required fields, etc.) +- [ ] Tests verifying correct HTTP methods and URL paths +- [ ] Tests verifying request body structure (for POST/PATCH) +- [ ] Tests verifying query parameters (for GET) +- [ ] All tests passing (`make test`) +- [ ] Code coverage above 80% for new code + +## Common Testing Patterns + +### Testing Create Operations + +```python +def test_create_resource(self, client): + """Test creating a resource.""" + mock_response = Mock() + mock_response.json.return_value = { + "data": { + "id": "res-123", + "type": "resources", + "attributes": {"name": "test-resource"}, + } + } + client._transport.request = MagicMock(return_value=mock_response) + + options = ResourceCreateOptions(name="test-resource") + resource = client.resources.create("org-name", options) + + assert resource.id == "res-123" + + # Verify the request + call_args = client._transport.request.call_args + assert call_args[0][0] == "POST" + assert call_args[1]["json_body"]["data"]["type"] == "resources" +``` + +### Testing List Operations + +```python +def test_list_resources(self, client, mock_list_response): + """Test listing resources.""" + client._transport.request = MagicMock(return_value=mock_list_response) + + resources = list(client.resources.list("org-name")) + + assert len(resources) > 0 + assert all(isinstance(r, Resource) for r in resources) +``` + +### Testing Update Operations + +```python +def test_update_resource(self, client): + """Test updating a resource.""" + mock_response = Mock() + mock_response.json.return_value = { + "data": { + "id": "res-123", + "type": "resources", + "attributes": {"name": "updated-name"}, + } + } + client._transport.request = MagicMock(return_value=mock_response) + + options = ResourceUpdateOptions(name="updated-name") + resource = client.resources.update("res-123", options) + + assert resource.name == "updated-name" + + call_args = client._transport.request.call_args + assert call_args[0][0] == "PATCH" +``` + +### Testing Delete Operations + +```python +def test_delete_resource(self, client): + """Test deleting a resource.""" + mock_response = Mock() + mock_response.status_code = 204 + client._transport.request = MagicMock(return_value=mock_response) + + # Should not raise an exception + client.resources.delete("res-123") + + call_args = client._transport.request.call_args + assert call_args[0][0] == "DELETE" + assert "res-123" in call_args[0][1] +``` + +## Troubleshooting + +### Tests Pass Locally But Fail in CI + +- Ensure you're using the same Python version as CI +- Check for environment-specific issues (file paths, etc.) +- Run `make lint` to catch style issues + +### Import Errors + +```bash +# Reinstall in development mode +make dev-install + +# Or manually +pip install -e ".[dev]" +``` + +### Fixture Not Found + +Ensure fixtures are defined in the same test class or in `conftest.py`: + +```python +# In tests/conftest.py for shared fixtures +import pytest +from pytfe import TFEClient, TFEConfig + +@pytest.fixture +def client(): + """Create a test client.""" + config = TFEConfig(address="https://test.terraform.io", token="test-token") + return TFEClient(config) +``` + +## Additional Resources + +- [pytest documentation](https://docs.pytest.org/) +- [unittest.mock documentation](https://docs.python.org/3/library/unittest.mock.html) +- [Python testing best practices](https://docs.python-guide.org/writing/tests/) From d5b4c2ce5188318ee96a166f7721e12152626086 Mon Sep 17 00:00:00 2001 From: aayushsingh2502 Date: Tue, 6 Jan 2026 11:33:15 +0530 Subject: [PATCH 16/18] contributing doc fix --- docs/CONTRIBUTING.md | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index da977f1..02cca97 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -93,19 +93,6 @@ class Example(BaseModel): ) -class ExampleList(BaseModel): - """Represents a paginated list of examples.""" - - model_config = ConfigDict(populate_by_name=True) - - items: list[Example] = Field(default_factory=list, description="List of examples") - current_page: int | None = Field(None, description="Current page number") - total_pages: int | None = Field(None, description="Total number of pages") - prev_page: int | str | None = Field(None, description="Previous page number") - next_page: int | str | None = Field(None, description="Next page number") - total_count: int | None = Field(None, description="Total number of items") - - class ExampleListOptions(BaseModel): """Options for listing examples.""" @@ -333,7 +320,6 @@ __all__ = [ # ... existing exports ... "Example", "ExampleCreateOptions", - "ExampleList", "ExampleListOptions", "ExampleStatus", "ExampleUpdateOptions", From 618d42a64e8d44b436fc28d10e245753740c1da6 Mon Sep 17 00:00:00 2001 From: Sivaselvan32 Date: Wed, 7 Jan 2026 12:34:03 +0530 Subject: [PATCH 17/18] fix(README):Corrected the address parameter for recommended examples --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2aefdf4..45c869f 100644 --- a/README.md +++ b/README.md @@ -28,11 +28,10 @@ Construct a new **pyTFE** client, then use the resource services on the client t from pytfe import TFEClient, TFEConfig config = TFEConfig( - host="https://tfe.local", + address="https://tfe.local", token="insert-your-token-here", - retry_server_errors=True, timeout=30.0, - user_agent="example-app/0.1 pytfe/0.1", + user_agent_suffix="example-app/0.1 pytfe/0.1", ) client = TFEClient(config) @@ -50,7 +49,7 @@ The default configuration reads the `TFE_ADDRESS` and `TFE_TOKEN` environment va 2. `TFE_TOKEN` โ€” An [API token](https://developer.hashicorp.com/terraform/cloud-docs/users-teams-organizations/api-tokens) for the HCP Terraform or Terraform Enterprise instance. -Environment variables are used as a fallback when `host` or `token` are not provided explicitly: +Environment variables are used as a fallback when `address` or `token` are not provided explicitly: #### Using the default configuration ```python From 7c216e593d83399e500182e318b229551ee5ef9e Mon Sep 17 00:00:00 2001 From: aayushsingh2502 Date: Thu, 8 Jan 2026 12:57:03 +0530 Subject: [PATCH 18/18] pyproject version upgrade and contributing doc update --- docs/CONTRIBUTING.md | 4 +--- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 02cca97..d9a41c1 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -4,8 +4,6 @@ If you find an issue with this package, please create an issue in GitHub. If you ## Adding New Functionality or Fixing Bugs -If you are adding a new endpoint, make sure to update the API coverage list where we keep track of the HCP Terraform APIs that this SDK supports. - If you are making relevant changes worth communicating to our users, please include a note about it in our `CHANGELOG.md`. You can include it as part of the PR where you are submitting your changes. `CHANGELOG.md` should have the next minor version listed as `# v0.X.0 (Unreleased)` and any changes can go under there. But if you feel that your changes are better suited for a patch version (like a critical bug fix), you may list a new section for this version. You should repeat the same formatting style introduced by previous versions. @@ -42,6 +40,7 @@ make test * A resource class should cover one RESTful resource, which sometimes involves two or more endpoints. * Each resource class must be registered in the `TFEClient` class in `client.py`. * You'll need to add unit tests that cover each method of the resource class with mocked responses. +* Each API resource implementation must have a corresponding example file added to the `examples/` directory demonstrating its usage. * Option classes serve as a proxy for either passing query params or request bodies: - `ListOptions` and `ReadOptions` are values passed as query parameters. - `CreateOptions` and `UpdateOptions` represent the request body. @@ -559,7 +558,6 @@ Before submitting a PR, ensure: - [ ] All tests pass (`make test`) - [ ] New functionality has unit tests - [ ] CHANGELOG.md is updated -- [ ] API coverage list is updated (if adding endpoints) - [ ] Example file is added/updated (if adding resource) - [ ] Docstrings are added to new classes/methods diff --git a/pyproject.toml b/pyproject.toml index 7f0fc62..5d42334 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "pytfe" -version = "0.1.0" +version = "0.1.1" description = "Official Python SDK for HashiCorp Terraform Cloud / Terraform Enterprise (TFE) API v2" readme = "README.md" license = { text = "MPL-2.0" }