appmesh 1.6.13__py3-none-any.whl → 1.6.15__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.
appmesh/client_http.py CHANGED
@@ -1,6 +1,8 @@
1
1
  # client_http.py
2
2
  # pylint: disable=broad-exception-raised,line-too-long,broad-exception-caught,too-many-lines,import-outside-toplevel
3
3
 
4
+ """App Mesh HTTP Client SDK for REST API interactions."""
5
+
4
6
  # Standard library imports
5
7
  import abc
6
8
  import base64
@@ -12,9 +14,11 @@ import os
12
14
  import sys
13
15
  import threading
14
16
  import time
17
+ from contextlib import suppress
15
18
  from datetime import datetime
16
19
  from enum import Enum, unique
17
20
  from http import HTTPStatus
21
+ from pathlib import Path
18
22
  from typing import Optional, Tuple, Union
19
23
  from urllib import parse
20
24
 
@@ -39,17 +43,6 @@ class AppMeshClient(metaclass=abc.ABCMeta):
39
43
 
40
44
  This client is designed for direct usage in applications that require access to App Mesh services over HTTP-based REST.
41
45
 
42
- Usage:
43
- - Install the App Mesh Python package:
44
- python3 -m pip install --upgrade appmesh
45
- - Import the client module:
46
- from appmesh import AppMeshClient
47
-
48
- Example:
49
- client = AppMeshClient()
50
- client.login("your-name", "your-password")
51
- response = client.app_view(app_name='ping')
52
-
53
46
  Attributes:
54
47
  - TLS (Transport Layer Security): Supports secure connections between the client and App Mesh service,
55
48
  ensuring encrypted communication.
@@ -112,8 +105,16 @@ class AppMeshClient(metaclass=abc.ABCMeta):
112
105
  - update_role()
113
106
  - view_roles()
114
107
  - view_groups()
108
+
109
+ Example:
110
+ >>> python -m pip install --upgrade appmesh
111
+ >>> from appmesh import AppMeshClient
112
+ >>> client = AppMeshClient()
113
+ >>> client.login("your-name", "your-password")
114
+ >>> response = client.app_view(app_name='ping')
115
115
  """
116
116
 
117
+ # Duration constants
117
118
  DURATION_ONE_WEEK_ISO = "P1W"
118
119
  DURATION_TWO_DAYS_ISO = "P2D"
119
120
  DURATION_TWO_DAYS_HALF_ISO = "P2DT12H"
@@ -121,20 +122,22 @@ class AppMeshClient(metaclass=abc.ABCMeta):
121
122
  TOKEN_REFRESH_OFFSET = 30 # 30s before token expire to refresh token
122
123
 
123
124
  # Platform-aware default SSL paths
124
- _DEFAULT_SSL_DIR = "c:/local/appmesh/ssl" if os.name == "nt" else "/opt/appmesh/ssl"
125
- DEFAULT_SSL_CA_CERT_PATH = os.path.join(_DEFAULT_SSL_DIR, "ca.pem")
126
- DEFAULT_SSL_CLIENT_CERT_PATH = os.path.join(_DEFAULT_SSL_DIR, "client.pem")
127
- DEFAULT_SSL_CLIENT_KEY_PATH = os.path.join(_DEFAULT_SSL_DIR, "client-key.pem")
125
+ _DEFAULT_SSL_DIR = Path("c:/local/appmesh/ssl" if os.name == "nt" else "/opt/appmesh/ssl")
126
+ DEFAULT_SSL_CA_CERT_PATH = str(_DEFAULT_SSL_DIR / "ca.pem")
127
+ DEFAULT_SSL_CLIENT_CERT_PATH = str(_DEFAULT_SSL_DIR / "client.pem")
128
+ DEFAULT_SSL_CLIENT_KEY_PATH = str(_DEFAULT_SSL_DIR / "client-key.pem")
128
129
 
130
+ # JWT constants
129
131
  DEFAULT_JWT_AUDIENCE = "appmesh-service"
130
132
 
133
+ # HTTP headers and constants
131
134
  JSON_KEY_MESSAGE = "message"
132
135
  HTTP_USER_AGENT = "appmesh/python"
133
136
  HTTP_HEADER_KEY_AUTH = "Authorization"
134
137
  HTTP_HEADER_KEY_USER_AGENT = "User-Agent"
135
138
  HTTP_HEADER_KEY_X_TARGET_HOST = "X-Target-Host"
136
139
  HTTP_HEADER_KEY_X_FILE_PATH = "X-File-Path"
137
- HTTP_HEADER_JWT_set_cookie = "X-Set-Cookie"
140
+ HTTP_HEADER_JWT_SET_COOKIE = "X-Set-Cookie"
138
141
  HTTP_HEADER_NAME_CSRF_TOKEN = "X-CSRF-Token"
139
142
  COOKIE_TOKEN = "appmesh_auth_token"
140
143
  COOKIE_CSRF_TOKEN = "appmesh_csrf_token"
@@ -154,8 +157,6 @@ class AppMeshClient(metaclass=abc.ABCMeta):
154
157
 
155
158
  def __init__(self, response: requests.Response):
156
159
  super().__init__()
157
-
158
- # copy essential fields from response
159
160
  self.__dict__.update(response.__dict__)
160
161
 
161
162
  self._converted_text = None
@@ -164,28 +165,24 @@ class AppMeshClient(metaclass=abc.ABCMeta):
164
165
  # Check if we need to convert encoding on Windows
165
166
  if sys.platform == "win32":
166
167
  content_type = response.headers.get("Content-Type", "").lower()
167
- if response.status_code == HTTPStatus.OK and "text/plain" in content_type and "utf-8" in content_type:
168
+ is_ok = response.status_code == HTTPStatus.OK
169
+ is_utf8_text = "text/plain" in content_type and "utf-8" in content_type
170
+
171
+ if is_ok and is_utf8_text:
168
172
  try:
169
173
  local_encoding = locale.getpreferredencoding()
170
-
171
- if local_encoding.lower() not in ["utf-8", "utf8"]:
174
+ if local_encoding.lower() not in {"utf-8", "utf8"}:
172
175
  # Ensure response is decoded as UTF-8 first
173
176
  self.encoding = "utf-8"
174
177
  utf8_text = self.text # This gives us proper Unicode string
175
178
 
176
- # Convert Unicode to local encoding, then back to Unicode
177
- # This simulates how text would appear in local encoding
178
- try:
179
+ with suppress(UnicodeEncodeError, LookupError):
180
+ # Convert Unicode to local encoding, then back to Unicode
179
181
  local_bytes = utf8_text.encode(local_encoding, errors="replace")
180
182
  self._converted_text = local_bytes.decode(local_encoding)
181
183
  self._should_convert = True
182
- except (UnicodeEncodeError, LookupError):
183
- # If local encoding can't handle the characters, fall back to UTF-8
184
- self._converted_text = utf8_text
185
- self._should_convert = True
186
184
 
187
185
  except (UnicodeError, LookupError):
188
- # If any conversion fails, keep original UTF-8
189
186
  self.encoding = "utf-8"
190
187
 
191
188
  @property
@@ -193,45 +190,33 @@ class AppMeshClient(metaclass=abc.ABCMeta):
193
190
  """Return converted text if needed, otherwise original text."""
194
191
  if self._should_convert and self._converted_text is not None:
195
192
  return self._converted_text
196
- # return the original text from _response without modification
197
193
  return super().text
198
194
 
199
195
  def __init__(
200
196
  self,
201
197
  rest_url: str = "https://127.0.0.1:6060",
202
- rest_ssl_verify=DEFAULT_SSL_CA_CERT_PATH if os.path.exists(DEFAULT_SSL_CA_CERT_PATH) else False,
203
- rest_ssl_client_cert=(DEFAULT_SSL_CLIENT_CERT_PATH, DEFAULT_SSL_CLIENT_KEY_PATH) if os.path.exists(DEFAULT_SSL_CLIENT_CERT_PATH) else None,
204
- rest_timeout=(60, 300),
198
+ rest_ssl_verify: Union[bool, str] = DEFAULT_SSL_CA_CERT_PATH,
199
+ rest_ssl_client_cert: Optional[Union[str, Tuple[str, str]]] = (DEFAULT_SSL_CLIENT_CERT_PATH, DEFAULT_SSL_CLIENT_KEY_PATH),
200
+ rest_timeout: Tuple[float, float] = (60, 300),
205
201
  jwt_token: Optional[str] = None,
206
202
  rest_cookie_file: Optional[str] = None,
207
- auto_refresh_token=False,
203
+ auto_refresh_token: bool = False,
208
204
  ):
209
205
  """Initialize an App Mesh HTTP client for interacting with the App Mesh server via secure HTTPS.
210
206
 
211
207
  Args:
