Skip to content

Commit 6d2f02b

Browse files
committed
add new features
1 parent e112db8 commit 6d2f02b

23 files changed

Lines changed: 1967 additions & 18 deletions

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,25 @@ Related data is now a complete, first-class part of every response. Before, `?in
1212
* `task_stages.read(task_stage_id, TaskStageReadOptions(include=[...]))`: `run`, `run.workspace`, `task-results`, `policy-evaluations`.
1313
* `organizations.read(name, OrganizationReadOptions(include=[...]))`: `subscription`. The new `options` argument is optional, so existing calls are unchanged.
1414

15+
### New resources
16+
* Added `client.ip_ranges` — read HCP Terraform / Terraform Enterprise outbound IP ranges via `GET /api/meta/ip-ranges`. `read(modified_since=None)` returns an `IPRange` (CIDR lists for `api`, `notifications`, `sentinel`, `vcs`), or `None` when an `If-Modified-Since` date is supplied and the ranges are unchanged (HTTP 304). New model: `IPRange`.
17+
* Added `client.plan_exports` — export Terraform plan data (Sentinel mock bundles). `create(options)`, `read(id)`, `delete(id)`, and `download(id)` (returns the `.tar.gz` archive bytes, following the temporary presigned-URL redirect). New models: `PlanExport`, `PlanExportCreateOptions`, `PlanExportStatus`, `PlanExportDataType`, `PlanExportStatusTimestamps`. New errors: `InvalidPlanExportIDError`, `RequiredPlanError`.
18+
* Added `client.cost_estimates` — read run cost estimates. `read(id)` returns a `CostEstimate`; `logs(id)` returns the estimate's log output text. `CostEstimate`, `CostEstimateStatus`, and `CostEstimateStatusTimestamps` are now exported from `pytfe.models`. New error: `InvalidCostEstimateIDError`.
19+
* Added IP allowlists (the JSON:API `cidr-range-lists` / `cidr-ranges` resources) as `client.cidr_range_lists` and `client.cidr_ranges`. `cidr_range_lists` supports `list`, `create`, `read`, `update`, `delete`, plus `list_cidr_ranges`, `add_cidr_range`, and `add_agent_pools` / `remove_agent_pools`; `cidr_ranges` supports `read`, `update`, `delete`. New models: `CIDRRangeList`, `CIDRRange`, `EnforcementScope`, and their create/update/list options. New errors: `InvalidCIDRRangeListIDError`, `InvalidCIDRRangeIDError`, `RequiredCIDRBlockError`.
20+
1521
## Bug Fixes
1622

1723
### Relationships
1824
* Fixed `workspaces.read*(include=[WorkspaceIncludeOpt.OUTPUTS])` returning outputs with `None` name, value, and type. Workspace `outputs` is now filled from the `included` data. [#134](https://github.com/hashicorp/python-tfe/issues/134)
1925
* Fixed `policy_set.read*(include=[current_version | newest_version])` returning an id-only stub. `PolicySetVersion` is now exported from `pytfe.models` and fully resolved, so the version's `source`, `created_at`, and `status` are populated.
2026
* Fixed `variable_set.read` inventing placeholder values (such as `name="workspace-<id>"` or `key="var-<id>"`) for `workspaces`, `projects`, and `vars`. These are now id-only stubs by default and fill from `included` when requested.
2127

28+
### Cost estimates
29+
* Fixed `CostEstimate` failing to parse real API responses: `status-timestamps` now treats every timestamp as optional (the API only returns the ones that have occurred) and adds the missing `pending-at`, and `error-message` now accepts `null`. Previously an included `cost-estimate` with a null error or partial timestamps would silently collapse to an id-only stub.
30+
31+
### Transport
32+
* Fixed the shared HTTP client retaining `Set-Cookie` session cookies across requests. The `/api/meta/ip-ranges` endpoint returns an `_atlas_session_data` cookie; once stored, that browser session silently overrode bearer-token auth on every subsequent request, causing spurious `401`/`404` errors. The transport now never persists cookies (this SDK authenticates only with the bearer token). Without this fix, any call to `client.ip_ranges.read()` broke all later authenticated calls on the same client.
33+
2234
# Released
2335
# v1.1.0
2436

docs/api-coverage.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ resource list is reconciled against the public
88

99
**Legend:** ✅ Covered &nbsp;·&nbsp; 🟡 Partial &nbsp;·&nbsp; ❌ Not yet implemented
1010

11-
pytfe implements **61 resource namespaces**. The resources still missing or
11+
pytfe implements **66 resource namespaces**. The resources still missing or
1212
partially covered are listed at the bottom of this page.
1313

1414
## Covered resources
@@ -27,6 +27,8 @@ partially covered are listed at the bottom of this page.
2727
| | Team workspace access | `client.team_workspace_accesses` ||
2828
| | Users | `client.users` ||
2929
| | SSH keys | `client.ssh_keys` ||
30+
| | IP allowlists (CIDR range lists) | `client.cidr_range_lists` ||
31+
| | CIDR ranges | `client.cidr_ranges` ||
3032
| Workspaces & config | Workspaces | `client.workspaces` ||
3133
| | Workspace resources | `client.workspace_resources` ||
3234
| | Projects | `client.projects` ||
@@ -39,7 +41,9 @@ partially covered are listed at the bottom of this page.
3941
| | Run events | `client.run_events` ||
4042
| | Run triggers | `client.run_triggers` ||
4143
| | Plans | `client.plans` ||
44+
| | Plan exports | `client.plan_exports` ||
4245
| | Applies | `client.applies` ||
46+
| | Cost estimates | `client.cost_estimates` ||
4347
| | Comments | `client.comments` ||
4448
| | Query runs | `client.query_runs` ||
4549
| | State versions | `client.state_versions` ||
@@ -75,6 +79,7 @@ partially covered are listed at the bottom of this page.
7579
| | Azure OIDC configurations | `client.azure_oidc_configurations` ||
7680
| | GCP OIDC configurations | `client.gcp_oidc_configurations` ||
7781
| | Vault OIDC configurations | `client.vault_oidc_configurations` ||
82+
| Meta | IP ranges | `client.ip_ranges` ||
7883
| Admin (TFE site-admin) | Organizations, users, runs, workspaces | `client.admin.organizations` / `.users` / `.runs` / `.workspaces` ||
7984
| | Terraform / OPA / Sentinel versions | `client.admin.terraform_versions` / `.opa_versions` / `.sentinel_versions` ||
8085
| | SAML / SCIM / SMTP settings + SCIM tokens | `client.admin.saml_settings` / `.scim_settings` / `.scim_tokens` / `.smtp_settings` ||
@@ -96,15 +101,11 @@ Public HCP Terraform API resources that do not yet have a pytfe client namespace
96101
| Assessment results | Health-assessment reads. Model exists (`models/assessment_result.py`); surfaced indirectly via `workspace.current_assessment_result`. |
97102
| Audit trails tokens | Auth tokens for the audit-trail streaming API. |
98103
| Change requests ||
99-
| Cost estimates | Run cost-estimation reads. Model exists (`models/cost_estimate.py`). |
100104
| Feature sets | Organization feature sets. |
101105
| GPG keys | Private Registry provider signing keys. |
102106
| Group member roles | Team member role assignments. |
103107
| Invoices | Organization billing invoices. |
104-
| IP allowlists | Organization IP allowlist. |
105-
| IP ranges | `/api/meta/ip-ranges`. |
106108
| Metrics service tokens | Metrics endpoint service tokens. |
107-
| Plan exports | Sentinel mock / plan-export download. Model exists (`models/plan_export.py`). |
108109
| Stack configuration summary | Builds on the existing stack_configuration resource |
109110
| Stack deployment | Core Stacks deployment lifecycle |
110111
| Stack deployment groups | Extends stack_deployment |

examples/cidr_range_list.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# Copyright IBM Corp. 2025, 2026
2+
# SPDX-License-Identifier: MPL-2.0
3+
4+
"""IP allowlist (CIDR range list) demo for the python-tfe SDK.
5+
6+
HCP Terraform's IP allowlist feature is exposed as the JSON:API
7+
``cidr-range-lists`` and ``cidr-ranges`` resources, surfaced here as
8+
``client.cidr_range_lists`` and ``client.cidr_ranges``.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import argparse
14+
import os
15+
16+
from pytfe import TFEClient, TFEConfig
17+
from pytfe.models import (
18+
CIDRRangeCreateOptions,
19+
CIDRRangeListCreateOptions,
20+
EnforcementScope,
21+
)
22+
23+
24+
def _print_header(title: str):
25+
print("\n" + "=" * 80)
26+
print(title)
27+
print("=" * 80)
28+
29+
30+
def main():
31+
parser = argparse.ArgumentParser(
32+
description="IP allowlists (CIDR range lists) demo for python-tfe SDK"
33+
)
34+
parser.add_argument(
35+
"--address", default=os.getenv("TFE_ADDRESS", "https://app.terraform.io")
36+
)
37+
parser.add_argument("--token", default=os.getenv("TFE_TOKEN", ""))
38+
parser.add_argument("--organization", default=os.getenv("TFE_ORG", ""))
39+
parser.add_argument(
40+
"--create",
41+
action="store_true",
42+
help="Create an allowlist (with --name) and add --cidr to it",
43+
)
44+
parser.add_argument("--name", help="Name for the new IP allowlist")
45+
parser.add_argument(
46+
"--cidr", help="CIDR block to add (e.g. 192.168.1.0/24), with --create"
47+
)
48+
parser.add_argument("--id", help="IP allowlist ID for --read / --delete")
49+
parser.add_argument("--read", action="store_true", help="Read one IP allowlist")
50+
parser.add_argument(
51+
"--delete", action="store_true", help="Delete the IP allowlist (--id)"
52+
)
53+
args = parser.parse_args()
54+
55+
if not args.token:
56+
print("TFE_TOKEN is not set")
57+
return 2
58+
59+
client = TFEClient(TFEConfig(address=args.address, token=args.token))
60+
61+
_print_header(f"Listing IP allowlists for {args.organization}")
62+
for crl in client.cidr_range_lists.list(args.organization):
63+
ranges = list(client.cidr_range_lists.list_cidr_ranges(crl.id))
64+
print(f"- {crl.id} {crl.name} scope={crl.enforcement_scope}")
65+
for r in ranges:
66+
print(f" {r.id} {r.cidr_block}")
67+
68+
if args.create:
69+
if not args.name:
70+
print("--name is required for --create")
71+
return 2
72+
_print_header(f"Creating IP allowlist: {args.name}")
73+
crl = client.cidr_range_lists.create(
74+
args.organization,
75+
CIDRRangeListCreateOptions(
76+
name=args.name,
77+
enforcement_scope=EnforcementScope.SELECTED_AGENT_POOLS,
78+
),
79+
)
80+
print(f"Created {crl.id}")
81+
if args.cidr:
82+
cidr = client.cidr_range_lists.add_cidr_range(
83+
crl.id, CIDRRangeCreateOptions(cidr_block=args.cidr)
84+
)
85+
print(f"Added CIDR range {cidr.id}: {cidr.cidr_block}")
86+
87+
if args.read:
88+
if not args.id:
89+
print("--id is required for --read")
90+
return 2
91+
_print_header(f"Reading IP allowlist: {args.id}")
92+
crl = client.cidr_range_lists.read(args.id)
93+
print(f"Name: {crl.name}")
94+
print(f"Description: {crl.description}")
95+
print(f"Enforcement scope: {crl.enforcement_scope}")
96+
97+
if args.delete and args.id:
98+
_print_header(f"Deleting IP allowlist: {args.id}")
99+
client.cidr_range_lists.delete(args.id)
100+
print("Deleted.")
101+
102+
return 0
103+
104+
105+
if __name__ == "__main__":
106+
raise SystemExit(main())

examples/cost_estimate.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Copyright IBM Corp. 2025, 2026
2+
# SPDX-License-Identifier: MPL-2.0
3+
4+
from __future__ import annotations
5+
6+
import argparse
7+
import os
8+
9+
from pytfe import TFEClient, TFEConfig
10+
11+
12+
def _print_header(title: str):
13+
print("\n" + "=" * 80)
14+
print(title)
15+
print("=" * 80)
16+
17+
18+
def main():
19+
parser = argparse.ArgumentParser(
20+
description="Cost estimates demo for python-tfe SDK"
21+
)
22+
parser.add_argument(
23+
"--address", default=os.getenv("TFE_ADDRESS", "https://app.terraform.io")
24+
)
25+
parser.add_argument("--token", default=os.getenv("TFE_TOKEN", ""))
26+
parser.add_argument(
27+
"--cost-estimate-id", help="Cost estimate ID to read (e.g. ce-xxxxx)"
28+
)
29+
parser.add_argument(
30+
"--run-id",
31+
help="Run ID to discover the cost estimate from (e.g. run-xxxxx)",
32+
)
33+
parser.add_argument(
34+
"--logs", action="store_true", help="Also print the cost estimate logs"
35+
)
36+
args = parser.parse_args()
37+
38+
if not args.token:
39+
print("TFE_TOKEN is not set")
40+
return 2
41+
42+
cfg = TFEConfig(address=args.address, token=args.token)
43+
client = TFEClient(cfg)
44+
45+
ce_id = args.cost_estimate_id
46+
47+
# Cost estimates have no list endpoint; the ID lives on a run's
48+
# relationships.cost-estimate. Look it up when only a run ID is given.
49+
if not ce_id and args.run_id:
50+
_print_header(f"Discovering cost estimate from run: {args.run_id}")
51+
run = client.runs.read(args.run_id)
52+
if run.cost_estimate and run.cost_estimate.id:
53+
ce_id = run.cost_estimate.id
54+
print(f"Found cost estimate: {ce_id}")
55+
else:
56+
print("This run has no cost estimate.")
57+
return 0
58+
59+
if not ce_id:
60+
print("Provide --cost-estimate-id or --run-id")
61+
return 2
62+
63+
_print_header(f"Reading cost estimate: {ce_id}")
64+
ce = client.cost_estimates.read(ce_id)
65+
print(f"ID: {ce.id}")
66+
print(f"Status: {ce.status}")
67+
print(f"Resources: {ce.resources_count} "
68+
f"(matched={ce.matched_resources_count}, "
69+
f"unmatched={ce.unmatched_resources_count})")
70+
print(f"Prior monthly cost: {ce.prior_monthly_cost}")
71+
print(f"Proposed monthly cost: {ce.proposed_monthly_cost}")
72+
print(f"Delta monthly cost: {ce.delta_monthly_cost}")
73+
if ce.error_message:
74+
print(f"Error: {ce.error_message}")
75+
76+
if args.logs:
77+
_print_header(f"Cost estimate logs: {ce_id}")
78+
print(client.cost_estimates.logs(ce_id) or "(no log output yet)")
79+
80+
return 0
81+
82+
83+
if __name__ == "__main__":
84+
raise SystemExit(main())

examples/ip_ranges.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Copyright IBM Corp. 2025, 2026
2+
# SPDX-License-Identifier: MPL-2.0
3+
4+
from __future__ import annotations
5+
6+
import argparse
7+
import os
8+
from datetime import datetime
9+
10+
from pytfe import TFEClient, TFEConfig
11+
12+
13+
def _print_header(title: str):
14+
print("\n" + "=" * 80)
15+
print(title)
16+
print("=" * 80)
17+
18+
19+
def main():
20+
parser = argparse.ArgumentParser(description="IP ranges demo for python-tfe SDK")
21+
parser.add_argument(
22+
"--address", default=os.getenv("TFE_ADDRESS", "https://app.terraform.io")
23+
)
24+
parser.add_argument("--token", default=os.getenv("TFE_TOKEN", ""))
25+
parser.add_argument(
26+
"--modified-since",
27+
help="ISO-8601 timestamp; only fetch ranges changed since then "
28+
"(e.g. 2020-05-26T15:10:05).",
29+
)
30+
args = parser.parse_args()
31+
32+
# The IP ranges endpoint does not require authentication.
33+
cfg = TFEConfig(address=args.address, token=args.token)
34+
client = TFEClient(cfg)
35+
36+
modified_since = (
37+
datetime.fromisoformat(args.modified_since) if args.modified_since else None
38+
)
39+
40+
_print_header("Reading HCP Terraform / TFE IP ranges")
41+
ranges = client.ip_ranges.read(modified_since=modified_since)
42+
43+
if ranges is None:
44+
print("Not modified since the supplied date.")
45+
return
46+
47+
for name, cidrs in (
48+
("API", ranges.api),
49+
("Notifications", ranges.notifications),
50+
("Sentinel", ranges.sentinel),
51+
("VCS", ranges.vcs),
52+
):
53+
print(f"\n{name} ({len(cidrs)} ranges):")
54+
for cidr in cidrs:
55+
print(f" - {cidr}")
56+
57+
58+
if __name__ == "__main__":
59+
main()

0 commit comments

Comments
 (0)