appmesh 1.3.5__py3-none-any.whl → 1.3.7__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
@@ -1,352 +1,26 @@
1
- #!/usr/bin/python3
2
1
  """App Mesh Python SDK"""
2
+
3
+ # pylint: disable=broad-exception-raised,line-too-long,broad-exception-caught,too-many-lines, import-outside-toplevel, protected-access
4
+
5
+ # Standard library imports
3
6
  import abc
4
7
  import base64
5
- from contextlib import contextmanager
6
- import copy
7
8
  import json
8
9
  import os
9
- import socket
10
- import ssl
11
- import uuid
12
-
13
- from enum import Enum, unique
14
10
  from datetime import datetime
11
+ from enum import Enum, unique
15
12
  from http import HTTPStatus
16
- from typing import Optional, Tuple
13
+ from typing import Tuple, Union
17
14
  from urllib import parse
18
15
 
16
+ # Third-party imports
19
17
  import aniso8601
20
18
  import requests
21
19
 
22
- # pylint: disable=broad-exception-raised,line-too-long,broad-exception-caught,too-many-lines, import-outside-toplevel
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
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"
31
-
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_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"
38
-
39
- HTTP_USER_AGENT = "appmesh/python"
40
- HTTP_USER_AGENT_TCP = "appmesh/python/tcp"
41
- HTTP_HEADER_KEY_USER_AGENT = "User-Agent"
42
- HTTP_HEADER_KEY_X_SEND_FILE_SOCKET = "X-Send-File-Socket"
43
- HTTP_HEADER_KEY_X_RECV_FILE_SOCKET = "X-Recv-File-Socket"
44
- HTTP_HEADER_KEY_X_TARGET_HOST = "X-Target-Host"
45
-
46
-
47
- class App(object):
48
- """
49
- App object present an application in App Mesh
50
- """
51
-
52
- @staticmethod
53
- def _get_str_item(data: dict, key) -> Optional[str]:
54
- """Retrieve a string value from a dictionary by key, if it exists and is a valid string."""
55
- return data[key] if (data and key in data and data[key] and isinstance(data[key], str)) else None
56
-
57
- @staticmethod
58
- def _get_int_item(data: dict, key) -> Optional[int]:
59
- """Retrieve an integer value from a dictionary by key, if it exists and is a valid integer."""
60
- return int(data[key]) if (data and key in data and data[key] and isinstance(data[key], int)) else None
61
-
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."""
65
- return bool(data[key]) if (data and key in data and data[key]) else None
66
-
67
- @staticmethod
68
- def _get_native_item(data: dict, key) -> Optional[object]:
69
- """Retrieve a deep copy of a value from a dictionary by key, if it exists."""
70
- return copy.deepcopy(data[key]) if (data and key in data and data[key]) else None
71
-
72
- @unique
73
- class Permission(Enum):
74
- """Application permission definition"""
75
-
76
- DENY = "1"
77
- READ = "2"
78
- WRITE = "3"
79
-
80
- class Behavior(object):
81
- """
82
- Application error handling behavior definition object
83
- """
84
-
85
- @unique
86
- class Action(Enum):
87
- """Application exit behavior definition"""
88
-
89
- RESTART = "restart"
90
- STANDBY = "standby"
91
- KEEPALIVE = "keepalive"
92
- REMOVE = "remove"
93
-
94
- def __init__(self, data=None) -> None:
95
- if isinstance(data, (str, bytes, bytearray)):
96
- data = json.loads(data)
97
-
98
- self.exit = App._get_str_item(data, "exit")
99
- """default exit behavior [restart,standby,keepalive,remove]"""
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"""
103
-
104
- def set_exit_behavior(self, a: Action) -> None:
105
- """Set error handling behavior while application exit"""
106
- self.exit = a.value
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
111
-
112
- class DailyLimitation(object):
113
- """
114
- Application avialable day time definition object
115
- """
116
-
117
- def __init__(self, data=None) -> None:
118
- if isinstance(data, (str, bytes, bytearray)):
119
- data = json.loads(data)
120
-
121
- self.daily_start = App._get_int_item(data, "daily_start")
122
- """daily start time (e.g., '09:00:00+08')"""
123
-
124
- self.daily_end = App._get_int_item(data, "daily_end")
125
- """daily end time (e.g., '20:00:00+08')"""
126
-
127
- def set_daily_range(self, start: datetime, end: datetime) -> None:
128
- """Set valid day hour range"""
129
- self.daily_start = int(start.timestamp())
130
- self.daily_end = int(end.timestamp())
131
-
132
- class ResourceLimitation(object):
133
- """
134
- Application cgroup limitation definition object
135
- """
136
-
137
- def __init__(self, data=None) -> None:
138
- if isinstance(data, (str, bytes, bytearray)):
139
- data = json.loads(data)
140
-
141
- self.cpu_shares = App._get_int_item(data, "cpu_shares")
142
- """CPU shares (relative weight)"""
143
-
144
- self.memory_mb = App._get_int_item(data, "memory_mb")
145
- """physical memory limit in MByte"""
146
-
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
- """
156
-
157
- if isinstance(data, (str, bytes, bytearray)):
158
- data = json.loads(data)
159
-
160
- self.name = App._get_str_item(data, "name")
161
- """application name (unique)"""
162
-
163
- self.command = App._get_str_item(data, "command")
164
- """full command line with arguments"""
165
-
166
- self.shell = App._get_bool_item(data, "shell")
167
- """use shell mode, cmd can be more shell commands with string format"""
168
-
169
- self.session_login = App._get_bool_item(data, "session_login")
170
- """app run in session login mode"""
171
-
172
- self.description = App._get_str_item(data, "description")
173
- """application description string"""
174
-
175
- self.metadata = App._get_native_item(data, "metadata")
176
- """metadata string/JSON (input for application, pass to process stdin)"""
177
-
178
- self.working_dir = App._get_str_item(data, "working_dir")
179
- """working directory"""
180
-
181
- self.status = App._get_int_item(data, "status")
182
- """initial application status (true is enable, false is disabled)"""
183
-
184
- self.docker_image = App._get_str_item(data, "docker_image")
185
- """docker image which used to run command line (for docker container application)"""
186
-
187
- self.stdout_cache_num = App._get_int_item(data, "stdout_cache_num")
188
- """stdout file cache number"""
189
-
190
- self.start_time = App._get_int_item(data, "start_time")
191
- """start date time for app (ISO8601 time format, e.g., '2020-10-11T09:22:05')"""
192
-
193
- self.end_time = App._get_int_item(data, "end_time")
194
- """end date time for app (ISO8601 time format, e.g., '2020-10-11T10:22:05')"""
195
-
196
- self.interval = App._get_int_item(data, "interval")
197
- """start interval seconds for short running app, support ISO 8601 durations and cron expression (e.g., 'P1Y2M3DT4H5M6S' 'P5W' '* */5 * * * *')"""
198
-
199
- self.cron = App._get_bool_item(data, "cron")
200
- """indicate interval parameter use cron expression or not"""
201
-
202
- self.daily_limitation = App.DailyLimitation(App._get_native_item(data, "daily_limitation"))
203
-
204
- self.retention = App._get_str_item(data, "retention")
205
- """extra timeout seconds for stopping current process, support ISO 8601 durations (e.g., 'P1Y2M3DT4H5M6S' 'P5W')."""
206
-
207
- self.health_check_cmd = App._get_str_item(data, "health_check_cmd")
208
- """health check script command (e.g., sh -x 'curl host:port/health', return 0 is health)"""
209
-
210
- self.permission = App._get_int_item(data, "permission")
211
- """application user permission, value is 2 bit integer: [group & other], each bit can be deny:1, read:2, write: 3."""
212
- self.behavior = App.Behavior(App._get_native_item(data, "behavior"))
213
-
214
- self.env = dict()
215
- """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()
221
- """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
- self.pid = App._get_int_item(data, "pid")
227
- """process id used to attach to the running process"""
228
- self.resource_limit = App.ResourceLimitation(App._get_native_item(data, "resource_limit"))
229
-
230
- # readonly attributes
231
- self.owner = App._get_str_item(data, "owner")
232
- """owner name"""
233
- self.pstree = App._get_str_item(data, "pstree")
234
- """process tree"""
235
- self.container_id = App._get_str_item(data, "container_id")
236
- """container id"""
237
- self.memory = App._get_int_item(data, "memory")
238
- """memory usage"""
239
- self.cpu = App._get_int_item(data, "cpu")
240
- """cpu usage"""
241
- self.fd = App._get_int_item(data, "fd")
242
- """file descriptor usage"""
243
- self.last_start_time = App._get_int_item(data, "last_start_time")
244
- """last start time"""
245
- self.last_exit_time = App._get_int_item(data, "last_exit_time")
246
- """last exit time"""
247
- self.health = App._get_int_item(data, "health")
248
- """health status"""
249
- self.version = App._get_int_item(data, "version")
250
- """version number"""
251
- self.return_code = App._get_int_item(data, "return_code")
252
- """last exit code"""
253
-
254
- def set_valid_time(self, start: datetime, end: datetime) -> None:
255
- """Set avialable time window"""
256
- self.start_time = int(start.timestamp()) if start else None
257
- self.end_time = int(end.timestamp()) if end else None
258
-
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
265
-
266
- def set_permission(self, group_user: Permission, others_user: Permission) -> None:
267
- """Set application permission"""
268
- self.permission = int(group_user.value + others_user.value)
269
-
270
- def __str__(self) -> str:
271
- return json.dumps(self.json())
272
-
273
- def json(self):
274
- """serialize with JSON format"""
275
- 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:
283
- 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)
292
- return output
293
-
294
-
295
- class AppOutput(object):
296
- """App output information"""
297
-
298
- def __init__(self, status_code: HTTPStatus, output: str, out_position: Optional[int], exit_code: Optional[int]) -> None:
299
-
300
- self.status_code = status_code
301
- """HTTP status code"""
302
-
303
- self.output = output
304
- """HTTP response text"""
305
-
306
- self.out_position = out_position
307
- """Current read position (int or None)"""
308
-
309
- self.exit_code = exit_code
310
- """Process exit code (int or None)"""
311
-
312
-
313
- class AppRun(object):
314
- """
315
- Application run object indicate to a remote run from run_async()
316
- """
317
-
318
- def __init__(self, client, app_name: str, process_id: str):
319
- self.app_name = app_name
320
- """application name"""
321
- self.proc_uid = process_id
322
- """process_uuid from run_async()"""
323
- self._client = client
324
- """AppMeshClient object"""
325
- self._forwarding_host = client.forwarding_host
326
- """forward host indicates the target server for this app run"""
327
-
328
- @contextmanager
329
- def forwarding_host(self):
330
- """context manager for forward host override to self._client"""
331
- original_value = self._client.forwarding_host
332
- self._client.forwarding_host = self._forwarding_host
333
- try:
334
- yield
335
- finally:
336
- self._client.forwarding_host = original_value
337
-
338
- def wait(self, stdout_print: bool = True, timeout: int = 0) -> int:
339
- """Wait for an async run to be finished
340
-
341
- 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
344
-
345
- Returns:
346
- int: return exit code if process finished, return None for timeout or exception.
347
- """
348
- with self.forwarding_host():
349
- return self._client.run_async_wait(self, stdout_print, timeout)
20
+ # Local application-specific imports
21
+ from .app import App
22
+ from .app_run import AppRun
23
+ from .app_output import AppOutput
350
24
 
351
25
 
352
26
  class AppMeshClient(metaclass=abc.ABCMeta):
@@ -377,6 +51,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
377
51
  token-based authentication and authorization to enforce fine-grained permissions.
378
52
 
379
53
  Methods:
54
+ # Authentication Management
380
55
  - login()
381
56
  - logoff()
382
57
  - authentication()
@@ -385,6 +60,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
385
60
  - totp_secret()
386
61
  - totp_setup()
387
62
 
63
+ # Application Management
388
64
  - app_add()
389
65
  - app_delete()
390
66
  - app_disable()
@@ -394,24 +70,27 @@ class AppMeshClient(metaclass=abc.ABCMeta):
394
70
  - app_view()
395
71
  - app_view_all()
396
72
 
73
+ # Run Application Operations
397
74
  - run_async()
398
75
  - run_async_wait()
399
76
  - run_sync()
400
77
 
78
+ # System Management
79
+ - forwarding_host
401
80
  - config_set()
402
81
  - config_view()
403
82
  - log_level_set()
404
83
  - host_resource()
405
- - forwarding_host
406
84
  - metrics()
407
-
408
85
  - tag_add()
409
86
  - tag_delete()
410
87
  - tag_view()
411
88
 
89
+ # File Management
412
90
  - file_download()
413
91
  - file_upload()
414
92
 
93
+ # User and Role Management
415
94
  - user_add()
416
95
  - user_delete()
417
96
  - user_lock()
@@ -427,6 +106,19 @@ class AppMeshClient(metaclass=abc.ABCMeta):
427
106
  - groups_view()
428
107
  """