212
- rest_url (str, optional): The server's base URI, including protocol, hostname, and port. Defaults to `"https://127.0.0.1:6060"`.
213
-
214
- rest_ssl_verify (Union[bool, str], optional): Configures SSL certificate verification for HTTPS requests:
215
- - `True`: Uses system CA certificates to verify the server's identity.
216
- - `False`: Disables SSL verification (insecure, use cautiously for development).
217
- - `str`: Path to a custom CA certificate or directory for verification. This option allows custom CA configuration,
218
- which may be necessary in environments requiring specific CA chains that differ from the default system CAs.
219
- To use both a custom CA and the system's default CAs, create a combined CA bundle by concatenating them into a single file. (e.g., `cat custom_ca.pem /etc/ssl/certs/ca-certificates.crt > combined_ca.pem`).
220
-
221
- rest_ssl_client_cert (Union[str, Tuple[str, str]], optional): Path to the SSL client certificate and key. Can be:
222
- - `str`: A path to a single PEM file containing both the client certificate and private key.
223
- - `tuple`: A pair of paths (`cert_file`, `key_file`), where `cert_file` is the client certificate file path and `key_file` is the private key file path.
224
-
225
- rest_timeout (tuple, optional): HTTP connection timeouts for API requests, as `(connect_timeout, read_timeout)`.
226
- The default is `(60, 300)`, where `60` seconds is the maximum time to establish a connection and `300` seconds for the maximum read duration.
227
-
228
- rest_cookie_file (str, optional): Path to a file for storing session cookies.
229
- If provided, cookies will be saved to and loaded from this file to maintain session state across client instances instead of keep jwt_token.
230
-
231
- jwt_token (str, optional): JWT token for API authentication, used in headers to authorize requests where required.
232
- auto_refresh_token (bool, optional): Enable automatic token refresh before expiration.
233
- When enabled, a background timer will monitor token expiration and attempt to refresh
234
- the token before it expires. This works with both native App Mesh tokens and Keycloak tokens.
208
+ rest_url: The server's base URI. Defaults to "https://127.0.0.1:6060".
209
+ rest_ssl_verify: SSL server verification mode:
210
+ - True: Use system CAs.
211
+ - False: Disable verification (insecure).
212
+ - str: Path to custom CA or directory. To include system CAs, combine them into one file (e.g., cat custom_ca.pem /etc/ssl/certs/ca-certificates.crt > combined_ca.pem).
213
+ rest_ssl_client_cert: SSL client certificate file(s):
214
+ - str: Single PEM file with cert+key
215
+ - tuple: (cert_path, key_path)
216
+ rest_timeout: Timeouts `(connect_timeout, read_timeout)` in seconds. Default `(60, 300)`.
217
+ rest_cookie_file: Path to a file for storing session cookies (alternative to jwt_token).
218
+ jwt_token: JWT token for API authentication, overrides cookie file if both provided.
219
+ auto_refresh_token: Enable automatic token refresh before expiration (supports App Mesh and Keycloak tokens).
235
220
  """
236
221
  self._ensure_logging_configured()
237
222
  self.auth_server_url = rest_url
@@ -244,86 +229,83 @@ class AppMeshClient(metaclass=abc.ABCMeta):
244
229
  # Token auto-refresh
245
230
  self._token_refresh_timer = None
246
231
  self._auto_refresh_token = auto_refresh_token
247
- self.jwt_token = jwt_token # Set property last after all dependencies are initialized to setup refresh timer
248
232
 
249
233
  # Session and cookie management
250
234
  self._lock = threading.Lock()
251
235
  self.session = requests.Session()
252
- self.cookie_file = self._load_cookies(rest_cookie_file)
236
+ self.cookie_file = rest_cookie_file
237
+ loaded = self._load_cookies(rest_cookie_file)
238
+ cookie_token = self._get_cookie_value(self.session.cookies, self.COOKIE_TOKEN) if loaded else None
239
+
240
+ # Set property last after all dependencies are initialized to setup refresh timer
241
+ self.jwt_token = jwt_token or cookie_token
253
242
 
254
243
  @staticmethod
255
- def _ensure_logging_configured():
256
- """Ensure logging is configured. If no handlers are configured, add a default console handler."""
244
+ def _ensure_logging_configured() -> None:
245
+ """Ensure logging is configured with a default console handler if needed."""
257
246
  if not logging.root.handlers:
258
247
  logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
259
248
 
260
249
  def _get_access_token(self) -> str:
261
- return self.jwt_token
250
+ """Get the current access token."""
251
+ return self.jwt_token or ""
262
252
 
263
- def _load_cookies(self, cookie_file: Optional[str]) -> str:
264
- """Load cookies from the cookie file and return the file path ."""
253
+ def _load_cookies(self, cookie_file: Optional[str]) -> bool:
254
+ """ "Load cookies from a Mozilla-format file into the session"""
265
255
  if not cookie_file:
266
- return ""
256
+ return False
267
257
 
258
+ cookie_path = Path(cookie_file)
268
259
  self.session.cookies = cookiejar.MozillaCookieJar(cookie_file)
269
- if os.path.exists(cookie_file):
260
+
261
+ if cookie_path.exists():
270
262
  self.session.cookies.load(ignore_discard=True, ignore_expires=True)
271
- self.jwt_token = self._get_cookie_value(self.session.cookies, self.COOKIE_TOKEN)
272
263
  else:
273
- os.makedirs(os.path.dirname(cookie_file), exist_ok=True)
264
+ cookie_path.parent.mkdir(parents=True, exist_ok=True)
274
265
  self.session.cookies.save(ignore_discard=True, ignore_expires=True)
275
266
  if os.name == "posix":
276
- os.chmod(cookie_file, 0o600) # User read/write only
277
- return cookie_file
267
+ cookie_path.chmod(0o600) # User read/write only
268
+
269
+ return True
278
270
 
279
271
  @staticmethod
280
- def _get_cookie_value(cookies, name, check_expiry=True) -> Optional[str]:
272
+ def _get_cookie_value(cookies, name: str, check_expiry: bool = True) -> Optional[str]:
281
273
  """Get cookie value by name, checking expiry if requested."""
282
274
  # If it's a RequestsCookieJar, use .get() but check expiry manually if requested
283
275
  if hasattr(cookies, "get") and not isinstance(cookies, list):
284
276
  cookie = cookies.get(name)
285
277
  if cookie is None:
286
278
  return None
287
- if check_expiry and getattr(cookie, "expires", None):
279
+ if check_expiry and hasattr(cookie, "expires") and cookie.expires:
288
280
  if cookie.expires < time.time():
289
281
  return None # expired
290
282
  return cookie.value if hasattr(cookie, "value") else cookie
291
283
 
292
284
  # Otherwise, assume it's a MozillaCookieJar — iterate manually
293
- for c in cookies:
294
- if c.name == name:
295
- if check_expiry and getattr(c, "expires", None):
296
- if c.expires < time.time():
285
+ for cookie in cookies:
286
+ if cookie.name == name:
287
+ if check_expiry and hasattr(cookie, "expires") and cookie.expires:
288
+ if cookie.expires < time.time():
297
289
  return None # expired
298
- return c.value
290
+ return cookie.value
299
291
 
300
292
  return None
301
293
 
302
- def _check_and_refresh_token(self):
303
- """Check and refresh token if needed, then schedule next check.
304
-
305
- This method is triggered by the refresh timer and will:
306
- 1. Check if token needs refresh based on expiration time
307
- 2. Refresh the token if needed
308
- 3. Schedule the next refresh check
309
- """
294
+ def _check_and_refresh_token(self) -> None:
295
+ """Check and refresh token if needed, then schedule next check."""
310
296
  if not self.jwt_token:
311
297
  return
312
298
 
313
- # Check if token needs refresh
314
299
  needs_refresh = True
315
300
  time_to_expiry = float("inf")
316
301
 
317
302
  # Check token expiration directly from JWT
318
- try:
303
+ with suppress(Exception):
319
304
  decoded_token = jwt.decode(self._get_access_token(), options={"verify_signature": False})
320
305
  expiry = decoded_token.get("exp", 0)
321
306
  current_time = time.time()
322
307
  time_to_expiry = expiry - current_time
323
- # Refresh if token expires within 5 minutes
324
308
  needs_refresh = time_to_expiry < self.TOKEN_REFRESH_OFFSET
325
- except Exception as e:
326
- logging.debug("Failed to parse JWT token for expiration check: %s", str(e))
327
309
 
328
310
  # Refresh token if needed
329
311
  if needs_refresh:
@@ -331,25 +313,14 @@ class AppMeshClient(metaclass=abc.ABCMeta):
331
313
  self.renew_token()
332
314
  logging.info("Token successfully refreshed")
333
315
  except Exception as e:
334
- logging.error("Token refresh failed: %s", str(e))
316
+ logging.error("Token refresh failed: %s", e)
335
317
 
336
318
  # Schedule next check if auto-refresh is still enabled
337
319
  if self._auto_refresh_token and self.jwt_token:
338
320
  self._schedule_token_refresh(time_to_expiry)
339
321
 
340
- def _schedule_token_refresh(self, time_to_expiry=None):
341
- """Schedule next token refresh check.
342
-
343
- Args:
344
- time_to_expiry (float, optional): Time in seconds until token expiration.
345
- When provided, helps calculate optimal refresh timing.
346
-
347
- Calculates appropriate check interval:
348
- - If token expires soon (within 5 minutes), refresh immediately
349
- - Otherwise schedule refresh for the earlier of:
350
- 1. 5 minutes before expiration
351
- 2. 60 seconds from now
352
- """
322
+ def _schedule_token_refresh(self, time_to_expiry: Optional[float] = None) -> None:
323
+ """Schedule next token refresh check."""
353
324
  # Cancel existing timer if any
354
325
  if self._token_refresh_timer:
355
326
  self._token_refresh_timer.cancel()
@@ -373,23 +344,22 @@ class AppMeshClient(metaclass=abc.ABCMeta):
373
344
  self._token_refresh_timer.start()
374
345
  logging.debug("Auto-refresh: Next token check scheduled in %.1f seconds", check_interval)
375
346
  except Exception as e:
376
- logging.error("Auto-refresh: Failed to schedule token refresh: %s", str(e))
347
+ logging.error("Auto-refresh: Failed to schedule token refresh: %s", e)
377
348
 
378
349
  def close(self):
379
350
  """Close the session and release resources."""
380
351
  # Cancel token refresh timer
381
- if hasattr(self, "_token_refresh_timer") and self._token_refresh_timer:
352
+ if self._token_refresh_timer:
382
353
  self._token_refresh_timer.cancel()
383
354
  self._token_refresh_timer = None
384
355
 
385
356
  # Close the session
386
- if hasattr(self, "session") and self.session:
357
+ if self.session:
387
358
  self.session.close()
388
359
  self.session = None
389
360
 
390
361
  # Clean token
391
- if hasattr(self, "_jwt_token") and self._jwt_token:
392
- self._jwt_token = None
362
+ self._jwt_token = None
393
363
 
394
364
  def __enter__(self):
395
365
  """Support for context manager protocol."""
@@ -399,114 +369,61 @@ class AppMeshClient(metaclass=abc.ABCMeta):
399
369
  """Support for context manager protocol, ensuring resources are released."""
400
370
  self.close()
401
371
 
402
- def __del__(self):
403
- """Ensure resources are properly released when the object is garbage collected."""
404
- try:
405
- self.close()
406
- except Exception:
407
- pass # Never raise in __del__
408
-
409
372
  @property
