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 +244 -209
- {appmesh-1.3.4.dist-info → appmesh-1.3.6.dist-info}/METADATA +1 -1
- appmesh-1.3.6.dist-info/RECORD +6 -0
- {appmesh-1.3.4.dist-info → appmesh-1.3.6.dist-info}/WHEEL +1 -1
- appmesh-1.3.4.dist-info/RECORD +0 -6
- {appmesh-1.3.4.dist-info → appmesh-1.3.6.dist-info}/top_level.txt +0 -0
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
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
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
|
-
"""
|
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
|
-
|
82
|
+
Manages application error handling behavior, including exit and control behaviors.
|
80
83
|
"""
|
81
84
|
|
82
85
|
@unique
|
83
86
|
class Action(Enum):
|
84
|
-
"""
|
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
|
-
"""
|
99
|
+
"""Default exit behavior, options: 'restart', 'standby', 'keepalive', 'remove'."""
|
97
100
|
|
98
|
-
self.control = App._get_native_item(data, "control")
|
99
|
-
"""
|
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,
|
102
|
-
"""Set
|
103
|
-
self.exit =
|
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,
|
106
|
-
"""
|
107
|
-
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
|
108
111
|
|
109
112
|
class DailyLimitation(object):
|
110
113
|
"""
|
111
|
-
|
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
|
-
"""
|
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
|
-
"""
|
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
|
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
|
-
|
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
|
142
|
+
"""CPU shares, relative weight of CPU usage."""
|
140
143
|
|
141
144
|
self.memory_mb = App._get_int_item(data, "memory_mb")
|
142
|
-
"""
|
145
|
+
"""Physical memory limit in MB."""
|
143
146
|
|
144
147
|
self.memory_virt_mb = App._get_int_item(data, "memory_virt_mb")
|
145
|
-
"""
|
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 =
|
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
|
-
|
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
|
-
#
|
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
|
-
"""
|
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,
|
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
|
-
"""
|
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
|
-
"""
|
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"] =
|
274
|
-
output["daily_limitation"] =
|
275
|
-
output["resource_limit"] =
|
276
|
-
|
277
|
-
def
|
278
|
-
|
279
|
-
|
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
|
-
|
282
|
-
|
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
|
-
"""
|
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
|
-
"""
|
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
|
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
|
-
"""
|
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
|
-
|
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
|
291
|
+
"""Name of the application associated with this run."""
|
292
|
+
|
318
293
|
self.proc_uid = process_id
|
319
|
-
"""
|
294
|
+
"""Unique process ID from `run_async()`."""
|
295
|
+
|
320
296
|
self._client = client
|
321
|
-
"""AppMeshClient
|
297
|
+
"""Instance of `AppMeshClient` used to manage this application run."""
|
298
|
+
|
322
299
|
self._forwarding_host = client.forwarding_host
|
323
|
-
"""
|
300
|
+
"""Target server for the application run, used for forwarding."""
|
324
301
|
|
325
302
|
@contextmanager
|
326
303
|
def forwarding_host(self):
|
327
|
-
"""
|
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
|
313
|
+
"""Wait for the asynchronous run to complete.
|
337
314
|
|
338
315
|
Args:
|
339
|
-
stdout_print (bool, optional):
|
340
|
-
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`.
|
341
318
|
|
342
319
|
Returns:
|
343
|
-
int:
|
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=
|
441
|
-
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,
|
442
419
|
rest_timeout=(60, 300),
|
443
420
|
jwt_token=None,
|
444
421
|
):
|
445
|
-
"""
|
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
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
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=
|
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=
|
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,
|
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
|
-
|
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":
|
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=
|
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,
|
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
|
-
|
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(
|
1272
|
-
header = {"File-Path":
|
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=
|
1306
|
-
life_cycle_seconds=
|
1307
|
-
):
|
1308
|
-
"""
|
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):
|
1313
|
-
|
1314
|
-
|
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
|
-
|
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={
|
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=
|
1369
|
-
life_cycle_seconds=
|
1370
|
-
) -> Tuple[int, str]:
|
1371
|
-
"""
|
1372
|
-
|
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):
|
1376
|
-
|
1377
|
-
|
1378
|
-
|
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:
|
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={
|
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=
|
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):
|
1487
|
-
|
1488
|
-
|
1489
|
-
|
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
|
-
|
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
|
-
|
1509
|
-
|
1510
|
-
|
1511
|
-
|
1512
|
-
context.
|
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
|
1577
|
+
length (bytes): data length to be received
|
1541
1578
|
|
1542
1579
|
Raises:
|
1543
|
-
EOFError:
|
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
|
-
|
1648
|
+
appmesh_request = RequestMsg()
|
1612
1649
|
if super().jwt_token:
|
1613
|
-
|
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
|
-
|
1617
|
-
|
1618
|
-
|
1619
|
-
|
1620
|
-
|
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
|
-
|
1660
|
+
appmesh_request.body = bytes(json.dumps(body, indent=2), ENCODING_UTF8)
|
1624
1661
|
elif isinstance(body, str):
|
1625
|
-
|
1662
|
+
appmesh_request.body = bytes(body, ENCODING_UTF8)
|
1626
1663
|
elif isinstance(body, bytes):
|
1627
|
-
|
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
|
-
|
1669
|
+
appmesh_request.headers[k] = v
|
1633
1670
|
if query:
|
1634
1671
|
for k, v in query.items():
|
1635
|
-
|
1636
|
-
data =
|
1637
|
-
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))
|
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(
|
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
|
-
|
1649
|
-
|
1650
|
-
|
1651
|
-
|
1652
|
-
|
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
|
-
|
1655
|
-
return
|
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,
|
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
|
-
|
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":
|
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(
|
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(
|
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,
|
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
|
-
|
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":
|
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
|
-
|
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(
|
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(
|
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)
|
@@ -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.4.dist-info/RECORD
DELETED
@@ -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,,
|
File without changes
|