appmesh 1.3.4__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,25 +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
- REST_TEXT_MESSAGE_JSON_KEY = "message"
33
- MESSAGE_ENCODING_UTF8 = "utf-8"
34
- TCP_MESSAGE_HEADER_LENGTH = 4
32
+ # TLS-optimized chunk size (slightly less than maximum TLS record size)
33
+ # leaves some room for TLS overhead (like headers) within the 16 KB limit.
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"
35
38
 
36
39
  HTTP_USER_AGENT = "appmesh/python"
37
40
  HTTP_USER_AGENT_TCP = "appmesh/python/tcp"
@@ -43,32 +46,32 @@ HTTP_HEADER_KEY_X_TARGET_HOST = "X-Target-Host"
43
46
 
44
47
  class App(object):
45
48
  """
46
- App object present an application in App Mesh
49
+ Represents an application in App Mesh, including configuration, resource limitations, behaviors, and permissions.
47
50
  """
48
51
 
49
52
  @staticmethod
50
- def _get_str_item(data: dict, key) -> Optional[str]:
53
+ def _get_str_item(data: dict, key: str) -> Optional[str]:
51
54
  """Retrieve a string value from a dictionary by key, if it exists and is a valid string."""
52
55
  return data[key] if (data and key in data and data[key] and isinstance(data[key], str)) else None
53
56
 
54
57
  @staticmethod
55
- def _get_int_item(data: dict, key) -> Optional[int]:
58
+ def _get_int_item(data: dict, key: str) -> Optional[int]:
56
59
  """Retrieve an integer value from a dictionary by key, if it exists and is a valid integer."""
57
60
  return int(data[key]) if (data and key in data and data[key] and isinstance(data[key], int)) else None
58
61
 
59
62
  @staticmethod
60
- def _get_bool_item(data: dict, key) -> Optional[bool]:
61
- """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."""
62
65
  return bool(data[key]) if (data and key in data and data[key]) else None
63
66
 
64
67
  @staticmethod
65
- def _get_native_item(data: dict, key) -> Optional[object]:
68
+ def _get_native_item(data: dict, key: str) -> Optional[object]:
66
69
  """Retrieve a deep copy of a value from a dictionary by key, if it exists."""
67
70
  return copy.deepcopy(data[key]) if (data and key in data and data[key]) else None
68
71
 
69
72
  @unique
70
73
  class Permission(Enum):
71
- """Application permission definition"""
74
+ """Defines application permission levels."""
72
75
 
73
76
  DENY = "1"
74
77
  READ = "2"
@@ -76,12 +79,12 @@ class App(object):
76
79
 
77
80
  class Behavior(object):
78
81
  """
79
- Application error handling behavior definition object
82
+ Manages application error handling behavior, including exit and control behaviors.
80
83
  """
81
84
 
82
85
  @unique
83
86
  class Action(Enum):
84
- """Application exit behavior definition"""
87
+ """Defines actions for application exit behaviors."""
85
88
 
86
89
  RESTART = "restart"
87
90
  STANDBY = "standby"
@@ -93,22 +96,22 @@ class App(object):
93
96
  data = json.loads(data)
94
97
 
95
98
  self.exit = App._get_str_item(data, "exit")
96
- """default exit behavior [restart,standby,keepalive,remove]"""
99
+ """Default exit behavior, options: 'restart', 'standby', 'keepalive', 'remove'."""
97
100
 
98
- self.control = App._get_native_item(data, "control") if App._get_native_item(data, "control") else dict()
99
- """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"""
100
103
 
101
- def set_exit_behavior(self, a: Action) -> None:
102
- """Set error handling behavior while application exit"""
103
- 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
104
107
 
105
- def set_control_behavior(self, control_code: int, a: Action) -> None:
106
- """Set error handling behavior while application exit with specific return code"""
107
- 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
108
111
 
109
112
  class DailyLimitation(object):
110
113
  """
111
- Application avialable day time definition object
114
+ Defines application availability within a daily time range.
112
115
  """
113
116
 
114
117
  def __init__(self, data=None) -> None:
@@ -116,19 +119,19 @@ class App(object):
116
119
  data = json.loads(data)
117
120
 
118
121
  self.daily_start = App._get_int_item(data, "daily_start")
119
- """daily start time (e.g., '09:00:00+08')"""
122
+ """Start time for application availability (e.g., 09:00:00+08)."""
120
123
 
121
124
  self.daily_end = App._get_int_item(data, "daily_end")
122
- """daily end time (e.g., '20:00:00+08')"""
125
+ """End time for application availability (e.g., 20:00:00+08)."""
123
126
 