410
373
  def jwt_token(self) -> str:
411
- """Get the current JWT (JSON Web Token) used for authentication.
412
-
413
- This property manages the authentication token used for securing API requests.
414
- The token is used to authenticate and authorize requests to the service.
415
-
416
- Returns:
417
- str: The current JWT token string.
418
- Returns empty string if no token is set.
419
-
420
- Notes:
421
- - The token typically includes claims for identity and permissions
422
- - Token format: "header.payload.signature"
423
- - Tokens are time-sensitive and may expire
424
- """
425
- return self._jwt_token
374
+ """Get the current JWT (JSON Web Token) used for authentication."""
375
+ return self._jwt_token or ""
426
376
 
427
377
  @jwt_token.setter
428
- def jwt_token(self, token: str) -> None:
378
+ def jwt_token(self, token: Optional[str]) -> None:
429
379
  """Set the JWT token for authentication.
430
380
 
431
- Configure the JWT token used for authenticating requests. The token should be
432
- a valid JWT issued by a trusted authority.
433
-
434
- Args:
435
- token (str): JWT token string in standard JWT format
436
- (e.g., "eyJhbGci...payload...signature")
437
- Pass empty string to clear the token.
438
-
439
- Example:
440
- >>> client.jwt_token = "eyJhbGci..." # Set new token
441
- >>> client.jwt_token = "" # Clear token
442
-
443
- Notes:
444
- Security best practices:
445
- - Store tokens securely
446
- - Never log or expose complete tokens
447
- - Refresh tokens before expiration
448
- - Validate token format before setting
381
+ Note:
382
+ This setter has no effect when cookie-based authentication is enabled (i.e., when a cookie file is being used).
449
383
  """
450
384
  if self._jwt_token == token:
451
385
  return # No change
452
386
  self._jwt_token = token
453
387
 
454
- # handle refresh
388
+ # Handle refresh
455
389
  if self._jwt_token and self._auto_refresh_token:
456
390
  self._schedule_token_refresh()
457
391
  elif self._token_refresh_timer:
458
392
  self._token_refresh_timer.cancel()
459
393
  self._token_refresh_timer = None
460
394
 
461
- # handle session
395
+ # Handle session persistence
462
396
  with self._lock:
463
- if hasattr(self, "cookie_file") and self.cookie_file:
397
+ if self.cookie_file:
464
398
  self.session.cookies.save(ignore_discard=True, ignore_expires=True)
465
399
 
466
400
  @property
467
401
  def forward_to(self) -> str:
468
- """Get the target host address for request forwarding in a cluster setup.
402
+ """Target host for request forwarding in a cluster.
469
403
 
470
- This property manages the destination host where requests will be forwarded to
471
- within a cluster configuration. The host can be specified in two formats:
472
- 1. hostname/IP only: will use the current service's port
473
- 2. hostname/IP with port: will use the specified port
404
+ Supports:
405
+ - "hostname" or "IP" uses current service port
406
+ - "hostname:port" or "IP:port" uses specified port
474
407
 
475
408
  Returns:
476
- str: The target host address in either format:
477
- - "hostname" or "IP" (using current service port)
478
- - "hostname:port" or "IP:port" (using specified port)
479
- Returns empty string if no forwarding host is set.
409
+ str: Target host (e.g., "node" or "node:6060"), or empty string if unset.
480
410
 
481
411
  Notes:
482
- For proper JWT token sharing across the cluster:
483
- - All nodes must share the same JWT salt configuration
484
- - All nodes must use identical JWT issuer settings
485
- - When port is omitted, current service port will be used
412
+ For JWT sharing across the cluster:
413
+ - All nodes must use the same `JWTSalt` and `Issuer` for JWT settings
414
+ - If port is omitted, current service port is used
486
415
  """
487
- return self._forward_to
416
+ return self._forward_to or ""
488
417
 
489
418
  @forward_to.setter
490
419
  def forward_to(self, host: str) -> None:
491
- """Set the target host address for request forwarding.
492
-
493
- Configure the destination host where requests should be forwarded to. This is
494
- used in cluster setups for request routing and load distribution.
495
-
496
- Args:
497
- host (str): Target host address in one of two formats:
498
- 1. "hostname" or "IP" - will use current service port
499
- (e.g., "backend-node" or "192.168.1.100")
500
- 2. "hostname:port" or "IP:port" - will use specified port
501
- (e.g., "backend-node:6060" or "192.168.1.100:6060")
502
- Pass empty string to disable forwarding.
420
+ """Set target host for forwarding.
503
421
 
504
422
  Examples:
505
423
  >>> client.forward_to = "backend-node:6060" # Use specific port
506
424
  >>> client.forward_to = "backend-node" # Use current service port
507
425
  >>> client.forward_to = None # Disable forwarding
508
426
  """
509
-
510
427
  self._forward_to = host
511
428
 
512
429
  ########################################
@@ -520,37 +437,48 @@ class AppMeshClient(metaclass=abc.ABCMeta):
520
437
  timeout_seconds: Union[str, int] = DURATION_ONE_WEEK_ISO,
521
438
  audience: Optional[str] = None,
522
439
  ) -> str:
523
- """Login with user name and password
440
+ """Login with user name and password.
524
441
 
525
442
  Args:
526
- user_name (str): the name of the user.
527
- user_pwd (str): the password of the user.
528
- totp_code (str, optional): the TOTP code if enabled for the user.
529
- timeout_seconds (int | str, optional): token expire timeout of seconds. support ISO 8601 durations (e.g., 'P1Y2M3DT4H5M6S' 'P1W').
530
- audience (str, optional): The audience of the JWT token, should be available by JWT service configuration (default is 'appmesh-service').
443
+ user_name: The name of the user.
444
+ user_pwd: The password of the user.
445
+ totp_code: The TOTP code if enabled for the user.
446
+ timeout_seconds: Token expire timeout. Supports ISO 8601 durations (e.g., 'P1Y2M3DT4H5M6S' 'P1W').
447
+ audience: The audience of the JWT token, should be available by JWT service configuration (default is 'appmesh-service').
531
448
 
532
449
  Returns:
533
- str: JWT token.
450
+ JWT token.
534
451
  """
535
452
  # Standard App Mesh authentication
536
453
  self.jwt_token = None
454
+
455
+ credentials = f"{user_name}:{user_pwd}".encode()
456
+ headers = {
457
+ self.HTTP_HEADER_KEY_AUTH: f"Basic {base64.b64encode(credentials).decode()}",
458
+ "X-Expire-Seconds": str(self._parse_duration(timeout_seconds)),
459
+ }
460
+ if audience:
461
+ headers["X-Audience"] = audience
462
+ if self.cookie_file:
463
+ headers[self.HTTP_HEADER_JWT_SET_COOKIE] = "true"
464
+ # if totp_code:
465
+ # headers["X-Totp-Code"] = totp_code
466
+
537
467
  resp = self._request_http(
538
468
  AppMeshClient.Method.POST,
539
469
  path="/appmesh/login",
540
- header={
541
- self.HTTP_HEADER_KEY_AUTH: "Basic " + base64.b64encode(f"{user_name}:{user_pwd}".encode()).decode(),
542
- "X-Expire-Seconds": str(self._parse_duration(timeout_seconds)),
543
- **({"X-Audience": audience} if audience else {}),
544
- **({self.HTTP_HEADER_JWT_set_cookie: "true"} if self.cookie_file else {}),
545
- # **({"X-Totp-Code": totp_code} if totp_code else {}),
546
- },
470
+ header=headers,
547
471
  )
472
+
548
473
  if resp.status_code == HTTPStatus.OK:
549
474
  if "access_token" in resp.json():
550
475
  self.jwt_token = resp.json()["access_token"]
551
- elif resp.status_code == HTTPStatus.PRECONDITION_REQUIRED and "totp_challenge" in resp.json():
552
- challenge = resp.json()["totp_challenge"]
553
- self.validate_totp(user_name, challenge, totp_code, timeout_seconds)
476
+ elif resp.status_code == HTTPStatus.PRECONDITION_REQUIRED:
477
+ if not totp_code:
478
+ raise Exception("TOTP code required")
479
+ if "totp_challenge" in resp.json():
480
+ challenge = resp.json()["totp_challenge"]
481
+ self.validate_totp(user_name, challenge, totp_code, timeout_seconds)
554
482
  else:
555
483
  raise Exception(resp.text)
556
484
 
@@ -560,49 +488,46 @@ class AppMeshClient(metaclass=abc.ABCMeta):
560
488
  """Validate TOTP challenge and obtain a new JWT token.
561
489
 
562
490
  Args:
563
- username (str): Username to validate
564
- challenge (str): Challenge string from server
565
- code (str): TOTP code to validate
566
- timeout (Union[int, str], optional): Token expiry timeout.
567
- Accepts ISO 8601 duration format (e.g., 'P1Y2M3DT4H5M6S', 'P1W') or seconds.
568
- Defaults to DURATION_ONE_WEEK_ISO.
491
+ username: Username to validate.
492
+ challenge: Challenge string from server.
493
+ code: TOTP code to validate.
494
+ timeout: Token expiration duration, defaults to `DURATION_ONE_WEEK_ISO` (1 week).
495
+ Accepts either:
496
+ - **ISO 8601 duration string** (e.g., `'P1Y2M3DT4H5M6S'`, `'P1W'`)
497
+ - **Numeric value (seconds)** for simpler cases.
569
498
 
570
499
  Returns:
