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