Skip to content

Commit fe8cda9

Browse files
committed
added inital support for oauth client managment
1 parent 967b1c5 commit fe8cda9

9 files changed

Lines changed: 522 additions & 0 deletions

File tree

pyatlan/client/aio/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from .file import AsyncFileClient
3636
from .group import AsyncGroupClient
3737
from .impersonate import AsyncImpersonationClient
38+
from .oauth_client import AsyncOAuthClientClient
3839
from .open_lineage import AsyncOpenLineageClient
3940
from .query import AsyncQueryClient
4041
from .role import AsyncRoleClient
@@ -61,6 +62,7 @@
6162
"AsyncImpersonationClient",
6263
"AsyncIndexSearchResults",
6364
"AsyncLineageListResults",
65+
"AsyncOAuthClientClient",
6466
"AsyncOpenLineageClient",
6567
"AsyncQueryClient",
6668
"AsyncRoleClient",

pyatlan/client/aio/client.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
from pyatlan.client.aio.group import AsyncGroupClient
4343
from pyatlan.client.aio.impersonate import AsyncImpersonationClient
4444
from pyatlan.client.aio.oauth import AsyncOAuthTokenManager
45+
from pyatlan.client.aio.oauth_client import AsyncOAuthClientClient
4546
from pyatlan.client.aio.open_lineage import AsyncOpenLineageClient
4647
from pyatlan.client.aio.query import AsyncQueryClient
4748
from pyatlan.client.aio.role import AsyncRoleClient
@@ -113,6 +114,9 @@ class AsyncAtlanClient(AtlanClient):
113114
_async_sso_client: Optional[AsyncSSOClient] = PrivateAttr(default=None)
114115
_async_task_client: Optional[AsyncTaskClient] = PrivateAttr(default=None)
115116
_async_token_client: Optional[AsyncTokenClient] = PrivateAttr(default=None)
117+
_async_oauth_client_client: Optional[AsyncOAuthClientClient] = PrivateAttr(
118+
default=None
119+
)
116120
_async_typedef_client: Optional[AsyncTypeDefClient] = PrivateAttr(default=None)
117121
_async_user_client: Optional[AsyncUserClient] = PrivateAttr(default=None)
118122
_async_workflow_client: Optional[AsyncWorkflowClient] = PrivateAttr(default=None)
@@ -362,6 +366,13 @@ def token(self) -> AsyncTokenClient: # type: ignore[override]
362366
self._async_token_client = AsyncTokenClient(self) # type: ignore[arg-type]
363367
return self._async_token_client
364368

369+
@property
370+
def oauth_client(self) -> AsyncOAuthClientClient: # type: ignore[override]
371+
"""Get async OAuth client client with same API as sync"""
372+
if self._async_oauth_client_client is None:
373+
self._async_oauth_client_client = AsyncOAuthClientClient(self) # type: ignore[arg-type]
374+
return self._async_oauth_client_client
375+
365376
@property
366377
def typedef(self) -> AsyncTypeDefClient: # type: ignore[override]
367378
"""Get async typedef client with same API as sync"""

