appmesh 1.6.12__tar.gz → 1.6.14__tar.gz

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.
Files changed (24) hide show
  1. {appmesh-1.6.12 → appmesh-1.6.14}/PKG-INFO +2 -1
  2. {appmesh-1.6.12 → appmesh-1.6.14}/appmesh/app.py +28 -18
  3. {appmesh-1.6.12 → appmesh-1.6.14}/appmesh/client_http.py +47 -61
  4. {appmesh-1.6.12 → appmesh-1.6.14}/appmesh/client_http_oauth.py +2 -2
  5. appmesh-1.6.14/appmesh/client_tcp.py +293 -0
  6. {appmesh-1.6.12 → appmesh-1.6.14}/appmesh/server_http.py +20 -23
  7. {appmesh-1.6.12 → appmesh-1.6.14}/appmesh/server_tcp.py +4 -8
  8. {appmesh-1.6.12 → appmesh-1.6.14}/appmesh/tcp_transport.py +0 -2
  9. {appmesh-1.6.12 → appmesh-1.6.14}/appmesh.egg-info/PKG-INFO +2 -1
  10. {appmesh-1.6.12 → appmesh-1.6.14}/appmesh.egg-info/requires.txt +3 -0
  11. {appmesh-1.6.12 → appmesh-1.6.14}/setup.py +2 -3
  12. appmesh-1.6.12/appmesh/client_tcp.py +0 -246
  13. {appmesh-1.6.12 → appmesh-1.6.14}/appmesh/__init__.py +0 -0
  14. {appmesh-1.6.12 → appmesh-1.6.14}/appmesh/app_output.py +0 -0
  15. {appmesh-1.6.12 → appmesh-1.6.14}/appmesh/app_run.py +0 -0
  16. {appmesh-1.6.12 → appmesh-1.6.14}/appmesh/appmesh_client.py +0 -0
  17. {appmesh-1.6.12 → appmesh-1.6.14}/appmesh/tcp_messages.py +0 -0
  18. {appmesh-1.6.12 → appmesh-1.6.14}/appmesh.egg-info/SOURCES.txt +0 -0
  19. {appmesh-1.6.12 → appmesh-1.6.14}/appmesh.egg-info/dependency_links.txt +0 -0
  20. {appmesh-1.6.12 → appmesh-1.6.14}/appmesh.egg-info/top_level.txt +0 -0
  21. {appmesh-1.6.12 → appmesh-1.6.14}/pyproject.toml +0 -0
  22. {appmesh-1.6.12 → appmesh-1.6.14}/setup.cfg +0 -0
  23. {appmesh-1.6.12 → appmesh-1.6.14}/test/test_appmesh_client.py +0 -0
  24. {appmesh-1.6.12 → appmesh-1.6.14}/test/test_oauth2.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: appmesh
3
- Version: 1.6.12
3
+ Version: 1.6.14
4
4
  Summary: Client SDK for App Mesh
5
5
  Home-page: https://github.com/laoshanxi/app-mesh
6
6
  Author: laoshanxi
@@ -17,6 +17,7 @@ Requires-Dist: msgpack
17
17
  Requires-Dist: requests_toolbelt
18
18
  Requires-Dist: aniso8601
19
19
  Requires-Dist: PyJWT
20
+ Requires-Dist: dataclasses; python_version < "3.7"
20
21
  Dynamic: author
21
22
  Dynamic: author-email
22
23
  Dynamic: classifier
@@ -134,75 +134,85 @@ class App:
134
134
 
135
135
  # Application configuration
136
136
  self.name = _get_str(data, "name")
137
- """application name (unique)"""
137
+ """app name (unique)"""
138
138
  self.command = _get_str(data, "command")
139
139
  """full command line with arguments"""
140
140
  self.shell = _get_bool(data, "shell")
141
- """use shell mode, cmd can be more shell commands with string format"""
141
+ """Whether run command in shell mode (enables shell syntax such as pipes and compound commands)"""
142
142
  self.session_login = _get_bool(data, "session_login")
143
- """app run in session login mode"""
143
+ """Whether to run the app in session login mode (inheriting the user's full login environment)"""
144
144
  self.description = _get_str(data, "description")
145
- """application description string"""
145
+ """app description string"""
146
146
  self.metadata = _get_item(data, "metadata")
147
- """metadata string/JSON (input for application, pass to process stdin)"""
147
+ """metadata string/JSON (input for app, pass to process stdin)"""
148
148
  self.working_dir = _get_str(data, "working_dir")
149
149
  """working directory"""
150
150
  self.status = _get_int(data, "status")
151
- """initial application status (true is enable, false is disabled)"""
151
+ """app status: 1 for enabled, 0 for disabled"""
152
152
  self.docker_image = _get_str(data, "docker_image")
153
- """docker image which used to run command line (for docker container application)"""
153
+ """Docker image for containerized execution"""
154
154
  self.stdout_cache_num = _get_int(data, "stdout_cache_num")
155
- """stdout file cache number"""
155
+ """maximum number of stdout log files to retain"""
156
156
  self.start_time = _get_int(data, "start_time")
157
157
  """start date time for app (ISO8601 time format, e.g., '2020-10-11T09:22:05')"""
158
158
  self.end_time = _get_int(data, "end_time")
159
159
  """end date time for app (ISO8601 time format, e.g., '2020-10-11T10:22:05')"""
160
- self.interval = _get_int(data, "interval")
160
+ self.start_interval_seconds = _get_int(data, "start_interval_seconds")
161
161
  """start interval seconds for short running app, support ISO 8601 durations and cron expression (e.g., 'P1Y2M3DT4H5M6S' 'P5W' '* */5 * * * *')"""
162
162
  self.cron = _get_bool(data, "cron")
163
- """indicate interval parameter use cron expression or not"""
163
+ """Whether the interval is specified as a cron expression"""
164
164
  self.daily_limitation = App.DailyLimitation(_get_item(data, "daily_limitation"))
165
165
  self.retention = _get_str(data, "retention")
166
166
  """extra timeout seconds for stopping current process, support ISO 8601 durations (e.g., 'P1Y2M3DT4H5M6S' 'P5W')."""
167
167
  self.health_check_cmd = _get_str(data, "health_check_cmd")
168
168
  """health check script command (e.g., sh -x 'curl host:port/health', return 0 is health)"""
169
169
  self.permission = _get_int(data, "permission")
170
- """application user permission, value is 2 bit integer: [group & other], each bit can be deny:1, read:2, write: 3."""
170
+ """app user permission, value is 2 bit integer: [group & other], each bit can be deny:1, read:2, write: 3."""
171
171
  self.behavior = App.Behavior(_get_item(data, "behavior"))
172
172
 
173
173
  self.env = data.get("env", {}) if data else {}
174
174
  """environment variables (e.g., -e env1=value1 -e env2=value2, APP_DOCKER_OPTS is used to input docker run parameters)"""
175
175
  self.sec_env = data.get("sec_env", {}) if data else {}