124
127
  def set_daily_range(self, start: datetime, end: datetime) -> None:
125
- """Set valid day hour range"""
128
+ """Set the valid daily start and end times."""
126
129
  self.daily_start = int(start.timestamp())
127
130
  self.daily_end = int(end.timestamp())
128
131
 
129
132
  class ResourceLimitation(object):
130
133
  """
131
- Application cgroup limitation definition object
134
+ Defines application resource limits, such as CPU and memory usage.
132
135
  """
133
136
 
134
137
  def __init__(self, data=None) -> None:
@@ -136,95 +139,65 @@ class App(object):
136
139
  data = json.loads(data)
137
140
 
138
141
  self.cpu_shares = App._get_int_item(data, "cpu_shares")
139
- """CPU shares (relative weight)"""
142
+ """CPU shares, relative weight of CPU usage."""
140
143
 
141
144
  self.memory_mb = App._get_int_item(data, "memory_mb")
142
- """physical memory limit in MByte"""
145
+ """Physical memory limit in MB."""
143
146
 
144
147
  self.memory_virt_mb = App._get_int_item(data, "memory_virt_mb")
145
- """virtual memory limit in MByte"""
146
-
147
- def __init__(self, data=None):
148
- """Construct an App Mesh Application object
149
-
150
- Args:
151
- data (str | dict | json, optional): application definition data
152
- """
148
+ """Virtual memory limit in MB."""
153
149
 
150
+ def __init__(self, data=None) -> None:
151
+ """Initialize an App instance with optional configuration data."""
154
152
  if isinstance(data, (str, bytes, bytearray)):
155
153
  data = json.loads(data)
156
154
 
157
155
  self.name = App._get_str_item(data, "name")
158
156
  """application name (unique)"""
159
-
160
157
  self.command = App._get_str_item(data, "command")
161
158
  """full command line with arguments"""
162
-
163
159
  self.shell = App._get_bool_item(data, "shell")
164
160
  """use shell mode, cmd can be more shell commands with string format"""
165
-
166
161
  self.session_login = App._get_bool_item(data, "session_login")
167
162
  """app run in session login mode"""
168
-
169
163
  self.description = App._get_str_item(data, "description")
170
164
  """application description string"""
171
-
172
165
  self.metadata = App._get_native_item(data, "metadata")
173
166
  """metadata string/JSON (input for application, pass to process stdin)"""
174
-
175
167
  self.working_dir = App._get_str_item(data, "working_dir")
176
168
  """working directory"""
177
-
178
169
  self.status = App._get_int_item(data, "status")
179
170
  """initial application status (true is enable, false is disabled)"""
180
-
181
171
  self.docker_image = App._get_str_item(data, "docker_image")
182
172
  """docker image which used to run command line (for docker container application)"""
183
-
184
173
  self.stdout_cache_num = App._get_int_item(data, "stdout_cache_num")
185
174
  """stdout file cache number"""
186
-
187
175
  self.start_time = App._get_int_item(data, "start_time")
188
176
  """start date time for app (ISO8601 time format, e.g., '2020-10-11T09:22:05')"""
189
-
190
177
  self.end_time = App._get_int_item(data, "end_time")
191
178
  """end date time for app (ISO8601 time format, e.g., '2020-10-11T10:22:05')"""
192
-
193
179
  self.interval = App._get_int_item(data, "interval")
194
180
  """start interval seconds for short running app, support ISO 8601 durations and cron expression (e.g., 'P1Y2M3DT4H5M6S' 'P5W' '* */5 * * * *')"""
195
-
196
181
  self.cron = App._get_bool_item(data, "cron")
197
182
  """indicate interval parameter use cron expression or not"""
198
-
199
183
  self.daily_limitation = App.DailyLimitation(App._get_native_item(data, "daily_limitation"))
200
-
201
184
  self.retention = App._get_str_item(data, "retention")
202
185
  """extra timeout seconds for stopping current process, support ISO 8601 durations (e.g., 'P1Y2M3DT4H5M6S' 'P5W')."""
203
-
204
186
  self.health_check_cmd = App._get_str_item(data, "health_check_cmd")
205
187
  """health check script command (e.g., sh -x 'curl host:port/health', return 0 is health)"""
206
-
207
188
  self.permission = App._get_int_item(data, "permission")
208
189
  """application user permission, value is 2 bit integer: [group & other], each bit can be deny:1, read:2, write: 3."""
209
190
  self.behavior = App.Behavior(App._get_native_item(data, "behavior"))
210
191
 
211
- self.env = dict()
192
+ self.env = data.get("env", {}) if data else {}
212
193
  """environment variables (e.g., -e env1=value1 -e env2=value2, APP_DOCKER_OPTS is used to input docker run parameters)"""