429
108
 
109
+ DURATION_ONE_WEEK_ISO = "P1W"
110
+ DURATION_TWO_DAYS_ISO = "P2D"
111
+ DURATION_TWO_DAYS_HALF_ISO = "P2DT12H"
112
+
113
+ DEFAULT_SSL_CA_CERT_PATH = "/opt/appmesh/ssl/ca.pem"
114
+ DEFAULT_SSL_CLIENT_CERT_PATH = "/opt/appmesh/ssl/client.pem"
115
+ DEFAULT_SSL_CLIENT_KEY_PATH = "/opt/appmesh/ssl/client-key.pem"
116
+
117
+ JSON_KEY_MESSAGE = "message"
118
+ HTTP_USER_AGENT = "appmesh/python"
119
+ HTTP_HEADER_KEY_USER_AGENT = "User-Agent"
120
+ HTTP_HEADER_KEY_X_TARGET_HOST = "X-Target-Host"
121
+
430
122
  @unique
431
123
  class Method(Enum):
432
124
  """REST methods"""
@@ -440,20 +132,31 @@ class AppMeshClient(metaclass=abc.ABCMeta):
440
132
  def __init__(
441
133
  self,
442
134
  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,
135
+ rest_ssl_verify=DEFAULT_SSL_CA_CERT_PATH if os.path.exists(DEFAULT_SSL_CA_CERT_PATH) else False,
136
+ 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
137
  rest_timeout=(60, 300),
446
138
  jwt_token=None,
447
139
  ):