176
- """security environment variables, encrypt in server side with application owner's cipher"""
176
+ """security environment variables, encrypt in server side with app owner's cipher"""
177
177
  self.pid = _get_int(data, "pid")
178
178
  """process id used to attach to the running process"""
179
179
  self.resource_limit = App.ResourceLimitation(_get_item(data, "resource_limit"))
180
180
 
181
181
  # Read-only attributes
182
+ self.register_time = _get_int(data, "register_time")
183
+ """app register time"""
184
+ self.starts = _get_int(data, "starts")
185
+ """number of times started"""
182
186
  self.owner = _get_str(data, "owner")
183
- """owner name"""
187
+ """owner name of app mesh user who created the app"""
184
188
  self.user = _get_str(data, "pid_user")
185
- """process user name"""
189
+ """process OS user name"""
186
190
  self.pstree = _get_str(data, "pstree")
187
191
  """process tree"""
188
192
  self.container_id = _get_str(data, "container_id")
189
- """container id"""
193
+ """docker container id"""
190
194
  self.memory = _get_int(data, "memory")
191
195
  """memory usage"""
192
196
  self.cpu = _get_int(data, "cpu")
193
197
  """cpu usage"""
194
198
  self.fd = _get_int(data, "fd")
195
199
  """file descriptor usage"""
200
+ self.stdout_cache_size = _get_int(data, "stdout_cache_size")
201
+ """number of stdout log files currently retained"""
196
202
  self.last_start_time = _get_int(data, "last_start_time")
197
203
  """last start time"""
198
204
  self.last_exit_time = _get_int(data, "last_exit_time")
199
205
  """last exit time"""
206
+ self.last_error = _get_str(data, "last_error")
207
+ """last error message"""
208
+ self.next_start_time = _get_int(data, "next_start_time")
209
+ """next start time"""
200
210
  self.health = _get_int(data, "health")
201
- """health status"""
211
+ """health status: 0 for healthy, 1 for unhealthy"""
202
212
  self.version = _get_int(data, "version")
203
- """version number"""
213
+ """app version"""
204
214
  self.return_code = _get_int(data, "return_code")
205
- """last exit code"""
215
+ """last process exit code"""
206
216
  self.task_id = _get_int(data, "task_id")
207
217
  """current task id"""
208
218
  self.task_status = _get_str(data, "task_status")
@@ -12,6 +12,7 @@ import os
12
12
  import sys
13
13
  import threading
14
14
  import time
15
+ from pathlib import Path
15
16
  from datetime import datetime
16
17
  from enum import Enum, unique
17
18
  from http import HTTPStatus
@@ -39,17 +40,6 @@ class AppMeshClient(metaclass=abc.ABCMeta):
39
40
 
40
41
  This client is designed for direct usage in applications that require access to App Mesh services over HTTP-based REST.
41
42
 
42
- Usage:
43
- - Install the App Mesh Python package:
44
- python3 -m pip install --upgrade appmesh
45
- - Import the client module:
46
- from appmesh import AppMeshClient
47
-
48
- Example:
49
- client = AppMeshClient()
50
- client.login("your-name", "your-password")
51
- response = client.app_view(app_name='ping')
52
-
53
43
  Attributes:
54
44
  - TLS (Transport Layer Security): Supports secure connections between the client and App Mesh service,
55
45
  ensuring encrypted communication.
@@ -112,6 +102,13 @@ class AppMeshClient(metaclass=abc.ABCMeta):
112
102
  - update_role()
113
103
  - view_roles()
114
104
  - view_groups()
105
+
106
+ Example:
107
+ >>> python -m pip install --upgrade appmesh
108
+ >>> from appmesh import AppMeshClient
109
+ >>> client = AppMeshClient()
110
+ >>> client.login("your-name", "your-password")
111
+ >>> response = client.app_view(app_name='ping')
115
112
  """
116
113
 
117
114
  DURATION_ONE_WEEK_ISO = "P1W"
@@ -121,10 +118,10 @@ class AppMeshClient(metaclass=abc.ABCMeta):
121
118
  TOKEN_REFRESH_OFFSET = 30 # 30s before token expire to refresh token
122
119
 
123
120
  # Platform-aware default SSL paths
124
- _DEFAULT_SSL_DIR = "c:/local/appmesh/ssl" if os.name == "nt" else "/opt/appmesh/ssl"
125
- DEFAULT_SSL_CA_CERT_PATH = os.path.join(_DEFAULT_SSL_DIR, "ca.pem")
126
- DEFAULT_SSL_CLIENT_CERT_PATH = os.path.join(_DEFAULT_SSL_DIR, "client.pem")
127
- DEFAULT_SSL_CLIENT_KEY_PATH = os.path.join(_DEFAULT_SSL_DIR, "client-key.pem")
121
+ _DEFAULT_SSL_DIR = Path("C:/local/appmesh/ssl") if os.name == "nt" else Path("/opt/appmesh/ssl")
122
+ DEFAULT_SSL_CA_CERT_PATH = str(_DEFAULT_SSL_DIR / "ca.pem")
123
+ DEFAULT_SSL_CLIENT_CERT_PATH = str(_DEFAULT_SSL_DIR / "client.pem")
124
+ DEFAULT_SSL_CLIENT_KEY_PATH = str(_DEFAULT_SSL_DIR / "client-key.pem")
128
125
 
129
126
  DEFAULT_JWT_AUDIENCE = "appmesh-service"
130
127
 
@@ -199,39 +196,28 @@ class AppMeshClient(metaclass=abc.ABCMeta):
199
196
  def __init__(
200
197
  self,
201
198
  rest_url: str = "https://127.0.0.1:6060",
202
- rest_ssl_verify=DEFAULT_SSL_CA_CERT_PATH if os.path.exists(DEFAULT_SSL_CA_CERT_PATH) else False,
203
- 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,
204
- rest_timeout=(60, 300),
199
+ rest_ssl_verify: Union[bool, str] = DEFAULT_SSL_CA_CERT_PATH,
200
+ rest_ssl_client_cert: Optional[Union[str, Tuple[str, str]]] = (DEFAULT_SSL_CLIENT_CERT_PATH, DEFAULT_SSL_CLIENT_KEY_PATH),
201
+ rest_timeout: Tuple[float, float] = (60, 300),
205
202
  jwt_token: Optional[str] = None,
206
203
  rest_cookie_file: Optional[str] = None,
207
- auto_refresh_token=False,
204
+ auto_refresh_token: bool = False,
208
205
  ):
209
206
  """Initialize an App Mesh HTTP client for interacting with the App Mesh server via secure HTTPS.
210
207
 
211
208
  Args:
212
- rest_url (str, optional): The server's base URI, including protocol, hostname, and port. Defaults to `"https://127.0.0.1:6060"`.
213
-
214
- rest_ssl_verify (Union[bool, str], optional): Configures SSL certificate verification for HTTPS requests:
215
- - `True`: Uses system CA certificates to verify the server's identity.
216
- - `False`: Disables SSL verification (insecure, use cautiously for development).
217
- - `str`: Path to a custom CA certificate or directory for verification. This option allows custom CA configuration,
218
- which may be necessary in environments requiring specific CA chains that differ from the default system CAs.
219
- To use both a custom CA and the system's default CAs, create a combined CA bundle by concatenating them into a single file. (e.g., `cat custom_ca.pem /etc/ssl/certs/ca-certificates.crt > combined_ca.pem`).
220
-
221
- rest_ssl_client_cert (Union[str, Tuple[str, str]], optional): Path to the SSL client certificate and key. Can be:
222
- - `str`: A path to a single PEM file containing both the client certificate and private key.
223
- - `tuple`: A pair of paths (`cert_file`, `key_file`), where `cert_file` is the client certificate file path and `key_file` is the private key file path.
224
-
225
- rest_timeout (tuple, optional): HTTP connection timeouts for API requests, as `(connect_timeout, read_timeout)`.
226
- The default is `(60, 300)`, where `60` seconds is the maximum time to establish a connection and `300` seconds for the maximum read duration.
227
-
228
- rest_cookie_file (str, optional): Path to a file for storing session cookies.
229
- If provided, cookies will be saved to and loaded from this file to maintain session state across client instances instead of keep jwt_token.
230
-
231
- jwt_token (str, optional): JWT token for API authentication, used in headers to authorize requests where required.
232
- auto_refresh_token (bool, optional): Enable automatic token refresh before expiration.
233
- When enabled, a background timer will monitor token expiration and attempt to refresh
234
- the token before it expires. This works with both native App Mesh tokens and Keycloak tokens.
209
+ rest_url: Base server URI (protocol, host, port). Default: `"https://127.0.0.1:6060"`.
210
+ rest_ssl_verify: SSL verification mode for HTTPS requests:
211
+ - True: Use system CAs.
212
+ - False: Disable verification (insecure).
213
+ - str: Path to custom CA or directory. To include system CAs, combine them into one file (e.g., cat custom_ca.pem /etc/ssl/certs/ca-certificates.crt > combined_ca.pem).
214
+ rest_ssl_client_cert: SSL client certificate:
215
+ - str: Path to single PEM with cert+key
216
+ - tuple: (cert_path, key_path)
217
+ rest_timeout: Timeouts `(connect, read)` in seconds. Default `(60, 300)`.
218
+ rest_cookie_file: Cookie file path for session persistence (alternative to jwt_token).
219
+ jwt_token: JWT token for API authentication, overrides cookie file if both provided.
220
+ auto_refresh_token: Auto-refresh JWT before expiry (supports App Mesh and Keycloak tokens).
235
221
  """
236
222
  self._ensure_logging_configured()
237
223
  self.auth_server_url = rest_url
@@ -1202,13 +1188,13 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1202
1188
  ########################################
1203
1189
  # File management
1204
1190
  ########################################
1205
- def download_file(self, remote_file: str, local_file: str, apply_file_attributes: bool = True) -> None:
1191
+ def download_file(self, remote_file: str, local_file: str, preserve_permissions: bool = True) -> None:
1206
1192
  """Download a remote file to the local system. Optionally, the local file will have the same permission as the remote file.
1207
1193
 
1208
1194
  Args:
1209
1195
  remote_file (str): the remote file path.
1210
1196
  local_file (str): the local file path to be downloaded.
1211
- apply_file_attributes (bool): whether to apply file attributes (permissions, owner, group) to the local file.
1197
+ preserve_permissions (bool): whether to apply file attributes (permissions, owner, group) to the local file.
1212
1198
  """
1213
1199
  resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/file/download", header={self.HTTP_HEADER_KEY_X_FILE_PATH: remote_file})
1214
1200
  resp.raise_for_status()
@@ -1220,7 +1206,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1220
1206
  fp.write(chunk)
1221
1207
 
1222
1208
  # Apply file attributes (permissions, owner, group) if requested
1223
- if apply_file_attributes and sys.platform != "win32":
1209
+ if preserve_permissions and sys.platform != "win32":
1224
1210
  if "X-File-Mode" in resp.headers:
1225
1211
  os.chmod(path=local_file, mode=int(resp.headers["X-File-Mode"]))
1226
1212
  if "X-File-User" in resp.headers and "X-File-Group" in resp.headers:
@@ -1231,7 +1217,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1231
1217
  except PermissionError:
1232
1218
  logging.warning(f"Warning: Unable to change owner/group of {local_file}. Operation requires elevated privileges.")
1233
1219
 
1234
- def upload_file(self, local_file: str, remote_file: str, apply_file_attributes: bool = True) -> None:
1220
+ def upload_file(self, local_file: str, remote_file: str, preserve_permissions: bool = True) -> None:
1235
1221
  """Upload a local file to the remote server. Optionally, the remote file will have the same permission as the local file.
1236
1222
 
1237
1223
  Dependency:
@@ -1241,7 +1227,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1241
1227
  Args:
1242
1228
  local_file (str): the local file path.
1243
1229
  remote_file (str): the target remote file to be uploaded.