213
- if data and "env" in data:
214
- for k, v in data["env"].items():
215
- self.env[k] = v
216
-
217
- self.sec_env = dict()
194
+ self.sec_env = data.get("sec_env", {}) if data else {}
218
195
  """security environment variables, encrypt in server side with application owner's cipher"""
219
- if data and "sec_env" in data:
220
- for k, v in data["sec_env"].items():
221
- self.sec_env[k] = v
222
-
223
196
  self.pid = App._get_int_item(data, "pid")
224
197
  """process id used to attach to the running process"""
225
198
  self.resource_limit = App.ResourceLimitation(App._get_native_item(data, "resource_limit"))
226
199
 
227
- # readonly attributes
200
+ # Read-only attributes
228
201
  self.owner = App._get_str_item(data, "owner")
229
202
  """owner name"""
230
203
  self.pstree = App._get_str_item(data, "pstree")
@@ -249,82 +222,86 @@ class App(object):
249
222
  """last exit code"""
250
223
 
251
224
  def set_valid_time(self, start: datetime, end: datetime) -> None:
252
- """Set avialable time window"""
225
+ """Define the valid time window for the application."""
253
226
  self.start_time = int(start.timestamp()) if start else None
254
227
  self.end_time = int(end.timestamp()) if end else None
255
228
 
256
- def set_env(self, k: str, v: str, secure: bool = False) -> None:
257
- """Set environment variable"""
258
- if secure:
259
- self.sec_env[k] = v
260
- else:
261
- 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
262
232
 
263
233
  def set_permission(self, group_user: Permission, others_user: Permission) -> None:
264
- """Set application permission"""
234
+ """Define application permissions based on user roles."""
265
235
  self.permission = int(group_user.value + others_user.value)
266
236
 
267
237
  def __str__(self) -> str:
238
+ """Return a JSON string representation of the application."""
268
239
  return json.dumps(self.json())
269
240
 
270
- def json(self):
271
- """serialize with JSON format"""
241
+ def json(self) -> dict:
242
+ """Convert the application data into a JSON-compatible dictionary, removing empty items."""
272
243
  output = copy.deepcopy(self.__dict__)
273
- output["behavior"] = copy.deepcopy(self.behavior.__dict__)
274
- output["daily_limitation"] = copy.deepcopy(self.daily_limitation.__dict__)
275
- output["resource_limit"] = copy.deepcopy(self.resource_limit.__dict__)
276
-
277
- def clean_empty_item(data, key) -> None:
278
- value = data[key]
279
- 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
280
257
  del data[key]
281
- elif isinstance(value, dict) and key != "metadata":
282
- for k in list(value):
283
- clean_empty_item(value, k)
284
-
285
- for k in list(output):
286
- clean_empty_item(output, k)
287
- for k in list(output):
288
- clean_empty_item(output, k)
258
+
259
+ clean_empty(output)
289
260
  return output
290
261
 
291
262
 
292
263
  class AppOutput(object):
293
- """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
+ """
294
268
 
295
269
  def __init__(self, status_code: HTTPStatus, output: str, out_position: Optional[int], exit_code: Optional[int]) -> None:
296
-
297
270
  self.status_code = status_code
298
- """HTTP status code"""
271
+ """HTTP status code from the `app_output()` API request, indicating the result status."""
299
272
 
300
273
  self.output = output
301
- """HTTP response text"""
274
+ """Captured stdout content of the application as returned by the `app_output()` API."""
302
275
 
303
276
  self.out_position = out_position
304
- """Current read position (int or None)"""
277
+ """Current read position in the application's stdout stream, or `None` if not applicable."""
305
278
 
306
279
  self.exit_code = exit_code
307
- """Process exit code (int or None)"""
280
+ """Exit code of the application, or `None` if the process is still running or hasn't exited."""
308
281
 
309
282
 
310
283
  class AppRun(object):
311
284
  """
312
- 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.
313
287
  """
314
288
 
315
289
  def __init__(self, client, app_name: str, process_id: str):
316
290
  self.app_name = app_name
317
- """application name"""
291
+ """Name of the application associated with this run."""
292
+
318
293
  self.proc_uid = process_id
319
- """process_uuid from run_async()"""
294
+ """Unique process ID from `run_async()`."""
295
+
320
296
  self._client = client
321
- """AppMeshClient object"""
297
+ """Instance of `AppMeshClient` used to manage this application run."""
298
+
322
299
  self._forwarding_host = client.forwarding_host
