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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions examples/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env python3
"""Example usage of the Users API.

This example demonstrates how to read a user by ID using the Python TFE SDK.
"""

import os
import sys

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

from pytfe import TFEClient, TFEConfig


def main() -> None:
"""Read and print user details from Terraform Cloud."""
user_id = os.getenv("TFE_USER_ID")

try:
client = TFEClient(TFEConfig.from_env())

current_user = client.users.read_current()
print("=== Current Terraform Cloud User ===")
print(f"User ID: {current_user.id}")
print(f"Username: {current_user.username}")
print(f"Email: {current_user.email or 'N/A'}")
print(f"Auth Method: {current_user.auth_method or 'N/A'}")

if not user_id:
print("\nTFE_USER_ID not set. Skipping client.users.read(user_id).")
return

user = client.users.read(user_id)

print("\n=== Terraform Cloud User By ID ===")
print(f"User ID: {user.id}")
print(f"Username: {user.username}")
print(f"Email: {user.email or 'N/A'}")
print(f"Auth Method: {user.auth_method or 'N/A'}")
except Exception as e:
print(f"Error running user example: {e}")


if __name__ == "__main__":
main()
2 changes: 2 additions & 0 deletions src/pytfe/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from .resources.ssh_keys import SSHKeys
from .resources.state_version_outputs import StateVersionOutputs
from .resources.state_versions import StateVersions
from .resources.user import Users
from .resources.variable import Variables
from .resources.variable_sets import VariableSets, VariableSetVariables
from .resources.workspace_resources import WorkspaceResourcesService
Expand Down Expand Up @@ -73,6 +74,7 @@ def __init__(self, config: TFEConfig | None = None):
self.plans = Plans(self._transport)
self.organizations = Organizations(self._transport)
self.organization_memberships = OrganizationMemberships(self._transport)
self.users = Users(self._transport)
self.organization_tokens = OrganizationTokens(self._transport)
self.projects = Projects(self._transport)
self.variables = Variables(self._transport)
Expand Down
3 changes: 2 additions & 1 deletion src/pytfe/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@
Tag,
TagBinding,
TagList,
) # if you put ExecutionMode enum here
)

