gl-connectors-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gl_connectors_cli/__init__.py +5 -0
- gl_connectors_cli/__main__.py +12 -0
- gl_connectors_cli/api/__init__.py +19 -0
- gl_connectors_cli/api/auth.py +47 -0
- gl_connectors_cli/api/base.py +86 -0
- gl_connectors_cli/api/integrations.py +199 -0
- gl_connectors_cli/api/models.py +47 -0
- gl_connectors_cli/api/users.py +66 -0
- gl_connectors_cli/commands/__init__.py +1 -0
- gl_connectors_cli/commands/auth.py +236 -0
- gl_connectors_cli/commands/base.py +63 -0
- gl_connectors_cli/commands/integrations.py +533 -0
- gl_connectors_cli/commands/users.py +144 -0
- gl_connectors_cli/config.py +223 -0
- gl_connectors_cli/constants.py +119 -0
- gl_connectors_cli/main.py +103 -0
- gl_connectors_cli/utils.py +327 -0
- gl_connectors_cli-0.1.0.dist-info/METADATA +200 -0
- gl_connectors_cli-0.1.0.dist-info/RECORD +22 -0
- gl_connectors_cli-0.1.0.dist-info/WHEEL +5 -0
- gl_connectors_cli-0.1.0.dist-info/entry_points.txt +2 -0
- gl_connectors_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""GL Connectors CLI API clients.
|
|
2
|
+
|
|
3
|
+
Author:
|
|
4
|
+
I Gusti Ngurah Gana Untaran (i.gusti.n.g.untaran@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from gl_connectors_cli.api.auth import AuthAPIClient
|
|
8
|
+
from gl_connectors_cli.api.integrations import IntegrationsAPIClient
|
|
9
|
+
from gl_connectors_cli.api.models import IntegrationDetail, Token, User
|
|
10
|
+
from gl_connectors_cli.api.users import UsersAPIClient
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"AuthAPIClient",
|
|
14
|
+
"UsersAPIClient",
|
|
15
|
+
"IntegrationsAPIClient",
|
|
16
|
+
"User",
|
|
17
|
+
"Token",
|
|
18
|
+
"IntegrationDetail",
|
|
19
|
+
]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Authentication API client for GL Connectors CLI.
|
|
2
|
+
|
|
3
|
+
Author:
|
|
4
|
+
I Gusti Ngurah Gana Untaran (i.gusti.n.g.untaran@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from gl_connectors_cli.api.base import BaseAPIClient
|
|
8
|
+
from gl_connectors_cli.api.models import Token
|
|
9
|
+
from gl_connectors_cli.constants import API_KEY_HEADER, HTTP_POST
|
|
10
|
+
from gl_connectors_cli.utils import CLIError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AuthAPIClient(BaseAPIClient):
|
|
14
|
+
"""Authentication API client."""
|
|
15
|
+
|
|
16
|
+
def authenticate_user(self, client_key: str, identifier: str, secret: str) -> Token:
|
|
17
|
+
"""Authenticate user and get JWT token.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
client_key: Client API key
|
|
21
|
+
identifier: User identifier
|
|
22
|
+
secret: User secret
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Authentication token information
|
|
26
|
+
|
|
27
|
+
Raises:
|
|
28
|
+
CLIError: If authentication fails
|
|
29
|
+
|
|
30
|
+
"""
|
|
31
|
+
headers = {
|
|
32
|
+
API_KEY_HEADER: client_key,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
data = {"identifier": identifier, "secret": secret}
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
response = self._make_request(HTTP_POST, "/clients/oauth-token", headers=headers, data=data)
|
|
39
|
+
token_data = response.get("data", None)
|
|
40
|
+
|
|
41
|
+
if not token_data:
|
|
42
|
+
raise CLIError("Authentication error. Please check your credentials and try again.")
|
|
43
|
+
|
|
44
|
+
return Token(**token_data)
|
|
45
|
+
|
|
46
|
+
except Exception as e:
|
|
47
|
+
raise CLIError(str(e)) from e
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Base API client for GL Connectors CLI.
|
|
2
|
+
|
|
3
|
+
Author:
|
|
4
|
+
I Gusti Ngurah Gana Untaran (i.gusti.n.g.untaran@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from typing import Any, Dict, Optional
|
|
9
|
+
|
|
10
|
+
import requests
|
|
11
|
+
|
|
12
|
+
from gl_connectors_cli.constants import HTTP_DELETE, HTTP_GET, HTTP_POST, HTTP_PUT
|
|
13
|
+
from gl_connectors_cli.utils import CLIError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BaseAPIClient:
|
|
17
|
+
"""Base API client with shared request functionality."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, base_url: str):
|
|
20
|
+
"""Initialize base API client.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
base_url: Connector base URL
|
|
24
|
+
|
|
25
|
+
"""
|
|
26
|
+
self.base_url = base_url.rstrip("/")
|
|
27
|
+
self.session = requests.Session()
|
|
28
|
+
|
|
29
|
+
def _make_request(
|
|
30
|
+
self,
|
|
31
|
+
method: str,
|
|
32
|
+
endpoint: str,
|
|
33
|
+
headers: Optional[Dict[str, str]] = None,
|
|
34
|
+
data: Optional[Dict[str, Any]] = None,
|
|
35
|
+
params: Optional[Dict[str, str]] = None,
|
|
36
|
+
) -> Dict[str, Any]:
|
|
37
|
+
"""Make HTTP request to Connector.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
method: HTTP method (GET, POST, DELETE, etc.)
|
|
41
|
+
endpoint: API endpoint path
|
|
42
|
+
headers: Request headers
|
|
43
|
+
data: Request body data
|
|
44
|
+
params: URL parameters
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Response JSON data
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
CLIError: If request fails
|
|
51
|
+
|
|
52
|
+
"""
|
|
53
|
+
url = f"{self.base_url}{endpoint}"
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
if method.upper() == HTTP_GET:
|
|
57
|
+
response = self.session.get(url, headers=headers, params=params)
|
|
58
|
+
elif method.upper() == HTTP_POST:
|
|
59
|
+
response = self.session.post(url, headers=headers, json=data, params=params)
|
|
60
|
+
elif method.upper() == HTTP_DELETE:
|
|
61
|
+
response = self.session.delete(url, headers=headers, params=params)
|
|
62
|
+
elif method.upper() == HTTP_PUT:
|
|
63
|
+
response = self.session.put(url, headers=headers, json=data, params=params)
|
|
64
|
+
else:
|
|
65
|
+
raise CLIError(f"Unsupported HTTP method: {method}")
|
|
66
|
+
|
|
67
|
+
response_data = response.json()
|
|
68
|
+
if not response.ok or ("success" in response_data and response_data["success"] is False):
|
|
69
|
+
error_data = response_data.get("error", {})
|
|
70
|
+
error_message = error_data.get("message", response.status_code)
|
|
71
|
+
raise CLIError(f"API error: {error_message}")
|
|
72
|
+
|
|
73
|
+
return response_data
|
|
74
|
+
|
|
75
|
+
except requests.exceptions.ConnectionError as e:
|
|
76
|
+
raise CLIError(f"Failed to connect to {self.base_url}: {str(e)}") from e
|
|
77
|
+
except requests.exceptions.Timeout as e:
|
|
78
|
+
raise CLIError(f"Request timeout: {str(e)}") from e
|
|
79
|
+
except requests.exceptions.RequestException as e:
|
|
80
|
+
raise CLIError(f"Request failed: {str(e)}") from e
|
|
81
|
+
except json.JSONDecodeError as e:
|
|
82
|
+
raise CLIError(f"Invalid JSON response from server: {str(e)}") from e
|
|
83
|
+
except CLIError as e:
|
|
84
|
+
raise e
|
|
85
|
+
except Exception as e:
|
|
86
|
+
raise CLIError(f"An unexpected error occurred: {str(e)}") from e
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Integrations API client for GL Connectors CLI.
|
|
2
|
+
|
|
3
|
+
Author:
|
|
4
|
+
I Gusti Ngurah Gana Untaran (i.gusti.n.g.untaran@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Dict, List
|
|
8
|
+
|
|
9
|
+
from gl_connectors_cli.api.base import BaseAPIClient
|
|
10
|
+
from gl_connectors_cli.api.models import IntegrationDetail
|
|
11
|
+
from gl_connectors_cli.constants import (
|
|
12
|
+
API_KEY_HEADER,
|
|
13
|
+
AUTHORIZATION_HEADER,
|
|
14
|
+
BEARER_PREFIX,
|
|
15
|
+
HTTP_DELETE,
|
|
16
|
+
HTTP_GET,
|
|
17
|
+
HTTP_POST,
|
|
18
|
+
)
|
|
19
|
+
from gl_connectors_cli.utils import CLIError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class IntegrationsAPIClient(BaseAPIClient):
|
|
23
|
+
"""Integrations API client."""
|
|
24
|
+
|
|
25
|
+
def get_connectors(self, client_key: str) -> List[str]:
|
|
26
|
+
"""Get all available connectors.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
client_key: Client API key
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
List of available connectors
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
CLIError: If request fails
|
|
36
|
+
|
|
37
|
+
"""
|
|
38
|
+
headers = {AUTHORIZATION_HEADER: f"{BEARER_PREFIX} {client_key}"}
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
response = self._make_request(HTTP_GET, "/connectors", headers=headers)
|
|
42
|
+
|
|
43
|
+
return list(response.keys())
|
|
44
|
+
|
|
45
|
+
except Exception as e:
|
|
46
|
+
raise CLIError(f"Failed to get connectors: {str(e)}") from e
|
|
47
|
+
|
|
48
|
+
def check_integration_status(self, client_key: str, connector_name: str, token: str) -> bool:
|
|
49
|
+
"""Check if integration exists for a connector.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
client_key: Client API key
|
|
53
|
+
connector_name: Name of the connector
|
|
54
|
+
token: User JWT token
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Integration status information (True if integration exists, False otherwise)
|
|
58
|
+
|
|
59
|
+
Raises:
|
|
60
|
+
CLIError: If request fails
|
|
61
|
+
|
|
62
|
+
"""
|
|
63
|
+
headers = {API_KEY_HEADER: client_key, AUTHORIZATION_HEADER: f"{BEARER_PREFIX} {token}"}
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
response = self._make_request(HTTP_GET, f"/connectors/{connector_name}/integration-exists", headers=headers)
|
|
67
|
+
return response.get("data", {}).get("has_integration", False)
|
|
68
|
+
|
|
69
|
+
except Exception as e:
|
|
70
|
+
raise CLIError(f"Failed to check integration status: {str(e)}") from e
|
|
71
|
+
|
|
72
|
+
def initiate_integration(self, client_key: str, connector_name: str, token: str) -> str:
|
|
73
|
+
"""Initiate OAuth integration for a connector.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
client_key: Client API key
|
|
77
|
+
connector_name: Name of the connector
|
|
78
|
+
token: User JWT token
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
str: Integration initiation response (contains OAuth URL)
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
CLIError: If request fails
|
|
85
|
+
|
|
86
|
+
"""
|
|
87
|
+
headers = {
|
|
88
|
+
API_KEY_HEADER: client_key,
|
|
89
|
+
AUTHORIZATION_HEADER: f"{BEARER_PREFIX} {token}",
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
callback_url = f"{self.base_url}/connectors/{connector_name}/success-authorize-callback"
|
|
93
|
+
|
|
94
|
+
data = {"callback_url": callback_url}
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
response = self._make_request(
|
|
98
|
+
HTTP_POST, f"/connectors/{connector_name}/integrations", headers=headers, data=data
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if not response.get("data", {}).get("url"):
|
|
102
|
+
raise CLIError("Failed to initiate integration.")
|
|
103
|
+
|
|
104
|
+
return response.get("data", {}).get("url", "")
|
|
105
|
+
|
|
106
|
+
except Exception as e:
|
|
107
|
+
raise CLIError(f"Failed to initiate integration: {str(e)}") from e
|
|
108
|
+
|
|
109
|
+
def remove_integration(
|
|
110
|
+
self, client_key: str, connector_name: str, user_identifier: str, token: str
|
|
111
|
+
) -> Dict[str, Any]:
|
|
112
|
+
"""Remove integration for a connector and specific user identifier.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
client_key: Client API key
|
|
116
|
+
connector_name: Name of the connector
|
|
117
|
+
user_identifier: User identifier to specify which integration to remove
|
|
118
|
+
token: User JWT token
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Removal response
|
|
122
|
+
|
|
123
|
+
Raises:
|
|
124
|
+
CLIError: If request fails
|
|
125
|
+
|
|
126
|
+
"""
|
|
127
|
+
headers = {API_KEY_HEADER: client_key, AUTHORIZATION_HEADER: f"{BEARER_PREFIX} {token}"}
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
response = self._make_request(
|
|
131
|
+
HTTP_DELETE, f"/connectors/{connector_name}/integrations/{user_identifier}", headers=headers
|
|
132
|
+
)
|
|
133
|
+
return response.get("data", {})
|
|
134
|
+
|
|
135
|
+
except Exception as e:
|
|
136
|
+
raise CLIError(f"Failed to remove integration: {str(e)}") from e
|
|
137
|
+
|
|
138
|
+
def get_integration_details_by_identifier(
|
|
139
|
+
self, client_key: str, connector: str, user_identifier: str, token: str
|
|
140
|
+
) -> IntegrationDetail:
|
|
141
|
+
"""Get a specific integration by user identifier.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
client_key: Client API key
|
|
145
|
+
connector: Connector name
|
|
146
|
+
user_identifier: The third-party service user identifier (e.g., GitHub username, Google email)
|
|
147
|
+
token: User token
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Integration details including access token
|
|
151
|
+
|
|
152
|
+
Raises:
|
|
153
|
+
CLIError: If request fails or integration not found
|
|
154
|
+
|
|
155
|
+
"""
|
|
156
|
+
headers = {API_KEY_HEADER: client_key, AUTHORIZATION_HEADER: f"{BEARER_PREFIX} {token}"}
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
response = self._make_request(
|
|
160
|
+
HTTP_GET, f"/connectors/{connector}/integrations/{user_identifier}", headers=headers
|
|
161
|
+
)
|
|
162
|
+
data = response.get("data", {})
|
|
163
|
+
if data.get("success") is False:
|
|
164
|
+
raise CLIError({data.get("error", "Unknown error")})
|
|
165
|
+
|
|
166
|
+
return IntegrationDetail(**response.get("data", {}))
|
|
167
|
+
|
|
168
|
+
except Exception as e:
|
|
169
|
+
raise CLIError(f"Failed to get integration for {user_identifier} in {connector}: {str(e)}") from e
|
|
170
|
+
|
|
171
|
+
def set_selected_integration(
|
|
172
|
+
self, client_key: str, connector_name: str, user_identifier: str, token: str
|
|
173
|
+
) -> Dict[str, Any]:
|
|
174
|
+
"""Set integration as selected/unselected.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
client_key: Client API key
|
|
178
|
+
connector_name: Name of the connector
|
|
179
|
+
user_identifier: User identifier for the integration
|
|
180
|
+
token: User JWT token
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Update response
|
|
184
|
+
|
|
185
|
+
Raises:
|
|
186
|
+
CLIError: If request fails
|
|
187
|
+
|
|
188
|
+
"""
|
|
189
|
+
headers = {API_KEY_HEADER: client_key, AUTHORIZATION_HEADER: f"{BEARER_PREFIX} {token}"}
|
|
190
|
+
data = {"selected": True}
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
response = self._make_request(
|
|
194
|
+
HTTP_POST, f"/connectors/{connector_name}/integrations/{user_identifier}", headers=headers, data=data
|
|
195
|
+
)
|
|
196
|
+
return response.get("data", {})
|
|
197
|
+
|
|
198
|
+
except Exception as e:
|
|
199
|
+
raise CLIError(f"Failed to update integration selection: {str(e)}") from e
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Shared data models for GL Connectors CLI API clients.
|
|
2
|
+
|
|
3
|
+
Author:
|
|
4
|
+
I Gusti Ngurah Gana Untaran (i.gusti.n.g.untaran@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import Any, Dict, List
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class User(BaseModel):
|
|
14
|
+
"""GL Connectors user information."""
|
|
15
|
+
|
|
16
|
+
id: str
|
|
17
|
+
identifier: str = ""
|
|
18
|
+
secret_preview: str = ""
|
|
19
|
+
is_active: bool = True
|
|
20
|
+
client_id: str = ""
|
|
21
|
+
integrations: List[Dict[str, Any]] = Field(default_factory=list)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CreatedUser(User):
|
|
25
|
+
"""GL Connectors user information with secret."""
|
|
26
|
+
|
|
27
|
+
secret: str = ""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Token(BaseModel):
|
|
31
|
+
"""GL Connectors authentication token."""
|
|
32
|
+
|
|
33
|
+
token: str
|
|
34
|
+
token_type: str = "Bearer"
|
|
35
|
+
expires_at: datetime = Field(default_factory=datetime.now)
|
|
36
|
+
is_revoked: bool = False
|
|
37
|
+
user_id: str = ""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class IntegrationDetail(BaseModel):
|
|
41
|
+
"""Integration detail."""
|
|
42
|
+
|
|
43
|
+
connector: str
|
|
44
|
+
user_identifier: str
|
|
45
|
+
auth_string: str
|
|
46
|
+
auth_scopes: List[str]
|
|
47
|
+
selected: bool = False
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Users API client for GL Connectors CLI.
|
|
2
|
+
|
|
3
|
+
Author:
|
|
4
|
+
I Gusti Ngurah Gana Untaran (i.gusti.n.g.untaran@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from gl_connectors_cli.api.base import BaseAPIClient
|
|
8
|
+
from gl_connectors_cli.api.models import CreatedUser, User
|
|
9
|
+
from gl_connectors_cli.constants import API_KEY_HEADER, AUTHORIZATION_HEADER, BEARER_PREFIX, HTTP_GET, HTTP_POST
|
|
10
|
+
from gl_connectors_cli.utils import CLIError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class UsersAPIClient(BaseAPIClient):
|
|
14
|
+
"""Users API client."""
|
|
15
|
+
|
|
16
|
+
def create_user(self, client_key: str, username: str) -> CreatedUser:
|
|
17
|
+
"""Create a new user.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
client_key: Client API key
|
|
21
|
+
username: Username for the new user
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
CreatedUser: User creation response data
|
|
25
|
+
|
|
26
|
+
Raises:
|
|
27
|
+
CLIError: If user creation fails
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
headers = {
|
|
31
|
+
API_KEY_HEADER: client_key,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
data = {"identifier": username}
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
response = self._make_request(HTTP_POST, "/clients/register-user", headers=headers, data=data)
|
|
38
|
+
user_data = response.get("data", {})
|
|
39
|
+
return CreatedUser(**user_data)
|
|
40
|
+
|
|
41
|
+
except Exception as e:
|
|
42
|
+
raise CLIError(f"Failed to create user: {str(e)}") from e
|
|
43
|
+
|
|
44
|
+
def get_user_info(self, client_key: str, token: str) -> User:
|
|
45
|
+
"""Get current user information.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
client_key: Client API key
|
|
49
|
+
token: User JWT token
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
User: User information
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
CLIError: If request fails
|
|
56
|
+
|
|
57
|
+
"""
|
|
58
|
+
headers = {AUTHORIZATION_HEADER: f"{BEARER_PREFIX} {token}", API_KEY_HEADER: client_key}
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
response = self._make_request(HTTP_GET, "/clients/user", headers=headers)
|
|
62
|
+
user_data = response.get("data", {})
|
|
63
|
+
return User(**user_data)
|
|
64
|
+
|
|
65
|
+
except Exception as e:
|
|
66
|
+
raise CLIError(f"Failed to get user info: {str(e)}") from e
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI commands module."""
|