448
- """Construct an App Mesh client object
140
+ """Initialize an App Mesh HTTP client for interacting with the App Mesh server via secure HTTPS.
449
141
 
450
142
  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().
143
+ rest_url (str, optional): The server's base URI, including protocol, hostname, and port. Defaults to `"https://127.0.0.1:6060"`.
144
+
145
+ rest_ssl_verify (Union[bool, str], optional): Configures SSL certificate verification for HTTPS requests:
146
+ - `True`: Uses system CA certificates to verify the server's identity.
147
+ - `False`: Disables SSL verification (insecure, use cautiously for development).
148
+ - `str`: Path to a custom CA certificate or directory for verification. This option allows custom CA configuration,
149
+ which may be necessary in environments requiring specific CA chains that differ from the default system CAs.
150
+
151
+ rest_ssl_client_cert (Union[tuple, str], optional): Specifies a client certificate for mutual TLS authentication:
152
+ - If a `str`, provides the path to a PEM file with both client certificate and private key.
153
+ - If a `tuple`, contains two paths as (`cert`, `key`), where `cert` is the certificate file and `key` is the private key file.
154
+
155
+ rest_timeout (tuple, optional): HTTP connection timeouts for API requests, as `(connect_timeout, read_timeout)`.
156
+ The default is `(60, 300)`, where `60` seconds is the maximum time to establish a connection and `300` seconds for the maximum read duration.
157
+
158
+ jwt_token (str, optional): JWT token for API authentication, used in headers to authorize requests where required.
159
+
457
160
  """
458
161
 
459
162
  self.server_url = rest_url
@@ -465,44 +168,97 @@ class AppMeshClient(metaclass=abc.ABCMeta):
465
168
 
466
169
  @property
467
170
  def jwt_token(self) -> str:
468
- """property for jwt_token
171
+ """Get the current JWT (JSON Web Token) used for authentication.
172
+
173
+ This property manages the authentication token used for securing API requests.
174
+ The token is used to authenticate and authorize requests to the service.
469
175
 
470
176
  Returns:
471
- str: _description_
177
+ str: The current JWT token string.
178
+ Returns empty string if no token is set.
179
+
180
+ Notes:
181
+ - The token typically includes claims for identity and permissions
182
+ - Token format: "header.payload.signature"
183
+ - Tokens are time-sensitive and may expire
472
184
  """
