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 +56 -31
- appmesh/keycloak.py +23 -7
- appmesh/tcp_transport.py +58 -31
- {appmesh-1.4.7.dist-info → appmesh-1.4.8.dist-info}/METADATA +1 -1
- {appmesh-1.4.7.dist-info → appmesh-1.4.8.dist-info}/RECORD +7 -7
- {appmesh-1.4.7.dist-info → appmesh-1.4.8.dist-info}/WHEEL +1 -1
- {appmesh-1.4.7.dist-info → appmesh-1.4.8.dist-info}/top_level.txt +0 -0
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
|
-
|
149
|
-
-
|
150
|
-
-
|
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
|
-
-
|
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.
|
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
|
-
|
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,
|
187
|
+
if hasattr(self, "session") and self.session:
|
187
188
|
self.session.close()
|
188
|
-
|
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
|
-
|
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
|
-
|
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.
|
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.
|
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
|
-
|
1187
|
-
|
1188
|
-
|
1189
|
-
|
1190
|
-
|
1191
|
-
|
1192
|
-
|
1193
|
-
|
1194
|
-
|
1195
|
-
|
1196
|
-
|
1197
|
-
|
1198
|
-
|
1199
|
-
|
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
|
-
|
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
|
-
|
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.
|
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.
|
56
|
-
self.userinfo_endpoint = f"{self.
|
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,
|
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.
|
35
|
-
|
36
|
-
|
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
|
-
|
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
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
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
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
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
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
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
|
-
|
183
|
+
try:
|
184
|
+
chunk_size = self._socket.recv_into(view[bytes_received:], length - bytes_received)
|
160
185
|
|
161
|
-
|
162
|
-
|
186
|
+
if chunk_size == 0:
|
187
|
+
raise EOFError("Connection closed by peer")
|
163
188
|
|
164
|
-
|
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
|
@@ -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=
|
7
|
-
appmesh/keycloak.py,sha256=
|
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
|
11
|
-
appmesh-1.4.
|
12
|
-
appmesh-1.4.
|
13
|
-
appmesh-1.4.
|
14
|
-
appmesh-1.4.
|
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,,
|
File without changes
|