appmesh 1.6.14__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,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,6 +114,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
111
114
  >>> response = client.app_view(app_name='ping')
112
115
  """
113
116
 
117
+ # Duration constants
114
118
  DURATION_ONE_WEEK_ISO = "P1W"
115
119
  DURATION_TWO_DAYS_ISO = "P2D"
116
120
  DURATION_TWO_DAYS_HALF_ISO = "P2DT12H"
@@ -118,20 +122,22 @@ class AppMeshClient(metaclass=abc.ABCMeta):
118
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")
125
+ _DEFAULT_SSL_DIR = Path("c:/local/appmesh/ssl" if os.name == "nt" else "/opt/appmesh/ssl")
122
126
  DEFAULT_SSL_CA_CERT_PATH = str(_DEFAULT_SSL_DIR / "ca.pem")
123
127
  DEFAULT_SSL_CLIENT_CERT_PATH = str(_DEFAULT_SSL_DIR / "client.pem")
124
128
  DEFAULT_SSL_CLIENT_KEY_PATH = str(_DEFAULT_SSL_DIR / "client-key.pem")
125
129
 
130
+ # JWT constants
126
131
  DEFAULT_JWT_AUDIENCE = "appmesh-service"
127
132
 
133
+ # HTTP headers and constants
128
134
  JSON_KEY_MESSAGE = "message"
129
135
  HTTP_USER_AGENT = "appmesh/python"
130
136
  HTTP_HEADER_KEY_AUTH = "Authorization"
131
137
  HTTP_HEADER_KEY_USER_AGENT = "User-Agent"
132
138
  HTTP_HEADER_KEY_X_TARGET_HOST = "X-Target-Host"
133
139
  HTTP_HEADER_KEY_X_FILE_PATH = "X-File-Path"
134
- HTTP_HEADER_JWT_set_cookie = "X-Set-Cookie"
140
+ HTTP_HEADER_JWT_SET_COOKIE = "X-Set-Cookie"
135
141
  HTTP_HEADER_NAME_CSRF_TOKEN = "X-CSRF-Token"
136
142
  COOKIE_TOKEN = "appmesh_auth_token"
137
143
  COOKIE_CSRF_TOKEN = "appmesh_csrf_token"
@@ -151,8 +157,6 @@ class AppMeshClient(metaclass=abc.ABCMeta):
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,7 +190,6 @@ 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__(
@@ -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,83 @@ 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
 
246
249
  def _get_access_token(self) -> str:
247
- return self.jwt_token
250
+ """Get the current access token."""
251
+ return self.jwt_token or ""
248
252
 
249
- def _load_cookies(self, cookie_file: Optional[str]) -> str:
250
- """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"""
251
255
  if not cookie_file:
252
- return ""
256
+ return False
253
257
 
258
+ cookie_path = Path(cookie_file)
254
259
  self.session.cookies = cookiejar.MozillaCookieJar(cookie_file)
255
- if os.path.exists(cookie_file):
260
+
261
+ if cookie_path.exists():
256
262
  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
263
  else:
259
- os.makedirs(os.path.dirname(cookie_file), exist_ok=True)
264
+ cookie_path.parent.mkdir(parents=True, exist_ok=True)
260
265
  self.session.cookies.save(ignore_discard=True, ignore_expires=True)
261
266
  if os.name == "posix":
262
- os.chmod(cookie_file, 0o600) # User read/write only
263
- return cookie_file
267
+ cookie_path.chmod(0o600) # User read/write only
268
+
269
+ return True
264
270
 
265
271
  @staticmethod
266
- 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]:
267
273
  """Get cookie value by name, checking expiry if requested."""
268
274
  # If it's a RequestsCookieJar, use .get() but check expiry manually if requested
269
275
  if hasattr(cookies, "get") and not isinstance(cookies, list):
270
276
  cookie = cookies.get(name)
271
277
  if cookie is None:
272
278
  return None
273
- if check_expiry and getattr(cookie, "expires", None):
279
+ if check_expiry and hasattr(cookie, "expires") and cookie.expires:
274
280
  if cookie.expires < time.time():
275
281
  return None # expired
276
282
  return cookie.value if hasattr(cookie, "value") else cookie
277
283
 
278
284
  # 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():
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():
283
289
  return None # expired
284
- return c.value
290
+ return cookie.value
285
291
 
286
292
  return None
287
293
 
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
- """
294
+ def _check_and_refresh_token(self) -> None:
295
+ """Check and refresh token if needed, then schedule next check."""
296
296
  if not self.jwt_token:
297
297
  return
298
298
 
299
- # Check if token needs refresh
300
299
  needs_refresh = True
301
300
  time_to_expiry = float("inf")
302
301
 
303
302
  # Check token expiration directly from JWT
304
- try:
303
+ with suppress(Exception):
305
304
  decoded_token = jwt.decode(self._get_access_token(), options={"verify_signature": False})
306
305
  expiry = decoded_token.get("exp", 0)
307
306
  current_time = time.time()
308
307
  time_to_expiry = expiry - current_time
309
- # Refresh if token expires within 5 minutes
310
308
  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))
313
309
 
314
310
  # Refresh token if needed
315
311
  if needs_refresh:
@@ -317,25 +313,14 @@ class AppMeshClient(metaclass=abc.ABCMeta):
317
313
  self.renew_token()
318
314
  logging.info("Token successfully refreshed")
319
315
  except Exception as e:
320
- logging.error("Token refresh failed: %s", str(e))
316
+ logging.error("Token refresh failed: %s", e)
321
317
 
