Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,4 @@ protos.bin

# Local RSA keys
.local/*

54 changes: 54 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,59 @@
# Unreleased

# Released
# v0.1.5

* `pytfe.__version__` added in src/pytfe/init.py via importlib.metadata.version("pytfe"). This will resolve to the version from pyproject.toml.
* Updated comments, sshkey, stateversion and cost-estimate models to have id as mandatory attribute by @isivaselvan [#137](https://github.com/hashicorp/python-tfe/pull/137)
* Updated workspace resource to include additional relationship models include AgentPool, Configuration-version, Run, Variables and State-version by @isivaselvan [#138](https://github.com/hashicorp/python-tfe/pull/138)

## Bug Fixes
* Run.read / Run.create fail with pydantic ValidationError when response has a `cost-estimate` and `comments` relationship.

# v0.1.4

## Enhancements
* Standardize Notification Configuration option models on Pydantic [#132](https://github.com/hashicorp/python-tfe/pull/132)

# v0.1.3

## Enhancements

### Iterator Pattern Migration
* Migrated Run resource list operations to iterator pattern by @NimishaShrivastava-dev [#91](https://github.com/hashicorp/python-tfe/pull/91)
* Migrated Policy resource list operations to iterator pattern by @TanyaSingh369-svg [#92](https://github.com/hashicorp/python-tfe/pull/92)
* Migrated Policy Set resource list operations to iterator pattern by @TanyaSingh369-svg [#95](https://github.com/hashicorp/python-tfe/pull/95)
* Migrated Run Event resource list operations to iterator pattern by @NimishaShrivastava-dev [#97](https://github.com/hashicorp/python-tfe/pull/97)
* Migrated SSH Keys resource list operations to iterator pattern by @NimishaShrivastava-dev [#101](https://github.com/hashicorp/python-tfe/pull/101)
* Migrated Notification Configuration resource list operations to iterator pattern by @TanyaSingh369-svg [#109](https://github.com/hashicorp/python-tfe/pull/109)
* Migrated Variable Set list operations to iterator pattern by @isivaselvan [#113](https://github.com/hashicorp/python-tfe/pull/113)
* Migrated Variable Set Variables list operations to iterator pattern by @isivaselvan [#113](https://github.com/hashicorp/python-tfe/pull/113)
* Migrated State Version list operations to iterator pattern by @isivaselvan [#113](https://github.com/hashicorp/python-tfe/pull/113)
* Migrated State Version Output list operations to iterator pattern by @isivaselvan [#113](https://github.com/hashicorp/python-tfe/pull/113)
* Migrated Policy Check list operations to iterator pattern by @isivaselvan [#113](https://github.com/hashicorp/python-tfe/pull/113)
* Refreshed examples and unit tests to align with iterator pattern updates by @NimishaShrivastava-dev, @TanyaSingh369-svg, @isivaselvan [#91](https://github.com/hashicorp/python-tfe/pull/91) [#92](https://github.com/hashicorp/python-tfe/pull/92) [#95](https://github.com/hashicorp/python-tfe/pull/95) [#97](https://github.com/hashicorp/python-tfe/pull/97) [#101](https://github.com/hashicorp/python-tfe/pull/101) [#109](https://github.com/hashicorp/python-tfe/pull/109) [#113](https://github.com/hashicorp/python-tfe/pull/113)

### Project and Workspace Management
* Updated Project create and update models, including Project model refinements by @isivaselvan [#120](https://github.com/hashicorp/python-tfe/pull/120)
* Updated Project endpoints for list-effective-tag-bindings and delete-tag-bindings by @isivaselvan [#120](https://github.com/hashicorp/python-tfe/pull/120)
* Refactored Workspace models to improve validation with Pydantic by @isivaselvan [#106](https://github.com/hashicorp/python-tfe/pull/106)

## Breaking Change

### List Method Behavior
* Standardized list methods across multiple resources to iterator-based behavior, replacing legacy list response patterns by @NimishaShrivastava-dev, @TanyaSingh369-svg, @isivaselvan [#91](https://github.com/hashicorp/python-tfe/pull/91) [#92](https://github.com/hashicorp/python-tfe/pull/92) [#95](https://github.com/hashicorp/python-tfe/pull/95) [#97](https://github.com/hashicorp/python-tfe/pull/97) [#101](https://github.com/hashicorp/python-tfe/pull/101) [#109](https://github.com/hashicorp/python-tfe/pull/109) [#113](https://github.com/hashicorp/python-tfe/pull/113)

## Bug Fixes
* Fixed pagination parameter handling across iterator-based page traversal by @isivaselvan [#111](https://github.com/hashicorp/python-tfe/pull/111)
* Fixed state version and state version output model import/export registration by @isivaselvan [#105](https://github.com/hashicorp/python-tfe/pull/105)
* Fixed the tag based filtering of workspace in list operation by @isivaselvan [#106](https://github.com/hashicorp/python-tfe/pull/106)
* Fixed the project response of workspace relationship by @isivaselvan [#106](https://github.com/hashicorp/python-tfe/pull/106)
* Fixed configuration version examples and added terraform+cloud support for ConfigurationSource usage by @isivaselvan [#107](https://github.com/hashicorp/python-tfe/pull/107)
* Fixed configuration upload packaging flow (tarfile-based handling) by @isivaselvan [#107](https://github.com/hashicorp/python-tfe/pull/107)
* Updated agent pool workspace assign/remove operations to consistently return AgentPool objects by @KshitijaChoudhari [#110](https://github.com/hashicorp/python-tfe/pull/110)
* Updated Run relationships handling for improved model consistency by @ibm-richard [#119](https://github.com/hashicorp/python-tfe/pull/119)
* Updated additional Run Source attributes by @isivaselvan [#123](https://github.com/hashicorp/python-tfe/pull/123)

# v0.1.2

## Features
Expand Down
34 changes: 30 additions & 4 deletions examples/agent_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
This example demonstrates:
1. Agent Pool CRUD operations (Create, Read, Update, Delete)
2. Agent token creation and management
3. Using the organization SDK client
3. Workspace assignment using assign_to_workspaces and remove_from_workspaces
4. Proper error handling

Make sure to set the following environment variables:
- TFE_TOKEN: Your Terraform Cloud/Enterprise API token
- TFE_ADDRESS: Your Terraform Cloud/Enterprise URL (optional, defaults to https://app.terraform.io)
- TFE_ORG: Your organization name
- TFE_WORKSPACE_ID: A workspace ID for testing workspace assignment (optional)

Usage:
export TFE_TOKEN="your-token-here"
Expand All @@ -27,8 +28,10 @@
from pytfe.errors import NotFound
from pytfe.models import (
AgentPoolAllowedWorkspacePolicy,
AgentPoolAssignToWorkspacesOptions,
AgentPoolCreateOptions,
AgentPoolListOptions,
AgentPoolRemoveFromWorkspacesOptions,
AgentPoolUpdateOptions,
AgentTokenCreateOptions,
)
Expand All @@ -40,6 +43,9 @@ def main():
token = os.environ.get("TFE_TOKEN")
org = os.environ.get("TFE_ORG")
address = os.environ.get("TFE_ADDRESS", "https://app.terraform.io")
workspace_id = os.environ.get(
"TFE_WORKSPACE_ID"
) # optional, for workspace assignment

if not token:
print("TFE_TOKEN environment variable is required")
Expand Down Expand Up @@ -99,7 +105,27 @@ def main():
updated_pool = client.agent_pools.update(new_pool.id, update_options)
print(f"Updated agent pool name to: {updated_pool.name}")

# Example 5: Create an agent token
# Example 5: Workspace assignment
# assign_to_workspaces sends PATCH /agent-pools/:id with relationships.allowed-workspaces
# remove_from_workspaces sends PATCH /agent-pools/:id with relationships.excluded-workspaces
if workspace_id:
print("\n Assigning workspace to agent pool...")
updated_pool = client.agent_pools.assign_to_workspaces(
new_pool.id,
AgentPoolAssignToWorkspacesOptions(workspace_ids=[workspace_id]),
)
print(f" Assigned workspace {workspace_id} to pool {updated_pool.name}")

print("\n Removing workspace from agent pool...")
updated_pool = client.agent_pools.remove_from_workspaces(
new_pool.id,
AgentPoolRemoveFromWorkspacesOptions(workspace_ids=[workspace_id]),
)
print(f" Removed workspace {workspace_id} from pool {updated_pool.name}")
else:
print("\n Skipping workspace assignment (set TFE_WORKSPACE_ID to test)")

# Example 6: Create an agent token
print("\n Creating agent token...")
token_options = AgentTokenCreateOptions(
description="SDK example token" # Optional description
Expand All @@ -110,7 +136,7 @@ def main():
if agent_token.token:
print(f" Token (first 10 chars): {agent_token.token[:10]}...")

# Example 6: List agent tokens
# Example 7: List agent tokens
print("\n Listing agent tokens...")
tokens = client.agent_tokens.list(new_pool.id)

Expand All @@ -120,7 +146,7 @@ def main():
for token in token_list:
print(f" - {token.description or 'No description'} (ID: {token.id})")

# Example 7: Clean up - delete the token and pool
# Example 8: Clean up - delete the token and pool
print("\n Cleaning up...")
client.agent_tokens.delete(agent_token.id)
print("Deleted agent token")
Expand Down
65 changes: 14 additions & 51 deletions examples/configuration_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,29 +280,8 @@ def main():
print(f" - {filename} ({size} bytes)")

try:
# Create tar.gz archive manually since go-slug isn't available
print("Creating tar.gz archive manually...")

import tarfile

# Create tar.gz archive in memory
archive_buffer = io.BytesIO()
with tarfile.open(fileobj=archive_buffer, mode="w:gz") as tar:
# Add all files from the temp directory
for filename in files:
filepath = os.path.join(temp_dir, filename)
tar.add(filepath, arcname=filename)

archive_buffer.seek(0)
archive_bytes = archive_buffer.getvalue()
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...")
archive_buffer.seek(0) # Reset buffer position
client.configuration_versions.upload_tar_gzip(
new_cv.upload_url, archive_buffer
)
print("Uploading Terraform configuration...")
client.configuration_versions.upload(new_cv.upload_url, temp_dir)
print("Terraform configuration uploaded successfully!")

# Wait and check status
Expand Down Expand Up @@ -411,8 +390,6 @@ def main():
# =====================================================
# TEST 4: UPLOAD CONFIGURATION VERSION
# =====================================================
# Test 4: Upload function (requires go-slug)
# =====================================================
print("\n4. Testing upload() function:")
try:
# Create a fresh configuration version specifically for upload testing
Expand Down Expand Up @@ -444,33 +421,19 @@ def main():
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!")

# Check 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}")
client.configuration_versions.upload(upload_url, temp_dir)
print("Configuration uploaded successfully!")

if updated_cv.status.value != "pending":
print("Status changed (upload processed)")
else:
print("Status still pending (may need more time)")
# Check 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}")

except ImportError as e:
if "go-slug" in str(e):
print("go-slug package not available")
print("Install with: pip install go-slug")
print(
"Upload function exists but requires go-slug for packaging"
)
print(
"Function correctly raises ImportError when go-slug unavailable"
)
else:
raise
if updated_cv.status.value != "pending":
print("Status changed (upload processed)")
else:
print("Status still pending (may need more time)")

except Exception as e:
print(f"Error: {e}")
Expand Down Expand Up @@ -871,7 +834,7 @@ def main():
"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 4: upload() - Upload Terraform configurations (stdlib tarfile)")
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")
Expand Down
67 changes: 40 additions & 27 deletions examples/notification_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,9 @@
"""

import os
import sys

# Add the src directory to the Python path so we can import the tfe module
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))

from pytfe.client import TFEClient
from pytfe.models.notification_configuration import (
from pytfe.models import (
NotificationConfigurationCreateOptions,
NotificationConfigurationListOptions,
NotificationConfigurationSubscribableChoice,
Expand All @@ -35,9 +31,15 @@ def main():

print("=== Python TFE Notification Configuration Example ===\n")

# Resolve workspace and team from environment (fallback to demo placeholders)
workspace_id = os.getenv("TFE_WORKSPACE_ID", "ws-example123456789")
workspace_name = os.getenv("TFE_ORG", "your-workspace-name")
# Resolve organization and workspace from environment variables
org_name = os.environ["TFE_ORG"]
workspace_name = os.getenv("TFE_WORKSPACE_NAME", "test-api")
workspace_id = os.getenv("TFE_WORKSPACE_ID", "")
if not workspace_id:
print(f"Looking up workspace '{workspace_name}' in org '{org_name}'...")
ws = client.workspaces.read(workspace_name, organization=org_name)
workspace_id = ws.id
print(f"Resolved workspace ID: {workspace_id}")
print(f"Using workspace: {workspace_name} (ID: {workspace_id})")

team_id = os.getenv("TFE_TEAM_ID", "team-example123456789")
Expand All @@ -50,13 +52,13 @@ def main():
# ===== List notification configurations for workspace =====
print("1. Listing notification configurations for workspace...")
try:
workspace_notifications = client.notification_configurations.list(
subscribable_id=workspace_id
workspace_notifications_list = list(
client.notification_configurations.list(subscribable_id=workspace_id)
)
print(
f"Found {len(workspace_notifications.items)} notification configurations"
f"Found {len(workspace_notifications_list)} notification configurations"
)
for nc in workspace_notifications.items:
for nc in workspace_notifications_list:
print(f"- {nc.name} (ID: {nc.id}, Enabled: {nc.enabled})")
except Exception as e:
print(f"Error listing workspace notifications: {e}")
Expand All @@ -72,13 +74,15 @@ def main():
options = NotificationConfigurationListOptions(
subscribable_choice=team_choice
)
team_notifications = client.notification_configurations.list(
subscribable_id=team_id, options=options
team_notifications_list = list(
client.notification_configurations.list(
subscribable_id=team_id, options=options
)
)
print(
f"Found {len(team_notifications.items)} team notification configurations"
f"Found {len(team_notifications_list)} team notification configurations"
)
for nc in team_notifications.items:
for nc in team_notifications_list:
print(f"- {nc.name} (ID: {nc.id}, Enabled: {nc.enabled})")
except Exception as e:
error_msg = str(e).lower()
Expand All @@ -96,16 +100,20 @@ def main():
workspace_choice = NotificationConfigurationSubscribableChoice(
workspace={"id": workspace_id}
)
slack_url = os.getenv(
# Use GENERIC destination type with a URL that returns HTTP 200.
# SLACK/MICROSOFT_TEAMS destinations are auto-verified by HCP Terraform
# at creation time; a fake Slack URL returns 302 and causes the create
# call to fail immediately. GENERIC webhooks + httpbin always succeed.
webhook_url = os.getenv(
"WEBHOOK_URL",
"https://hooks.slack.com/services/YOUR_SLACK_WORKSPACE/YOUR_CHANNEL/YOUR_WEBHOOK_TOKEN",
"https://httpbin.org/status/200",
)
create_options = NotificationConfigurationCreateOptions(
destination_type=NotificationDestinationType.SLACK,
destination_type=NotificationDestinationType.GENERIC,
enabled=True,
name="Python TFE Example Slack Notification",
name="Python TFE Example Generic Notification",
subscribable_choice=workspace_choice,
url=slack_url,
url=webhook_url,
triggers=[
NotificationTriggerType.COMPLETED,
NotificationTriggerType.ERRORED,
Expand Down Expand Up @@ -178,12 +186,17 @@ def main():

except Exception as e:
error_msg = str(e).lower()
if "verification failed" in error_msg and "404" in error_msg:
print(" Webhook verification failed (expected with fake URL)")
print("The fake Slack URL returns 404 - this is normal for testing")
print("To test real verification, use a webhook from:")
print("webhook.site (instant test URL)")
print("Slack, Teams, or Discord webhook")
if "verification failed" in error_msg and (
"404" in error_msg or "302" in error_msg
):
print("Webhook verification failed (expected with fake URL)")
print(
"The URL returned a non-200 response - this is normal for testing"
)
print("To test real verification, use a webhook from webhook.site,")
print(
"Slack, Teams, or Discord, or set WEBHOOK_URL=https://httpbin.org/status/200"
)
else:
print(f" Error in workspace notification operations: {e}")

Expand Down
Loading