appmesh 1.6.1__py3-none-any.whl → 1.6.3__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/__init__.py CHANGED
@@ -9,10 +9,40 @@ Example:
9
9
  client = AppMeshClient()
10
10
  """
11
11
 
12
- from .app import App
13
- from .client_http import AppMeshClient
14
- from .client_tcp import AppMeshClientTCP
15
- from .server_http import AppMeshServer
16
- from .server_tcp import AppMeshServerTCP
12
+ import sys
13
+ from types import ModuleType
17
14
 
18
- __all__ = ["App", "AppMeshClient", "AppMeshClientTCP", "AppMeshServer", "AppMeshServerTCP"]
15
+ __all__ = ["App", "AppMeshClient", "AppMeshClientTCP", "AppMeshClientOAuth", "AppMeshServer", "AppMeshServerTCP"]
16
+
17
+ _LAZY_IMPORTS = {
18
+ "App": ("app", "App"), # from .app import App
19
+ "AppMeshClient": ("client_http", "AppMeshClient"), # from .client_http import AppMeshClient
20
+ "AppMeshClientTCP": ("client_tcp", "AppMeshClientTCP"), # from .client_tcp import AppMeshClientTCP
21
+ "AppMeshClientOAuth": ("client_http_oauth", "AppMeshClientOAuth"), # from .client_http_oauth import AppMeshClientOAuth
22
+ "AppMeshServer": ("server_http", "AppMeshServer"), # from .server_http import AppMeshServer
23
+ "AppMeshServerTCP": ("server_tcp", "AppMeshServerTCP"), # from .server_tcp import AppMeshServerTCP
24
+ }
25
+
26
+
27
+ def _lazy_import(name):
28
+ """Helper function for lazy importing."""
29
+ if name in _LAZY_IMPORTS:
30
+ module_name, attr_name = _LAZY_IMPORTS[name]
31
+ module = __import__(f"{__name__}.{module_name}", fromlist=[attr_name])
32
+ return getattr(module, attr_name)
33
+ raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
34
+
35
+
36
+ if sys.version_info >= (3, 7):
37
+
38
+ def __getattr__(name):
39
+ return _lazy_import(name)
40
+
41
+ else:
42
+ # Python 3.6 compatibility
43
+ class _LazyModule(ModuleType):
44
+ def __getattr__(self, name):
45
+ return _lazy_import(name)
46
+
47
+ sys.modules[__name__] = _LazyModule(__name__)
48
+ sys.modules[__name__].__dict__.update(globals())
appmesh/app.py CHANGED
@@ -5,7 +5,7 @@ import json
5
5
  import copy
6
6
 
7
7
  from datetime import datetime
8
- from typing import Optional
8
+ from typing import Optional, Any
9
9
  from enum import Enum, unique
10
10
 
11
11
  # pylint: disable=line-too-long
@@ -39,7 +39,7 @@ class App:
39
39
  return None
40
40
 
41
41
  @staticmethod
42
- def _get_native_item(data: dict, key: str) -> Optional[object]:
42
+ def _get_native_item(data: dict, key: str) -> Optional[Any]:
43
43
  """Retrieve a deep copy of a value from a dictionary by key, if it exists."""
44
44
  return copy.deepcopy(data[key]) if (data and key in data and data[key] is not None) else None
45
45
 
appmesh/app_run.py CHANGED
@@ -2,6 +2,7 @@
2
2
  """Application run object for remote application execution."""
3
3
 
4
4
  from contextlib import contextmanager
5
+ from typing import Optional
5
6
 
6
7
  # pylint: disable=line-too-long
7
8
 
@@ -42,7 +43,7 @@ class AppRun:
42
43
  finally:
43
44
  self._client.forward_to = original_value
44
45
 
45
- def wait(self, stdout_print: bool = True, timeout: int = 0) -> int:
46
+ def wait(self, stdout_print: bool = True, timeout: int = 0) -> Optional[int]:
46
47
  """Wait for the asynchronous run to complete.
47
48
 
48
49
  Args:
appmesh/appmesh_client.py CHANGED
@@ -5,4 +5,6 @@
5
5
  # AppMeshClient, App, and AppOutput classes. The updated implementation can be found
6
6
  # in client_http.py, where these classes are now primarily maintained.
7
7
 
8
- from .client_http import AppMeshClient, App, AppOutput
8
+ from .client_http import AppMeshClient
9
+ from .app import App
10
+ from .app_output import AppOutput
appmesh/client_http.py CHANGED
@@ -3,8 +3,10 @@
3
3
  import abc
4
4
  import base64
5
5
  import json
6
+ import locale
6
7
  import logging
7
8
  import os
9
+ import sys
8
10
  import time
9
11
  from datetime import datetime
10
12
  from enum import Enum, unique
@@ -20,6 +22,64 @@ from .app_run import AppRun
20
22
  from .app_output import AppOutput
21
23
 
22
24
 