322
318
  # Schedule next check if auto-refresh is still enabled
323
319
  if self._auto_refresh_token and self.jwt_token:
324
320
  self._schedule_token_refresh(time_to_expiry)
325
321
 
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
- """
322
+ def _schedule_token_refresh(self, time_to_expiry: Optional[float] = None) -> None:
323
+ """Schedule next token refresh check."""
339
324
  # Cancel existing timer if any
340
325
  if self._token_refresh_timer:
341
326
  self._token_refresh_timer.cancel()
@@ -359,23 +344,22 @@ class AppMeshClient(metaclass=abc.ABCMeta):
359
344
  self._token_refresh_timer.start()
360
345
  logging.debug("Auto-refresh: Next token check scheduled in %.1f seconds", check_interval)
361
346
  except Exception as e:
362
- logging.error("Auto-refresh: Failed to schedule token refresh: %s", str(e))
347
+ logging.error("Auto-refresh: Failed to schedule token refresh: %s", e)
363
348
 
364
349
  def close(self):
365
350
  """Close the session and release resources."""
366
351
  # Cancel token refresh timer
367
- if hasattr(self, "_token_refresh_timer") and self._token_refresh_timer:
352
+ if self._token_refresh_timer:
368
353
  self._token_refresh_timer.cancel()
369
354
  self._token_refresh_timer = None
370
355
 
371
356
  # Close the session
372
- if hasattr(self, "session") and self.session:
357
+ if self.session:
373
358
  self.session.close()
374
359
  self.session = None
375
360
 
376
361
  # Clean token
377
- if hasattr(self, "_jwt_token") and self._jwt_token:
378
- self._jwt_token = None
362
+ self._jwt_token = None
379
363
 
380
364
  def __enter__(self):
381
365
  """Support for context manager protocol."""
@@ -385,114 +369,61 @@ class AppMeshClient(metaclass=abc.ABCMeta):
385
369
  """Support for context manager protocol, ensuring resources are released."""
386
370
  self.close()
387
371
 
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
372
  @property
396
373
  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
374
+ """Get the current JWT (JSON Web Token) used for authentication."""
375
+ return self._jwt_token or ""
412
376
 
413
377
  @jwt_token.setter
414
- def jwt_token(self, token: str) -> None:
378
+ def jwt_token(self, token: Optional[str]) -> None:
415
379
  """Set the JWT token for authentication.
416
380
 
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
381
+ Note:
382
+ This setter has no effect when cookie-based authentication is enabled (i.e., when a cookie file is being used).
435
383
  """
436
384
  if self._jwt_token == token:
437
385
  return # No change
438
386
  self._jwt_token = token
439
387
 
440
- # handle refresh
388
+ # Handle refresh
441
389
  if self._jwt_token and self._auto_refresh_token:
442
390
  self._schedule_token_refresh()
443
391
  elif self._token_refresh_timer:
444
392
  self._token_refresh_timer.cancel()
445
393
  self._token_refresh_timer = None
446
394
 
447
- # handle session
395
+ # Handle session persistence
448
396
  with self._lock:
449
- if hasattr(self, "cookie_file") and self.cookie_file:
397
+ if self.cookie_file:
450
398
  self.session.cookies.save(ignore_discard=True, ignore_expires=True)
451
399
 
452
400
  @property
453
401
  def forward_to(self) -> str:
454
- """Get the target host address for request forwarding in a cluster setup.
402
+ """Target host for request forwarding in a cluster.
455
403
 
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
404
+ Supports:
405
+ - "hostname" or "IP" uses current service port
406
+ - "hostname:port" or "IP:port" uses specified port
460
407
 
461
408
  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.
409
+ str: Target host (e.g., "node" or "node:6060"), or empty string if unset.
466
410
 
467
411
  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
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
472
415
  """
473
- return self._forward_to
416
+ return self._forward_to or ""
474
417
 
475
418
  @forward_to.setter
476
419
  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.
420
+ """Set target host for forwarding.
489
421
 
490
422
  Examples:
491
423
  >>> client.forward_to = "backend-node:6060" # Use specific port
492
424
  >>> client.forward_to = "backend-node" # Use current service port
493
425
  >>> client.forward_to = None # Disable forwarding
494
426
  """
495
-
496
427
  self._forward_to = host
497
428
 
498
429
  ########################################
@@ -506,37 +437,48 @@ class AppMeshClient(metaclass=abc.ABCMeta):
506
437
  timeout_seconds: Union[str, int] = DURATION_ONE_WEEK_ISO,
507
438
  audience: Optional[str] = None,
508
439
  ) -> str:
509
- """Login with user name and password
440
+ """Login with user name and password.
510
441
 
511
442
  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').
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').
517
448
 
518
449
  Returns:
519
- str: JWT token.
450
+ JWT token.
520
451
  """
521
452
  # Standard App Mesh authentication
522
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
+
523
467
  resp = self._request_http(
524
468
  AppMeshClient.Method.POST,
525
469
  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
- },
470
+ header=headers,
533
471
  )
472
+
534
473
  if resp.status_code == HTTPStatus.OK:
535
474
  if "access_token" in resp.json():
536
475
  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)
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)
540
482
  else:
541
483
  raise Exception(resp.text)
542
484
 
@@ -546,49 +488,46 @@ class AppMeshClient(metaclass=abc.ABCMeta):
546
488
  """Validate TOTP challenge and obtain a new JWT token.
547
489
 
548
490
  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.
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.
555
498
 
