appmesh 1.3.6__py3-none-any.whl → 1.3.8__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/tcp_client.py ADDED
@@ -0,0 +1,216 @@
1
+ # TCP-based App Mesh Client
2
+ # pylint: disable=line-too-long,broad-exception-raised, ,broad-exception-caught,import-outside-toplevel,protected-access
3
+
4
+ import json
5
+ import os
6
+ import socket
7
+ import uuid
8
+ import requests
9
+ from .appmesh_client import AppMeshClient
10
+ from .tcp_transport import TCPTransport
11
+ from .tcp_messages import RequestMessage, ResponseMessage
12
+
13
+
14
+ class AppMeshClientTCP(AppMeshClient):
15
+ """
16
+ Client SDK for interacting with the App Mesh service over TCP, with enhanced support for large file transfers.
17
+
18
+ The `AppMeshClientTCP` class extends the functionality of `AppMeshClient` by offering a TCP-based communication layer
19
+ for the App Mesh REST API. It overrides the file download and upload methods to support large file transfers with
20
+ improved performance, leveraging TCP for lower latency and higher throughput compared to HTTP.
21
+
22
+ This client is suitable for applications requiring efficient data transfers and high-throughput operations within the
23
+ App Mesh ecosystem, while maintaining compatibility with all other attributes and methods from `AppMeshClient`.
24
+
25
+ Dependency:
26
+ - Install the required package for message serialization:
27
+ pip3 install msgpack
28
+
29
+ Usage:
30
+ - Import the client module:
31
+ from appmesh import appmesh_client
32
+
33
+ Example:
34
+ client = appmesh_client.AppMeshClientTCP()
35
+ client.login("your-name", "your-password")
36
+ client.file_download("/tmp/os-release", "os-release")
37
+
38
+ Attributes:
39
+ - Inherits all attributes from `AppMeshClient`, including TLS secure connections and JWT-based authentication.
40
+ - Optimized for TCP-based communication to provide better performance for large file transfers.
41
+
42
+ Methods:
43
+ - file_download()
44
+ - file_upload()
45
+ - Inherits all other methods from `AppMeshClient`, providing a consistent interface for managing applications within App Mesh.
46
+ """
47
+
48
+ TCP_BLOCK_SIZE = 16 * 1024 - 128 # TLS-optimized chunk size, leaves some room for TLS overhead (like headers) within the 16 KB limit.
49
+ ENCODING_UTF8 = "utf-8"
50
+ HTTP_USER_AGENT_TCP = "appmesh/python/tcp"
51
+ HTTP_HEADER_KEY_X_SEND_FILE_SOCKET = "X-Send-File-Socket"
52
+ HTTP_HEADER_KEY_X_RECV_FILE_SOCKET = "X-Recv-File-Socket"
53
+
54
+ def __init__(
55
+ self,
56
+ rest_ssl_verify=AppMeshClient.DEFAULT_SSL_CA_CERT_PATH if os.path.exists(AppMeshClient.DEFAULT_SSL_CA_CERT_PATH) else False,
57
+ rest_ssl_client_cert=None,
58
+ jwt_token=None,
59
+ tcp_address=("localhost", 6059),
60
+ ):
61
+ """Construct an App Mesh client TCP object to communicate securely with an App Mesh server over TLS.
62
+
63
+ Args:
64
+ rest_ssl_verify (Union[bool, str], optional): Specifies SSL certificate verification behavior. Can be:
65
+ - `True`: Uses the system’s default CA certificates to verify the server’s identity.
66
+ - `False`: Disables SSL certificate verification (insecure, intended for development).
67
+ - `str`: Specifies a custom CA bundle or directory for server certificate verification. If a string is provided,
68
+ it should either be a file path to a custom CA certificate (CA bundle) or a directory path containing multiple
69
+ certificates (CA directory).
70
+
71
+ **Note**: Unlike HTTP requests, TCP connections cannot automatically retrieve intermediate or public CA certificates.
72
+ When `rest_ssl_verify` is a path, it explicitly identifies a CA issuer to ensure certificate validation.
73
+
74
+ rest_ssl_client_cert (Union[str, Tuple[str, str]], optional): Path to the SSL client certificate and key. If a `str`,
75
+ it should be the path to a PEM file containing both the client certificate and private key. If a `tuple`, it should
76
+ be a pair of paths: (`cert`, `key`), where `cert` is the client certificate file and `key` is the private key file.
77
+
78
+ jwt_token (str, optional): JWT token for authentication. Used in methods requiring login and user authorization.
79
+
80
+ tcp_address (Tuple[str, int], optional): Address and port for establishing a TCP connection to the server.
81
+ Defaults to `("localhost", 6059)`.
82
+ """
83
+ self.tcp_transport = TCPTransport(address=tcp_address, ssl_verify=rest_ssl_verify, ssl_client_cert=rest_ssl_client_cert)
84
+ super().__init__(rest_ssl_verify=rest_ssl_verify, rest_ssl_client_cert=rest_ssl_client_cert, jwt_token=jwt_token)
85
+
86
+ def _request_http(self, method: AppMeshClient.Method, path: str, query: dict = None, header: dict = None, body=None) -> requests.Response:
87
+ """TCP API
88
+
89
+ Args:
90
+ method (Method): AppMeshClient.Method.
91
+ path (str): URI patch str.
92
+ query (dict, optional): HTTP query parameters.
93
+ header (dict, optional): HTTP headers.
94
+ body (_type_, optional): object to send in the body of the :class:`Request`.
95
+
96
+ Returns:
97
+ requests.Response: HTTP response
98
+ """
99
+
100
+ if not self.tcp_transport.connected():
101
+ self.tcp_transport.connect()
102
+
103
+ appmesh_request = RequestMessage()
104
+ if super().jwt_token:
105
+ appmesh_request.headers["Authorization"] = "Bearer " + super().jwt_token
106
+ if super().forwarding_host and len(super().forwarding_host) > 0:
107
+ raise Exception("Not support forward request in TCP mode")
108
+ appmesh_request.headers[self.HTTP_HEADER_KEY_USER_AGENT] = self.HTTP_USER_AGENT_TCP
109
+ appmesh_request.uuid = str(uuid.uuid1())
110
+ appmesh_request.http_method = method.value
111
+ appmesh_request.request_uri = path
112
+ appmesh_request.client_addr = socket.gethostname()
113
+ if body:
114
+ if isinstance(body, dict) or isinstance(body, list):
115
+ appmesh_request.body = bytes(json.dumps(body, indent=2), self.ENCODING_UTF8)
116
+ elif isinstance(body, str):
117
+ appmesh_request.body = bytes(body, self.ENCODING_UTF8)
118
+ elif isinstance(body, bytes):
119
+ appmesh_request.body = body
120
+ else:
121
+ raise Exception(f"UnSupported body type: {type(body)}")
122
+ if header:
123
+ for k, v in header.items():
124
+ appmesh_request.headers[k] = v
125
+ if query:
126
+ for k, v in query.items():
127
+ appmesh_request.querys[k] = v
128
+ data = appmesh_request.serialize()
129
+ self.tcp_transport.send_message(data)
130
+
131
+ resp_data = self.tcp_transport.receive_message()
132
+ if resp_data is None or len(resp_data) == 0:
133
+ self.tcp_transport.close()
134
+ raise Exception("socket connection broken")
135
+ appmesh_resp = ResponseMessage().desirialize(resp_data)
136
+ response = requests.Response()
137
+ response.status_code = appmesh_resp.http_status
138
+ response.encoding = self.ENCODING_UTF8
139
+ response._content = appmesh_resp.body.encode(self.ENCODING_UTF8)
140
+ response.headers = appmesh_resp.headers
141
+ if appmesh_resp.body_msg_type:
142
+ response.headers["Content-Type"] = appmesh_resp.body_msg_type
143
+ return response
144
+
145
+ ########################################
146
+ # File management
147
+ ########################################
148
+ def file_download(self, remote_file: str, local_file: str, apply_file_attributes: bool = True) -> None:
149
+ """Copy a remote file to local, the local file will have the same permission as the remote file
150
+
151
+ Args:
152
+ remote_file (str): the remote file path.
153
+ local_file (str): the local file path to be downloaded.
154
+ apply_file_attributes (bool): whether to apply file attributes (permissions, owner, group) to the local file.
155
+ """
156
+ header = {"File-Path": remote_file}
157
+ header[self.HTTP_HEADER_KEY_X_RECV_FILE_SOCKET] = "true"
158
+ resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/file/download", header=header)
159
+
160
+ resp.raise_for_status()
161
+ if self.HTTP_HEADER_KEY_X_RECV_FILE_SOCKET not in resp.headers:
162
+ raise ValueError(f"Server did not respond with socket transfer option: {self.HTTP_HEADER_KEY_X_RECV_FILE_SOCKET}")
163
+
164
+ with open(local_file, "wb") as fp:
165
+ while chunk_data := self.tcp_transport.receive_message():
166
+ fp.write(chunk_data)
167
+
168
+ if apply_file_attributes:
169
+ if "File-Mode" in resp.headers:
170
+ os.chmod(path=local_file, mode=int(resp.headers["File-Mode"]))
171
+ if "File-User" in resp.headers and "File-Group" in resp.headers:
172
+ file_uid = int(resp.headers["File-User"])
173
+ file_gid = int(resp.headers["File-Group"])
174
+ try:
175
+ os.chown(path=local_file, uid=file_uid, gid=file_gid)
176
+ except PermissionError:
177
+ print(f"Warning: Unable to change owner/group of {local_file}. Operation requires elevated privileges.")
178
+
179
+ def file_upload(self, local_file: str, remote_file: str, apply_file_attributes: bool = True) -> None:
180
+ """Upload a local file to the remote server, the remote file will have the same permission as the local file
181
+
182
+ Dependency:
183
+ sudo apt install python3-pip
184
+ pip3 install requests_toolbelt
185
+
186
+ Args:
187
+ local_file (str): the local file path.
188
+ remote_file (str): the target remote file to be uploaded.
189
+ apply_file_attributes (bool): whether to upload file attributes (permissions, owner, group) along with the file.
190
+ """
191
+ if not os.path.exists(local_file):
192
+ raise FileNotFoundError(f"Local file not found: {local_file}")
193
+
194
+ with open(file=local_file, mode="rb") as fp:
195
+ header = {"File-Path": remote_file, "Content-Type": "text/plain"}
196
+ header[self.HTTP_HEADER_KEY_X_SEND_FILE_SOCKET] = "true"
197
+
198
+ if apply_file_attributes:
199
+ file_stat = os.stat(local_file)
200
+ header["File-Mode"] = str(file_stat.st_mode & 0o777) # Mask to keep only permission bits
201
+ header["File-User"] = str(file_stat.st_uid)
202
+ header["File-Group"] = str(file_stat.st_gid)
203
+
204
+ resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/file/upload", header=header)
205
+
206
+ resp.raise_for_status()
207
+ if self.HTTP_HEADER_KEY_X_SEND_FILE_SOCKET not in resp.headers:
208
+ raise ValueError(f"Server did not respond with socket transfer option: {self.HTTP_HEADER_KEY_X_SEND_FILE_SOCKET}")
209
+
210
+ chunk_size = self.TCP_BLOCK_SIZE
211
+ while True:
212
+ chunk_data = fp.read(chunk_size)
213
+ if not chunk_data:
214
+ self.tcp_transport.send_message([])
215
+ break
216
+ self.tcp_transport.send_message(chunk_data)
@@ -0,0 +1,41 @@
1
+ # tcp_messages.py
2
+
3
+ import msgpack
4
+
5
+
6
+ class RequestMessage:
7
+ """HTTP request message"""
8
+
9
+ uuid: str = ""
10
+ request_uri: str = ""
11
+ http_method: str = ""
12
+ client_addr: str = ""
13
+ body: bytes = b""
14
+ headers: dict = {}
15
+ querys: dict = {}
16
+
17
+ def serialize(self) -> bytes:
18
+ """Serialize request message to bytes"""
19
+ # http://www.cnitblog.com/luckydmz/archive/2019/11/20/91959.html
20
+ self_dict = vars(self)
21
+ self_dict["headers"] = self.headers
22
+ self_dict["querys"] = self.querys
23
+ return msgpack.dumps(self_dict)
24
+
25
+
26
+ class ResponseMessage:
27
+ """HTTP response message"""
28
+
29
+ uuid: str = ""
30
+ request_uri: str = ""
31
+ http_status: int = 0
32
+ body_msg_type: str = ""
33
+ body: str = ""
34
+ headers: dict = {}
35
+
36
+ def desirialize(self, buf: bytes):
37
+ """Deserialize response message"""
38
+ dic = msgpack.unpackb(buf)
39
+ for k, v in dic.items():
40
+ setattr(self, k, v)
41
+ return self
@@ -0,0 +1,160 @@
1
+ # tcp_transport.py
2
+
3
+ import os
4
+ import socket
5
+ import ssl
6
+ from typing import Optional, Tuple, Union
7
+
8
+
9
+ class TCPTransport:
10
+ """TCP Transport layer handling socket connections"""
11
+
12
+ # Number of bytes used for the message length header
13
+ # Must match the C++ service implementation which uses uint32_t (4 bytes)
14
+ # Format: Big-endian unsigned 32-bit integer
15
+ TCP_HEADER_LENGTH = 4
16
+ MAX_MESSAGE_SIZE = 300 * 1024 * 1024 # 300 MiB message size limit
17
+
18
+ def __init__(self, address: Tuple[str, int], ssl_verify: Union[bool, str], ssl_client_cert: Union[str, Tuple[str, str]]):
19
+ """Construct an TCPTransport object to send and recieve TCP data.
20
+
21
+ Args:
22
+ ssl_verify (Union[bool, str], optional): Specifies SSL certificate verification behavior. Can be:
23
+ - `True`: Uses the system's default CA certificates to verify the server's identity.
24
+ - `False`: Disables SSL certificate verification (insecure, intended for development).
25
+ - `str`: Specifies a custom CA bundle or directory for server certificate verification. If a string is provided,
26
+ it should either be a file path to a custom CA certificate (CA bundle) or a directory path containing multiple
27
+ certificates (CA directory).
28
+
29
+ **Note**: Unlike HTTP requests, TCP connections cannot automatically retrieve intermediate or public CA certificates.
30
+ When `rest_ssl_verify` is a path, it explicitly identifies a CA issuer to ensure certificate validation.
31
+
32
+ ssl_client_cert (Union[str, Tuple[str, str]], optional): Path to the SSL client certificate and key. If a `str`,
33
+ it should be the path to a PEM file containing both the client certificate and private key. If a `tuple`, it should
34
+ be a pair of paths: (`cert`, `key`), where `cert` is the client certificate file and `key` is the private key file.
35
+
36
+ tcp_address (Tuple[str, int], optional): Address and port for establishing a TCP connection to the server.
37
+ Defaults to `("localhost", 6059)`.
38
+ """
39
+ self.tcp_address = address
40
+ self.ssl_verify = ssl_verify
41
+ self.ssl_client_cert = ssl_client_cert
42
+ self._socket = None
43
+
44
+ def __enter__(self):
45
+ """Context manager entry"""
46
+ if not self.connected():
47
+ self.connect()
48
+ return self
49
+
50
+ def __exit__(self, exc_type, exc_val, exc_tb):
51
+ """Context manager exit"""
52
+ self.close()
53
+
54
+ def __del__(self) -> None:
55
+ """De-construction"""
56
+ self.close()
57
+
58
+ def connect(self) -> None:
59
+ """Establish tcp connection"""
60
+ context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
61
+ # Set minimum TLS version
62
+ if hasattr(context, "minimum_version"):
63
+ context.minimum_version = ssl.TLSVersion.TLSv1_2
64
+ else:
65
+ context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
66
+ # Configure SSL verification
67
+ if not self.ssl_verify:
68
+ context.verify_mode = ssl.CERT_NONE
69
+ else:
70
+ context.verify_mode = ssl.CERT_REQUIRED # Require certificate verification
71
+ context.load_default_certs() # Load system's default CA certificates
72
+ if isinstance(self.ssl_verify, str):
73
+ if os.path.isfile(self.ssl_verify):
74
+ # Load custom CA certificate file
75
+ context.load_verify_locations(cafile=self.ssl_verify)
76
+ elif os.path.isdir(self.ssl_verify):
77
+ # Load CA certificates from directory
78
+ context.load_verify_locations(capath=self.ssl_verify)
79
+ else:
80
+ raise ValueError(f"ssl_verify path '{self.ssl_verify}' is neither a file nor a directory")
81
+
82
+ if self.ssl_client_cert is not None:
83
+ # Load client-side certificate and private key
84
+ context.load_cert_chain(certfile=self.ssl_client_cert[0], keyfile=self.ssl_client_cert[1])
85
+
86
+ # Create a TCP socket
87
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
88
+ sock.setblocking(True)
89
+ sock.settimeout(30) # Connection timeout set to 30 seconds
90
+ # Wrap the socket with SSL/TLS
91
+ ssl_socket = context.wrap_socket(sock, server_hostname=self.tcp_address[0])
92
+ # Connect to the server
93
+ ssl_socket.connect(self.tcp_address)
94
+ # Disable Nagle's algorithm
95
+ ssl_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
96
+ # After connecting, set separate timeout for recv/send
97
+ # ssl_socket.settimeout(20) # 20 seconds for recv/send
98
+ self._socket = ssl_socket
99
+
100
+ def close(self) -> None:
101
+ """Close socket connection"""
102
+ if self._socket:
103
+ try:
104
+ self._socket.close()
105
+ except Exception as e:
106
+ print(f"Error closing socket: {e}")
107
+ finally:
108
+ self._socket = None
109
+
110
+ def connected(self) -> bool:
111
+ """Check whether socket is connected"""
112
+ return self._socket is not None
113
+
114
+ def send_message(self, data) -> None:
115
+ """Send a message with a prefixed header indicating its length"""
116
+ length = len(data)
117
+ self._socket.sendall(length.to_bytes(self.TCP_HEADER_LENGTH, byteorder="big", signed=False))
118
+ if length > 0:
119
+ self._socket.sendall(data)
120
+
121
+ def receive_message(self) -> Optional[bytearray]:
122
+ """Receive a message with a prefixed header indicating its length"""
123
+ length = int.from_bytes(self._recvall(self.TCP_HEADER_LENGTH), byteorder="big", signed=False)
124
+ if length > self.MAX_MESSAGE_SIZE:
125
+ raise ValueError(f"Message size {length} exceeds maximum allowed {self.MAX_MESSAGE_SIZE}")
126
+ if length > 0:
127
+ return self._recvall(length)
128
+ return None
129
+
130
+ def _recvall(self, length: int) -> bytearray:
131
+ """socket recv data with fixed length
132
+ https://stackoverflow.com/questions/64466530/using-a-custom-socket-recvall-function-works-only-if-thread-is-put-to-sleep
133
+ Args:
134
+ length (int): data length to be received
135
+
136
+ Returns:
137
+ bytearray: Received data
138
+
139
+ Raises:
140
+ EOFError: If connection closes prematurely
141
+ ValueError: If length is invalid
142
+ """
143
+ if length <= 0:
144
+ raise ValueError(f"Invalid length: {length}")
145
+
146
+ # Pre-allocate buffer of exact size needed
147
+ buffer = bytearray(length)
148
+ view = memoryview(buffer)
149
+ bytes_received = 0
150
+
151
+ while bytes_received < length:
152
+ # Use recv_into to read directly into our buffer
153
+ chunk_size = self._socket.recv_into(view[bytes_received:], length - bytes_received)
154
+
155
+ if chunk_size == 0:
156
+ raise EOFError("Connection closed by peer")
157
+
158
+ bytes_received += chunk_size
159
+
160
+ return buffer
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: appmesh
3
- Version: 1.3.6
3
+ Version: 1.3.8
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,13 @@
1
+ appmesh/__init__.py,sha256=vgiSdMzlzDwgHxBMDoFaKWb77g2nJVciRf4z_ssAlwE,431
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=0ltkqHZUq094gKneYmC0bEZCP0X9kHTp9fccKdWFWP0,339
6
+ appmesh/http_client.py,sha256=miO52kW8d9s0tGn42SxbOPtVNu9ZL8HDFCoCCMXm_EA,47484
7
+ appmesh/tcp_client.py,sha256=cbnx_YbBMBbJeR5DY7u5jmlgHcYqbjzffirSiJ-zT1o,10852
8
+ appmesh/tcp_messages.py,sha256=w1Kehz_aX4X2CYAUsy9mFVJRhxnLQwwc6L58W4YkQqs,969
9
+ appmesh/tcp_transport.py,sha256=YlKMaE-oaKLmGuBdWIyKz3YH4ZPMgJWHBZYbINtUoYM,6934
10
+ appmesh-1.3.8.dist-info/METADATA,sha256=GCrx58vUH1DvcQfLWXjG-pVJowoD7ykM6ZehOZVTGMI,11191
11
+ appmesh-1.3.8.dist-info/WHEEL,sha256=R06PA3UVYHThwHvxuRWMqaGcr-PuniXahwjmQRFMEkY,91
12
+ appmesh-1.3.8.dist-info/top_level.txt,sha256=-y0MNQOGJxUzLdHZ6E_Rfv5_LNCkV-GTmOBME_b6pg8,8
13
+ appmesh-1.3.8.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.3.0)
2
+ Generator: setuptools (75.5.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,6 +0,0 @@
1
- appmesh/__init__.py,sha256=xRdXeFHEieRauuJZElbEBASgXG0ZzU1a5_0isAhM7Gw,11
2
- appmesh/appmesh_client.py,sha256=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,,