571
- str: New JWT token if validation succeeds
572
-
573
- Raises:
574
- Exception: If validation fails or server returns error
500
+ New JWT token if validation succeeds.
575
501
  """
502
+ body = {
503
+ "user_name": username,
504
+ "totp_code": code,
505
+ "totp_challenge": challenge,
506
+ "expire_seconds": self._parse_duration(timeout),
507
+ }
508
+
509
+ headers = {self.HTTP_HEADER_JWT_SET_COOKIE: "true"} if self.cookie_file else {}
510
+
576
511
  resp = self._request_http(
577
512
  AppMeshClient.Method.POST,
578
513
  path="/appmesh/totp/validate",
579
- body={
580
- "user_name": username,
581
- "totp_code": code,
582
- "totp_challenge": challenge,
583
- "expire_seconds": self._parse_duration(timeout),
584
- },
585
- header={self.HTTP_HEADER_JWT_set_cookie: "true"} if self.cookie_file else {},
514
+ body=body,
515
+ header=headers,
586
516
  )
517
+
587
518
  if resp.status_code == HTTPStatus.OK and "access_token" in resp.json():
588
519
  self.jwt_token = resp.json()["access_token"]
589
520
  return self.jwt_token
590
521
  raise Exception(resp.text)
591
522
 
592
523
  def logoff(self) -> bool:
593
- """Log out of the current session from the server.
594
-
595
- Returns:
596
- bool: logoff success or failure.
597
- """
598
- result = False
599
- # Standard App Mesh logout
600
- if self.jwt_token and isinstance(self.jwt_token, str):
601
- resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/self/logoff")
602
- self.jwt_token = None
603
- result = resp.status_code == HTTPStatus.OK
524
+ """Log out of the current session from the server."""
525
+ if not self.jwt_token or not isinstance(self.jwt_token, str):
526
+ return False
604
527
 
605
- return result
528
+ resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/self/logoff")
529
+ self.jwt_token = None
530
+ return resp.status_code == HTTPStatus.OK
606
531
 
607
532
  def authentication(self, token: str, permission: Optional[str] = None, audience: Optional[str] = None) -> bool:
608
533
  """Deprecated: Use authenticate() instead."""
@@ -612,191 +537,158 @@ class AppMeshClient(metaclass=abc.ABCMeta):
612
537
  """Authenticate with a token and verify permission if specified.
613
538
 
614
539
  Args:
615
- token (str): JWT token returned from login().
616
- permission (str, optional): the permission ID used to verify the token user
617
- permission ID can be:
618
- - pre-defined by App Mesh from security.yaml (e.g 'app-view', 'app-delete')
619
- - defined by input from role_update() or security.yaml
620
- audience (str, optional):The audience of the JWT token used to verify the target service.
540
+ token: JWT token returned from login().
541
+ permission: Permission ID to verify the token user.
542
+ Can be one of:
543
+ - pre-defined by App Mesh from security.yaml (e.g 'app-view', 'app-delete')
544
+ - defined by input from role_update() or security.yaml
545
+ audience: The audience of the JWT token.
621
546
 
622
547
  Returns:
623
- bool: authentication success or failure.
548
+ True if authentication succeeds.
624
549
  """
625
550
  old_token = self.jwt_token
626
551
  self.jwt_token = token
627
- headers = {
628
- **({"X-Audience": audience} if audience else {}),
629
- **({"X-Permission": permission} if permission else {}),
630
- }
552
+
553
+ headers = {}
554
+ if audience:
555
+ headers["X-Audience"] = audience
556
+ if permission:
557
+ headers["X-Permission"] = permission
558
+
631
559
  resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/auth", header=headers)
560
+
632
561
  if resp.status_code != HTTPStatus.OK:
633
562
  self.jwt_token = old_token
634
563
  raise Exception(resp.text)
635
- return resp.status_code == HTTPStatus.OK
564
+
565
+ return True
636
566
 
637
567
  def renew_token(self, timeout: Union[int, str] = DURATION_ONE_WEEK_ISO) -> str:
638
568
  """Renew the current token.
639
569
 
640
570
  Args:
641
- timeout_seconds (int | str, optional): token expire timeout of seconds. support ISO 8601 durations (e.g., 'P1Y2M3DT4H5M6S' 'P1W').
571
+ timeout: Token expire timeout.
642
572
 
643
573
  Returns:
644
- str: The new JWT token. The old token will be invalidated.
645
-
646
- Raises:
647
- Exception: If token renewal fails or no token exists to renew
574
+ The new JWT token.
648
575
  """
649
- # Ensure token exists
650
576
  if not self.jwt_token:
651
577
  raise Exception("No token to renew")
652
578
 
653
- try:
654
- # Handle App Mesh token (string format)
655
- if isinstance(self.jwt_token, str):
656
- resp = self._request_http(
657
- AppMeshClient.Method.POST,
658
- path="/appmesh/token/renew",
659
- header={"X-Expire-Seconds": str(self._parse_duration(timeout))},
660
- )
661
- if resp.status_code == HTTPStatus.OK:
662
- if "access_token" in resp.json():
663
- new_token = resp.json()["access_token"]
664
- self.jwt_token = new_token
665
- else:
666
- raise Exception("Token renewal response missing access_token")
667
- else:
668
- raise Exception(resp.text)
669
- else:
670
- raise Exception("Unsupported token format")
579
+ if not isinstance(self.jwt_token, str):
580
+ raise Exception("Unsupported token format")
671
581
 
672
- return self.jwt_token
582
+ resp = self._request_http(
583
+ AppMeshClient.Method.POST,
584
+ path="/appmesh/token/renew",
585
+ header={"X-Expire-Seconds": str(self._parse_duration(timeout))},
586
+ )
673
587
 
674
- except Exception as e:
675
- logging.error("Token renewal failed: %s", str(e))
676
- raise Exception(f"Token renewal failed: {str(e)}") from e
588
+ if resp.status_code == HTTPStatus.OK:
589
+ response_data = resp.json()
590
+ if "access_token" not in response_data:
591
+ raise Exception("Token renewal response missing access_token")
592
+ self.jwt_token = response_data["access_token"]
593
+ else:
594
+ raise Exception(resp.text)
677
595
 
678
- def get_totp_secret(self) -> str:
679
- """
680
- Generate TOTP secret for the current user and return a secret.
596
+ return self.jwt_token
681
597
 
682
- Returns:
683
- str: TOTP secret string
684
- """
598
+ def get_totp_secret(self) -> str:
599
+ """Generate TOTP secret for the current user."""
685
600
  resp = self._request_http(method=AppMeshClient.Method.POST, path="/appmesh/totp/secret")
686
- if resp.status_code == HTTPStatus.OK:
687
- totp_uri = base64.b64decode(resp.json()["mfa_uri"]).decode()
688
- return self._parse_totp_uri(totp_uri).get("secret")
689
601
 
690
- raise Exception(resp.text)
602
+ if resp.status_code != HTTPStatus.OK:
603
+ raise Exception(resp.text)
604
+
605
+ totp_uri = base64.b64decode(resp.json()["mfa_uri"]).decode()
606
+ parsed_uri = self._parse_totp_uri(totp_uri)
607
+ secret = parsed_uri.get("secret")
608
+ if secret is None:
609
+ raise Exception("TOTP URI does not contain a 'secret' field")
610
+ return secret
691
611
 
692
612
  def setup_totp(self, totp_code: str) -> str:
693
613
  """Set up 2FA for the current user.
694
614
 
695
615
  Args:
696
- totp_code (str): TOTP code
616
+ totp_code: TOTP code.
697
617
 
698
618
  Returns:
699
- str: The new JWT token if setup success, the old token will be blocked.
619
+ The new JWT token if setup succeeds.
700
620
  """
701
621
  resp = self._request_http(
702
622
  method=AppMeshClient.Method.POST,
703
623
  path="/appmesh/totp/setup",
704
624
  header={"X-Totp-Code": totp_code},
705
625
  )
626
+
706
627
  if resp.status_code == HTTPStatus.OK:
707
628
  if "access_token" in resp.json():
708
629
  self.jwt_token = resp.json()["access_token"]
709
630
  return self.jwt_token
710
- else:
711
- raise Exception(resp.text)
712
631
 
713
- def disable_totp(self, user: str = "self") -> bool:
714
- """Disable 2FA for the specified user.
715
-
716
- Args:
717
- user (str, optional): user name for disable TOTP.
632
+ raise Exception(resp.text)
718
633
 
719
- Returns:
720
- bool: success or failure.
721
- """
634
+ def disable_totp(self, user: str = "self") -> None:
635
+ """Disable 2FA for the specified user."""
722
636
  resp = self._request_http(
723
637
  method=AppMeshClient.Method.POST,
724
638
  path=f"/appmesh/totp/{user}/disable",
725
639
  )
640
+
726
641
  if resp.status_code != HTTPStatus.OK:
727
642
  raise Exception(resp.text)
728
- return resp.status_code == HTTPStatus.OK
729
643
 
730
644
  @staticmethod
731
645
  def _parse_totp_uri(totp_uri: str) -> dict:
732
- """Extract TOTP parameters
733
-
734
- Args:
735
- totp_uri (str): TOTP uri
736
-
737
- Returns:
738
- dict: eextract parameters
739
- """
646
+ """Extract TOTP parameters from URI."""
740
647
  parsed_info = {}
741
648
  parsed_uri = parse.urlparse(totp_uri)
742
649
 
743
650
  # Extract label from the path
744
- parsed_info["label"] = parsed_uri.path[1:] # Remove the leading slash
651
+ parsed_info["label"] = parsed_uri.path[1:] # Remove leading slash
745
652
 
746
653
  # Extract parameters from the query string
747
654
  query_params = parse.parse_qs(parsed_uri.query)
748
655
  for key, value in query_params.items():
749
656
  parsed_info[key] = value[0]
657
+
750
658
  return parsed_info
751
659
 
752
660
  ########################################
753
661
  # Application view
754
662
  ########################################
755
663
  def view_app(self, app_name: str) -> App:
756
- """Get information about a specific application.
757
-
758
- Args:
759
- app_name (str): the application name.
760
-
761
- Returns:
762
- App: the application object both contain static configuration and runtime information.
763
-
764
- Exception:
765
- failed request or no such application
766
- """
664
+ """Get information about a specific application."""
767
665
  resp = self._request_http(AppMeshClient.Method.GET, path=f"/appmesh/app/{app_name}")
666
+
768
667
  if resp.status_code != HTTPStatus.OK:
769
668
  raise Exception(resp.text)
770
- return App(resp.json())
771
-
772
- def view_all_apps(self):
773
- """Get information about all applications.
774
669
 
