diff --git a/examples/agent.py b/examples/agent.py index d756abf..c80eb62 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 @@ -81,45 +81,45 @@ 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.") + 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..bbaf14e 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,39 +76,39 @@ 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}") - 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...") + 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 @@ -118,21 +118,21 @@ 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") + 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/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 e983e94..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,43 +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 ) @@ -341,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() @@ -371,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", @@ -405,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() @@ -428,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() @@ -494,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 @@ -505,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() @@ -568,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"]: @@ -598,62 +588,56 @@ 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}") + print(f" Error: {e}") import traceback traceback.print_exc() @@ -673,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) @@ -716,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() @@ -768,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 @@ -786,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() @@ -825,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( @@ -871,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 @@ -887,20 +863,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..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,29 +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() @@ -215,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() @@ -265,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 96671dd..e9cf62a 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,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) @@ -298,7 +294,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 +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 @@ -327,7 +323,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 +334,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 +363,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 +374,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 +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 @@ -434,14 +428,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..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 93b8f0f..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" ) @@ -282,7 +278,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 +292,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..da6d45d --- /dev/null +++ b/examples/organization_membership.py @@ -0,0 +1,314 @@ +#!/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)") + 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") + 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") + 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}") + 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/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 new file mode 100644 index 0000000..9ffdf71 --- /dev/null +++ b/examples/policy_set_parameter.py @@ -0,0 +1,213 @@ +from __future__ import annotations + +import argparse +import os + +from pytfe import TFEClient, TFEConfig +from pytfe.models import ( + PolicySetParameterCreateOptions, + PolicySetParameterListOptions, + PolicySetParameterUpdateOptions, +) + + +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-size", + type=int, + default=100, + 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") + 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" + ) + 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_size=args.page_size, + ) + + 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 param_count == 0: + print("No parameters found.") + else: + print(f"Total: {param_count} parameters") + + # 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) 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") + 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: + 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 if args.value else "", + 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") + 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__": + main() diff --git a/examples/project.py b/examples/project.py index 6999e4f..7702b09 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(" 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"), @@ -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,16 +656,16 @@ 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(" 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: @@ -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" - print(f" {feature_name}: {status}") + 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(" 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 0be3b99..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)" + " 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}") + 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 @@ -829,26 +827,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..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 @@ -262,9 +260,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 +295,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..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 @@ -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,8 +67,8 @@ 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"Created reserved tag key: {new_rtk.id} - {new_rtk.key}") + print(f"Disable Overrides: {new_rtk.disable_overrides}") # 3. Update the reserved tag key print("\n3. Updating 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" Disable Overrides: {updated_rtk.disable_overrides}") + 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" Total pages: {paginated_rtks.total_pages}") - print(f" Total count: {paginated_rtks.total_count}") + 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.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 102874d..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 c651210..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,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,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) @@ -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..743bd98 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("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,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" 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!") + 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("\n๐Ÿ”ง Try using a User Token instead of Organization Token") + 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") 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/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 bc5227e..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:") @@ -175,15 +171,15 @@ 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}") - 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") @@ -262,12 +258,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}") + 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 3f55a94..4dfb643 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: @@ -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: diff --git a/examples/workspace_resources.py b/examples/workspace_resources.py new file mode 100644 index 0000000..15fbf1b --- /dev/null +++ b/examples/workspace_resources.py @@ -0,0 +1,118 @@ +"""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 3894886..f50cdfe 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 @@ -16,6 +17,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 @@ -31,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 @@ -64,11 +67,13 @@ 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) 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) @@ -84,6 +89,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..61853d1 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" @@ -460,3 +468,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..c70dd05 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, @@ -133,6 +141,12 @@ PolicySetRemoveWorkspacesOptions, PolicySetUpdateOptions, ) +from .policy_set_parameter import ( + PolicySetParameter, + PolicySetParameterCreateOptions, + PolicySetParameterListOptions, + PolicySetParameterUpdateOptions, +) from .policy_types import ( EnforcementLevel, PolicyKind, @@ -274,6 +288,11 @@ SSHKeyListOptions, SSHKeyUpdateOptions, ) +from .team import ( + OrganizationAccess, + Team, + TeamPermissions, +) # Variables from .variable import ( @@ -334,6 +353,12 @@ WorkspaceUpdateRemoteStateConsumersOptions, ) +# โ”€โ”€ Workspace Resources โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +from .workspace_resource import ( + WorkspaceResource, + WorkspaceResourceListOptions, +) + # โ”€โ”€ Public surface โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ __all__ = [ # OAuth @@ -449,6 +474,15 @@ "Organization", "OrganizationCreateOptions", "OrganizationUpdateOptions", + "OrganizationMembership", + "OrganizationMembershipCreateOptions", + "OrganizationMembershipListOptions", + "OrganizationMembershipReadOptions", + "OrganizationMembershipStatus", + "OrgMembershipIncludeOpt", + "OrganizationAccess", + "Team", + "TeamPermissions", "Project", "ProjectAddTagBindingsOptions", "ProjectCreateOptions", @@ -496,6 +530,9 @@ "WorkspaceTagListOptions", "WorkspaceUpdateOptions", "WorkspaceUpdateRemoteStateConsumersOptions", + # Workspace Resources + "WorkspaceResource", + "WorkspaceResourceListOptions", "RunQueue", "ReadRunQueueOptions", # Runs @@ -586,6 +623,11 @@ "PolicySetRemoveWorkspaceExclusionsOptions", "PolicySetRemoveProjectsOptions", "PolicySetUpdateOptions", + # Policy Set Parameters + "PolicySetParameter", + "PolicySetParameterCreateOptions", + "PolicySetParameterListOptions", + "PolicySetParameterUpdateOptions", "PolicyKind", "EnforcementLevel", # Variable Sets 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/policy_set_parameter.py b/src/pytfe/models/policy_set_parameter.py new file mode 100644 index 0000000..01a88c2 --- /dev/null +++ b/src/pytfe/models/policy_set_parameter.py @@ -0,0 +1,44 @@ +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 PolicySetParameterListOptions(BaseModel): + model_config = ConfigDict(populate_by_name=True, validate_by_name=True) + + 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/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/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/organization_membership.py b/src/pytfe/resources/organization_membership.py new file mode 100644 index 0000000..659608b --- /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 | None = None, + ) -> 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/policy_set_parameter.py b/src/pytfe/resources/policy_set_parameter.py new file mode 100644 index 0000000..076579c --- /dev/null +++ b/src/pytfe/resources/policy_set_parameter.py @@ -0,0 +1,134 @@ +from __future__ import annotations + +from collections.abc import Iterator +from typing import Any + +from ..errors import ( + InvalidCategoryError, + InvalidParamIDError, + InvalidPolicySetIDError, + RequiredCategoryError, + RequiredKeyError, +) +from ..models.policy_set_parameter import ( + PolicySetParameter, + PolicySetParameterCreateOptions, + PolicySetParameterListOptions, + PolicySetParameterUpdateOptions, +) +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 + ) -> 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 {} + path = f"/api/v2/policy-sets/{policy_set_id}/parameters" + for item in self._list(path, params=params): + yield self._policy_set_parameter_from(item) + + 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, + ) + 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.""" + 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}", + ) + data = r.json().get("data", {}) + return self._policy_set_parameter_from(data) + + def update( + self, + policy_set_id: str, + parameter_id: str, + options: PolicySetParameterUpdateOptions, + ) -> PolicySetParameter: + """Update values of an existing parameter.""" + if not valid_string_id(policy_set_id): + raise InvalidPolicySetIDError() + + if not valid_string_id(parameter_id): + raise InvalidParamIDError() + 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, + ) + 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.""" + if not valid_string_id(policy_set_id): + raise InvalidPolicySetIDError() + + 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 + + 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) 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/src/pytfe/resources/workspace_resources.py b/src/pytfe/resources/workspace_resources.py new file mode 100644 index 0000000..617b5f7 --- /dev/null +++ b/src/pytfe/resources/workspace_resources.py @@ -0,0 +1,66 @@ +"""Workspace resources service for Terraform Enterprise.""" + +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 = f"/api/v2/workspaces/{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_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 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 diff --git a/tests/units/test_workspace_resources.py b/tests/units/test_workspace_resources.py new file mode 100644 index 0000000..f685d84 --- /dev/null +++ b/tests/units/test_workspace_resources.py @@ -0,0 +1,278 @@ +"""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_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