473
185
  return self._jwt_token
474
186
 
475
187
  @jwt_token.setter
476
188
  def jwt_token(self, token: str) -> None:
477
- """setter for jwt_token
189
+ """Set the JWT token for authentication.
190
+
191
+ Configure the JWT token used for authenticating requests. The token should be
192
+ a valid JWT issued by a trusted authority.
478
193
 
479
194
  Args:
480
- token (str): _description_
195
+ token (str): JWT token string in standard JWT format
196
+ (e.g., "eyJhbGci...payload...signature")
197
+ Pass empty string to clear the token.
198
+
199
+ Example:
200
+ >>> client.jwt_token = "eyJhbGci..." # Set new token
201
+ >>> client.jwt_token = "" # Clear token
202
+
203
+ Notes:
204
+ Security best practices:
205
+ - Store tokens securely
206
+ - Never log or expose complete tokens
207
+ - Refresh tokens before expiration
208
+ - Validate token format before setting
481
209
  """
482
210
  self._jwt_token = token
483
211
 
484
212
  @property
485
213
  def forwarding_host(self) -> str:
486
- """property for forwarding_host
214
+ """Get the target host address for request forwarding in a cluster setup.
215
+
216
+ This property manages the destination host where requests will be forwarded to
217
+ within a cluster configuration. The host can be specified in two formats:
218
+ 1. hostname/IP only: will use the current service's port
219
+ 2. hostname/IP with port: will use the specified port
487
220
 
488
221
  Returns:
489
- str: forward request to target host (host:port)
222
+ str: The target host address in either format:
223
+ - "hostname" or "IP" (using current service port)
224
+ - "hostname:port" or "IP:port" (using specified port)
225
+ Returns empty string if no forwarding host is set.
226
+
227
+ Notes:
228
+ For proper JWT token sharing across the cluster:
229
+ - All nodes must share the same JWT salt configuration
230
+ - All nodes must use identical JWT issuer settings
231
+ - When port is omitted, current service port will be used
490
232
  """
491
233
  return self._forwarding_host
492
234
 
493
235
  @forwarding_host.setter
494
236
  def forwarding_host(self, host: str) -> None:
495
- """setter for forwarding_host
237
+ """Set the target host address for request forwarding.
238
+
239
+ Configure the destination host where requests should be forwarded to. This is
240
+ used in cluster setups for request routing and load distribution.
496
241
 
497
242
  Args:
498
- host (str): forward request to target host (host:port)
243
+ host (str): Target host address in one of two formats:
244
+ 1. "hostname" or "IP" - will use current service port
245
+ (e.g., "backend-node" or "192.168.1.100")
246
+ 2. "hostname:port" or "IP:port" - will use specified port
247
+ (e.g., "backend-node:6060" or "192.168.1.100:6060")
248
+ Pass empty string to disable forwarding.
249
+
250
+ Examples:
251
+ >>> client.forwarding_host = "backend-node:6060" # Use specific port
252
+ >>> client.forwarding_host = "backend-node" # Use current service port
253
+ >>> client.forwarding_host = None # Disable forwarding
499
254
  """
255
+
500
256
  self._forwarding_host = host
501
257
 
502
258
  ########################################
503
259
  # Security
504
260
  ########################################
505
- def login(self, user_name: str, user_pwd: str, totp_code="", timeout_seconds=DEFAULT_TOKEN_EXPIRE_SECONDS) -> str:
261
+ def login(self, user_name: str, user_pwd: str, totp_code="", timeout_seconds=DURATION_ONE_WEEK_ISO) -> str:
506
262
  """Login with user name and password
507
263
 
508
264
  Args:
@@ -584,7 +340,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
584
340
  raise Exception(resp.text)
585
341
  return resp.status_code == HTTPStatus.OK
586
342
 
