appmesh 1.3.5__py3-none-any.whl → 1.3.6__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/appmesh_client.py +210 -192
- {appmesh-1.3.5.dist-info → appmesh-1.3.6.dist-info}/METADATA +1 -1
- appmesh-1.3.6.dist-info/RECORD +6 -0
- appmesh-1.3.5.dist-info/RECORD +0 -6
- {appmesh-1.3.5.dist-info → appmesh-1.3.6.dist-info}/WHEEL +0 -0
- {appmesh-1.3.5.dist-info → appmesh-1.3.6.dist-info}/top_level.txt +0 -0
appmesh/appmesh_client.py
CHANGED
@@ -13,28 +13,28 @@ import uuid
|
|
13
13
|
from enum import Enum, unique
|
14
14
|
from datetime import datetime
|
15
15
|
from http import HTTPStatus
|
16
|
-
from typing import Optional, Tuple
|
16
|
+
from typing import Optional, Tuple, Union
|
17
17
|
from urllib import parse
|
18
18
|
|
19
19
|
import aniso8601
|
20
20
|
import requests
|
21
21
|
|
22
|
-
# pylint: disable=broad-exception-raised,line-too-long,broad-exception-caught,too-many-lines, import-outside-toplevel
|
22
|
+
# pylint: disable=broad-exception-raised,line-too-long,broad-exception-caught,too-many-lines, import-outside-toplevel, protected-access
|
23
23
|
|
24
|
-
|
25
|
-
|
26
|
-
|
24
|
+
DURATION_ONE_WEEK_ISO = "P1W"
|
25
|
+
DURATION_TWO_DAYS_ISO = "P2D"
|
26
|
+
DURATION_TWO_DAYS_HALF_ISO = "P2DT12H"
|
27
27
|
|
28
|
-
|
29
|
-
|
30
|
-
|
28
|
+
DEFAULT_SSL_CA_CERT_PATH = "/opt/appmesh/ssl/ca.pem"
|
29
|
+
DEFAULT_SSL_CLIENT_CERT_PATH = "/opt/appmesh/ssl/client.pem"
|
30
|
+
DEFAULT_SSL_CLIENT_KEY_PATH = "/opt/appmesh/ssl/client-key.pem"
|
31
31
|
|
32
32
|
# TLS-optimized chunk size (slightly less than maximum TLS record size)
|
33
33
|
# leaves some room for TLS overhead (like headers) within the 16 KB limit.
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
34
|
+
TCP_BLOCK_SIZE = 16 * 1024 - 256 # target to 16KB
|
35
|
+
TCP_HEADER_LENGTH = 4
|
36
|
+
JSON_KEY_MESSAGE = "message"
|
37
|
+
ENCODING_UTF8 = "utf-8"
|
38
38
|
|
39
39
|
HTTP_USER_AGENT = "appmesh/python"
|
40
40
|
HTTP_USER_AGENT_TCP = "appmesh/python/tcp"
|
@@ -46,32 +46,32 @@ HTTP_HEADER_KEY_X_TARGET_HOST = "X-Target-Host"
|
|
46
46
|
|
47
47
|
class App(object):
|
48
48
|
"""
|
49
|
-
|
49
|
+
Represents an application in App Mesh, including configuration, resource limitations, behaviors, and permissions.
|
50
50
|
"""
|
51
51
|
|
52
52
|
@staticmethod
|
53
|
-
def _get_str_item(data: dict, key) -> Optional[str]:
|
53
|
+
def _get_str_item(data: dict, key: str) -> Optional[str]:
|
54
54
|
"""Retrieve a string value from a dictionary by key, if it exists and is a valid string."""
|
55
55
|
return data[key] if (data and key in data and data[key] and isinstance(data[key], str)) else None
|
56
56
|
|
57
57
|
@staticmethod
|
58
|
-
def _get_int_item(data: dict, key) -> Optional[int]:
|
58
|
+
def _get_int_item(data: dict, key: str) -> Optional[int]:
|
59
59
|
"""Retrieve an integer value from a dictionary by key, if it exists and is a valid integer."""
|
60
60
|
return int(data[key]) if (data and key in data and data[key] and isinstance(data[key], int)) else None
|
61
61
|
|
62
62
|
@staticmethod
|
63
|
-
def _get_bool_item(data: dict, key) -> Optional[bool]:
|
64
|
-
"""Retrieve a boolean value from a dictionary by key, if it exists."""
|
63
|
+
def _get_bool_item(data: dict, key: str) -> Optional[bool]:
|
64
|
+
"""Retrieve a boolean value from a dictionary by key, if it exists and is boolean-like."""
|
65
65
|
return bool(data[key]) if (data and key in data and data[key]) else None
|
66
66
|
|
67
67
|
@staticmethod
|
68
|
-
def _get_native_item(data: dict, key) -> Optional[object]:
|
68
|
+
def _get_native_item(data: dict, key: str) -> Optional[object]:
|
69
69
|
"""Retrieve a deep copy of a value from a dictionary by key, if it exists."""
|
70
70
|
return copy.deepcopy(data[key]) if (data and key in data and data[key]) else None
|
71
71
|
|
72
72
|
@unique
|
73
73
|
class Permission(Enum):
|
74
|
-
"""
|
74
|
+
"""Defines application permission levels."""
|
75
75
|
|
76
76
|
DENY = "1"
|
77
77
|
READ = "2"
|
@@ -79,12 +79,12 @@ class App(object):
|
|
79
79
|
|
80
80
|
class Behavior(object):
|
81
81
|
"""
|
82
|
-
|
82
|
+
Manages application error handling behavior, including exit and control behaviors.
|
83
83
|
"""
|
84
84
|
|
85
85
|
@unique
|
86
86
|
class Action(Enum):
|
87
|
-
"""
|
87
|
+
"""Defines actions for application exit behaviors."""
|
88
88
|
|
89
89
|
RESTART = "restart"
|
90
90
|
STANDBY = "standby"
|
@@ -96,22 +96,22 @@ class App(object):
|
|
96
96
|
data = json.loads(data)
|
97
97
|
|
98
98
|
self.exit = App._get_str_item(data, "exit")
|
99
|
-
"""
|
99
|
+
"""Default exit behavior, options: 'restart', 'standby', 'keepalive', 'remove'."""
|
100
100
|
|
101
|
-
self.control = App._get_native_item(data, "control")
|
102
|
-
"""
|
101
|
+
self.control = App._get_native_item(data, "control") or {}
|
102
|
+
"""Exit code specific behavior (e.g, --control 0:restart --control 1:standby), higher priority than default exit behavior"""
|
103
103
|
|
104
|
-
def set_exit_behavior(self,
|
105
|
-
"""Set
|
106
|
-
self.exit =
|
104
|
+
def set_exit_behavior(self, action: Action) -> None:
|
105
|
+
"""Set default behavior for application exit."""
|
106
|
+
self.exit = action.value
|
107
107
|
|
108
|
-
def set_control_behavior(self, control_code: int,
|
109
|
-
"""
|
110
|
-
self.control[str(control_code)] =
|
108
|
+
def set_control_behavior(self, control_code: int, action: Action) -> None:
|
109
|
+
"""Define behavior for specific exit codes."""
|
110
|
+
self.control[str(control_code)] = action.value
|
111
111
|
|
112
112
|
class DailyLimitation(object):
|
113
113
|
"""
|
114
|
-
|
114
|
+
Defines application availability within a daily time range.
|
115
115
|
"""
|
116
116
|
|
117
117
|
def __init__(self, data=None) -> None:
|
@@ -119,19 +119,19 @@ class App(object):
|
|
119
119
|
data = json.loads(data)
|
120
120
|
|
121
121
|
self.daily_start = App._get_int_item(data, "daily_start")
|
122
|
-
"""
|
122
|
+
"""Start time for application availability (e.g., 09:00:00+08)."""
|
123
123
|
|
124
124
|
self.daily_end = App._get_int_item(data, "daily_end")
|
125
|
-
"""
|
125
|
+
"""End time for application availability (e.g., 20:00:00+08)."""
|
126
126
|
|
127
127
|
def set_daily_range(self, start: datetime, end: datetime) -> None:
|
128
|
-
"""Set valid
|
128
|
+
"""Set the valid daily start and end times."""
|
129
129
|
self.daily_start = int(start.timestamp())
|
130
130
|
self.daily_end = int(end.timestamp())
|
131
131
|
|
132
132
|
class ResourceLimitation(object):
|
133
133
|
"""
|
134
|
-
|
134
|
+
Defines application resource limits, such as CPU and memory usage.
|
135
135
|
"""
|
136
136
|
|
137
137
|
def __init__(self, data=None) -> None:
|
@@ -139,95 +139,65 @@ class App(object):
|
|
139
139
|
data = json.loads(data)
|
140
140
|
|
141
141
|
self.cpu_shares = App._get_int_item(data, "cpu_shares")
|
142
|
-
"""CPU shares
|
142
|
+
"""CPU shares, relative weight of CPU usage."""
|
143
143
|
|
144
144
|
self.memory_mb = App._get_int_item(data, "memory_mb")
|
145
|
-
"""
|
145
|
+
"""Physical memory limit in MB."""
|
146
146
|
|
147
147
|
self.memory_virt_mb = App._get_int_item(data, "memory_virt_mb")
|
148
|
-
"""
|
149
|
-
|
150
|
-
def __init__(self, data=None):
|
151
|
-
"""Construct an App Mesh Application object
|
152
|
-
|
153
|
-
Args:
|
154
|
-
data (str | dict | json, optional): application definition data
|
155
|
-
"""
|
148
|
+
"""Virtual memory limit in MB."""
|
156
149
|
|
150
|
+
def __init__(self, data=None) -> None:
|
151
|
+
"""Initialize an App instance with optional configuration data."""
|
157
152
|
if isinstance(data, (str, bytes, bytearray)):
|
158
153
|
data = json.loads(data)
|
159
154
|
|
160
155
|
self.name = App._get_str_item(data, "name")
|
161
156
|
"""application name (unique)"""
|
162
|
-
|
163
157
|
self.command = App._get_str_item(data, "command")
|
164
158
|
"""full command line with arguments"""
|
165
|
-
|
166
159
|
self.shell = App._get_bool_item(data, "shell")
|
167
160
|
"""use shell mode, cmd can be more shell commands with string format"""
|
168
|
-
|
169
161
|
self.session_login = App._get_bool_item(data, "session_login")
|
170
162
|
"""app run in session login mode"""
|
171
|
-
|
172
163
|
self.description = App._get_str_item(data, "description")
|
173
164
|
"""application description string"""
|
174
|
-
|
175
165
|
self.metadata = App._get_native_item(data, "metadata")
|
176
166
|
"""metadata string/JSON (input for application, pass to process stdin)"""
|
177
|
-
|
178
167
|
self.working_dir = App._get_str_item(data, "working_dir")
|
179
168
|
"""working directory"""
|
180
|
-
|
181
169
|
self.status = App._get_int_item(data, "status")
|
182
170
|
"""initial application status (true is enable, false is disabled)"""
|
183
|
-
|
184
171
|
self.docker_image = App._get_str_item(data, "docker_image")
|
185
172
|
"""docker image which used to run command line (for docker container application)"""
|
186
|
-
|
187
173
|
self.stdout_cache_num = App._get_int_item(data, "stdout_cache_num")
|
188
174
|
"""stdout file cache number"""
|
189
|
-
|
190
175
|
self.start_time = App._get_int_item(data, "start_time")
|
191
176
|
"""start date time for app (ISO8601 time format, e.g., '2020-10-11T09:22:05')"""
|
192
|
-
|
193
177
|
self.end_time = App._get_int_item(data, "end_time")
|
194
178
|
"""end date time for app (ISO8601 time format, e.g., '2020-10-11T10:22:05')"""
|
195
|
-
|
196
179
|
self.interval = App._get_int_item(data, "interval")
|
197
180
|
"""start interval seconds for short running app, support ISO 8601 durations and cron expression (e.g., 'P1Y2M3DT4H5M6S' 'P5W' '* */5 * * * *')"""
|
198
|
-
|
199
181
|
self.cron = App._get_bool_item(data, "cron")
|
200
182
|
"""indicate interval parameter use cron expression or not"""
|
201
|
-
|
202
183
|
self.daily_limitation = App.DailyLimitation(App._get_native_item(data, "daily_limitation"))
|
203
|
-
|
204
184
|
self.retention = App._get_str_item(data, "retention")
|
205
185
|
"""extra timeout seconds for stopping current process, support ISO 8601 durations (e.g., 'P1Y2M3DT4H5M6S' 'P5W')."""
|
206
|
-
|
207
186
|
self.health_check_cmd = App._get_str_item(data, "health_check_cmd")
|
208
187
|
"""health check script command (e.g., sh -x 'curl host:port/health', return 0 is health)"""
|
209
|
-
|
210
188
|
self.permission = App._get_int_item(data, "permission")
|
211
189
|
"""application user permission, value is 2 bit integer: [group & other], each bit can be deny:1, read:2, write: 3."""
|
212
190
|
self.behavior = App.Behavior(App._get_native_item(data, "behavior"))
|
213
191
|
|
214
|
-
self.env =
|
192
|
+
self.env = data.get("env", {}) if data else {}
|
215
193
|
"""environment variables (e.g., -e env1=value1 -e env2=value2, APP_DOCKER_OPTS is used to input docker run parameters)"""
|
216
|
-
|
217
|
-
for k, v in data["env"].items():
|
218
|
-
self.env[k] = v
|
219
|
-
|
220
|
-
self.sec_env = dict()
|
194
|
+
self.sec_env = data.get("sec_env", {}) if data else {}
|
221
195
|
"""security environment variables, encrypt in server side with application owner's cipher"""
|
222
|
-
if data and "sec_env" in data:
|
223
|
-
for k, v in data["sec_env"].items():
|
224
|
-
self.sec_env[k] = v
|
225
|
-
|
226
196
|
self.pid = App._get_int_item(data, "pid")
|
227
197
|
"""process id used to attach to the running process"""
|
228
198
|
self.resource_limit = App.ResourceLimitation(App._get_native_item(data, "resource_limit"))
|
229
199
|
|
230
|
-
#
|
200
|
+
# Read-only attributes
|
231
201
|
self.owner = App._get_str_item(data, "owner")
|
232
202
|
"""owner name"""
|
233
203
|
self.pstree = App._get_str_item(data, "pstree")
|
@@ -252,82 +222,86 @@ class App(object):
|
|
252
222
|
"""last exit code"""
|
253
223
|
|
254
224
|
def set_valid_time(self, start: datetime, end: datetime) -> None:
|
255
|
-
"""
|
225
|
+
"""Define the valid time window for the application."""
|
256
226
|
self.start_time = int(start.timestamp()) if start else None
|
257
227
|
self.end_time = int(end.timestamp()) if end else None
|
258
228
|
|
259
|
-
def set_env(self,
|
260
|
-
"""Set environment variable"""
|
261
|
-
if secure
|
262
|
-
self.sec_env[k] = v
|
263
|
-
else:
|
264
|
-
self.env[k] = v
|
229
|
+
def set_env(self, key: str, value: str, secure: bool = False) -> None:
|
230
|
+
"""Set an environment variable, marking it secure if specified."""
|
231
|
+
(self.sec_env if secure else self.env)[key] = value
|
265
232
|
|
266
233
|
def set_permission(self, group_user: Permission, others_user: Permission) -> None:
|
267
|
-
"""
|
234
|
+
"""Define application permissions based on user roles."""
|
268
235
|
self.permission = int(group_user.value + others_user.value)
|
269
236
|
|
270
237
|
def __str__(self) -> str:
|
238
|
+
"""Return a JSON string representation of the application."""
|
271
239
|
return json.dumps(self.json())
|
272
240
|
|
273
|
-
def json(self):
|
274
|
-
"""
|
241
|
+
def json(self) -> dict:
|
242
|
+
"""Convert the application data into a JSON-compatible dictionary, removing empty items."""
|
275
243
|
output = copy.deepcopy(self.__dict__)
|
276
|
-
output["behavior"] =
|
277
|
-
output["daily_limitation"] =
|
278
|
-
output["resource_limit"] =
|
279
|
-
|
280
|
-
def
|
281
|
-
|
282
|
-
|
244
|
+
output["behavior"] = self.behavior.__dict__
|
245
|
+
output["daily_limitation"] = self.daily_limitation.__dict__
|
246
|
+
output["resource_limit"] = self.resource_limit.__dict__
|
247
|
+
|
248
|
+
def clean_empty(data: dict) -> None:
|
249
|
+
keys_to_delete = []
|
250
|
+
for key, value in data.items():
|
251
|
+
if isinstance(value, dict) and key != "metadata":
|
252
|
+
clean_empty(value) # Recursive call (without check user metadata)
|
253
|
+
if data[key] in [None, "", {}]:
|
254
|
+
keys_to_delete.append(key) # Mark keys for deletion
|
255
|
+
|
256
|
+
for key in keys_to_delete: # Delete keys after the loop to avoid modifying dict during iteration
|
283
257
|
del data[key]
|
284
|
-
|
285
|
-
|
286
|
-
clean_empty_item(value, k)
|
287
|
-
|
288
|
-
for k in list(output):
|
289
|
-
clean_empty_item(output, k)
|
290
|
-
for k in list(output):
|
291
|
-
clean_empty_item(output, k)
|
258
|
+
|
259
|
+
clean_empty(output)
|
292
260
|
return output
|
293
261
|
|
294
262
|
|
295
263
|
class AppOutput(object):
|
296
|
-
"""
|
264
|
+
"""
|
265
|
+
Represents the output information returned by the `app_output()` API, including the application's
|
266
|
+
stdout content, current read position, status code, and exit code.
|
267
|
+
"""
|
297
268
|
|
298
269
|
def __init__(self, status_code: HTTPStatus, output: str, out_position: Optional[int], exit_code: Optional[int]) -> None:
|
299
|
-
|
300
270
|
self.status_code = status_code
|
301
|
-
"""HTTP status code"""
|
271
|
+
"""HTTP status code from the `app_output()` API request, indicating the result status."""
|
302
272
|
|
303
273
|
self.output = output
|
304
|
-
"""
|
274
|
+
"""Captured stdout content of the application as returned by the `app_output()` API."""
|
305
275
|
|
306
276
|
self.out_position = out_position
|
307
|
-
"""Current read position
|
277
|
+
"""Current read position in the application's stdout stream, or `None` if not applicable."""
|
308
278
|
|
309
279
|
self.exit_code = exit_code
|
310
|
-
"""
|
280
|
+
"""Exit code of the application, or `None` if the process is still running or hasn't exited."""
|
311
281
|
|
312
282
|
|
313
283
|
class AppRun(object):
|
314
284
|
"""
|
315
|
-
|
285
|
+
Represents an application run object initiated by `run_async()` for monitoring and retrieving
|
286
|
+
the result of a remote application run.
|
316
287
|
"""
|
317
288
|
|
318
289
|
def __init__(self, client, app_name: str, process_id: str):
|
319
290
|
self.app_name = app_name
|
320
|
-
"""application
|
291
|
+
"""Name of the application associated with this run."""
|
292
|
+
|
321
293
|
self.proc_uid = process_id
|
322
|
-
"""
|
294
|
+
"""Unique process ID from `run_async()`."""
|
295
|
+
|
323
296
|
self._client = client
|
324
|
-
"""AppMeshClient
|
297
|
+
"""Instance of `AppMeshClient` used to manage this application run."""
|
298
|
+
|
325
299
|
self._forwarding_host = client.forwarding_host
|
326
|
-
"""
|
300
|
+
"""Target server for the application run, used for forwarding."""
|
327
301
|
|
328
302
|
@contextmanager
|
329
303
|
def forwarding_host(self):
|
330
|
-
"""
|
304
|
+
"""Context manager to override the `forwarding_host` for the duration of the run."""
|
331
305
|
original_value = self._client.forwarding_host
|
332
306
|
self._client.forwarding_host = self._forwarding_host
|
333
307
|
try:
|
@@ -336,14 +310,14 @@ class AppRun(object):
|
|
336
310
|
self._client.forwarding_host = original_value
|
337
311
|
|
338
312
|
def wait(self, stdout_print: bool = True, timeout: int = 0) -> int:
|
339
|
-
"""Wait for
|
313
|
+
"""Wait for the asynchronous run to complete.
|
340
314
|
|
341
315
|
Args:
|
342
|
-
stdout_print (bool, optional):
|
343
|
-
timeout (int, optional):
|
316
|
+
stdout_print (bool, optional): If `True`, prints remote stdout to local. Defaults to `True`.
|
317
|
+
timeout (int, optional): Maximum time to wait in seconds. If `0`, waits until completion. Defaults to `0`.
|
344
318
|
|
345
319
|
Returns:
|
346
|
-
int:
|
320
|
+
int: Exit code if the process finishes successfully. Returns `None` on timeout or exception.
|
347
321
|
"""
|
348
322
|
with self.forwarding_host():
|
349
323
|
return self._client.run_async_wait(self, stdout_print, timeout)
|
@@ -440,20 +414,31 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
440
414
|
def __init__(
|
441
415
|
self,
|
442
416
|
rest_url: str = "https://127.0.0.1:6060",
|
443
|
-
rest_ssl_verify=
|
444
|
-
rest_ssl_client_cert=(
|
417
|
+
rest_ssl_verify=DEFAULT_SSL_CA_CERT_PATH if os.path.exists(DEFAULT_SSL_CA_CERT_PATH) else False,
|
418
|
+
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,
|
445
419
|
rest_timeout=(60, 300),
|
446
420
|
jwt_token=None,
|
447
421
|
):
|
448
|
-
"""
|
422
|
+
"""Initialize an App Mesh HTTP client for interacting with the App Mesh server via secure HTTPS.
|
449
423
|
|
450
424
|
Args:
|
451
|
-
rest_url (str, optional): server URI
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
425
|
+
rest_url (str, optional): The server's base URI, including protocol, hostname, and port. Defaults to `"https://127.0.0.1:6060"`.
|
426
|
+
|
427
|
+
rest_ssl_verify (Union[bool, str], optional): Configures SSL certificate verification for HTTPS requests:
|
428
|
+
- `True`: Uses system CA certificates to verify the server's identity.
|
429
|
+
- `False`: Disables SSL verification (insecure, use cautiously for development).
|
430
|
+
- `str`: Path to a custom CA certificate or directory for verification. This option allows custom CA configuration,
|
431
|
+
which may be necessary in environments requiring specific CA chains that differ from the default system CAs.
|
432
|
+
|
433
|
+
rest_ssl_client_cert (Union[tuple, str], optional): Specifies a client certificate for mutual TLS authentication:
|
434
|
+
- If a `str`, provides the path to a PEM file with both client certificate and private key.
|
435
|
+
- If a `tuple`, contains two paths as (`cert`, `key`), where `cert` is the certificate file and `key` is the private key file.
|
436
|
+
|
437
|
+
rest_timeout (tuple, optional): HTTP connection timeouts for API requests, as `(connect_timeout, read_timeout)`.
|
438
|
+
The default is `(60, 300)`, where `60` seconds is the maximum time to establish a connection and `300` seconds for the maximum read duration.
|
439
|
+
|
440
|
+
jwt_token (str, optional): JWT token for API authentication, used in headers to authorize requests where required.
|
441
|
+
|
457
442
|
"""
|
458
443
|
|
459
444
|
self.server_url = rest_url
|
@@ -502,7 +487,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
502
487
|
########################################
|
503
488
|
# Security
|
504
489
|
########################################
|
505
|
-
def login(self, user_name: str, user_pwd: str, totp_code="", timeout_seconds=
|
490
|
+
def login(self, user_name: str, user_pwd: str, totp_code="", timeout_seconds=DURATION_ONE_WEEK_ISO) -> str:
|
506
491
|
"""Login with user name and password
|
507
492
|
|
508
493
|
Args:
|
@@ -584,7 +569,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
584
569
|
raise Exception(resp.text)
|
585
570
|
return resp.status_code == HTTPStatus.OK
|
586
571
|
|
587
|
-
def renew(self, timeout_seconds=
|
572
|
+
def renew(self, timeout_seconds=DURATION_ONE_WEEK_ISO) -> str:
|
588
573
|
"""Renew current token
|
589
574
|
|
590
575
|
Args:
|
@@ -1304,31 +1289,44 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1304
1289
|
|
1305
1290
|
def run_async(
|
1306
1291
|
self,
|
1307
|
-
app: App,
|
1308
|
-
max_time_seconds=
|
1309
|
-
life_cycle_seconds=
|
1310
|
-
):
|
1311
|
-
"""
|
1312
|
-
Asyncrized run will not block process
|
1292
|
+
app: Union[App, str],
|
1293
|
+
max_time_seconds: Union[int, str] = DURATION_TWO_DAYS_ISO,
|
1294
|
+
life_cycle_seconds: Union[int, str] = DURATION_TWO_DAYS_HALF_ISO,
|
1295
|
+
) -> AppRun:
|
1296
|
+
"""Run an application asynchronously on a remote system without blocking the API.
|
1313
1297
|
|
1314
1298
|
Args:
|
1315
|
-
app (App):
|
1316
|
-
|
1317
|
-
|
1299
|
+
app (Union[App, str]): An `App` instance or a shell command string.
|
1300
|
+
- If `app` is a string, it is treated as a shell command for the remote run,
|
1301
|
+
and an `App` instance is created as:
|
1302
|
+
`App({"command": "<command_string>", "shell": True})`.
|
1303
|
+
- If `app` is an `App` object, providing only the `name` attribute (without
|
1304
|
+
a command) will run an existing application; otherwise, it is treated as a new application.
|
1305
|
+
max_time_seconds (Union[int, str], optional): Maximum runtime for the remote process.
|
1306
|
+
Accepts ISO 8601 duration format (e.g., 'P1Y2M3DT4H5M6S', 'P5W'). Defaults to `P2D`.
|
1307
|
+
life_cycle_seconds (Union[int, str], optional): Maximum lifecycle time for the remote process.
|
1308
|
+
Accepts ISO 8601 duration format. Defaults to `P2DT12H`.
|
1318
1309
|
|
1319
1310
|
Returns:
|
1320
|
-
|
1321
|
-
str: process_uuid, process UUID for this run
|
1311
|
+
AppRun: An application run object that can be used to monitor and retrieve the result of the run.
|
1322
1312
|
"""
|
1313
|
+
if isinstance(app, str):
|
1314
|
+
app = App({"command": app, "shell": True})
|
1315
|
+
|
1323
1316
|
path = "/appmesh/app/run"
|
1324
1317
|
resp = self._request_http(
|
1325
1318
|
AppMeshClient.Method.POST,
|
1326
1319
|
body=app.json(),
|
1327
1320
|
path=path,
|
1328
|
-
query={
|
1321
|
+
query={
|
1322
|
+
"timeout": self._parse_duration(max_time_seconds),
|
1323
|
+
"lifecycle": self._parse_duration(life_cycle_seconds),
|
1324
|
+
},
|
1329
1325
|
)
|
1330
1326
|
if resp.status_code != HTTPStatus.OK:
|
1331
1327
|
raise Exception(resp.text)
|
1328
|
+
|
1329
|
+
# Return an AppRun object with the application name and process UUID
|
1332
1330
|
return AppRun(self, resp.json()["name"], resp.json()["process_uuid"])
|
1333
1331
|
|
1334
1332
|
def run_async_wait(self, run: AppRun, stdout_print: bool = True, timeout: int = 0) -> int:
|
@@ -1366,30 +1364,41 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1366
1364
|
|
1367
1365
|
def run_sync(
|
1368
1366
|
self,
|
1369
|
-
app: App,
|
1367
|
+
app: Union[App, str],
|
1370
1368
|
stdout_print: bool = True,
|
1371
|
-
max_time_seconds=
|
1372
|
-
life_cycle_seconds=
|
1373
|
-
) -> Tuple[int, str]:
|
1374
|
-
"""
|
1375
|
-
|
1369
|
+
max_time_seconds: Union[int, str] = DURATION_TWO_DAYS_ISO,
|
1370
|
+
life_cycle_seconds: Union[int, str] = DURATION_TWO_DAYS_HALF_ISO,
|
1371
|
+
) -> Tuple[Union[int, None], str]:
|
1372
|
+
"""Synchronously run an application remotely, blocking until completion, and return the result.
|
1373
|
+
|
1374
|
+
If 'app' is a string, it is treated as a shell command and converted to an App instance.
|
1375
|
+
If 'app' is App object, the name attribute is used to run an existing application if specified.
|
1376
1376
|
|
1377
1377
|
Args:
|
1378
|
-
app (App):
|
1379
|
-
|
1380
|
-
|
1381
|
-
|
1378
|
+
app (Union[App, str]): An App instance or a shell command string.
|
1379
|
+
If a string, an App instance is created as:
|
1380
|
+
`appmesh_client.App({"command": "<command_string>", "shell": True})`
|
1381
|
+
stdout_print (bool, optional): If True, prints the remote stdout locally. Defaults to True.
|
1382
|
+
max_time_seconds (Union[int, str], optional): Maximum runtime for the remote process.
|
1383
|
+
Supports ISO 8601 duration format (e.g., 'P1Y2M3DT4H5M6S', 'P5W'). Defaults to DEFAULT_RUN_APP_TIMEOUT_SECONDS.
|
1384
|
+
life_cycle_seconds (Union[int, str], optional): Maximum lifecycle time for the remote process.
|
1385
|
+
Supports ISO 8601 duration format. Defaults to DEFAULT_RUN_APP_LIFECYCLE_SECONDS.
|
1382
1386
|
|
1383
1387
|
Returns:
|
1384
|
-
int:
|
1385
|
-
str: stdout text
|
1388
|
+
Tuple[Union[int, None], str]: Exit code of the process (None if unavailable) and the stdout text.
|
1386
1389
|
"""
|
1390
|
+
if isinstance(app, str):
|
1391
|
+
app = App({"command": app, "shell": True})
|
1392
|
+
|
1387
1393
|
path = "/appmesh/app/syncrun"
|
1388
1394
|
resp = self._request_http(
|
1389
1395
|
AppMeshClient.Method.POST,
|
1390
1396
|
body=app.json(),
|
1391
1397
|
path=path,
|
1392
|
-
query={
|
1398
|
+
query={
|
1399
|
+
"timeout": self._parse_duration(max_time_seconds),
|
1400
|
+
"lifecycle": self._parse_duration(life_cycle_seconds),
|
1401
|
+
},
|
1393
1402
|
)
|
1394
1403
|
exit_code = None
|
1395
1404
|
if resp.status_code == HTTPStatus.OK:
|
@@ -1399,6 +1408,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1399
1408
|
exit_code = int(resp.headers.get("Exit-Code"))
|
1400
1409
|
elif stdout_print:
|
1401
1410
|
print(resp.text)
|
1411
|
+
|
1402
1412
|
return exit_code, resp.text
|
1403
1413
|
|
1404
1414
|
def _request_http(self, method: Method, path: str, query: dict = None, header: dict = None, body=None) -> requests.Response:
|
@@ -1478,20 +1488,32 @@ class AppMeshClientTCP(AppMeshClient):
|
|
1478
1488
|
|
1479
1489
|
def __init__(
|
1480
1490
|
self,
|
1481
|
-
rest_ssl_verify=
|
1491
|
+
rest_ssl_verify=DEFAULT_SSL_CA_CERT_PATH if os.path.exists(DEFAULT_SSL_CA_CERT_PATH) else False,
|
1482
1492
|
rest_ssl_client_cert=None,
|
1483
1493
|
jwt_token=None,
|
1484
1494
|
tcp_address=("localhost", 6059),
|
1485
1495
|
):
|
1486
|
-
"""Construct an App Mesh client TCP object
|
1496
|
+
"""Construct an App Mesh client TCP object to communicate securely with an App Mesh server over TLS.
|
1487
1497
|
|
1488
1498
|
Args:
|
1489
|
-
rest_ssl_verify (str, optional):
|
1490
|
-
|
1491
|
-
|
1492
|
-
|
1499
|
+
rest_ssl_verify (Union[bool, str], optional): Specifies SSL certificate verification behavior. Can be:
|
1500
|
+
- `True`: Uses the system’s default CA certificates to verify the server’s identity.
|
1501
|
+
- `False`: Disables SSL certificate verification (insecure, intended for development).
|
1502
|
+
- `str`: Specifies a custom CA bundle or directory for server certificate verification. If a string is provided,
|
1503
|
+
it should either be a file path to a custom CA certificate (CA bundle) or a directory path containing multiple
|
1504
|
+
certificates (CA directory).
|
1505
|
+
|
1506
|
+
**Note**: Unlike HTTP requests, TCP connections cannot automatically retrieve intermediate or public CA certificates.
|
1507
|
+
When `rest_ssl_verify` is a path, it explicitly identifies a CA issuer to ensure certificate validation.
|
1508
|
+
|
1509
|
+
rest_ssl_client_cert (Union[str, Tuple[str, str]], optional): Path to the SSL client certificate and key. If a `str`,
|
1510
|
+
it should be the path to a PEM file containing both the client certificate and private key. If a `tuple`, it should
|
1511
|
+
be a pair of paths: (`cert`, `key`), where `cert` is the client certificate file and `key` is the private key file.
|
1512
|
+
|
1513
|
+
jwt_token (str, optional): JWT token for authentication. Used in methods requiring login and user authorization.
|
1493
1514
|
|
1494
|
-
tcp_address (
|
1515
|
+
tcp_address (Tuple[str, int], optional): Address and port for establishing a TCP connection to the server.
|
1516
|
+
Defaults to `("localhost", 6059)`.
|
1495
1517
|
"""
|
1496
1518
|
self.tcp_address = tcp_address
|
1497
1519
|
self.__socket_client = None
|
@@ -1517,12 +1539,8 @@ class AppMeshClientTCP(AppMeshClient):
|
|
1517
1539
|
context.load_default_certs() # Load system's default CA certificates
|
1518
1540
|
if isinstance(self.ssl_verify, str):
|
1519
1541
|
if os.path.isfile(self.ssl_verify):
|
1520
|
-
#
|
1521
|
-
|
1522
|
-
context.load_verify_locations(cafile=self.ssl_verify)
|
1523
|
-
except ssl.SSLError:
|
1524
|
-
# If loading fails, try using just the default CAs
|
1525
|
-
context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
|
1542
|
+
# Load custom CA certificate file
|
1543
|
+
context.load_verify_locations(cafile=self.ssl_verify)
|
1526
1544
|
elif os.path.isdir(self.ssl_verify):
|
1527
1545
|
# Load CA certificates from directory
|
1528
1546
|
context.load_verify_locations(capath=self.ssl_verify)
|
@@ -1556,10 +1574,10 @@ class AppMeshClientTCP(AppMeshClient):
|
|
1556
1574
|
"""socket recv data with fixed length
|
1557
1575
|
https://stackoverflow.com/questions/64466530/using-a-custom-socket-recvall-function-works-only-if-thread-is-put-to-sleep
|
1558
1576
|
Args:
|
1559
|
-
length (bytes): data length to be
|
1577
|
+
length (bytes): data length to be received
|
1560
1578
|
|
1561
1579
|
Raises:
|
1562
|
-
EOFError:
|
1580
|
+
EOFError: socket closed unexpectedly
|
1563
1581
|
|
1564
1582
|
Returns:
|
1565
1583
|
bytes: socket data
|
@@ -1627,51 +1645,51 @@ class AppMeshClientTCP(AppMeshClient):
|
|
1627
1645
|
if self.__socket_client is None:
|
1628
1646
|
self.__connect_socket()
|
1629
1647
|
|
1630
|
-
|
1648
|
+
appmesh_request = RequestMsg()
|
1631
1649
|
if super().jwt_token:
|
1632
|
-
|
1650
|
+
appmesh_request.headers["Authorization"] = "Bearer " + super().jwt_token
|
1633
1651
|
if super().forwarding_host and len(super().forwarding_host) > 0:
|
1634
1652
|
raise Exception("Not support forward request in TCP mode")
|
1635
|
-
|
1636
|
-
|
1637
|
-
|
1638
|
-
|
1639
|
-
|
1653
|
+
appmesh_request.headers[HTTP_HEADER_KEY_USER_AGENT] = HTTP_USER_AGENT_TCP
|
1654
|
+
appmesh_request.uuid = str(uuid.uuid1())
|
1655
|
+
appmesh_request.http_method = method.value
|
1656
|
+
appmesh_request.request_uri = path
|
1657
|
+
appmesh_request.client_addr = socket.gethostname()
|
1640
1658
|
if body:
|
1641
1659
|
if isinstance(body, dict) or isinstance(body, list):
|
1642
|
-
|
1660
|
+
appmesh_request.body = bytes(json.dumps(body, indent=2), ENCODING_UTF8)
|
1643
1661
|
elif isinstance(body, str):
|
1644
|
-
|
1662
|
+
appmesh_request.body = bytes(body, ENCODING_UTF8)
|
1645
1663
|
elif isinstance(body, bytes):
|
1646
|
-
|
1664
|
+
appmesh_request.body = body
|
1647
1665
|
else:
|
1648
1666
|
raise Exception(f"UnSupported body type: {type(body)}")
|
1649
1667
|
if header:
|
1650
1668
|
for k, v in header.items():
|
1651
|
-
|
1669
|
+
appmesh_request.headers[k] = v
|
1652
1670
|
if query:
|
1653
1671
|
for k, v in query.items():
|
1654
|
-
|
1655
|
-
data =
|
1656
|
-
self.__socket_client.sendall(len(data).to_bytes(
|
1672
|
+
appmesh_request.querys[k] = v
|
1673
|
+
data = appmesh_request.serialize()
|
1674
|
+
self.__socket_client.sendall(len(data).to_bytes(TCP_HEADER_LENGTH, byteorder="big", signed=False))
|
1657
1675
|
self.__socket_client.sendall(data)
|
1658
1676
|
|
1659
1677
|
# https://developers.google.com/protocol-buffers/docs/pythontutorial
|
1660
1678
|
# https://stackoverflow.com/questions/33913308/socket-module-how-to-send-integer
|
1661
1679
|
resp_data = bytes()
|
1662
|
-
resp_data = self.__recvall(int.from_bytes(self.__recvall(
|
1680
|
+
resp_data = self.__recvall(int.from_bytes(self.__recvall(TCP_HEADER_LENGTH), byteorder="big", signed=False))
|
1663
1681
|
if resp_data is None or len(resp_data) == 0:
|
1664
1682
|
self.__close_socket()
|
1665
1683
|
raise Exception("socket connection broken")
|
1666
1684
|
appmesh_resp = ResponseMsg().desirialize(resp_data)
|
1667
|
-
|
1668
|
-
|
1669
|
-
|
1670
|
-
|
1671
|
-
|
1685
|
+
response = requests.Response()
|
1686
|
+
response.status_code = appmesh_resp.http_status
|
1687
|
+
response.encoding = ENCODING_UTF8
|
1688
|
+
response._content = appmesh_resp.body.encode(ENCODING_UTF8)
|
1689
|
+
response.headers = appmesh_resp.headers
|
1672
1690
|
if appmesh_resp.body_msg_type:
|
1673
|
-
|
1674
|
-
return
|
1691
|
+
response.headers["Content-Type"] = appmesh_resp.body_msg_type
|
1692
|
+
return response
|
1675
1693
|
|
1676
1694
|
########################################
|
1677
1695
|
# File management
|
@@ -1694,14 +1712,14 @@ class AppMeshClientTCP(AppMeshClient):
|
|
1694
1712
|
|
1695
1713
|
with open(local_file, "wb") as fp:
|
1696
1714
|
chunk_data = bytes()
|
1697
|
-
chunk_size = int.from_bytes(self.__recvall(
|
1715
|
+
chunk_size = int.from_bytes(self.__recvall(TCP_HEADER_LENGTH), byteorder="big", signed=False)
|
1698
1716
|
while chunk_size > 0:
|
1699
1717
|
chunk_data = self.__recvall(chunk_size)
|
1700
1718
|
if chunk_data is None or len(chunk_data) == 0:
|
1701
1719
|
self.__close_socket()
|
1702
1720
|
raise Exception("socket connection broken")
|
1703
1721
|
fp.write(chunk_data)
|
1704
|
-
chunk_size = int.from_bytes(self.__recvall(
|
1722
|
+
chunk_size = int.from_bytes(self.__recvall(TCP_HEADER_LENGTH), byteorder="big", signed=False)
|
1705
1723
|
|
1706
1724
|
if apply_file_attributes:
|
1707
1725
|
if "File-Mode" in resp.headers:
|
@@ -1746,11 +1764,11 @@ class AppMeshClientTCP(AppMeshClient):
|
|
1746
1764
|
if HTTP_HEADER_KEY_X_SEND_FILE_SOCKET not in resp.headers:
|
1747
1765
|
raise ValueError(f"Server did not respond with socket transfer option: {HTTP_HEADER_KEY_X_SEND_FILE_SOCKET}")
|
1748
1766
|
|
1749
|
-
chunk_size =
|
1767
|
+
chunk_size = TCP_BLOCK_SIZE
|
1750
1768
|
while True:
|
1751
1769
|
chunk_data = fp.read(chunk_size)
|
1752
1770
|
if not chunk_data:
|
1753
|
-
self.__socket_client.sendall((0).to_bytes(
|
1771
|
+
self.__socket_client.sendall((0).to_bytes(TCP_HEADER_LENGTH, byteorder="big", signed=False))
|
1754
1772
|
break
|
1755
|
-
self.__socket_client.sendall(len(chunk_data).to_bytes(
|
1773
|
+
self.__socket_client.sendall(len(chunk_data).to_bytes(TCP_HEADER_LENGTH, byteorder="big", signed=False))
|
1756
1774
|
self.__socket_client.sendall(chunk_data)
|
@@ -0,0 +1,6 @@
|
|
1
|
+
appmesh/__init__.py,sha256=xRdXeFHEieRauuJZElbEBASgXG0ZzU1a5_0isAhM7Gw,11
|
2
|
+
appmesh/appmesh_client.py,sha256=NgwX6BjOUIMFZbKrtw3JVs24QbOf65NqTs1rWsgRmFM,72900
|
3
|
+
appmesh-1.3.6.dist-info/METADATA,sha256=QUa1JXHj03Bbs-kmuzuEfGDTIJyYPmMNul87qtxyu4M,11191
|
4
|
+
appmesh-1.3.6.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
|
5
|
+
appmesh-1.3.6.dist-info/top_level.txt,sha256=-y0MNQOGJxUzLdHZ6E_Rfv5_LNCkV-GTmOBME_b6pg8,8
|
6
|
+
appmesh-1.3.6.dist-info/RECORD,,
|
appmesh-1.3.5.dist-info/RECORD
DELETED
@@ -1,6 +0,0 @@
|
|
1
|
-
appmesh/__init__.py,sha256=xRdXeFHEieRauuJZElbEBASgXG0ZzU1a5_0isAhM7Gw,11
|
2
|
-
appmesh/appmesh_client.py,sha256=bduPEDpI36XQyK_U9y3EvpZvO9CbpDeAB5402yf9qhU,69329
|
3
|
-
appmesh-1.3.5.dist-info/METADATA,sha256=VSkQis_Azjvw2FwjrK8qjOvYKw2WRNB37J2tYm_KEbw,11191
|
4
|
-
appmesh-1.3.5.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
|
5
|
-
appmesh-1.3.5.dist-info/top_level.txt,sha256=-y0MNQOGJxUzLdHZ6E_Rfv5_LNCkV-GTmOBME_b6pg8,8
|
6
|
-
appmesh-1.3.5.dist-info/RECORD,,
|
File without changes
|
File without changes
|