775
- Returns:
776
- list: the application object both contain static configuration and runtime information, only return applications that the user has permissions.
670
+ return App(resp.json())
777
671
 
778
- Exception:
779
- failed request or no such application
780
- """
672
+ def view_all_apps(self) -> list:
673
+ """Get information about all applications."""
781
674
  resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/applications")
675
+
782
676
  if resp.status_code != HTTPStatus.OK:
783
677
  raise Exception(resp.text)
784
- apps = []
785
- for app in resp.json():
786
- apps.append(App(app))
787
- return apps
678
+
679
+ return [App(app) for app in resp.json()]
788
680
 
789
681
  def get_app_output(self, app_name: str, stdout_position: int = 0, stdout_index: int = 0, stdout_maxsize: int = 10240, process_uuid: str = "", timeout: int = 0) -> AppOutput:
790
682
  """Get the stdout/stderr of an application.
791
683
 
792
684
  Args:
793
- app_name (str): the application name
794
- stdout_position (int, optional): start read position, 0 means start from beginning.
795
- stdout_index (int, optional): index of history process stdout, 0 means get from current running process,
685
+ app_name: the application name
686
+ stdout_position: start read position, 0 means start from beginning.
687
+ stdout_index: index of history process stdout, 0 means get from current running process,
796
688
  the stdout number depends on 'stdout_cache_size' of the application.
797
- stdout_maxsize (int, optional): max buffer size to read.
798
- process_uuid (str, optional): used to get the specified process.
799
- timeout (int, optional): wait for the running process for some time(seconds) to get the output.
689
+ stdout_maxsize: max buffer size to read.
690
+ process_uuid: used to get the specified process.
691
+ timeout: wait for the running process for some time(seconds) to get the output.
800
692
 
801
693
  Returns:
802
694
  AppOutput object.
@@ -812,450 +704,312 @@ class AppMeshClient(metaclass=abc.ABCMeta):
812
704
  "timeout": str(timeout),
813
705
  },
814
706
  )
707
+
815
708
  out_position = int(resp.headers["X-Output-Position"]) if "X-Output-Position" in resp.headers else None
816
709
  exit_code = int(resp.headers["X-Exit-Code"]) if "X-Exit-Code" in resp.headers else None
710
+
817
711
  return AppOutput(status_code=resp.status_code, output=resp.text, out_position=out_position, exit_code=exit_code)
818
712
 
819
713
  def check_app_health(self, app_name: str) -> bool:
820
- """Check the health status of an application.
821
-
822
- Args:
823
- app_name (str): the application name.
824
-
825
- Returns:
826
- bool: healthy or not
827
- """
714
+ """Check the health status of an application."""
828
715
  resp = self._request_http(AppMeshClient.Method.GET, path=f"/appmesh/app/{app_name}/health")
716
+
829
717
  if resp.status_code != HTTPStatus.OK:
830
718
  raise Exception(resp.text)
719
+
831
720
  return int(resp.text) == 0
832
721
 
833
722
  ########################################
834
723
  # Application manage
835
724
  ########################################
836
725
  def add_app(self, app: App) -> App:
837
- """Register a new application.
838
-
839
- Args:
840
- app (App): the application definition.
841
-
842
- Returns:
843
- App: resigtered application object.
844
-
845
- Exception:
846
- failed request
847
- """
726
+ # type: (App) -> App
727
+ """Register a new application."""
848
728
  resp = self._request_http(AppMeshClient.Method.PUT, path=f"/appmesh/app/{app.name}", body=app.json())
729
+
849
730
  if resp.status_code != HTTPStatus.OK:
850
731
  raise Exception(resp.text)
732
+
851
733
  return App(resp.json())
852
734
 
853
735
  def delete_app(self, app_name: str) -> bool:
854
- """Remove an application.
855
-
856
- Args:
857
- app_name (str): the application name.
858
-
859
- Returns:
860
- bool: True for delete success, Flase for not exist anymore.
861
- """
736
+ """Remove an application."""
862
737
  resp = self._request_http(AppMeshClient.Method.DELETE, path=f"/appmesh/app/{app_name}")
738
+
863
739
  if resp.status_code == HTTPStatus.OK:
864
740
  return True
865
- elif resp.status_code == HTTPStatus.NOT_FOUND:
741
+ if resp.status_code == HTTPStatus.NOT_FOUND:
866
742
  return False
867
- else:
868
- raise Exception(resp.text)
869
-
870
- def enable_app(self, app_name: str) -> bool:
871
- """Enable an application.
872
743
 
873
- Args:
874
- app_name (str): the application name.
744
+ raise Exception(resp.text)
875
745
 
876
- Returns:
877
- bool: success or failure.
878
- """
746
+ def enable_app(self, app_name: str) -> None:
747
+ """Enable an application."""
879
748
  resp = self._request_http(AppMeshClient.Method.POST, path=f"/appmesh/app/{app_name}/enable")
749
+
880
750
  if resp.status_code != HTTPStatus.OK:
881
751
  raise Exception(resp.text)
882
- return resp.status_code == HTTPStatus.OK
883
-
884
- def disable_app(self, app_name: str) -> bool:
885
- """Disable an application.
886
752
 
887
- Args:
888
- app_name (str): the application name.
889
-
890
- Returns:
891
- bool: success or failure.
892
- """
753
+ def disable_app(self, app_name: str) -> None:
754
+ """Disable an application."""
893
755
  resp = self._request_http(AppMeshClient.Method.POST, path=f"/appmesh/app/{app_name}/disable")
756
+
894
757
  if resp.status_code != HTTPStatus.OK:
895
758
  raise Exception(resp.text)
896
- return resp.status_code == HTTPStatus.OK
897
759
 
898
760
  ########################################
899
761
  # Configuration
900
762
  ########################################
901
763
  def view_host_resources(self) -> dict:
902
- """Get a report of host resources including CPU, memory, and disk.
903
-
904
- Returns:
905
- dict: the host resource json.
906
- """
764
+ """Get a report of host resources including CPU, memory, and disk."""
907
765
  resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/resources")
766
+
908
767
  if resp.status_code != HTTPStatus.OK:
909
768
  raise Exception(resp.text)
769
+
910
770
  return resp.json()
911
771
 
912
772
  def view_config(self) -> dict:
913
- """Get the App Mesh configuration in JSON format.
914
-
915
- Returns:
916
- dict: the configuration json.
917
- """
773
+ """Get the App Mesh configuration in JSON format."""
918
774
  resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/config")
775
+
919
776
  if resp.status_code != HTTPStatus.OK:
920
777
  raise Exception(resp.text)
778
+
921
779
  return resp.json()
922
780
 
923
781
  def set_config(self, config_json: dict) -> dict:
924
- """Update the configuration.
925
-
926
- Args:
927
- cfg_json (dict): the new configuration json.
928
-
929
- Returns:
930
- dict: the updated configuration json.
931
- """
782
+ """Update the configuration."""
932
783
  resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/config", body=config_json)
784
+
933
785
  if resp.status_code != HTTPStatus.OK:
934
786
  raise Exception(resp.text)
787
+
935
788
  return resp.json()
936
789
 
937
790
  def set_log_level(self, level: str = "DEBUG") -> str:
938
- """Update the log level.
939
-
940
- Args:
941
- level (str, optional): log level.
942
-
943
- Returns:
944
- str: the updated log level.
945
- """
791
+ """Update the log level."""
946
792
  resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/config", body={"BaseConfig": {"LogLevel": level}})
793
+
947
794
  if resp.status_code != HTTPStatus.OK:
948
795
  raise Exception(resp.text)
796
+
949
797
  return resp.json()["BaseConfig"]["LogLevel"]
950
798
 
951
799
  ########################################
952
800
  # User Management
953
801
  ########################################
954
- def update_user_password(self, old_password: str, new_password: str, user_name: str = "self") -> bool:
955
- """Change the password of a user.
956
-
957
- Args:
958
- user_name (str): the user name.
959
- old_password (str): the old password string.
960
- new_password (str):the new password string.
802
+ def update_user_password(self, old_password: str, new_password: str, user_name: str = "self") -> None:
803
+ """Change the password of a user."""
804
+ body = {
805
+ "old_password": base64.b64encode(old_password.encode()).decode(),
806
+ "new_password": base64.b64encode(new_password.encode()).decode(),
807
+ }
961
808
 
962
- Returns:
963
- bool: success
964
- """
965
809
  resp = self._request_http(
966
810
  method=AppMeshClient.Method.POST,
967
811
  path=f"/appmesh/user/{user_name}/passwd",
968
- body={
969
- "old_password": base64.b64encode(old_password.encode()).decode(),
970
- "new_password": base64.b64encode(new_password.encode()).decode(),
971
- },
812
+ body=body,
972
813
  )
814
+
973
815
  if resp.status_code != HTTPStatus.OK:
974
816
  raise Exception(resp.text)
975
- return True
976
-
977
- def add_user(self, user_name: str, user_json: dict) -> bool:
978
- """Add a new user.
979
-
980
- Args:
981
- user_name (str): the user name.
982
- user_json (dict): user definition, follow same user format from security.yaml.
983
817
 
984
- Returns:
985
- bool: success or failure.
986
- """
818
+ def add_user(self, user_name: str, user_json: dict) -> None:
819
+ """Add a new user."""
987
820
  resp = self._request_http(
988
821
  method=AppMeshClient.Method.PUT,
989
822
  path=f"/appmesh/user/{user_name}",
990
823
  body=user_json,
991
824
  )
992
- return resp.status_code == HTTPStatus.OK
993
-
994
- def delete_user(self, user_name: str) -> bool:
995
- """Delete a user.
996
-
997
- Args:
998
- user_name (str): the user name.
825
+ if resp.status_code != HTTPStatus.OK:
826
+ raise Exception(resp.text)
999
827
 
1000
- Returns:
1001
- bool: success or failure.
1002
- """
828
+ def delete_user(self, user_name: str):
829
+ """Delete a user."""
1003
830
  resp = self._request_http(
1004
831
  method=AppMeshClient.Method.DELETE,
1005
832
  path=f"/appmesh/user/{user_name}",
1006
833
  )
