appmesh 1.4.6__py3-none-any.whl → 1.4.8__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/http_client.py CHANGED
@@ -8,9 +8,9 @@ from datetime import datetime
8
8
  from enum import Enum, unique
9
9
  from http import HTTPStatus
10
10
  from typing import Optional, Tuple, Union
11
+ from urllib import parse
11
12
  import aniso8601
12
13
  import requests
13
- import urllib
14
14
  from .app import App
15
15
  from .app_run import AppRun
16
16
  from .app_output import AppOutput
@@ -131,6 +131,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
131
131
  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,
132
132
  rest_timeout=(60, 300),
133
133
  jwt_token=None,
134
+ oauth2_config=None,
134
135
  ):
135
136
  """Initialize an App Mesh HTTP client for interacting with the App Mesh server via secure HTTPS.
136
137
 
@@ -143,24 +144,68 @@ class AppMeshClient(metaclass=abc.ABCMeta):
143
144
  - `str`: Path to a custom CA certificate or directory for verification. This option allows custom CA configuration,
144
145
  which may be necessary in environments requiring specific CA chains that differ from the default system CAs.
145
146
 
146
- rest_ssl_client_cert (Union[tuple, str], optional): Specifies a client certificate for mutual TLS authentication:
147
- - If a `str`, provides the path to a PEM file with both client certificate and private key.
148
- - If a `tuple`, contains two paths as (`cert`, `key`), where `cert` is the certificate file and `key` is the private key file.
147
+ ssl_client_cert (Union[str, Tuple[str, str]], optional): Path to the SSL client certificate and key. Can be:
148
+ - `str`: A path to a single PEM file containing both the client certificate and private key.
149
+ - `tuple`: A pair of paths (`cert_file`, `key_file`), where `cert_file` is the client certificate file path and `key_file` is the private key file path.
149
150
 
150
151
  rest_timeout (tuple, optional): HTTP connection timeouts for API requests, as `(connect_timeout, read_timeout)`.
151
152
  The default is `(60, 300)`, where `60` seconds is the maximum time to establish a connection and `300` seconds for the maximum read duration.
152
153
 
153
154
  jwt_token (str, optional): JWT token for API authentication, used in headers to authorize requests where required.
154
155
 
156
+ oauth2 (Dict[str, Any], optional): Keycloak configuration for oauth2 authentication:
157
+ - auth_server_url: Keycloak server URL (e.g. "https://keycloak.example.com")
158
+ - realm: Keycloak realm
159
+ - client_id: Keycloak client ID
160
+ - client_secret: Keycloak client secret (optional)
155
161
  """
156
162
 
157
- self.server_url = rest_url
163
+ self.session = requests.Session()
164
+ self.auth_server_url = rest_url
158
165
  self._jwt_token = jwt_token
159
166
  self.ssl_verify = rest_ssl_verify
160
167
  self.ssl_client_cert = rest_ssl_client_cert
161
168
  self.rest_timeout = rest_timeout
162
169
  self._forward_to = None
163
170
 
171
+ # Keycloak integration
172
+ self._keycloak_client = None
173
+ if oauth2_config:
174
+ from .keycloak import KeycloakClient # lazy import
175
+
176
+ self._keycloak_client = KeycloakClient(
177
+ auth_server_url=oauth2_config.get("auth_server_url"),
178
+ realm=oauth2_config.get("realm"),
179
+ client_id=oauth2_config.get("client_id"),
180
+ client_secret=oauth2_config.get("client_secret"),
181
+ ssl_verify=oauth2_config.get("ssl_verify", self.ssl_verify),
182
+ timeout=oauth2_config.get("timeout", (30, 60)),
183
+ )
184
+
185
+ def close(self):
186
+ """Close the session and release resources."""
187
+ if hasattr(self, "session") and self.session:
188
+ self.session.close()
189
+ self.session = None
190
+ if self._keycloak_client and hasattr(self._keycloak_client, "close"):
191
+ self._keycloak_client.close()
192
+ self._keycloak_client = None
193
+
194
+ def __enter__(self):
195
+ """Support for context manager protocol."""
196
+ return self
197
+
198
+ def __exit__(self, exc_type, exc_val, exc_tb):
199
+ """Support for context manager protocol, ensuring resources are released."""
200
+ self.close()
201
+
202
+ def __del__(self):
203
+ """Ensure resources are properly released when the object is garbage collected."""
204
+ try:
205
+ self.close()
206
+ except Exception:
207
+ pass # Avoid exceptions during garbage collection
208
+
164
209
  @property