323
- """forward host indicates the target server for this app run"""
300
+ """Target server for the application run, used for forwarding."""
324
301
 
325
302
  @contextmanager
326
303
  def forwarding_host(self):
327
- """context manager for forward host override to self._client"""
304
+ """Context manager to override the `forwarding_host` for the duration of the run."""
328
305
  original_value = self._client.forwarding_host
329
306
  self._client.forwarding_host = self._forwarding_host
330
307
  try:
@@ -333,14 +310,14 @@ class AppRun(object):
333
310
  self._client.forwarding_host = original_value
334
311
 
335
312
  def wait(self, stdout_print: bool = True, timeout: int = 0) -> int:
336
- """Wait for an async run to be finished
313
+ """Wait for the asynchronous run to complete.
337
314
 
338
315
  Args:
339
- stdout_print (bool, optional): print remote stdout to local or not.
340
- 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`.
341
318
 
342
319
  Returns:
343
- 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.
344
321
  """
345
322
  with self.forwarding_host():
346
323
  return self._client.run_async_wait(self, stdout_print, timeout)
@@ -437,20 +414,31 @@ class AppMeshClient(metaclass=abc.ABCMeta):
437
414
  def __init__(
438
415
  self,
439
416
  rest_url: str = "https://127.0.0.1:6060",
440
- rest_ssl_verify=DEFAULT_SSL_CA_PEM_FILE if os.path.exists(DEFAULT_SSL_CA_PEM_FILE) else False,
441
- 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,
442
419
  rest_timeout=(60, 300),
443
420
  jwt_token=None,
444
421
  ):
445
- """Construct an App Mesh client object
422
+ """Initialize an App Mesh HTTP client for interacting with the App Mesh server via secure HTTPS.
446
423
 
447
424
  Args:
448
- rest_url (str, optional): server URI string.
449
- rest_ssl_verify (str, optional): (optional) SSL CA certification. Either a boolean, in which case it controls whether we verify
450
- 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``.
451
- 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.
452
- rest_timeout (tuple, optional): HTTP timeout, Defaults to 60 seconds for connect timeout and 300 seconds for read timeout
453
- jwt_token (str, optional): JWT token, provide correct token is same with login() & authenticate().
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
+
454
442
  """
455
443
 
456
444
  self.server_url = rest_url
@@ -499,7 +487,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
499
487
  ########################################
500
488
  # Security
501
489
  ########################################
502
- 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:
503
491
  """Login with user name and password
504
492
 
505
493
  Args:
@@ -581,7 +569,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
581
569
  raise Exception(resp.text)
582
570
  return resp.status_code == HTTPStatus.OK
583
571
 
584
- def renew(self, timeout_seconds=DEFAULT_TOKEN_EXPIRE_SECONDS) -> str:
572
+ def renew(self, timeout_seconds=DURATION_ONE_WEEK_ISO) -> str:
585
573
  """Renew current token
586
574
 
587
575
  Args:
@@ -1221,20 +1209,20 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1221
1209
  ########################################
1222
1210
  # File management
1223
1211
  ########################################
1224
- def file_download(self, file_path: str, local_file: str, apply_file_attributes: bool = True) -> None:
1212
+ def file_download(self, remote_file: str, local_file: str, apply_file_attributes: bool = True) -> None:
1225
1213
  """Copy a remote file to local. Optionally, the local file will have the same permission as the remote file.
1226
1214
 
1227
1215
  Args:
1228
- file_path (str): the remote file path.
1216
+ remote_file (str): the remote file path.
1229
1217
  local_file (str): the local file path to be downloaded.
1230
1218
  apply_file_attributes (bool): whether to apply file attributes (permissions, owner, group) to the local file.
1231
1219
  """
1232
- resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/file/download", header={"File-Path": file_path})
1220
+ resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/file/download", header={"File-Path": remote_file})
1233
1221
  resp.raise_for_status()
1234
1222
 
1235
1223
  # Write the file content locally
1236
1224
  with open(local_file, "wb") as fp:
1237
- for chunk in resp.iter_content(chunk_size=8192): # 8 KB
1225
+ for chunk in resp.iter_content(chunk_size=8 * 1024): # 8 KB
1238
1226
  if chunk:
1239
1227
  fp.write(chunk)
1240
1228
 
@@ -1250,7 +1238,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1250
1238
  except PermissionError:
1251
1239
  print(f"Warning: Unable to change owner/group of {local_file}. Operation requires elevated privileges.")
1252
1240
 
1253
- def file_upload(self, local_file: str, file_path: str, apply_file_attributes: bool = True) -> None:
1241
+ def file_upload(self, local_file: str, remote_file: str, apply_file_attributes: bool = True) -> None:
1254
1242
  """Upload a local file to the remote server. Optionally, the remote file will have the same permission as the local file.
1255
1243
 
1256
1244
  Dependency:
@@ -1259,7 +1247,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1259
1247
 
1260
1248
  Args:
1261
1249
  local_file (str): the local file path.
1262
- file_path (str): the target remote file to be uploaded.
1250
+ remote_file (str): the target remote file to be uploaded.
1263
1251
  apply_file_attributes (bool): whether to upload file attributes (permissions, owner, group) along with the file.
1264
1252
  """
1265
1253
  if not os.path.exists(local_file):
@@ -1268,13 +1256,13 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1268
1256
  from requests_toolbelt import MultipartEncoder
1269
1257
 
1270
1258
  with open(file=local_file, mode="rb") as fp:
1271
- encoder = MultipartEncoder(fields={"filename": os.path.basename(file_path), "file": ("filename", fp, "application/octet-stream")})
1272
- header = {"File-Path": file_path, "Content-Type": encoder.content_type}
1259
+ encoder = MultipartEncoder(fields={"filename": os.path.basename(remote_file), "file": ("filename", fp, "application/octet-stream")})
1260
+ header = {"File-Path": remote_file, "Content-Type": encoder.content_type}
1273
1261
 
1274
1262
  # Include file attributes (permissions, owner, group) if requested
1275
1263
  if apply_file_attributes:
1276
1264
  file_stat = os.stat(local_file)
1277
- header["File-Mode"] = str(file_stat.st_mode)
1265
+ header["File-Mode"] = str(file_stat.st_mode & 0o777) # Mask to keep only permission bits
1278
1266
  header["File-User"] = str(file_stat.st_uid)
1279
1267
  header["File-Group"] = str(file_stat.st_gid)
1280
1268
 
@@ -1301,31 +1289,44 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1301
1289
 
1302
1290
  def run_async(
1303
1291
  self,
1304
- app: App,
1305
- max_time_seconds=DEFAULT_RUN_APP_TIMEOUT_SECONDS,
1306
- life_cycle_seconds=DEFAULT_RUN_APP_LIFECYCLE_SECONDS,
1307
- ):
1308
- """Asyncrized run a command remotely, 'name' attribute in app_json dict used to run an existing application
1309
- 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.
1310
1297
 
1311
1298
  Args:
1312
- app (App): application object.
1313
- max_time_seconds (int | str, optional): max run time for the remote process, support ISO 8601 durations (e.g., 'P1Y2M3DT4H5M6S' 'P5W').
1314
- 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`.
1315
1309
 