1244
- apply_file_attributes (bool): whether to upload file attributes (permissions, owner, group) along with the file.
1230
+ preserve_permissions (bool): whether to upload file attributes (permissions, owner, group) along with the file.
1245
1231
  """
1246
1232
  if not os.path.exists(local_file):
1247
1233
  raise FileNotFoundError(f"Local file not found: {local_file}")
@@ -1253,7 +1239,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1253
1239
  header = {self.HTTP_HEADER_KEY_X_FILE_PATH: parse.quote(remote_file), "Content-Type": encoder.content_type}
1254
1240
 
1255
1241
  # Include file attributes (permissions, owner, group) if requested
1256
- if apply_file_attributes:
1242
+ if preserve_permissions:
1257
1243
  file_stat = os.stat(local_file)
1258
1244
  header["X-File-Mode"] = str(file_stat.st_mode & 0o777) # Mask to keep only permission bits
1259
1245
  header["X-File-User"] = str(file_stat.st_uid)
@@ -1486,24 +1472,24 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1486
1472
  header.setdefault("Content-Type", "application/json")
1487
1473
 
1488
1474
  try:
1475
+ request_kwargs = {
1476
+ "url": rest_url,
1477
+ "headers": header,
1478
+ "cert": self.ssl_client_cert,
1479
+ "verify": self.ssl_verify,
1480
+ "timeout": self.rest_timeout,
1481
+ }
1482
+
1489
1483
  if method is AppMeshClient.Method.GET:
1490
- resp = self.session.get(url=rest_url, params=query, headers=header, cert=self.ssl_client_cert, verify=self.ssl_verify, timeout=self.rest_timeout)
1484
+ resp = self.session.get(params=query, **request_kwargs)
1491
1485
  elif method is AppMeshClient.Method.POST:
1492
- resp = self.session.post(
1493
- url=rest_url,
1494
- params=query,
1495
- headers=header,
1496
- data=body,
1497
- cert=self.ssl_client_cert,
1498
- verify=self.ssl_verify,
1499
- timeout=self.rest_timeout,
1500
- )
1486
+ resp = self.session.post(params=query, data=body, **request_kwargs)
1501
1487
  elif method is AppMeshClient.Method.POST_STREAM:
1502
- resp = self.session.post(url=rest_url, params=query, headers=header, data=body, cert=self.ssl_client_cert, verify=self.ssl_verify, stream=True, timeout=self.rest_timeout)
1488
+ resp = self.session.post(params=query, data=body, stream=True, **request_kwargs)
1503
1489
  elif method is AppMeshClient.Method.DELETE:
1504
- resp = self.session.delete(url=rest_url, headers=header, cert=self.ssl_client_cert, verify=self.ssl_verify, timeout=self.rest_timeout)
1490
+ resp = self.session.delete(**request_kwargs)
1505
1491
  elif method is AppMeshClient.Method.PUT:
1506
- resp = self.session.put(url=rest_url, params=query, headers=header, data=body, cert=self.ssl_client_cert, verify=self.ssl_verify, timeout=self.rest_timeout)
1492
+ resp = self.session.put(params=query, data=body, **request_kwargs)
1507
1493
  else:
1508
1494
  raise Exception("Invalid http method", method)
1509
1495
 
@@ -18,8 +18,8 @@ class AppMeshClientOAuth(AppMeshClient):
18
18
  self,
19
19
  oauth2: dict, # Required for Keycloak
20
20
  rest_url: str = "https://127.0.0.1:6060",
21
- rest_ssl_verify=AppMeshClient.DEFAULT_SSL_CA_CERT_PATH if os.path.exists(AppMeshClient.DEFAULT_SSL_CA_CERT_PATH) else False,
22
- rest_ssl_client_cert=(AppMeshClient.DEFAULT_SSL_CLIENT_CERT_PATH, AppMeshClient.DEFAULT_SSL_CLIENT_KEY_PATH) if os.path.exists(AppMeshClient.DEFAULT_SSL_CLIENT_CERT_PATH) else None,
21
+ rest_ssl_verify=AppMeshClient.DEFAULT_SSL_CA_CERT_PATH,
22
+ rest_ssl_client_cert=AppMeshClient.DEFAULT_SSL_CLIENT_CERT_PATH,
23
23
  rest_timeout=(60, 300),
24
24
  jwt_token=None, # Keycloak dict
25
25
  auto_refresh_token: bool = True, # Default to True for Keycloak
@@ -0,0 +1,293 @@
1
+ # client_tcp.py
2
+ # pylint: disable=line-too-long,broad-exception-raised,broad-exception-caught,import-outside-toplevel,protected-access
3
+
4
+ # Standard library imports
5
+ import json
6
+ import os
7
+ import socket
8
+ import sys
9
+ import uuid
10
+ from typing import Optional, Tuple, Union
11
+
12
+ # Third-party imports
13
+ import requests
14
+
15
+ # Local imports
16
+ from .client_http import AppMeshClient
17
+ from .tcp_messages import RequestMessage, ResponseMessage
18
+ from .tcp_transport import TCPTransport
19
+
20
+
21
+ class AppMeshClientTCP(AppMeshClient):
22
+ """Client SDK for interacting with the App Mesh service over TCP.
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
+ Attributes:
32
+ Inherits all attributes from `AppMeshClient`, including TLS secure connections and JWT-based authentication.
33
+
34
+ Methods:
35
+ - download_file()
36
+ - upload_file()
37
+ - Inherits all other methods from `AppMeshClient`, providing a consistent interface for managing applications within App Mesh.
38
+
39
+ Example:
40
+ >>> from appmesh import AppMeshClientTCP
41
+ >>> client = AppMeshClientTCP()
42
+ >>> client.login("your-name", "your-password")
43
+ >>> client.download_file("/tmp/os-release", "os-release")
44
+ """
45
+
46
+ # TLS-optimized chunk size, leaves room for TLS overhead within the 16 KB limit
47
+ TCP_BLOCK_SIZE = 16 * 1024 - 128
48
+ ENCODING_UTF8 = "utf-8"
49
+ HTTP_USER_AGENT_TCP = "appmesh/python/tcp"
50
+ HTTP_HEADER_KEY_X_SEND_FILE_SOCKET = "X-Send-File-Socket"
51
+ HTTP_HEADER_KEY_X_RECV_FILE_SOCKET = "X-Recv-File-Socket"
52
+
53
+ def __init__(
54
+ self,
55
+ rest_ssl_verify: Union[bool, str] = AppMeshClient.DEFAULT_SSL_CA_CERT_PATH,
56
+ rest_ssl_client_cert: Optional[Union[str, Tuple[str, str]]] = None,
57
+ jwt_token: Optional[str] = None,
58
+ tcp_address: Tuple[str, int] = ("127.0.0.1", 6059),
59
+ ):
60
+ """Construct an App Mesh client TCP object to communicate securely with an App Mesh server over TLS.
61
+
62
+ Args:
63
+ rest_ssl_verify: SSL certificate verification behavior. Can be True, False, or a path to CA bundle.
64
+ - True: Use system CA certificates (e.g., /etc/ssl/certs/ on Linux)
65
+ - False: Disable verification (insecure)
66
+ - str: Path to custom CA bundle or directory
67
+ ssl_client_cert: SSL client certificate:
68
+ - str: Path to single PEM with cert+key
69
+ - tuple: (cert_path, key_path)
70
+ jwt_token: Pre-configured JWT Token for authenticating requests.
71
+ tcp_address: Server address as (host, port) tuple, defaults to ("127.0.0.1", 6059).
72
+
73
+ Note:
74
+ TCP connections require an explicit full-chain CA specification for certificate validation,
75
+ unlike HTTP, which can retrieve intermediate certificates automatically.
76
+ """
77
+ self.tcp_transport = TCPTransport(address=tcp_address, ssl_verify=rest_ssl_verify, ssl_client_cert=rest_ssl_client_cert)
78
+ super().__init__(rest_ssl_verify=rest_ssl_verify, rest_ssl_client_cert=rest_ssl_client_cert, jwt_token=jwt_token)
79
+
80
+ def close(self):
81
+ """Close the connection and release resources."""
82
+ if hasattr(self, "tcp_transport") and self.tcp_transport:
83
+ self.tcp_transport.close()
84
+ self.tcp_transport = None
85
+ return super().close()
86
+
87
+ def __del__(self):
88
+ """Ensure resources are properly released when the object is garbage collected."""
89
+ try:
90
+ self.close()
91
+ except Exception:
92
+ pass # Never raise in __del__
93
+
94
+ def _covert_bytes(self, body) -> bytes:
95
+ """Prepare request body for transmission."""
96
+ if body is None:
97
+ return b""
98
+
99
+ if isinstance(body, (bytes, bytearray, memoryview)):
100
+ return body
101
+
102
+ if isinstance(body, str):
103
+ return body.encode(self.ENCODING_UTF8)
104
+
105
+ if isinstance(body, (dict, list)):
106
+ return json.dumps(body).encode(self.ENCODING_UTF8)
107
+
108
+ raise TypeError(f"Unsupported body type: {type(body)}")
109
+
110
+ def _request_http(self, method: AppMeshClient.Method, path: str, query: Optional[dict] = None, header: Optional[dict] = None, body=None) -> requests.Response:
111
+ """Send HTTP request over TCP transport.
112
+
113
+ Args:
114
+ method: HTTP method.
115
+ path: URI path.
116
+ query: Query parameters.
117
+ header: HTTP headers.
118
+ body: Request body.
119
+
120
+ Returns:
121
+ Simulated HTTP response.
122
+ """
123
+
124
+ # Check for unsupported features
125
+ if super().forward_to:
126
+ raise RuntimeError("Forward request is not supported in TCP mode")
127
+
128
+ if not self.tcp_transport.connected():
129
+ self.tcp_transport.connect()
130
+
131
+ # Prepare request message (ensure no fields are assigned None!)
132
+ appmesh_request = RequestMessage()
133
+ appmesh_request.uuid = str(uuid.uuid1())
134
+ appmesh_request.http_method = method.value
135
+ appmesh_request.request_uri = path
136
+ appmesh_request.client_addr = socket.gethostname()
137
+ appmesh_request.headers[self.HTTP_HEADER_KEY_USER_AGENT] = self.HTTP_USER_AGENT_TCP
138
+
139
+ # Add authentication token
140
+ token = self._get_access_token()
141
+ if token:
142
+ appmesh_request.headers[self.HTTP_HEADER_KEY_AUTH] = token
143
+
144
+ # Add custom headers
145
+ if header:
146
+ appmesh_request.headers.update(header)
147
+
148
+ # Add query parameters
149
+ if query:
150
+ appmesh_request.query.update(query)
151
+
152
+ # Prepare body
153
+ body_bytes = self._covert_bytes(body)
154
+ if body_bytes:
155
+ appmesh_request.body = body_bytes
156
+
157
+ # Send request
158
+ data = appmesh_request.serialize()
159
+ self.tcp_transport.send_message(data)
160
+
161
+ # Receive response
162
+ resp_data = self.tcp_transport.receive_message()
163
+ if not resp_data: # Covers None and empty bytes
164
+ self.tcp_transport.close()
165
+ raise ConnectionError("Socket connection broken")
166
+
167
+ # Parse response
168
+ appmesh_resp = ResponseMessage().deserialize(resp_data)
169
+ response = requests.Response()
170
+ response.status_code = appmesh_resp.http_status
171
+ response.headers = appmesh_resp.headers
172
+
173
+ # Set response content
174
+ # response.encoding = self.ENCODING_UTF8 # only need when charset not in appmesh_resp.body_msg_type
175
+ if isinstance(appmesh_resp.body, bytes):
176
+ response._content = appmesh_resp.body
177
+ else:
178
+ response._content = str(appmesh_resp.body).encode(self.ENCODING_UTF8)
179
+
180
+ # Set content type
181
+ if appmesh_resp.body_msg_type:
182
+ response.headers["Content-Type"] = appmesh_resp.body_msg_type
183
+
184
+ return AppMeshClient.EncodingResponse(response)
185
+
186
+ def _apply_file_attributes(self, local_file: str, headers: dict) -> None:
187
+ """Apply file attributes from headers to local file."""
188
+ if sys.platform == "win32":
189
+ return
190
+
191
+ # Apply file mode
192
+ if "X-File-Mode" in headers:
193
+ try:
194
+ os.chmod(local_file, int(headers["X-File-Mode"]))
195
+ except (ValueError, OSError) as e:
196
+ self._logger.warning("Failed to set file mode: %s", e)
197
+
198
+ # Apply file ownership
199
+ if "X-File-User" in headers and "X-File-Group" in headers:
200
+ try:
201
+ file_uid = int(headers["X-File-User"])
202
+ file_gid = int(headers["X-File-Group"])
203
+ os.chown(local_file, uid=file_uid, gid=file_gid)
204
+ except (ValueError, PermissionError, OSError) as e:
205
+ if isinstance(e, PermissionError):
206
+ print(f"Warning: Unable to change owner/group of {local_file}. " "Operation requires elevated privileges.")
207
+ else:
208
+ self._logger.warning("Failed to set file ownership: %s", e)
209
+
210
+ def _get_file_attributes(self, local_file: str) -> dict:
211
+ """Get file attributes as header dictionary."""
212
+ if sys.platform == "win32":
213
+ return {}
214
+
215
+ try:
216
+ file_stat = os.stat(local_file)
217
+ return {
218
+ "X-File-Mode": str(file_stat.st_mode & 0o777),
219
+ "X-File-User": str(file_stat.st_uid),
220
+ "X-File-Group": str(file_stat.st_gid),
221
+ }
222
+ except OSError as e:
223
+ self._logger.warning("Failed to get file attributes: %s", e)
224
+ return {}
225
+
226
+ def download_file(self, remote_file: str, local_file: str, preserve_permissions: bool = True) -> None:
227
+ """Copy a remote file to local, preserving file attributes if requested.
228
+
229
+ Args:
230
+ remote_file: Remote file path.
231
+ local_file: Local destination path.
232
+ preserve_permissions: Apply remote file permissions/ownership locally.
233
+ """
234
+ header = {
235
+ AppMeshClient.HTTP_HEADER_KEY_X_FILE_PATH: remote_file,
236
+ self.HTTP_HEADER_KEY_X_RECV_FILE_SOCKET: "true",
237
+ }
238
+
239
+ resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/file/download", header=header)
240
+ resp.raise_for_status()
241
+
242
+ if self.HTTP_HEADER_KEY_X_RECV_FILE_SOCKET not in resp.headers:
243
+ raise ValueError(f"Server did not respond with socket transfer option: " f"{self.HTTP_HEADER_KEY_X_RECV_FILE_SOCKET}")
244
+
245
+ # Download file chunks
246
+ with open(local_file, "wb") as fp:
247
+ while True:
248
+ chunk_data = self.tcp_transport.receive_message()
249
+ if not chunk_data:
250
+ break
251
+ fp.write(chunk_data)
252
+
253
+ # Apply file attributes if requested
254
+ if preserve_permissions:
255
+ self._apply_file_attributes(local_file, resp.headers)
256
+
257
+ def upload_file(self, local_file: str, remote_file: str, preserve_permissions: bool = True) -> None:
258
+ """Upload a local file to remote server, preserving file attributes if requested.
259
+
260
+ Args:
261
+ local_file: Local file path.
262
+ remote_file: Remote destination path.
263
+ preserve_permissions: Upload file permissions/ownership metadata.
264
+ """
265
+ if not os.path.exists(local_file):
266
+ raise FileNotFoundError(f"Local file not found: {local_file}")
267
+
268
+ # Prepare headers
269
+ header = {
270
+ AppMeshClient.HTTP_HEADER_KEY_X_FILE_PATH: remote_file,
271
+ "Content-Type": "text/plain",
272
+ self.HTTP_HEADER_KEY_X_SEND_FILE_SOCKET: "true",
273
+ }
274
+
275
+ # Add file attributes if requested
276
+ if preserve_permissions:
277
+ header.update(self._get_file_attributes(local_file))
278
+
279
+ # Initiate upload
280
+ resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/file/upload", header=header)
281
+ resp.raise_for_status()
282
+
283
+ if self.HTTP_HEADER_KEY_X_SEND_FILE_SOCKET not in resp.headers:
284
+ raise ValueError(f"Server did not respond with socket transfer option: " f"{self.HTTP_HEADER_KEY_X_SEND_FILE_SOCKET}")
285
+
286
+ # Upload file chunks
287
+ with open(local_file, "rb") as fp:
288
+ while True:
289
+ chunk_data = fp.read(self.TCP_BLOCK_SIZE)
290
+ if not chunk_data:
291
+ self.tcp_transport.send_message([]) # EOF signal
292
+ break
293
+ self.tcp_transport.send_message(chunk_data)
@@ -1,6 +1,8 @@
1
1
  # server_http.py
2
2
  # pylint: disable=line-too-long,broad-exception-raised,broad-exception-caught,import-outside-toplevel,protected-access
3
3
 
4
+ """HTTP server SDK implementation for App Mesh."""
5
+
4
6
  # Standard library imports
5
7
  import abc
6
8
  import logging
@@ -16,8 +18,7 @@ logger = logging.getLogger(__name__)
16
18
 
17
19
 
18
20
  class AppMeshServer(metaclass=abc.ABCMeta):
19
- """
20
- Server SDK for App Mesh application interacting with the local App Mesh REST service over HTTPS.
21
+ """Server SDK for App Mesh application interacting with the local App Mesh REST service over HTTPS.
21
22
 
22
23
  Build-in runtime environment variables required:
23
24
  - APP_MESH_PROCESS_KEY
@@ -34,18 +35,13 @@ class AppMeshServer(metaclass=abc.ABCMeta):
34
35
  context.task_return(result)
35
36
  """