1007
- return resp.status_code == HTTPStatus.OK
1008
-
1009
- def lock_user(self, user_name: str) -> bool:
1010
- """Lock a user.
1011
-
1012
- Args:
1013
- user_name (str): the user name.
834
+ if resp.status_code != HTTPStatus.OK:
835
+ raise Exception(resp.text)
1014
836
 
1015
- Returns:
1016
- bool: success or failure.
1017
- """
837
+ def lock_user(self, user_name: str) -> None:
838
+ """Lock a user."""
1018
839
  resp = self._request_http(
1019
840
  method=AppMeshClient.Method.POST,
1020
841
  path=f"/appmesh/user/{user_name}/lock",
1021
842
  )
843
+
1022
844
  if resp.status_code != HTTPStatus.OK:
1023
845
  raise Exception(resp.text)
1024
- return resp.status_code == HTTPStatus.OK
1025
-
1026
- def unlock_user(self, user_name: str) -> bool:
1027
- """Unlock a user.
1028
846
 
1029
- Args:
1030
- user_name (str): the user name.
1031
-
1032
- Returns:
1033
- bool: success or failure.
1034
- """
847
+ def unlock_user(self, user_name: str) -> None:
848
+ """Unlock a user."""
1035
849
  resp = self._request_http(
1036
850
  method=AppMeshClient.Method.POST,
1037
851
  path=f"/appmesh/user/{user_name}/unlock",
1038
852
  )
853
+
1039
854
  if resp.status_code != HTTPStatus.OK:
1040
855
  raise Exception(resp.text)
1041
- return resp.status_code == HTTPStatus.OK
1042
856
 
1043
857
  def view_users(self) -> dict:
1044
- """Get information about all users.
1045
-
1046
- Returns:
1047
- dict: all user definition
1048
- """
858
+ """Get information about all users."""
1049
859
  resp = self._request_http(method=AppMeshClient.Method.GET, path="/appmesh/users")
860
+
1050
861
  if resp.status_code != HTTPStatus.OK:
1051
862
  raise Exception(resp.text)
863
+
1052
864
  return resp.json()
1053
865
 
1054
866
  def view_self(self) -> dict:
1055
- """Get information about the current user.
1056
-
1057
- Returns:
1058
- dict: user definition.
1059
- """
867
+ """Get information about the current user."""
1060
868
  resp = self._request_http(method=AppMeshClient.Method.GET, path="/appmesh/user/self")
869
+
1061
870
  if resp.status_code != HTTPStatus.OK:
1062
871
  raise Exception(resp.text)
872
+
1063
873
  return resp.json()
1064
874
 
1065
875
  def view_groups(self) -> list:
1066
- """Get information about all user groups.
1067
-
1068
- Returns:
1069
- dict: user group array.
1070
- """
876
+ """Get information about all user groups."""
1071
877
  resp = self._request_http(method=AppMeshClient.Method.GET, path="/appmesh/user/groups")
878
+
1072
879
  if resp.status_code != HTTPStatus.OK:
1073
880
  raise Exception(resp.text)
881
+
1074
882
  return resp.json()
1075
883
 
1076
884
  def view_permissions(self) -> list:
1077
- """Get information about all available permissions.
1078
-
1079
- Returns:
1080
- dict: permission array
1081
- """
885
+ """Get information about all available permissions."""
1082
886
  resp = self._request_http(method=AppMeshClient.Method.GET, path="/appmesh/permissions")
887
+
1083
888
  if resp.status_code != HTTPStatus.OK:
1084
889
  raise Exception(resp.text)
890
+
1085
891
  return resp.json()
1086
892
 
1087
893
  def view_user_permissions(self) -> list:
1088
- """Get information about the permissions of the current user.
1089
-
1090
- Returns:
1091
- dict: user permission array.
1092
- """
894
+ """Get information about the permissions of the current user."""
1093
895
  resp = self._request_http(method=AppMeshClient.Method.GET, path="/appmesh/user/permissions")
896
+
1094
897
  if resp.status_code != HTTPStatus.OK:
1095
898
  raise Exception(resp.text)
899
+
1096
900
  return resp.json()
1097
901
 
1098
902
  def view_roles(self) -> list:
1099
- """Get information about all roles with permission definitions.
1100
-
1101
- Returns:
1102
- dict: all role definition.
1103
- """
903
+ """Get information about all roles with permission definitions."""
1104
904
  resp = self._request_http(method=AppMeshClient.Method.GET, path="/appmesh/roles")
905
+
1105
906
  if resp.status_code != HTTPStatus.OK:
1106
907
  raise Exception(resp.text)
1107
- return resp.json()
1108
908
 
1109
- def update_role(self, role_name: str, role_permission_json: dict) -> bool:
1110
- """Update or add a role with defined permissions.
1111
-
1112
- Args:
1113
- role_name (str): the role name.
1114
- role_permission_json (dict): role permission definition array, e.g: ["app-control", "app-delete"]
909
+ return resp.json()
1115
910
 
1116
- Returns:
1117
- bool: success or failure.
1118
- """
911
+ def update_role(self, role_name: str, role_permission_json: dict) -> None:
912
+ """Update or add a role with defined permissions."""
1119
913
  resp = self._request_http(method=AppMeshClient.Method.POST, path=f"/appmesh/role/{role_name}", body=role_permission_json)
914
+
1120
915
  if resp.status_code != HTTPStatus.OK:
1121
916
  raise Exception(resp.text)
1122
- return resp.status_code == HTTPStatus.OK
1123
-
1124
- def delete_role(self, role_name: str) -> bool:
1125
- """Delete a user role.
1126
-
1127
- Args:
1128
- role_name (str): the role name.
1129
917
 
1130
- Returns:
1131
- bool: success or failure.
1132
- """
918
+ def delete_role(self, role_name: str) -> None:
919
+ """Delete a user role."""
1133
920
  resp = self._request_http(
1134
921
  method=AppMeshClient.Method.DELETE,
1135
922
  path=f"/appmesh/role/{role_name}",
1136
923
  )
924
+
1137
925
  if resp.status_code != HTTPStatus.OK:
1138
926
  raise Exception(resp.text)
1139
- return resp.status_code == HTTPStatus.OK
1140
927
 
1141
928
  ########################################
1142
929
  # Tag management
1143
930
  ########################################
1144
- def add_tag(self, tag_name: str, tag_value: str) -> bool:
1145
- """Add a new label.
1146
-
1147
- Args:
1148
- tag_name (str): the label name.
1149
- tag_value (str): the label value.
1150
-
1151
- Returns:
1152
- bool: success or failure.
1153
- """
931
+ def add_tag(self, tag_name: str, tag_value: str) -> None:
932
+ """Add a new label."""
1154
933
  resp = self._request_http(
1155
934
  AppMeshClient.Method.PUT,
1156
935
  query={"value": tag_value},
1157
936
  path=f"/appmesh/label/{tag_name}",
1158
937
  )
938
+
1159
939
  if resp.status_code != HTTPStatus.OK:
1160
940
  raise Exception(resp.text)
1161
- return resp.status_code == HTTPStatus.OK
1162
-
1163
- def delete_tag(self, tag_name: str) -> bool:
1164
- """Delete a label.
1165
941
 
1166
- Args:
1167
- tag_name (str): the label name.
1168
-
1169
- Returns:
1170
- bool: success or failure.
1171
- """
942
+ def delete_tag(self, tag_name: str) -> None:
943
+ """Delete a label."""
1172
944
  resp = self._request_http(AppMeshClient.Method.DELETE, path=f"/appmesh/label/{tag_name}")
945
+
1173
946
  if resp.status_code != HTTPStatus.OK:
1174
947
  raise Exception(resp.text)
1175
- return resp.status_code == HTTPStatus.OK
1176
948
 
1177
949
  def view_tags(self) -> dict:
1178
- """Get information about all labels.
1179
-
1180
- Returns:
1181
- dict: label data.
1182
- """
950
+ """Get information about all labels."""
1183
951
  resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/labels")
952
+
1184
953
  if resp.status_code != HTTPStatus.OK:
1185
954
  raise Exception(resp.text)
955
+
1186
956
  return resp.json()
1187
957
 
1188
958
  ########################################
1189
- # Promethus metrics
959
+ # Prometheus metrics
1190
960
  ########################################
1191
- def get_metrics(self):
1192
- """Get Prometheus metrics.
1193
-
1194
- Returns:
1195
- str: prometheus metrics texts
1196
- """
961
+ def get_metrics(self) -> str:
962
+ """Get Prometheus metrics."""
1197
963
  resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/metrics")
964
+
1198
965
  if resp.status_code != HTTPStatus.OK:
1199
966
  raise Exception(resp.text)
967
+
1200
968
  return resp.text
1201
969
 
1202
970
  ########################################
1203
971
  # File management
1204
972
  ########################################
1205
- def download_file(self, remote_file: str, local_file: str, apply_file_attributes: bool = True) -> None:
1206
- """Download a remote file to the local system. Optionally, the local file will have the same permission as the remote file.
1207
-
1208
- Args:
1209
- remote_file (str): the remote file path.
1210
- local_file (str): the local file path to be downloaded.
1211
- apply_file_attributes (bool): whether to apply file attributes (permissions, owner, group) to the local file.
1212
- """
973
+ def download_file(self, remote_file: str, local_file: str, preserve_permissions: bool = True) -> None:
974
+ """Download a remote file to the local system."""
1213
975
  resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/file/download", header={self.HTTP_HEADER_KEY_X_FILE_PATH: remote_file})
1214
976
  resp.raise_for_status()
1215
977
 
1216
978
  # Write the file content locally
1217
- with open(local_file, "wb") as fp:
1218
- for chunk in resp.iter_content(chunk_size=8 * 1024): # 8 KB
979
+ local_path = Path(local_file)
980
+ with local_path.open("wb") as fp:
981
+ for chunk in resp.iter_content(chunk_size=8 * 1024):
1219
982
  if chunk:
1220
983
  fp.write(chunk)
1221
984
 
1222
985
  # Apply file attributes (permissions, owner, group) if requested
1223
- if apply_file_attributes and sys.platform != "win32":
986
+ if preserve_permissions and sys.platform != "win32":
1224
987
  if "X-File-Mode" in resp.headers:
1225
- os.chmod(path=local_file, mode=int(resp.headers["X-File-Mode"]))
988
+ local_path.chmod(int(resp.headers["X-File-Mode"]))
989
+
1226
990
  if "X-File-User" in resp.headers and "X-File-Group" in resp.headers:
1227
991
  file_uid = int(resp.headers["X-File-User"])
1228
992
  file_gid = int(resp.headers["X-File-Group"])
1229
- try:
993
+ with suppress(PermissionError):
1230
994
  os.chown(path=local_file, uid=file_uid, gid=file_gid)
1231
- except PermissionError:
1232
- logging.warning(f"Warning: Unable to change owner/group of {local_file}. Operation requires elevated privileges.")
1233
-
1234
- def upload_file(self, local_file: str, remote_file: str, apply_file_attributes: bool = True) -> None:
1235
- """Upload a local file to the remote server. Optionally, the remote file will have the same permission as the local file.
1236
995
 
1237
- Dependency:
1238
- sudo apt install python3-pip
1239
- pip3 install requests_toolbelt
1240
-
1241
- Args:
1242
- local_file (str): the local file path.
1243
- remote_file (str): the target remote file to be uploaded.
1244
- apply_file_attributes (bool): whether to upload file attributes (permissions, owner, group) along with the file.
1245
- """
1246
- if not os.path.exists(local_file):
996
+ def upload_file(self, local_file: str, remote_file: str, preserve_permissions: bool = True) -> None:
997
+ """Upload a local file to the remote server."""
998
+ local_path = Path(local_file)
999
+ if not local_path.exists():
1247
1000
  raise FileNotFoundError(f"Local file not found: {local_file}")
1248
1001
 
1249
1002
  from requests_toolbelt import MultipartEncoder
1250
1003
 
1251
- with open(file=local_file, mode="rb") as fp:
1004
+ with local_path.open("rb") as fp:
1252
1005
  encoder = MultipartEncoder(fields={"filename": os.path.basename(remote_file), "file": ("filename", fp, "application/octet-stream")})
1006
+
1253
1007
  header = {self.HTTP_HEADER_KEY_X_FILE_PATH: parse.quote(remote_file), "Content-Type": encoder.content_type}
1254
1008
 
1255
- # Include file attributes (permissions, owner, group) if requested
1256
- if apply_file_attributes:
1257
- file_stat = os.stat(local_file)
1258
- header["X-File-Mode"] = str(file_stat.st_mode & 0o777) # Mask to keep only permission bits
1009
+ # Include file attributes if requested
1010
+ if preserve_permissions:
1011
+ file_stat = local_path.stat()
1012
+ header["X-File-Mode"] = str(file_stat.st_mode & 0o777) # Mask to keep only permission bits
1259
1013
  header["X-File-User"] = str(file_stat.st_uid)
1260
1014
  header["X-File-Group"] = str(file_stat.st_gid)
1261
1015
 
@@ -1272,13 +1026,14 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1272
1026
  ########################################
1273
1027
  # Application run
1274
1028
  ########################################
1275
- def _parse_duration(self, timeout) -> int:
1029
+ @staticmethod
1030
+ def _parse_duration(timeout: Union[int, str]) -> int:
1031
+ """Parse duration from int or ISO 8601 string."""
1276
1032
  if isinstance(timeout, int):
1277
1033
  return timeout
1278
- elif isinstance(timeout, str):
1034
+ if isinstance(timeout, str):
1279
1035
  return int(aniso8601.parse_duration(timeout).total_seconds())
1280
- else:
1281
- raise TypeError(f"Invalid timeout type: {str(timeout)}")
1036
+ raise TypeError(f"Invalid timeout type: {timeout}")
1282
1037
 
1283
1038
  def run_task(self, app_name: str, data: str, timeout: int = 300) -> str:
1284
1039
  """Client send an invocation message to a running App Mesh application and wait for result.
@@ -1287,20 +1042,20 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1287
1042
  forward it to the specified running application instance.
1288
1043
 
1289
1044
  Args:
1290
- app_name (str): Name of the target application (as registered in App Mesh).
1291
- data (str): Payload to deliver to the application. Typically a string.
1292
- timeout (int): Maximum time in seconds to wait for a response from the application. Defaults to 60 seconds.
1045
+ app_name: Name of the target application (as registered in App Mesh).
1046
+ data: Payload to deliver to the application. Typically a string.
1047
+ timeout: Maximum time in seconds to wait for a response from the application. Defaults to 60 seconds.
1293
1048
 
1294
1049
  Returns:
1295
1050
  str: The HTTP response body returned by the remote application/service.
1296
1051
  """
1297
- path = f"/appmesh/app/{app_name}/task"
1298
1052
  resp = self._request_http(
1299
1053
  AppMeshClient.Method.POST,
1300
- path=path,
1054
+ path=f"/appmesh/app/{app_name}/task",
1301
1055
  body=data,
1302
1056
  query={"timeout": str(timeout)},
1303
1057
  )
1058
+
1304
1059
  if resp.status_code != HTTPStatus.OK:
1305
1060
  raise Exception(resp.text)
1306
1061
 
@@ -1310,15 +1065,14 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1310
1065
  """Client cancle a running task to a App Mesh application.
1311
1066
 
1312
1067
  Args:
1313
- app_name (str): Name of the target application (as registered in App Mesh).
1068
+ app_name: Name of the target application (as registered in App Mesh).
1314
1069
 
1315
1070
  Returns:
1316
1071
  bool: Task exist and cancled status.
1317
1072
  """
1318
- path = f"/appmesh/app/{app_name}/task"
1319
1073
  resp = self._request_http(
1320
1074
  AppMeshClient.Method.DELETE,
1321
- path=path,
1075
+ path=f"/appmesh/app/{app_name}/task",
1322
1076
  )
1323
1077
  return resp.status_code == HTTPStatus.OK
1324
1078
 
@@ -1331,15 +1085,15 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1331
1085
  """Run an application asynchronously on a remote system without blocking the API.
1332
1086
 
1333
1087
  Args:
1334
- app (Union[App, str]): An `App` instance or a shell command string.
1088
+ app: An `App` instance or a shell command string.
1335
1089
  - If `app` is a string, it is treated as a shell command for the remote run,
1336
1090
  and an `App` instance is created as:
1337
1091
  `App({"command": "<command_string>", "shell": True})`.
1338
1092
  - If `app` is an `App` object, providing only the `name` attribute (without
1339
1093
  a command) will run an existing application; otherwise, it is treated as a new application.
1340
- max_time_seconds (Union[int, str], optional): Maximum runtime for the remote process.
1094
+ max_time_seconds: Maximum runtime for the remote process.
1341
1095
  Accepts ISO 8601 duration format (e.g., 'P1Y2M3DT4H5M6S', 'P5W'). Defaults to `P2D`.
1342
- life_cycle_seconds (Union[int, str], optional): Maximum lifecycle time for the remote process.
1096
+ life_cycle_seconds: Maximum lifecycle time for the remote process.
1343
1097
  Accepts ISO 8601 duration format. Defaults to `P2DT12H`.
1344
1098
 
1345
1099
  Returns:
@@ -1348,56 +1102,63 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1348
1102
  if isinstance(app, str):
1349
1103
  app = App({"command": app, "shell": True})
1350
1104
 
1351
- path = "/appmesh/app/run"
1352
1105
  resp = self._request_http(
1353
1106
  AppMeshClient.Method.POST,
1354
1107
  body=app.json(),
1355
- path=path,
1108
+ path="/appmesh/app/run",
1356
1109
  query={
1357
1110
  "timeout": str(self._parse_duration(max_time_seconds)),
1358
1111
  "lifecycle": str(self._parse_duration(life_cycle_seconds)),
1359
1112
  },
1360
1113
  )
1114
+
1361
1115
  if resp.status_code != HTTPStatus.OK:
1362
1116
  raise Exception(resp.text)
1363
1117
 
1364
- # Return an AppRun object with the application name and process UUID
1365
- return AppRun(self, resp.json()["name"], resp.json()["process_uuid"])
1118
+ response_data = resp.json()
1119
+ return AppRun(self, response_data["name"], response_data["process_uuid"])
1366
1120
 
1367
- def wait_for_async_run(self, run: AppRun, stdout_print: bool = True, timeout: int = 0) -> int:
1121
+ def wait_for_async_run(self, run: AppRun, stdout_print: bool = True, timeout: int = 0) -> Optional[int]:
1368
1122
  """Wait for an asynchronous run to finish.
1369
1123
 
1370
1124
  Args:
1371
- run (AppRun): asyncrized run result from run_async().
1372
- stdout_print (bool, optional): print remote stdout to local or not.
1373
- timeout (int, optional): wait max timeout seconds and return if not finished, 0 means wait until finished
1125
+ run: asyncrized run result from run_async().
1126
+ stdout_print: print remote stdout to local or not.
1127
+ timeout : wait max timeout seconds and return if not finished, 0 means wait until finished
1374
1128
 
1375
1129
  Returns:
1376
- int: return exit code if process finished, return None for timeout or exception.
1130
+ return exit code if process finished, return None for timeout or exception.
1377
1131
  """
1378
- if run:
1379
- last_output_position = 0
1380
- start = datetime.now()
1381
- interval = 1 if self.__class__.__name__ == "AppMeshClient" else 1000
1382
- while len(run.proc_uid) > 0:
1383
- app_out = self.get_app_output(app_name=run.app_name, stdout_position=last_output_position, stdout_index=0, process_uuid=run.proc_uid, timeout=interval)
1384
- if app_out.output and stdout_print:
1385
- print(app_out.output, end="", flush=True)
1386
- if app_out.out_position is not None:
1387
- last_output_position = app_out.out_position
1388
- if app_out.exit_code is not None:
1389
- # success
1390
- try:
1391
- self.delete_app(run.app_name)
1392
- except Exception:
1393
- pass
1394
- return app_out.exit_code
1395
- if app_out.status_code != HTTPStatus.OK:
1396
- # failed
1397
- break
1398
- if timeout > 0 and (datetime.now() - start).seconds > timeout:
1399
- # timeout
1400
- break
1132
+ if not run:
1133
+ return None
1134
+
1135
+ last_output_position = 0
1136
+ start = datetime.now()
1137
+ interval = 1 if self.__class__.__name__ == "AppMeshClient" else 1000
1138
+
1139
+ while run.proc_uid:
1140
+ app_out = self.get_app_output(app_name=run.app_name, stdout_position=last_output_position, stdout_index=0, process_uuid=run.proc_uid, timeout=interval)
1141
+
1142
+ if app_out.output and stdout_print:
1143
+ print(app_out.output, end="", flush=True)
1144
+
1145
+ if app_out.out_position is not None:
1146
+ last_output_position = app_out.out_position
1147
+
1148
+ if app_out.exit_code is not None:
1149
+ # success
1150
+ with suppress(Exception):
1151
+ self.delete_app(run.app_name)
1152
+ return app_out.exit_code
1153
+
1154
+ if app_out.status_code != HTTPStatus.OK:
1155
+ # failed
1156
+ break
1157
+
1158
+ if timeout > 0 and (datetime.now() - start).seconds > timeout:
1159
+ # timeout
1160
+ break
1161
+
1401
1162
  return None
1402
1163
 
1403
1164
  def run_app_sync(
@@ -1413,101 +1174,98 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1413
1174
  If 'app' is App object, the name attribute is used to run an existing application if specified.
1414
1175
 
1415
1176
  Args:
1416
- app (Union[App, str]): An App instance or a shell command string.
1177
+ app: An App instance or a shell command string.
1417
1178
  If a string, an App instance is created as:
1418
1179
  `appmesh.App({"command": "<command_string>", "shell": True})`
1419
- stdout_print (bool, optional): If True, prints the remote stdout locally. Defaults to True.
1420
- max_time_seconds (Union[int, str], optional): Maximum runtime for the remote process.
1180
+ stdout_print: If True, prints the remote stdout locally. Defaults to True.
1181
+ max_time_seconds: Maximum runtime for the remote process.
1421
1182
  Supports ISO 8601 duration format (e.g., 'P1Y2M3DT4H5M6S', 'P5W'). Defaults to DEFAULT_RUN_APP_TIMEOUT_SECONDS.
1422
- life_cycle_seconds (Union[int, str], optional): Maximum lifecycle time for the remote process.
1183
+ life_cycle_seconds: Maximum lifecycle time for the remote process.
1423
1184
  Supports ISO 8601 duration format. Defaults to DEFAULT_RUN_APP_LIFECYCLE_SECONDS.
1424
1185
 
1425
1186
  Returns:
1426
- Tuple[Union[int, None], str]: Exit code of the process (None if unavailable) and the stdout text.
1187
+ Exit code of the process (None if unavailable) and the stdout text.
1427
1188
  """
1428
1189
  if isinstance(app, str):
1429
1190
  app = App({"command": app, "shell": True})
1430
1191
 
1431
- path = "/appmesh/app/syncrun"
1432
1192
  resp = self._request_http(
1433
1193
  AppMeshClient.Method.POST,
1434
1194
  body=app.json(),
1435
- path=path,
1195
+ path="/appmesh/app/syncrun",
1436
1196
  query={
1437
1197
  "timeout": str(self._parse_duration(max_time_seconds)),
1438
1198
  "lifecycle": str(self._parse_duration(life_cycle_seconds)),
1439
1199
  },
1440
1200
  )
1201
+
1441
1202
  exit_code = None
1442
1203
  if resp.status_code == HTTPStatus.OK:
1443
1204
  if stdout_print:
1444
1205
  print(resp.text, end="")
1445
1206
  if "X-Exit-Code" in resp.headers:
1446
- exit_code = int(resp.headers.get("X-Exit-Code"))
1207
+ exit_code = int(resp.headers["X-Exit-Code"])
1447
1208
  elif stdout_print:
1448
1209
  print(resp.text)
1449
1210
 
1450
1211
  return exit_code, resp.text
1451
1212
 
1452
- def _request_http(self, method: Method, path: str, query: dict = None, header: dict = None, body=None) -> requests.Response:
1453
- """Make an HTTP request.
1454
-
1455
- Args:
1456
- method (Method): AppMeshClient.Method.
1457
- path (str): URI patch str.
1458
- query (dict, optional): HTTP query parameters.
1459
- header (dict, optional): HTTP headers.
1460
- body (_type_, optional): object to send in the body of the :class:`Request`.
1461
-
1462
- Returns:
1463
- requests.Response: HTTP response
1464
- """
1213
+ def _request_http(self, method: Method, path: str, query: Optional[dict] = None, header: Optional[dict] = None, body=None) -> requests.Response:
1214
+ """Make an HTTP request."""
1465
1215
  rest_url = parse.urljoin(self.auth_server_url, path)
1466
1216
 
1467
1217
  # Prepare headers
1468
- header = {} if header is None else header
1218
+ headers = header.copy() if header else {}
1469
1219
 
1470
- # JWT or Cookie token
1471
- if self.cookie_file and self._get_cookie_value(self.session.cookies, self.COOKIE_CSRF_TOKEN):
1472
- header[self.HTTP_HEADER_NAME_CSRF_TOKEN] = self._get_cookie_value(self.session.cookies, self.COOKIE_CSRF_TOKEN)
1473
- elif self._get_access_token():
1474
- header[self.HTTP_HEADER_KEY_AUTH] = f"Bearer {self._get_access_token()}"
1220
+ if self.cookie_file:
1221
+ # Cookie-based token
1222
+ csrf_token = self._get_cookie_value(self.session.cookies, self.COOKIE_CSRF_TOKEN)
1223
+ if csrf_token:
1224
+ headers[self.HTTP_HEADER_NAME_CSRF_TOKEN] = csrf_token
1225
+ else:
1226
+ # Api-based token
1227
+ access_token = self._get_access_token()
1228
+ if access_token:
1229
+ headers[self.HTTP_HEADER_KEY_AUTH] = f"Bearer {access_token}"
1475
1230
 
1476
- if self.forward_to and len(self.forward_to) > 0:
1477
- if ":" in self.forward_to:
1478
- header[self.HTTP_HEADER_KEY_X_TARGET_HOST] = self.forward_to
1479
- else:
1480
- header[self.HTTP_HEADER_KEY_X_TARGET_HOST] = self.forward_to + ":" + str(parse.urlsplit(self.auth_server_url).port)
1481
- header[self.HTTP_HEADER_KEY_USER_AGENT] = self.HTTP_USER_AGENT
1231
+ if self.forward_to:
1232
+ target_host = self.forward_to
1233
+ if ":" not in target_host:
1234
+ port = parse.urlsplit(self.auth_server_url).port
1235
+ target_host = f"{target_host}:{port}"
1236
+ headers[self.HTTP_HEADER_KEY_X_TARGET_HOST] = target_host
1237
+
1238
+ headers[self.HTTP_HEADER_KEY_USER_AGENT] = self.HTTP_USER_AGENT
1482
1239
 
1483
1240
  # Convert body to JSON string if it's a dict or list
1484
1241
  if isinstance(body, (dict, list)):
1485
1242
  body = json.dumps(body)
1486
- header.setdefault("Content-Type", "application/json")
1243
+ headers.setdefault("Content-Type", "application/json")
1487
1244
 
1488
1245
  try:
1489
- if method is AppMeshClient.Method.GET:
1490
- resp = self.session.get(url=rest_url, params=query, headers=header, cert=self.ssl_client_cert, verify=self.ssl_verify, timeout=self.rest_timeout)
1491
- elif method is AppMeshClient.Method.POST:
1492
- resp = self.session.post(
1493
- url=rest_url,
1494
- params=query,
1495
- headers=header,
1496
- data=body,
1497
- cert=self.ssl_client_cert,
1498
- verify=self.ssl_verify,
1499
- timeout=self.rest_timeout,
1500
- )
1501
- elif method is AppMeshClient.Method.POST_STREAM:
1502
- resp = self.session.post(url=rest_url, params=query, headers=header, data=body, cert=self.ssl_client_cert, verify=self.ssl_verify, stream=True, timeout=self.rest_timeout)
1503
- elif method is AppMeshClient.Method.DELETE:
1504
- resp = self.session.delete(url=rest_url, headers=header, cert=self.ssl_client_cert, verify=self.ssl_verify, timeout=self.rest_timeout)
1505
- elif method is AppMeshClient.Method.PUT:
1506
- resp = self.session.put(url=rest_url, params=query, headers=header, data=body, cert=self.ssl_client_cert, verify=self.ssl_verify, timeout=self.rest_timeout)
1246
+ request_kwargs = {
1247
+ "url": rest_url,
1248
+ "headers": headers,
1249
+ "cert": self.ssl_client_cert,
1250
+ "verify": self.ssl_verify,
1251
+ "timeout": self.rest_timeout,
1252
+ }
1253
+
1254
+ if method == AppMeshClient.Method.GET:
1255
+ resp = self.session.get(params=query, **request_kwargs)
1256
+ elif method == AppMeshClient.Method.POST:
1257
+ resp = self.session.post(params=query, data=body, **request_kwargs)
1258
+ elif method == AppMeshClient.Method.POST_STREAM:
1259
+ resp = self.session.post(params=query, data=body, stream=True, **request_kwargs)
1260
+ elif method == AppMeshClient.Method.DELETE:
1261
+ resp = self.session.delete(**request_kwargs)
1262
+ elif method == AppMeshClient.Method.PUT:
1263
+ resp = self.session.put(params=query, data=body, **request_kwargs)
1507
1264
  else:
1508
1265
  raise Exception("Invalid http method", method)
1509
1266
 
1510
1267
  # Wrap the response for encoding handling
1511
1268
  return AppMeshClient.EncodingResponse(resp)
1269
+
1512
1270
  except requests.exceptions.RequestException as e:
1513
- raise Exception(f"HTTP request failed: {str(e)}") from e
1271
+ raise Exception(f"HTTP request failed: {e}") from e