25
+ class EncodingResponse:
26
+ """Wrapper for requests.Response that handles encoding conversion on Windows."""
27
+
28
+ def __init__(self, response: requests.Response):
29
+ self._response = response
30
+ self._converted_text = None
31
+ self._should_convert = False
32
+
33
+ # Check if we need to convert encoding on Windows
34
+ if sys.platform == "win32":
35
+ content_type = response.headers.get("Content-Type", "").lower()
36
+ if response.status_code == HTTPStatus.OK and "text/plain" in content_type and "utf-8" in content_type:
37
+ try:
38
+ local_encoding = locale.getpreferredencoding()
39
+
40
+ if local_encoding.lower() not in ["utf-8", "utf8"]:
41
+ # Ensure response is decoded as UTF-8 first
42
+ response.encoding = "utf-8"
43
+ utf8_text = response.text # This gives us proper Unicode string
44
+
45
+ # Convert Unicode to local encoding, then back to Unicode
46
+ # This simulates how text would appear in local encoding
47
+ try:
48
+ local_bytes = utf8_text.encode(local_encoding, errors="replace")
49
+ self._converted_text = local_bytes.decode(local_encoding)
50
+ self._should_convert = True
51
+ except (UnicodeEncodeError, LookupError):
52
+ # If local encoding can't handle the characters, fall back to UTF-8
53
+ self._converted_text = utf8_text
54
+ self._should_convert = True
55
+
56
+ except (UnicodeError, LookupError):
57
+ # If any conversion fails, keep original UTF-8
58
+ response.encoding = "utf-8"
59
+
60
+ @property
61
+ def text(self):
62
+ """Return converted text if needed, otherwise original text."""
63
+ if self._should_convert and self._converted_text is not None:
64
+ return self._converted_text
65
+ # return the original text from _response without modification
66
+ return self._response.text
67
+
68
+ def __getattr__(self, name):
69
+ """Dynamically delegate attribute access to the original response"""
70
+ return getattr(self._response, name)
71
+
72
+ def __dir__(self):
73
+ """Optional: allow dir() to show attributes of both wrapper and response"""
74
+ return list(set(dir(self._response) + list(self.__dict__.keys())))
75
+
76
+ @property
77
+ def __class__(self):
78
+ """Optional: allow isinstance checks for requests.Response"""
79
+ # Pretend to be a requests.Response for isinstance checks
80
+ return requests.Response
81
+
82
+
23
83
  class AppMeshClient(metaclass=abc.ABCMeta):
24
84
  """
25
85
  Client SDK for interacting with the App Mesh service via REST API.
@@ -107,7 +167,8 @@ class AppMeshClient(metaclass=abc.ABCMeta):
107
167
  DURATION_ONE_WEEK_ISO = "P1W"
108
168
  DURATION_TWO_DAYS_ISO = "P2D"
109
169
  DURATION_TWO_DAYS_HALF_ISO = "P2DT12H"
110
- TOKEN_REFRESH_INTERVAL = 60
170
+ TOKEN_REFRESH_INTERVAL = 300 # 5 min to refresh token
171
+ TOKEN_REFRESH_OFFSET = 30 # 30s before token expire to refresh token
111
172
 
112
173
  # Platform-aware default SSL paths
113
174
  _DEFAULT_SSL_DIR = "c:/local/appmesh/ssl" if os.name == "nt" else "/opt/appmesh/ssl"
@@ -140,7 +201,6 @@ class AppMeshClient(metaclass=abc.ABCMeta):
140
201
  rest_ssl_client_cert=(DEFAULT_SSL_CLIENT_CERT_PATH, DEFAULT_SSL_CLIENT_KEY_PATH) if os.path.exists(DEFAULT_SSL_CLIENT_CERT_PATH) else None,
141
202
  rest_timeout=(60, 300),
142
203
  jwt_token=None,
143
- oauth2=None,
144
204
  auto_refresh_token=False,
145
205
  ):
146
206
  """Initialize an App Mesh HTTP client for interacting with the App Mesh server via secure HTTPS.
@@ -163,15 +223,6 @@ class AppMeshClient(metaclass=abc.ABCMeta):
163
223
  The default is `(60, 300)`, where `60` seconds is the maximum time to establish a connection and `300` seconds for the maximum read duration.
164
224
 
165
225
  jwt_token (str, optional): JWT token for API authentication, used in headers to authorize requests where required.
166
-
167
- oauth2 (Dict[str, Any], optional): Keycloak configuration for oauth2 authentication:
168
- - server_url: Keycloak server URL (e.g. "https://keycloak.example.com/auth/")
169
- - realm: Keycloak realm
170
- - client_id: Keycloak client ID
171
- - client_secret: Keycloak client secret (optional)
172
- Using this parameter enables Keycloak integration for authentication. The 'python-keycloak' package
173
- will be imported on-demand only when this parameter is, make sure package is installed (pip3 install python-keycloak).
174
-
175
226
  auto_refresh_token (bool, optional): Enable automatic token refresh before expiration.
176
227
  When enabled, a background timer will monitor token expiration and attempt to refresh
177
228
  the token before it expires. This works with both native App Mesh tokens and Keycloak tokens.
@@ -185,29 +236,10 @@ class AppMeshClient(metaclass=abc.ABCMeta):
185
236
  self.rest_timeout = rest_timeout
186
237
  self._forward_to = None
187
238
 
188
- # Keycloak integration
189
- self._keycloak_openid = None
190
- if oauth2:
191
- try:
192
- from keycloak import KeycloakOpenID
193
-
194
- self._keycloak_openid = KeycloakOpenID(
195
- server_url=oauth2.get("auth_server_url"),
196
- client_id=oauth2.get("client_id"),
197
- realm_name=oauth2.get("realm"),
198
- client_secret_key=oauth2.get("client_secret"),
199
- verify=self.ssl_verify,
200
- timeout=rest_timeout,
201
- )
202
- except ImportError:
203
- logging.error("Keycloak package not installed. Install with: pip install python-keycloak")
204
- raise Exception("Keycloak integration requested but python-keycloak package is not installed")
205
-
206
239
  # Token auto-refresh
207
240
  self._token_refresh_timer = None
208
241
  self._auto_refresh_token = auto_refresh_token
209
- if auto_refresh_token and jwt_token:
210
- self._schedule_token_refresh()
242
+ self.jwt_token = jwt_token # Set property last after all dependencies are initialized
211
243
 
212
244
  @staticmethod
213
245
  def _ensure_logging_configured():
@@ -215,6 +247,9 @@ class AppMeshClient(metaclass=abc.ABCMeta):
215
247
  if not logging.root.handlers:
216
248
  logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
217
249
 
250
+ def _get_access_token(self) -> str:
251
+ return self.jwt_token
252
+
218
253
  def _check_and_refresh_token(self):
219
254
  """Check and refresh token if needed, then schedule next check.
220
255
 
@@ -232,12 +267,12 @@ class AppMeshClient(metaclass=abc.ABCMeta):
232
267
 
233
268
  # Check token expiration directly from JWT
234
269
  try:
235
- decoded_token = jwt.decode(self.jwt_token if isinstance(self.jwt_token, str) else self.jwt_token.get("access_token", ""), options={"verify_signature": False})
270
+ decoded_token = jwt.decode(self._get_access_token(), options={"verify_signature": False})
236
271
  expiry = decoded_token.get("exp", 0)
237
272
  current_time = time.time()
238
273
  time_to_expiry = expiry - current_time
239
274
  # Refresh if token expires within 5 minutes
240
- needs_refresh = time_to_expiry < 300
275
+ needs_refresh = time_to_expiry < self.TOKEN_REFRESH_OFFSET
241
276
  except Exception as e:
242
277
  logging.debug("Failed to parse JWT token for expiration check: %s", str(e))
243
278
 
@@ -277,11 +312,11 @@ class AppMeshClient(metaclass=abc.ABCMeta):
277
312
 
278
313
  # Calculate more precise check time if expiry is known
279
314
  if time_to_expiry is not None:
280
- if time_to_expiry <= 300: # Expires within 5 minutes
315
+ if time_to_expiry <= self.TOKEN_REFRESH_OFFSET: # Expires within 5 minutes
281
316
  check_interval = 1 # Almost immediate refresh
282
317
  else:
283
318
  # Check at earlier of 5 minutes before expiry or regular interval
284
- check_interval = min(time_to_expiry - 300, self.TOKEN_REFRESH_INTERVAL)
319
+ check_interval = min(time_to_expiry - self.TOKEN_REFRESH_OFFSET, self.TOKEN_REFRESH_INTERVAL)
285
320
 
286
321
  # Create timer to execute refresh check
287
322
  self._token_refresh_timer = threading.Timer(check_interval, self._check_and_refresh_token)
@@ -303,14 +338,9 @@ class AppMeshClient(metaclass=abc.ABCMeta):
303
338
  self.session.close()
304
339
  self.session = None
305
340
 
306
- # Logout from Keycloak if needed
307
- if hasattr(self, "_keycloak_openid") and self._keycloak_openid and hasattr(self, "_jwt_token") and self._jwt_token and isinstance(self._jwt_token, dict) and "refresh_token" in self._jwt_token:
308
- try:
309
- self._keycloak_openid.logout(self._jwt_token.get("refresh_token"))
310
- except Exception as e:
311
- logging.warning("Failed to logout from Keycloak: %s", str(e))
312
- finally:
313
- self._keycloak_openid = None
341
+ # Clean token
342
+ if hasattr(self, "_jwt_token") and self._jwt_token:
343
+ self._jwt_token = None
314
344
 
315
345
  def __enter__(self):
316
346
  """Support for context manager protocol."""
@@ -370,6 +400,13 @@ class AppMeshClient(metaclass=abc.ABCMeta):
370
400
  """
371
401
  self._jwt_token = token
372
402
 
403
+ # handle refresh
404
+ if self._jwt_token and self._auto_refresh_token:
405
+ self._schedule_token_refresh()
406
+ elif self._token_refresh_timer:
407
+ self._token_refresh_timer.cancel()
408
+ self._token_refresh_timer = None
409
+
373
410
  @property
374
411
  def forward_to(self) -> str:
375
412
  """Get the target host address for request forwarding in a cluster setup.
@@ -419,7 +456,14 @@ class AppMeshClient(metaclass=abc.ABCMeta):
419
456
  ########################################
420
457
  # Security
421
458
  ########################################
422
- def login(self, user_name: str, user_pwd: str, totp_code: Optional[str] = "", timeout_seconds: Union[str, int] = DURATION_ONE_WEEK_ISO, audience: Optional[str] = None) -> str:
459
+ def login(
460
+ self,
461
+ user_name: str,
462
+ user_pwd: str,
463
+ totp_code: Optional[str] = "",
464
+ timeout_seconds: Union[str, int] = DURATION_ONE_WEEK_ISO,
465
+ audience: Optional[str] = None,
466
+ ) -> str:
423
467
  """Login with user name and password
424
468
 
425
469
  Args:
@@ -432,20 +476,6 @@ class AppMeshClient(metaclass=abc.ABCMeta):
432
476
  Returns:
433
477
  str: JWT token.
434
478
  """
435
- # Keycloak authentication if configured
436
- if self._keycloak_openid:
437
- self.jwt_token = self._keycloak_openid.token(
438
- username=user_name,
439
- password=user_pwd,
440
- totp=totp_code if totp_code else None,
441
- grant_type="password", # grant type for token request: "password" / "client_credentials" / "refresh_token"
442
- scope="openid", # what information to include in the token, such as "openid profile email"
443
- )
444
-
445
- if self._auto_refresh_token:
446
- self._schedule_token_refresh()
447
- return self.jwt_token
448
-
449
479
  # Standard App Mesh authentication
450
480
  self.jwt_token = None
451
481
  resp = self._request_http(
@@ -467,8 +497,6 @@ class AppMeshClient(metaclass=abc.ABCMeta):
467
497
  else:
468
498
  raise Exception(resp.text)
469
499
 
470
- if self._auto_refresh_token:
471
- self._schedule_token_refresh()
472
500
  return self.jwt_token
473
501
 
474
502
  def validate_totp(self, username: str, challenge: str, code: str, timeout: Union[int, str] = DURATION_ONE_WEEK_ISO) -> str:
@@ -510,24 +538,12 @@ class AppMeshClient(metaclass=abc.ABCMeta):
510
538
  bool: logoff success or failure.
511
539
  """
512
540
  result = False
513
- # Handle Keycloak logout if configured
514
- if self._keycloak_openid and self.jwt_token and isinstance(self.jwt_token, dict) and "refresh_token" in self.jwt_token:
515
- refresh_token = self.jwt_token.get("refresh_token")
516
- self._keycloak_openid.logout(refresh_token)
517
- self.jwt_token = None
518
- result = True
519
-
520
541
  # Standard App Mesh logout
521
542
  if self.jwt_token and isinstance(self.jwt_token, str):
522
543
  resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/self/logoff")
523
544
  self.jwt_token = None
524
545
  result = resp.status_code == HTTPStatus.OK
525
546
 
526
- # Cancel token refresh timer
527
- if self._token_refresh_timer:
528
- self._token_refresh_timer.cancel()
529
- self._token_refresh_timer = None
530
-
531
547
  return result
532
548
 
533
549
  def authentication(self, token: str, permission: Optional[str] = None, audience: Optional[str] = None) -> bool:
@@ -577,13 +593,8 @@ class AppMeshClient(metaclass=abc.ABCMeta):
577
593
  raise Exception("No token to renew")
578
594
 
579
595
  try:
580
- # Handle Keycloak token (dictionary format)
581
- if self._keycloak_openid and isinstance(self.jwt_token, dict) and "refresh_token" in self.jwt_token:
582
- new_token = self._keycloak_openid.refresh_token(self.jwt_token.get("refresh_token"))
583
- self.jwt_token = new_token
584
-
585
596
  # Handle App Mesh token (string format)