36
37
 
38
+ _RETRY_DELAY_SECONDS = 0.1
39
+
37
40
  def __init__(
38
41
  self,
39
42
  rest_url: str = "https://127.0.0.1:6060",
40
- rest_ssl_verify=AppMeshClient.DEFAULT_SSL_CA_CERT_PATH if os.path.exists(AppMeshClient.DEFAULT_SSL_CA_CERT_PATH) else False,
41
- rest_ssl_client_cert=(
42
- (
43
- AppMeshClient.DEFAULT_SSL_CLIENT_CERT_PATH,
44
- AppMeshClient.DEFAULT_SSL_CLIENT_KEY_PATH,
45
- )
46
- if os.path.exists(AppMeshClient.DEFAULT_SSL_CLIENT_CERT_PATH)
47
- else None
48
- ),
43
+ rest_ssl_verify: Union[bool, str] = AppMeshClient.DEFAULT_SSL_CA_CERT_PATH,
44
+ rest_ssl_client_cert: Optional[Union[str, Tuple[str, str]]] = None,
49
45
  rest_timeout: Tuple[float, float] = (60, 300),
50
46
  *,
51
47
  logger_: Optional[logging.Logger] = None,
@@ -63,6 +59,7 @@ class AppMeshServer(metaclass=abc.ABCMeta):
63
59
  """Read and validate required runtime environment variables."""
64
60
  process_key = os.getenv("APP_MESH_PROCESS_KEY")
65
61
  app_name = os.getenv("APP_MESH_APPLICATION_NAME")
62
+
66
63
  if not process_key:
67
64
  raise Exception("Missing environment variable: APP_MESH_PROCESS_KEY. This must be set by App Mesh service.")
68
65
  if not app_name:
@@ -75,43 +72,43 @@ class AppMeshServer(metaclass=abc.ABCMeta):
75
72
  Used by App Mesh application process to obtain the payload from App Mesh service
76
73
  that a client pushed to it.
77
74
 
78
-
79
75
  Returns:
80
- Union[str, bytes]: The payload provided by the client as returned by the service.
76
+ The payload provided by the client as returned by the service.
81
77
  """
82
78
  pkey, app_name = self._get_runtime_env()
83
79
  path = f"/appmesh/app/{app_name}/task"
80
+ query_params = {"process_key": pkey}
84
81
 
85
82
  while True:
86
83
  resp = self._client._request_http(
87
84
  AppMeshClient.Method.GET,
88
85
  path=path,
89
- query={"process_key": pkey},
86
+ query=query_params,
90
87
  )
91
88
 
92
- if resp.status_code != HTTPStatus.OK:
93
- self._logger.warning(f"task_fetch failed with status {resp.status_code}: {resp.text}, retrying...")
94
- time.sleep(0.1)
95
- continue
89
+ if resp.status_code == HTTPStatus.OK:
90
+ return resp.content
96
91
 
97
- return resp.content
92
+ self._logger.warning("task_fetch failed with status %d: %s, retrying...", resp.status_code, resp.text)
93
+ time.sleep(self._RETRY_DELAY_SECONDS)
98
94
 
99
95
  def task_return(self, result: Union[str, bytes]) -> None:
100
96
  """Return the result of a server-side invocation back to the original client.
101
97
 
102
- Used by App Mesh application process to posts the `result` to App Mesh service
103
- after processed payload data so the invoking client can retrieve it.
98
+ Used by App Mesh application process to post the `result` to App Mesh service
99
+ after processing payload data so the invoking client can retrieve it.
104
100
 
105
101
  Args:
106
- result (Union[str, bytes]): Result payload to be delivered back to the client.
102
+ result: Result payload to be delivered back to the client.
107
103
  """
108
104
  pkey, app_name = self._get_runtime_env()
109
105
  path = f"/appmesh/app/{app_name}/task"
106
+ query_params = {"process_key": pkey}
110
107
 
111
108
  resp = self._client._request_http(
112
109
  AppMeshClient.Method.PUT,
113
110
  path=path,
114
- query={"process_key": pkey},
111
+ query=query_params,
115
112
  body=result,
116
113
  )
117
114
 
@@ -3,8 +3,7 @@
3
3
 
4
4
  # Standard library imports
5
5
  import logging
6
- import os
7
- from typing import Optional, Tuple
6
+ from typing import Optional, Tuple, Union
8
7
 
9
8
  # Local imports
10
9
  from .client_http import AppMeshClient
@@ -15,14 +14,12 @@ logger = logging.getLogger(__name__)
15
14
 
16
15
 
17
16
  class AppMeshServerTCP(AppMeshServer):
18
- """
19
- Server SDK for interacting with the local App Mesh service over TCP (TLS).
20
- """
17
+ """Server SDK for interacting with the local App Mesh service over TCP (TLS)."""
21
18
 
22
19
  def __init__(
23
20
  self,
24
- rest_ssl_verify=AppMeshClient.DEFAULT_SSL_CA_CERT_PATH if os.path.exists(AppMeshClient.DEFAULT_SSL_CA_CERT_PATH) else False,
25
- rest_ssl_client_cert=None,
21
+ rest_ssl_verify: Union[bool, str] = AppMeshClient.DEFAULT_SSL_CA_CERT_PATH,
22
+ rest_ssl_client_cert: Optional[Union[str, Tuple[str, str]]] = None,
26
23
  tcp_address: Tuple[str, int] = ("127.0.0.1", 6059),
27
24
  *,
28
25
  logger_: Optional[logging.Logger] = None,
@@ -34,6 +31,5 @@ class AppMeshServerTCP(AppMeshServer):
34
31
  """
35
32
  # Deliberately avoid calling super().__init__ to inject a TCP client while keeping the same public API.
36
33
  object.__init__(self)
37
- # super().__init__(rest_ssl_verify=rest_ssl_verify, rest_ssl_client_cert=rest_ssl_client_cert)
38
34
  self._client = AppMeshClientTCP(rest_ssl_verify=rest_ssl_verify, rest_ssl_client_cert=rest_ssl_client_cert, tcp_address=tcp_address)
39
35
  self._logger = logger_ or logger
@@ -29,12 +29,10 @@ class TCPTransport:
29
29
 
30
30
  Args:
31
31
  address: Server address as (host, port) tuple.
32
-
33
32
  ssl_verify: SSL server verification mode:
34
33
  - True: Use system CA certificates
35
34
  - False: Disable verification (insecure)
36
35
  - str: Path to custom CA bundle or directory
37
-
38
36
  ssl_client_cert: SSL client certificate:
39
37
  - str: Path to PEM file with cert and key
40
38
  - tuple: (cert_path, key_path)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: appmesh
3
- Version: 1.6.12
3
+ Version: 1.6.14
4
4
  Summary: Client SDK for App Mesh
5
5
  Home-page: https://github.com/laoshanxi/app-mesh
6
6
  Author: laoshanxi
@@ -17,6 +17,7 @@ Requires-Dist: msgpack
17
17
  Requires-Dist: requests_toolbelt
18
18
  Requires-Dist: aniso8601
19
19
  Requires-Dist: PyJWT
20
+ Requires-Dist: dataclasses; python_version < "3.7"
20
21
  Dynamic: author
21
22
  Dynamic: author-email
22
23
  Dynamic: classifier
@@ -3,3 +3,6 @@ msgpack
3
3
  requests_toolbelt
4
4
  aniso8601
5
5
  PyJWT
6
+
7
+ [:python_version < "3.7"]
8
+ dataclasses
@@ -10,7 +10,7 @@ with io.open(os.path.abspath(readme_path), mode="r", encoding="utf-8") as fh:
10
10
 
11
11
  def get_version():
12
12
  """PyPI package version"""
13
- return "1.6.12"
13
+ return "1.6.14"
14
14
 
15
15
 
16
16
  # Dependencies
@@ -20,9 +20,8 @@ install_requires = [
20
20
  "requests_toolbelt",
21
21
  "aniso8601",
22
22
  "PyJWT",
23
+ "dataclasses; python_version < '3.7'",
23
24
  ]
24
- if sys.version_info < (3, 7):
25
- install_requires.append("dataclasses")
26
25
 
27
26
  setuptools.setup(
28
27
  name="appmesh",
@@ -1,246 +0,0 @@
1
- # client_tcp.py
2
- # pylint: disable=line-too-long,broad-exception-raised,broad-exception-caught,import-outside-toplevel,protected-access
3
-
4
- # Standard library imports
5
- import json
6
- import os
7
- import socket
8
- import sys
9
- import uuid
10
-
11
- # Third-party imports
12
- import requests
13
-
14
- # Local imports
15
- from .client_http import AppMeshClient
16
- from .tcp_messages import RequestMessage, ResponseMessage
17
- from .tcp_transport import TCPTransport
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 AppMeshClientTCP
38
-
39
- Example:
40
- 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
- ENCODING_UTF8 = "utf-8"
56
- HTTP_USER_AGENT_TCP = "appmesh/python/tcp"
57
- HTTP_HEADER_KEY_X_SEND_FILE_SOCKET = "X-Send-File-Socket"
58
- HTTP_HEADER_KEY_X_RECV_FILE_SOCKET = "X-Recv-File-Socket"
59
-
60
- def __init__(
61
- self,
62
- rest_ssl_verify=AppMeshClient.DEFAULT_SSL_CA_CERT_PATH if os.path.exists(AppMeshClient.DEFAULT_SSL_CA_CERT_PATH) else False,
63
- rest_ssl_client_cert=None,
64
- jwt_token=None,
65
- tcp_address=("127.0.0.1", 6059),
66
- ):
67
- """Construct an App Mesh client TCP object to communicate securely with an App Mesh server over TLS.
68
-
69
- Args:
70
- rest_ssl_verify (Union[bool, str], optional): Specifies SSL certificate verification behavior. Can be:
71
- - `True`: Uses the system’s default CA certificates to verify the server’s identity.
72
- - `False`: Disables SSL certificate verification (insecure, intended for development).
73
- - `str`: Specifies a custom CA bundle or directory for server certificate verification. If a string is provided,
74
- it should either be a file path to a custom CA certificate (CA bundle) or a directory path containing multiple
75
- certificates (CA directory).
76
-
77
- **Note**: Unlike HTTP requests, TCP connections cannot automatically retrieve intermediate or public CA certificates.
78
- When `rest_ssl_verify` is a path, it explicitly identifies a CA issuer to ensure certificate validation.
79
-
80
- rest_ssl_client_cert (Union[str, Tuple[str, str]], optional): Path to the SSL client certificate and key. If a `str`,
81
- it should be the path to a PEM file containing both the client certificate and private key. If a `tuple`, it should
82
- be a pair of paths: (`cert`, `key`), where `cert` is the client certificate file and `key` is the private key file.
83
-
84
- jwt_token (str, optional): JWT token for authentication. Used in methods requiring login and user authorization.
85
-
86
- tcp_address (Tuple[str, int], optional): Address and port for establishing a TCP connection to the server.
87
- Defaults to `("127.0.0.1", 6059)`.
88
- """
89
- self.tcp_transport = TCPTransport(address=tcp_address, ssl_verify=rest_ssl_verify, ssl_client_cert=rest_ssl_client_cert)
90
- super().__init__(rest_ssl_verify=rest_ssl_verify, rest_ssl_client_cert=rest_ssl_client_cert, jwt_token=jwt_token)
91
-
92
- def close(self):
93
- """Close the connection and release resources."""
94
- if hasattr(self, "tcp_transport") and self.tcp_transport:
95
- self.tcp_transport.close()
96
- self.tcp_transport = None
97
- return super().close()
98
-
99
- def __del__(self):
100
- """Ensure resources are properly released when the object is garbage collected."""
101
- try:
102
- self.close()
103
- except Exception:
104
- pass # Never raise in __del__
105
- super().__del__()
106
-
107
- def _request_http(self, method: AppMeshClient.Method, path: str, query: dict = None, header: dict = None, body=None) -> requests.Response:
108
- """Send HTTP request over TCP transport.
109
-
110
- Args:
111
- method (Method): HTTP method.
112
- path (str): URI path.
113
- query (dict, optional): Query parameters.
114
- header (dict, optional): HTTP headers.
115
- body: Request body.
116
-
117
- Returns:
118
- requests.Response: Simulated HTTP response.
119
- """
120
- if not self.tcp_transport.connected():
121
- self.tcp_transport.connect()
122
-
123
- appmesh_request = RequestMessage()
124
- token = self._get_access_token()
125
- if token:
126
- appmesh_request.headers[self.HTTP_HEADER_KEY_AUTH] = token
127
- if super().forward_to and len(super().forward_to) > 0:
128
- raise Exception("Not support forward request in TCP mode")
129
- appmesh_request.headers[self.HTTP_HEADER_KEY_USER_AGENT] = self.HTTP_USER_AGENT_TCP
130
- appmesh_request.uuid = str(uuid.uuid1())
131
- appmesh_request.http_method = method.value
132
- appmesh_request.request_uri = path
133
- appmesh_request.client_addr = socket.gethostname()
134
-
135
- if body:
136
- if isinstance(body, (dict, list)):
137
- appmesh_request.body = bytes(json.dumps(body, indent=2), self.ENCODING_UTF8)
138
- elif isinstance(body, str):
139
- appmesh_request.body = bytes(body, self.ENCODING_UTF8)
140
- elif isinstance(body, bytes):
141
- appmesh_request.body = body
142
- else:
143
- raise Exception(f"UnSupported body type: {type(body)}")
144
-
145
- if header:
146
- for k, v in header.items():
147
- appmesh_request.headers[k] = v
148
- if query:
149
- for k, v in query.items():
150
- appmesh_request.query[k] = v
151
-
152
- data = appmesh_request.serialize()
153
- self.tcp_transport.send_message(data)
154
-
155
- resp_data = self.tcp_transport.receive_message()
156
- if not resp_data: # Covers None and empty bytes
157
- self.tcp_transport.close()
158
- raise Exception("socket connection broken")
159
-
160
- appmesh_resp = ResponseMessage().deserialize(resp_data)
161
- response = requests.Response()
162
- response.status_code = appmesh_resp.http_status
163
- # response.encoding = self.ENCODING_UTF8 # only need when charset not in appmesh_resp.body_msg_type
164
- response._content = appmesh_resp.body if isinstance(appmesh_resp.body, bytes) else str(appmesh_resp.body).encode(self.ENCODING_UTF8)
165
- response.headers = appmesh_resp.headers
166
- if appmesh_resp.body_msg_type:
167
- response.headers["Content-Type"] = appmesh_resp.body_msg_type
168
-
169
- return AppMeshClient.EncodingResponse(response)
170
-
171
- ########################################
172
- # File management
173
- ########################################
174
- def download_file(self, remote_file: str, local_file: str, apply_file_attributes: bool = True) -> None:
175
- """Copy a remote file to local, preserving file attributes if requested.
176
-
177
- Args:
178
- remote_file (str): Remote file path.
179
- local_file (str): Local destination path.
180
- apply_file_attributes (bool): Apply remote file permissions/ownership locally.
181
- """
182
- header = {
183
- AppMeshClient.HTTP_HEADER_KEY_X_FILE_PATH: remote_file,
184
- self.HTTP_HEADER_KEY_X_RECV_FILE_SOCKET: "true",
185
- }
186
- resp = self._request_http(AppMeshClient.Method.GET, path="/appmesh/file/download", header=header)
187
- resp.raise_for_status()
188
-
189
- if self.HTTP_HEADER_KEY_X_RECV_FILE_SOCKET not in resp.headers:
190
- raise ValueError(f"Server did not respond with socket transfer option: {self.HTTP_HEADER_KEY_X_RECV_FILE_SOCKET}")
191
-
192
- with open(local_file, "wb") as fp:
193
- while True:
194
- chunk_data = self.tcp_transport.receive_message()
195
- if not chunk_data:
196
- break
197
- fp.write(chunk_data)
198
-
199
- if apply_file_attributes and sys.platform != "win32":
200
- if "X-File-Mode" in resp.headers:
201
- os.chmod(path=local_file, mode=int(resp.headers["X-File-Mode"]))
202
- if "X-File-User" in resp.headers and "X-File-Group" in resp.headers:
203
- file_uid = int(resp.headers["X-File-User"])
204
- file_gid = int(resp.headers["X-File-Group"])
205
- try:
206
- os.chown(path=local_file, uid=file_uid, gid=file_gid)
207
- except PermissionError:
208
- print(f"Warning: Unable to change owner/group of {local_file}. Operation requires elevated privileges.")
209
-
210
- def upload_file(self, local_file: str, remote_file: str, apply_file_attributes: bool = True) -> None:
211
- """Upload a local file to remote server, preserving file attributes if requested.
212
-
213
- Args:
214
- local_file (str): Local file path.
215
- remote_file (str): Remote destination path.
216
- apply_file_attributes (bool): Upload file permissions/ownership metadata.
217
- """
218
- if not os.path.exists(local_file):
219
- raise FileNotFoundError(f"Local file not found: {local_file}")
220
-
221
- with open(file=local_file, mode="rb") as fp:
222
- header = {
223
- AppMeshClient.HTTP_HEADER_KEY_X_FILE_PATH: remote_file,
224
- "Content-Type": "text/plain",
225
- self.HTTP_HEADER_KEY_X_SEND_FILE_SOCKET: "true",
226
- }
227
-
228
- if apply_file_attributes:
229
- file_stat = os.stat(local_file)
230
- header["X-File-Mode"] = str(file_stat.st_mode & 0o777) # Mask to keep only permission bits
231
- header["X-File-User"] = str(file_stat.st_uid)
232
- header["X-File-Group"] = str(file_stat.st_gid)
233
-
234
- resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/file/upload", header=header)
235
- resp.raise_for_status()
236
-
237
- if self.HTTP_HEADER_KEY_X_SEND_FILE_SOCKET not in resp.headers:
238
- raise ValueError(f"Server did not respond with socket transfer option: {self.HTTP_HEADER_KEY_X_SEND_FILE_SOCKET}")
239
-
240
- chunk_size = self.TCP_BLOCK_SIZE
241
- while True:
242
- chunk_data = fp.read(chunk_size)
243
- if not chunk_data:
244
- self.tcp_transport.send_message([]) # EOF signal
245
- break
246
- self.tcp_transport.send_message(chunk_data)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes