appmesh 1.3.7__py3-none-any.whl → 1.3.9__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.
@@ -1,20 +1,14 @@
1
- """Application output information"""
2
-
1
+ # TCP-based App Mesh Client
3
2
  # pylint: disable=line-too-long,broad-exception-raised, ,broad-exception-caught,import-outside-toplevel,protected-access
4
3
 
5
- # Standard library imports
6
4
  import json
7
5
  import os
8
6
  import socket
9
- import ssl
10
7
  import uuid
11
-
12
- # Third-party imports
13
8
  import requests
14
- import msgpack
15
-
16
- # Local application-specific imports
17
9
  from .appmesh_client import AppMeshClient
10
+ from .tcp_transport import TCPTransport
11
+ from .tcp_messages import RequestMessage, ResponseMessage
18
12
 
19
13
 
20
14
  class AppMeshClientTCP(AppMeshClient):
@@ -52,7 +46,6 @@ class AppMeshClientTCP(AppMeshClient):
52
46
  """
53
47
 
54
48
  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
49
  ENCODING_UTF8 = "utf-8"
57
50
  HTTP_USER_AGENT_TCP = "appmesh/python/tcp"
58
51
  HTTP_HEADER_KEY_X_SEND_FILE_SOCKET = "X-Send-File-Socket"
@@ -87,98 +80,9 @@ class AppMeshClientTCP(AppMeshClient):
87
80
  tcp_address (Tuple[str, int], optional): Address and port for establishing a TCP connection to the server.
88
81
  Defaults to `("localhost", 6059)`.
89
82
  """
90
- self.tcp_address = tcp_address
91
- self.__socket_client = None
83
+ self.tcp_transport = TCPTransport(address=tcp_address, ssl_verify=rest_ssl_verify, ssl_client_cert=rest_ssl_client_cert)
92
84
  super().__init__(rest_ssl_verify=rest_ssl_verify, rest_ssl_client_cert=rest_ssl_client_cert, jwt_token=jwt_token)
93
85
 
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
86
  def _request_http(self, method: AppMeshClient.Method, path: str, query: dict = None, header: dict = None, body=None) -> requests.Response:
183
87
  """TCP API
184
88
 
@@ -193,10 +97,10 @@ class AppMeshClientTCP(AppMeshClient):
193
97
  requests.Response: HTTP response
194
98
  """
195
99
 
196
- if self.__socket_client is None:
197
- self.__socket_client = self.__connect_socket()
100
+ if not self.tcp_transport.connected():
101
+ self.tcp_transport.connect()
198
102
 
199
- appmesh_request = RequestMsg()
103
+ appmesh_request = RequestMessage()
200
104
  if super().jwt_token:
201
105
  appmesh_request.headers["Authorization"] = "Bearer " + super().jwt_token
202
106
  if super().forwarding_host and len(super().forwarding_host) > 0:
@@ -222,16 +126,13 @@ class AppMeshClientTCP(AppMeshClient):
222
126
  for k, v in query.items():
223
127
  appmesh_request.querys[k] = v
224
128
  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)
129
+ self.tcp_transport.send_message(data)
227
130
 
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))
131
+ resp_data = self.tcp_transport.receive_message()
231
132
  if resp_data is None or len(resp_data) == 0:
232
- self.__close_socket()
133
+ self.tcp_transport.close()
233
134
  raise Exception("socket connection broken")
234
- appmesh_resp = ResponseMsg().desirialize(resp_data)
135
+ appmesh_resp = ResponseMessage().desirialize(resp_data)
235
136
  response = requests.Response()
236
137
  response.status_code = appmesh_resp.http_status
237
138
  response.encoding = self.ENCODING_UTF8
@@ -261,15 +162,11 @@ class AppMeshClientTCP(AppMeshClient):
261
162
  raise ValueError(f"Server did not respond with socket transfer option: {self.HTTP_HEADER_KEY_X_RECV_FILE_SOCKET}")
262
163
 
263
164
  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")
165
+ while True:
166
+ chunk_data = self.tcp_transport.receive_message()
167
+ if not chunk_data:
168
+ break
271
169
  fp.write(chunk_data)
272
- chunk_size = int.from_bytes(self.__recvall(self.TCP_HEADER_LENGTH), byteorder="big", signed=False)
273
170
 
274
171
  if apply_file_attributes:
275
172
  if "File-Mode" in resp.headers:
@@ -307,7 +204,6 @@ class AppMeshClientTCP(AppMeshClient):
307
204
  header["File-User"] = str(file_stat.st_uid)
308
205
  header["File-Group"] = str(file_stat.st_gid)
309
206
 
310
- # https://stackoverflow.com/questions/22567306/python-requests-file-upload
311
207
  resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/file/upload", header=header)
312
208
 
313
209
  resp.raise_for_status()
@@ -318,45 +214,6 @@ class AppMeshClientTCP(AppMeshClient):
318
214
  while True:
319
215
  chunk_data = fp.read(chunk_size)
320
216
  if not chunk_data:
321
- self.__socket_client.sendall((0).to_bytes(self.TCP_HEADER_LENGTH, byteorder="big", signed=False))
217
+ self.tcp_transport.send_message([])
322
218
  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
219
+ 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.7
3
+ Version: 1.3.9
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=UsdD_APVPRy5CtgX1_hJyljols8cmCBoEiWlKH2gYOM,10933
8
+ appmesh/tcp_messages.py,sha256=w1Kehz_aX4X2CYAUsy9mFVJRhxnLQwwc6L58W4YkQqs,969
9
+ appmesh/tcp_transport.py,sha256=YlKMaE-oaKLmGuBdWIyKz3YH4ZPMgJWHBZYbINtUoYM,6934
10
+ appmesh-1.3.9.dist-info/METADATA,sha256=YvI0e_YQxbntROWaY7oxJn9ksDq-dCXDmGwyv1xE6Vk,11191
11
+ appmesh-1.3.9.dist-info/WHEEL,sha256=R06PA3UVYHThwHvxuRWMqaGcr-PuniXahwjmQRFMEkY,91
12
+ appmesh-1.3.9.dist-info/top_level.txt,sha256=-y0MNQOGJxUzLdHZ6E_Rfv5_LNCkV-GTmOBME_b6pg8,8
13
+ appmesh-1.3.9.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,10 +0,0 @@
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,,