1316
1310
  Returns:
1317
- str: app_name, new application name for this run
1318
- 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.
1319
1312
  """
1313
+ if isinstance(app, str):
1314
+ app = App({"command": app, "shell": True})
1315
+
1320
1316
  path = "/appmesh/app/run"
1321
1317
  resp = self._request_http(
1322
1318
  AppMeshClient.Method.POST,
1323
1319
  body=app.json(),
1324
1320
  path=path,
1325
- 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
+ },
1326
1325
  )
1327
1326
  if resp.status_code != HTTPStatus.OK:
1328
1327
  raise Exception(resp.text)
1328
+
1329
+ # Return an AppRun object with the application name and process UUID
1329
1330
  return AppRun(self, resp.json()["name"], resp.json()["process_uuid"])
1330
1331
 
1331
1332
  def run_async_wait(self, run: AppRun, stdout_print: bool = True, timeout: int = 0) -> int:
@@ -1363,30 +1364,41 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1363
1364
 
1364
1365
  def run_sync(
1365
1366
  self,
1366
- app: App,
1367
+ app: Union[App, str],
1367
1368
  stdout_print: bool = True,
1368
- max_time_seconds=DEFAULT_RUN_APP_TIMEOUT_SECONDS,
1369
- life_cycle_seconds=DEFAULT_RUN_APP_LIFECYCLE_SECONDS,
1370
- ) -> Tuple[int, str]:
1371
- """Block run a command remotely, 'name' attribute in app_json dict used to run an existing application
1372
- 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.
1373
1376
 
1374
1377
  Args:
1375
- app (App): application object.
1376
- stdout_print (bool, optional): whether print remote stdout to local or not. Defaults to True.
1377
- max_time_seconds (int | str, optional): max run time for the remote process. support ISO 8601 durations (e.g., 'P1Y2M3DT4H5M6S' 'P5W').
1378
- 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.
1379
1386
 
1380
1387
  Returns:
1381
- int: process exit code, return None if no exit code.
1382
- str: stdout text
1388
+ Tuple[Union[int, None], str]: Exit code of the process (None if unavailable) and the stdout text.
1383
1389
  """
1390
+ if isinstance(app, str):
1391
+ app = App({"command": app, "shell": True})
1392
+
1384
1393
  path = "/appmesh/app/syncrun"
1385
1394
  resp = self._request_http(
1386
1395
  AppMeshClient.Method.POST,
1387
1396
  body=app.json(),
1388
1397
  path=path,
1389
- 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
+ },
1390
1402
  )
1391
1403
  exit_code = None
1392
1404
  if resp.status_code == HTTPStatus.OK:
@@ -1396,6 +1408,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1396
1408
  exit_code = int(resp.headers.get("Exit-Code"))
1397
1409
  elif stdout_print:
1398
1410
  print(resp.text)
1411
+
1399
1412
  return exit_code, resp.text
1400
1413
 
1401
1414
  def _request_http(self, method: Method, path: str, query: dict = None, header: dict = None, body=None) -> requests.Response:
@@ -1475,24 +1488,36 @@ class AppMeshClientTCP(AppMeshClient):
1475
1488
 
1476
1489
  def __init__(
1477
1490
  self,
1478
- 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,
1479
1492
  rest_ssl_client_cert=None,
1480
1493
  jwt_token=None,
1481
1494
  tcp_address=("localhost", 6059),
1482
1495
  ):
1483
- """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.
1484
1497
 
1485
1498
  Args:
1486
- rest_ssl_verify (str, optional): (optional) SSL CA certification. Either a boolean, in which case it controls whether we verify
1487
- 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``.
1488
- 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.
1489
- jwt_token (str, optional): JWT token, provide correct token is same with login() & authenticate().
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).
1490
1505
 
1491
- tcp_address (tuple, optional): TCP connect address.
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.
1514
+
1515
+ tcp_address (Tuple[str, int], optional): Address and port for establishing a TCP connection to the server.
1516
+ Defaults to `("localhost", 6059)`.
1492
1517
  """
1493
- super().__init__(rest_ssl_verify=rest_ssl_verify, rest_ssl_client_cert=rest_ssl_client_cert, jwt_token=jwt_token)
1494
1518
  self.tcp_address = tcp_address
1495
1519
  self.__socket_client = None
1520
+ super().__init__(rest_ssl_verify=rest_ssl_verify, rest_ssl_client_cert=rest_ssl_client_cert, jwt_token=jwt_token)
1496
1521
 
1497
1522
  def __del__(self) -> None:
1498
1523
  """De-construction"""
@@ -1501,15 +1526,27 @@ class AppMeshClientTCP(AppMeshClient):
1501
1526
  def __connect_socket(self) -> None:
1502
1527
  """Establish tcp connection"""
1503
1528
  context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
1529
+ # Set minimum TLS version
1504
1530
  if hasattr(context, "minimum_version"):
1505
1531
  context.minimum_version = ssl.TLSVersion.TLSv1_2
1506
1532
  else:
1507
1533
  context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
1508
- if self.ssl_verify:
1509
- context.verify_mode = ssl.CERT_REQUIRED
1510
- if isinstance(self.ssl_verify, str):
1511
- # Load server-side certificate authority (CA) certificates
1512
- context.load_verify_locations(self.ssl_verify)
1534
+ # Configure SSL verification
1535
+ if not self.ssl_verify:
1536
+ context.verify_mode = ssl.CERT_NONE
1537
+ else:
1538
+ context.verify_mode = ssl.CERT_REQUIRED # Require certificate verification
1539
+ context.load_default_certs() # Load system's default CA certificates
1540
+ if isinstance(self.ssl_verify, str):
1541
+ if os.path.isfile(self.ssl_verify):
1542
+ # Load custom CA certificate file
1543
+ context.load_verify_locations(cafile=self.ssl_verify)
1544
+ elif os.path.isdir(self.ssl_verify):
1545
+ # Load CA certificates from directory
1546
+ context.load_verify_locations(capath=self.ssl_verify)
1547
+ else:
1548
+ raise ValueError(f"ssl_verify path '{self.ssl_verify}' is neither a file nor a directory")
1549
+
1513
1550
  if self.ssl_client_cert is not None:
1514
1551
  # Load client-side certificate and private key
1515
1552
  context.load_cert_chain(certfile=self.ssl_client_cert[0], keyfile=self.ssl_client_cert[1])
@@ -1537,10 +1574,10 @@ class AppMeshClientTCP(AppMeshClient):
1537
1574
  """socket recv data with fixed length
1538
1575
  https://stackoverflow.com/questions/64466530/using-a-custom-socket-recvall-function-works-only-if-thread-is-put-to-sleep