587
- def renew(self, timeout_seconds=DEFAULT_TOKEN_EXPIRE_SECONDS) -> str:
343
+ def renew(self, timeout_seconds=DURATION_ONE_WEEK_ISO) -> str:
588
344
  """Renew current token
589
345
 
590
346
  Args:
@@ -1304,31 +1060,44 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1304
1060
 
1305
1061
  def run_async(
1306
1062
  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
1063
+ app: Union[App, str],
1064
+ max_time_seconds: Union[int, str] = DURATION_TWO_DAYS_ISO,
1065
+ life_cycle_seconds: Union[int, str] = DURATION_TWO_DAYS_HALF_ISO,
1066
+ ) -> AppRun:
1067
+ """Run an application asynchronously on a remote system without blocking the API.
1313
1068
 
1314
1069
  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').
1070
+ app (Union[App, str]): An `App` instance or a shell command string.
1071
+ - If `app` is a string, it is treated as a shell command for the remote run,
1072
+ and an `App` instance is created as:
1073
+ `App({"command": "<command_string>", "shell": True})`.
1074
+ - If `app` is an `App` object, providing only the `name` attribute (without
1075
+ a command) will run an existing application; otherwise, it is treated as a new application.
1076
+ max_time_seconds (Union[int, str], optional): Maximum runtime for the remote process.
1077
+ Accepts ISO 8601 duration format (e.g., 'P1Y2M3DT4H5M6S', 'P5W'). Defaults to `P2D`.
1078
+ life_cycle_seconds (Union[int, str], optional): Maximum lifecycle time for the remote process.
1079
+ Accepts ISO 8601 duration format. Defaults to `P2DT12H`.
1318
1080
 
1319
1081
  Returns:
1320
- str: app_name, new application name for this run
1321
- str: process_uuid, process UUID for this run
1082
+ AppRun: An application run object that can be used to monitor and retrieve the result of the run.
1322
1083
  """
1084
+ if isinstance(app, str):
1085
+ app = App({"command": app, "shell": True})
1086
+
1323
1087
  path = "/appmesh/app/run"
1324
1088
  resp = self._request_http(
1325
1089
  AppMeshClient.Method.POST,
1326
1090
  body=app.json(),
1327
1091
  path=path,
1328
- query={"timeout": self._parse_duration(max_time_seconds), "lifecycle": self._parse_duration(life_cycle_seconds)},
1092
+ query={
1093
+ "timeout": self._parse_duration(max_time_seconds),
1094
+ "lifecycle": self._parse_duration(life_cycle_seconds),
1095
+ },
1329
1096
  )
1330
1097
  if resp.status_code != HTTPStatus.OK:
1331
1098
  raise Exception(resp.text)
1099
+
1100
+ # Return an AppRun object with the application name and process UUID
1332
1101
  return AppRun(self, resp.json()["name"], resp.json()["process_uuid"])
1333
1102
 
1334
1103
  def run_async_wait(self, run: AppRun, stdout_print: bool = True, timeout: int = 0) -> int:
@@ -1366,30 +1135,41 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1366
1135
 
1367
1136
  def run_sync(
1368
1137
  self,
1369
- app: App,
1138
+ app: Union[App, str],
1370
1139
  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
1140
+ max_time_seconds: Union[int, str] = DURATION_TWO_DAYS_ISO,
1141
+ life_cycle_seconds: Union[int, str] = DURATION_TWO_DAYS_HALF_ISO,
1142
+ ) -> Tuple[Union[int, None], str]:
1143
+ """Synchronously run an application remotely, blocking until completion, and return the result.
1144
+
1145
+ If 'app' is a string, it is treated as a shell command and converted to an App instance.
1146
+ If 'app' is App object, the name attribute is used to run an existing application if specified.
1376
1147
 
1377
1148
  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').
1149
+ app (Union[App, str]): An App instance or a shell command string.
1150
+ If a string, an App instance is created as:
1151
+ `appmesh_client.App({"command": "<command_string>", "shell": True})`
1152
+ stdout_print (bool, optional): If True, prints the remote stdout locally. Defaults to True.
1153
+ max_time_seconds (Union[int, str], optional): Maximum runtime for the remote process.
1154
+ Supports ISO 8601 duration format (e.g., 'P1Y2M3DT4H5M6S', 'P5W'). Defaults to DEFAULT_RUN_APP_TIMEOUT_SECONDS.
1155
+ life_cycle_seconds (Union[int, str], optional): Maximum lifecycle time for the remote process.
1156
+ Supports ISO 8601 duration format. Defaults to DEFAULT_RUN_APP_LIFECYCLE_SECONDS.
1382
1157
 
1383
1158
  Returns:
1384
- int: process exit code, return None if no exit code.
1385
- str: stdout text
1159
+ Tuple[Union[int, None], str]: Exit code of the process (None if unavailable) and the stdout text.
1386
1160
  """
1161
+ if isinstance(app, str):
1162
+ app = App({"command": app, "shell": True})
1163
+
1387
1164
  path = "/appmesh/app/syncrun"
1388
1165
  resp = self._request_http(
1389
1166
  AppMeshClient.Method.POST,
1390
1167
  body=app.json(),
1391
1168
  path=path,
1392
- query={"timeout": self._parse_duration(max_time_seconds), "lifecycle": self._parse_duration(life_cycle_seconds)},
1169
+ query={
1170
+ "timeout": self._parse_duration(max_time_seconds),
1171
+ "lifecycle": self._parse_duration(life_cycle_seconds),
1172
+ },
1393
1173
  )
1394
1174
  exit_code = None
1395
1175
  if resp.status_code == HTTPStatus.OK:
@@ -1399,6 +1179,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1399
1179
  exit_code = int(resp.headers.get("Exit-Code"))
1400
1180
  elif stdout_print:
1401
1181
  print(resp.text)
1182
+
1402
1183
  return exit_code, resp.text
1403
1184
 
1404
1185
  def _request_http(self, method: Method, path: str, query: dict = None, header: dict = None, body=None) -> requests.Response:
@@ -1421,10 +1202,10 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1421
1202
  header["Authorization"] = "Bearer " + self.jwt_token
1422
1203
  if self.forwarding_host and len(self.forwarding_host) > 0:
1423
1204
  if ":" in self.forwarding_host:
1424
- header[HTTP_HEADER_KEY_X_TARGET_HOST] = self.forwarding_host
1205
+ header[self.HTTP_HEADER_KEY_X_TARGET_HOST] = self.forwarding_host
1425
1206
  else:
1426
- header[HTTP_HEADER_KEY_X_TARGET_HOST] = self.forwarding_host + ":" + str(parse.urlsplit(self.server_url).port)
1427
- header[HTTP_HEADER_KEY_USER_AGENT] = HTTP_USER_AGENT
1207
+ header[self.HTTP_HEADER_KEY_X_TARGET_HOST] = self.forwarding_host + ":" + str(parse.urlsplit(self.server_url).port)
1208
+ header[self.HTTP_HEADER_KEY_USER_AGENT] = self.HTTP_USER_AGENT
1428
1209
 
1429
1210
  if method is AppMeshClient.Method.GET:
1430
1211
  return requests.get(url=rest_url, params=query, headers=header, cert=self.ssl_client_cert, verify=self.ssl_verify, timeout=self.rest_timeout)
@@ -1440,317 +1221,3 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1440
1221
  return requests.put(url=rest_url, params=query, headers=header, json=body, cert=self.ssl_client_cert, verify=self.ssl_verify, timeout=self.rest_timeout)
1441
1222
  else:
1442
1223
  raise Exception("Invalid http method", method)
1443
-
1444
-
1445
- class AppMeshClientTCP(AppMeshClient):
1446
- """
1447
- Client SDK for interacting with the App Mesh service over TCP, with enhanced support for large file transfers.
1448
-
1449
- The `AppMeshClientTCP` class extends the functionality of `AppMeshClient` by offering a TCP-based communication layer
1450
- for the App Mesh REST API. It overrides the file download and upload methods to support large file transfers with
1451
- improved performance, leveraging TCP for lower latency and higher throughput compared to HTTP.
1452
-
1453
- This client is suitable for applications requiring efficient data transfers and high-throughput operations within the
1454
- App Mesh ecosystem, while maintaining compatibility with all other attributes and methods from `AppMeshClient`.
1455
-
1456
- Dependency:
1457
- - Install the required package for message serialization:
1458
- pip3 install msgpack
1459
-
1460
- Usage:
1461
- - Import the client module:
1462
- from appmesh import appmesh_client
1463
-
1464
- Example:
1465
- client = appmesh_client.AppMeshClientTCP()
1466
- client.login("your-name", "your-password")
1467
- client.file_download("/tmp/os-release", "os-release")
1468
-
1469
- Attributes:
1470
- - Inherits all attributes from `AppMeshClient`, including TLS secure connections and JWT-based authentication.
1471
- - Optimized for TCP-based communication to provide better performance for large file transfers.
1472
-
1473
- Methods:
1474
- - file_download()
1475
- - file_upload()
1476
- - Inherits all other methods from `AppMeshClient`, providing a consistent interface for managing applications within App Mesh.
1477
- """
1478
-
1479
- def __init__(
1480
- self,
1481
- rest_ssl_verify=DEFAULT_SSL_CA_PEM_FILE if os.path.exists(DEFAULT_SSL_CA_PEM_FILE) else False,
1482
- rest_ssl_client_cert=None,
1483
- jwt_token=None,
1484
- tcp_address=("localhost", 6059),
1485
- ):
1486
- """Construct an App Mesh client TCP object
1487
-
1488
- 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().
1493
-
1494
- tcp_address (tuple, optional): TCP connect address.
1495
- """
1496
- self.tcp_address = tcp_address
1497
- self.__socket_client = None
1498
- super().__init__(rest_ssl_verify=rest_ssl_verify, rest_ssl_client_cert=rest_ssl_client_cert, jwt_token=jwt_token)
1499
-
1500
- def __del__(self) -> None:
1501
- """De-construction"""
1502
- self.__close_socket()
1503
-
1504
- def __connect_socket(self) -> None:
1505
- """Establish tcp connection"""
1506
- context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
1507
- # Set minimum TLS version
1508
- if hasattr(context, "minimum_version"):
1509
- context.minimum_version = ssl.TLSVersion.TLSv1_2
1510
- else:
1511
- context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
1512
- # Configure SSL verification
1513
- if not self.ssl_verify:
1514
- context.verify_mode = ssl.CERT_NONE
1515
- else:
1516
- context.verify_mode = ssl.CERT_REQUIRED # Require certificate verification
1517
- context.load_default_certs() # Load system's default CA certificates
1518
- if isinstance(self.ssl_verify, str):
1519
- 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)
1526
- elif os.path.isdir(self.ssl_verify):
1527
- # Load CA certificates from directory
1528
- context.load_verify_locations(capath=self.ssl_verify)
1529
- else:
1530
- raise ValueError(f"ssl_verify path '{self.ssl_verify}' is neither a file nor a directory")
1531
-
1532
- if self.ssl_client_cert is not None:
1533
- # Load client-side certificate and private key
1534
- context.load_cert_chain(certfile=self.ssl_client_cert[0], keyfile=self.ssl_client_cert[1])
1535
-
1536
- # Create a TCP socket
1537
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1538
- sock.setblocking(True)
1539
- # Wrap the socket with SSL/TLS
1540
- self.__socket_client = context.wrap_socket(sock, server_hostname=self.tcp_address[0])
1541
- # Connect to the server
1542
- self.__socket_client.connect(self.tcp_address)
1543
- # Disable Nagle's algorithm
1544
- self.__socket_client.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
1545
-
1546
- def __close_socket(self) -> None:
1547
- """Close socket connection"""
1548
- if self.__socket_client:
1549
- try:
1550
- self.__socket_client.close()
1551
- self.__socket_client = None
1552
- except Exception as ex:
1553
- print(ex)
1554
-
1555
- def __recvall(self, length: int) -> bytes:
1556
- """socket recv data with fixed length
1557
- https://stackoverflow.com/questions/64466530/using-a-custom-socket-recvall-function-works-only-if-thread-is-put-to-sleep
1558
- Args:
1559
- length (bytes): data length to be recieved
1560
-
1561
- Raises:
1562
- EOFError: _description_
1563
-
1564
- Returns:
1565
- bytes: socket data
1566
- """
1567
- fragments = []
1568
- while length:
1569
- chunk = self.__socket_client.recv(length)
1570
- if not chunk:
1571
- raise EOFError("socket closed")
1572
- length -= len(chunk)
1573
- fragments.append(chunk)
1574
- return b"".join(fragments)
1575
-
1576
- def _request_http(self, method: AppMeshClient.Method, path: str, query: dict = None, header: dict = None, body=None) -> requests.Response:
1577
- """TCP API
1578
-
1579
- Args:
1580
- method (Method): AppMeshClient.Method.
1581
- path (str): URI patch str.
1582
- query (dict, optional): HTTP query parameters.
1583
- header (dict, optional): HTTP headers.
1584
- body (_type_, optional): object to send in the body of the :class:`Request`.
1585
-
1586
- Returns:
1587
- requests.Response: HTTP response
1588
- """
1589
- import msgpack
1590
-
1591
- class RequestMsg:
1592
- """HTTP request message"""
1593
-
1594
- uuid: str = ""
1595
- request_uri: str = ""
1596
- http_method: str = ""
1597
- client_addr: str = ""
1598
- body: bytes = b""
1599
- headers: dict = {}
1600
- querys: dict = {}
1601
-
1602
- def serialize(self) -> bytes:
1603
- """Serialize request message to bytes"""
1604
- # http://www.cnitblog.com/luckydmz/archive/2019/11/20/91959.html
1605
- self_dict = vars(self)
1606
- self_dict["headers"] = self.headers
1607
- self_dict["querys"] = self.querys
1608
- return msgpack.dumps(self_dict)
1609
-
1610
- class ResponseMsg:
1611
- """HTTP response message"""
1612
-
1613
- uuid: str = ""
1614
- request_uri: str = ""
1615
- http_status: int = 0
1616
- body_msg_type: str = ""
1617
- body: str = ""
1618
- headers: dict = {}
1619
-
1620
- def desirialize(self, buf: bytes):
1621
- """Deserialize response message"""
1622
- dic = msgpack.unpackb(buf)
1623
- for k, v in dic.items():
1624
- setattr(self, k, v)
1625
- return self
1626
-
1627
- if self.__socket_client is None:
1628
- self.__connect_socket()
1629
-
1630
- appmesh_requst = RequestMsg()
1631
- if super().jwt_token:
1632
- appmesh_requst.headers["Authorization"] = "Bearer " + super().jwt_token
1633
- if super().forwarding_host and len(super().forwarding_host) > 0:
1634
- 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()
1640
- if body:
1641
- if isinstance(body, dict) or isinstance(body, list):
1642
- appmesh_requst.body = bytes(json.dumps(body, indent=2), MESSAGE_ENCODING_UTF8)
1643
- elif isinstance(body, str):
1644
- appmesh_requst.body = bytes(body, MESSAGE_ENCODING_UTF8)
1645
- elif isinstance(body, bytes):
1646
- appmesh_requst.body = body
1647
- else:
1648
- raise Exception(f"UnSupported body type: {type(body)}")
1649
- if header:
1650
- for k, v in header.items():
1651
- appmesh_requst.headers[k] = v
1652
- if query:
1653
- 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))
1657
- self.__socket_client.sendall(data)
1658
-
1659
- # https://developers.google.com/protocol-buffers/docs/pythontutorial
1660
- # https://stackoverflow.com/questions/33913308/socket-module-how-to-send-integer
1661
- resp_data = bytes()
1662
- resp_data = self.__recvall(int.from_bytes(self.__recvall(TCP_MESSAGE_HEADER_LENGTH), byteorder="big", signed=False))
1663
- if resp_data is None or len(resp_data) == 0:
1664
- self.__close_socket()
1665
- raise Exception("socket connection broken")
1666
- 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
1672
- if appmesh_resp.body_msg_type:
1673
- http_resp.headers["Content-Type"] = appmesh_resp.body_msg_type
1674
- return http_resp
1675
-
1676
- ########################################
1677
- # File management
1678
- ########################################
1679
- def file_download(self, remote_file: str, local_file: str, apply_file_attributes: bool = True) -> None:
1680
- """Copy a remote file to local, the local file will have the same permission as the remote file
1681
-
1682
- Args:
1683
- remote_file (str): the remote file path.
1684
- local_file (str): the local file path to be downloaded.
1685
- apply_file_attributes (bool): whether to apply file attributes (permissions, owner, group) to the local file.
1686
- """
1687
- header = {"File-Path": remote_file}
1688
- header[HTTP_HEADER_KEY_X_RECV_FILE_SOCKET] = "true"
1689
- resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/file/download", header=header)
1690
-
1691
- resp.raise_for_status()
1692
- if HTTP_HEADER_KEY_X_RECV_FILE_SOCKET not in resp.headers:
1693
- raise ValueError(f"Server did not respond with socket transfer option: {HTTP_HEADER_KEY_X_RECV_FILE_SOCKET}")
1694
-
1695
- with open(local_file, "wb") as fp:
1696
- chunk_data = bytes()
1697
- chunk_size = int.from_bytes(self.__recvall(TCP_MESSAGE_HEADER_LENGTH), byteorder="big", signed=False)
1698
- while chunk_size > 0:
1699
- chunk_data = self.__recvall(chunk_size)
1700
- if chunk_data is None or len(chunk_data) == 0:
1701
- self.__close_socket()
1702
- raise Exception("socket connection broken")
1703
- fp.write(chunk_data)
1704
- chunk_size = int.from_bytes(self.__recvall(TCP_MESSAGE_HEADER_LENGTH), byteorder="big", signed=False)
1705
-
1706
- if apply_file_attributes:
1707
- if "File-Mode" in resp.headers:
1708
- os.chmod(path=local_file, mode=int(resp.headers["File-Mode"]))
1709
- if "File-User" in resp.headers and "File-Group" in resp.headers:
1710
- file_uid = int(resp.headers["File-User"])
1711
- file_gid = int(resp.headers["File-Group"])
1712
- try:
1713
- os.chown(path=local_file, uid=file_uid, gid=file_gid)
1714
- except PermissionError:
1715
- print(f"Warning: Unable to change owner/group of {local_file}. Operation requires elevated privileges.")
1716
-
1717
- def file_upload(self, local_file: str, remote_file: str, apply_file_attributes: bool = True) -> None:
1718
- """Upload a local file to the remote server, the remote file will have the same permission as the local file
1719
-
1720
- Dependency:
1721
- sudo apt install python3-pip
1722
- pip3 install requests_toolbelt
1723
-
1724
- Args:
1725
- local_file (str): the local file path.
1726
- remote_file (str): the target remote file to be uploaded.
1727
- apply_file_attributes (bool): whether to upload file attributes (permissions, owner, group) along with the file.
1728
- """
1729
- if not os.path.exists(local_file):
1730
- raise FileNotFoundError(f"Local file not found: {local_file}")
1731
-
1732
- with open(file=local_file, mode="rb") as fp:
1733
- header = {"File-Path": remote_file, "Content-Type": "text/plain"}
1734
- header[HTTP_HEADER_KEY_X_SEND_FILE_SOCKET] = "true"
1735
-
1736
- if apply_file_attributes:
1737
- file_stat = os.stat(local_file)
1738
- header["File-Mode"] = str(file_stat.st_mode & 0o777) # Mask to keep only permission bits
1739
- header["File-User"] = str(file_stat.st_uid)
1740
- header["File-Group"] = str(file_stat.st_gid)
1741
-
1742
- # https://stackoverflow.com/questions/22567306/python-requests-file-upload
1743
- resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/file/upload", header=header)
1744
-
1745
- resp.raise_for_status()
1746
- if HTTP_HEADER_KEY_X_SEND_FILE_SOCKET not in resp.headers:
1747
- raise ValueError(f"Server did not respond with socket transfer option: {HTTP_HEADER_KEY_X_SEND_FILE_SOCKET}")
1748
-
1749
- chunk_size = TCP_CHUNK_BLOCK_SIZE
1750
- while True:
1751
- chunk_data = fp.read(chunk_size)
1752
- if not chunk_data:
1753
- self.__socket_client.sendall((0).to_bytes(TCP_MESSAGE_HEADER_LENGTH, byteorder="big", signed=False))
1754
- break
1755
- self.__socket_client.sendall(len(chunk_data).to_bytes(TCP_MESSAGE_HEADER_LENGTH, byteorder="big", signed=False))
1756
- self.__socket_client.sendall(chunk_data)