pyatlan/client/aio/oauth_client.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# Copyright 2025 Atlan Pte. Ltd.
3+
4+
from __future__ import annotations
5+
6+
from typing import Optional
7+
8+
from pydantic.v1 import validate_arguments
9+
10+
from pyatlan.client.common import (
11+
AsyncApiCaller,
12+
OAuthClientGet,
13+
OAuthClientGetAll,
14+
OAuthClientGetById,
15+
OAuthClientPurge,
16+
OAuthClientUpdate,
17+
)
18+
from pyatlan.errors import ErrorCode
19+
from pyatlan.model.oauth_clients import OAuthClient, OAuthClientResponse
20+
21+
22+
class AsyncOAuthClientClient:
23+
"""
24+
Async client for managing OAuth client credentials.
25+
"""
26+
27+
def __init__(self, client: AsyncApiCaller):
28+
if not isinstance(client, AsyncApiCaller):
29+
raise ErrorCode.INVALID_PARAMETER_TYPE.exception_with_parameters(
30+
"client", "AsyncApiCaller"
31+
)
32+
self._client = client
33+
34+
async def get_all(self) -> OAuthClientResponse:
35+
"""
36+
Retrieves all OAuth clients defined in Atlan.
37+
38+
:returns: an OAuthClientResponse containing all OAuth clients
39+
:raises AtlanError: on any API communication issue
40+
"""
41+
endpoint, query_params = OAuthClientGetAll.prepare_request()
42+
raw_json = await self._client._call_api(endpoint, query_params)
43+
return OAuthClientGetAll.process_response(raw_json)
44+
45+
@validate_arguments
46+
async def get(
47+
self,
48+
limit: Optional[int] = None,
49+
offset: int = 0,
50+
sort: Optional[str] = None,
51+
) -> OAuthClientResponse:
52+
"""
53+
Retrieves OAuth clients defined in Atlan with pagination support.
54+
55+
:param limit: maximum number of results to be returned
56+
:param offset: starting point for results to return, for paging
57+
:param sort: property by which to sort the results (e.g., 'createdAt' for descending)
58+
:returns: an OAuthClientResponse containing records and pagination info
59+
:raises AtlanError: on any API communication issue
60+
"""
61+
endpoint, query_params = OAuthClientGet.prepare_request(limit, offset, sort)
62+
raw_json = await self._client._call_api(endpoint, query_params)
63+
return OAuthClientGet.process_response(raw_json)
64+
65+
@validate_arguments
66+
async def get_by_id(self, client_id: str) -> OAuthClient:
67+
"""
68+
Retrieves the OAuth client with the specified client ID.
69+
70+
:param client_id: unique client identifier (e.g., 'oauth-client-xxx')
71+
:returns: the OAuthClient with the specified client ID
72+
:raises AtlanError: on any API communication issue
73+
"""
74+
endpoint, query_params = OAuthClientGetById.prepare_request(client_id)
75+
raw_json = await self._client._call_api(endpoint, query_params)
76+
return OAuthClientGetById.process_response(raw_json)
77+
78+
@validate_arguments
79+
async def update(
80+
self,
81+
client_id: str,
82+
display_name: Optional[str] = None,
83+
description: Optional[str] = None,
84+
) -> OAuthClient:
85+
"""
86+
Update an existing OAuth client with the provided settings.
87+
88+
:param client_id: unique client identifier (e.g., 'oauth-client-xxx')
89+
:param display_name: human-readable name for the OAuth client
90+
:param description: optional explanation of the OAuth client
91+
:returns: the updated OAuthClient
92+
:raises AtlanError: on any API communication issue
93+
"""
94+
endpoint, request_obj = OAuthClientUpdate.prepare_request(
95+
client_id, display_name, description
96+
)
97+
raw_json = await self._client._call_api(endpoint, request_obj=request_obj)
98+
return OAuthClientUpdate.process_response(raw_json)
99+
100+
@validate_arguments
101+
async def purge(self, client_id: str) -> None:
102+
"""
103+
Delete (purge) the specified OAuth client.
104+
105+
:param client_id: unique client identifier (e.g., 'oauth-client-xxx')
106+
:raises AtlanError: on any API communication issue
107+
"""
108+
endpoint, _ = OAuthClientPurge.prepare_request(client_id)
109+
await self._client._call_api(endpoint)

pyatlan/client/atlan.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
from pyatlan.client.group import GroupClient
5050
from pyatlan.client.impersonate import ImpersonationClient
5151
from pyatlan.client.oauth import OAuthTokenManager
52+
from pyatlan.client.oauth_client import OAuthClientClient
5253
from pyatlan.client.open_lineage import OpenLineageClient
5354
from pyatlan.client.query import QueryClient
5455
from pyatlan.client.role import RoleClient
@@ -151,6 +152,7 @@ class AtlanClient(BaseSettings):
151152
_asset_client: Optional[AssetClient] = PrivateAttr(default=None)
152153
_typedef_client: Optional[TypeDefClient] = PrivateAttr(default=None)
153154
_token_client: Optional[TokenClient] = PrivateAttr(default=None)
155+
_oauth_client_client: Optional[OAuthClientClient] = PrivateAttr(default=None)
154156
_user_client: Optional[UserClient] = PrivateAttr(default=None)
155157
_impersonate_client: Optional[ImpersonationClient] = PrivateAttr(default=None)
156158
_query_client: Optional[QueryClient] = PrivateAttr(default=None)
@@ -343,6 +345,12 @@ def token(self) -> TokenClient:
343345
self._token_client = TokenClient(client=self)
344346
return self._token_client
345347

348+
@property
349+
def oauth_client(self) -> OAuthClientClient:
350+
if self._oauth_client_client is None:
351+
self._oauth_client_client = OAuthClientClient(client=self)
352+
return self._oauth_client_client
353+
346354
@property
347355
def typedef(self) -> TypeDefClient:
348356
if self._typedef_client is None:

pyatlan/client/common/__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,15 @@
9797
ImpersonateUser,
9898
)
9999

100+
# OAuth client shared logic classes
101+
from .oauth_client import (
102+
OAuthClientGet,
103+
OAuthClientGetAll,
104+
OAuthClientGetById,
105+
OAuthClientPurge,
106+
OAuthClientUpdate,
107+
)
108+
100109
# OpenLineage shared logic classes
101110
from .open_lineage import (
102111
OpenLineageCreateConnection,
@@ -255,6 +264,12 @@
255264
"ImpersonateGetClientSecret",
256265
"ImpersonateGetUserId",
257266
"ImpersonateUser",
267+
# OAuth client shared logic classes
268+
"OAuthClientGet",
269+
"OAuthClientGetAll",
270+
"OAuthClientGetById",
271+
"OAuthClientPurge",
272+
"OAuthClientUpdate",
258273
# OpenLineage shared logic classes
259274
"OpenLineageCreateConnection",
260275
"OpenLineageCreateCredential",
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# Copyright 2025 Atlan Pte. Ltd.
3+
4+
from __future__ import annotations
5+
6+
from typing import Dict, Optional
7+
8+
from pyatlan.client.constants import (
9+
DELETE_OAUTH_CLIENT,
10+
GET_OAUTH_CLIENT_BY_ID,
11+
GET_OAUTH_CLIENTS,
12+
UPDATE_OAUTH_CLIENT,
13+
)
14+
from pyatlan.model.oauth_clients import OAuthClient, OAuthClientResponse
15+
16+
17+
class OAuthClientGetAll:
18+
"""Shared logic for getting all OAuth clients without pagination."""
19+
20+
@staticmethod
21+
def prepare_request() -> tuple:
22+
"""
23+
Prepare the request for getting all OAuth clients.
24+
25+
:returns: tuple of (endpoint, query_params)
26+
"""
27+
return GET_OAUTH_CLIENTS.format_path_with_params(), None
28+
29+
@staticmethod
30+
def process_response(raw_json: Dict) -> OAuthClientResponse:
31+
"""
32+
Process the API response into an OAuthClientResponse object.
33+
34+
:param raw_json: raw response from the API
35+
:returns: OAuthClientResponse with pagination info and records
36+
"""
37+
return OAuthClientResponse(**raw_json)
38+
39+
40+
class OAuthClientGet:
41+
"""Shared logic for getting OAuth clients with pagination."""
42+
43+
@staticmethod
44+
def prepare_request(
45+
limit: Optional[int] = None,
46+
offset: int = 0,
47+
sort: Optional[str] = None,
48+
) -> tuple:
49+
"""
50+
Prepare the request for getting OAuth clients with pagination.
51+
52+
:param limit: maximum number of results to be returned
53+
:param offset: starting point for results to return, for paging
54+
:param sort: property by which to sort the results (e.g., 'createdAt' for descending)
55+
:returns: tuple of (endpoint, query_params)
56+
"""
57+
query_params: Dict[str, str] = {
58+
"count": "true",
59+
"offset": str(offset),
60+
}
61+
if limit is not None:
62+
query_params["limit"] = str(limit)
63+
if sort is not None:
64+
query_params["sort"] = sort
65+
66+
return GET_OAUTH_CLIENTS.format_path_with_params(), query_params
67+
68+
@staticmethod
69+
def process_response(raw_json: Dict) -> OAuthClientResponse:
70+
"""
71+
Process the API response into an OAuthClientResponse object.
72+
73+
:param raw_json: raw response from the API
74+
:returns: OAuthClientResponse with pagination info and records
75+
"""
76+
return OAuthClientResponse(**raw_json)
77+
78+
79+
class OAuthClientGetById:
80+
"""Shared logic for getting an OAuth client by its client ID."""
81+
82+
@staticmethod
83+
def prepare_request(client_id: str) -> tuple:
84+
"""
85+
Prepare the request for getting an OAuth client by its client ID.
86+
87+
:param client_id: unique client identifier (e.g., 'oauth-client-xxx')
88+
:returns: tuple of (endpoint, query_params)
89+
"""
90+
endpoint = GET_OAUTH_CLIENT_BY_ID.format_path(
91+
{"client_id": client_id}
92+
).format_path_with_params()
93+
return endpoint, None
94+
95+
@staticmethod
96+
def process_response(raw_json: Dict) -> OAuthClient:
97+
"""
98+
Process the API response into an OAuthClient object.
99+
100+
:param raw_json: raw response from the API
101+
:returns: OAuthClient object
102+
"""
103+
return OAuthClient(**raw_json)
104+
105+
106+
class OAuthClientUpdate:
107+
"""Shared logic for updating OAuth clients."""
108+
109+
@staticmethod
110+
def prepare_request(
111+
client_id: str,
112+
display_name: Optional[str] = None,
113+
description: Optional[str] = None,
114+
) -> tuple:
115+
"""
116+
Prepare the request for updating an OAuth client.
117+
118+
:param client_id: unique client identifier (e.g., 'oauth-client-xxx')
119+
:param display_name: human-readable name for the OAuth client
120+
:param description: optional explanation of the OAuth client
121+
:returns: tuple of (endpoint, request_dict)
122+
"""
123+
# Build request dict with only non-None values to avoid sending null fields
124+
request_dict: Dict[str, str] = {}
125+
if display_name is not None:
126+
request_dict["displayName"] = display_name
127+
if description is not None:
128+
request_dict["description"] = description
129+
130+
endpoint = UPDATE_OAUTH_CLIENT.format_path(
131+
{"client_id": client_id}
132+
).format_path_with_params()
133+
return endpoint, request_dict
134+
135+
@staticmethod
136+
def process_response(raw_json: Dict) -> OAuthClient:
137+
"""
138+
Process the API response into an OAuthClient.
139+
140+
:param raw_json: raw response from the API
141+
:returns: the updated OAuthClient
142+
"""
143+
return OAuthClient(**raw_json)
144+
145+
146+
class OAuthClientPurge:
147+
"""Shared logic for deleting OAuth clients."""
148+
149+
@staticmethod
150+
def prepare_request(client_id: str) -> tuple:
151+
"""
152+
Prepare the request for deleting an OAuth client.
153+
154+
:param client_id: unique client identifier (e.g., 'oauth-client-xxx')
155+
:returns: tuple of (endpoint, None)
156+
"""
157+
endpoint = DELETE_OAUTH_CLIENT.format_path(
158+
{"client_id": client_id}
159+
).format_path_with_params()
160+
return endpoint, None

0 commit comments

Comments
 (0)