diff --git a/Makefile b/Makefile index 731afd8..8eb5654 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: help fmt fmt-check lint check test install dev-install type-check clean all venv activate PYTHON := python3 -SRC_DIR := src/tfe +SRC_DIR := src/pytfe TEST_DIR := tests VENV := .venv VENV_PYTHON := $(VENV)/bin/python diff --git a/README.md b/README.md index 67d9700..1c0d9e7 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![PyPI](https://img.shields.io/pypi/v/pytfe.svg)](https://pypi.org/project/pytfe/) [![Python Versions](https://img.shields.io/pypi/pyversions/pytfe.svg)](https://pypi.org/project/pytfe/) -[![CI](https://github.com/hashicorp/python-tfe/actions/workflows/ci.yml/badge.svg)](https://github.com/hashicorp/python-tfe/actions/workflows/ci.yml) +[![Test](https://github.com/hashicorp/python-tfe/actions/workflows/test.yml/badge.svg)](https://github.com/hashicorp/python-tfe/actions/workflows/test.yml) [![License](https://img.shields.io/github/license/hashicorp/python-tfe.svg)](./LICENSE) [![Issues](https://img.shields.io/github/issues/hashicorp/python-tfe.svg)](https://github.com/hashicorp/python-tfe/issues) diff --git a/examples/agent.py b/examples/agent.py index 01daf88..d756abf 100644 --- a/examples/agent.py +++ b/examples/agent.py @@ -17,15 +17,15 @@ Usage: export TFE_TOKEN="your-token-here" export TFE_ORG="your-organization" - python examples/agent_simple.py + python examples/agent.py """ import os -from tfe.client import TFEClient -from tfe.config import TFEConfig -from tfe.errors import NotFound -from tfe.models.agent import AgentListOptions +from pytfe.client import TFEClient +from pytfe.config import TFEConfig +from pytfe.errors import NotFound +from pytfe.models import AgentListOptions def main(): @@ -45,7 +45,7 @@ def main(): # Create TFE client config = TFEConfig(token=token, address=address) - client = TFEClient(config=config) + client = TFEClient(config) print(f"🔗 Connected to: {address}") print(f"🏢 Organization: {org}") diff --git a/examples/agent_pool.py b/examples/agent_pool.py index d9a37df..1b6e15b 100644 --- a/examples/agent_pool.py +++ b/examples/agent_pool.py @@ -14,15 +14,15 @@ Usage: export TFE_TOKEN="your-token-here" export TFE_ORG="your-organization" - python examples/agent_pool_simple.py + python examples/agent_pool.py """ import os import uuid -from tfe import TFEClient, TFEConfig -from tfe.errors import NotFound -from tfe.models.agent import ( +from pytfe import TFEClient, TFEConfig +from pytfe.errors import NotFound +from pytfe.models import ( AgentPoolAllowedWorkspacePolicy, AgentPoolCreateOptions, AgentPoolListOptions, diff --git a/examples/apply.py b/examples/apply.py index 218697d..8fb9780 100644 --- a/examples/apply.py +++ b/examples/apply.py @@ -3,7 +3,7 @@ import argparse import os -from tfe import TFEClient, TFEConfig +from pytfe import TFEClient, TFEConfig def _print_header(title: str): diff --git a/examples/configuration_version_complete_test.py b/examples/configuration_version.py similarity index 99% rename from examples/configuration_version_complete_test.py rename to examples/configuration_version.py index 3785b44..e983e94 100644 --- a/examples/configuration_version_complete_test.py +++ b/examples/configuration_version.py @@ -2,7 +2,7 @@ """ Complete Configuration Version Testing Suite -This file contains individual tests for all 12 configuration version functions implemented in src/tfe/resources/configuration_version.py: +This file contains individual tests for all 12 configuration version functions: CONFIGURATION VERSION FUNCTIONS AVAILABLE FOR TESTING: 1. list() - List configuration versions for a workspace @@ -35,8 +35,8 @@ # Add the src directory to the path sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) -from tfe import TFEClient, TFEConfig -from tfe.models import ( +from pytfe import TFEClient, TFEConfig +from pytfe.models import ( ConfigurationVersionCreateOptions, ConfigurationVersionListOptions, ConfigurationVersionReadOptions, diff --git a/examples/oauth_client_complete_test.py b/examples/oauth_client.py similarity index 99% rename from examples/oauth_client_complete_test.py rename to examples/oauth_client.py index d09867a..96671dd 100644 --- a/examples/oauth_client_complete_test.py +++ b/examples/oauth_client.py @@ -2,7 +2,7 @@ """ Complete OAuth Client Testing Suite -This file contains individual tests for all 8 OAuth client functions implemented in src/tfe/resources/oauth_client.py: +This file contains individual tests for all 8 OAuth client functions: PUBLIC FUNCTIONS AVAILABLE FOR TESTING: 1. list() - List all OAuth clients for an organization @@ -35,9 +35,9 @@ # Add the src directory to the path sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) -from tfe import TFEClient, TFEConfig -from tfe.errors import NotFound -from tfe.models.oauth_client import ( +from pytfe import TFEClient, TFEConfig +from pytfe.errors import NotFound +from pytfe.models import ( OAuthClientAddProjectsOptions, OAuthClientCreateOptions, OAuthClientIncludeOpt, diff --git a/examples/oauth_token_complete_test.py b/examples/oauth_token.py similarity index 97% rename from examples/oauth_token_complete_test.py rename to examples/oauth_token.py index fba05b8..725fb59 100644 --- a/examples/oauth_token_complete_test.py +++ b/examples/oauth_token.py @@ -2,7 +2,7 @@ """ Complete OAuth Token Testing Suite -This file contains individual tests for all 4 OAuth token functions implemented in src/tfe/resources/oauth_token.py: +This file contains individual tests for all 4 OAuth token functions: FUNCTIONS AVAILABLE FOR TESTING: 1. list() - List OAuth tokens for an organization @@ -28,9 +28,9 @@ # Add the src directory to the path sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) -from tfe import TFEClient, TFEConfig -from tfe.errors import NotFound -from tfe.models.oauth_token import OAuthTokenListOptions, OAuthTokenUpdateOptions +from pytfe import TFEClient, TFEConfig +from pytfe.errors import NotFound +from pytfe.models import OAuthTokenListOptions, OAuthTokenUpdateOptions def main(): diff --git a/examples/org.py b/examples/org.py index 7636450..93b8f0f 100644 --- a/examples/org.py +++ b/examples/org.py @@ -1,5 +1,5 @@ -from tfe import TFEClient, TFEConfig -from tfe.types import ( +from pytfe import TFEClient, TFEConfig +from pytfe.models import ( DataRetentionPolicyDeleteOlderSetOptions, DataRetentionPolicyDontDeleteSetOptions, OrganizationCreateOptions, diff --git a/examples/plan.py b/examples/plan.py index ec36269..41b1e77 100644 --- a/examples/plan.py +++ b/examples/plan.py @@ -4,7 +4,7 @@ import json import os -from tfe import TFEClient, TFEConfig +from pytfe import TFEClient, TFEConfig def _print_header(title: str): diff --git a/examples/policy.py b/examples/policy.py index 55cfcf7..d196597 100644 --- a/examples/policy.py +++ b/examples/policy.py @@ -22,14 +22,14 @@ import os from pathlib import Path -from tfe import TFEClient, TFEConfig -from tfe.models.policy import ( +from pytfe import TFEClient, TFEConfig +from pytfe.models import ( EnforcementLevel, PolicyCreateOptions, + PolicyKind, PolicyListOptions, PolicyUpdateOptions, ) -from tfe.models.policy_set import PolicyKind def _print_header(title: str): diff --git a/examples/policy_check.py b/examples/policy_check.py index f47a180..771a576 100644 --- a/examples/policy_check.py +++ b/examples/policy_check.py @@ -3,8 +3,8 @@ import argparse import os -from tfe import TFEClient, TFEConfig -from tfe.models.policy_check import PolicyCheckListOptions +from pytfe import TFEClient, TFEConfig +from pytfe.models import PolicyCheckListOptions def _print_header(title: str): diff --git a/examples/project.py b/examples/project.py index 3866365..6999e4f 100644 --- a/examples/project.py +++ b/examples/project.py @@ -1,7 +1,7 @@ """ Comprehensive Integration Test for python-tfe Projects CRUD Operations -This file tests all CRUD operations from src/tfe/resources/projects.py: +This file tests all CRUD operations: - List: Get all projects in an organization - Create: Add new projects with validation - Read: Get specific project details @@ -29,16 +29,16 @@ import pytest -from tfe._http import HTTPTransport -from tfe.config import TFEConfig -from tfe.resources.projects import Projects -from tfe.types import ( +from pytfe._http import HTTPTransport +from pytfe.config import TFEConfig +from pytfe.models import ( ProjectAddTagBindingsOptions, ProjectCreateOptions, ProjectListOptions, ProjectUpdateOptions, TagBinding, ) +from pytfe.resources.projects import Projects @pytest.fixture diff --git a/examples/query_run.py b/examples/query_run.py index 0b3e4e6..610caa7 100644 --- a/examples/query_run.py +++ b/examples/query_run.py @@ -30,8 +30,8 @@ import time from datetime import datetime -from tfe import TFEClient, TFEConfig -from tfe.models.query_run import ( +from pytfe import TFEClient, TFEConfig +from pytfe.models import ( QueryRunCancelOptions, QueryRunCreateOptions, QueryRunForceCancelOptions, diff --git a/examples/registry_module_complete_test.py b/examples/registry_module.py similarity index 99% rename from examples/registry_module_complete_test.py rename to examples/registry_module.py index a6eddd9..0be3b99 100644 --- a/examples/registry_module_complete_test.py +++ b/examples/registry_module.py @@ -2,7 +2,7 @@ """ Complete Registry Module Testing Suite -This file contains individual tests for all 15 registry module functions implemented in src/tfe/resources/registry_module.py: +This file contains individual tests for all 15 registry module functions: PUBLIC FUNCTIONS AVAILABLE FOR TESTING: 1. list() - List registry modules in organization @@ -39,9 +39,9 @@ # Add the src directory to the path sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) -from tfe import TFEClient, TFEConfig -from tfe.errors import NotFound -from tfe.models.registry_module_types import ( +from pytfe import TFEClient, TFEConfig +from pytfe.errors import NotFound +from pytfe.models import ( AgentExecutionMode, RegistryModuleCreateOptions, RegistryModuleCreateVersionOptions, diff --git a/examples/registry_provider_individual.py b/examples/registry_provider.py similarity index 99% rename from examples/registry_provider_individual.py rename to examples/registry_provider.py index bf26faf..bcd3e32 100644 --- a/examples/registry_provider_individual.py +++ b/examples/registry_provider.py @@ -23,8 +23,8 @@ # Add the src directory to the path sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) -from tfe import TFEClient -from tfe.models.registry_provider_types import ( +from pytfe import TFEClient +from pytfe.models import ( RegistryName, RegistryProviderCreateOptions, RegistryProviderID, diff --git a/examples/reserved_tag_key.py b/examples/reserved_tag_key.py index 1c0d1ea..8eeb5e7 100644 --- a/examples/reserved_tag_key.py +++ b/examples/reserved_tag_key.py @@ -18,9 +18,9 @@ # Add the source directory to the path for direct execution sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) -from tfe import TFEClient, TFEConfig -from tfe.errors import TFEError -from tfe.models import ( +from pytfe import TFEClient, TFEConfig +from pytfe.errors import TFEError +from pytfe.models import ( ReservedTagKeyCreateOptions, ReservedTagKeyListOptions, ReservedTagKeyUpdateOptions, diff --git a/examples/run.py b/examples/run.py index 3dd588e..c4760ac 100644 --- a/examples/run.py +++ b/examples/run.py @@ -4,8 +4,8 @@ import os from datetime import datetime -from tfe import TFEClient, TFEConfig -from tfe.models.run import ( +from pytfe import TFEClient, TFEConfig +from pytfe.models import ( RunCreateOptions, RunIncludeOpt, RunListForOrganizationOptions, @@ -141,7 +141,7 @@ def main(): workspace_data = client.workspaces.read_by_id(args.workspace_id) # Create the workspace object that run models expect - from tfe.models.workspace import Workspace + from pytfe.models.workspace import Workspace workspace = Workspace( id=workspace_data.id, diff --git a/examples/run_events.py b/examples/run_events.py index e075f8f..75033c2 100644 --- a/examples/run_events.py +++ b/examples/run_events.py @@ -37,8 +37,8 @@ import argparse import os -from tfe import TFEClient, TFEConfig -from tfe.models.run_event import ( +from pytfe import TFEClient, TFEConfig +from pytfe.models import ( RunEventIncludeOpt, RunEventListOptions, RunEventReadOptions, diff --git a/examples/run_task.py b/examples/run_task.py index 0418564..5f27a5e 100644 --- a/examples/run_task.py +++ b/examples/run_task.py @@ -1,8 +1,8 @@ import time import traceback -from tfe import TFEClient, TFEConfig -from tfe.models.run_task import ( +from pytfe import TFEClient, TFEConfig +from pytfe.models import ( RunTaskCreateOptions, RunTaskIncludeOptions, RunTaskListOptions, diff --git a/examples/run_trigger.py b/examples/run_trigger.py index e0eec19..fb2a44f 100644 --- a/examples/run_trigger.py +++ b/examples/run_trigger.py @@ -1,14 +1,14 @@ import time import traceback -from tfe import TFEClient, TFEConfig -from tfe.models.run_trigger import ( +from pytfe import TFEClient, TFEConfig +from pytfe.models import ( RunTriggerCreateOptions, RunTriggerFilterOp, RunTriggerIncludeOp, RunTriggerListOptions, + Workspace, ) -from tfe.models.workspace import Workspace def run_trigger_list(client, workspace_id): diff --git a/examples/ssh_keys.py b/examples/ssh_keys.py index 0dc6693..7d2efa3 100644 --- a/examples/ssh_keys.py +++ b/examples/ssh_keys.py @@ -28,9 +28,9 @@ # Add the source directory to the path for direct execution sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) -from tfe import TFEClient, TFEConfig -from tfe.errors import NotFound, TFEError -from tfe.models import SSHKeyCreateOptions, SSHKeyListOptions, SSHKeyUpdateOptions +from pytfe import TFEClient, TFEConfig +from pytfe.errors import NotFound, TFEError +from pytfe.models import SSHKeyCreateOptions, SSHKeyListOptions, SSHKeyUpdateOptions # Configuration TFE_TOKEN = os.getenv("TFE_TOKEN") diff --git a/examples/state_versions.py b/examples/state_versions.py index 581b0e8..6d3f8b8 100644 --- a/examples/state_versions.py +++ b/examples/state_versions.py @@ -4,14 +4,14 @@ import os from pathlib import Path -from tfe import TFEClient, TFEConfig -from tfe.errors import ErrStateVersionUploadNotSupported -from tfe.models.state_version import ( +from pytfe import TFEClient, TFEConfig +from pytfe.errors import ErrStateVersionUploadNotSupported +from pytfe.models import ( StateVersionCreateOptions, StateVersionCurrentOptions, StateVersionListOptions, + StateVersionOutputsListOptions, ) -from tfe.models.state_version_output import StateVersionOutputsListOptions def _print_header(title: str): diff --git a/examples/variable_sets_example.py b/examples/variable_sets.py similarity index 99% rename from examples/variable_sets_example.py rename to examples/variable_sets.py index df62102..85749f9 100644 --- a/examples/variable_sets_example.py +++ b/examples/variable_sets.py @@ -15,8 +15,7 @@ import os -from tfe import TFEClient, TFEConfig -from tfe.types import ( +from pytfe.types import ( CategoryType, Parent, Project, @@ -34,6 +33,8 @@ Workspace, ) +from pytfe import TFEClient, TFEConfig + def variable_set_example(): """Demonstrate Variable Set operations.""" @@ -192,7 +193,7 @@ def variable_set_example(): print("7. Workspace operations example...") try: # List some workspaces first - from tfe.types import WorkspaceListOptions + from pytfe.types import WorkspaceListOptions workspace_options = WorkspaceListOptions(page_size=5) workspaces = list( @@ -271,7 +272,7 @@ def variable_set_example(): # 9. Read the variable set with includes print("9. Reading variable set with includes...") - from tfe.types import VariableSetReadOptions + from pytfe.types import VariableSetReadOptions read_options = VariableSetReadOptions( include=[VariableSetIncludeOpt.VARS, VariableSetIncludeOpt.WORKSPACES] diff --git a/examples/variables.py b/examples/variables.py index b3987d9..bc5227e 100644 --- a/examples/variables.py +++ b/examples/variables.py @@ -11,8 +11,8 @@ # Add the src directory to the path sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) -from tfe import TFEClient, TFEConfig -from tfe.types import CategoryType, VariableCreateOptions, VariableUpdateOptions +from pytfe import TFEClient, TFEConfig +from pytfe.models import CategoryType, VariableCreateOptions, VariableUpdateOptions def main(): diff --git a/examples/workspace.py b/examples/workspace.py index d5c67c5..dcea019 100644 --- a/examples/workspace.py +++ b/examples/workspace.py @@ -22,8 +22,8 @@ # Add the source directory to the path for direct execution sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) -from tfe import TFEClient, TFEConfig -from tfe.types import ( +from pytfe import TFEClient, TFEConfig +from pytfe.models import ( DataRetentionPolicyDeleteOlderSetOptions, DataRetentionPolicyDontDeleteSetOptions, ExecutionMode, @@ -386,7 +386,7 @@ def demo_remote_state_consumer_operations( print("\n Getting real workspaces for consumer demonstration...") # Get existing workspaces from the organization to use as examples - from tfe.types import WorkspaceListOptions + from pytfe.types import WorkspaceListOptions org_list_options = WorkspaceListOptions(page_size=5) @@ -434,7 +434,7 @@ def demo_remote_state_consumer_operations( ) # Create mock workspaces for demonstration only - from tfe.types import Workspace + from pytfe.types import Workspace demo_consumer_1 = Workspace( id="ws-demo-consumer-1", diff --git a/pyproject.toml b/pyproject.toml index 1b11eeb..31f289f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,8 +3,8 @@ requires = ["hatchling>=1.21"] build-backend = "hatchling.build" [project] -name = "tfe" -version = "0.1.0a1" +name = "pytfe" +version = "0.0.1-alpha" description = "Official Python SDK for HashiCorp Terraform Cloud / Terraform Enterprise (TFE) API v2" readme = "README.md" license = { text = "MPL-2.0" } @@ -14,7 +14,6 @@ dependencies = [ "httpx>=0.27.0,<0.29.0", "pydantic>=2.6,<3", "typing-extensions>=4.8", - "anyio>=4.0", "h2>=4.3.0" ] classifiers = [ @@ -42,7 +41,7 @@ dev = [ "Bug Tracker" = "https://github.com/hashicorp/python-tfe/issues" [tool.hatch.build.targets.wheel] -packages = ["src/tfe"] +packages = ["src/pytfe"] # Ruff configuration [tool.ruff] @@ -114,4 +113,7 @@ show_error_codes = true [[tool.mypy.overrides]] module = "tests.*" -disallow_untyped_defs = false \ No newline at end of file +disallow_untyped_defs = false + +[tool.hatch.build.targets.sdist] +include = ["src/**", "README.md", "LICENSE", "docs/**"] \ No newline at end of file diff --git a/src/pytfe/__init__.py b/src/pytfe/__init__.py new file mode 100644 index 0000000..bc7be16 --- /dev/null +++ b/src/pytfe/__init__.py @@ -0,0 +1,5 @@ +from . import errors, models +from .client import TFEClient +from .config import TFEConfig + +__all__ = ["TFEConfig", "TFEClient", "errors", "models"] diff --git a/src/tfe/_http.py b/src/pytfe/_http.py similarity index 71% rename from src/tfe/_http.py rename to src/pytfe/_http.py index 9c51efd..2c32012 100644 --- a/src/tfe/_http.py +++ b/src/pytfe/_http.py @@ -6,7 +6,6 @@ from typing import Any from urllib.parse import urljoin -import anyio import httpx from ._jsonapi import build_headers, parse_error_payload @@ -37,7 +36,7 @@ def __init__( backoff_cap: float, backoff_jitter: bool, http2: bool, - proxies: dict | None, + proxies: str | None, ca_bundle: str | None, ): self.base = address.rstrip("/") @@ -54,11 +53,11 @@ def __init__( self.proxies = proxies self.ca_bundle = ca_bundle self._sync = httpx.Client( - http2=http2, timeout=timeout, verify=ca_bundle or verify_tls - ) # proxies=proxies - self._async = httpx.AsyncClient( - http2=http2, timeout=timeout, verify=ca_bundle or verify_tls - ) # proxies=proxies + http2=http2, + timeout=timeout, + verify=ca_bundle or verify_tls, + proxy=proxies, + ) def _build_url(self, path: str) -> str: # IMPORTANT: don't prefix absolute URLs (hosted_state, signed blobs, etc.) @@ -109,46 +108,6 @@ def request( self._raise_if_error(resp) return resp - async def arequest( - self, - method: str, - path: str, - *, - params: Mapping[str, Any] | None = None, - json_body: Mapping[str, Any] | None = None, - data: bytes | None = None, - headers: dict[str, str] | None = None, - allow_redirects: bool = True, - ) -> httpx.Response: - url = f"{self.base}{path}" - hdrs = dict(self.headers) - hdrs.update(headers or {}) - attempt = 0 - while True: - try: - resp = await self._async.request( - method, - url, - params=params, - json=json_body, - content=data, - headers=hdrs, - follow_redirects=allow_redirects, - ) - except httpx.HTTPError as e: - if attempt >= self.max_retries: - raise ServerError(str(e)) from e - await self._asleep(attempt, None) - attempt += 1 - continue - if resp.status_code in _RETRY_STATUSES and attempt < self.max_retries: - retry_after = _parse_retry_after(resp) - await self._asleep(attempt, retry_after) - attempt += 1 - continue - self._raise_if_error(resp) - return resp - def _sleep(self, attempt: int, retry_after: float | None) -> None: if retry_after is not None: time.sleep(retry_after) @@ -156,13 +115,6 @@ def _sleep(self, attempt: int, retry_after: float | None) -> None: delay = min(self.backoff_cap, self.backoff_base * (2**attempt)) time.sleep(delay) - async def _asleep(self, attempt: int, retry_after: float | None) -> None: - if retry_after is not None: - await anyio.sleep(retry_after) - return - delay = min(self.backoff_cap, self.backoff_base * (2**attempt)) - await anyio.sleep(delay) - def _raise_if_error(self, resp: httpx.Response) -> None: status = resp.status_code diff --git a/src/tfe/_jsonapi.py b/src/pytfe/_jsonapi.py similarity index 95% rename from src/tfe/_jsonapi.py rename to src/pytfe/_jsonapi.py index aa0e426..3a12f60 100644 --- a/src/tfe/_jsonapi.py +++ b/src/pytfe/_jsonapi.py @@ -4,7 +4,7 @@ def build_headers(user_agent_suffix: str | None = None) -> dict[str, str]: - ua = "python-tfe/0.1" + ua = "pytfe/0.1" if user_agent_suffix: ua = f"{ua} {user_agent_suffix}" return { diff --git a/src/tfe/client.py b/src/pytfe/client.py similarity index 97% rename from src/tfe/client.py rename to src/pytfe/client.py index 5c7be10..aca6c6d 100644 --- a/src/tfe/client.py +++ b/src/pytfe/client.py @@ -84,4 +84,7 @@ def __init__(self, config: TFEConfig | None = None): self.reserved_tag_key = ReservedTagKey(self._transport) def close(self) -> None: - pass + try: + self._transport._sync.close() + except Exception: + pass diff --git a/src/tfe/config.py b/src/pytfe/config.py similarity index 95% rename from src/tfe/config.py rename to src/pytfe/config.py index 6a70e34..e4c3da3 100644 --- a/src/tfe/config.py +++ b/src/pytfe/config.py @@ -22,7 +22,7 @@ class TFEConfig(BaseModel): backoff_cap: float = 8.0 backoff_jitter: bool = True http2: bool = True - proxies: dict[str, str] | None = None + proxies: str | None = None ca_bundle: str | None = os.getenv("SSL_CERT_FILE", None) @classmethod diff --git a/src/tfe/errors.py b/src/pytfe/errors.py similarity index 100% rename from src/tfe/errors.py rename to src/pytfe/errors.py diff --git a/src/tfe/models/__init__.py b/src/pytfe/models/__init__.py similarity index 62% rename from src/tfe/models/__init__.py rename to src/pytfe/models/__init__.py index 996b8f8..cc79a7f 100644 --- a/src/tfe/models/__init__.py +++ b/src/pytfe/models/__init__.py @@ -1,10 +1,6 @@ -"""Types package for TFE client.""" +from __future__ import annotations -# Import all types from the main types module by using importlib to avoid circular imports -import importlib.util -import os - -# Re-export all agent and agent pool types +# ── Agent & Agent Pools ──────────────────────────────────────────────────────── from .agent import ( Agent, AgentListOptions, @@ -23,8 +19,20 @@ AgentTokenListOptions, ) -# Re-export all configuration version types -from .configuration_version_types import ( +# ── Core models split out of old types.py ───────────────────────────────────── +# Adjust these imports to match where you placed them during the split. +# Common / pagination / enums +from .common import ( + EffectiveTagBinding, + Pagination, + Tag, + TagBinding, + TagList, +) # if you put ExecutionMode enum here + +# ── Configuration Versions ──────────────────────────────────────────────────── +# (Old: .configuration_version_types) → import directly from real module +from .configuration_version import ( ConfigurationSource, ConfigurationStatus, ConfigurationVersion, @@ -37,7 +45,18 @@ IngressAttributes, ) -# Re-export all OAuth client types +# Data retention policy family +from .data_retention_policy import ( + DataRetentionPolicy, + DataRetentionPolicyChoice, + DataRetentionPolicyDeleteOlder, + DataRetentionPolicyDeleteOlderSetOptions, + DataRetentionPolicyDontDelete, + DataRetentionPolicyDontDeleteSetOptions, + DataRetentionPolicySetOptions, +) + +# ── OAuth ───────────────────────────────────────────────────────────────────── from .oauth_client import ( OAuthClient, OAuthClientAddProjectsOptions, @@ -50,8 +69,6 @@ OAuthClientUpdateOptions, ServiceProviderType, ) - -# Re-export all OAuth token types from .oauth_token import ( OAuthToken, OAuthTokenList, @@ -59,7 +76,19 @@ OAuthTokenUpdateOptions, ) -# Re-export all query run types +# Organization / Project +from .organization import ( + Entitlements, + ExecutionMode, + Organization, + OrganizationCreateOptions, + OrganizationUpdateOptions, + ReadRunQueueOptions, + RunQueue, +) +from .project import Project + +# ── Query Runs ──────────────────────────────────────────────────────────────── from .query_run import ( QueryRun, QueryRunCancelOptions, @@ -74,8 +103,9 @@ QueryRunType, ) -# Re-export all registry module types -from .registry_module_types import ( +# ── Registry Modules / Providers ────────────────────────────────────────────── +# (Old: .registry_module_types / .registry_provider_types) → import from real modules +from .registry_module import ( AgentExecutionMode, Commit, CommitList, @@ -106,9 +136,7 @@ TerraformRegistryModule, TestConfig, ) - -# Re-export all registry provider types -from .registry_provider_types import ( +from .registry_provider import ( RegistryProvider, RegistryProviderCreateOptions, RegistryProviderID, @@ -119,7 +147,7 @@ RegistryProviderReadOptions, ) -# Re-export all reserved tag key types +# ── Reserved Tag Keys ───────────────────────────────────────────────────────── from .reserved_tag_key import ( ReservedTagKey, ReservedTagKeyCreateOptions, @@ -128,7 +156,13 @@ ReservedTagKeyUpdateOptions, ) -# Re-export all SSH key types +# Runs +from .run import ( + Run, + RunStatus, +) + +# ── SSH Keys ────────────────────────────────────────────────────────────────── from .ssh_key import ( SSHKey, SSHKeyCreateOptions, @@ -137,9 +171,46 @@ SSHKeyUpdateOptions, ) -# Define what should be available when importing with * +# Variables +from .variable import ( + Variable, + VariableCreateOptions, + VariableListOptions, + VariableUpdateOptions, +) + +# Workspaces +from .workspace import ( + LockedByChoice, + VCSRepo, + Workspace, + WorkspaceActions, + WorkspaceAddRemoteStateConsumersOptions, + WorkspaceAddTagBindingsOptions, + WorkspaceAddTagsOptions, + WorkspaceAssignSSHKeyOptions, + WorkspaceCreateOptions, + WorkspaceIncludeOpt, + WorkspaceList, + WorkspaceListOptions, + WorkspaceListRemoteStateConsumersOptions, + WorkspaceLockOptions, + WorkspaceOutputs, + WorkspacePermissions, + WorkspaceReadOptions, + WorkspaceRemoveRemoteStateConsumersOptions, + WorkspaceRemoveTagsOptions, + WorkspaceRemoveVCSConnectionOptions, + WorkspaceSettingOverwrites, + WorkspaceSource, + WorkspaceTagListOptions, + WorkspaceUpdateOptions, + WorkspaceUpdateRemoteStateConsumersOptions, +) + +# ── Public surface ──────────────────────────────────────────────────────────── __all__ = [ - # OAuth client types + # OAuth "OAuthClient", "OAuthClientAddProjectsOptions", "OAuthClientCreateOptions", @@ -150,24 +221,24 @@ "OAuthClientRemoveProjectsOptions", "OAuthClientUpdateOptions", "ServiceProviderType", - # OAuth token types + # OAuth token "OAuthToken", "OAuthTokenList", "OAuthTokenListOptions", "OAuthTokenUpdateOptions", - # SSH key types + # SSH keys "SSHKey", "SSHKeyCreateOptions", "SSHKeyList", "SSHKeyListOptions", "SSHKeyUpdateOptions", - # Reserved tag key types + # Reserved tag keys "ReservedTagKey", "ReservedTagKeyCreateOptions", "ReservedTagKeyList", "ReservedTagKeyListOptions", "ReservedTagKeyUpdateOptions", - # Agent and agent pool types + # Agent & pools "Agent", "AgentPool", "AgentPoolAllowedWorkspacePolicy", @@ -183,7 +254,7 @@ "AgentToken", "AgentTokenCreateOptions", "AgentTokenListOptions", - # Configuration version types + # Configuration versions "ConfigurationSource", "ConfigurationStatus", "ConfigurationVersion", @@ -194,7 +265,7 @@ "ConfigurationVersionUpload", "ConfigVerIncludeOpt", "IngressAttributes", - # Registry module types + # Registry modules "AgentExecutionMode", "Commit", "CommitList", @@ -224,7 +295,7 @@ "Root", "TestConfig", "TerraformRegistryModule", - # Registry provider types + # Registry providers "RegistryProvider", "RegistryProviderCreateOptions", "RegistryProviderID", @@ -233,7 +304,7 @@ "RegistryProviderListOptions", "RegistryProviderPermissions", "RegistryProviderReadOptions", - # Query run types + # Query runs "QueryRun", "QueryRunCancelOptions", "QueryRunCreateOptions", @@ -245,8 +316,14 @@ "QueryRunResults", "QueryRunStatus", "QueryRunType", - # Main types from types.py (will be dynamically added below) - "Capacity", + # Core (from old types.py, now split) + "Entitlements", + "ExecutionMode", + "Pagination", + "Organization", + "OrganizationCreateOptions", + "OrganizationUpdateOptions", + "Project", "DataRetentionPolicy", "DataRetentionPolicyChoice", "DataRetentionPolicyDeleteOlder", @@ -255,18 +332,6 @@ "DataRetentionPolicyDontDeleteSetOptions", "DataRetentionPolicySetOptions", "EffectiveTagBinding", - "Entitlements", - "ExecutionMode", - "LockedByChoice", - "Organization", - "OrganizationCreateOptions", - "OrganizationUpdateOptions", - "Pagination", - "Project", - "ReadRunQueueOptions", - "Run", - "RunQueue", - "RunStatus", "Tag", "TagBinding", "TagList", @@ -274,6 +339,7 @@ "VariableCreateOptions", "VariableListOptions", "VariableUpdateOptions", + "LockedByChoice", "VCSRepo", "Workspace", "WorkspaceActions", @@ -298,16 +364,8 @@ "WorkspaceTagListOptions", "WorkspaceUpdateOptions", "WorkspaceUpdateRemoteStateConsumersOptions", + "Run", + "RunQueue", + "RunStatus", + "ReadRunQueueOptions", ] - -# Load the main types.py file that's at the same level as this types/ directory -types_py_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "types.py") -spec = importlib.util.spec_from_file_location("main_types", types_py_path) -if spec is not None and spec.loader is not None: - main_types = importlib.util.module_from_spec(spec) - spec.loader.exec_module(main_types) - - # Re-export all main types - for name in dir(main_types): - if not name.startswith("_"): - globals()[name] = getattr(main_types, name) diff --git a/src/tfe/models/agent.py b/src/pytfe/models/agent.py similarity index 100% rename from src/tfe/models/agent.py rename to src/pytfe/models/agent.py diff --git a/src/tfe/models/apply.py b/src/pytfe/models/apply.py similarity index 100% rename from src/tfe/models/apply.py rename to src/pytfe/models/apply.py diff --git a/src/tfe/models/comment.py b/src/pytfe/models/comment.py similarity index 100% rename from src/tfe/models/comment.py rename to src/pytfe/models/comment.py diff --git a/src/pytfe/models/common.py b/src/pytfe/models/common.py new file mode 100644 index 0000000..a6dd569 --- /dev/null +++ b/src/pytfe/models/common.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +from typing import Any + +from pydantic import BaseModel, Field + + +class Pagination(BaseModel): + current_page: int + total_count: int + previous_page: int | None = None + next_page: int | None = None + total_pages: int | None = None + # Add other pagination fields as needed + + +class Tag(BaseModel): + id: str | None = None + name: str = "" + + +class TagBinding(BaseModel): + id: str | None = None + key: str + value: str | None = None + + +class TagList(BaseModel): + items: list[Tag] = Field(default_factory=list) + pagination: Pagination | None = None + + +class EffectiveTagBinding(BaseModel): + id: str + key: str + value: str | None = None + links: dict[str, Any] = Field(default_factory=dict) diff --git a/src/tfe/models/configuration_version_types.py b/src/pytfe/models/configuration_version.py similarity index 96% rename from src/tfe/models/configuration_version_types.py rename to src/pytfe/models/configuration_version.py index 4355dc7..6bc613a 100644 --- a/src/tfe/models/configuration_version_types.py +++ b/src/pytfe/models/configuration_version.py @@ -61,12 +61,12 @@ class ConfigurationVersion(BaseModel): """Configuration version model.""" id: str - auto_queue_runs: bool = Field(alias="auto-queue-runs") + auto_queue_runs: bool = Field(alias="auto-queue-runs", default=False) error: str | None = None error_message: str | None = Field(alias="error-message", default=None) - source: ConfigurationSource + source: ConfigurationSource | None = None speculative: bool = False - status: ConfigurationStatus + status: ConfigurationStatus | None = None status_timestamps: dict[str, str] | None = Field( alias="status-timestamps", default=None ) diff --git a/src/tfe/models/cost_estimate.py b/src/pytfe/models/cost_estimate.py similarity index 100% rename from src/tfe/models/cost_estimate.py rename to src/pytfe/models/cost_estimate.py diff --git a/src/pytfe/models/data_retention_policy.py b/src/pytfe/models/data_retention_policy.py new file mode 100644 index 0000000..6b6349f --- /dev/null +++ b/src/pytfe/models/data_retention_policy.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +from pydantic import BaseModel + + +class DataRetentionPolicy(BaseModel): + """Deprecated: Use DataRetentionPolicyDeleteOlder instead.""" + + id: str + delete_older_than_n_days: int + + +class DataRetentionPolicyDeleteOlder(BaseModel): + id: str + delete_older_than_n_days: int + + +class DataRetentionPolicyDontDelete(BaseModel): + id: str + + +class DataRetentionPolicyChoice(BaseModel): + """Polymorphic data retention policy choice.""" + + data_retention_policy: DataRetentionPolicy | None = None + data_retention_policy_delete_older: DataRetentionPolicyDeleteOlder | None = None + data_retention_policy_dont_delete: DataRetentionPolicyDontDelete | None = None + + def is_populated(self) -> bool: + """Returns whether one of the choices is populated.""" + return ( + self.data_retention_policy is not None + or self.data_retention_policy_delete_older is not None + or self.data_retention_policy_dont_delete is not None + ) + + def convert_to_legacy_struct(self) -> DataRetentionPolicy | None: + """Convert the DataRetentionPolicyChoice to the legacy DataRetentionPolicy struct.""" + if not self.is_populated(): + return None + + if self.data_retention_policy is not None: + return self.data_retention_policy + elif self.data_retention_policy_delete_older is not None: + return DataRetentionPolicy( + id=self.data_retention_policy_delete_older.id, + delete_older_than_n_days=self.data_retention_policy_delete_older.delete_older_than_n_days, + ) + return None + + +class DataRetentionPolicySetOptions(BaseModel): + """Deprecated: Use DataRetentionPolicyDeleteOlderSetOptions instead.""" + + delete_older_than_n_days: int + + +class DataRetentionPolicyDeleteOlderSetOptions(BaseModel): + delete_older_than_n_days: int + + +class DataRetentionPolicyDontDeleteSetOptions(BaseModel): + pass # No additional fields needed diff --git a/src/tfe/models/oauth_client.py b/src/pytfe/models/oauth_client.py similarity index 100% rename from src/tfe/models/oauth_client.py rename to src/pytfe/models/oauth_client.py diff --git a/src/tfe/models/oauth_token.py b/src/pytfe/models/oauth_token.py similarity index 100% rename from src/tfe/models/oauth_token.py rename to src/pytfe/models/oauth_token.py diff --git a/src/tfe/models/organization.py b/src/pytfe/models/organization.py similarity index 70% rename from src/tfe/models/organization.py rename to src/pytfe/models/organization.py index 8450ddf..21e40b2 100644 --- a/src/tfe/models/organization.py +++ b/src/pytfe/models/organization.py @@ -101,12 +101,6 @@ class Organization(BaseModel): data_retention_policy_choice: dict | None = None -class Project(BaseModel): - id: str - name: str - organization: str - - class Capacity(BaseModel): organization: str pending: int @@ -144,6 +138,7 @@ class Pagination(BaseModel): # Add other pagination fields as needed +# RunQueue represents the current run queue of an organization. class RunQueue(BaseModel): pagination: Pagination | None = None items: list[Run] = Field(default_factory=list) @@ -153,63 +148,3 @@ class ReadRunQueueOptions(BaseModel): # List options for pagination page_number: int | None = None page_size: int | None = None - - -class DataRetentionPolicy(BaseModel): - """Deprecated: Use DataRetentionPolicyDeleteOlder instead.""" - - id: str - delete_older_than_n_days: int - - -class DataRetentionPolicyDeleteOlder(BaseModel): - id: str - delete_older_than_n_days: int - - -class DataRetentionPolicyDontDelete(BaseModel): - id: str - - -class DataRetentionPolicyChoice(BaseModel): - """Polymorphic data retention policy choice.""" - - data_retention_policy: DataRetentionPolicy | None = None - data_retention_policy_delete_older: DataRetentionPolicyDeleteOlder | None = None - data_retention_policy_dont_delete: DataRetentionPolicyDontDelete | None = None - - def is_populated(self) -> bool: - """Returns whether one of the choices is populated.""" - return ( - self.data_retention_policy is not None - or self.data_retention_policy_delete_older is not None - or self.data_retention_policy_dont_delete is not None - ) - - def convert_to_legacy_struct(self) -> DataRetentionPolicy | None: - """Convert the DataRetentionPolicyChoice to the legacy DataRetentionPolicy struct.""" - if not self.is_populated(): - return None - - if self.data_retention_policy is not None: - return self.data_retention_policy - elif self.data_retention_policy_delete_older is not None: - return DataRetentionPolicy( - id=self.data_retention_policy_delete_older.id, - delete_older_than_n_days=self.data_retention_policy_delete_older.delete_older_than_n_days, - ) - return None - - -class DataRetentionPolicySetOptions(BaseModel): - """Deprecated: Use DataRetentionPolicyDeleteOlderSetOptions instead.""" - - delete_older_than_n_days: int - - -class DataRetentionPolicyDeleteOlderSetOptions(BaseModel): - delete_older_than_n_days: int - - -class DataRetentionPolicyDontDeleteSetOptions(BaseModel): - pass # No additional fields needed diff --git a/src/tfe/models/plan.py b/src/pytfe/models/plan.py similarity index 100% rename from src/tfe/models/plan.py rename to src/pytfe/models/plan.py diff --git a/src/tfe/models/plan_export.py b/src/pytfe/models/plan_export.py similarity index 100% rename from src/tfe/models/plan_export.py rename to src/pytfe/models/plan_export.py diff --git a/src/tfe/models/policy.py b/src/pytfe/models/policy.py similarity index 100% rename from src/tfe/models/policy.py rename to src/pytfe/models/policy.py diff --git a/src/tfe/models/policy_check.py b/src/pytfe/models/policy_check.py similarity index 100% rename from src/tfe/models/policy_check.py rename to src/pytfe/models/policy_check.py diff --git a/src/tfe/models/policy_set.py b/src/pytfe/models/policy_set.py similarity index 100% rename from src/tfe/models/policy_set.py rename to src/pytfe/models/policy_set.py diff --git a/src/pytfe/models/project.py b/src/pytfe/models/project.py new file mode 100644 index 0000000..3f4b9c6 --- /dev/null +++ b/src/pytfe/models/project.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +from pydantic import BaseModel, Field + +from .common import TagBinding + + +class Project(BaseModel): + id: str + name: str | None = None + description: str = "" + organization: str | None = None + created_at: str = "" + updated_at: str = "" + workspace_count: int = 0 + default_execution_mode: str = "remote" + + +class ProjectListOptions(BaseModel): + """Options for listing projects""" + + # Optional: String used to filter results by complete project name + name: str | None = None + # Optional: Query string to search projects by names + query: str | None = None + # Optional: Include related resources + include: list[str] | None = None + # Pagination options + page_number: int | None = None + page_size: int | None = None + + +class ProjectCreateOptions(BaseModel): + """Options for creating a project""" + + # Required: A name to identify the project + name: str + # Optional: A description for the project + description: str | None = None + + +class ProjectUpdateOptions(BaseModel): + """Options for updating a project""" + + # Optional: A name to identify the project + name: str | None = None + # Optional: A description for the project + description: str | None = None + + +class ProjectAddTagBindingsOptions(BaseModel): + """Options for adding tag bindings to a project""" + + tag_bindings: list[TagBinding] = Field(default_factory=list) diff --git a/src/tfe/models/query_run.py b/src/pytfe/models/query_run.py similarity index 100% rename from src/tfe/models/query_run.py rename to src/pytfe/models/query_run.py diff --git a/src/tfe/models/registry_module_types.py b/src/pytfe/models/registry_module.py similarity index 94% rename from src/tfe/models/registry_module_types.py rename to src/pytfe/models/registry_module.py index 3d20a88..d7c39be 100644 --- a/src/tfe/models/registry_module_types.py +++ b/src/pytfe/models/registry_module.py @@ -1,22 +1,10 @@ from __future__ import annotations -import importlib.util -import os from enum import Enum from typing import Any from pydantic import BaseModel, ConfigDict, Field -# Load the main types.py file to get Organization and other main types -# Path: from /src/tfe/types/registry_module_types.py to /src/tfe/types.py -types_py_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "types.py") -spec = importlib.util.spec_from_file_location("main_types", types_py_path) -if spec is not None and spec.loader is not None: - main_types = importlib.util.module_from_spec(spec) - spec.loader.exec_module(main_types) -else: - raise ImportError("Could not load main types module") - class RegistryName(str, Enum): """Registry name enum for public/private registries.""" diff --git a/src/tfe/models/registry_provider_types.py b/src/pytfe/models/registry_provider.py similarity index 100% rename from src/tfe/models/registry_provider_types.py rename to src/pytfe/models/registry_provider.py diff --git a/src/tfe/models/reserved_tag_key.py b/src/pytfe/models/reserved_tag_key.py similarity index 100% rename from src/tfe/models/reserved_tag_key.py rename to src/pytfe/models/reserved_tag_key.py diff --git a/src/tfe/models/run.py b/src/pytfe/models/run.py similarity index 100% rename from src/tfe/models/run.py rename to src/pytfe/models/run.py diff --git a/src/tfe/models/run_event.py b/src/pytfe/models/run_event.py similarity index 100% rename from src/tfe/models/run_event.py rename to src/pytfe/models/run_event.py diff --git a/src/tfe/models/run_task.py b/src/pytfe/models/run_task.py similarity index 98% rename from src/tfe/models/run_task.py rename to src/pytfe/models/run_task.py index 2fde05a..8741162 100644 --- a/src/tfe/models/run_task.py +++ b/src/pytfe/models/run_task.py @@ -4,7 +4,7 @@ from pydantic import BaseModel, Field -from ..types import Pagination +from ..models.common import Pagination from .agent import AgentPool from .organization import Organization from .workspace_run_task import WorkspaceRunTask diff --git a/src/tfe/models/run_trigger.py b/src/pytfe/models/run_trigger.py similarity index 96% rename from src/tfe/models/run_trigger.py rename to src/pytfe/models/run_trigger.py index bc3d55a..b9a4e74 100644 --- a/src/tfe/models/run_trigger.py +++ b/src/pytfe/models/run_trigger.py @@ -5,7 +5,7 @@ from pydantic import BaseModel, Field -from ..types import Pagination +from ..models.common import Pagination from .workspace import Workspace diff --git a/src/tfe/models/ssh_key.py b/src/pytfe/models/ssh_key.py similarity index 100% rename from src/tfe/models/ssh_key.py rename to src/pytfe/models/ssh_key.py diff --git a/src/tfe/models/state_version.py b/src/pytfe/models/state_version.py similarity index 100% rename from src/tfe/models/state_version.py rename to src/pytfe/models/state_version.py diff --git a/src/tfe/models/state_version_output.py b/src/pytfe/models/state_version_output.py similarity index 100% rename from src/tfe/models/state_version_output.py rename to src/pytfe/models/state_version_output.py diff --git a/src/tfe/models/task_stage.py b/src/pytfe/models/task_stage.py similarity index 100% rename from src/tfe/models/task_stage.py rename to src/pytfe/models/task_stage.py diff --git a/src/tfe/models/user.py b/src/pytfe/models/user.py similarity index 100% rename from src/tfe/models/user.py rename to src/pytfe/models/user.py diff --git a/src/pytfe/models/variable.py b/src/pytfe/models/variable.py new file mode 100644 index 0000000..e9a137a --- /dev/null +++ b/src/pytfe/models/variable.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +from enum import Enum + +from pydantic import BaseModel + + +class CategoryType(str, Enum): + ENV = "env" + POLICY_SET = "policy-set" + TERRAFORM = "terraform" + + +class Variable(BaseModel): + id: str | None = None + key: str | None = None + value: str | None = None + description: str | None = None + category: CategoryType | None = None + hcl: bool | None = None + sensitive: bool | None = None + version_id: str | None = None + workspace: dict | None = None + + +class VariableListOptions(BaseModel): + # Base pagination options would be handled by the service layer + pass + + +class VariableCreateOptions(BaseModel): + key: str | None = None + value: str | None = None + description: str | None = None + category: CategoryType | None = None + hcl: bool | None = None + sensitive: bool | None = None + + +class VariableUpdateOptions(BaseModel): + key: str | None = None + value: str | None = None + description: str | None = None + category: CategoryType | None = None + hcl: bool | None = None + sensitive: bool | None = None diff --git a/src/pytfe/models/variable_set.py b/src/pytfe/models/variable_set.py new file mode 100644 index 0000000..0daee06 --- /dev/null +++ b/src/pytfe/models/variable_set.py @@ -0,0 +1,162 @@ +from __future__ import annotations + +from datetime import datetime +from enum import Enum + +from pydantic import BaseModel, Field + +from .organization import Organization +from .project import Project +from .variable import CategoryType +from .workspace import Workspace + + +class VariableSetIncludeOpt(str, Enum): + """Include options for variable set operations.""" + + WORKSPACES = "workspaces" + PROJECTS = "projects" + VARS = "vars" + CURRENT_RUN = "current-run" + + +class Parent(BaseModel): + """Parent represents the variable set's parent (organizations and projects are supported).""" + + organization: Organization | None = None + project: Project | None = None + + +class VariableSet(BaseModel): + """Represents a Terraform Enterprise variable set.""" + + id: str | None = None + name: str | None = None + description: str | None = None + global_: bool | None = Field(default=None, alias="global") + priority: bool | None = None + created_at: datetime | None = None + updated_at: datetime | None = None + + # Relations + organization: Organization | None = None + workspaces: list[Workspace] = Field(default_factory=list) + projects: list[Project] = Field(default_factory=list) + vars: list[VariableSetVariable] = Field(default_factory=list) + parent: Parent | None = None + + +class VariableSetVariable(BaseModel): + """Represents a variable within a variable set.""" + + id: str | None = None + key: str + value: str | None = None + description: str | None = None + category: CategoryType + hcl: bool | None = None + sensitive: bool | None = None + version_id: str | None = None + + # Relations + variable_set: VariableSet | None = None + + +# Variable Set Options + + +class VariableSetListOptions(BaseModel): + """Options for listing variable sets.""" + + # Pagination options + page_number: int | None = None + page_size: int | None = None + include: list[VariableSetIncludeOpt] | None = None + query: str | None = None # Filter by name + + +class VariableSetCreateOptions(BaseModel): + """Options for creating a variable set.""" + + name: str + description: str | None = None + global_: bool = Field(alias="global") + priority: bool | None = None + parent: Parent | None = None + + +class VariableSetReadOptions(BaseModel): + """Options for reading a variable set.""" + + include: list[VariableSetIncludeOpt] | None = None + + +class VariableSetUpdateOptions(BaseModel): + """Options for updating a variable set.""" + + name: str | None = None + description: str | None = None + global_: bool | None = Field(alias="global", default=None) + priority: bool | None = None + + +class VariableSetApplyToWorkspacesOptions(BaseModel): + """Options for applying a variable set to workspaces.""" + + workspaces: list[Workspace] = Field(default_factory=list) + + +class VariableSetRemoveFromWorkspacesOptions(BaseModel): + """Options for removing a variable set from workspaces.""" + + workspaces: list[Workspace] = Field(default_factory=list) + + +class VariableSetApplyToProjectsOptions(BaseModel): + """Options for applying a variable set to projects.""" + + projects: list[Project] = Field(default_factory=list) + + +class VariableSetRemoveFromProjectsOptions(BaseModel): + """Options for removing a variable set from projects.""" + + projects: list[Project] = Field(default_factory=list) + + +class VariableSetUpdateWorkspacesOptions(BaseModel): + """Options for updating workspaces associated with a variable set.""" + + workspaces: list[Workspace] = Field(default_factory=list) + + +# Variable Set Variable Options + + +class VariableSetVariableListOptions(BaseModel): + """Options for listing variables in a variable set.""" + + # Pagination options + page_number: int | None = None + page_size: int | None = None + + +class VariableSetVariableCreateOptions(BaseModel): + """Options for creating a variable in a variable set.""" + + key: str + value: str | None = None + description: str | None = None + category: CategoryType + hcl: bool | None = None + sensitive: bool | None = None + + +class VariableSetVariableUpdateOptions(BaseModel): + """Options for updating a variable in a variable set.""" + + key: str | None = None + value: str | None = None + description: str | None = None + hcl: bool | None = None + sensitive: bool | None = None diff --git a/src/pytfe/models/workspace.py b/src/pytfe/models/workspace.py new file mode 100644 index 0000000..2205621 --- /dev/null +++ b/src/pytfe/models/workspace.py @@ -0,0 +1,340 @@ +from __future__ import annotations + +from datetime import datetime +from enum import Enum +from typing import Any + +from pydantic import BaseModel, Field + +from .common import EffectiveTagBinding, Pagination, Tag, TagBinding +from .data_retention_policy import DataRetentionPolicy, DataRetentionPolicyChoice +from .organization import ExecutionMode +from .project import Project + + +class Workspace(BaseModel): + id: str + name: str | None = None + organization: str | None = None + execution_mode: ExecutionMode | None = None + project_id: str | None = None + + # Core attributes + actions: WorkspaceActions | None = None + allow_destroy_plan: bool = False + assessments_enabled: bool = False + auto_apply: bool = False + auto_apply_run_trigger: bool = False + auto_destroy_at: datetime | None = None + auto_destroy_activity_duration: str | None = None + can_queue_destroy_plan: bool = False + created_at: datetime | None = None + description: str = "" + environment: str = "" + file_triggers_enabled: bool = False + global_remote_state: bool = False + inherits_project_auto_destroy: bool = False + locked: bool = False + migration_environment: str = "" + no_code_upgrade_available: bool = False + operations: bool = False + permissions: WorkspacePermissions | None = None + queue_all_runs: bool = False + speculative_enabled: bool = False + source: WorkspaceSource | None = None + source_name: str = "" + source_url: str = "" + structured_run_output_enabled: bool = False + terraform_version: str = "" + trigger_prefixes: list[str] = Field(default_factory=list) + trigger_patterns: list[str] = Field(default_factory=list) + vcs_repo: VCSRepo | None = None + working_directory: str = "" + updated_at: datetime | None = None + resource_count: int = 0 + apply_duration_average: float | None = None # in seconds + plan_duration_average: float | None = None # in seconds + policy_check_failures: int = 0 + run_failures: int = 0 + runs_count: int = 0 + tag_names: list[str] = Field(default_factory=list) + setting_overwrites: WorkspaceSettingOverwrites | None = None + + # Relations + agent_pool: Any | None = None # AgentPool object + current_run: Any | None = None # Run object + current_state_version: Any | None = None # StateVersion object + project: Project | None = None + ssh_key: Any | None = None # SSHKey object + outputs: list[WorkspaceOutputs] = Field(default_factory=list) + tags: list[Tag] = Field(default_factory=list) + # tags: list[Tag] = Field(default_factory=list) + current_configuration_version: Any | None = None # ConfigurationVersion object + locked_by: LockedByChoice | None = None + variables: list[Any] = Field(default_factory=list) # Variable objects + tag_bindings: list[TagBinding] = Field(default_factory=list) + effective_tag_bindings: list[EffectiveTagBinding] = Field(default_factory=list) + + # Links + links: dict[str, Any] = Field(default_factory=dict) + data_retention_policy: DataRetentionPolicy | None = None + data_retention_policy_choice: DataRetentionPolicyChoice | None = None + + +class WorkspaceIncludeOpt(str, Enum): + ORGANIZATION = "organization" + CURRENT_CONFIG_VER = "current_configuration_version" + CURRENT_CONFIG_VER_INGRESS = "current_configuration_version.ingress_attributes" + CURRENT_RUN = "current_run" + CURRENT_RUN_PLAN = "current_run.plan" + CURRENT_RUN_CONFIG_VER = "current_run.configuration_version" + CURRENT_RUN_CONFIG_VER_INGRESS = ( + "current_run.configuration_version.ingress_attributes" + ) + EFFECTIVE_TAG_BINDINGS = "effective_tag_bindings" + LOCKED_BY = "locked_by" + README = "readme" + OUTPUTS = "outputs" + CURRENT_STATE_VER = "current-state-version" + PROJECT = "project" + + +class WorkspaceSource(str, Enum): + API = "tfe-api" + MODULE = "tfe-module" + UI = "tfe-ui" + TERRAFORM = "terraform" + + +class WorkspaceActions(BaseModel): + is_destroyable: bool = False + + +class WorkspacePermissions(BaseModel): + can_destroy: bool = False + can_force_unlock: bool = False + can_lock: bool = False + can_manage_run_tasks: bool = False + can_queue_apply: bool = False + can_queue_destroy: bool = False + can_queue_run: bool = False + can_read_settings: bool = False + can_unlock: bool = False + can_update: bool = False + can_update_variable: bool = False + can_force_delete: bool | None = None + + +class WorkspaceSettingOverwrites(BaseModel): + execution_mode: bool | None = None + agent_pool: bool | None = None + + +class WorkspaceOutputs(BaseModel): + id: str + name: str + sensitive: bool = False + output_type: str + value: Any | None = None + + +class LockedByChoice(BaseModel): + run: Any | None = None + user: Any | None = None + team: Any | None = None + + +class WorkspaceListOptions(BaseModel): + """Options for listing workspaces.""" + + # Pagination options (from ListOptions) + page_number: int | None = None + page_size: int | None = None + + # Search and filter options + search: str | None = None # search[name] - partial workspace name + tags: str | None = None # search[tags] - comma-separated tag names + exclude_tags: str | None = ( + None # search[exclude-tags] - comma-separated tag names to exclude + ) + wildcard_name: str | None = None # search[wildcard-name] - substring matching + project_id: str | None = None # filter[project][id] - project ID filter + current_run_status: str | None = ( + None # filter[current-run][status] - run status filter + ) + + # Tag binding filters (not URL encoded, handled specially) + tag_bindings: list[TagBinding] = Field(default_factory=list) + + # Include related resources + include: list[WorkspaceIncludeOpt] = Field(default_factory=list) + + # Sorting options + sort: str | None = ( + None # "name" (default) or "current-run.created-at", prepend "-" to reverse + ) + + +class WorkspaceReadOptions(BaseModel): + include: list[WorkspaceIncludeOpt] = Field(default_factory=list) + + +class WorkspaceCreateOptions(BaseModel): + name: str + type: str = "workspaces" + agent_pool_id: str | None = None + allow_destroy_plan: bool | None = None + assessments_enabled: bool | None = None + auto_apply: bool | None = None + auto_apply_run_trigger: bool | None = None + auto_destroy_at: datetime | None = None + auto_destroy_activity_duration: str | None = None + inherits_project_auto_destroy: bool | None = None + description: str | None = None + execution_mode: ExecutionMode | None = None + file_triggers_enabled: bool | None = None + global_remote_state: bool | None = None + migration_environment: str | None = None + operations: bool | None = None + queue_all_runs: bool | None = None + speculative_enabled: bool | None = None + source_name: str | None = None + source_url: str | None = None + structured_run_output_enabled: bool | None = None + terraform_version: str | None = None + trigger_prefixes: list[str] = Field(default_factory=list) + trigger_patterns: list[str] = Field(default_factory=list) + vcs_repo: VCSRepo | None = None + working_directory: str | None = None + hyok_enabled: bool | None = None + tags: list[Tag] = Field(default_factory=list) + setting_overwrites: WorkspaceSettingOverwrites | None = None + project: Project | None = None + tag_bindings: list[TagBinding] = Field(default_factory=list) + + +class WorkspaceUpdateOptions(BaseModel): + name: str + type: str = "workspaces" + agent_pool_id: str | None = None + allow_destroy_plan: bool | None = None + assessments_enabled: bool | None = None + auto_apply: bool | None = None + auto_apply_run_trigger: bool | None = None + auto_destroy_at: datetime | None = None + auto_destroy_activity_duration: str | None = None + inherits_project_auto_destroy: bool | None = None + description: str | None = None + execution_mode: ExecutionMode | None = None + file_triggers_enabled: bool | None = None + global_remote_state: bool | None = None + operations: bool | None = None + queue_all_runs: bool | None = None + speculative_enabled: bool | None = None + structured_run_output_enabled: bool | None = None + terraform_version: str | None = None + trigger_prefixes: list[str] = Field(default_factory=list) + trigger_patterns: list[str] = Field(default_factory=list) + vcs_repo: VCSRepo | None = None + working_directory: str | None = None + hyok_enabled: bool | None = None + setting_overwrites: WorkspaceSettingOverwrites | None = None + project: Project | None = None + tag_bindings: list[TagBinding] = Field(default_factory=list) + + +class WorkspaceList(BaseModel): + items: list[Workspace] = Field(default_factory=list) + pagination: Pagination | None = None + + +class WorkspaceRemoveVCSConnectionOptions(BaseModel): + """Options for removing VCS connection from a workspace.""" + + # Currently no options are defined, but this class can be extended in the future + id: str + vcs_repo: VCSRepo | None = None + + +class WorkspaceLockOptions(BaseModel): + """Options for locking a workspace.""" + + # Specifies the reason for locking the workspace. + reason: str + + +class WorkspaceAssignSSHKeyOptions(BaseModel): + """Options for assigning an SSH key to a workspace.""" + + ssh_key_id: str + type: str = "workspaces" + + +class workspaceUnassignSSHKeyOptions(BaseModel): + """Options for unassigning an SSH key from a workspace.""" + + # Must be nil to unset the currently assigned SSH key. + ssh_key_id: str + type: str = "workspaces" + + +class WorkspaceListRemoteStateConsumersOptions(BaseModel): + """Options for listing remote state consumers of a workspace.""" + + # Pagination options (from ListOptions) + page_number: int | None = None + page_size: int | None = None + + +class WorkspaceAddRemoteStateConsumersOptions(BaseModel): + """Options for adding remote state consumers to a workspace.""" + + workspaces: list[Workspace] = Field(default_factory=list) + + +class WorkspaceRemoveRemoteStateConsumersOptions(BaseModel): + """Options for removing remote state consumers from a workspace.""" + + workspaces: list[Workspace] = Field(default_factory=list) + + +class WorkspaceUpdateRemoteStateConsumersOptions(BaseModel): + """Options for updating remote state consumers of a workspace.""" + + workspaces: list[Workspace] = Field(default_factory=list) + + +class WorkspaceTagListOptions(BaseModel): + """Options for listing tags of a workspace.""" + + # Pagination options (from ListOptions) + page_number: int | None = None + page_size: int | None = None + query: str | None = None + + +class WorkspaceAddTagsOptions(BaseModel): + """Options for adding tags to a workspace.""" + + tags: list[Tag] = Field(default_factory=list) + + +class WorkspaceRemoveTagsOptions(BaseModel): + """Options for removing tags from a workspace.""" + + tags: list[Tag] = Field(default_factory=list) + + +class WorkspaceAddTagBindingsOptions(BaseModel): + """Options for adding tag bindings to a workspace.""" + + tag_bindings: list[TagBinding] = Field(default_factory=list) + + +class VCSRepo(BaseModel): + branch: str | None = None + identifier: str | None = None + ingress_submodules: bool | None = None + oauth_token_id: str | None = None + tags_regex: str | None = None + gha_installation_id: str | None = None diff --git a/src/tfe/models/workspace_run_task.py b/src/pytfe/models/workspace_run_task.py similarity index 100% rename from src/tfe/models/workspace_run_task.py rename to src/pytfe/models/workspace_run_task.py diff --git a/src/pytfe/py.typed b/src/pytfe/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/tfe/resources/_base.py b/src/pytfe/resources/_base.py similarity index 52% rename from src/tfe/resources/_base.py rename to src/pytfe/resources/_base.py index a0eaa71..0d8bf10 100644 --- a/src/tfe/resources/_base.py +++ b/src/pytfe/resources/_base.py @@ -1,6 +1,6 @@ from __future__ import annotations -from collections.abc import AsyncIterator, Iterator +from collections.abc import Iterator from typing import Any from .._http import HTTPTransport @@ -31,30 +31,3 @@ def _list( if len(data) < page_size: break page += 1 - - -""" -Warning: Do Not Use this Async Service as its not stable with HashiCorp API. -""" - - -class _AService: - def __init__(self, t: HTTPTransport) -> None: - self.t = t - - async def _alist( - self, path: str, *, params: dict | None = None - ) -> AsyncIterator[dict[str, Any]]: - page = 1 - while True: - p = dict(params or {}) - p.setdefault("page[number]", page) - p.setdefault("page[size]", 100) - r = await self.t.arequest("GET", path, params=p) - data = r.json().get("data", []) - for item in data: - yield item - page_size = int(p["page[size]"]) - if len(data) < page_size: - break - page += 1 diff --git a/src/tfe/resources/admin/settings.py b/src/pytfe/resources/admin/settings.py similarity index 50% rename from src/tfe/resources/admin/settings.py rename to src/pytfe/resources/admin/settings.py index 44a654d..8ee9407 100644 --- a/src/tfe/resources/admin/settings.py +++ b/src/pytfe/resources/admin/settings.py @@ -2,16 +2,10 @@ from typing import Any -from .._base import _AService, _Service +from .._base import _Service class AdminSettings(_Service): def terraform_versions(self) -> Any: r = self.t.request("GET", "/api/v2/admin/terraform-versions") return r.json() - - -class AdminSettingsAsync(_AService): - async def terraform_versions(self) -> Any: - r = await self.t.arequest("GET", "/api/v2/admin/terraform-versions") - return r.json() diff --git a/src/tfe/resources/agent_pools.py b/src/pytfe/resources/agent_pools.py similarity index 100% rename from src/tfe/resources/agent_pools.py rename to src/pytfe/resources/agent_pools.py diff --git a/src/tfe/resources/agents.py b/src/pytfe/resources/agents.py similarity index 100% rename from src/tfe/resources/agents.py rename to src/pytfe/resources/agents.py diff --git a/src/tfe/resources/apply.py b/src/pytfe/resources/apply.py similarity index 100% rename from src/tfe/resources/apply.py rename to src/pytfe/resources/apply.py diff --git a/src/tfe/resources/configuration_version.py b/src/pytfe/resources/configuration_version.py similarity index 99% rename from src/tfe/resources/configuration_version.py rename to src/pytfe/resources/configuration_version.py index 8fdb33b..50c5125 100644 --- a/src/tfe/resources/configuration_version.py +++ b/src/pytfe/resources/configuration_version.py @@ -12,7 +12,7 @@ ServerError, TFEError, ) -from ..models.configuration_version_types import ( +from ..models.configuration_version import ( ConfigurationVersion, ConfigurationVersionCreateOptions, ConfigurationVersionListOptions, diff --git a/src/tfe/resources/oauth_client.py b/src/pytfe/resources/oauth_client.py similarity index 100% rename from src/tfe/resources/oauth_client.py rename to src/pytfe/resources/oauth_client.py diff --git a/src/tfe/resources/oauth_token.py b/src/pytfe/resources/oauth_token.py similarity index 100% rename from src/tfe/resources/oauth_token.py rename to src/pytfe/resources/oauth_token.py diff --git a/src/tfe/resources/organizations.py b/src/pytfe/resources/organizations.py similarity index 98% rename from src/tfe/resources/organizations.py rename to src/pytfe/resources/organizations.py index 95ec83e..fe6f94c 100644 --- a/src/tfe/resources/organizations.py +++ b/src/pytfe/resources/organizations.py @@ -9,8 +9,7 @@ ERR_REQUIRED_EMAIL, ERR_REQUIRED_NAME, ) -from ..types import ( - Capacity, +from ..models.data_retention_policy import ( DataRetentionPolicy, DataRetentionPolicyChoice, DataRetentionPolicyDeleteOlder, @@ -18,6 +17,9 @@ DataRetentionPolicyDontDelete, DataRetentionPolicyDontDeleteSetOptions, DataRetentionPolicySetOptions, +) +from ..models.organization import ( + Capacity, Entitlements, Organization, OrganizationCreateOptions, @@ -167,7 +169,7 @@ def read_run_queue( ) data = r.json() - from ..types import Pagination, Run, RunStatus + from ..models.organization import Pagination, Run, RunStatus runs = [] for item in data.get("data", []): diff --git a/src/tfe/resources/plan.py b/src/pytfe/resources/plan.py similarity index 100% rename from src/tfe/resources/plan.py rename to src/pytfe/resources/plan.py diff --git a/src/tfe/resources/policy.py b/src/pytfe/resources/policy.py similarity index 100% rename from src/tfe/resources/policy.py rename to src/pytfe/resources/policy.py diff --git a/src/tfe/resources/policy_check.py b/src/pytfe/resources/policy_check.py similarity index 100% rename from src/tfe/resources/policy_check.py rename to src/pytfe/resources/policy_check.py diff --git a/src/tfe/resources/projects.py b/src/pytfe/resources/projects.py similarity index 99% rename from src/tfe/resources/projects.py rename to src/pytfe/resources/projects.py index 1bd02d5..9011a3c 100644 --- a/src/tfe/resources/projects.py +++ b/src/pytfe/resources/projects.py @@ -5,14 +5,16 @@ from collections.abc import Iterator from typing import Any -from ..types import ( +from ..models.common import ( EffectiveTagBinding, + TagBinding, +) +from ..models.project import ( Project, ProjectAddTagBindingsOptions, ProjectCreateOptions, ProjectListOptions, ProjectUpdateOptions, - TagBinding, ) from ..utils import valid_string, valid_string_id from ._base import _Service diff --git a/src/tfe/resources/query_run.py b/src/pytfe/resources/query_run.py similarity index 100% rename from src/tfe/resources/query_run.py rename to src/pytfe/resources/query_run.py diff --git a/src/tfe/resources/registry_module.py b/src/pytfe/resources/registry_module.py similarity index 99% rename from src/tfe/resources/registry_module.py rename to src/pytfe/resources/registry_module.py index 1991895..8daf49e 100644 --- a/src/tfe/resources/registry_module.py +++ b/src/pytfe/resources/registry_module.py @@ -9,7 +9,7 @@ ERR_INVALID_ORG, ERR_INVALID_VERSION, ) -from ..models.registry_module_types import ( +from ..models.registry_module import ( AgentExecutionMode, Commit, CommitList, diff --git a/src/tfe/resources/registry_provider.py b/src/pytfe/resources/registry_provider.py similarity index 99% rename from src/tfe/resources/registry_provider.py rename to src/pytfe/resources/registry_provider.py index 3c1fd22..2511636 100644 --- a/src/tfe/resources/registry_provider.py +++ b/src/pytfe/resources/registry_provider.py @@ -6,7 +6,7 @@ from ..errors import ( ERR_INVALID_ORG, ) -from ..models.registry_provider_types import ( +from ..models.registry_provider import ( RegistryName, RegistryProvider, RegistryProviderCreateOptions, diff --git a/src/tfe/resources/reserved_tag_key.py b/src/pytfe/resources/reserved_tag_key.py similarity index 100% rename from src/tfe/resources/reserved_tag_key.py rename to src/pytfe/resources/reserved_tag_key.py diff --git a/src/tfe/resources/run.py b/src/pytfe/resources/run.py similarity index 100% rename from src/tfe/resources/run.py rename to src/pytfe/resources/run.py diff --git a/src/tfe/resources/run_event.py b/src/pytfe/resources/run_event.py similarity index 100% rename from src/tfe/resources/run_event.py rename to src/pytfe/resources/run_event.py diff --git a/src/tfe/resources/run_task.py b/src/pytfe/resources/run_task.py similarity index 100% rename from src/tfe/resources/run_task.py rename to src/pytfe/resources/run_task.py diff --git a/src/tfe/resources/run_trigger.py b/src/pytfe/resources/run_trigger.py similarity index 100% rename from src/tfe/resources/run_trigger.py rename to src/pytfe/resources/run_trigger.py diff --git a/src/tfe/resources/ssh_keys.py b/src/pytfe/resources/ssh_keys.py similarity index 100% rename from src/tfe/resources/ssh_keys.py rename to src/pytfe/resources/ssh_keys.py diff --git a/src/tfe/resources/state_version_outputs.py b/src/pytfe/resources/state_version_outputs.py similarity index 100% rename from src/tfe/resources/state_version_outputs.py rename to src/pytfe/resources/state_version_outputs.py diff --git a/src/tfe/resources/state_versions.py b/src/pytfe/resources/state_versions.py similarity index 100% rename from src/tfe/resources/state_versions.py rename to src/pytfe/resources/state_versions.py diff --git a/src/tfe/resources/variable.py b/src/pytfe/resources/variable.py similarity index 99% rename from src/tfe/resources/variable.py rename to src/pytfe/resources/variable.py index b6751f5..6eff36d 100644 --- a/src/tfe/resources/variable.py +++ b/src/pytfe/resources/variable.py @@ -9,7 +9,7 @@ ERR_REQUIRED_CATEGORY, ERR_REQUIRED_KEY, ) -from ..types import ( +from ..models.variable import ( Variable, VariableCreateOptions, VariableListOptions, diff --git a/src/tfe/resources/variable_sets.py b/src/pytfe/resources/variable_sets.py similarity index 99% rename from src/tfe/resources/variable_sets.py rename to src/pytfe/resources/variable_sets.py index bc0e0db..4bf4353 100644 --- a/src/tfe/resources/variable_sets.py +++ b/src/pytfe/resources/variable_sets.py @@ -3,9 +3,8 @@ import builtins from typing import Any -from tfe._http import HTTPTransport -from tfe.resources._base import _Service -from tfe.types import ( +from .._http import HTTPTransport +from ..models.variable_set import ( VariableSet, VariableSetApplyToProjectsOptions, VariableSetApplyToWorkspacesOptions, @@ -22,6 +21,7 @@ VariableSetVariableListOptions, VariableSetVariableUpdateOptions, ) +from ._base import _Service class VariableSets(_Service): diff --git a/src/tfe/resources/workspaces.py b/src/pytfe/resources/workspaces.py similarity index 99% rename from src/tfe/resources/workspaces.py rename to src/pytfe/resources/workspaces.py index dbfdf5e..2a3a6b2 100644 --- a/src/tfe/resources/workspaces.py +++ b/src/pytfe/resources/workspaces.py @@ -16,7 +16,12 @@ WorkspaceMinimumLimitError, WorkspaceRequiredError, ) -from ..types import ( +from ..models.common import ( + EffectiveTagBinding, + Tag, + TagBinding, +) +from ..models.data_retention_policy import ( DataRetentionPolicy, DataRetentionPolicyChoice, DataRetentionPolicyDeleteOlder, @@ -24,11 +29,10 @@ DataRetentionPolicyDontDelete, DataRetentionPolicyDontDeleteSetOptions, DataRetentionPolicySetOptions, - EffectiveTagBinding, +) +from ..models.workspace import ( ExecutionMode, LockedByChoice, - Tag, - TagBinding, VCSRepo, Workspace, WorkspaceActions, diff --git a/src/tfe/utils.py b/src/pytfe/utils.py similarity index 80% rename from src/tfe/utils.py rename to src/pytfe/utils.py index 4b48fde..9adf58d 100644 --- a/src/tfe/utils.py +++ b/src/pytfe/utils.py @@ -31,7 +31,7 @@ UnsupportedBothTriggerPatternsAndPrefixesError, UnsupportedOperationsError, ) -from .types import ( +from .models.workspace import ( VCSRepo, WorkspaceCreateOptions, WorkspaceUpdateOptions, @@ -285,6 +285,76 @@ def validate_oauth_client_remove_projects_options( raise ValueError(ERR_PROJECT_MIN_LIMIT) +def valid_project_name(name: str) -> bool: + """Validate project name format""" + if not valid_string(name): + return False + # Project names can contain letters, numbers, spaces, hyphens, underscores, and periods + # Must be between 1 and 90 characters + if len(name) > 90: + return False + # Allow most printable characters except some special ones + # Based on Terraform Cloud API documentation + pattern = re.compile(r"^[a-zA-Z0-9\s._-]+$") + return bool(pattern.match(name)) + + +def valid_organization_name(org_name: str) -> bool: + """Validate organization name format""" + if not valid_string(org_name): + return False + # Organization names must be valid identifiers + return valid_string_id(org_name) + + +def validate_project_create_options( + organization: str, name: str, description: str | None = None +) -> None: + """Validate project creation parameters""" + if not valid_organization_name(organization): + raise ValueError("Organization name is required and must be valid") + + if not valid_string(name): + raise ValueError("Project name is required") + + if not valid_project_name(name): + raise ValueError("Project name contains invalid characters or is too long") + + if description is not None and not valid_string(description): + raise ValueError("Description must be a valid string") + + +def validate_project_update_options( + project_id: str, name: str | None = None, description: str | None = None +) -> None: + """Validate project update parameters""" + if not valid_string_id(project_id): + raise ValueError("Project ID is required") + + if name is not None: + if not valid_string(name): + raise ValueError("Project name cannot be empty") + if not valid_project_name(name): + raise ValueError("Project name contains invalid characters or is too long") + + if description is not None and not valid_string(description): + raise ValueError("Description must be a valid string") + + +def validate_project_list_options( + organization: str, query: str | None = None, name: str | None = None +) -> None: + """Validate project list options.""" + if not valid_organization_name(organization): + raise ValueError("Organization name is required and must be valid") + + if query and not valid_string(query): + raise ValueError("Query must be a valid string") + + if name and not valid_project_name(name): + raise ValueError("Project name must be valid") + + def pack_contents(path: str) -> io.BytesIO: """ Pack directory contents into a tar.gz archive suitable for upload. diff --git a/src/tfe/__init__.py b/src/tfe/__init__.py deleted file mode 100644 index 8c1b6c6..0000000 --- a/src/tfe/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from . import errors -from .client import TFEClient -from .config import TFEConfig - -__all__ = ["TFEConfig", "TFEClient", "errors"] diff --git a/src/tfe/aclient.py b/src/tfe/aclient.py deleted file mode 100644 index 9580ba5..0000000 --- a/src/tfe/aclient.py +++ /dev/null @@ -1,27 +0,0 @@ -""" -Async TFE Client: This client should not be used for now. -""" - -from __future__ import annotations - -from ._http import HTTPTransport -from .config import TFEConfig - - -class AsyncTFEClient: - def __init__(self, config: TFEConfig | None = None): - cfg = config or TFEConfig.from_env() - self._transport = HTTPTransport( - cfg.address, - cfg.token, - timeout=cfg.timeout, - verify_tls=cfg.verify_tls, - user_agent_suffix=cfg.user_agent_suffix, - max_retries=cfg.max_retries, - backoff_base=cfg.backoff_base, - backoff_cap=cfg.backoff_cap, - backoff_jitter=cfg.backoff_jitter, - http2=cfg.http2, - proxies=cfg.proxies, - ca_bundle=cfg.ca_bundle, - ) diff --git a/src/tfe/models/configuration_version.py b/src/tfe/models/configuration_version.py deleted file mode 100644 index a3e3023..0000000 --- a/src/tfe/models/configuration_version.py +++ /dev/null @@ -1,17 +0,0 @@ -from __future__ import annotations - -from pydantic import BaseModel, Field - - -class ConfigurationVersion(BaseModel): - id: str - auto_queue_runs: bool = Field(..., alias="auto-queue-runs") - error: str | None = Field(None, alias="error") - error_message: str | None = Field(None, alias="error-message") - # source: ConfigurationSource = Field(..., alias="source") - speculative: bool = Field(..., alias="speculative") - provisional: bool = Field(..., alias="provisional") - # status: ConfigurationStatus = Field(..., alias="status") - # status_timestamps: CVStatusTimestamps = Field(..., alias="status-timestamps") - upload_url: str | None = Field(None, alias="upload-url") - # ingress_attributes: IngressAttributes | None = Field(None, alias="ingress-attributes") diff --git a/src/tfe/models/workspace.py b/src/tfe/models/workspace.py deleted file mode 100644 index 98b5ae7..0000000 --- a/src/tfe/models/workspace.py +++ /dev/null @@ -1,14 +0,0 @@ -from __future__ import annotations - -from pydantic import BaseModel, Field - -from .organization import ExecutionMode - - -class Workspace(BaseModel): - id: str - name: str - organization: str - execution_mode: ExecutionMode | None = None - project_id: str | None = None - tags: list[str] = Field(default_factory=list) diff --git a/src/tfe/project.py b/src/tfe/project.py deleted file mode 100644 index 9fcfa71..0000000 --- a/src/tfe/project.py +++ /dev/null @@ -1,83 +0,0 @@ -"""Project-specific utility functions and validation.""" - -import re -from typing import Any - -from .utils import valid_string, valid_string_id - - -def valid_project_name(name: str) -> bool: - """Validate project name format""" - if not valid_string(name): - return False - # Project names can contain letters, numbers, spaces, hyphens, underscores, and periods - # Must be between 1 and 90 characters - if len(name) > 90: - return False - # Allow most printable characters except some special ones - # Based on Terraform Cloud API documentation - pattern = re.compile(r"^[a-zA-Z0-9\s._-]+$") - return bool(pattern.match(name)) - - -def valid_organization_name(org_name: str) -> bool: - """Validate organization name format""" - if not valid_string(org_name): - return False - # Organization names must be valid identifiers - return valid_string_id(org_name) - - -def validate_project_create_options( - organization: str, name: str, description: str | None = None -) -> None: - """Validate project creation parameters""" - if not valid_organization_name(organization): - raise ValueError("Organization name is required and must be valid") - - if not valid_string(name): - raise ValueError("Project name is required") - - if not valid_project_name(name): - raise ValueError("Project name contains invalid characters or is too long") - - if description is not None and not valid_string(description): - raise ValueError("Description must be a valid string") - - -def validate_project_update_options( - project_id: str, name: str | None = None, description: str | None = None -) -> None: - """Validate project update parameters""" - if not valid_string_id(project_id): - raise ValueError("Project ID is required") - - if name is not None: - if not valid_string(name): - raise ValueError("Project name cannot be empty") - if not valid_project_name(name): - raise ValueError("Project name contains invalid characters or is too long") - - if description is not None and not valid_string(description): - raise ValueError("Description must be a valid string") - - -def validate_project_list_options( - organization: str, query: str | None = None, name: str | None = None -) -> None: - """Validate project list options.""" - if not valid_organization_name(organization): - raise ValueError("Organization name is required and must be valid") - - if query and not valid_string(query): - raise ValueError("Query must be a valid string") - - if name and not valid_project_name(name): - raise ValueError("Project name must be valid") - - -def _safe_str(value: Any, default: str = "") -> str: - """Safely convert a value to string with optional default.""" - if value is None: - return default - return str(value) diff --git a/src/tfe/types.py b/src/tfe/types.py deleted file mode 100644 index 2f0a4f9..0000000 --- a/src/tfe/types.py +++ /dev/null @@ -1,810 +0,0 @@ -from __future__ import annotations - -from datetime import datetime -from enum import Enum -from typing import Any - -from pydantic import BaseModel, Field - - -class OrganizationUpdateOptions(BaseModel): - name: str | None = None - email: str | None = None - assessments_enforced: bool | None = None - collaborator_auth_policy: str | None = None - cost_estimation_enabled: bool | None = None - default_execution_mode: str | None = None - external_id: str | None = None - is_unified: bool | None = None - owners_team_saml_role_id: str | None = None - permissions: dict | None = None - saml_enabled: bool | None = None - session_remember: int | None = None - session_timeout: int | None = None - two_factor_conformant: bool | None = None - send_passing_statuses_for_untriggered_speculative_plans: bool | None = None - remaining_testable_count: int | None = None - speculative_plan_management_enabled: bool | None = None - aggregated_commit_status_enabled: bool | None = None - allow_force_delete_workspaces: bool | None = None - default_project: dict | None = None - default_agent_pool: dict | None = None - data_retention_policy: dict | None = None - data_retention_policy_choice: dict | None = None - - -class OrganizationCreateOptions(BaseModel): - name: str | None = None - email: str | None = None - assessments_enforced: bool | None = None - collaborator_auth_policy: str | None = None - cost_estimation_enabled: bool | None = None - default_execution_mode: str | None = None - external_id: str | None = None - is_unified: bool | None = None - owners_team_saml_role_id: str | None = None - permissions: dict | None = None - saml_enabled: bool | None = None - session_remember: int | None = None - session_timeout: int | None = None - two_factor_conformant: bool | None = None - send_passing_statuses_for_untriggered_speculative_plans: bool | None = None - remaining_testable_count: int | None = None - speculative_plan_management_enabled: bool | None = None - aggregated_commit_status_enabled: bool | None = None - allow_force_delete_workspaces: bool | None = None - default_project: dict | None = None - default_agent_pool: dict | None = None - data_retention_policy: dict | None = None - data_retention_policy_choice: dict | None = None - - -class ExecutionMode(str, Enum): - REMOTE = "remote" - AGENT = "agent" - LOCAL = "local" - - -class RunStatus(str, Enum): - PLANNING = "planning" - PLANNED = "planned" - APPLIED = "applied" - CANCELED = "canceled" - ERRORED = "errored" - - -class Organization(BaseModel): - name: str | None = None - assessments_enforced: bool | None = None - collaborator_auth_policy: str | None = None - cost_estimation_enabled: bool | None = None - created_at: datetime | None = None - default_execution_mode: str | None = None - email: str | None = None - external_id: str | None = None - id: str | None = None - is_unified: bool | None = None - owners_team_saml_role_id: str | None = None - permissions: dict | None = None - saml_enabled: bool | None = None - session_remember: int | None = None - session_timeout: int | None = None - trial_expires_at: datetime | None = None - two_factor_conformant: bool | None = None - send_passing_statuses_for_untriggered_speculative_plans: bool | None = None - remaining_testable_count: int | None = None - speculative_plan_management_enabled: bool | None = None - aggregated_commit_status_enabled: bool | None = None - allow_force_delete_workspaces: bool | None = None - default_project: dict | None = None - default_agent_pool: dict | None = None - data_retention_policy: dict | None = None - data_retention_policy_choice: dict | None = None - - -class Project(BaseModel): - """Project represents a Terraform Enterprise project""" - - id: str - name: str | None = None - description: str = "" - organization: str | None = None - created_at: str = "" - updated_at: str = "" - workspace_count: int = 0 - default_execution_mode: str = "remote" - - -class ProjectListOptions(BaseModel): - """Options for listing projects""" - - # Optional: String used to filter results by complete project name - name: str | None = None - # Optional: Query string to search projects by names - query: str | None = None - # Optional: Include related resources - include: list[str] | None = None - # Pagination options - page_number: int | None = None - page_size: int | None = None - - -class ProjectCreateOptions(BaseModel): - """Options for creating a project""" - - # Required: A name to identify the project - name: str - # Optional: A description for the project - description: str | None = None - - -class ProjectUpdateOptions(BaseModel): - """Options for updating a project""" - - # Optional: A name to identify the project - name: str | None = None - # Optional: A description for the project - description: str | None = None - - -class ProjectAddTagBindingsOptions(BaseModel): - """Options for adding tag bindings to a project""" - - tag_bindings: list[TagBinding] = Field(default_factory=list) - - -class Workspace(BaseModel): - id: str - name: str | None = None - organization: str | None = None - execution_mode: ExecutionMode | None = None - project_id: str | None = None - - # Core attributes - actions: WorkspaceActions | None = None - allow_destroy_plan: bool = False - assessments_enabled: bool = False - auto_apply: bool = False - auto_apply_run_trigger: bool = False - auto_destroy_at: datetime | None = None - auto_destroy_activity_duration: str | None = None - can_queue_destroy_plan: bool = False - created_at: datetime | None = None - description: str = "" - environment: str = "" - file_triggers_enabled: bool = False - global_remote_state: bool = False - inherits_project_auto_destroy: bool = False - locked: bool = False - migration_environment: str = "" - no_code_upgrade_available: bool = False - operations: bool = False - permissions: WorkspacePermissions | None = None - queue_all_runs: bool = False - speculative_enabled: bool = False - source: WorkspaceSource | None = None - source_name: str = "" - source_url: str = "" - structured_run_output_enabled: bool = False - terraform_version: str = "" - trigger_prefixes: list[str] = Field(default_factory=list) - trigger_patterns: list[str] = Field(default_factory=list) - vcs_repo: VCSRepo | None = None - working_directory: str = "" - updated_at: datetime | None = None - resource_count: int = 0 - apply_duration_average: float | None = None # in seconds - plan_duration_average: float | None = None # in seconds - policy_check_failures: int = 0 - run_failures: int = 0 - runs_count: int = 0 - tag_names: list[str] = Field(default_factory=list) - setting_overwrites: WorkspaceSettingOverwrites | None = None - - # Relations - agent_pool: Any | None = None # AgentPool object - current_run: Any | None = None # Run object - current_state_version: Any | None = None # StateVersion object - project: Project | None = None - ssh_key: Any | None = None # SSHKey object - outputs: list[WorkspaceOutputs] = Field(default_factory=list) - tags: list[Tag] = Field(default_factory=list) - current_configuration_version: Any | None = None # ConfigurationVersion object - locked_by: LockedByChoice | None = None - variables: list[Any] = Field(default_factory=list) # Variable objects - tag_bindings: list[TagBinding] = Field(default_factory=list) - effective_tag_bindings: list[EffectiveTagBinding] = Field(default_factory=list) - - # Links - links: dict[str, Any] = Field(default_factory=dict) - data_retention_policy: DataRetentionPolicy | None = None - data_retention_policy_choice: DataRetentionPolicyChoice | None = None - - -class Capacity(BaseModel): - organization: str - pending: int - running: int - - -class Entitlements(BaseModel): - id: str - agents: bool | None = None - audit_logging: bool | None = None - cost_estimation: bool | None = None - global_run_tasks: bool | None = None - operations: bool | None = None - private_module_registry: bool | None = None - private_run_tasks: bool | None = None - run_tasks: bool | None = None - sso: bool | None = None - sentinel: bool | None = None - state_storage: bool | None = None - teams: bool | None = None - vcs_integrations: bool | None = None - waypoint_actions: bool | None = None - waypoint_templates_and_addons: bool | None = None - - -class Run(BaseModel): - id: str - status: RunStatus - # Add other Run fields as needed - - -class Pagination(BaseModel): - current_page: int - total_count: int - previous_page: int | None = None - next_page: int | None = None - total_pages: int | None = None - # Add other pagination fields as needed - - -class RunQueue(BaseModel): - pagination: Pagination | None = None - items: list[Run] = Field(default_factory=list) - - -class ReadRunQueueOptions(BaseModel): - # List options for pagination - page_number: int | None = None - page_size: int | None = None - - -class DataRetentionPolicy(BaseModel): - """Deprecated: Use DataRetentionPolicyDeleteOlder instead.""" - - id: str - delete_older_than_n_days: int - - -class DataRetentionPolicyDeleteOlder(BaseModel): - id: str - delete_older_than_n_days: int - - -class DataRetentionPolicyDontDelete(BaseModel): - id: str - - -class DataRetentionPolicyChoice(BaseModel): - """Polymorphic data retention policy choice.""" - - data_retention_policy: DataRetentionPolicy | None = None - data_retention_policy_delete_older: DataRetentionPolicyDeleteOlder | None = None - data_retention_policy_dont_delete: DataRetentionPolicyDontDelete | None = None - - def is_populated(self) -> bool: - """Returns whether one of the choices is populated.""" - return ( - self.data_retention_policy is not None - or self.data_retention_policy_delete_older is not None - or self.data_retention_policy_dont_delete is not None - ) - - def convert_to_legacy_struct(self) -> DataRetentionPolicy | None: - """Convert the DataRetentionPolicyChoice to the legacy DataRetentionPolicy struct.""" - if not self.is_populated(): - return None - - if self.data_retention_policy is not None: - return self.data_retention_policy - elif self.data_retention_policy_delete_older is not None: - return DataRetentionPolicy( - id=self.data_retention_policy_delete_older.id, - delete_older_than_n_days=self.data_retention_policy_delete_older.delete_older_than_n_days, - ) - return None - - -class DataRetentionPolicySetOptions(BaseModel): - """Deprecated: Use DataRetentionPolicyDeleteOlderSetOptions instead.""" - - delete_older_than_n_days: int - - -class DataRetentionPolicyDeleteOlderSetOptions(BaseModel): - delete_older_than_n_days: int - - -class DataRetentionPolicyDontDeleteSetOptions(BaseModel): - pass # No additional fields needed - - -# Variables related models -class CategoryType(str, Enum): - ENV = "env" - POLICY_SET = "policy-set" - TERRAFORM = "terraform" - - -class Variable(BaseModel): - id: str | None = None - key: str | None = None - value: str | None = None - description: str | None = None - category: CategoryType | None = None - hcl: bool | None = None - sensitive: bool | None = None - version_id: str | None = None - workspace: dict | None = None - - -class VariableListOptions(BaseModel): - # Base pagination options would be handled by the service layer - pass - - -class VariableCreateOptions(BaseModel): - key: str | None = None - value: str | None = None - description: str | None = None - category: CategoryType | None = None - hcl: bool | None = None - sensitive: bool | None = None - - -class VariableUpdateOptions(BaseModel): - key: str | None = None - value: str | None = None - description: str | None = None - category: CategoryType | None = None - hcl: bool | None = None - sensitive: bool | None = None - - -class Tag(BaseModel): - id: str | None = None - name: str = "" - - -class TagBinding(BaseModel): - id: str | None = None - key: str - value: str | None = None - - -class EffectiveTagBinding(BaseModel): - id: str - key: str - value: str | None = None - links: dict[str, Any] = Field(default_factory=dict) - - -class WorkspaceIncludeOpt(str, Enum): - ORGANIZATION = "organization" - CURRENT_CONFIG_VER = "current_configuration_version" - CURRENT_CONFIG_VER_INGRESS = "current_configuration_version.ingress_attributes" - CURRENT_RUN = "current_run" - CURRENT_RUN_PLAN = "current_run.plan" - CURRENT_RUN_CONFIG_VER = "current_run.configuration_version" - CURRENT_RUN_CONFIG_VER_INGRESS = ( - "current_run.configuration_version.ingress_attributes" - ) - EFFECTIVE_TAG_BINDINGS = "effective_tag_bindings" - LOCKED_BY = "locked_by" - README = "readme" - OUTPUTS = "outputs" - CURRENT_STATE_VER = "current-state-version" - PROJECT = "project" - - -class VCSRepo(BaseModel): - branch: str | None = None - identifier: str | None = None - ingress_submodules: bool | None = None - oauth_token_id: str | None = None - tags_regex: str | None = None - gha_installation_id: str | None = None - - -class WorkspaceSource(str, Enum): - API = "tfe-api" - MODULE = "tfe-module" - UI = "tfe-ui" - TERRAFORM = "terraform" - - -class WorkspaceActions(BaseModel): - is_destroyable: bool = False - - -class WorkspacePermissions(BaseModel): - can_destroy: bool = False - can_force_unlock: bool = False - can_lock: bool = False - can_manage_run_tasks: bool = False - can_queue_apply: bool = False - can_queue_destroy: bool = False - can_queue_run: bool = False - can_read_settings: bool = False - can_unlock: bool = False - can_update: bool = False - can_update_variable: bool = False - can_force_delete: bool | None = None - - -class WorkspaceSettingOverwrites(BaseModel): - execution_mode: bool | None = None - agent_pool: bool | None = None - - -class WorkspaceOutputs(BaseModel): - id: str - name: str - sensitive: bool = False - output_type: str - value: Any | None = None - - -class LockedByChoice(BaseModel): - run: Any | None = None - user: Any | None = None - team: Any | None = None - - -class WorkspaceListOptions(BaseModel): - """Options for listing workspaces.""" - - # Pagination options (from ListOptions) - page_number: int | None = None - page_size: int | None = None - - # Search and filter options - search: str | None = None # search[name] - partial workspace name - tags: str | None = None # search[tags] - comma-separated tag names - exclude_tags: str | None = ( - None # search[exclude-tags] - comma-separated tag names to exclude - ) - wildcard_name: str | None = None # search[wildcard-name] - substring matching - project_id: str | None = None # filter[project][id] - project ID filter - current_run_status: str | None = ( - None # filter[current-run][status] - run status filter - ) - - # Tag binding filters (not URL encoded, handled specially) - tag_bindings: list[TagBinding] = Field(default_factory=list) - - # Include related resources - include: list[WorkspaceIncludeOpt] = Field(default_factory=list) - - # Sorting options - sort: str | None = ( - None # "name" (default) or "current-run.created-at", prepend "-" to reverse - ) - - -class WorkspaceReadOptions(BaseModel): - include: list[WorkspaceIncludeOpt] = Field(default_factory=list) - - -class WorkspaceCreateOptions(BaseModel): - name: str - type: str = "workspaces" - agent_pool_id: str | None = None - allow_destroy_plan: bool | None = None - assessments_enabled: bool | None = None - auto_apply: bool | None = None - auto_apply_run_trigger: bool | None = None - auto_destroy_at: datetime | None = None - auto_destroy_activity_duration: str | None = None - inherits_project_auto_destroy: bool | None = None - description: str | None = None - execution_mode: ExecutionMode | None = None - file_triggers_enabled: bool | None = None - global_remote_state: bool | None = None - migration_environment: str | None = None - operations: bool | None = None - queue_all_runs: bool | None = None - speculative_enabled: bool | None = None - source_name: str | None = None - source_url: str | None = None - structured_run_output_enabled: bool | None = None - terraform_version: str | None = None - trigger_prefixes: list[str] = Field(default_factory=list) - trigger_patterns: list[str] = Field(default_factory=list) - vcs_repo: VCSRepo | None = None - working_directory: str | None = None - hyok_enabled: bool | None = None - tags: list[Tag] = Field(default_factory=list) - setting_overwrites: WorkspaceSettingOverwrites | None = None - project: Project | None = None - tag_bindings: list[TagBinding] = Field(default_factory=list) - - -class WorkspaceUpdateOptions(BaseModel): - name: str - type: str = "workspaces" - agent_pool_id: str | None = None - allow_destroy_plan: bool | None = None - assessments_enabled: bool | None = None - auto_apply: bool | None = None - auto_apply_run_trigger: bool | None = None - auto_destroy_at: datetime | None = None - auto_destroy_activity_duration: str | None = None - inherits_project_auto_destroy: bool | None = None - description: str | None = None - execution_mode: ExecutionMode | None = None - file_triggers_enabled: bool | None = None - global_remote_state: bool | None = None - operations: bool | None = None - queue_all_runs: bool | None = None - speculative_enabled: bool | None = None - structured_run_output_enabled: bool | None = None - terraform_version: str | None = None - trigger_prefixes: list[str] = Field(default_factory=list) - trigger_patterns: list[str] = Field(default_factory=list) - vcs_repo: VCSRepo | None = None - working_directory: str | None = None - hyok_enabled: bool | None = None - setting_overwrites: WorkspaceSettingOverwrites | None = None - project: Project | None = None - tag_bindings: list[TagBinding] = Field(default_factory=list) - - -class WorkspaceList(BaseModel): - items: list[Workspace] = Field(default_factory=list) - pagination: Pagination | None = None - - -class TagList(BaseModel): - items: list[Tag] = Field(default_factory=list) - pagination: Pagination | None = None - - -class WorkspaceRemoveVCSConnectionOptions(BaseModel): - """Options for removing VCS connection from a workspace.""" - - # Currently no options are defined, but this class can be extended in the future - id: str - vcs_repo: VCSRepo | None = None - - -class WorkspaceLockOptions(BaseModel): - """Options for locking a workspace.""" - - # Specifies the reason for locking the workspace. - reason: str - - -class WorkspaceAssignSSHKeyOptions(BaseModel): - """Options for assigning an SSH key to a workspace.""" - - ssh_key_id: str - type: str = "workspaces" - - -class workspaceUnassignSSHKeyOptions(BaseModel): - """Options for unassigning an SSH key from a workspace.""" - - # Must be nil to unset the currently assigned SSH key. - ssh_key_id: str - type: str = "workspaces" - - -class WorkspaceListRemoteStateConsumersOptions(BaseModel): - """Options for listing remote state consumers of a workspace.""" - - # Pagination options (from ListOptions) - page_number: int | None = None - page_size: int | None = None - - -class WorkspaceAddRemoteStateConsumersOptions(BaseModel): - """Options for adding remote state consumers to a workspace.""" - - workspaces: list[Workspace] = Field(default_factory=list) - - -class WorkspaceRemoveRemoteStateConsumersOptions(BaseModel): - """Options for removing remote state consumers from a workspace.""" - - workspaces: list[Workspace] = Field(default_factory=list) - - -class WorkspaceUpdateRemoteStateConsumersOptions(BaseModel): - """Options for updating remote state consumers of a workspace.""" - - workspaces: list[Workspace] = Field(default_factory=list) - - -class WorkspaceTagListOptions(BaseModel): - """Options for listing tags of a workspace.""" - - # Pagination options (from ListOptions) - page_number: int | None = None - page_size: int | None = None - query: str | None = None - - -class WorkspaceAddTagsOptions(BaseModel): - """Options for adding tags to a workspace.""" - - tags: list[Tag] = Field(default_factory=list) - - -class WorkspaceRemoveTagsOptions(BaseModel): - """Options for removing tags from a workspace.""" - - tags: list[Tag] = Field(default_factory=list) - - -class WorkspaceAddTagBindingsOptions(BaseModel): - """Options for adding tag bindings to a workspace.""" - - tag_bindings: list[TagBinding] = Field(default_factory=list) - - -# Variable Set related types - - -class VariableSetIncludeOpt(str, Enum): - """Include options for variable set operations.""" - - WORKSPACES = "workspaces" - PROJECTS = "projects" - VARS = "vars" - CURRENT_RUN = "current-run" - - -class Parent(BaseModel): - """Parent represents the variable set's parent (organizations and projects are supported).""" - - organization: Organization | None = None - project: Project | None = None - - -class VariableSet(BaseModel): - """Represents a Terraform Enterprise variable set.""" - - id: str | None = None - name: str | None = None - description: str | None = None - global_: bool | None = Field(default=None, alias="global") - priority: bool | None = None - created_at: datetime | None = None - updated_at: datetime | None = None - - # Relations - organization: Organization | None = None - workspaces: list[Workspace] = Field(default_factory=list) - projects: list[Project] = Field(default_factory=list) - vars: list[VariableSetVariable] = Field(default_factory=list) - parent: Parent | None = None - - -class VariableSetVariable(BaseModel): - """Represents a variable within a variable set.""" - - id: str | None = None - key: str - value: str | None = None - description: str | None = None - category: CategoryType - hcl: bool | None = None - sensitive: bool | None = None - version_id: str | None = None - - # Relations - variable_set: VariableSet | None = None - - -# Variable Set Options - - -class VariableSetListOptions(BaseModel): - """Options for listing variable sets.""" - - # Pagination options - page_number: int | None = None - page_size: int | None = None - include: list[VariableSetIncludeOpt] | None = None - query: str | None = None # Filter by name - - -class VariableSetCreateOptions(BaseModel): - """Options for creating a variable set.""" - - name: str - description: str | None = None - global_: bool = Field(alias="global") - priority: bool | None = None - parent: Parent | None = None - - -class VariableSetReadOptions(BaseModel): - """Options for reading a variable set.""" - - include: list[VariableSetIncludeOpt] | None = None - - -class VariableSetUpdateOptions(BaseModel): - """Options for updating a variable set.""" - - name: str | None = None - description: str | None = None - global_: bool | None = Field(alias="global", default=None) - priority: bool | None = None - - -class VariableSetApplyToWorkspacesOptions(BaseModel): - """Options for applying a variable set to workspaces.""" - - workspaces: list[Workspace] = Field(default_factory=list) - - -class VariableSetRemoveFromWorkspacesOptions(BaseModel): - """Options for removing a variable set from workspaces.""" - - workspaces: list[Workspace] = Field(default_factory=list) - - -class VariableSetApplyToProjectsOptions(BaseModel): - """Options for applying a variable set to projects.""" - - projects: list[Project] = Field(default_factory=list) - - -class VariableSetRemoveFromProjectsOptions(BaseModel): - """Options for removing a variable set from projects.""" - - projects: list[Project] = Field(default_factory=list) - - -class VariableSetUpdateWorkspacesOptions(BaseModel): - """Options for updating workspaces associated with a variable set.""" - - workspaces: list[Workspace] = Field(default_factory=list) - - -# Variable Set Variable Options - - -class VariableSetVariableListOptions(BaseModel): - """Options for listing variables in a variable set.""" - - # Pagination options - page_number: int | None = None - page_size: int | None = None - - -class VariableSetVariableCreateOptions(BaseModel): - """Options for creating a variable in a variable set.""" - - key: str - value: str | None = None - description: str | None = None - category: CategoryType - hcl: bool | None = None - sensitive: bool | None = None - - -class VariableSetVariableUpdateOptions(BaseModel): - """Options for updating a variable in a variable set.""" - - key: str | None = None - value: str | None = None - description: str | None = None - hcl: bool | None = None - sensitive: bool | None = None diff --git a/tests/units/test_agent_pools.py b/tests/units/test_agent_pools.py index f4e29ee..a113ac6 100644 --- a/tests/units/test_agent_pools.py +++ b/tests/units/test_agent_pools.py @@ -15,8 +15,8 @@ import pytest -from tfe.errors import AuthError, NotFound, ValidationError -from tfe.models.agent import ( +from pytfe.errors import AuthError, NotFound, ValidationError +from pytfe.models.agent import ( AgentPool, AgentPoolAllowedWorkspacePolicy, AgentPoolCreateOptions, @@ -98,7 +98,7 @@ def mock_transport(self): @pytest.fixture def agent_pools_service(self, mock_transport): """Create agent pools service with mocked transport.""" - from tfe.resources.agent_pools import AgentPools + from pytfe.resources.agent_pools import AgentPools return AgentPools(mock_transport) @@ -276,7 +276,7 @@ def mock_transport(self): @pytest.fixture def agent_tokens_service(self, mock_transport): """Create agent tokens service with mocked transport.""" - from tfe.resources.agents import AgentTokens + from pytfe.resources.agents import AgentTokens return AgentTokens(mock_transport) @@ -396,7 +396,7 @@ def mock_transport(self): @pytest.fixture def agent_pools_service(self, mock_transport): """Create agent pools service with mocked transport.""" - from tfe.resources.agent_pools import AgentPools + from pytfe.resources.agent_pools import AgentPools return AgentPools(mock_transport) diff --git a/tests/units/test_agents.py b/tests/units/test_agents.py index 446c98c..94e9b33 100644 --- a/tests/units/test_agents.py +++ b/tests/units/test_agents.py @@ -14,8 +14,8 @@ import pytest -from tfe.errors import AuthError, NotFound -from tfe.models.agent import ( +from pytfe.errors import AuthError, NotFound +from pytfe.models.agent import ( Agent, AgentStatus, ) @@ -72,7 +72,7 @@ def mock_transport(self): @pytest.fixture def agents_service(self, mock_transport): """Create agents service with mocked transport.""" - from tfe.resources.agents import Agents + from pytfe.resources.agents import Agents return Agents(mock_transport) @@ -154,7 +154,7 @@ def mock_transport(self): @pytest.fixture def agents_service(self, mock_transport): """Create agents service with mocked transport.""" - from tfe.resources.agents import Agents + from pytfe.resources.agents import Agents return Agents(mock_transport) diff --git a/tests/units/test_apply.py b/tests/units/test_apply.py index 7553873..458c87b 100644 --- a/tests/units/test_apply.py +++ b/tests/units/test_apply.py @@ -5,9 +5,9 @@ import unittest from unittest.mock import MagicMock, patch -from tfe.errors import InvalidApplyIDError -from tfe.models.apply import Apply -from tfe.resources.apply import Applies +from pytfe.errors import InvalidApplyIDError +from pytfe.models.apply import Apply +from pytfe.resources.apply import Applies class TestApplies(unittest.TestCase): @@ -65,7 +65,7 @@ def test_read_apply_success(self): assert result.resource_destructions == 0 assert result.resource_imports == 0 - @patch("tfe.resources.apply.Applies.read") + @patch("pytfe.resources.apply.Applies.read") def test_logs_success(self, mock_read): """Test successful logs retrieval.""" # Mock the apply object @@ -81,7 +81,7 @@ def test_logs_success(self, mock_read): # Verify it returns empty string (placeholder implementation) assert result == "" - @patch("tfe.resources.apply.Applies.read") + @patch("pytfe.resources.apply.Applies.read") def test_logs_no_url_error(self, mock_read): """Test logs method when apply has no log URL.""" # Mock apply with no log URL diff --git a/tests/units/test_configuration_version.py b/tests/units/test_configuration_version.py index 2775351..f4f0fa2 100644 --- a/tests/units/test_configuration_version.py +++ b/tests/units/test_configuration_version.py @@ -21,8 +21,7 @@ import pytest -from src.tfe.errors import NotFound, TFEError -from src.tfe.models.configuration_version_types import ( +from pytfe.models.configuration_version import ( ConfigurationSource, ConfigurationStatus, ConfigurationVersionCreateOptions, @@ -30,7 +29,8 @@ ConfigurationVersionReadOptions, ConfigVerIncludeOpt, ) -from src.tfe.resources.configuration_version import ConfigurationVersions +from src.pytfe.errors import NotFound, TFEError +from src.pytfe.resources.configuration_version import ConfigurationVersions @pytest.fixture @@ -348,11 +348,11 @@ def test_upload_missing_slug(self, configuration_versions_service): upload_url = "https://example.com/upload" directory_path = "/tmp/test" - with patch("src.tfe.utils.slug", None): + with patch("src.pytfe.utils.slug", None): with pytest.raises(ImportError, match="go-slug package is required"): configuration_versions_service.upload(upload_url, directory_path) - @patch("src.tfe.utils.slug") + @patch("src.pytfe.utils.slug") def test_upload_success(self, mock_slug, configuration_versions_service): """Test successful upload.""" # Mock slug.pack @@ -645,7 +645,7 @@ class TestConfigurationVersionsValidation: def test_valid_string_id_valid(self, configuration_versions_service): """Test valid_string_id with valid configuration version ID.""" - from src.tfe.utils import valid_string_id + from src.pytfe.utils import valid_string_id # This should return True and not raise an exception result = valid_string_id("cv-ntv3HbhJqvFzamy7") @@ -653,7 +653,7 @@ def test_valid_string_id_valid(self, configuration_versions_service): def test_valid_string_id_invalid(self, configuration_versions_service): """Test valid_string_id with invalid configuration version ID.""" - from src.tfe.utils import valid_string_id + from src.pytfe.utils import valid_string_id # This should return False result = valid_string_id("") diff --git a/tests/units/test_oauth_client.py b/tests/units/test_oauth_client.py index 14820ee..00e4ff3 100644 --- a/tests/units/test_oauth_client.py +++ b/tests/units/test_oauth_client.py @@ -9,12 +9,12 @@ import pytest -from src.tfe._http import HTTPTransport -from src.tfe.errors import ( +from src.pytfe._http import HTTPTransport +from src.pytfe.errors import ( ERR_INVALID_OAUTH_CLIENT_ID, ERR_INVALID_ORG, ) -from src.tfe.models.oauth_client import ( +from src.pytfe.models.oauth_client import ( OAuthClientAddProjectsOptions, OAuthClientCreateOptions, OAuthClientIncludeOpt, @@ -24,7 +24,7 @@ OAuthClientUpdateOptions, ServiceProviderType, ) -from src.tfe.resources.oauth_client import OAuthClients +from src.pytfe.resources.oauth_client import OAuthClients class TestOAuthClientParsing: diff --git a/tests/units/test_oauth_token.py b/tests/units/test_oauth_token.py index 135548f..1af0708 100644 --- a/tests/units/test_oauth_token.py +++ b/tests/units/test_oauth_token.py @@ -9,16 +9,16 @@ import pytest -from src.tfe._http import HTTPTransport -from src.tfe.errors import ( +from src.pytfe._http import HTTPTransport +from src.pytfe.errors import ( ERR_INVALID_OAUTH_TOKEN_ID, ERR_INVALID_ORG, ) -from src.tfe.models.oauth_token import ( +from src.pytfe.models.oauth_token import ( OAuthTokenListOptions, OAuthTokenUpdateOptions, ) -from src.tfe.resources.oauth_token import OAuthTokens +from src.pytfe.resources.oauth_token import OAuthTokens class TestOAuthTokenParsing: diff --git a/tests/units/test_plan.py b/tests/units/test_plan.py index 0d933cf..a8d5146 100644 --- a/tests/units/test_plan.py +++ b/tests/units/test_plan.py @@ -4,8 +4,8 @@ import pytest -from tfe.errors import InvalidPlanIDError -from tfe.resources.plan import Plans +from pytfe.errors import InvalidPlanIDError +from pytfe.resources.plan import Plans class TestPlans: diff --git a/tests/units/test_policy.py b/tests/units/test_policy.py index 258cc43..af4790c 100644 --- a/tests/units/test_policy.py +++ b/tests/units/test_policy.py @@ -4,21 +4,21 @@ import pytest -from tfe._http import HTTPTransport -from tfe.errors import ( +from pytfe._http import HTTPTransport +from pytfe.errors import ( InvalidOrgError, InvalidPolicyIDError, RequiredNameError, ) -from tfe.models.policy import ( +from pytfe.models.policy import ( EnforcementLevel, Policy, PolicyCreateOptions, PolicyList, PolicyUpdateOptions, ) -from tfe.models.policy_set import PolicyKind -from tfe.resources.policy import Policies +from pytfe.models.policy_set import PolicyKind +from pytfe.resources.policy import Policies class TestPolicies: diff --git a/tests/units/test_project.py b/tests/units/test_project.py index 7876f74..262787a 100644 --- a/tests/units/test_project.py +++ b/tests/units/test_project.py @@ -1,14 +1,16 @@ from unittest.mock import Mock -from tfe.resources.projects import Projects, _safe_str -from tfe.types import ( +from pytfe.models import ( EffectiveTagBinding, +) +from pytfe.models.project import ( Project, ProjectAddTagBindingsOptions, ProjectCreateOptions, ProjectUpdateOptions, TagBinding, ) +from pytfe.resources.projects import Projects, _safe_str class TestProjects: diff --git a/tests/units/test_query_run.py b/tests/units/test_query_run.py index d9396ca..8808090 100644 --- a/tests/units/test_query_run.py +++ b/tests/units/test_query_run.py @@ -3,9 +3,9 @@ import pytest -from tfe import TFEClient, TFEConfig -from tfe.errors import InvalidOrgError, InvalidQueryRunIDError -from tfe.models.query_run import ( +from pytfe import TFEClient, TFEConfig +from pytfe.errors import InvalidOrgError, InvalidQueryRunIDError +from pytfe.models.query_run import ( QueryRun, QueryRunCancelOptions, QueryRunCreateOptions, @@ -450,7 +450,7 @@ def client(self): from unittest.mock import MagicMock, patch # Mock the HTTPTransport to prevent any network calls during initialization - with patch("tfe.client.HTTPTransport") as mock_transport_class: + with patch("pytfe.client.HTTPTransport") as mock_transport_class: mock_transport_instance = MagicMock() mock_transport_class.return_value = mock_transport_instance diff --git a/tests/units/test_reserved_tag_key.py b/tests/units/test_reserved_tag_key.py index 318d11a..12cbc50 100644 --- a/tests/units/test_reserved_tag_key.py +++ b/tests/units/test_reserved_tag_key.py @@ -4,17 +4,17 @@ import pytest -from src.tfe._http import HTTPTransport -from src.tfe.errors import ( +from src.pytfe._http import HTTPTransport +from src.pytfe.errors import ( InvalidOrgError, ValidationError, ) -from src.tfe.models.reserved_tag_key import ( +from src.pytfe.models.reserved_tag_key import ( ReservedTagKeyCreateOptions, ReservedTagKeyListOptions, ReservedTagKeyUpdateOptions, ) -from src.tfe.resources.reserved_tag_key import ReservedTagKey +from src.pytfe.resources.reserved_tag_key import ReservedTagKey class TestReservedTagKeyParsing: diff --git a/tests/units/test_run.py b/tests/units/test_run.py index b21c737..bce2d2a 100644 --- a/tests/units/test_run.py +++ b/tests/units/test_run.py @@ -4,13 +4,13 @@ import pytest -from tfe._http import HTTPTransport -from tfe.errors import ( +from pytfe._http import HTTPTransport +from pytfe.errors import ( InvalidRunIDError, RequiredWorkspaceError, TerraformVersionValidForPlanOnlyError, ) -from tfe.models.run import ( +from pytfe.models.run import ( OrganizationRunList, Run, RunApplyOptions, @@ -27,8 +27,8 @@ RunStatus, RunVariable, ) -from tfe.models.workspace import Workspace -from tfe.resources.run import Runs +from pytfe.models.workspace import Workspace +from pytfe.resources.run import Runs class TestRuns: diff --git a/tests/units/test_run_task.py b/tests/units/test_run_task.py index 8a8ce15..a428d79 100644 --- a/tests/units/test_run_task.py +++ b/tests/units/test_run_task.py @@ -4,16 +4,16 @@ import pytest -from tfe._http import HTTPTransport -from tfe.errors import ( +from pytfe._http import HTTPTransport +from pytfe.errors import ( InvalidOrgError, InvalidRunTaskCategoryError, InvalidRunTaskIDError, InvalidRunTaskURLError, RequiredNameError, ) -from tfe.models.agent import AgentPool -from tfe.models.run_task import ( +from pytfe.models.agent import AgentPool +from pytfe.models.run_task import ( GlobalRunTaskOptions, RunTaskCreateOptions, RunTaskIncludeOptions, @@ -23,7 +23,7 @@ Stage, TaskEnforcementLevel, ) -from tfe.resources.run_task import RunTasks, _run_task_from +from pytfe.resources.run_task import RunTasks, _run_task_from class TestRunTaskFrom: diff --git a/tests/units/test_run_trigger.py b/tests/units/test_run_trigger.py index a0cbfda..901dafb 100644 --- a/tests/units/test_run_trigger.py +++ b/tests/units/test_run_trigger.py @@ -5,8 +5,8 @@ import pytest -from tfe._http import HTTPTransport -from tfe.errors import ( +from pytfe._http import HTTPTransport +from pytfe.errors import ( InvalidRunTriggerIDError, InvalidRunTriggerTypeError, InvalidWorkspaceIDError, @@ -14,7 +14,7 @@ RequiredSourceableError, UnsupportedRunTriggerTypeError, ) -from tfe.models.run_trigger import ( +from pytfe.models.run_trigger import ( RunTrigger, RunTriggerCreateOptions, RunTriggerFilterOp, @@ -22,8 +22,8 @@ RunTriggerListOptions, SourceableChoice, ) -from tfe.models.workspace import Workspace -from tfe.resources.run_trigger import RunTriggers, _run_trigger_from +from pytfe.models.workspace import Workspace +from pytfe.resources.run_trigger import RunTriggers, _run_trigger_from class TestRunTriggerFrom: @@ -310,7 +310,7 @@ def test_validate_run_trigger_filter_param_validations(self, run_triggers_servic """Test validation with invalid filter parameter.""" # This should be tested by mocking the enum validation - with patch("tfe.resources.run_trigger.RunTriggerFilterOp") as mock_enum: + with patch("pytfe.resources.run_trigger.RunTriggerFilterOp") as mock_enum: mock_enum.__contains__ = Mock(return_value=False) with pytest.raises(InvalidRunTriggerTypeError): diff --git a/tests/units/test_ssh_keys.py b/tests/units/test_ssh_keys.py index dde405c..057b9ac 100644 --- a/tests/units/test_ssh_keys.py +++ b/tests/units/test_ssh_keys.py @@ -4,16 +4,16 @@ import pytest -from src.tfe._http import HTTPTransport -from src.tfe.errors import ( +from src.pytfe._http import HTTPTransport +from src.pytfe.errors import ( InvalidOrgError, InvalidSSHKeyIDError, ) -from src.tfe.models.ssh_key import ( +from src.pytfe.models.ssh_key import ( SSHKeyCreateOptions, SSHKeyUpdateOptions, ) -from src.tfe.resources.ssh_keys import SSHKeys +from src.pytfe.resources.ssh_keys import SSHKeys class TestSSHKeyParsing: diff --git a/tests/units/test_transport.py b/tests/units/test_transport.py index cad04eb..3d4fac3 100644 --- a/tests/units/test_transport.py +++ b/tests/units/test_transport.py @@ -1,5 +1,5 @@ -from tfe._http import HTTPTransport -from tfe.config import TFEConfig +from pytfe._http import HTTPTransport +from pytfe.config import TFEConfig def test_http_transport_init(): diff --git a/tests/units/test_variable_sets.py b/tests/units/test_variable_sets.py index 77a672d..01ad868 100644 --- a/tests/units/test_variable_sets.py +++ b/tests/units/test_variable_sets.py @@ -4,8 +4,7 @@ import pytest -from tfe.resources.variable_sets import VariableSets, VariableSetVariables -from tfe.types import ( +from pytfe.models.variable_set import ( CategoryType, Parent, Project, @@ -25,6 +24,7 @@ VariableSetVariableUpdateOptions, Workspace, ) +from pytfe.resources.variable_sets import VariableSets, VariableSetVariables class TestVariableSets: diff --git a/tests/units/test_workspaces.py b/tests/units/test_workspaces.py index 29c838c..9448e8d 100644 --- a/tests/units/test_workspaces.py +++ b/tests/units/test_workspaces.py @@ -9,7 +9,7 @@ import pytest -from src.tfe.errors import ( +from src.pytfe.errors import ( InvalidOrgError, InvalidSSHKeyIDError, InvalidWorkspaceIDError, @@ -19,16 +19,21 @@ RequiredSSHKeyIDError, WorkspaceMinimumLimitError, ) -from src.tfe.resources.workspaces import Workspaces, _ws_from -from src.tfe.types import ( +from src.pytfe.models.common import ( + EffectiveTagBinding, + Tag, + TagBinding, +) +from src.pytfe.models.data_retention_policy import ( DataRetentionPolicyDeleteOlderSetOptions, DataRetentionPolicyDontDeleteSetOptions, DataRetentionPolicySetOptions, - EffectiveTagBinding, +) +from src.pytfe.models.organization import ( ExecutionMode, - Project, - Tag, - TagBinding, +) +from src.pytfe.models.project import Project +from src.pytfe.models.workspace import ( VCSRepo, Workspace, WorkspaceAddRemoteStateConsumersOptions, @@ -47,6 +52,7 @@ WorkspaceUpdateOptions, WorkspaceUpdateRemoteStateConsumersOptions, ) +from src.pytfe.resources.workspaces import Workspaces, _ws_from class TestWorkspaceOperations: @@ -233,7 +239,7 @@ def test_read_workspace_with_options( sample_workspace_response ) - from src.tfe.types import WorkspaceIncludeOpt + from src.pytfe.models.workspace import WorkspaceIncludeOpt options = WorkspaceReadOptions( include=[WorkspaceIncludeOpt.CURRENT_RUN, WorkspaceIncludeOpt.OUTPUTS]