appmesh 1.4.7__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
@@ -14,7 +14,6 @@ import requests
14
14
  from .app import App
15
15
  from .app_run import AppRun
16
16
  from .app_output import AppOutput
17
- from .keycloak import KeycloakClient
18
17
 
19
18
 
20
19
  class AppMeshClient(metaclass=abc.ABCMeta):
@@ -145,9 +144,9 @@ class AppMeshClient(metaclass=abc.ABCMeta):
145
144
  - `str`: Path to a custom CA certificate or directory for verification. This option allows custom CA configuration,
146
145
  which may be necessary in environments requiring specific CA chains that differ from the default system CAs.
147
146
 
148
- rest_ssl_client_cert (Union[tuple, str], optional): Specifies a client certificate for mutual TLS authentication:
149
- - If a `str`, provides the path to a PEM file with both client certificate and private key.
150
- - 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.
151
150
 
152
151
  rest_timeout (tuple, optional): HTTP connection timeouts for API requests, as `(connect_timeout, read_timeout)`.
153
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.
@@ -155,14 +154,14 @@ class AppMeshClient(metaclass=abc.ABCMeta):
155
154
  jwt_token (str, optional): JWT token for API authentication, used in headers to authorize requests where required.
156
155
 
157
156
  oauth2 (Dict[str, Any], optional): Keycloak configuration for oauth2 authentication:
158
- - server_url: Keycloak server URL (e.g. "https://keycloak.example.com")
157
+ - auth_server_url: Keycloak server URL (e.g. "https://keycloak.example.com")
159
158
  - realm: Keycloak realm
160
159
  - client_id: Keycloak client ID
161
160
  - client_secret: Keycloak client secret (optional)
162
161
  """
163
162
 
164
163
  self.session = requests.Session()
165
- self.server_url = rest_url
164
+ self.auth_server_url = rest_url
166
165
  self._jwt_token = jwt_token
167
166
  self.ssl_verify = rest_ssl_verify
168
167
  self.ssl_client_cert = rest_ssl_client_cert
@@ -172,30 +171,41 @@ class AppMeshClient(metaclass=abc.ABCMeta):
172
171
  # Keycloak integration
173
172
  self._keycloak_client = None
174
173
  if oauth2_config:
174
+ from .keycloak import KeycloakClient # lazy import
175
+
175
176
  self._keycloak_client = KeycloakClient(
176
- server_url=oauth2_config.get("server_url"),
177
+ auth_server_url=oauth2_config.get("auth_server_url"),
177
178
  realm=oauth2_config.get("realm"),
178
179
  client_id=oauth2_config.get("client_id"),
179
180
  client_secret=oauth2_config.get("client_secret"),
180
181
  ssl_verify=oauth2_config.get("ssl_verify", self.ssl_verify),
181
182
  timeout=oauth2_config.get("timeout", (30, 60)),
182
183
  )
183
-
184
+
184
185
  def close(self):
185
186
  """Close the session and release resources."""
186
- if hasattr(self, 'session'):
187
+ if hasattr(self, "session") and self.session:
187
188
  self.session.close()
188
- if self._keycloak_client and hasattr(self._keycloak_client, 'close'):
189
+ self.session = None
190
+ if self._keycloak_client and hasattr(self._keycloak_client, "close"):
189
191
  self._keycloak_client.close()
190
-
192
+ self._keycloak_client = None
193
+
191
194
  def __enter__(self):
192
195
  """Support for context manager protocol."""
193
196
  return self
194
-
197
+
195
198
  def __exit__(self, exc_type, exc_val, exc_tb):
196
199
  """Support for context manager protocol, ensuring resources are released."""
197
200
  self.close()
198
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
+
199
209
  @property
200
210
  def jwt_token(self) -> str:
201
211
  """Get the current JWT (JSON Web Token) used for authentication.
@@ -1090,12 +1100,15 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1090
1100
  while len(run.proc_uid) > 0:
1091
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)
1092
1102
  if app_out.output and stdout_print:
1093
- print(app_out.output, end="")
1103
+ print(app_out.output, end="", flush=True)
1094
1104
  if app_out.out_position is not None:
1095
1105
  last_output_position = app_out.out_position
1096
1106
  if app_out.exit_code is not None:
1097
1107
  # success
1098
- self.delete_app(run.app_name)
1108
+ try:
1109
+ self.delete_app(run.app_name)
1110
+ except Exception:
1111
+ pass
1099
1112
  return app_out.exit_code
1100
1113
  if app_out.status_code != HTTPStatus.OK:
1101
1114
  # failed
@@ -1169,9 +1182,12 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1169
1182
  """
1170
1183
  # Try to refresh token via Keycloak if using Keycloak and token needs refreshing
1171
1184
  if self._keycloak_client and self.jwt_token and not self._keycloak_client.validate_token():
1172
- self.jwt_token = self._keycloak_client.get_active_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)}")
1173
1189
 
1174
- rest_url = parse.urljoin(self.server_url, path)
1190
+ rest_url = parse.urljoin(self.auth_server_url, path)
1175
1191
 
1176
1192
  header = {} if header is None else header
1177
1193
  if self.jwt_token:
@@ -1180,20 +1196,29 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1180
1196
  if ":" in self.forward_to:
1181
1197
  header[self.HTTP_HEADER_KEY_X_TARGET_HOST] = self.forward_to
1182
1198
  else:
1183
- header[self.HTTP_HEADER_KEY_X_TARGET_HOST] = self.forward_to + ":" + str(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)
1184
1200
  header[self.HTTP_HEADER_KEY_USER_AGENT] = self.HTTP_USER_AGENT
1185
1201
 
1186
- if method is AppMeshClient.Method.GET:
1187
- return self.session.get(url=rest_url, params=query, headers=header, cert=self.ssl_client_cert, verify=self.ssl_verify, timeout=self.rest_timeout)
1188
- elif method is AppMeshClient.Method.POST:
1189
- return self.session.post(
1190
- 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
1191
- )
1192
- elif method is AppMeshClient.Method.POST_STREAM:
1193
- 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)
1194
- elif method is AppMeshClient.Method.DELETE:
1195
- return self.session.delete(url=rest_url, headers=header, cert=self.ssl_client_cert, verify=self.ssl_verify, timeout=self.rest_timeout)
1196
- elif method is AppMeshClient.Method.PUT:
1197
- 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)
1198
- else:
1199
- 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 CHANGED
@@ -19,7 +19,7 @@ class KeycloakClient:
19
19
 
20
20
  def __init__(
21
21
  self,
22
- server_url: str,
22
+ auth_server_url: str,
23
23
  realm: str,
24
24
  client_id: str,
25
25
  client_secret: Optional[str] = None,
@@ -30,7 +30,7 @@ class KeycloakClient:
30
30
  """Initialize Keycloak client.
31
31
 
32
32
  Args:
33
- server_url (str): Keycloak server URL (e.g. https://keycloak.example.com/auth)
33
+ auth_server_url (str): Keycloak server URL (e.g. https://keycloak.example.com/auth)
34
34
  realm (str): Keycloak realm name
35
35
  client_id (str): Client ID registered in Keycloak
36
36
  client_secret (Optional[str], optional): Client secret if using confidential client. Defaults to None.
@@ -38,7 +38,7 @@ class KeycloakClient:
38
38
  timeout (Tuple[int, int], optional): Connection and read timeouts. Defaults to (10, 60).
39
39
  token_refresh_threshold (int, optional): Seconds before token expiry to trigger refresh. Defaults to 30.
40
40
  """
41
- self.server_url = server_url.rstrip("/")
41
+ self.auth_server_url = auth_server_url.rstrip("/")
42
42
  self.realm = realm
43
43
  self.client_id = client_id
44
44
  self.client_secret = client_secret
@@ -52,12 +52,27 @@ class KeycloakClient:
52
52
  self.token_expires_at = 0
53
53
 
54
54
  # Construct the token endpoint URL
55
- self.token_endpoint = f"{self.server_url}/realms/{self.realm}/protocol/openid-connect/token"
56
- self.userinfo_endpoint = f"{self.server_url}/realms/{self.realm}/protocol/openid-connect/userinfo"
57
-
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
58
  # Create a session for connection pooling
59
59
  self.session = requests.Session()
60
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
+
61
76
  def authenticate(self, username: str, password: str) -> str:
62
77
  """Authenticate with username and password.
63
78
 
@@ -203,8 +218,9 @@ class KeycloakClient:
203
218
 
204
219
  def close(self) -> None:
205
220
  """Close the session and release resources."""
206
- if hasattr(self, 'session'):
221
+ if hasattr(self, "session") and self.session:
207
222
  self.session.close()
223
+ self.session = None
208
224
 
209
225
  @staticmethod
210
226
  def decode_token(token: str) -> Dict[str, Any]:
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.7
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
@@ -3,12 +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=dtrZryrkJ-dmT_NQE5v0fZPE00-PCD9VB9xL1Wu8ORI,47403
7
- appmesh/keycloak.py,sha256=siyifDaH3EASIti8s0DzxBo477m8eCRmzKwK6dqGRDY,7497
6
+ appmesh/http_client.py,sha256=S90Ldndx3wfUYE2Ln31fnKKBNXbDROQd3FAzFTEdZtA,48314
7
+ appmesh/keycloak.py,sha256=BJZ35FPO0C2wDeDhCcd6cE4ba9BF4UZ-4o5QelmbGHg,8036
8
8
  appmesh/tcp_client.py,sha256=RkHl5s8jE333BJOgxJqJ_fvjbdRQza7ciV49vLT6YO4,10923
9
9
  appmesh/tcp_messages.py,sha256=w1Kehz_aX4X2CYAUsy9mFVJRhxnLQwwc6L58W4YkQqs,969
10
- appmesh/tcp_transport.py,sha256=UMGby2oKV4k7lyXZUMSOe2Je34fb1w7nTkxEpatKLKg,7256
11
- appmesh-1.4.7.dist-info/METADATA,sha256=Tinrv6mkVtpVQi-ZZm_rTpCWLu-NoTEUvL-dBAQVaZg,11663
12
- appmesh-1.4.7.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
13
- appmesh-1.4.7.dist-info/top_level.txt,sha256=-y0MNQOGJxUzLdHZ6E_Rfv5_LNCkV-GTmOBME_b6pg8,8
14
- appmesh-1.4.7.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.2)
2
+ Generator: setuptools (76.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5