# if you put ExecutionMode enum here
# ── Configuration Versions ────────────────────────────────────────────────────
# (Old: .configuration_version_types) → import directly from real module
from .configuration_version import (
Expand Down
48 changes: 37 additions & 11 deletions src/pytfe/models/user.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,52 @@
# Copyright IBM Corp. 2025, 2026
# SPDX-License-Identifier: MPL-2.0
from pydantic import BaseModel, ConfigDict, Field

from __future__ import annotations

from pydantic import BaseModel, ConfigDict, Field
class TwoFactor(BaseModel):
model_config = ConfigDict(populate_by_name=True, validate_by_name=True)

enabled: bool = Field(default=False, alias="enabled")
verified: bool = Field(default=False, alias="verified")


class UserPermissions(BaseModel):
model_config = ConfigDict(populate_by_name=True, validate_by_name=True)

can_create_organizations: bool = Field(
default=False, alias="can-create-organizations"
)
can_change_email: bool = Field(default=False, alias="can-change-email")
can_change_username: bool = Field(default=False, alias="can-change-username")
can_manage_user_tokens: bool = Field(default=False, alias="can-manage-user-tokens")
can_view_2fa_settings: bool = Field(default=False, alias="can-view2fa-settings")
can_manage_hcp_account: bool = Field(default=False, alias="can-manage-hcp-account")


class User(BaseModel):
model_config = ConfigDict(populate_by_name=True, validate_by_name=True)

id: str = Field(..., alias="id")
avatar_url: str = Field(default="", alias="avatar-url")
email: str = Field(default="", alias="email")
auth_method: str | None = Field(default=None, alias="auth-method")
avatar_url: str | None = Field(default=None, alias="avatar-url")
email: str | None = Field(default=None, alias="email")
is_service_account: bool = Field(default=False, alias="is-service-account")
two_factor: dict = Field(default_factory=dict, alias="two-factor")
unconfirmed_email: str = Field(default="", alias="unconfirmed-email")
two_factor: TwoFactor | None = Field(default=None, alias="two-factor")
unconfirmed_email: str | None = Field(default=None, alias="unconfirmed-email")
username: str = Field(default="", alias="username")
v2_only: bool = Field(default=False, alias="v2-only")
is_site_admin: bool = Field(default=False, alias="is-site-admin") # Deprecated
is_admin: bool = Field(default=False, alias="is-admin")
is_sso_login: bool = Field(default=False, alias="is-sso-login")
permissions: dict = Field(default_factory=dict, alias="permissions")

is_site_admin: bool | None = Field(
default=None, alias="is-site-admin"
) # Deprecated
is_admin: bool | None = Field(default=None, alias="is-admin")
is_sso_login: bool | None = Field(default=None, alias="is-sso-login")
permissions: UserPermissions | None = Field(default=None, alias="permissions")
# Relations
# authentication_tokens: AuthenticationTokens = Field(..., alias="authentication-tokens")


class UserUpdateCurrentOptions(BaseModel):
model_config = ConfigDict(populate_by_name=True, validate_by_name=True)

username: str | None = Field(default=None, alias="username")
email: str | None = Field(default=None, alias="email")
40 changes: 40 additions & 0 deletions src/pytfe/resources/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from __future__ import annotations

from ..models.user import User, UserUpdateCurrentOptions
from ..utils import valid_string_id
from ._base import _Service


class Users(_Service):
def read(self, user_id: str) -> User:
if not valid_string_id(user_id):
raise ValueError("invalid user id")

r = self.t.request("GET", f"/api/v2/users/{user_id}")
d = r.json()["data"]
attr = d.get("attributes", {}) or {}
user_data = dict(attr)
user_data["id"] = d.get("id")
return User(**user_data)

def read_current(self) -> User:
r = self.t.request("GET", "/api/v2/account/details")
d = r.json()["data"]
attr = d.get("attributes", {}) or {}
user_data = dict(attr)
user_data["id"] = d.get("id")
return User(**user_data)

def update_current(self, options: UserUpdateCurrentOptions) -> User:
body = {
"data": {
"type": "users",
"attributes": options.model_dump(exclude_none=True),
}
}
r = self.t.request("PATCH", "/api/v2/account/update", json_body=body)
d = r.json()["data"]
attr = d.get("attributes", {}) or {}
user_data = dict(attr)
user_data["id"] = d.get("id")
return User(**user_data)
1 change: 0 additions & 1 deletion tests/units/test_agent_pools.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
from unittest.mock import Mock

import pytest

from pytfe.errors import AuthError, NotFound, ValidationError
from pytfe.models.agent import (
AgentPool,
Expand Down
1 change: 0 additions & 1 deletion tests/units/test_agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
from unittest.mock import Mock

import pytest

from pytfe.errors import AuthError, NotFound
from pytfe.models.agent import (
Agent,
Expand Down
2 changes: 1 addition & 1 deletion tests/units/test_configuration_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
from unittest.mock import Mock

import pytest

from pytfe.models.configuration_version import (
ConfigurationSource,
ConfigurationStatus,
Expand All @@ -32,6 +31,7 @@
ConfigurationVersionReadOptions,
ConfigVerIncludeOpt,
)

from src.pytfe.errors import NotFound, TFEError
from src.pytfe.resources.configuration_version import ConfigurationVersions

Expand Down
1 change: 0 additions & 1 deletion tests/units/test_notification_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
from unittest.mock import Mock

import pytest

from pytfe.errors import InvalidOrgError, ValidationError
from pytfe.models.notification_configuration import (
DeliveryResponse,
Expand Down
1 change: 0 additions & 1 deletion tests/units/test_organization_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from unittest.mock import Mock, patch

import pytest

from pytfe._http import HTTPTransport
from pytfe.errors import ERR_INVALID_ORG
from pytfe.models.organization_token import (
Expand Down
1 change: 0 additions & 1 deletion tests/units/test_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from unittest.mock import Mock, patch

import pytest

from pytfe.errors import InvalidPlanIDError
from pytfe.resources.plan import Plans

Expand Down
1 change: 0 additions & 1 deletion tests/units/test_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from unittest.mock import Mock, patch

import pytest

from pytfe._http import HTTPTransport
from pytfe.errors import (
InvalidOrgError,
Expand Down
1 change: 0 additions & 1 deletion tests/units/test_policy_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from unittest.mock import Mock, patch

import pytest

from pytfe._http import HTTPTransport
from pytfe.errors import InvalidPolicyCheckIDError, InvalidRunIDError
from pytfe.models.policy_check import (
Expand Down
1 change: 0 additions & 1 deletion tests/units/test_policy_evaluation.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from unittest.mock import Mock

import pytest

from pytfe._http import HTTPTransport
from pytfe.errors import InvalidTaskStageIDError
from pytfe.models.policy_evaluation import (
Expand Down
1 change: 0 additions & 1 deletion tests/units/test_policy_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from unittest.mock import Mock

import pytest

from pytfe._http import HTTPTransport
from pytfe.errors import (
InvalidNameError,
Expand Down
1 change: 0 additions & 1 deletion tests/units/test_policy_set_parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from unittest.mock import Mock, patch

import pytest

from pytfe._http import HTTPTransport
from pytfe.errors import (
InvalidCategoryError,
Expand Down
1 change: 0 additions & 1 deletion tests/units/test_query_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
from unittest.mock import Mock, patch

import pytest

from pytfe.errors import InvalidQueryRunIDError, InvalidWorkspaceIDError
from pytfe.models import (
QueryRun,
Expand Down
1 change: 0 additions & 1 deletion tests/units/test_registry_provider_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from unittest.mock import Mock, patch

import pytest

from pytfe._http import HTTPTransport
from pytfe.errors import (
InvalidKeyIDError,
Expand Down
1 change: 0 additions & 1 deletion tests/units/test_reserved_tag_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from unittest.mock import Mock

import pytest

from pytfe._http import HTTPTransport
from pytfe.errors import (
InvalidOrgError,
Expand Down
1 change: 0 additions & 1 deletion tests/units/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from unittest.mock import Mock, patch

import pytest

from pytfe._http import HTTPTransport
from pytfe.errors import (
InvalidRunIDError,
Expand Down
1 change: 0 additions & 1 deletion tests/units/test_run_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from unittest.mock import Mock, patch

import pytest

from pytfe._http import HTTPTransport
from pytfe.errors import InvalidRunEventIDError, InvalidRunIDError
from pytfe.models.run_event import (
Expand Down
1 change: 0 additions & 1 deletion tests/units/test_run_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from unittest.mock import Mock, patch

import pytest

from pytfe._http import HTTPTransport
from pytfe.errors import (
InvalidOrgError,
Expand Down
1 change: 0 additions & 1 deletion tests/units/test_run_trigger.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from unittest.mock import Mock, patch

import pytest

from pytfe._http import HTTPTransport
from pytfe.errors import (
InvalidRunTriggerIDError,
Expand Down
1 change: 0 additions & 1 deletion tests/units/test_ssh_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from unittest.mock import Mock, patch

import pytest

from pytfe._http import HTTPTransport
from pytfe.errors import (
InvalidOrgError,
Expand Down
1 change: 0 additions & 1 deletion tests/units/test_stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from unittest.mock import Mock

import pytest

from pytfe._http import HTTPTransport
from pytfe.models.agent import AgentPool
from pytfe.models.project import Project
Expand Down
1 change: 0 additions & 1 deletion tests/units/test_state_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from unittest.mock import Mock, patch

import pytest

from pytfe._http import HTTPTransport
from pytfe.errors import NotFound
from pytfe.models.state_version import (
Expand Down
1 change: 0 additions & 1 deletion tests/units/test_team.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from unittest.mock import Mock, patch

import pytest

from pytfe._http import HTTPTransport
from pytfe.errors import ERR_INVALID_ORG, InvalidTeamIDError
from pytfe.models import (
Expand Down
1 change: 0 additions & 1 deletion tests/units/test_team_project_access.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from unittest.mock import Mock

import pytest

from pytfe._http import HTTPTransport
from pytfe.errors import InvalidTeamProjectAccessIDError
from pytfe.models.project import Project
Expand Down
Loading
Loading