1539
1576
  Args:
1540
- length (bytes): data length to be recieved
1577
+ length (bytes): data length to be received
1541
1578
 
1542
1579
  Raises:
1543
- EOFError: _description_
1580
+ EOFError: socket closed unexpectedly
1544
1581
 
1545
1582
  Returns:
1546
1583
  bytes: socket data
@@ -1608,64 +1645,64 @@ class AppMeshClientTCP(AppMeshClient):
1608
1645
  if self.__socket_client is None:
1609
1646
  self.__connect_socket()
1610
1647
 
1611
- appmesh_requst = RequestMsg()
1648
+ appmesh_request = RequestMsg()
1612
1649
  if super().jwt_token:
1613
- appmesh_requst.headers["Authorization"] = "Bearer " + super().jwt_token
1650
+ appmesh_request.headers["Authorization"] = "Bearer " + super().jwt_token
1614
1651
  if super().forwarding_host and len(super().forwarding_host) > 0:
1615
1652
  raise Exception("Not support forward request in TCP mode")
1616
- appmesh_requst.headers[HTTP_HEADER_KEY_USER_AGENT] = HTTP_USER_AGENT_TCP
1617
- appmesh_requst.uuid = str(uuid.uuid1())
1618
- appmesh_requst.http_method = method.value
1619
- appmesh_requst.request_uri = path
1620
- 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()
1621
1658
  if body:
1622
1659
  if isinstance(body, dict) or isinstance(body, list):
1623
- appmesh_requst.body = bytes(json.dumps(body, indent=2), MESSAGE_ENCODING_UTF8)
1660
+ appmesh_request.body = bytes(json.dumps(body, indent=2), ENCODING_UTF8)
1624
1661
  elif isinstance(body, str):
1625
- appmesh_requst.body = bytes(body, MESSAGE_ENCODING_UTF8)
1662
+ appmesh_request.body = bytes(body, ENCODING_UTF8)
1626
1663
  elif isinstance(body, bytes):
1627
- appmesh_requst.body = body
1664
+ appmesh_request.body = body
1628
1665
  else:
1629
1666
  raise Exception(f"UnSupported body type: {type(body)}")
1630
1667
  if header:
1631
1668
  for k, v in header.items():
1632
- appmesh_requst.headers[k] = v
1669
+ appmesh_request.headers[k] = v
1633
1670
  if query:
1634
1671
  for k, v in query.items():
1635
- appmesh_requst.querys[k] = v
1636
- data = appmesh_requst.serialize()
1637
- 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))
1638
1675
  self.__socket_client.sendall(data)
1639
1676
 
1640
1677
  # https://developers.google.com/protocol-buffers/docs/pythontutorial
1641
1678
  # https://stackoverflow.com/questions/33913308/socket-module-how-to-send-integer
1642
1679
  resp_data = bytes()
1643
- 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))
1644
1681
  if resp_data is None or len(resp_data) == 0:
1645
1682
  self.__close_socket()
1646
1683
  raise Exception("socket connection broken")
1647
1684
  appmesh_resp = ResponseMsg().desirialize(resp_data)
1648
- http_resp = requests.Response()
1649
- http_resp.status_code = appmesh_resp.http_status
1650
- http_resp._content = appmesh_resp.body.encode("utf8")
1651
- http_resp.headers = appmesh_resp.headers
1652
- 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
1653
1690
  if appmesh_resp.body_msg_type:
1654
- http_resp.headers["Content-Type"] = appmesh_resp.body_msg_type
1655
- return http_resp
1691
+ response.headers["Content-Type"] = appmesh_resp.body_msg_type
1692
+ return response
1656
1693
 
1657
1694
  ########################################
1658
1695
  # File management
1659
1696
  ########################################
1660
- def file_download(self, file_path: str, local_file: str, apply_file_attributes: bool = True) -> None:
1697
+ def file_download(self, remote_file: str, local_file: str, apply_file_attributes: bool = True) -> None:
1661
1698
  """Copy a remote file to local, the local file will have the same permission as the remote file
1662
1699
 
1663
1700
  Args:
1664
- file_path (str): the remote file path.
1701
+ remote_file (str): the remote file path.
1665
1702
  local_file (str): the local file path to be downloaded.
1666
1703
  apply_file_attributes (bool): whether to apply file attributes (permissions, owner, group) to the local file.
1667
1704
  """
1668
- header = {"File-Path": file_path}
1705
+ header = {"File-Path": remote_file}
1669
1706
  header[HTTP_HEADER_KEY_X_RECV_FILE_SOCKET] = "true"