556
499
  Returns:
557
- str: New JWT token if validation succeeds
558
-
559
- Raises:
560
- Exception: If validation fails or server returns error
500
+ New JWT token if validation succeeds.
561
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
+
562
511
  resp = self._request_http(
563
512
  AppMeshClient.Method.POST,
564
513
  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 {},
514
+ body=body,
515
+ header=headers,
572
516
  )
517
+
573
518
  if resp.status_code == HTTPStatus.OK and "access_token" in resp.json():
574
519
  self.jwt_token = resp.json()["access_token"]
575
520
  return self.jwt_token
576
521
  raise Exception(resp.text)
577
522
 
578
523
  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
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
590
527
 
591
- 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
592
531
 
593
532
  def authentication(self, token: str, permission: Optional[str] = None, audience: Optional[str] = None) -> bool:
594
533
  """Deprecated: Use authenticate() instead."""
@@ -598,191 +537,158 @@ class AppMeshClient(metaclass=abc.ABCMeta):
598
537
  """Authenticate with a token and verify permission if specified.
599
538
 
600
539
  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.
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.
607
546
 
608
547
  Returns:
609
- bool: authentication success or failure.
548
+ True if authentication succeeds.
610
549
  """
611
550
  old_token = self.jwt_token
612
551
  self.jwt_token = token
613
- headers = {
614
- **({"X-Audience": audience} if audience else {}),
615
- **({"X-Permission": permission} if permission else {}),
616
- }
552
+
553
+ headers = {}
554
+ if audience:
555
+ headers["X-Audience"] = audience
556
+ if permission:
557
+ headers["X-Permission"] = permission
558
+
617
559
  resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/auth", header=headers)
560
+
618
561
  if resp.status_code != HTTPStatus.OK:
619
562
  self.jwt_token = old_token
620
563
  raise Exception(resp.text)
621
- return resp.status_code == HTTPStatus.OK
564
+
565
+ return True
622
566
 
623
567
  def renew_token(self, timeout: Union[int, str] = DURATION_ONE_WEEK_ISO) -> str:
624
568
  """Renew the current token.
625
569
 
626
570
  Args:
627
- timeout_seconds (int | str, optional): token expire timeout of seconds. support ISO 8601 durations (e.g., 'P1Y2M3DT4H5M6S' 'P1W').
571
+ timeout: Token expire timeout.
628
572
 
629
573
  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
574
+ The new JWT token.
634
575
  """
635
- # Ensure token exists
636
576
  if not self.jwt_token:
637
577
  raise Exception("No token to renew")
638
578
 
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")
579
+ if not isinstance(self.jwt_token, str):
580
+ raise Exception("Unsupported token format")
657
581
 
658
- 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
+ )
659
587
 
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
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)
663
595
 
664
- def get_totp_secret(self) -> str:
665
- """
666
- Generate TOTP secret for the current user and return a secret.
596
+ return self.jwt_token
667
597
 
668
- Returns:
669
- str: TOTP secret string
670
- """
598
+ def get_totp_secret(self) -> str:
599
+ """Generate TOTP secret for the current user."""
671
600
  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")
675
601
 
676
- 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
677
611
 
678
612
  def setup_totp(self, totp_code: str) -> str:
679
613
  """Set up 2FA for the current user.
680
614
 
681
615
  Args:
682
- totp_code (str): TOTP code
616
+ totp_code: TOTP code.
683
617
 
684
618
  Returns:
685
- str: The new JWT token if setup success, the old token will be blocked.
619
+ The new JWT token if setup succeeds.
686
620
  """
687
621
  resp = self._request_http(
688
622
  method=AppMeshClient.Method.POST,
689
623
  path="/appmesh/totp/setup",
690
624
  header={"X-Totp-Code": totp_code},
691
625
  )
626
+
692
627
  if resp.status_code == HTTPStatus.OK:
693
628
  if "access_token" in resp.json():
694
629
  self.jwt_token = resp.json()["access_token"]
695
630
  return self.jwt_token
696
- else:
697
- raise Exception(resp.text)
698
631
 
699
- def disable_totp(self, user: str = "self") -> bool:
700
- """Disable 2FA for the specified user.
701
-
702
- Args:
703
- user (str, optional): user name for disable TOTP.
632
+ raise Exception(resp.text)
704
633
 
705
- Returns:
706
- bool: success or failure.
707
- """
634
+ def disable_totp(self, user: str = "self") -> None:
635
+ """Disable 2FA for the specified user."""
708
636
  resp = self._request_http(
709
637
  method=AppMeshClient.Method.POST,
710
638
  path=f"/appmesh/totp/{user}/disable",
711
639
  )
640
+
712
641
  if resp.status_code != HTTPStatus.OK:
713
642
  raise Exception(resp.text)
714
- return resp.status_code == HTTPStatus.OK
715
643
 
716
644
  @staticmethod
717
645
  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
- """
646
+ """Extract TOTP parameters from URI."""
726
647
  parsed_info = {}
727
648
  parsed_uri = parse.urlparse(totp_uri)
728
649
 
729
650
  # Extract label from the path
730
- parsed_info["label"] = parsed_uri.path[1:] # Remove the leading slash
651
+ parsed_info["label"] = parsed_uri.path[1:] # Remove leading slash
731
652
 
732
653
  # Extract parameters from the query string
733
654
  query_params = parse.parse_qs(parsed_uri.query)
734
655
  for key, value in query_params.items():
735
656
  parsed_info[key] = value[0]
657
+
736
658
  return parsed_info
737
659
 
738
660
  ########################################
739
661
  # Application view
740
662
  ########################################
741
663
  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.
749
-
750
- Exception:
751
- failed request or no such application
752
- """
664
+ """Get information about a specific application."""
753
665
  resp = self._request_http(AppMeshClient.Method.GET, path=f"/appmesh/app/{app_name}")
666
+
754
667
  if resp.status_code != HTTPStatus.OK:
755
668
  raise Exception(resp.text)
756
- return App(resp.json())
757
-
758
- def view_all_apps(self):
759
- """Get information about all applications.
760
669
 
761
- Returns:
762
- list: the application object both contain static configuration and runtime information, only return applications that the user has permissions.
670
+ return App(resp.json())
763
671
 
764
- Exception:
765
- failed request or no such application
766
- """
672
+ def view_all_apps(self) -> list:
673
+ """Get information about all applications."""
767
674
  resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/applications")
675
+
768
676
  if resp.status_code != HTTPStatus.OK:
769
677
  raise Exception(resp.text)
770
- apps = []
771
- for app in resp.json():
772
- apps.append(App(app))
773
- return apps
678
+
679
+ return [App(app) for app in resp.json()]
774
680
 
775
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:
776
682
  """Get the stdout/stderr of an application.
777
683
 
778
684
  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,
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,
782
688
  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.
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.
786
692
 
787
693
  Returns:
788
694
  AppOutput object.
@@ -798,450 +704,312 @@ class AppMeshClient(metaclass=abc.ABCMeta):
798
704
  "timeout": str(timeout),
799
705
  },
800
706
  )
707
+
801
708
  out_position = int(resp.headers["X-Output-Position"]) if "X-Output-Position" in resp.headers else None
802
709
  exit_code = int(resp.headers["X-Exit-Code"]) if "X-Exit-Code" in resp.headers else None
710
+
803
711
  return AppOutput(status_code=resp.status_code, output=resp.text, out_position=out_position, exit_code=exit_code)
804
712
 
805
713
  def check_app_health(self, app_name: str) -> bool:
806
- """Check the health status of an application.
807
-
808
- Args:
809
- app_name (str): the application name.
810
-
811
- Returns:
812
- bool: healthy or not
813
- """
714
+ """Check the health status of an application."""
814
715
  resp = self._request_http(AppMeshClient.Method.GET, path=f"/appmesh/app/{app_name}/health")
716
+
815
717
  if resp.status_code != HTTPStatus.OK:
816
718
  raise Exception(resp.text)
719
+
817
720
  return int(resp.text) == 0
818
721
 
819
722
  ########################################
820
723
  # Application manage
821
724
  ########################################
822
725
  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.
830
-
831
- Exception:
832
- failed request
833
- """
726
+ # type: (App) -> App
727
+ """Register a new application."""
834
728
  resp = self._request_http(AppMeshClient.Method.PUT, path=f"/appmesh/app/{app.name}", body=app.json())
729
+
835
730
  if resp.status_code != HTTPStatus.OK:
836
731
  raise Exception(resp.text)
732
+
837
733
  return App(resp.json())
838
734
 
839
735
  def delete_app(self, app_name: str) -> bool:
840
- """Remove an application.
841
-
842
- Args:
843
- app_name (str): the application name.
844
-
845
- Returns:
846
- bool: True for delete success, Flase for not exist anymore.
847
- """
736
+ """Remove an application."""
848
737
  resp = self._request_http(AppMeshClient.Method.DELETE, path=f"/appmesh/app/{app_name}")
738
+
849
739
  if resp.status_code == HTTPStatus.OK:
850
740
  return True
851
- elif resp.status_code == HTTPStatus.NOT_FOUND:
741
+ if resp.status_code == HTTPStatus.NOT_FOUND:
852
742
  return False
853
- else:
854
- raise Exception(resp.text)
855
-
856
- def enable_app(self, app_name: str) -> bool:
857
- """Enable an application.
858
743
 
859
- Args:
860
- app_name (str): the application name.
744
+ raise Exception(resp.text)
861
745
 
862
- Returns:
863
- bool: success or failure.
864
- """
746
+ def enable_app(self, app_name: str) -> None:
747
+ """Enable an application."""
865
748
  resp = self._request_http(AppMeshClient.Method.POST, path=f"/appmesh/app/{app_name}/enable")
749
+
866
750
  if resp.status_code != HTTPStatus.OK:
867
751
  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
752
 
873
- Args:
874
- app_name (str): the application name.
875
-
876
- Returns:
877
- bool: success or failure.
878
- """
753
+ def disable_app(self, app_name: str) -> None:
754
+ """Disable an application."""
879
755
  resp = self._request_http(AppMeshClient.Method.POST, path=f"/appmesh/app/{app_name}/disable")
756
+
880
757
  if resp.status_code != HTTPStatus.OK:
881
758
  raise Exception(resp.text)
882
- return resp.status_code == HTTPStatus.OK
883
759
 
884
760
  ########################################
885
761
  # Configuration
886
762
  ########################################
887
763
  def view_host_resources(self) -> dict:
888
- """Get a report of host resources including CPU, memory, and disk.
889
-
890
- Returns:
891
- dict: the host resource json.
892
- """
764
+ """Get a report of host resources including CPU, memory, and disk."""
893
765
  resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/resources")
766
+
894
767
  if resp.status_code != HTTPStatus.OK:
895
768
  raise Exception(resp.text)
769
+
896
770
  return resp.json()
897
771
 
898
772
  def view_config(self) -> dict:
899
- """Get the App Mesh configuration in JSON format.
900
-
901
- Returns:
902
- dict: the configuration json.
903
- """
773
+ """Get the App Mesh configuration in JSON format."""
904
774
  resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/config")
775
+
905
776
  if resp.status_code != HTTPStatus.OK:
906
777
  raise Exception(resp.text)
778
+
907
779
  return resp.json()
908
780
 
909
781
  def set_config(self, config_json: dict) -> dict:
910
- """Update the configuration.
911
-
912
- Args:
913
- cfg_json (dict): the new configuration json.
914
-
915
- Returns:
916
- dict: the updated configuration json.
917
- """
782
+ """Update the configuration."""
918
783
  resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/config", body=config_json)
784
+
919
785
  if resp.status_code != HTTPStatus.OK:
920
786
  raise Exception(resp.text)
787
+
921
788
  return resp.json()
922
789
 
923
790
  def set_log_level(self, level: str = "DEBUG") -> str:
924
- """Update the log level.
925
-
926
- Args:
927
- level (str, optional): log level.
928
-
929
- Returns:
930
- str: the updated log level.
931
- """
791
+ """Update the log level."""
932
792
  resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/config", body={"BaseConfig": {"LogLevel": level}})
793
+
933
794
  if resp.status_code != HTTPStatus.OK:
934
795
  raise Exception(resp.text)
796
+
935
797
  return resp.json()["BaseConfig"]["LogLevel"]
936
798
 
937
799
  ########################################
938
800
  # User Management
939
801
  ########################################
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.
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
+ }
947
808
 
948
- Returns:
949
- bool: success
950
- """
951
809
  resp = self._request_http(
952
810
  method=AppMeshClient.Method.POST,
953
811
  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
- },
812
+ body=body,
958
813
  )
814
+
959
815
  if resp.status_code != HTTPStatus.OK:
960
816
  raise Exception(resp.text)
961
- return True
962
-
963
- def add_user(self, user_name: str, user_json: dict) -> bool:
964
- """Add a new user.
965
817
 
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
- """
818
+ def add_user(self, user_name: str, user_json: dict) -> None:
819
+ """Add a new user."""
973
820
  resp = self._request_http(
974
821
  method=AppMeshClient.Method.PUT,
975
822
  path=f"/appmesh/user/{user_name}",
976
823
  body=user_json,
977
824
  )
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.
825
+ if resp.status_code != HTTPStatus.OK:
826
+ raise Exception(resp.text)
985
827
 
986
- Returns:
987
- bool: success or failure.
988
- """
828
+ def delete_user(self, user_name: str):
829
+ """Delete a user."""
989
830
  resp = self._request_http(
990
831
  method=AppMeshClient.Method.DELETE,
991
832
  path=f"/appmesh/user/{user_name}",
992
833
  )
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.
834
+ if resp.status_code != HTTPStatus.OK:
835
+ raise Exception(resp.text)
1000
836
 
1001
- Returns:
1002
- bool: success or failure.
1003
- """
837
+ def lock_user(self, user_name: str) -> None:
838
+ """Lock a user."""
1004
839
  resp = self._request_http(
1005
840
  method=AppMeshClient.Method.POST,
1006
841
  path=f"/appmesh/user/{user_name}/lock",
1007
842
  )
843
+
1008
844
  if resp.status_code != HTTPStatus.OK:
1009
845
  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
846
 
1018
- Returns:
1019
- bool: success or failure.
1020
- """
847
+ def unlock_user(self, user_name: str) -> None:
848
+ """Unlock a user."""
1021
849
  resp = self._request_http(
1022
850
  method=AppMeshClient.Method.POST,
1023
851
  path=f"/appmesh/user/{user_name}/unlock",
1024
852
  )
853
+
1025
854
  if resp.status_code != HTTPStatus.OK:
1026
855
  raise Exception(resp.text)
1027
- return resp.status_code == HTTPStatus.OK
1028
856
 
1029
857
  def view_users(self) -> dict:
1030
- """Get information about all users.
1031
-
1032
- Returns:
1033
- dict: all user definition
1034
- """
858
+ """Get information about all users."""
1035
859
  resp = self._request_http(method=AppMeshClient.Method.GET, path="/appmesh/users")
860
+
1036
861
  if resp.status_code != HTTPStatus.OK:
1037
862
  raise Exception(resp.text)
863
+
1038
864
  return resp.json()
1039
865
 
1040
866
  def view_self(self) -> dict:
1041
- """Get information about the current user.
1042
-
1043
- Returns:
1044
- dict: user definition.
1045
- """
867
+ """Get information about the current user."""
1046
868
  resp = self._request_http(method=AppMeshClient.Method.GET, path="/appmesh/user/self")
869
+
1047
870
  if resp.status_code != HTTPStatus.OK:
1048
871
  raise Exception(resp.text)
872
+
1049
873
  return resp.json()
1050
874
 
1051
875
  def view_groups(self) -> list:
1052
- """Get information about all user groups.
1053
-
1054
- Returns:
1055
- dict: user group array.
1056
- """
876
+ """Get information about all user groups."""
1057
877
  resp = self._request_http(method=AppMeshClient.Method.GET, path="/appmesh/user/groups")
878
+
1058
879
  if resp.status_code != HTTPStatus.OK:
1059
880
  raise Exception(resp.text)
881
+
1060
882
  return resp.json()
1061
883
 
1062
884
  def view_permissions(self) -> list:
1063
- """Get information about all available permissions.
1064
-
1065
- Returns:
1066
- dict: permission array
1067
- """
885
+ """Get information about all available permissions."""
1068
886
  resp = self._request_http(method=AppMeshClient.Method.GET, path="/appmesh/permissions")
887
+
1069
888
  if resp.status_code != HTTPStatus.OK:
1070
889
  raise Exception(resp.text)
890
+
1071
891
  return resp.json()
1072
892
 
1073
893
  def view_user_permissions(self) -> list:
1074
- """Get information about the permissions of the current user.
1075
-
1076
- Returns:
1077
- dict: user permission array.
1078
- """
894
+ """Get information about the permissions of the current user."""
1079
895
  resp = self._request_http(method=AppMeshClient.Method.GET, path="/appmesh/user/permissions")
896
+
1080
897
  if resp.status_code != HTTPStatus.OK:
1081
898
  raise Exception(resp.text)
899
+
1082
900
  return resp.json()
1083
901
 
1084
902
  def view_roles(self) -> list:
1085
- """Get information about all roles with permission definitions.
1086
-
1087
- Returns:
1088
- dict: all role definition.
1089
- """
903
+ """Get information about all roles with permission definitions."""
1090
904
  resp = self._request_http(method=AppMeshClient.Method.GET, path="/appmesh/roles")
905
+
1091
906
  if resp.status_code != HTTPStatus.OK:
1092
907
  raise Exception(resp.text)
1093
- return resp.json()
1094
-
1095
- def update_role(self, role_name: str, role_permission_json: dict) -> bool:
1096
- """Update or add a role with defined permissions.
1097
908
 
