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 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
- DEFAULT_TOKEN_EXPIRE_SECONDS = "P1W" # default 7 day(s)
25
- DEFAULT_RUN_APP_TIMEOUT_SECONDS = "P2D" # 2 days
26
- DEFAULT_RUN_APP_LIFECYCLE_SECONDS = "P2DT12H" # 2.5 days
24
+ DURATION_ONE_WEEK_ISO = "P1W"
25
+ DURATION_TWO_DAYS_ISO = "P2D"
26
+ DURATION_TWO_DAYS_HALF_ISO = "P2DT12H"
27
27
 
28
- DEFAULT_SSL_CA_PEM_FILE = "/opt/appmesh/ssl/ca.pem"
29
- DEFAULT_SSL_CLIENT_PEM_FILE = "/opt/appmesh/ssl/client.pem"
30
- DEFAULT_SSL_CLIENT_PEM_KEY_FILE = "/opt/appmesh/ssl/client-key.pem"
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
- TCP_CHUNK_BLOCK_SIZE = 16 * 1024 - 256 # target to 16KB
35
- TCP_MESSAGE_HEADER_LENGTH = 4
36
- REST_TEXT_MESSAGE_JSON_KEY = "message"
37
- MESSAGE_ENCODING_UTF8 = "utf-8"
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
- App object present an application in App Mesh
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
- """Application permission definition"""
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
- Application error handling behavior definition object
82
+ Manages application error handling behavior, including exit and control behaviors.
83
83
  """
84
84
 
85
85
  @unique
86
86
  class Action(Enum):
87
- """Application exit behavior definition"""
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
- """default exit behavior [restart,standby,keepalive,remove]"""
99
+ """Default exit behavior, options: 'restart', 'standby', 'keepalive', 'remove'."""
100
100
 
101
- self.control = App._get_native_item(data, "control") if App._get_native_item(data, "control") else dict()
102
- """exit code behavior (e.g, --control 0:restart --control 1:standby), higher priority than default exit behavior"""
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, a: Action) -> None:
105
- """Set error handling behavior while application exit"""
106
- self.exit = a.value
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, a: Action) -> None:
109
- """Set error handling behavior while application exit with specific return code"""
110
- self.control[str(control_code)] = a.value
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
- Application avialable day time definition object
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
- """daily start time (e.g., '09:00:00+08')"""
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
- """daily end time (e.g., '20:00:00+08')"""
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 day hour range"""
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
- Application cgroup limitation definition object
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 (relative weight)"""
142
+ """CPU shares, relative weight of CPU usage."""
143
143
 
144
144
  self.memory_mb = App._get_int_item(data, "memory_mb")
145
- """physical memory limit in MByte"""
145
+ """Physical memory limit in MB."""
146
146
 
147
147
  self.memory_virt_mb = App._get_int_item(data, "memory_virt_mb")
148
- """virtual memory limit in MByte"""
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 = dict()
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
- if data and "env" in data:
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
- # readonly attributes
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
- """Set avialable time window"""
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, k: str, v: str, secure: bool = False) -> None:
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
- """Set application permission"""
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
- """serialize with JSON format"""
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"] = copy.deepcopy(self.behavior.__dict__)
277
- output["daily_limitation"] = copy.deepcopy(self.daily_limitation.__dict__)
278
- output["resource_limit"] = copy.deepcopy(self.resource_limit.__dict__)
279
-
280
- def clean_empty_item(data, key) -> None:
281
- value = data[key]
282
- if not value:
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
- elif isinstance(value, dict) and key != "metadata":
285
- for k in list(value):
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
- """App output information"""
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
- """HTTP response text"""
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 (int or None)"""
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
- """Process exit code (int or None)"""
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
- Application run object indicate to a remote run from run_async()
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 name"""
291
+ """Name of the application associated with this run."""
292
+
321
293
  self.proc_uid = process_id
322
- """process_uuid from run_async()"""
294
+ """Unique process ID from `run_async()`."""
295
+
323
296
  self._client = client
324
- """AppMeshClient object"""
297
+ """Instance of `AppMeshClient` used to manage this application run."""
298
+
325
299
  self._forwarding_host = client.forwarding_host
326
- """forward host indicates the target server for this app run"""
300
+ """Target server for the application run, used for forwarding."""
327
301
 
328
302
  @contextmanager
329
303
  def forwarding_host(self):
330
- """context manager for forward host override to self._client"""
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 an async run to be finished
313
+ """Wait for the asynchronous run to complete.
340
314
 
341
315
  Args:
342
- stdout_print (bool, optional): print remote stdout to local or not.
343
- timeout (int, optional): wait max timeout seconds and return if not finished, 0 means wait until finished
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: return exit code if process finished, return None for timeout or exception.
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=DEFAULT_SSL_CA_PEM_FILE if os.path.exists(DEFAULT_SSL_CA_PEM_FILE) else False,
444
- rest_ssl_client_cert=(DEFAULT_SSL_CLIENT_PEM_FILE, DEFAULT_SSL_CLIENT_PEM_KEY_FILE) if os.path.exists(DEFAULT_SSL_CLIENT_PEM_FILE) else None,
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
- """Construct an App Mesh client object
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 string.
452
- rest_ssl_verify (str, optional): (optional) SSL CA certification. Either a boolean, in which case it controls whether we verify
453
- the server's TLS certificate, or a string, in which case it must be a path to a CA bundle to use. Defaults to ``True``.
454
- rest_ssl_client_cert (tuple, optional): SSL client certificate and key pair. If String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair.
455
- rest_timeout (tuple, optional): HTTP timeout, Defaults to 60 seconds for connect timeout and 300 seconds for read timeout
456
- jwt_token (str, optional): JWT token, provide correct token is same with login() & authentication().
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=DEFAULT_TOKEN_EXPIRE_SECONDS) -> str:
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=DEFAULT_TOKEN_EXPIRE_SECONDS) -> str:
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=DEFAULT_RUN_APP_TIMEOUT_SECONDS,
1309
- life_cycle_seconds=DEFAULT_RUN_APP_LIFECYCLE_SECONDS,
1310
- ):
1311
- """Asyncrized run a command remotely, 'name' attribute in app_json dict used to run an existing application
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): application object.
1316
- max_time_seconds (int | str, optional): max run time for the remote process, support ISO 8601 durations (e.g., 'P1Y2M3DT4H5M6S' 'P5W').
1317
- life_cycle_seconds (int | str, optional): max lifecycle time for the remote process. support ISO 8601 durations (e.g., 'P1Y2M3DT4H5M6S' 'P5W').
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
- str: app_name, new application name for this run
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={"timeout": self._parse_duration(max_time_seconds), "lifecycle": self._parse_duration(life_cycle_seconds)},
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=DEFAULT_RUN_APP_TIMEOUT_SECONDS,
1372
- life_cycle_seconds=DEFAULT_RUN_APP_LIFECYCLE_SECONDS,
1373
- ) -> Tuple[int, str]:
1374
- """Block run a command remotely, 'name' attribute in app_json dict used to run an existing application
1375
- The synchronized run will block the process until the remote run is finished then return the result from HTTP response
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): application object.
1379
- stdout_print (bool, optional): whether print remote stdout to local or not. Defaults to True.
1380
- max_time_seconds (int | str, optional): max run time for the remote process. support ISO 8601 durations (e.g., 'P1Y2M3DT4H5M6S' 'P5W').
1381
- life_cycle_seconds (int | str, optional): max lifecycle time for the remote process. support ISO 8601 durations (e.g., 'P1Y2M3DT4H5M6S' 'P5W').
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: process exit code, return None if no exit code.
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={"timeout": self._parse_duration(max_time_seconds), "lifecycle": self._parse_duration(life_cycle_seconds)},
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=DEFAULT_SSL_CA_PEM_FILE if os.path.exists(DEFAULT_SSL_CA_PEM_FILE) else False,
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): (optional) SSL CA certification. Either a boolean, in which case it controls whether we verify
1490
- the server's TLS certificate, or a string, in which case it must be a path to a CA bundle to use. Defaults to ``True``.
1491
- rest_ssl_client_cert (tuple, optional): SSL client certificate and key pair . If String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair.
1492
- jwt_token (str, optional): JWT token, provide correct token is same with login() & authentication().
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 (tuple, optional): TCP connect 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
- # Add custom CA certificate file
1521
- try:
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 recieved
1577
+ length (bytes): data length to be received
1560
1578
 
1561
1579
  Raises:
1562
- EOFError: _description_
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
- appmesh_requst = RequestMsg()
1648
+ appmesh_request = RequestMsg()
1631
1649
  if super().jwt_token:
1632
- appmesh_requst.headers["Authorization"] = "Bearer " + super().jwt_token
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
- appmesh_requst.headers[HTTP_HEADER_KEY_USER_AGENT] = HTTP_USER_AGENT_TCP
1636
- appmesh_requst.uuid = str(uuid.uuid1())
1637
- appmesh_requst.http_method = method.value
1638
- appmesh_requst.request_uri = path
1639
- appmesh_requst.client_addr = socket.gethostname()
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
- appmesh_requst.body = bytes(json.dumps(body, indent=2), MESSAGE_ENCODING_UTF8)
1660
+ appmesh_request.body = bytes(json.dumps(body, indent=2), ENCODING_UTF8)
1643
1661
  elif isinstance(body, str):
1644
- appmesh_requst.body = bytes(body, MESSAGE_ENCODING_UTF8)
1662
+ appmesh_request.body = bytes(body, ENCODING_UTF8)
1645
1663
  elif isinstance(body, bytes):
1646
- appmesh_requst.body = body
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
- appmesh_requst.headers[k] = v
1669
+ appmesh_request.headers[k] = v
1652
1670
  if query:
1653
1671
  for k, v in query.items():
1654
- appmesh_requst.querys[k] = v
1655
- data = appmesh_requst.serialize()
1656
- self.__socket_client.sendall(len(data).to_bytes(TCP_MESSAGE_HEADER_LENGTH, byteorder="big", signed=False))
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(TCP_MESSAGE_HEADER_LENGTH), byteorder="big", signed=False))
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
- http_resp = requests.Response()
1668
- http_resp.status_code = appmesh_resp.http_status
1669
- http_resp._content = appmesh_resp.body.encode("utf8")
1670
- http_resp.headers = appmesh_resp.headers
1671
- http_resp.encoding = MESSAGE_ENCODING_UTF8
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
- http_resp.headers["Content-Type"] = appmesh_resp.body_msg_type
1674
- return http_resp
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(TCP_MESSAGE_HEADER_LENGTH), byteorder="big", signed=False)
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(TCP_MESSAGE_HEADER_LENGTH), byteorder="big", signed=False)
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 = TCP_CHUNK_BLOCK_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(TCP_MESSAGE_HEADER_LENGTH, byteorder="big", signed=False))
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(TCP_MESSAGE_HEADER_LENGTH, byteorder="big", signed=False))
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: appmesh
3
- Version: 1.3.5
3
+ Version: 1.3.6
4
4
  Summary: Client SDK for App Mesh
5
5
  Home-page: https://github.com/laoshanxi/app-mesh
6
6
  Author: laoshanxi
@@ -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,,
@@ -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,,