165
210
  def jwt_token(self) -> str:
166
211
  """Get the current JWT (JSON Web Token) used for authentication.
@@ -266,6 +311,12 @@ class AppMeshClient(metaclass=abc.ABCMeta):
266
311
  Returns:
267
312
  str: JWT token.
268
313
  """
314
+ # Keycloak authentication if configured
315
+ if self._keycloak_client:
316
+ self.jwt_token = self._keycloak_client.authenticate(user_name, user_pwd)
317
+ return self.jwt_token
318
+
319
+ # Standard App Mesh authentication
269
320
  self.jwt_token = None
270
321
  resp = self._request_http(
271
322
  AppMeshClient.Method.POST,
@@ -293,7 +344,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
293
344
  username (str): Username to validate
294
345
  challenge (str): Challenge string from server
295
346
  code (str): TOTP code to validate
296
- timeout (Union[int, str], optional): Token expiry timeout.
347
+ timeout (Union[int, str], optional): Token expiry timeout.
297
348
  Accepts ISO 8601 duration format (e.g., 'P1Y2M3DT4H5M6S', 'P1W') or seconds.
298
349
  Defaults to DURATION_ONE_WEEK_ISO.
299
350
 
@@ -325,11 +376,11 @@ class AppMeshClient(metaclass=abc.ABCMeta):
325
376
  Returns:
326
377
  bool: logoff success or failure.
327
378
  """
328
- resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/self/logoff")
329
- if resp.status_code != HTTPStatus.OK:
330
- raise Exception(resp.text)
331
- self.jwt_token = None
332
- return resp.status_code == HTTPStatus.OK
379
+ if self.jwt_token:
380
+ resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/self/logoff")
381
+ self.jwt_token = None
382
+ return resp.status_code == HTTPStatus.OK
383
+ return True
333
384
 
334
385
  def authentication(self, token: str, permission: Optional[str] = None, audience: Optional[str] = None) -> bool:
335
386
  """Deprecated: Use authenticate() instead."""
@@ -370,6 +421,12 @@ class AppMeshClient(metaclass=abc.ABCMeta):
370
421
  Returns:
371
422
  str: The new JWT token if renew success, the old token will be blocked.
372
423
  """
424
+ # Refresh the Keycloak token
425
+ if self._keycloak_client:
426
+ self.jwt_token = self._keycloak_client.refresh_tokens()
427
+ return self.jwt_token
428
+
429
+ # Refresh the App Mesh token
373
430
  assert self.jwt_token
374
431
  resp = self._request_http(
375
432
  AppMeshClient.Method.POST,
@@ -445,13 +502,13 @@ class AppMeshClient(metaclass=abc.ABCMeta):
445
502
  dict: eextract parameters
446
503
  """
447
504
  parsed_info = {}
448
- parsed_uri = urllib.parse.urlparse(totp_uri)
505
+ parsed_uri = parse.urlparse(totp_uri)
449
506
 
450
507
  # Extract label from the path
451
508
  parsed_info["label"] = parsed_uri.path[1:] # Remove the leading slash
452
509
 
453
510
  # Extract parameters from the query string
454
- query_params = urllib.parse.parse_qs(parsed_uri.query)
511
+ query_params = parse.parse_qs(parsed_uri.query)
455
512
  for key, value in query_params.items():
456
513
  parsed_info[key] = value[0]
457
514
  return parsed_info
@@ -953,7 +1010,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
953
1010
 
954
1011
  with open(file=local_file, mode="rb") as fp:
955
1012
  encoder = MultipartEncoder(fields={"filename": os.path.basename(remote_file), "file": ("filename", fp, "application/octet-stream")})
956
- header = {"File-Path": urllib.parse.quote(remote_file), "Content-Type": encoder.content_type}
1013
+ header = {"File-Path": parse.quote(remote_file), "Content-Type": encoder.content_type}
957
1014
 
958
1015
  # Include file attributes (permissions, owner, group) if requested
959
1016
  if apply_file_attributes:
@@ -1043,12 +1100,15 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1043
1100
  while len(run.proc_uid) > 0:
1044
1101
  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)
1045
1102
  if app_out.output and stdout_print:
1046
- print(app_out.output, end="")
1103
+ print(app_out.output, end="", flush=True)
1047
1104
  if app_out.out_position is not None:
1048
1105
  last_output_position = app_out.out_position
1049
1106
  if app_out.exit_code is not None:
1050
1107
  # success
1051
- self.delete_app(run.app_name)
1108
+ try:
1109
+ self.delete_app(run.app_name)
1110
+ except Exception:
1111
+ pass
1052
1112
  return app_out.exit_code
1053
1113
  if app_out.status_code != HTTPStatus.OK:
1054
1114
  # failed
@@ -1120,7 +1180,14 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1120
1180
  Returns:
1121
1181
  requests.Response: HTTP response
1122
1182
  """
1123
- rest_url = urllib.parse.urljoin(self.server_url, path)
1183
+ # Try to refresh token via Keycloak if using Keycloak and token needs refreshing
1184
+ if self._keycloak_client and self.jwt_token and not self._keycloak_client.validate_token():
1185
+ try:
1186
+ self.jwt_token = self._keycloak_client.get_active_token()
1187
+ except Exception as e:
1188
+ print(f"Token refresh failed: {str(e)}")
1189
+
1190
+ rest_url = parse.urljoin(self.auth_server_url, path)
1124
1191
 
1125
1192
  header = {} if header is None else header
1126
1193
  if self.jwt_token:
@@ -1129,20 +1196,29 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1129
1196
  if ":" in self.forward_to:
1130
1197
  header[self.HTTP_HEADER_KEY_X_TARGET_HOST] = self.forward_to
1131
1198
  else:
1132
- header[self.HTTP_HEADER_KEY_X_TARGET_HOST] = self.forward_to + ":" + str(urllib.parse.urlsplit(self.server_url).port)
1199
+ header[self.HTTP_HEADER_KEY_X_TARGET_HOST] = self.forward_to + ":" + str(parse.urlsplit(self.auth_server_url).port)
1133
1200
  header[self.HTTP_HEADER_KEY_USER_AGENT] = self.HTTP_USER_AGENT
1134
1201
 
1135
- if method is AppMeshClient.Method.GET:
1136
- return requests.get(url=rest_url, params=query, headers=header, cert=self.ssl_client_cert, verify=self.ssl_verify, timeout=self.rest_timeout)
1137
- elif method is AppMeshClient.Method.POST:
1138
- return requests.post(
1139
- url=rest_url, params=query, headers=header, data=json.dumps(body) if type(body) in (dict, list) else body, cert=self.ssl_client_cert, verify=self.ssl_verify, timeout=self.rest_timeout
1140
- )
1141
- elif method is AppMeshClient.Method.POST_STREAM:
1142
- return requests.post(url=rest_url, params=query, headers=header, data=body, cert=self.ssl_client_cert, verify=self.ssl_verify, stream=True, timeout=self.rest_timeout)
1143
- elif method is AppMeshClient.Method.DELETE:
1144
- return requests.delete(url=rest_url, headers=header, cert=self.ssl_client_cert, verify=self.ssl_verify, timeout=self.rest_timeout)
1145
- elif method is AppMeshClient.Method.PUT:
1146
- return requests.put(url=rest_url, params=query, headers=header, json=body, cert=self.ssl_client_cert, verify=self.ssl_verify, timeout=self.rest_timeout)
1147
- else:
1148
- raise Exception("Invalid http method", method)
1202
+ try:
1203
+ if method is AppMeshClient.Method.GET:
1204
+ return self.session.get(url=rest_url, params=query, headers=header, cert=self.ssl_client_cert, verify=self.ssl_verify, timeout=self.rest_timeout)
1205
+ elif method is AppMeshClient.Method.POST:
1206
+ return self.session.post(
1207
+ url=rest_url,
1208
+ params=query,
1209
+ headers=header,
1210
+ data=json.dumps(body) if type(body) in (dict, list) else body,
1211
+ cert=self.ssl_client_cert,
1212
+ verify=self.ssl_verify,
1213
+ timeout=self.rest_timeout,
1214
+ )
1215
+ elif method is AppMeshClient.Method.POST_STREAM:
1216
+ return self.session.post(url=rest_url, params=query, headers=header, data=body, cert=self.ssl_client_cert, verify=self.ssl_verify, stream=True, timeout=self.rest_timeout)
1217
+ elif method is AppMeshClient.Method.DELETE:
1218
+ return self.session.delete(url=rest_url, headers=header, cert=self.ssl_client_cert, verify=self.ssl_verify, timeout=self.rest_timeout)
1219
+ elif method is AppMeshClient.Method.PUT:
1220
+ return self.session.put(url=rest_url, params=query, headers=header, json=body, cert=self.ssl_client_cert, verify=self.ssl_verify, timeout=self.rest_timeout)
1221
+ else:
1222
+ raise Exception("Invalid http method", method)
1223
+ except requests.exceptions.RequestException as e:
1224
+ raise Exception(f"HTTP request failed: {str(e)}")
appmesh/keycloak.py ADDED
@@ -0,0 +1,248 @@
1
+ # Keycloak authentication client for App Mesh Python SDK
2
+
3
+ import base64
4
+ import json
5
+ import time
6
+ from typing import Dict, Optional, Tuple, Any
7
+ import requests
8
+
9
+
10
+ class KeycloakClient:
11
+ """
12
+ Client for authenticating with Keycloak and obtaining tokens for App Mesh.
13
+
14
+ This class handles the OAuth2/OIDC workflow with Keycloak, including:
15
+ - Direct authentication with username/password
16
+ - Token refresh
17
+ - Token validation
18
+ """
19
+
20
+ def __init__(
21
+ self,
22
+ auth_server_url: str,
23
+ realm: str,
24
+ client_id: str,
25
+ client_secret: Optional[str] = None,
26
+ ssl_verify: bool = True,
27
+ timeout: Tuple[int, int] = (10, 60),
28
+ token_refresh_threshold: int = 30,
29
+ ):
30
+ """Initialize Keycloak client.
31
+
32
+ Args:
33
+ auth_server_url (str): Keycloak server URL (e.g. https://keycloak.example.com/auth)
34
+ realm (str): Keycloak realm name
35
+ client_id (str): Client ID registered in Keycloak
36
+ client_secret (Optional[str], optional): Client secret if using confidential client. Defaults to None.
37
+ ssl_verify (bool, optional): Verify SSL certificates. Defaults to True.
38
+ timeout (Tuple[int, int], optional): Connection and read timeouts. Defaults to (10, 60).
39
+ token_refresh_threshold (int, optional): Seconds before token expiry to trigger refresh. Defaults to 30.
40
+ """
41
+ self.auth_server_url = auth_server_url.rstrip("/")
42
+ self.realm = realm
43
+ self.client_id = client_id
44
+ self.client_secret = client_secret
45
+ self.ssl_verify = ssl_verify
46
+ self.timeout = timeout
47
+ self.token_refresh_threshold = token_refresh_threshold
48
+
49
+ # Token storage
50
+ self.access_token = None
51
+ self.refresh_token = None
52
+ self.token_expires_at = 0
53
+
54
+ # Construct the token endpoint URL
55
+ self.token_endpoint = f"{self.auth_server_url}/realms/{self.realm}/protocol/openid-connect/token"
56
+ self.userinfo_endpoint = f"{self.auth_server_url}/realms/{self.realm}/protocol/openid-connect/userinfo"
57
+
58
+ # Create a session for connection pooling
59
+ self.session = requests.Session()
60
+
61
+ def __enter__(self):
62
+ """Support for context manager protocol."""
63
+ return self
64
+
65
+ def __exit__(self, exc_type, exc_val, exc_tb):
66
+ """Clean up resources when exiting context."""
67
+ self.close()
68
+
69
+ def __del__(self):
70
+ """Ensure resources are properly released when the object is garbage collected."""
71
+ try:
72
+ self.close()
73
+ except Exception:
74
+ pass # Avoid exceptions during garbage collection
75
+
76
+ def authenticate(self, username: str, password: str) -> str:
77
+ """Authenticate with username and password.
78
+
79
+ Args:
80
+ username (str): Username
81
+ password (str): Password
82
+
83
+ Returns:
84
+ str: Access token
85
+
86
+ Raises:
87
+ Exception: If authentication fails
88
+ """
89
+ data = {
90
+ "client_id": self.client_id,
91
+ "grant_type": "password",
92
+ "username": username,
93
+ "password": password,
94
+ }
95
+
96
+ if self.client_secret:
97
+ data["client_secret"] = self.client_secret
98
+
99
+ try:
100
+ response = self.session.post(self.token_endpoint, data=data, verify=self.ssl_verify, timeout=self.timeout)
101
+
102
+ if response.status_code != 200:
103
+ raise Exception(f"Authentication failed: {response.text}")
104
+
105
+ token_data = response.json()
106
+ self._update_token_data(token_data)
107
+ return self.access_token
108
+
109
+ except requests.RequestException as e:
110
+ raise Exception(f"Failed to connect to Keycloak: {str(e)}")
111
+
112
+ def refresh_tokens(self) -> str:
113
+ """Refresh the access token using the refresh token.
114
+
115
+ Returns:
116
+ str: New access token
117
+
118
+ Raises:
119
+ Exception: If refresh fails
120
+ """
121
+ if not self.refresh_token:
122
+ raise Exception("No refresh token available")
123
+
124
+ data = {
125
+ "client_id": self.client_id,
126
+ "grant_type": "refresh_token",
127
+ "refresh_token": self.refresh_token,
128
+ }
129
+
130
+ if self.client_secret:
131
+ data["client_secret"] = self.client_secret
132
+
133
+ try:
134
+ response = self.session.post(self.token_endpoint, data=data, verify=self.ssl_verify, timeout=self.timeout)
135
+
136
+ if response.status_code != 200:
137
+ raise Exception(f"Token refresh failed: {response.text}")
138
+
139
+ token_data = response.json()
140
+ self._update_token_data(token_data)
141
+ return self.access_token
142
+
143
+ except requests.RequestException as e:
144
+ raise Exception(f"Failed to connect to Keycloak: {str(e)}")
145
+
146
+ def validate_token(self) -> bool:
147
+ """Check if the current token is valid and not expired.
148
+
149
+ Returns:
150
+ bool: True if valid, False otherwise
151
+ """
152
+ if not self.access_token:
153
+ return False
154
+
155
+ # Check if the token is expired based on our local expiry time
156
+ if time.time() > self.token_expires_at:
157
+ return False
158
+
159
+ return True
160
+
161
+ def get_active_token(self) -> str:
162
+ """Get a valid access token, refreshing if necessary.
163
+
164
+ Returns:
165
+ str: Valid access token
166
+
167
+ Raises:
168
+ Exception: If no valid token can be obtained
169
+ """
170
+ if self.validate_token():
171
+ return self.access_token
172
+
173
+ if self.refresh_token:
174
+ try:
175
+ return self.refresh_tokens()
176
+ except Exception:
177
+ pass
178
+
179
+ raise Exception("No valid token available and unable to refresh")
180
+
181
+ def get_user_info(self) -> Dict[str, Any]:
182
+ """Get information about the authenticated user.
183
+
184
+ Returns:
185
+ Dict[str, Any]: User information
186
+
187
+ Raises:
188
+ Exception: If request fails
189
+ """
190
+ if not self.access_token:
191
+ raise Exception("Not authenticated")
192
+
193
+ headers = {"Authorization": f"Bearer {self.access_token}"}
194
+
195
+ try:
196
+ response = self.session.get(self.userinfo_endpoint, headers=headers, verify=self.ssl_verify, timeout=self.timeout)
197
+
198
+ if response.status_code != 200:
199
+ raise Exception(f"Failed to get user info: {response.text}")
200
+
201
+ return response.json()
202
+
203
+ except requests.RequestException as e:
204
+ raise Exception(f"Failed to connect to Keycloak: {str(e)}")
205
+
206
+ def _update_token_data(self, token_data: Dict[str, Any]) -> None:
207
+ """Update the stored token data.
208
+
209
+ Args:
210
+ token_data (Dict[str, Any]): Token data from Keycloak
211
+ """
212
+ self.access_token = token_data["access_token"]
213
+ self.refresh_token = token_data.get("refresh_token")
214
+
215
+ # Calculate token expiration time with configurable buffer
216
+ expires_in = token_data.get("expires_in", 300)
217
+ self.token_expires_at = time.time() + expires_in - self.token_refresh_threshold
218
+
219
+ def close(self) -> None:
220
+ """Close the session and release resources."""
221
+ if hasattr(self, "session") and self.session:
222
+ self.session.close()
223
+ self.session = None
224
+
225
+ @staticmethod
226
+ def decode_token(token: str) -> Dict[str, Any]:
227
+ """Decode a JWT token without verification.
228
+
229
+ Args:
230
+ token (str): JWT token
231
+
232
+ Returns:
233
+ Dict[str, Any]: Decoded token payload
234
+ """
235
+ parts = token.split(".")
236
+ if len(parts) != 3:
237
+ raise Exception("Invalid token format")
238
+
239
+ # Decode the payload (second part)
240
+ payload = parts[1]
241
+ # Add padding if necessary
242
+ payload += "=" * (4 - len(payload) % 4) if len(payload) % 4 else ""
243
+
244
+ try:
245
+ decoded = base64.b64decode(payload)
246
+ return json.loads(decoded)
247
+ except Exception as e:
248
+ raise Exception(f"Failed to decode token: {str(e)}")
appmesh/tcp_transport.py CHANGED
@@ -31,9 +31,9 @@ class TCPTransport:
31
31
  **Note**: Unlike HTTP requests, TCP connections cannot automatically retrieve intermediate or public CA certificates.
32
32
  When `rest_ssl_verify` is a path, it explicitly identifies a CA issuer to ensure certificate validation.
33
33
 
34
- ssl_client_cert (Union[str, Tuple[str, str]], optional): Path to the SSL client certificate and key. If a `str`,
35
- it should be the path to a PEM file containing both the client certificate and private key. If a `tuple`, it should
36
- be a pair of paths: (`cert`, `key`), where `cert` is the client certificate file and `key` is the private key file.
34
+ ssl_client_cert (Union[str, Tuple[str, str]], optional): Path to the SSL client certificate and key. Can be:
35
+ - `str`: A path to a single PEM file containing both the client certificate and private key.
36
+ - `tuple`: A pair of paths (`cert_file`, `key_file`), where `cert_file` is the client certificate file path and `key_file` is the private key file path.
37
37
 
38
38
  tcp_address (Tuple[str, int], optional): Address and port for establishing a TCP connection to the server.
39
39
  Defaults to `("localhost", 6059)`.
@@ -83,21 +83,31 @@ class TCPTransport:
83
83
 
84
84
  if self.ssl_client_cert is not None:
85
85
  # Load client-side certificate and private key
86
- context.load_cert_chain(certfile=self.ssl_client_cert[0], keyfile=self.ssl_client_cert[1])
86
+ if isinstance(self.ssl_client_cert, tuple) and len(self.ssl_client_cert) == 2:
87
+ context.load_cert_chain(certfile=self.ssl_client_cert[0], keyfile=self.ssl_client_cert[1])
88
+ elif isinstance(self.ssl_client_cert, str):
89
+ # Handle case where cert and key are in the same file
90
+ context.load_cert_chain(certfile=self.ssl_client_cert)
91
+ else:
92
+ raise ValueError("ssl_client_cert must be a string filepath or a tuple of (cert_file, key_file)")
87
93
 
88
94
  # Create a TCP socket
89
95
  sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
90
96
  sock.setblocking(True)
91
97
  sock.settimeout(30) # Connection timeout set to 30 seconds
92
- # Wrap the socket with SSL/TLS
93
- ssl_socket = context.wrap_socket(sock, server_hostname=self.tcp_address[0])
94
- # Connect to the server
95
- ssl_socket.connect(self.tcp_address)
96
- # Disable Nagle's algorithm
97
- ssl_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
98
- # After connecting, set separate timeout for recv/send
99
- # ssl_socket.settimeout(20) # 20 seconds for recv/send
100
- self._socket = ssl_socket
98
+ try:
99
+ # Wrap the socket with SSL/TLS
100
+ ssl_socket = context.wrap_socket(sock, server_hostname=self.tcp_address[0])
101
+ # Connect to the server
102
+ ssl_socket.connect(self.tcp_address)
103
+ # Disable Nagle's algorithm
104
+ ssl_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
105
+ # After connecting, set separate timeout for recv/send
106
+ # ssl_socket.settimeout(20) # 20 seconds for recv/send
107
+ self._socket = ssl_socket
108
+ except (socket.error, ssl.SSLError) as e:
109
+ sock.close()
110
+ raise RuntimeError(f"Failed to connect to {self.tcp_address}: {e}")
101
111
 
102
112
  def close(self) -> None:
103
113
  """Close socket connection"""
@@ -115,23 +125,37 @@ class TCPTransport:
115
125
 
116
126
  def send_message(self, data) -> None:
117
127
  """Send a message with a prefixed header indicating its length"""
118
- length = len(data)
119
- # Pack the header into 8 bytes using big-endian format
120
- self._socket.sendall(struct.pack("!II", self.TCP_MESSAGE_MAGIC, length))
121
- if length > 0:
122
- self._socket.sendall(data)
128
+ if self._socket is None:
129
+ raise RuntimeError("Cannot send message: not connected")
130
+
131
+ try:
132
+ length = len(data)
133
+ # Pack the header into 8 bytes using big-endian format
134
+ self._socket.sendall(struct.pack("!II", self.TCP_MESSAGE_MAGIC, length))
135
+ if length > 0:
136
+ self._socket.sendall(data)
137
+ except (socket.error, ssl.SSLError) as e:
138
+ self.close()
139
+ raise RuntimeError(f"Error sending message: {e}")
123
140
 
124
141
  def receive_message(self) -> Optional[bytearray]:
125
142
  """Receive a message with a prefixed header indicating its length and validate it"""
126
- # Unpack the data (big-endian format)
127
- magic, length = struct.unpack("!II", self._recvall(self.TCP_MESSAGE_HEADER_LENGTH))
128
- if magic != self.TCP_MESSAGE_MAGIC:
129
- raise ValueError(f"Invalid message: incorrect magic number 0x{magic:X}.")
130
- if length > self.TCP_MAX_BLOCK_SIZE:
131
- raise ValueError(f"Message size {length} exceeds the maximum allowed size of {self.TCP_MAX_BLOCK_SIZE} bytes.")
132
- if length > 0:
133
- return self._recvall(length)
134
- return None
143
+ if self._socket is None:
144
+ raise RuntimeError("Cannot receive message: not connected")
145
+
146
+ try:
147
+ # Unpack the data (big-endian format)
148
+ magic, length = struct.unpack("!II", self._recvall(self.TCP_MESSAGE_HEADER_LENGTH))
149
+ if magic != self.TCP_MESSAGE_MAGIC:
150
+ raise ValueError(f"Invalid message: incorrect magic number 0x{magic:X}.")
151
+ if length > self.TCP_MAX_BLOCK_SIZE:
152
+ raise ValueError(f"Message size {length} exceeds the maximum allowed size of {self.TCP_MAX_BLOCK_SIZE} bytes.")
153
+ if length > 0:
154
+ return self._recvall(length)
155
+ return None
156
+ except (socket.error, ssl.SSLError) as e:
157
+ self.close()
158
+ raise RuntimeError(f"Error receiving message: {e}")
135
159
 
136
160
  def _recvall(self, length: int) -> bytearray:
137
161
  """socket recv data with fixed length
@@ -156,11 +180,14 @@ class TCPTransport:
156
180
 
157
181
  while bytes_received < length:
158
182
  # Use recv_into to read directly into our buffer
159
- chunk_size = self._socket.recv_into(view[bytes_received:], length - bytes_received)
183
+ try:
184
+ chunk_size = self._socket.recv_into(view[bytes_received:], length - bytes_received)
160
185
 
161
- if chunk_size == 0:
162
- raise EOFError("Connection closed by peer")
186
+ if chunk_size == 0:
187
+ raise EOFError("Connection closed by peer")
163
188
 
164
- bytes_received += chunk_size
189
+ bytes_received += chunk_size
190
+ except socket.timeout:
191
+ raise socket.timeout(f"Socket operation timed out after receiving {bytes_received}/{length} bytes")
165
192
 
166
193
  return buffer
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: appmesh
3
- Version: 1.4.6
3
+ Version: 1.4.8
4
4
  Summary: Client SDK for App Mesh
5
5
  Home-page: https://github.com/laoshanxi/app-mesh
6
6
  Author: laoshanxi
@@ -28,13 +28,14 @@ Dynamic: requires-dist
28
28
  Dynamic: requires-python
29
29
  Dynamic: summary
30
30
 
31
- [![language.badge]][language.url] [![standard.badge]][standard.url] [![release.badge]][release.url] [![pypi.badge]][pypi.url] [![unittest.badge]][unittest.url] [![docker.badge]][docker.url] [![cockpit.badge]][cockpit.url]
31
+ [![language.badge]][language.url] [![standard.badge]][standard.url] [![unittest.badge]][unittest.url] [![docker.badge]][docker.url] [![cockpit.badge]][cockpit.url]
32
32
  [![Documentation Status](https://readthedocs.org/projects/app-mesh/badge/?version=latest)](https://app-mesh.readthedocs.io/en/latest/?badge=latest) [![Join the chat at https://gitter.im/app-mesh/community](https://badges.gitter.im/app-mesh/community.svg)](https://gitter.im/app-mesh/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
33
33
  <a href="https://scan.coverity.com/projects/laoshanxi-app-mesh">
34
34
  <img alt="Coverity Scan Build Status"
35
35
  src="https://img.shields.io/coverity/scan/21528.svg"/>
36
36
  </a>
37
37
  [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/laoshanxi/app-mesh/badge)](https://api.securityscorecards.dev/projects/github.com/laoshanxi/app-mesh)
38
+ [![release.badge]][release.url] [![pypi.badge]][pypi.url] [![npm.badge]][npm.url]
38
39
 
39
40
  # App Mesh: Advanced Application Management Platform
40
41
 
@@ -148,7 +149,7 @@ Refer to the [Installation doc](https://app-mesh.readthedocs.io/en/latest/Instal
148
149
  [standard.url]: https://en.wikipedia.org/wiki/C%2B%2B#Standardization
149
150
  [standard.badge]: https://img.shields.io/badge/C%2B%2B-11%2F14%2F17-blue.svg
150
151
  [release.url]: https://github.com/laoshanxi/app-mesh/releases
151
- [release.badge]: https://img.shields.io/github/v/release/laoshanxi/app-mesh.svg
152
+ [release.badge]: https://img.shields.io/github/v/release/laoshanxi/app-mesh?label=Github%20package
152
153
  [docker.url]: https://hub.docker.com/repository/docker/laoshanxi/appmesh
153
154
  [docker.badge]: https://img.shields.io/docker/pulls/laoshanxi/appmesh.svg
154
155
  [cockpit.url]: https://github.com/laoshanxi/app-mesh-ui
@@ -157,3 +158,5 @@ Refer to the [Installation doc](https://app-mesh.readthedocs.io/en/latest/Instal
157
158
  [unittest.badge]: https://img.shields.io/badge/UnitTest-Catch2-blue?logo=appveyor
158
159
  [pypi.badge]: https://img.shields.io/pypi/v/appmesh?label=PyPI%3Aappmesh
159
160
  [pypi.url]: https://pypi.org/project/appmesh/
161
+ [npm.badge]: https://img.shields.io/npm/v/appmesh?label=npm%3Aappmesh
162
+ [npm.url]: https://www.npmjs.com/package/appmesh
@@ -3,11 +3,12 @@ appmesh/app.py,sha256=9Q-SOOej-MH13BU5Dv2iTa-p-sECCJQp6ZX9DjWWmwE,10526
3
3
  appmesh/app_output.py,sha256=JK_TMKgjvaw4n_ys_vmN5S4MyWVZpmD7NlKz_UyMIM8,1015
4
4
  appmesh/app_run.py,sha256=9ISKGZ3k3kkbQvSsPfRfkOLqD9xhbqNOM7ork9F4w9c,1712
5
5
  appmesh/appmesh_client.py,sha256=0ltkqHZUq094gKneYmC0bEZCP0X9kHTp9fccKdWFWP0,339
6
- appmesh/http_client.py,sha256=qM0es3dM4PM95ymNqdTAuIvTD9-Xy6jPKhDW-Sn7ehg,45180
6
+ appmesh/http_client.py,sha256=S90Ldndx3wfUYE2Ln31fnKKBNXbDROQd3FAzFTEdZtA,48314
7
+ appmesh/keycloak.py,sha256=BJZ35FPO0C2wDeDhCcd6cE4ba9BF4UZ-4o5QelmbGHg,8036
7
8
  appmesh/tcp_client.py,sha256=RkHl5s8jE333BJOgxJqJ_fvjbdRQza7ciV49vLT6YO4,10923
8
9
  appmesh/tcp_messages.py,sha256=w1Kehz_aX4X2CYAUsy9mFVJRhxnLQwwc6L58W4YkQqs,969
9
- appmesh/tcp_transport.py,sha256=UMGby2oKV4k7lyXZUMSOe2Je34fb1w7nTkxEpatKLKg,7256
10
- appmesh-1.4.6.dist-info/METADATA,sha256=x8wWJAFfIPR3PU4nGmmZDezdmmeoCLFmkE-nrOM1lPY,11501
11
- appmesh-1.4.6.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
12
- appmesh-1.4.6.dist-info/top_level.txt,sha256=-y0MNQOGJxUzLdHZ6E_Rfv5_LNCkV-GTmOBME_b6pg8,8
13
- appmesh-1.4.6.dist-info/RECORD,,
10
+ appmesh/tcp_transport.py,sha256=-XDTQbsKL3yCbguHeW2jNqXpYgnCyHsH4rwcaJ46AS8,8645
11
+ appmesh-1.4.8.dist-info/METADATA,sha256=_Lmle-Icw-PkTf5yUvx3wHgaH0usfqAcVD2eePzjYS8,11663
12
+ appmesh-1.4.8.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
13
+ appmesh-1.4.8.dist-info/top_level.txt,sha256=-y0MNQOGJxUzLdHZ6E_Rfv5_LNCkV-GTmOBME_b6pg8,8
14
+ appmesh-1.4.8.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.0)
2
+ Generator: setuptools (76.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5