586
- elif isinstance(self.jwt_token, str):
597
+ if isinstance(self.jwt_token, str):
587
598
  resp = self._request_http(
588
599
  AppMeshClient.Method.POST,
589
600
  path="/appmesh/token/renew",
@@ -986,9 +997,6 @@ class AppMeshClient(metaclass=abc.ABCMeta):
986
997
  Returns:
987
998
  dict: user definition.
988
999
  """
989
- if self._keycloak_openid and isinstance(self.jwt_token, dict) and "access_token" in self.jwt_token:
990
- return self._keycloak_openid.userinfo(self.jwt_token.get("access_token"))
991
-
992
1000
  resp = self._request_http(method=AppMeshClient.Method.GET, path="/appmesh/user/self")
993
1001
  if resp.status_code != HTTPStatus.OK:
994
1002
  raise Exception(resp.text)
@@ -1238,6 +1246,22 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1238
1246
 
1239
1247
  return resp.text
1240
1248
 
1249
+ def cancle_task(self, app_name: str) -> bool:
1250
+ """Client cancle a running task to a App Mesh application.
1251
+
1252
+ Args:
1253
+ app_name (str): Name of the target application (as registered in App Mesh).
1254
+
1255
+ Returns:
1256
+ bool: Task exist and cancled status.
1257
+ """
1258
+ path = f"/appmesh/app/{app_name}/task"
1259
+ resp = self._request_http(
1260
+ AppMeshClient.Method.DELETE,
1261
+ path=path,
1262
+ )
1263
+ return resp.status_code == HTTPStatus.OK
1264
+
1241
1265
  def run_app_async(
1242
1266
  self,
1243
1267
  app: Union[App, str],
@@ -1383,7 +1407,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1383
1407
  # Prepare headers
1384
1408
  header = {} if header is None else header
1385
1409
  if self.jwt_token:
1386
- token = self.jwt_token["access_token"] if isinstance(self.jwt_token, dict) and "access_token" in self.jwt_token else self.jwt_token
1410
+ token = self._get_access_token()
1387
1411
  header["Authorization"] = "Bearer " + token
1388
1412
  if self.forward_to and len(self.forward_to) > 0:
1389
1413
  if ":" in self.forward_to:
@@ -1419,13 +1443,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1419
1443
  else:
1420
1444
  raise Exception("Invalid http method", method)
1421
1445
 
1422
- # Ensure response text decoding uses UTF-8 by default
1423
- try:
1424
- resp.encoding = "utf-8"
1425
- except Exception:
1426
- # If setting encoding fails for any reason, ignore and return the response as-is
1427
- pass
1428
-
1429
- return resp
1446
+ # Wrap the response for encoding handling
1447
+ return EncodingResponse(resp)
1430
1448
  except requests.exceptions.RequestException as e:
1431
1449
  raise Exception(f"HTTP request failed: {str(e)}")
@@ -0,0 +1,138 @@
1
+ # client_http_oauth.py
2
+ # pylint: disable=line-too-long,broad-exception-caught,too-many-lines, import-outside-toplevel, protected-access
3
+ import os
4
+ import logging
5
+ from typing import Optional, Union
6
+ from keycloak import KeycloakOpenID
7
+ from .client_http import AppMeshClient
8
+
9
+
10
+ class AppMeshClientOAuth(AppMeshClient):
11
+ """
12
+ AppMeshClient extended with Keycloak authentication support.
13
+
14
+ Managing tokens using Keycloak as the identity provider.
15
+ """
16
+
17
+ def __init__(
18
+ self,
19
+ oauth2: dict, # Required for Keycloak
20
+ rest_url: str = "https://127.0.0.1:6060",
21
+ rest_ssl_verify=AppMeshClient.DEFAULT_SSL_CA_CERT_PATH if os.path.exists(AppMeshClient.DEFAULT_SSL_CA_CERT_PATH) else False,
22
+ rest_ssl_client_cert=(AppMeshClient.DEFAULT_SSL_CLIENT_CERT_PATH, AppMeshClient.DEFAULT_SSL_CLIENT_KEY_PATH) if os.path.exists(AppMeshClient.DEFAULT_SSL_CLIENT_CERT_PATH) else None,
23
+ rest_timeout=(60, 300),
24
+ jwt_token=None, # Keycloak dict
25
+ auto_refresh_token: bool = True, # Default to True for Keycloak
26
+ ):
27
+ """Initialize an App Mesh HTTP client with Keycloak support.
28
+ Args:
29
+ oauth2 (Dict[str, str]): Keycloak configuration for oauth2 authentication:
30
+ - auth_server_url: Keycloak server URL (e.g. "https://keycloak.example.com/auth/")
31
+ - realm: Keycloak realm
32
+ - client_id: Keycloak client ID
33
+ - client_secret: Keycloak client secret (optional)
34
+ Using this parameter enables Keycloak integration for authentication. The 'python-keycloak' package
35
+ will be imported on-demand only when this parameter is, make sure package is installed (pip3 install python-keycloak).
36
+ """
37
+ # Initialize base class, disabling its Keycloak and auto-refresh logic
38
+ super().__init__(
39
+ rest_url=rest_url,
40
+ rest_ssl_verify=rest_ssl_verify,
41
+ rest_ssl_client_cert=rest_ssl_client_cert,
42
+ rest_timeout=rest_timeout,
43
+ jwt_token=jwt_token,
44
+ auto_refresh_token=auto_refresh_token,
45
+ )
46
+
47
+ # Keycloak integration
48
+ self._keycloak_openid = KeycloakOpenID(
49
+ server_url=oauth2.get("auth_server_url"),
50
+ client_id=oauth2.get("client_id"),
51
+ realm_name=oauth2.get("realm"),
52
+ client_secret_key=oauth2.get("client_secret"),
53
+ verify=self.ssl_verify,
54
+ timeout=rest_timeout,
55
+ )
56
+
57
+ # @override, from typing import override avialable from python3.12
58
+ def _get_access_token(self) -> str:
59
+ return self.jwt_token.get("access_token", "") if self.jwt_token else None
60
+
61
+ def login(
62
+ self,
63
+ user_name: str,
64
+ user_pwd: str,
65
+ totp_code: Optional[str] = "",
66
+ timeout_seconds: Union[str, int] = AppMeshClient.DURATION_ONE_WEEK_ISO,
67
+ ) -> dict:
68
+ """Login with user name and password using Keycloak.
69
+ Args:
70
+ user_name (str): the name of the user.
71
+ user_pwd (str): the password of the user.
72
+ totp_code (str, optional): the TOTP code if enabled for the user.
73
+ timeout_seconds (int | str, optional): token expire timeout of seconds. support ISO 8601 durations (e.g., 'P1Y2M3DT4H5M6S' 'P1W').
74
+
75
+ Returns:
76
+ dict: Keycloak token.
77
+ """
78
+ # Keycloak authentication
79
+ self.jwt_token = self._keycloak_openid.token(
80
+ username=user_name,
81
+ password=user_pwd,
82
+ totp=totp_code if totp_code else None,
83
+ grant_type="password", # grant type for token request: "password" / "client_credentials" / "refresh_token"
84
+ scope="openid", # what information to include in the token, such as "openid profile email"
85
+ )
86
+
87
+ return self.jwt_token
88
+
89
+ def logoff(self) -> bool:
90
+ """Log out of the current session from Keycloak and clean up."""
91
+ result = False
92
+ if self._keycloak_openid and self.jwt_token:
93
+ try:
94
+ self._keycloak_openid.logout(self.jwt_token.get("refresh_token"))
95
+ result = True
96
+ except Exception as e:
97
+ logging.warning("Failed to logout from Keycloak: %s", str(e))
98
+ finally:
99
+ self.jwt_token = None
100
+
101
+ # Call super to handle base class cleanup (timers, session)
102
+ super_result = super().logoff()
103
+
104
+ return result and super_result
105
+
106
+ def renew_token(self) -> dict:
107
+ """Renew the current Keycloak token."""
108
+ if not self.jwt_token or not isinstance(self.jwt_token, dict) or "refresh_token" not in self.jwt_token:
109
+ raise Exception("No Keycloak refresh token available to renew")
110
+
111
+ try:
112
+ # Handle Keycloak token (dictionary format)
113
+ new_token = self._keycloak_openid.refresh_token(self.jwt_token.get("refresh_token"))
114
+ self.jwt_token = new_token
115
+ return self.jwt_token
116
+ except Exception as e:
117
+ logging.error("Keycloak token renewal failed: %s", str(e))
118
+ raise Exception(f"Keycloak token renewal failed: {str(e)}") from e
119
+
120
+ def view_self(self) -> dict:
121
+ """Get information about the current user, using Keycloak userinfo if applicable.
122
+ Returns:
123
+ dict: user definition.
124
+ """
125
+ return self._keycloak_openid.userinfo(self._get_access_token())
126
+
127
+ def close(self):
128
+ """Close the session and release resources, including Keycloak logout."""
129
+ # Logout from Keycloak if needed
130
+ if hasattr(self, "_keycloak_openid") and self._keycloak_openid and hasattr(self, "_jwt_token") and self._jwt_token and isinstance(self._jwt_token, dict) and "refresh_token" in self._jwt_token:
131
+ try:
132
+ self._keycloak_openid.logout(self._jwt_token.get("refresh_token"))
133
+ except Exception as e:
134
+ logging.warning("Failed to logout from Keycloak: %s", str(e))
135
+ finally:
136
+ self._keycloak_openid = None
137
+ # Close the base class session and resources (timers, etc.)
138
+ super().close()
appmesh/client_tcp.py CHANGED
@@ -6,7 +6,7 @@ import os
6
6
  import socket
7
7
  import uuid
8
8
  import requests
9
- from .client_http import AppMeshClient
9
+ from .client_http import AppMeshClient, EncodingResponse
10
10
  from .tcp_transport import TCPTransport
11
11
  from .tcp_messages import RequestMessage, ResponseMessage
12
12
 
@@ -140,7 +140,7 @@ class AppMeshClientTCP(AppMeshClient):
140
140
  appmesh_request.headers[k] = v
141
141
  if query:
142
142
  for k, v in query.items():
143
- appmesh_request.querys[k] = v
143
+ appmesh_request.query[k] = v
144
144
 
145
145
  data = appmesh_request.serialize()
146
146
  self.tcp_transport.send_message(data)
@@ -153,13 +153,13 @@ class AppMeshClientTCP(AppMeshClient):
153
153
  appmesh_resp = ResponseMessage().deserialize(resp_data)
154
154
  response = requests.Response()
155
155
  response.status_code = appmesh_resp.http_status
156
- response.encoding = self.ENCODING_UTF8
157
- response._content = appmesh_resp.body.encode(self.ENCODING_UTF8)
156
+ # response.encoding = self.ENCODING_UTF8 # only need when charset not in appmesh_resp.body_msg_type
157
+ response._content = appmesh_resp.body if isinstance(appmesh_resp.body, bytes) else str(appmesh_resp.body).encode(self.ENCODING_UTF8)
158
158
  response.headers = appmesh_resp.headers
159
159
  if appmesh_resp.body_msg_type:
160
160
  response.headers["Content-Type"] = appmesh_resp.body_msg_type
161
161
 
162
- return response
162
+ return EncodingResponse(response)
163
163
 
164
164
  ########################################
165
165
  # File management
appmesh/server_http.py CHANGED
@@ -5,7 +5,7 @@ import abc
5
5
  import logging
6
6
  import os
7
7
  import time
8
- from typing import Optional, Tuple
8
+ from typing import Optional, Tuple, Union
9
9
  from http import HTTPStatus
10
10
  from .client_http import AppMeshClient
11
11
 
@@ -66,7 +66,7 @@ class AppMeshServer(metaclass=abc.ABCMeta):
66
66
  raise Exception("Missing environment variable: APP_MESH_APPLICATION_NAME. This must be set by App Mesh service.")
67
67
  return process_id, app_name
68
68
 
69
- def task_fetch(self) -> str:
69
+ def task_fetch(self) -> Union[str, bytes]:
70
70
  """Fetch task data in the currently running App Mesh application process.
71
71
 
72
72
  Used by App Mesh application process to obtain the payload from App Mesh service
@@ -74,7 +74,7 @@ class AppMeshServer(metaclass=abc.ABCMeta):
74
74
 
75
75
 
76
76
  Returns:
77
- str: The payload provided by the client as returned by the service.
77
+ Union[str, bytes]: The payload provided by the client as returned by the service.
78
78
  """
79
79
  process_id, app_name = self._get_runtime_env()
80
80
  path = f"/appmesh/app/{app_name}/task"
@@ -91,16 +91,16 @@ class AppMeshServer(metaclass=abc.ABCMeta):
91
91
  time.sleep(0.1)
92
92
  continue
93
93
 
94
- return resp.text
94
+ return resp.content
95
95
 
96
- def task_return(self, result: str) -> None:
96
+ def task_return(self, result: Union[str, bytes]) -> None:
97
97
  """Return the result of a server-side invocation back to the original client.
98
98
 
99
99
  Used by App Mesh application process to posts the `result` to App Mesh service
100
100
  after processed payload data so the invoking client can retrieve it.
101
101
 
102
102
  Args:
103
- result (str): Result payload to be delivered back to the client.
103
+ result (Union[str, bytes]): Result payload to be delivered back to the client.
104
104
  """
105
105
  process_id, app_name = self._get_runtime_env()
106
106
  path = f"/appmesh/app/{app_name}/task"
appmesh/tcp_messages.py CHANGED
@@ -1,41 +1,69 @@
1
1
  # tcp_messages.py
2
2
 
3
+ from typing import get_type_hints
3
4
  import msgpack
4
5
 
5
6
 
6
7
  class RequestMessage:
7
8
  """TCP request message for HTTP-like communication"""
8
9
 
9
- uuid: str = ""
10
- request_uri: str = ""
11
- http_method: str = ""
12
- client_addr: str = ""
13
- body: bytes = b""
14
- headers: dict = {}
15
- querys: dict = {}
10
+ def __init__(self):
11
+ self.uuid: str = ""
12
+ self.request_uri: str = ""
13
+ self.http_method: str = ""
14
+ self.client_addr: str = ""
15
+ self.body: bytes = b""
16
+ self.headers: dict = {}
17
+ self.query: dict = {}
16
18
 
17
19
  def serialize(self) -> bytes:
18
20
  """Serialize request message to bytes"""
19
- # http://www.cnitblog.com/luckydmz/archive/2019/11/20/91959.html
20
- self_dict = vars(self)
21
- self_dict["headers"] = self.headers
22
- self_dict["querys"] = self.querys
23
- return msgpack.dumps(self_dict)
21
+ return msgpack.dumps(vars(self), use_bin_type=True)
24
22
 
25
23
 
26
24
  class ResponseMessage:
27
25
  """TCP response message for HTTP-like communication"""
28
26
 
29
- uuid: str = ""
30
- request_uri: str = ""
31
- http_status: int = 0
32
- body_msg_type: str = ""
33
- body: str = ""
34
- headers: dict = {}
27
+ uuid: str
28
+ request_uri: str
29
+ http_status: int
30
+ body_msg_type: str
31
+ body: bytes
32
+ headers: dict
33
+
34
+ def __init__(self):
35
+ self.uuid = ""
36
+ self.request_uri = ""
37
+ self.http_status = 0
38
+ self.body_msg_type = ""
39
+ self.body = b""
40
+ self.headers = {}
35
41
 
36
42
  def deserialize(self, buf: bytes):
37
- """Deserialize response message"""
38
- dic = msgpack.unpackb(buf)
43
+ """Deserialize TCP msgpack buffer with proper type conversion."""
44
+ dic = msgpack.unpackb(buf, raw=False)
45
+ hints = get_type_hints(self.__class__)
46
+
39
47
  for k, v in dic.items():
48
+ if k not in hints:
49
+ continue # Skip unknown fields
50
+
51
+ # handle all types (int, bytes, dict, str)
52
+ t = hints[k]
53
+ if t is str:
54
+ if isinstance(v, bytes):
55
+ v = v.decode("utf-8", errors="replace")
56
+ elif v is None:
57
+ v = ""
58
+ else:
59
+ v = str(v)
60
+ elif t is bytes:
61
+ if isinstance(v, str):
62
+ v = v.encode("utf-8") # handle accidental str
63
+ elif v is None:
64
+ v = b""
65
+ elif t is int:
66
+ v = int(v or 0)
40
67
  setattr(self, k, v)
68
+
41
69
  return self
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: appmesh
3
- Version: 1.6.1
3
+ Version: 1.6.3
4
4
  Summary: Client SDK for App Mesh
5
5
  Home-page: https://github.com/laoshanxi/app-mesh
6
6
  Author: laoshanxi
@@ -40,6 +40,7 @@ Dynamic: summary
40
40
 
41
41
  # App Mesh: Advanced Application Management Platform
42
42
 
43
+ <div align=center><img src="https://github.com/laoshanxi/picture/raw/master/appmesh/whatis.gif" align=center /></div>
43
44
  App Mesh is an open-source, multi-tenant application management platform designed for cloud-native environments. It efficiently manages, schedules, and monitors both microservices and traditional applications, offering a lightweight alternative to Kubernetes. App Mesh bridges the gap between simple process managers and complex container orchestration systems, making it ideal for organizations seeking to modernize their infrastructure without adopting full container-native complexity. Supporting both containerized and native applications, it provides a versatile solution for diverse enterprise needs.
44
45
 
45
46
  <div align=center><img src="https://github.com/laoshanxi/picture/raw/master/appmesh/diagram.png" align=center /></div>
@@ -54,7 +55,7 @@ Cloud native | Schedule cloud-level applications to run on multiple hosts with r
54
55
  Micro service application | 🧱 [Consul micro-service cluster management](https://app-mesh.readthedocs.io/en/latest/CONSUL.html)
55
56
  Extra Features | Collect host/app resource usage <br> Remote shell command execution <br> File upload/download interface <br> Hot-update support `systemctl reload appmesh` <br> Bash completion <br> Reverse proxy <br> 🌐[Web GUI](https://github.com/laoshanxi/app-mesh-ui)
56
57
  Platform support | X86_64 <br> ARM32 <br> ARM64
57
- SDK | [Python](https://app-mesh.readthedocs.io/en/latest/api/appmesh_client.html) <br> [Golang](https://github.com/laoshanxi/app-mesh/blob/main/src/sdk/go/appmesh_client.go) <br> [JavaScript](https://www.npmjs.com/package/appmesh) <br> [Java](https://github.com/laoshanxi/app-mesh/packages/2227502) <br> [Swagger OpenAPI Specification](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/laoshanxi/app-mesh/main/src/daemon/rest/openapi.yaml)
58
+ SDK | [Python](https://app-mesh.readthedocs.io/en/latest/api/appmesh.html#module-appmesh.client_http) <br> [Golang](https://github.com/laoshanxi/app-mesh/blob/main/src/sdk/go/client_http.go) <br> [JavaScript](https://www.npmjs.com/package/appmesh) <br> [Java](https://github.com/laoshanxi/app-mesh/packages/2227502) <br> [Swagger OpenAPI Specification](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/laoshanxi/app-mesh/main/src/daemon/rest/openapi.yaml)
58
59
 
59
60
  ## Getting started
60
61
 
@@ -116,7 +117,6 @@ Refer to the [Installation doc](https://app-mesh.readthedocs.io/en/latest/Instal
116
117
 
117
118
  - [Build a powerful monitor system with Grafana/Prometheus/Loki](https://app-mesh.readthedocs.io/en/latest/success/build_powerful_monitor_system_with_Grafana_Prometheus_Loki.html)
118
119
  - [Customize application start behavior](https://app-mesh.readthedocs.io/en/latest/success/customize_app_startup_behavior.html)
119
- - [Manage cluster-level microservice applications](https://app-mesh.readthedocs.io/en/latest/success/manage_cluster_level_microservice_applications.html)
120
120
  - [Open service broker support local PV for Kubernetes](https://app-mesh.readthedocs.io/en/latest/success/open_service_broker_support_local_pv_for_K8S.html)
121
121
  - [Promote native application to microservice application](https://app-mesh.readthedocs.io/en/latest/success/promote_native_app_to_microservice_app.html)
122
122
  - [Secure REST file server](https://app-mesh.readthedocs.io/en/latest/success/secure_REST_file_server.html)
@@ -0,0 +1,16 @@
1
+ appmesh/__init__.py,sha256=TY1y5B5cE57uhraEzCFOZRWuo9SY1R-fYNRan8hCZOM,1670
2
+ appmesh/app.py,sha256=crD4DRFZJuHtZMfSsz7C-EwvjPmGZbFXYXvA_wCdvdI,10734
3
+ appmesh/app_output.py,sha256=vfn322AyixblI8DbXds08h6L_ybObiaRSifsA1-Xcoo,1035
4
+ appmesh/app_run.py,sha256=aYq852a29OThIi32Xtx5s0sTXZ97T0lHD5WXH8yfPoc,2018
5
+ appmesh/appmesh_client.py,sha256=ywB2222PtJUffdfdxZcBfdhZs1KYyc7JvzMxwuK2qyI,378
6
+ appmesh/client_http.py,sha256=WLdHmi2ryzCiXFyIIAbKH3arfcs__rxt4kwehpO_w6s,57711
7
+ appmesh/client_http_oauth.py,sha256=1d51o0JX_xtB8d2bEuM7_XJHcwMnhcjkbIq7GE1Zxm8,6120
8
+ appmesh/client_tcp.py,sha256=OrKmJ-SMSmCPXj5WVhWwBXiK8Z_WyCrDmTim0pe1pc0,11407
9
+ appmesh/server_http.py,sha256=vf_Kh7ZIyEuBijZp8I2Rv-Fy9gxFdPFn5Pp2rUNCT1U,4319
10
+ appmesh/server_tcp.py,sha256=biBFF5IGWFOw2ru831cfmzn1DVXcBm9e-W6CP2VkfzE,1444
11
+ appmesh/tcp_messages.py,sha256=H9S_iCy0IuufY2v50_SUgRvcyQmJsySG65tBe_xb3Ko,1878
12
+ appmesh/tcp_transport.py,sha256=f28zfZNH46tUHfT8F1PrCM1wUXiSBIW7R3ipMsXJqIU,8946
13
+ appmesh-1.6.3.dist-info/METADATA,sha256=Hcymb5xi90fgxLHqHNWHzicT2KLZzGPo4DonII-IQBQ,11828
14
+ appmesh-1.6.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
15
+ appmesh-1.6.3.dist-info/top_level.txt,sha256=-y0MNQOGJxUzLdHZ6E_Rfv5_LNCkV-GTmOBME_b6pg8,8
16
+ appmesh-1.6.3.dist-info/RECORD,,
@@ -1,15 +0,0 @@
1
- appmesh/__init__.py,sha256=_I-kLcLy7DM3gG2JJTRM_hUxBu3zvIPm2wCsz3NYes4,493
2
- appmesh/app.py,sha256=4lo66ob1ZFDgL8VYKxH4_sh-vbHcGkZNThURE0go-d4,10732
3
- appmesh/app_output.py,sha256=vfn322AyixblI8DbXds08h6L_ybObiaRSifsA1-Xcoo,1035
4
- appmesh/app_run.py,sha256=TJ9xVX8xqUN9-OOmr_8JlgXoyg3hTmDFNKvGf4w_oR4,1980
5
- appmesh/appmesh_client.py,sha256=7yy9c_ygJbqMekrUsHWmRObwHLK82qP65xQLXxfGd8U,339
6
- appmesh/client_http.py,sha256=jUIZJHrnu2-r6N6Cd9lQNPeQcZefB7ENl6DUn8Mugck,57896
7
- appmesh/client_tcp.py,sha256=hE0T_2Z0OQZdF5zi--iuvu2_-ka0DfSSQ4BP3oHlg44,11243
8
- appmesh/server_http.py,sha256=zr9sfS8g_mtP2kuAfmz-qU7rLSFTVt3srbFXaCbl8Y0,4253
9
- appmesh/server_tcp.py,sha256=biBFF5IGWFOw2ru831cfmzn1DVXcBm9e-W6CP2VkfzE,1444
10
- appmesh/tcp_messages.py,sha256=4XRv5lSm_8ElMg_37SuyIRrfxI7XFNNP_7SdO7us3PA,1023
11
- appmesh/tcp_transport.py,sha256=f28zfZNH46tUHfT8F1PrCM1wUXiSBIW7R3ipMsXJqIU,8946
12
- appmesh-1.6.1.dist-info/METADATA,sha256=DvJTf5922gT6fc-iOWizqodwljYIkIlCCI1SeTd0juM,11847
13
- appmesh-1.6.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
- appmesh-1.6.1.dist-info/top_level.txt,sha256=-y0MNQOGJxUzLdHZ6E_Rfv5_LNCkV-GTmOBME_b6pg8,8
15
- appmesh-1.6.1.dist-info/RECORD,,