1098
- Args:
1099
- role_name (str): the role name.
1100
- role_permission_json (dict): role permission definition array, e.g: ["app-control", "app-delete"]
909
+ return resp.json()
1101
910
 
1102
- Returns:
1103
- bool: success or failure.
1104
- """
911
+ def update_role(self, role_name: str, role_permission_json: dict) -> None:
912
+ """Update or add a role with defined permissions."""
1105
913
  resp = self._request_http(method=AppMeshClient.Method.POST, path=f"/appmesh/role/{role_name}", body=role_permission_json)
914
+
1106
915
  if resp.status_code != HTTPStatus.OK:
1107
916
  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
917
 
1113
- Args:
1114
- role_name (str): the role name.
1115
-
1116
- Returns:
1117
- bool: success or failure.
1118
- """
918
+ def delete_role(self, role_name: str) -> None:
919
+ """Delete a user role."""
1119
920
  resp = self._request_http(
1120
921
  method=AppMeshClient.Method.DELETE,
1121
922
  path=f"/appmesh/role/{role_name}",
1122
923
  )
924
+
1123
925
  if resp.status_code != HTTPStatus.OK:
1124
926
  raise Exception(resp.text)
1125
- return resp.status_code == HTTPStatus.OK
1126
927
 
1127
928
  ########################################
1128
929
  # Tag management
1129
930
  ########################################
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
- """
931
+ def add_tag(self, tag_name: str, tag_value: str) -> None:
932
+ """Add a new label."""
1140
933
  resp = self._request_http(
1141
934
  AppMeshClient.Method.PUT,
1142
935
  query={"value": tag_value},
1143
936
  path=f"/appmesh/label/{tag_name}",
1144
937
  )
938
+
1145
939
  if resp.status_code != HTTPStatus.OK:
1146
940
  raise Exception(resp.text)
1147
- return resp.status_code == HTTPStatus.OK
1148
941
 
1149
- def delete_tag(self, tag_name: str) -> bool:
1150
- """Delete a label.
1151
-
1152
- Args:
1153
- tag_name (str): the label name.
1154
-
1155
- Returns:
1156
- bool: success or failure.
1157
- """
942
+ def delete_tag(self, tag_name: str) -> None:
943
+ """Delete a label."""
1158
944
  resp = self._request_http(AppMeshClient.Method.DELETE, path=f"/appmesh/label/{tag_name}")
945
+
1159
946
  if resp.status_code != HTTPStatus.OK:
1160
947
  raise Exception(resp.text)
1161
- return resp.status_code == HTTPStatus.OK
1162
948
 
1163
949
  def view_tags(self) -> dict:
1164
- """Get information about all labels.
1165
-
1166
- Returns:
1167
- dict: label data.
1168
- """
950
+ """Get information about all labels."""
1169
951
  resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/labels")
952
+
1170
953
  if resp.status_code != HTTPStatus.OK:
1171
954
  raise Exception(resp.text)
955
+
1172
956
  return resp.json()
1173
957
 
1174
958
  ########################################
1175
- # Promethus metrics
959
+ # Prometheus metrics
1176
960
  ########################################
1177
- def get_metrics(self):
1178
- """Get Prometheus metrics.
1179
-
1180
- Returns:
1181
- str: prometheus metrics texts
1182
- """
961
+ def get_metrics(self) -> str:
962
+ """Get Prometheus metrics."""
1183
963
  resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/metrics")
964
+
1184
965
  if resp.status_code != HTTPStatus.OK:
1185
966
  raise Exception(resp.text)
967
+
1186
968
  return resp.text
1187
969
 
1188
970
  ########################################
1189
971
  # File management
1190
972
  ########################################
1191
973
  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
- """
974
+ """Download a remote file to the local system."""
1199
975
  resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/file/download", header={self.HTTP_HEADER_KEY_X_FILE_PATH: remote_file})
1200
976
  resp.raise_for_status()
1201
977
 
1202
978
  # 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
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):
1205
982
  if chunk:
1206
983
  fp.write(chunk)
1207
984
 
1208
985
  # Apply file attributes (permissions, owner, group) if requested
1209
986
  if preserve_permissions and sys.platform != "win32":
1210
987
  if "X-File-Mode" in resp.headers:
1211
- os.chmod(path=local_file, mode=int(resp.headers["X-File-Mode"]))
988
+ local_path.chmod(int(resp.headers["X-File-Mode"]))
989
+
1212
990
  if "X-File-User" in resp.headers and "X-File-Group" in resp.headers:
1213
991
  file_uid = int(resp.headers["X-File-User"])
1214
992
  file_gid = int(resp.headers["X-File-Group"])
1215
- try:
993
+ with suppress(PermissionError):
1216
994
  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
995
 
1220
996
  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):
997
+ """Upload a local file to the remote server."""
998
+ local_path = Path(local_file)
999
+ if not local_path.exists():
1233
1000
  raise FileNotFoundError(f"Local file not found: {local_file}")
1234
1001
 
1235
1002
  from requests_toolbelt import MultipartEncoder
1236
1003
 
1237
- with open(file=local_file, mode="rb") as fp:
1004
+ with local_path.open("rb") as fp:
1238
1005
  encoder = MultipartEncoder(fields={"filename": os.path.basename(remote_file), "file": ("filename", fp, "application/octet-stream")})
1006
+
1239
1007
  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
+ # Include file attributes if requested
1242
1010
  if preserve_permissions:
1243
- file_stat = os.stat(local_file)
1244
- header["X-File-Mode"] = str(file_stat.st_mode & 0o777) # Mask to keep only permission bits
1011
+ file_stat = local_path.stat()
1012
+ header["X-File-Mode"] = str(file_stat.st_mode & 0o777) # Mask to keep only permission bits
1245
1013
  header["X-File-User"] = str(file_stat.st_uid)
1246
1014
  header["X-File-Group"] = str(file_stat.st_gid)
1247
1015
 
@@ -1258,13 +1026,14 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1258
1026
  ########################################
1259
1027
  # Application run
1260
1028
  ########################################
1261
- 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."""
1262
1032
  if isinstance(timeout, int):
1263
1033
  return timeout
1264
- elif isinstance(timeout, str):
1034
+ if isinstance(timeout, str):
1265
1035
  return int(aniso8601.parse_duration(timeout).total_seconds())
1266
- else:
1267
- raise TypeError(f"Invalid timeout type: {str(timeout)}")
1036
+ raise TypeError(f"Invalid timeout type: {timeout}")
1268
1037
 
1269
1038
  def run_task(self, app_name: str, data: str, timeout: int = 300) -> str:
1270
1039
  """Client send an invocation message to a running App Mesh application and wait for result.
@@ -1273,20 +1042,20 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1273
1042
  forward it to the specified running application instance.
1274
1043
 
1275
1044
  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.
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.
1279
1048
 
1280
1049
  Returns:
1281
1050
  str: The HTTP response body returned by the remote application/service.
1282
1051
  """
1283
- path = f"/appmesh/app/{app_name}/task"
1284
1052
  resp = self._request_http(
1285
1053
  AppMeshClient.Method.POST,
1286
- path=path,
1054
+ path=f"/appmesh/app/{app_name}/task",
1287
1055
  body=data,
1288
1056
  query={"timeout": str(timeout)},
1289
1057
  )
1058
+
1290
1059
  if resp.status_code != HTTPStatus.OK:
1291
1060
  raise Exception(resp.text)
1292
1061
 
@@ -1296,15 +1065,14 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1296
1065
  """Client cancle a running task to a App Mesh application.
1297
1066
 
1298
1067
  Args:
1299
- 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).
1300
1069
 
1301
1070
  Returns:
1302
1071
  bool: Task exist and cancled status.
1303
1072
  """
1304
- path = f"/appmesh/app/{app_name}/task"
1305
1073
  resp = self._request_http(
1306
1074
  AppMeshClient.Method.DELETE,
1307
- path=path,
1075
+ path=f"/appmesh/app/{app_name}/task",
1308
1076
  )
1309
1077
  return resp.status_code == HTTPStatus.OK
1310
1078
 
@@ -1317,15 +1085,15 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1317
1085
  """Run an application asynchronously on a remote system without blocking the API.
1318
1086
 
1319
1087
  Args:
1320
- app (Union[App, str]): An `App` instance or a shell command string.
1088
+ app: An `App` instance or a shell command string.
1321
1089
  - If `app` is a string, it is treated as a shell command for the remote run,
1322
1090
  and an `App` instance is created as:
1323
1091
  `App({"command": "<command_string>", "shell": True})`.
1324
1092
  - If `app` is an `App` object, providing only the `name` attribute (without
1325
1093
  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.
1094
+ max_time_seconds: Maximum runtime for the remote process.
1327
1095
  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.
1096
+ life_cycle_seconds: Maximum lifecycle time for the remote process.
1329
1097
  Accepts ISO 8601 duration format. Defaults to `P2DT12H`.
1330
1098
 
1331
1099
  Returns:
@@ -1334,56 +1102,63 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1334
1102
  if isinstance(app, str):
1335
1103
  app = App({"command": app, "shell": True})
1336
1104
 
1337
- path = "/appmesh/app/run"
1338
1105
  resp = self._request_http(
1339
1106
  AppMeshClient.Method.POST,
1340
1107
  body=app.json(),
1341
- path=path,
1108
+ path="/appmesh/app/run",
1342
1109
  query={
1343
1110
  "timeout": str(self._parse_duration(max_time_seconds)),
1344
1111
  "lifecycle": str(self._parse_duration(life_cycle_seconds)),
1345
1112
  },
1346
1113
  )
1114
+
1347
1115
  if resp.status_code != HTTPStatus.OK:
1348
1116
  raise Exception(resp.text)
1349
1117
 
1350
- # Return an AppRun object with the application name and process UUID
1351
- 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"])
1352
1120
 
1353
- 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]:
1354
1122
  """Wait for an asynchronous run to finish.
1355
1123
 
1356
1124
  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
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
1360
1128
 
1361
1129
  Returns:
1362
- 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.
1363
1131
  """
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
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
+
1387
1162
  return None
1388
1163
 
1389
1164
  def run_app_sync(
@@ -1399,101 +1174,98 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1399
1174
  If 'app' is App object, the name attribute is used to run an existing application if specified.
1400
1175
 
1401
1176
  Args:
1402
- app (Union[App, str]): An App instance or a shell command string.
1177
+ app: An App instance or a shell command string.
1403
1178
  If a string, an App instance is created as:
1404
1179
  `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.
1180
+ stdout_print: If True, prints the remote stdout locally. Defaults to True.
1181
+ max_time_seconds: Maximum runtime for the remote process.
1407
1182
  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.
1183
+ life_cycle_seconds: Maximum lifecycle time for the remote process.
1409
1184
  Supports ISO 8601 duration format. Defaults to DEFAULT_RUN_APP_LIFECYCLE_SECONDS.
1410
1185
 
1411
1186
  Returns:
1412
- 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.
1413
1188
  """
1414
1189
  if isinstance(app, str):
1415
1190
  app = App({"command": app, "shell": True})
1416
1191
 
1417
- path = "/appmesh/app/syncrun"
1418
1192
  resp = self._request_http(
1419
1193
  AppMeshClient.Method.POST,
1420
1194
  body=app.json(),
1421
- path=path,
1195
+ path="/appmesh/app/syncrun",
1422
1196
  query={
1423
1197
  "timeout": str(self._parse_duration(max_time_seconds)),
1424
1198
  "lifecycle": str(self._parse_duration(life_cycle_seconds)),
1425
1199
  },
1426
1200
  )
1201
+
1427
1202
  exit_code = None
1428
1203
  if resp.status_code == HTTPStatus.OK:
1429
1204
  if stdout_print:
1430
1205
  print(resp.text, end="")
1431
1206
  if "X-Exit-Code" in resp.headers:
1432
- exit_code = int(resp.headers.get("X-Exit-Code"))
1207
+ exit_code = int(resp.headers["X-Exit-Code"])
1433
1208
  elif stdout_print:
1434
1209
  print(resp.text)
1435
1210
 
1436
1211
  return exit_code, resp.text
1437
1212
 
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
- """
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."""
1451
1215
  rest_url = parse.urljoin(self.auth_server_url, path)
1452
1216
 
1453
1217
  # Prepare headers
1454
- header = {} if header is None else header
1218
+ headers = header.copy() if header else {}
1455
1219
 
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()}"
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}"
1461
1230
 
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
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
1468
1239
 
1469
1240
  # Convert body to JSON string if it's a dict or list
1470
1241
  if isinstance(body, (dict, list)):
1471
1242
  body = json.dumps(body)
1472
- header.setdefault("Content-Type", "application/json")
1243
+ headers.setdefault("Content-Type", "application/json")
1473
1244
 
1474
1245
  try:
1475
1246
  request_kwargs = {
1476
1247
  "url": rest_url,
1477
- "headers": header,
1248
+ "headers": headers,
1478
1249
  "cert": self.ssl_client_cert,
1479
1250
  "verify": self.ssl_verify,
1480
1251
  "timeout": self.rest_timeout,
1481
1252
  }
1482
1253
 
1483
- if method is AppMeshClient.Method.GET:
1254
+ if method == AppMeshClient.Method.GET:
1484
1255
  resp = self.session.get(params=query, **request_kwargs)
1485
- elif method is AppMeshClient.Method.POST:
1256
+ elif method == AppMeshClient.Method.POST:
1486
1257
  resp = self.session.post(params=query, data=body, **request_kwargs)
1487
- elif method is AppMeshClient.Method.POST_STREAM:
1258
+ elif method == AppMeshClient.Method.POST_STREAM:
1488
1259
  resp = self.session.post(params=query, data=body, stream=True, **request_kwargs)
1489
- elif method is AppMeshClient.Method.DELETE:
1260
+ elif method == AppMeshClient.Method.DELETE:
1490
1261
  resp = self.session.delete(**request_kwargs)
1491
- elif method is AppMeshClient.Method.PUT:
1262
+ elif method == AppMeshClient.Method.PUT:
1492
1263
  resp = self.session.put(params=query, data=body, **request_kwargs)
1493
1264
  else:
1494
1265
  raise Exception("Invalid http method", method)
1495
1266
 
1496
1267
  # Wrap the response for encoding handling
1497
1268
  return AppMeshClient.EncodingResponse(resp)
1269
+
1498
1270
  except requests.exceptions.RequestException as e:
1499
- raise Exception(f"HTTP request failed: {str(e)}") from e
1271
+ raise Exception(f"HTTP request failed: {e}") from e