1670
1707
  resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/file/download", header=header)
1671
1708
 
@@ -1675,14 +1712,14 @@ class AppMeshClientTCP(AppMeshClient):
1675
1712
 
1676
1713
  with open(local_file, "wb") as fp:
1677
1714
  chunk_data = bytes()
1678
- 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)
1679
1716
  while chunk_size > 0:
1680
1717
  chunk_data = self.__recvall(chunk_size)
1681
1718
  if chunk_data is None or len(chunk_data) == 0:
1682
1719
  self.__close_socket()
1683
1720
  raise Exception("socket connection broken")
1684
1721
  fp.write(chunk_data)
1685
- 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)
1686
1723
 
1687
1724
  if apply_file_attributes:
1688
1725
  if "File-Mode" in resp.headers:
@@ -1695,7 +1732,7 @@ class AppMeshClientTCP(AppMeshClient):
1695
1732
  except PermissionError:
1696
1733
  print(f"Warning: Unable to change owner/group of {local_file}. Operation requires elevated privileges.")
1697
1734
 
1698
- def file_upload(self, local_file: str, file_path: str, apply_file_attributes: bool = True) -> None:
1735
+ def file_upload(self, local_file: str, remote_file: str, apply_file_attributes: bool = True) -> None:
1699
1736
  """Upload a local file to the remote server, the remote file will have the same permission as the local file
1700
1737
 
1701
1738
  Dependency:
@@ -1704,19 +1741,19 @@ class AppMeshClientTCP(AppMeshClient):
1704
1741
 
1705
1742
  Args:
1706
1743
  local_file (str): the local file path.
1707
- file_path (str): the target remote file to be uploaded.
1744
+ remote_file (str): the target remote file to be uploaded.
1708
1745
  apply_file_attributes (bool): whether to upload file attributes (permissions, owner, group) along with the file.
1709
1746
  """
1710
1747
  if not os.path.exists(local_file):
1711
1748
  raise FileNotFoundError(f"Local file not found: {local_file}")
1712
1749
 
1713
1750
  with open(file=local_file, mode="rb") as fp:
1714
- header = {"File-Path": file_path, "Content-Type": "text/plain"}
1751
+ header = {"File-Path": remote_file, "Content-Type": "text/plain"}
1715
1752
  header[HTTP_HEADER_KEY_X_SEND_FILE_SOCKET] = "true"
1716
1753
 
1717
1754
  if apply_file_attributes:
1718
1755
  file_stat = os.stat(local_file)
1719
- header["File-Mode"] = str(file_stat.st_mode)
1756
+ header["File-Mode"] = str(file_stat.st_mode & 0o777) # Mask to keep only permission bits
1720
1757
  header["File-User"] = str(file_stat.st_uid)
1721
1758
  header["File-Group"] = str(file_stat.st_gid)
1722
1759
 
@@ -1727,13 +1764,11 @@ class AppMeshClientTCP(AppMeshClient):
1727
1764
  if HTTP_HEADER_KEY_X_SEND_FILE_SOCKET not in resp.headers:
1728
1765
  raise ValueError(f"Server did not respond with socket transfer option: {HTTP_HEADER_KEY_X_SEND_FILE_SOCKET}")
1729
1766
 
1730
- # TLS-optimized chunk size (slightly less than maximum TLS record size)
1731
- # leaves some room for TLS overhead (like headers) within the 16 KB limit.
1732
- chunk_size = 16 * 1024 - 512 # target to 16KB
1767
+ chunk_size = TCP_BLOCK_SIZE
1733
1768
  while True:
1734
1769
  chunk_data = fp.read(chunk_size)
1735
1770
  if not chunk_data:
1736
- 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))
1737
1772
  break
1738
- 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))
1739
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.4
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,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.2.0)
2
+ Generator: setuptools (75.3.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,6 +0,0 @@
1
- appmesh/__init__.py,sha256=xRdXeFHEieRauuJZElbEBASgXG0ZzU1a5_0isAhM7Gw,11
2
- appmesh/appmesh_client.py,sha256=6lnfsR6SAhF66jWBPIVPbC43LPhyAL3amBHxSx30Cu4,68327
3
- appmesh-1.3.4.dist-info/METADATA,sha256=TZjYqpu2P33ZcN7ue8uTAMobwavbud7_zr9Eo2si7NY,11191
4
- appmesh-1.3.4.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
5
- appmesh-1.3.4.dist-info/top_level.txt,sha256=-y0MNQOGJxUzLdHZ6E_Rfv5_LNCkV-GTmOBME_b6pg8,8
6
- appmesh-1.3.4.dist-info/RECORD,,