appmesh 1.3.6__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.
@@ -0,0 +1,362 @@
1
+ """Application output information"""
2
+
3
+ # pylint: disable=line-too-long,broad-exception-raised, ,broad-exception-caught,import-outside-toplevel,protected-access
4
+
5
+ # Standard library imports
6
+ import json
7
+ import os
8
+ import socket
9
+ import ssl
10
+ import uuid
11
+
12
+ # Third-party imports
13
+ import requests
14
+ import msgpack
15
+
16
+ # Local application-specific imports
17
+ from .appmesh_client import AppMeshClient
18
+
19
+
20
+ class AppMeshClientTCP(AppMeshClient):
21
+ """
22
+ Client SDK for interacting with the App Mesh service over TCP, with enhanced support for large file transfers.
23
+
24
+ The `AppMeshClientTCP` class extends the functionality of `AppMeshClient` by offering a TCP-based communication layer
25
+ for the App Mesh REST API. It overrides the file download and upload methods to support large file transfers with
26
+ improved performance, leveraging TCP for lower latency and higher throughput compared to HTTP.
27
+
28
+ This client is suitable for applications requiring efficient data transfers and high-throughput operations within the
29
+ App Mesh ecosystem, while maintaining compatibility with all other attributes and methods from `AppMeshClient`.
30
+
31
+ Dependency:
32
+ - Install the required package for message serialization:
33
+ pip3 install msgpack
34
+
35
+ Usage:
36
+ - Import the client module:
37
+ from appmesh import appmesh_client
38
+
39
+ Example:
40
+ client = appmesh_client.AppMeshClientTCP()
41
+ client.login("your-name", "your-password")
42
+ client.file_download("/tmp/os-release", "os-release")
43
+
44
+ Attributes:
45
+ - Inherits all attributes from `AppMeshClient`, including TLS secure connections and JWT-based authentication.
46
+ - Optimized for TCP-based communication to provide better performance for large file transfers.
47
+
48
+ Methods:
49
+ - file_download()
50
+ - file_upload()
51
+ - Inherits all other methods from `AppMeshClient`, providing a consistent interface for managing applications within App Mesh.
52
+ """
53
+
54
+ TCP_BLOCK_SIZE = 16 * 1024 - 128 # TLS-optimized chunk size, leaves some room for TLS overhead (like headers) within the 16 KB limit.
55
+ TCP_HEADER_LENGTH = 4
56
+ ENCODING_UTF8 = "utf-8"
57
+ HTTP_USER_AGENT_TCP = "appmesh/python/tcp"
58
+ HTTP_HEADER_KEY_X_SEND_FILE_SOCKET = "X-Send-File-Socket"
59
+ HTTP_HEADER_KEY_X_RECV_FILE_SOCKET = "X-Recv-File-Socket"
60
+
61
+ def __init__(
62
+ self,
63
+ rest_ssl_verify=AppMeshClient.DEFAULT_SSL_CA_CERT_PATH if os.path.exists(AppMeshClient.DEFAULT_SSL_CA_CERT_PATH) else False,
64
+ rest_ssl_client_cert=None,
65
+ jwt_token=None,
66
+ tcp_address=("localhost", 6059),
67
+ ):
68
+ """Construct an App Mesh client TCP object to communicate securely with an App Mesh server over TLS.
69
+
70
+ Args:
71
+ rest_ssl_verify (Union[bool, str], optional): Specifies SSL certificate verification behavior. Can be:
72
+ - `True`: Uses the system’s default CA certificates to verify the server’s identity.
73
+ - `False`: Disables SSL certificate verification (insecure, intended for development).
74
+ - `str`: Specifies a custom CA bundle or directory for server certificate verification. If a string is provided,
75
+ it should either be a file path to a custom CA certificate (CA bundle) or a directory path containing multiple
76
+ certificates (CA directory).
77
+
78
+ **Note**: Unlike HTTP requests, TCP connections cannot automatically retrieve intermediate or public CA certificates.
79
+ When `rest_ssl_verify` is a path, it explicitly identifies a CA issuer to ensure certificate validation.
80
+
81
+ rest_ssl_client_cert (Union[str, Tuple[str, str]], optional): Path to the SSL client certificate and key. If a `str`,
82
+ it should be the path to a PEM file containing both the client certificate and private key. If a `tuple`, it should
83
+ be a pair of paths: (`cert`, `key`), where `cert` is the client certificate file and `key` is the private key file.
84
+
85
+ jwt_token (str, optional): JWT token for authentication. Used in methods requiring login and user authorization.
86
+
87
+ tcp_address (Tuple[str, int], optional): Address and port for establishing a TCP connection to the server.
88
+ Defaults to `("localhost", 6059)`.
89
+ """
90
+ self.tcp_address = tcp_address
91
+ self.__socket_client = None
92
+ super().__init__(rest_ssl_verify=rest_ssl_verify, rest_ssl_client_cert=rest_ssl_client_cert, jwt_token=jwt_token)
93
+
94
+ def __del__(self) -> None:
95
+ """De-construction"""
96
+ self.__close_socket()
97
+
98
+ def __connect_socket(self) -> ssl.SSLSocket:
99
+ """Establish tcp connection"""
100
+ context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
101
+ # Set minimum TLS version
102
+ if hasattr(context, "minimum_version"):
103
+ context.minimum_version = ssl.TLSVersion.TLSv1_2
104
+ else:
105
+ context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
106
+ # Configure SSL verification
107
+ if not self.ssl_verify:
108
+ context.verify_mode = ssl.CERT_NONE
109
+ else:
110
+ context.verify_mode = ssl.CERT_REQUIRED # Require certificate verification
111
+ context.load_default_certs() # Load system's default CA certificates
112
+ if isinstance(self.ssl_verify, str):
113
+ if os.path.isfile(self.ssl_verify):
114
+ # Load custom CA certificate file
115
+ context.load_verify_locations(cafile=self.ssl_verify)
116
+ elif os.path.isdir(self.ssl_verify):
117
+ # Load CA certificates from directory
118
+ context.load_verify_locations(capath=self.ssl_verify)
119
+ else:
120
+ raise ValueError(f"ssl_verify path '{self.ssl_verify}' is neither a file nor a directory")
121
+
122
+ if self.ssl_client_cert is not None:
123
+ # Load client-side certificate and private key
124
+ context.load_cert_chain(certfile=self.ssl_client_cert[0], keyfile=self.ssl_client_cert[1])
125
+
126
+ # Create a TCP socket
127
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
128
+ sock.setblocking(True)
129
+ sock.settimeout(30) # Connection timeout set to 30 seconds
130
+ # Wrap the socket with SSL/TLS
131
+ ssl_socket = context.wrap_socket(sock, server_hostname=self.tcp_address[0])
132
+ # Connect to the server
133
+ ssl_socket.connect(self.tcp_address)
134
+ # Disable Nagle's algorithm
135
+ ssl_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
136
+ # After connecting, set separate timeout for recv/send
137
+ # ssl_socket.settimeout(20) # 20 seconds for recv/send
138
+ return ssl_socket
139
+
140
+ def __close_socket(self) -> None:
141
+ """Close socket connection"""
142
+ if self.__socket_client:
143
+ try:
144
+ self.__socket_client.close()
145
+ except Exception as e:
146
+ print(f"Error closing socket: {e}")
147
+ finally:
148
+ self.__socket_client = None
149
+
150
+ def __recvall(self, length: int) -> bytearray:
151
+ """socket recv data with fixed length
152
+ https://stackoverflow.com/questions/64466530/using-a-custom-socket-recvall-function-works-only-if-thread-is-put-to-sleep
153
+ Args:
154
+ length (bytes): data length to be received
155
+
156
+ Returns:
157
+ bytearray: Received data
158
+
159
+ Raises:
160
+ EOFError: If connection closes prematurely
161
+ ValueError: If length is invalid
162
+ """
163
+ if length <= 0:
164
+ raise ValueError(f"Invalid length: {length}")
165
+
166
+ # Pre-allocate buffer of exact size needed
167
+ buffer = bytearray(length)
168
+ view = memoryview(buffer)
169
+ bytes_received = 0
170
+
171
+ while bytes_received < length:
172
+ # Use recv_into to read directly into our buffer
173
+ chunk_size = self.__socket_client.recv_into(view[bytes_received:], length - bytes_received)
174
+
175
+ if chunk_size == 0:
176
+ raise EOFError("Connection closed by peer")
177
+
178
+ bytes_received += chunk_size
179
+
180
+ return buffer
181
+
182
+ def _request_http(self, method: AppMeshClient.Method, path: str, query: dict = None, header: dict = None, body=None) -> requests.Response:
183
+ """TCP API
184
+
185
+ Args:
186
+ method (Method): AppMeshClient.Method.
187
+ path (str): URI patch str.
188
+ query (dict, optional): HTTP query parameters.
189
+ header (dict, optional): HTTP headers.
190
+ body (_type_, optional): object to send in the body of the :class:`Request`.
191
+
192
+ Returns:
193
+ requests.Response: HTTP response
194
+ """
195
+
196
+ if self.__socket_client is None:
197
+ self.__socket_client = self.__connect_socket()
198
+
199
+ appmesh_request = RequestMsg()
200
+ if super().jwt_token:
201
+ appmesh_request.headers["Authorization"] = "Bearer " + super().jwt_token
202
+ if super().forwarding_host and len(super().forwarding_host) > 0:
203
+ raise Exception("Not support forward request in TCP mode")
204
+ appmesh_request.headers[self.HTTP_HEADER_KEY_USER_AGENT] = self.HTTP_USER_AGENT_TCP
205
+ appmesh_request.uuid = str(uuid.uuid1())
206
+ appmesh_request.http_method = method.value
207
+ appmesh_request.request_uri = path
208
+ appmesh_request.client_addr = socket.gethostname()
209
+ if body:
210
+ if isinstance(body, dict) or isinstance(body, list):
211
+ appmesh_request.body = bytes(json.dumps(body, indent=2), self.ENCODING_UTF8)
212
+ elif isinstance(body, str):
213
+ appmesh_request.body = bytes(body, self.ENCODING_UTF8)
214
+ elif isinstance(body, bytes):
215
+ appmesh_request.body = body
216
+ else:
217
+ raise Exception(f"UnSupported body type: {type(body)}")
218
+ if header:
219
+ for k, v in header.items():
220
+ appmesh_request.headers[k] = v
221
+ if query:
222
+ for k, v in query.items():
223
+ appmesh_request.querys[k] = v
224
+ data = appmesh_request.serialize()
225
+ self.__socket_client.sendall(len(data).to_bytes(self.TCP_HEADER_LENGTH, byteorder="big", signed=False))
226
+ self.__socket_client.sendall(data)
227
+
228
+ # https://developers.google.com/protocol-buffers/docs/pythontutorial
229
+ # https://stackoverflow.com/questions/33913308/socket-module-how-to-send-integer
230
+ resp_data = self.__recvall(int.from_bytes(self.__recvall(self.TCP_HEADER_LENGTH), byteorder="big", signed=False))
231
+ if resp_data is None or len(resp_data) == 0:
232
+ self.__close_socket()
233
+ raise Exception("socket connection broken")
234
+ appmesh_resp = ResponseMsg().desirialize(resp_data)
235
+ response = requests.Response()
236
+ response.status_code = appmesh_resp.http_status
237
+ response.encoding = self.ENCODING_UTF8
238
+ response._content = appmesh_resp.body.encode(self.ENCODING_UTF8)
239
+ response.headers = appmesh_resp.headers
240
+ if appmesh_resp.body_msg_type:
241
+ response.headers["Content-Type"] = appmesh_resp.body_msg_type
242
+ return response
243
+
244
+ ########################################
245
+ # File management
246
+ ########################################
247
+ def file_download(self, remote_file: str, local_file: str, apply_file_attributes: bool = True) -> None:
248
+ """Copy a remote file to local, the local file will have the same permission as the remote file
249
+
250
+ Args:
251
+ remote_file (str): the remote file path.
252
+ local_file (str): the local file path to be downloaded.
253
+ apply_file_attributes (bool): whether to apply file attributes (permissions, owner, group) to the local file.
254
+ """
255
+ header = {"File-Path": remote_file}
256
+ header[self.HTTP_HEADER_KEY_X_RECV_FILE_SOCKET] = "true"
257
+ resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/file/download", header=header)
258
+
259
+ resp.raise_for_status()
260
+ if self.HTTP_HEADER_KEY_X_RECV_FILE_SOCKET not in resp.headers:
261
+ raise ValueError(f"Server did not respond with socket transfer option: {self.HTTP_HEADER_KEY_X_RECV_FILE_SOCKET}")
262
+
263
+ with open(local_file, "wb") as fp:
264
+ chunk_data = bytes()
265
+ chunk_size = int.from_bytes(self.__recvall(self.TCP_HEADER_LENGTH), byteorder="big", signed=False)
266
+ while chunk_size > 0:
267
+ chunk_data = self.__recvall(chunk_size)
268
+ if chunk_data is None or len(chunk_data) == 0:
269
+ self.__close_socket()
270
+ raise Exception("socket connection broken")
271
+ fp.write(chunk_data)
272
+ chunk_size = int.from_bytes(self.__recvall(self.TCP_HEADER_LENGTH), byteorder="big", signed=False)
273
+
274
+ if apply_file_attributes:
275
+ if "File-Mode" in resp.headers:
276
+ os.chmod(path=local_file, mode=int(resp.headers["File-Mode"]))
277
+ if "File-User" in resp.headers and "File-Group" in resp.headers:
278
+ file_uid = int(resp.headers["File-User"])
279
+ file_gid = int(resp.headers["File-Group"])
280
+ try:
281
+ os.chown(path=local_file, uid=file_uid, gid=file_gid)
282
+ except PermissionError:
283
+ print(f"Warning: Unable to change owner/group of {local_file}. Operation requires elevated privileges.")
284
+
285
+ def file_upload(self, local_file: str, remote_file: str, apply_file_attributes: bool = True) -> None:
286
+ """Upload a local file to the remote server, the remote file will have the same permission as the local file
287
+
288
+ Dependency:
289
+ sudo apt install python3-pip
290
+ pip3 install requests_toolbelt
291
+
292
+ Args:
293
+ local_file (str): the local file path.
294
+ remote_file (str): the target remote file to be uploaded.
295
+ apply_file_attributes (bool): whether to upload file attributes (permissions, owner, group) along with the file.
296
+ """
297
+ if not os.path.exists(local_file):
298
+ raise FileNotFoundError(f"Local file not found: {local_file}")
299
+
300
+ with open(file=local_file, mode="rb") as fp:
301
+ header = {"File-Path": remote_file, "Content-Type": "text/plain"}
302
+ header[self.HTTP_HEADER_KEY_X_SEND_FILE_SOCKET] = "true"
303
+
304
+ if apply_file_attributes:
305
+ file_stat = os.stat(local_file)
306
+ header["File-Mode"] = str(file_stat.st_mode & 0o777) # Mask to keep only permission bits
307
+ header["File-User"] = str(file_stat.st_uid)
308
+ header["File-Group"] = str(file_stat.st_gid)
309
+
310
+ # https://stackoverflow.com/questions/22567306/python-requests-file-upload
311
+ resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/file/upload", header=header)
312
+
313
+ resp.raise_for_status()
314
+ if self.HTTP_HEADER_KEY_X_SEND_FILE_SOCKET not in resp.headers:
315
+ raise ValueError(f"Server did not respond with socket transfer option: {self.HTTP_HEADER_KEY_X_SEND_FILE_SOCKET}")
316
+
317
+ chunk_size = self.TCP_BLOCK_SIZE
318
+ while True:
319
+ chunk_data = fp.read(chunk_size)
320
+ if not chunk_data:
321
+ self.__socket_client.sendall((0).to_bytes(self.TCP_HEADER_LENGTH, byteorder="big", signed=False))
322
+ break
323
+ self.__socket_client.sendall(len(chunk_data).to_bytes(self.TCP_HEADER_LENGTH, byteorder="big", signed=False))
324
+ self.__socket_client.sendall(chunk_data)
325
+
326
+
327
+ class RequestMsg:
328
+ """HTTP request message"""
329
+
330
+ uuid: str = ""
331
+ request_uri: str = ""
332
+ http_method: str = ""
333
+ client_addr: str = ""
334
+ body: bytes = b""
335
+ headers: dict = {}
336
+ querys: dict = {}
337
+
338
+ def serialize(self) -> bytes:
339
+ """Serialize request message to bytes"""
340
+ # http://www.cnitblog.com/luckydmz/archive/2019/11/20/91959.html
341
+ self_dict = vars(self)
342
+ self_dict["headers"] = self.headers
343
+ self_dict["querys"] = self.querys
344
+ return msgpack.dumps(self_dict)
345
+
346
+
347
+ class ResponseMsg:
348
+ """HTTP response message"""
349
+
350
+ uuid: str = ""
351
+ request_uri: str = ""
352
+ http_status: int = 0
353
+ body_msg_type: str = ""
354
+ body: str = ""
355
+ headers: dict = {}
356
+
357
+ def desirialize(self, buf: bytes):
358
+ """Deserialize response message"""
359
+ dic = msgpack.unpackb(buf)
360
+ for k, v in dic.items():
361
+ setattr(self, k, v)
362
+ return self
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: appmesh
3
- Version: 1.3.6
3
+ Version: 1.3.7
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,10 @@
1
+ appmesh/__init__.py,sha256=2-k6yrZr3TLHZ00YSiFHAYzsccSQ3eWZgoRs1eavZb8,442
2
+ appmesh/app.py,sha256=trPD1i7PFv7DTuy33Hr8-AC36r3h3XIBejdB3cUU8C8,10438
3
+ appmesh/app_output.py,sha256=JK_TMKgjvaw4n_ys_vmN5S4MyWVZpmD7NlKz_UyMIM8,1015
4
+ appmesh/app_run.py,sha256=D4j_SaA16_RtZ2-Ey6X4HIyngvLdfFHgyzYurDT1ATc,1753
5
+ appmesh/appmesh_client.py,sha256=9zysxEzSL3y2Csm7teLk7l6hknKzzTd-l4kWKeuuOj4,47571
6
+ appmesh/appmesh_client_tcp.py,sha256=BcPQcJqdPIWqP4vbRKHHEvlkkI83mqNfUse6AUGDbkE,16424
7
+ appmesh-1.3.7.dist-info/METADATA,sha256=rQoOicSO7Hz9lIocYszWWjpcpf65QzKFeILPyiB_tCA,11191
8
+ appmesh-1.3.7.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
9
+ appmesh-1.3.7.dist-info/top_level.txt,sha256=-y0MNQOGJxUzLdHZ6E_Rfv5_LNCkV-GTmOBME_b6pg8,8
10
+ appmesh-1.3.7.dist-info/RECORD,,
@@ -1,6 +0,0 @@
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,,