appmesh 1.6.1__py3-none-any.whl → 1.6.3__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/__init__.py +36 -6
- appmesh/app.py +2 -2
- appmesh/app_run.py +2 -1
- appmesh/appmesh_client.py +3 -1
- appmesh/client_http.py +108 -90
- appmesh/client_http_oauth.py +138 -0
- appmesh/client_tcp.py +5 -5
- appmesh/server_http.py +6 -6
- appmesh/tcp_messages.py +48 -20
- {appmesh-1.6.1.dist-info → appmesh-1.6.3.dist-info}/METADATA +3 -3
- appmesh-1.6.3.dist-info/RECORD +16 -0
- appmesh-1.6.1.dist-info/RECORD +0 -15
- {appmesh-1.6.1.dist-info → appmesh-1.6.3.dist-info}/WHEEL +0 -0
- {appmesh-1.6.1.dist-info → appmesh-1.6.3.dist-info}/top_level.txt +0 -0
appmesh/__init__.py
CHANGED
@@ -9,10 +9,40 @@ Example:
|
|
9
9
|
client = AppMeshClient()
|
10
10
|
"""
|
11
11
|
|
12
|
-
|
13
|
-
from
|
14
|
-
from .client_tcp import AppMeshClientTCP
|
15
|
-
from .server_http import AppMeshServer
|
16
|
-
from .server_tcp import AppMeshServerTCP
|
12
|
+
import sys
|
13
|
+
from types import ModuleType
|
17
14
|
|
18
|
-
__all__ = ["App", "AppMeshClient", "AppMeshClientTCP", "AppMeshServer", "AppMeshServerTCP"]
|
15
|
+
__all__ = ["App", "AppMeshClient", "AppMeshClientTCP", "AppMeshClientOAuth", "AppMeshServer", "AppMeshServerTCP"]
|
16
|
+
|
17
|
+
_LAZY_IMPORTS = {
|
18
|
+
"App": ("app", "App"), # from .app import App
|
19
|
+
"AppMeshClient": ("client_http", "AppMeshClient"), # from .client_http import AppMeshClient
|
20
|
+
"AppMeshClientTCP": ("client_tcp", "AppMeshClientTCP"), # from .client_tcp import AppMeshClientTCP
|
21
|
+
"AppMeshClientOAuth": ("client_http_oauth", "AppMeshClientOAuth"), # from .client_http_oauth import AppMeshClientOAuth
|
22
|
+
"AppMeshServer": ("server_http", "AppMeshServer"), # from .server_http import AppMeshServer
|
23
|
+
"AppMeshServerTCP": ("server_tcp", "AppMeshServerTCP"), # from .server_tcp import AppMeshServerTCP
|
24
|
+
}
|
25
|
+
|
26
|
+
|
27
|
+
def _lazy_import(name):
|
28
|
+
"""Helper function for lazy importing."""
|
29
|
+
if name in _LAZY_IMPORTS:
|
30
|
+
module_name, attr_name = _LAZY_IMPORTS[name]
|
31
|
+
module = __import__(f"{__name__}.{module_name}", fromlist=[attr_name])
|
32
|
+
return getattr(module, attr_name)
|
33
|
+
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
|
34
|
+
|
35
|
+
|
36
|
+
if sys.version_info >= (3, 7):
|
37
|
+
|
38
|
+
def __getattr__(name):
|
39
|
+
return _lazy_import(name)
|
40
|
+
|
41
|
+
else:
|
42
|
+
# Python 3.6 compatibility
|
43
|
+
class _LazyModule(ModuleType):
|
44
|
+
def __getattr__(self, name):
|
45
|
+
return _lazy_import(name)
|
46
|
+
|
47
|
+
sys.modules[__name__] = _LazyModule(__name__)
|
48
|
+
sys.modules[__name__].__dict__.update(globals())
|
appmesh/app.py
CHANGED
@@ -5,7 +5,7 @@ import json
|
|
5
5
|
import copy
|
6
6
|
|
7
7
|
from datetime import datetime
|
8
|
-
from typing import Optional
|
8
|
+
from typing import Optional, Any
|
9
9
|
from enum import Enum, unique
|
10
10
|
|
11
11
|
# pylint: disable=line-too-long
|
@@ -39,7 +39,7 @@ class App:
|
|
39
39
|
return None
|
40
40
|
|
41
41
|
@staticmethod
|
42
|
-
def _get_native_item(data: dict, key: str) -> Optional[
|
42
|
+
def _get_native_item(data: dict, key: str) -> Optional[Any]:
|
43
43
|
"""Retrieve a deep copy of a value from a dictionary by key, if it exists."""
|
44
44
|
return copy.deepcopy(data[key]) if (data and key in data and data[key] is not None) else None
|
45
45
|
|
appmesh/app_run.py
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
"""Application run object for remote application execution."""
|
3
3
|
|
4
4
|
from contextlib import contextmanager
|
5
|
+
from typing import Optional
|
5
6
|
|
6
7
|
# pylint: disable=line-too-long
|
7
8
|
|
@@ -42,7 +43,7 @@ class AppRun:
|
|
42
43
|
finally:
|
43
44
|
self._client.forward_to = original_value
|
44
45
|
|
45
|
-
def wait(self, stdout_print: bool = True, timeout: int = 0) -> int:
|
46
|
+
def wait(self, stdout_print: bool = True, timeout: int = 0) -> Optional[int]:
|
46
47
|
"""Wait for the asynchronous run to complete.
|
47
48
|
|
48
49
|
Args:
|
appmesh/appmesh_client.py
CHANGED
@@ -5,4 +5,6 @@
|
|
5
5
|
# AppMeshClient, App, and AppOutput classes. The updated implementation can be found
|
6
6
|
# in client_http.py, where these classes are now primarily maintained.
|
7
7
|
|
8
|
-
from .client_http import AppMeshClient
|
8
|
+
from .client_http import AppMeshClient
|
9
|
+
from .app import App
|
10
|
+
from .app_output import AppOutput
|
appmesh/client_http.py
CHANGED
@@ -3,8 +3,10 @@
|
|
3
3
|
import abc
|
4
4
|
import base64
|
5
5
|
import json
|
6
|
+
import locale
|
6
7
|
import logging
|
7
8
|
import os
|
9
|
+
import sys
|
8
10
|
import time
|
9
11
|
from datetime import datetime
|
10
12
|
from enum import Enum, unique
|
@@ -20,6 +22,64 @@ from .app_run import AppRun
|
|
20
22
|
from .app_output import AppOutput
|
21
23
|
|
22
24
|
|
25
|
+
class EncodingResponse:
|
26
|
+
"""Wrapper for requests.Response that handles encoding conversion on Windows."""
|
27
|
+
|
28
|
+
def __init__(self, response: requests.Response):
|
29
|
+
self._response = response
|
30
|
+
self._converted_text = None
|
31
|
+
self._should_convert = False
|
32
|
+
|
33
|
+
# Check if we need to convert encoding on Windows
|
34
|
+
if sys.platform == "win32":
|
35
|
+
content_type = response.headers.get("Content-Type", "").lower()
|
36
|
+
if response.status_code == HTTPStatus.OK and "text/plain" in content_type and "utf-8" in content_type:
|
37
|
+
try:
|
38
|
+
local_encoding = locale.getpreferredencoding()
|
39
|
+
|
40
|
+
if local_encoding.lower() not in ["utf-8", "utf8"]:
|
41
|
+
# Ensure response is decoded as UTF-8 first
|
42
|
+
response.encoding = "utf-8"
|
43
|
+
utf8_text = response.text # This gives us proper Unicode string
|
44
|
+
|
45
|
+
# Convert Unicode to local encoding, then back to Unicode
|
46
|
+
# This simulates how text would appear in local encoding
|
47
|
+
try:
|
48
|
+
local_bytes = utf8_text.encode(local_encoding, errors="replace")
|
49
|
+
self._converted_text = local_bytes.decode(local_encoding)
|
50
|
+
self._should_convert = True
|
51
|
+
except (UnicodeEncodeError, LookupError):
|
52
|
+
# If local encoding can't handle the characters, fall back to UTF-8
|
53
|
+
self._converted_text = utf8_text
|
54
|
+
self._should_convert = True
|
55
|
+
|
56
|
+
except (UnicodeError, LookupError):
|
57
|
+
# If any conversion fails, keep original UTF-8
|
58
|
+
response.encoding = "utf-8"
|
59
|
+
|
60
|
+
@property
|
61
|
+
def text(self):
|
62
|
+
"""Return converted text if needed, otherwise original text."""
|
63
|
+
if self._should_convert and self._converted_text is not None:
|
64
|
+
return self._converted_text
|
65
|
+
# return the original text from _response without modification
|
66
|
+
return self._response.text
|
67
|
+
|
68
|
+
def __getattr__(self, name):
|
69
|
+
"""Dynamically delegate attribute access to the original response"""
|
70
|
+
return getattr(self._response, name)
|
71
|
+
|
72
|
+
def __dir__(self):
|
73
|
+
"""Optional: allow dir() to show attributes of both wrapper and response"""
|
74
|
+
return list(set(dir(self._response) + list(self.__dict__.keys())))
|
75
|
+
|
76
|
+
@property
|
77
|
+
def __class__(self):
|
78
|
+
"""Optional: allow isinstance checks for requests.Response"""
|
79
|
+
# Pretend to be a requests.Response for isinstance checks
|
80
|
+
return requests.Response
|
81
|
+
|
82
|
+
|
23
83
|
class AppMeshClient(metaclass=abc.ABCMeta):
|
24
84
|
"""
|
25
85
|
Client SDK for interacting with the App Mesh service via REST API.
|
@@ -107,7 +167,8 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
107
167
|
DURATION_ONE_WEEK_ISO = "P1W"
|
108
168
|
DURATION_TWO_DAYS_ISO = "P2D"
|
109
169
|
DURATION_TWO_DAYS_HALF_ISO = "P2DT12H"
|
110
|
-
TOKEN_REFRESH_INTERVAL =
|
170
|
+
TOKEN_REFRESH_INTERVAL = 300 # 5 min to refresh token
|
171
|
+
TOKEN_REFRESH_OFFSET = 30 # 30s before token expire to refresh token
|
111
172
|
|
112
173
|
# Platform-aware default SSL paths
|
113
174
|
_DEFAULT_SSL_DIR = "c:/local/appmesh/ssl" if os.name == "nt" else "/opt/appmesh/ssl"
|
@@ -140,7 +201,6 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
140
201
|
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,
|
141
202
|
rest_timeout=(60, 300),
|
142
203
|
jwt_token=None,
|
143
|
-
oauth2=None,
|
144
204
|
auto_refresh_token=False,
|
145
205
|
):
|
146
206
|
"""Initialize an App Mesh HTTP client for interacting with the App Mesh server via secure HTTPS.
|
@@ -163,15 +223,6 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
163
223
|
The default is `(60, 300)`, where `60` seconds is the maximum time to establish a connection and `300` seconds for the maximum read duration.
|
164
224
|
|
165
225
|
jwt_token (str, optional): JWT token for API authentication, used in headers to authorize requests where required.
|
166
|
-
|
167
|
-
oauth2 (Dict[str, Any], optional): Keycloak configuration for oauth2 authentication:
|
168
|
-
- server_url: Keycloak server URL (e.g. "https://keycloak.example.com/auth/")
|
169
|
-
- realm: Keycloak realm
|
170
|
-
- client_id: Keycloak client ID
|
171
|
-
- client_secret: Keycloak client secret (optional)
|
172
|
-
Using this parameter enables Keycloak integration for authentication. The 'python-keycloak' package
|
173
|
-
will be imported on-demand only when this parameter is, make sure package is installed (pip3 install python-keycloak).
|
174
|
-
|
175
226
|
auto_refresh_token (bool, optional): Enable automatic token refresh before expiration.
|
176
227
|
When enabled, a background timer will monitor token expiration and attempt to refresh
|
177
228
|
the token before it expires. This works with both native App Mesh tokens and Keycloak tokens.
|
@@ -185,29 +236,10 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
185
236
|
self.rest_timeout = rest_timeout
|
186
237
|
self._forward_to = None
|
187
238
|
|
188
|
-
# Keycloak integration
|
189
|
-
self._keycloak_openid = None
|
190
|
-
if oauth2:
|
191
|
-
try:
|
192
|
-
from keycloak import KeycloakOpenID
|
193
|
-
|
194
|
-
self._keycloak_openid = KeycloakOpenID(
|
195
|
-
server_url=oauth2.get("auth_server_url"),
|
196
|
-
client_id=oauth2.get("client_id"),
|
197
|
-
realm_name=oauth2.get("realm"),
|
198
|
-
client_secret_key=oauth2.get("client_secret"),
|
199
|
-
verify=self.ssl_verify,
|
200
|
-
timeout=rest_timeout,
|
201
|
-
)
|
202
|
-
except ImportError:
|
203
|
-
logging.error("Keycloak package not installed. Install with: pip install python-keycloak")
|
204
|
-
raise Exception("Keycloak integration requested but python-keycloak package is not installed")
|
205
|
-
|
206
239
|
# Token auto-refresh
|
207
240
|
self._token_refresh_timer = None
|
208
241
|
self._auto_refresh_token = auto_refresh_token
|
209
|
-
|
210
|
-
self._schedule_token_refresh()
|
242
|
+
self.jwt_token = jwt_token # Set property last after all dependencies are initialized
|
211
243
|
|
212
244
|
@staticmethod
|
213
245
|
def _ensure_logging_configured():
|
@@ -215,6 +247,9 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
215
247
|
if not logging.root.handlers:
|
216
248
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
|
217
249
|
|
250
|
+
def _get_access_token(self) -> str:
|
251
|
+
return self.jwt_token
|
252
|
+
|
218
253
|
def _check_and_refresh_token(self):
|
219
254
|
"""Check and refresh token if needed, then schedule next check.
|
220
255
|
|
@@ -232,12 +267,12 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
232
267
|
|
233
268
|
# Check token expiration directly from JWT
|
234
269
|
try:
|
235
|
-
decoded_token = jwt.decode(self.
|
270
|
+
decoded_token = jwt.decode(self._get_access_token(), options={"verify_signature": False})
|
236
271
|
expiry = decoded_token.get("exp", 0)
|
237
272
|
current_time = time.time()
|
238
273
|
time_to_expiry = expiry - current_time
|
239
274
|
# Refresh if token expires within 5 minutes
|
240
|
-
needs_refresh = time_to_expiry <
|
275
|
+
needs_refresh = time_to_expiry < self.TOKEN_REFRESH_OFFSET
|
241
276
|
except Exception as e:
|
242
277
|
logging.debug("Failed to parse JWT token for expiration check: %s", str(e))
|
243
278
|
|
@@ -277,11 +312,11 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
277
312
|
|
278
313
|
# Calculate more precise check time if expiry is known
|
279
314
|
if time_to_expiry is not None:
|
280
|
-
if time_to_expiry <=
|
315
|
+
if time_to_expiry <= self.TOKEN_REFRESH_OFFSET: # Expires within 5 minutes
|
281
316
|
check_interval = 1 # Almost immediate refresh
|
282
317
|
else:
|
283
318
|
# Check at earlier of 5 minutes before expiry or regular interval
|
284
|
-
check_interval = min(time_to_expiry -
|
319
|
+
check_interval = min(time_to_expiry - self.TOKEN_REFRESH_OFFSET, self.TOKEN_REFRESH_INTERVAL)
|
285
320
|
|
286
321
|
# Create timer to execute refresh check
|
287
322
|
self._token_refresh_timer = threading.Timer(check_interval, self._check_and_refresh_token)
|
@@ -303,14 +338,9 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
303
338
|
self.session.close()
|
304
339
|
self.session = None
|
305
340
|
|
306
|
-
#
|
307
|
-
if hasattr(self, "
|
308
|
-
|
309
|
-
self._keycloak_openid.logout(self._jwt_token.get("refresh_token"))
|
310
|
-
except Exception as e:
|
311
|
-
logging.warning("Failed to logout from Keycloak: %s", str(e))
|
312
|
-
finally:
|
313
|
-
self._keycloak_openid = None
|
341
|
+
# Clean token
|
342
|
+
if hasattr(self, "_jwt_token") and self._jwt_token:
|
343
|
+
self._jwt_token = None
|
314
344
|
|
315
345
|
def __enter__(self):
|
316
346
|
"""Support for context manager protocol."""
|
@@ -370,6 +400,13 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
370
400
|
"""
|
371
401
|
self._jwt_token = token
|
372
402
|
|
403
|
+
# handle refresh
|
404
|
+
if self._jwt_token and self._auto_refresh_token:
|
405
|
+
self._schedule_token_refresh()
|
406
|
+
elif self._token_refresh_timer:
|
407
|
+
self._token_refresh_timer.cancel()
|
408
|
+
self._token_refresh_timer = None
|
409
|
+
|
373
410
|
@property
|
374
411
|
def forward_to(self) -> str:
|
375
412
|
"""Get the target host address for request forwarding in a cluster setup.
|
@@ -419,7 +456,14 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
419
456
|
########################################
|
420
457
|
# Security
|
421
458
|
########################################
|
422
|
-
def login(
|
459
|
+
def login(
|
460
|
+
self,
|
461
|
+
user_name: str,
|
462
|
+
user_pwd: str,
|
463
|
+
totp_code: Optional[str] = "",
|
464
|
+
timeout_seconds: Union[str, int] = DURATION_ONE_WEEK_ISO,
|
465
|
+
audience: Optional[str] = None,
|
466
|
+
) -> str:
|
423
467
|
"""Login with user name and password
|
424
468
|
|
425
469
|
Args:
|
@@ -432,20 +476,6 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
432
476
|
Returns:
|
433
477
|
str: JWT token.
|
434
478
|
"""
|
435
|
-
# Keycloak authentication if configured
|
436
|
-
if self._keycloak_openid:
|
437
|
-
self.jwt_token = self._keycloak_openid.token(
|
438
|
-
username=user_name,
|
439
|
-
password=user_pwd,
|
440
|
-
totp=totp_code if totp_code else None,
|
441
|
-
grant_type="password", # grant type for token request: "password" / "client_credentials" / "refresh_token"
|
442
|
-
scope="openid", # what information to include in the token, such as "openid profile email"
|
443
|
-
)
|
444
|
-
|
445
|
-
if self._auto_refresh_token:
|
446
|
-
self._schedule_token_refresh()
|
447
|
-
return self.jwt_token
|
448
|
-
|
449
479
|
# Standard App Mesh authentication
|
450
480
|
self.jwt_token = None
|
451
481
|
resp = self._request_http(
|
@@ -467,8 +497,6 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
467
497
|
else:
|
468
498
|
raise Exception(resp.text)
|
469
499
|
|
470
|
-
if self._auto_refresh_token:
|
471
|
-
self._schedule_token_refresh()
|
472
500
|
return self.jwt_token
|
473
501
|
|
474
502
|
def validate_totp(self, username: str, challenge: str, code: str, timeout: Union[int, str] = DURATION_ONE_WEEK_ISO) -> str:
|
@@ -510,24 +538,12 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
510
538
|
bool: logoff success or failure.
|
511
539
|
"""
|
512
540
|
result = False
|
513
|
-
# Handle Keycloak logout if configured
|
514
|
-
if self._keycloak_openid and self.jwt_token and isinstance(self.jwt_token, dict) and "refresh_token" in self.jwt_token:
|
515
|
-
refresh_token = self.jwt_token.get("refresh_token")
|
516
|
-
self._keycloak_openid.logout(refresh_token)
|
517
|
-
self.jwt_token = None
|
518
|
-
result = True
|
519
|
-
|
520
541
|
# Standard App Mesh logout
|
521
542
|
if self.jwt_token and isinstance(self.jwt_token, str):
|
522
543
|
resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/self/logoff")
|
523
544
|
self.jwt_token = None
|
524
545
|
result = resp.status_code == HTTPStatus.OK
|
525
546
|
|
526
|
-
# Cancel token refresh timer
|
527
|
-
if self._token_refresh_timer:
|
528
|
-
self._token_refresh_timer.cancel()
|
529
|
-
self._token_refresh_timer = None
|
530
|
-
|
531
547
|
return result
|
532
548
|
|
533
549
|
def authentication(self, token: str, permission: Optional[str] = None, audience: Optional[str] = None) -> bool:
|
@@ -577,13 +593,8 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
577
593
|
raise Exception("No token to renew")
|
578
594
|
|
579
595
|
try:
|
580
|
-
# Handle Keycloak token (dictionary format)
|
581
|
-
if self._keycloak_openid and isinstance(self.jwt_token, dict) and "refresh_token" in self.jwt_token:
|
582
|
-
new_token = self._keycloak_openid.refresh_token(self.jwt_token.get("refresh_token"))
|
583
|
-
self.jwt_token = new_token
|
584
|
-
|
585
596
|
# Handle App Mesh token (string format)
|
586
|
-
|
597
|
+
if isinstance(self.jwt_token, str):
|
587
598
|
resp = self._request_http(
|
588
599
|
AppMeshClient.Method.POST,
|
589
600
|
path="/appmesh/token/renew",
|
@@ -986,9 +997,6 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
986
997
|
Returns:
|
987
998
|
dict: user definition.
|
988
999
|
"""
|
989
|
-
if self._keycloak_openid and isinstance(self.jwt_token, dict) and "access_token" in self.jwt_token:
|
990
|
-
return self._keycloak_openid.userinfo(self.jwt_token.get("access_token"))
|
991
|
-
|
992
1000
|
resp = self._request_http(method=AppMeshClient.Method.GET, path="/appmesh/user/self")
|
993
1001
|
if resp.status_code != HTTPStatus.OK:
|
994
1002
|
raise Exception(resp.text)
|
@@ -1238,6 +1246,22 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1238
1246
|
|
1239
1247
|
return resp.text
|
1240
1248
|
|
1249
|
+
def cancle_task(self, app_name: str) -> bool:
|
1250
|
+
"""Client cancle a running task to a App Mesh application.
|
1251
|
+
|
1252
|
+
Args:
|
1253
|
+
app_name (str): Name of the target application (as registered in App Mesh).
|
1254
|
+
|
1255
|
+
Returns:
|
1256
|
+
bool: Task exist and cancled status.
|
1257
|
+
"""
|
1258
|
+
path = f"/appmesh/app/{app_name}/task"
|
1259
|
+
resp = self._request_http(
|
1260
|
+
AppMeshClient.Method.DELETE,
|
1261
|
+
path=path,
|
1262
|
+
)
|
1263
|
+
return resp.status_code == HTTPStatus.OK
|
1264
|
+
|
1241
1265
|
def run_app_async(
|
1242
1266
|
self,
|
1243
1267
|
app: Union[App, str],
|
@@ -1383,7 +1407,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1383
1407
|
# Prepare headers
|
1384
1408
|
header = {} if header is None else header
|
1385
1409
|
if self.jwt_token:
|
1386
|
-
token = self.
|
1410
|
+
token = self._get_access_token()
|
1387
1411
|
header["Authorization"] = "Bearer " + token
|
1388
1412
|
if self.forward_to and len(self.forward_to) > 0:
|
1389
1413
|
if ":" in self.forward_to:
|
@@ -1419,13 +1443,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1419
1443
|
else:
|
1420
1444
|
raise Exception("Invalid http method", method)
|
1421
1445
|
|
1422
|
-
#
|
1423
|
-
|
1424
|
-
resp.encoding = "utf-8"
|
1425
|
-
except Exception:
|
1426
|
-
# If setting encoding fails for any reason, ignore and return the response as-is
|
1427
|
-
pass
|
1428
|
-
|
1429
|
-
return resp
|
1446
|
+
# Wrap the response for encoding handling
|
1447
|
+
return EncodingResponse(resp)
|
1430
1448
|
except requests.exceptions.RequestException as e:
|
1431
1449
|
raise Exception(f"HTTP request failed: {str(e)}")
|
@@ -0,0 +1,138 @@
|
|
1
|
+
# client_http_oauth.py
|
2
|
+
# pylint: disable=line-too-long,broad-exception-caught,too-many-lines, import-outside-toplevel, protected-access
|
3
|
+
import os
|
4
|
+
import logging
|
5
|
+
from typing import Optional, Union
|
6
|
+
from keycloak import KeycloakOpenID
|
7
|
+
from .client_http import AppMeshClient
|
8
|
+
|
9
|
+
|
10
|
+
class AppMeshClientOAuth(AppMeshClient):
|
11
|
+
"""
|
12
|
+
AppMeshClient extended with Keycloak authentication support.
|
13
|
+
|
14
|
+
Managing tokens using Keycloak as the identity provider.
|
15
|
+
"""
|
16
|
+
|
17
|
+
def __init__(
|
18
|
+
self,
|
19
|
+
oauth2: dict, # Required for Keycloak
|
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,
|
23
|
+
rest_timeout=(60, 300),
|
24
|
+
jwt_token=None, # Keycloak dict
|
25
|
+
auto_refresh_token: bool = True, # Default to True for Keycloak
|
26
|
+
):
|
27
|
+
"""Initialize an App Mesh HTTP client with Keycloak support.
|
28
|
+
Args:
|
29
|
+
oauth2 (Dict[str, str]): Keycloak configuration for oauth2 authentication:
|
30
|
+
- auth_server_url: Keycloak server URL (e.g. "https://keycloak.example.com/auth/")
|
31
|
+
- realm: Keycloak realm
|
32
|
+
- client_id: Keycloak client ID
|
33
|
+
- client_secret: Keycloak client secret (optional)
|
34
|
+
Using this parameter enables Keycloak integration for authentication. The 'python-keycloak' package
|
35
|
+
will be imported on-demand only when this parameter is, make sure package is installed (pip3 install python-keycloak).
|
36
|
+
"""
|
37
|
+
# Initialize base class, disabling its Keycloak and auto-refresh logic
|
38
|
+
super().__init__(
|
39
|
+
rest_url=rest_url,
|
40
|
+
rest_ssl_verify=rest_ssl_verify,
|
41
|
+
rest_ssl_client_cert=rest_ssl_client_cert,
|
42
|
+
rest_timeout=rest_timeout,
|
43
|
+
jwt_token=jwt_token,
|
44
|
+
auto_refresh_token=auto_refresh_token,
|
45
|
+
)
|
46
|
+
|
47
|
+
# Keycloak integration
|
48
|
+
self._keycloak_openid = KeycloakOpenID(
|
49
|
+
server_url=oauth2.get("auth_server_url"),
|
50
|
+
client_id=oauth2.get("client_id"),
|
51
|
+
realm_name=oauth2.get("realm"),
|
52
|
+
client_secret_key=oauth2.get("client_secret"),
|
53
|
+
verify=self.ssl_verify,
|
54
|
+
timeout=rest_timeout,
|
55
|
+
)
|
56
|
+
|
57
|
+
# @override, from typing import override avialable from python3.12
|
58
|
+
def _get_access_token(self) -> str:
|
59
|
+
return self.jwt_token.get("access_token", "") if self.jwt_token else None
|
60
|
+
|
61
|
+
def login(
|
62
|
+
self,
|
63
|
+
user_name: str,
|
64
|
+
user_pwd: str,
|
65
|
+
totp_code: Optional[str] = "",
|
66
|
+
timeout_seconds: Union[str, int] = AppMeshClient.DURATION_ONE_WEEK_ISO,
|
67
|
+
) -> dict:
|
68
|
+
"""Login with user name and password using Keycloak.
|
69
|
+
Args:
|
70
|
+
user_name (str): the name of the user.
|
71
|
+
user_pwd (str): the password of the user.
|
72
|
+
totp_code (str, optional): the TOTP code if enabled for the user.
|
73
|
+
timeout_seconds (int | str, optional): token expire timeout of seconds. support ISO 8601 durations (e.g., 'P1Y2M3DT4H5M6S' 'P1W').
|
74
|
+
|
75
|
+
Returns:
|
76
|
+
dict: Keycloak token.
|
77
|
+
"""
|
78
|
+
# Keycloak authentication
|
79
|
+
self.jwt_token = self._keycloak_openid.token(
|
80
|
+
username=user_name,
|
81
|
+
password=user_pwd,
|
82
|
+
totp=totp_code if totp_code else None,
|
83
|
+
grant_type="password", # grant type for token request: "password" / "client_credentials" / "refresh_token"
|
84
|
+
scope="openid", # what information to include in the token, such as "openid profile email"
|
85
|
+
)
|
86
|
+
|
87
|
+
return self.jwt_token
|
88
|
+
|
89
|
+
def logoff(self) -> bool:
|
90
|
+
"""Log out of the current session from Keycloak and clean up."""
|
91
|
+
result = False
|
92
|
+
if self._keycloak_openid and self.jwt_token:
|
93
|
+
try:
|
94
|
+
self._keycloak_openid.logout(self.jwt_token.get("refresh_token"))
|
95
|
+
result = True
|
96
|
+
except Exception as e:
|
97
|
+
logging.warning("Failed to logout from Keycloak: %s", str(e))
|
98
|
+
finally:
|
99
|
+
self.jwt_token = None
|
100
|
+
|
101
|
+
# Call super to handle base class cleanup (timers, session)
|
102
|
+
super_result = super().logoff()
|
103
|
+
|
104
|
+
return result and super_result
|
105
|
+
|
106
|
+
def renew_token(self) -> dict:
|
107
|
+
"""Renew the current Keycloak token."""
|
108
|
+
if not self.jwt_token or not isinstance(self.jwt_token, dict) or "refresh_token" not in self.jwt_token:
|
109
|
+
raise Exception("No Keycloak refresh token available to renew")
|
110
|
+
|
111
|
+
try:
|
112
|
+
# Handle Keycloak token (dictionary format)
|
113
|
+
new_token = self._keycloak_openid.refresh_token(self.jwt_token.get("refresh_token"))
|
114
|
+
self.jwt_token = new_token
|
115
|
+
return self.jwt_token
|
116
|
+
except Exception as e:
|
117
|
+
logging.error("Keycloak token renewal failed: %s", str(e))
|
118
|
+
raise Exception(f"Keycloak token renewal failed: {str(e)}") from e
|
119
|
+
|
120
|
+
def view_self(self) -> dict:
|
121
|
+
"""Get information about the current user, using Keycloak userinfo if applicable.
|
122
|
+
Returns:
|
123
|
+
dict: user definition.
|
124
|
+
"""
|
125
|
+
return self._keycloak_openid.userinfo(self._get_access_token())
|
126
|
+
|
127
|
+
def close(self):
|
128
|
+
"""Close the session and release resources, including Keycloak logout."""
|
129
|
+
# Logout from Keycloak if needed
|
130
|
+
if hasattr(self, "_keycloak_openid") and self._keycloak_openid and hasattr(self, "_jwt_token") and self._jwt_token and isinstance(self._jwt_token, dict) and "refresh_token" in self._jwt_token:
|
131
|
+
try:
|
132
|
+
self._keycloak_openid.logout(self._jwt_token.get("refresh_token"))
|
133
|
+
except Exception as e:
|
134
|
+
logging.warning("Failed to logout from Keycloak: %s", str(e))
|
135
|
+
finally:
|
136
|
+
self._keycloak_openid = None
|
137
|
+
# Close the base class session and resources (timers, etc.)
|
138
|
+
super().close()
|
appmesh/client_tcp.py
CHANGED
@@ -6,7 +6,7 @@ import os
|
|
6
6
|
import socket
|
7
7
|
import uuid
|
8
8
|
import requests
|
9
|
-
from .client_http import AppMeshClient
|
9
|
+
from .client_http import AppMeshClient, EncodingResponse
|
10
10
|
from .tcp_transport import TCPTransport
|
11
11
|
from .tcp_messages import RequestMessage, ResponseMessage
|
12
12
|
|
@@ -140,7 +140,7 @@ class AppMeshClientTCP(AppMeshClient):
|
|
140
140
|
appmesh_request.headers[k] = v
|
141
141
|
if query:
|
142
142
|
for k, v in query.items():
|
143
|
-
appmesh_request.
|
143
|
+
appmesh_request.query[k] = v
|
144
144
|
|
145
145
|
data = appmesh_request.serialize()
|
146
146
|
self.tcp_transport.send_message(data)
|
@@ -153,13 +153,13 @@ class AppMeshClientTCP(AppMeshClient):
|
|
153
153
|
appmesh_resp = ResponseMessage().deserialize(resp_data)
|
154
154
|
response = requests.Response()
|
155
155
|
response.status_code = appmesh_resp.http_status
|
156
|
-
response.encoding = self.ENCODING_UTF8
|
157
|
-
response._content = appmesh_resp.body.encode(self.ENCODING_UTF8)
|
156
|
+
# response.encoding = self.ENCODING_UTF8 # only need when charset not in appmesh_resp.body_msg_type
|
157
|
+
response._content = appmesh_resp.body if isinstance(appmesh_resp.body, bytes) else str(appmesh_resp.body).encode(self.ENCODING_UTF8)
|
158
158
|
response.headers = appmesh_resp.headers
|
159
159
|
if appmesh_resp.body_msg_type:
|
160
160
|
response.headers["Content-Type"] = appmesh_resp.body_msg_type
|
161
161
|
|
162
|
-
return response
|
162
|
+
return EncodingResponse(response)
|
163
163
|
|
164
164
|
########################################
|
165
165
|
# File management
|
appmesh/server_http.py
CHANGED
@@ -5,7 +5,7 @@ import abc
|
|
5
5
|
import logging
|
6
6
|
import os
|
7
7
|
import time
|
8
|
-
from typing import Optional, Tuple
|
8
|
+
from typing import Optional, Tuple, Union
|
9
9
|
from http import HTTPStatus
|
10
10
|
from .client_http import AppMeshClient
|
11
11
|
|
@@ -66,7 +66,7 @@ class AppMeshServer(metaclass=abc.ABCMeta):
|
|
66
66
|
raise Exception("Missing environment variable: APP_MESH_APPLICATION_NAME. This must be set by App Mesh service.")
|
67
67
|
return process_id, app_name
|
68
68
|
|
69
|
-
def task_fetch(self) -> str:
|
69
|
+
def task_fetch(self) -> Union[str, bytes]:
|
70
70
|
"""Fetch task data in the currently running App Mesh application process.
|
71
71
|
|
72
72
|
Used by App Mesh application process to obtain the payload from App Mesh service
|
@@ -74,7 +74,7 @@ class AppMeshServer(metaclass=abc.ABCMeta):
|
|
74
74
|
|
75
75
|
|
76
76
|
Returns:
|
77
|
-
str: The payload provided by the client as returned by the service.
|
77
|
+
Union[str, bytes]: The payload provided by the client as returned by the service.
|
78
78
|
"""
|
79
79
|
process_id, app_name = self._get_runtime_env()
|
80
80
|
path = f"/appmesh/app/{app_name}/task"
|
@@ -91,16 +91,16 @@ class AppMeshServer(metaclass=abc.ABCMeta):
|
|
91
91
|
time.sleep(0.1)
|
92
92
|
continue
|
93
93
|
|
94
|
-
return resp.
|
94
|
+
return resp.content
|
95
95
|
|
96
|
-
def task_return(self, result: str) -> None:
|
96
|
+
def task_return(self, result: Union[str, bytes]) -> None:
|
97
97
|
"""Return the result of a server-side invocation back to the original client.
|
98
98
|
|
99
99
|
Used by App Mesh application process to posts the `result` to App Mesh service
|
100
100
|
after processed payload data so the invoking client can retrieve it.
|
101
101
|
|
102
102
|
Args:
|
103
|
-
result (str): Result payload to be delivered back to the client.
|
103
|
+
result (Union[str, bytes]): Result payload to be delivered back to the client.
|
104
104
|
"""
|
105
105
|
process_id, app_name = self._get_runtime_env()
|
106
106
|
path = f"/appmesh/app/{app_name}/task"
|
appmesh/tcp_messages.py
CHANGED
@@ -1,41 +1,69 @@
|
|
1
1
|
# tcp_messages.py
|
2
2
|
|
3
|
+
from typing import get_type_hints
|
3
4
|
import msgpack
|
4
5
|
|
5
6
|
|
6
7
|
class RequestMessage:
|
7
8
|
"""TCP request message for HTTP-like communication"""
|
8
9
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
10
|
+
def __init__(self):
|
11
|
+
self.uuid: str = ""
|
12
|
+
self.request_uri: str = ""
|
13
|
+
self.http_method: str = ""
|
14
|
+
self.client_addr: str = ""
|
15
|
+
self.body: bytes = b""
|
16
|
+
self.headers: dict = {}
|
17
|
+
self.query: dict = {}
|
16
18
|
|
17
19
|
def serialize(self) -> bytes:
|
18
20
|
"""Serialize request message to bytes"""
|
19
|
-
|
20
|
-
self_dict = vars(self)
|
21
|
-
self_dict["headers"] = self.headers
|
22
|
-
self_dict["querys"] = self.querys
|
23
|
-
return msgpack.dumps(self_dict)
|
21
|
+
return msgpack.dumps(vars(self), use_bin_type=True)
|
24
22
|
|
25
23
|
|
26
24
|
class ResponseMessage:
|
27
25
|
"""TCP response message for HTTP-like communication"""
|
28
26
|
|
29
|
-
uuid: str
|
30
|
-
request_uri: str
|
31
|
-
http_status: int
|
32
|
-
body_msg_type: str
|
33
|
-
body:
|
34
|
-
headers: dict
|
27
|
+
uuid: str
|
28
|
+
request_uri: str
|
29
|
+
http_status: int
|
30
|
+
body_msg_type: str
|
31
|
+
body: bytes
|
32
|
+
headers: dict
|
33
|
+
|
34
|
+
def __init__(self):
|
35
|
+
self.uuid = ""
|
36
|
+
self.request_uri = ""
|
37
|
+
self.http_status = 0
|
38
|
+
self.body_msg_type = ""
|
39
|
+
self.body = b""
|
40
|
+
self.headers = {}
|
35
41
|
|
36
42
|
def deserialize(self, buf: bytes):
|
37
|
-
"""Deserialize
|
38
|
-
dic = msgpack.unpackb(buf)
|
43
|
+
"""Deserialize TCP msgpack buffer with proper type conversion."""
|
44
|
+
dic = msgpack.unpackb(buf, raw=False)
|
45
|
+
hints = get_type_hints(self.__class__)
|
46
|
+
|
39
47
|
for k, v in dic.items():
|
48
|
+
if k not in hints:
|
49
|
+
continue # Skip unknown fields
|
50
|
+
|
51
|
+
# handle all types (int, bytes, dict, str)
|
52
|
+
t = hints[k]
|
53
|
+
if t is str:
|
54
|
+
if isinstance(v, bytes):
|
55
|
+
v = v.decode("utf-8", errors="replace")
|
56
|
+
elif v is None:
|
57
|
+
v = ""
|
58
|
+
else:
|
59
|
+
v = str(v)
|
60
|
+
elif t is bytes:
|
61
|
+
if isinstance(v, str):
|
62
|
+
v = v.encode("utf-8") # handle accidental str
|
63
|
+
elif v is None:
|
64
|
+
v = b""
|
65
|
+
elif t is int:
|
66
|
+
v = int(v or 0)
|
40
67
|
setattr(self, k, v)
|
68
|
+
|
41
69
|
return self
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: appmesh
|
3
|
-
Version: 1.6.
|
3
|
+
Version: 1.6.3
|
4
4
|
Summary: Client SDK for App Mesh
|
5
5
|
Home-page: https://github.com/laoshanxi/app-mesh
|
6
6
|
Author: laoshanxi
|
@@ -40,6 +40,7 @@ Dynamic: summary
|
|
40
40
|
|
41
41
|
# App Mesh: Advanced Application Management Platform
|
42
42
|
|
43
|
+
<div align=center><img src="https://github.com/laoshanxi/picture/raw/master/appmesh/whatis.gif" align=center /></div>
|
43
44
|
App Mesh is an open-source, multi-tenant application management platform designed for cloud-native environments. It efficiently manages, schedules, and monitors both microservices and traditional applications, offering a lightweight alternative to Kubernetes. App Mesh bridges the gap between simple process managers and complex container orchestration systems, making it ideal for organizations seeking to modernize their infrastructure without adopting full container-native complexity. Supporting both containerized and native applications, it provides a versatile solution for diverse enterprise needs.
|
44
45
|
|
45
46
|
<div align=center><img src="https://github.com/laoshanxi/picture/raw/master/appmesh/diagram.png" align=center /></div>
|
@@ -54,7 +55,7 @@ Cloud native | Schedule cloud-level applications to run on multiple hosts with r
|
|
54
55
|
Micro service application | 🧱 [Consul micro-service cluster management](https://app-mesh.readthedocs.io/en/latest/CONSUL.html)
|
55
56
|
Extra Features | Collect host/app resource usage <br> Remote shell command execution <br> File upload/download interface <br> Hot-update support `systemctl reload appmesh` <br> Bash completion <br> Reverse proxy <br> 🌐[Web GUI](https://github.com/laoshanxi/app-mesh-ui)
|
56
57
|
Platform support | X86_64 <br> ARM32 <br> ARM64
|
57
|
-
SDK | [Python](https://app-mesh.readthedocs.io/en/latest/api/
|
58
|
+
SDK | [Python](https://app-mesh.readthedocs.io/en/latest/api/appmesh.html#module-appmesh.client_http) <br> [Golang](https://github.com/laoshanxi/app-mesh/blob/main/src/sdk/go/client_http.go) <br> [JavaScript](https://www.npmjs.com/package/appmesh) <br> [Java](https://github.com/laoshanxi/app-mesh/packages/2227502) <br> [Swagger OpenAPI Specification](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/laoshanxi/app-mesh/main/src/daemon/rest/openapi.yaml)
|
58
59
|
|
59
60
|
## Getting started
|
60
61
|
|
@@ -116,7 +117,6 @@ Refer to the [Installation doc](https://app-mesh.readthedocs.io/en/latest/Instal
|
|
116
117
|
|
117
118
|
- [Build a powerful monitor system with Grafana/Prometheus/Loki](https://app-mesh.readthedocs.io/en/latest/success/build_powerful_monitor_system_with_Grafana_Prometheus_Loki.html)
|
118
119
|
- [Customize application start behavior](https://app-mesh.readthedocs.io/en/latest/success/customize_app_startup_behavior.html)
|
119
|
-
- [Manage cluster-level microservice applications](https://app-mesh.readthedocs.io/en/latest/success/manage_cluster_level_microservice_applications.html)
|
120
120
|
- [Open service broker support local PV for Kubernetes](https://app-mesh.readthedocs.io/en/latest/success/open_service_broker_support_local_pv_for_K8S.html)
|
121
121
|
- [Promote native application to microservice application](https://app-mesh.readthedocs.io/en/latest/success/promote_native_app_to_microservice_app.html)
|
122
122
|
- [Secure REST file server](https://app-mesh.readthedocs.io/en/latest/success/secure_REST_file_server.html)
|
@@ -0,0 +1,16 @@
|
|
1
|
+
appmesh/__init__.py,sha256=TY1y5B5cE57uhraEzCFOZRWuo9SY1R-fYNRan8hCZOM,1670
|
2
|
+
appmesh/app.py,sha256=crD4DRFZJuHtZMfSsz7C-EwvjPmGZbFXYXvA_wCdvdI,10734
|
3
|
+
appmesh/app_output.py,sha256=vfn322AyixblI8DbXds08h6L_ybObiaRSifsA1-Xcoo,1035
|
4
|
+
appmesh/app_run.py,sha256=aYq852a29OThIi32Xtx5s0sTXZ97T0lHD5WXH8yfPoc,2018
|
5
|
+
appmesh/appmesh_client.py,sha256=ywB2222PtJUffdfdxZcBfdhZs1KYyc7JvzMxwuK2qyI,378
|
6
|
+
appmesh/client_http.py,sha256=WLdHmi2ryzCiXFyIIAbKH3arfcs__rxt4kwehpO_w6s,57711
|
7
|
+
appmesh/client_http_oauth.py,sha256=1d51o0JX_xtB8d2bEuM7_XJHcwMnhcjkbIq7GE1Zxm8,6120
|
8
|
+
appmesh/client_tcp.py,sha256=OrKmJ-SMSmCPXj5WVhWwBXiK8Z_WyCrDmTim0pe1pc0,11407
|
9
|
+
appmesh/server_http.py,sha256=vf_Kh7ZIyEuBijZp8I2Rv-Fy9gxFdPFn5Pp2rUNCT1U,4319
|
10
|
+
appmesh/server_tcp.py,sha256=biBFF5IGWFOw2ru831cfmzn1DVXcBm9e-W6CP2VkfzE,1444
|
11
|
+
appmesh/tcp_messages.py,sha256=H9S_iCy0IuufY2v50_SUgRvcyQmJsySG65tBe_xb3Ko,1878
|
12
|
+
appmesh/tcp_transport.py,sha256=f28zfZNH46tUHfT8F1PrCM1wUXiSBIW7R3ipMsXJqIU,8946
|
13
|
+
appmesh-1.6.3.dist-info/METADATA,sha256=Hcymb5xi90fgxLHqHNWHzicT2KLZzGPo4DonII-IQBQ,11828
|
14
|
+
appmesh-1.6.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
15
|
+
appmesh-1.6.3.dist-info/top_level.txt,sha256=-y0MNQOGJxUzLdHZ6E_Rfv5_LNCkV-GTmOBME_b6pg8,8
|
16
|
+
appmesh-1.6.3.dist-info/RECORD,,
|
appmesh-1.6.1.dist-info/RECORD
DELETED
@@ -1,15 +0,0 @@
|
|
1
|
-
appmesh/__init__.py,sha256=_I-kLcLy7DM3gG2JJTRM_hUxBu3zvIPm2wCsz3NYes4,493
|
2
|
-
appmesh/app.py,sha256=4lo66ob1ZFDgL8VYKxH4_sh-vbHcGkZNThURE0go-d4,10732
|
3
|
-
appmesh/app_output.py,sha256=vfn322AyixblI8DbXds08h6L_ybObiaRSifsA1-Xcoo,1035
|
4
|
-
appmesh/app_run.py,sha256=TJ9xVX8xqUN9-OOmr_8JlgXoyg3hTmDFNKvGf4w_oR4,1980
|
5
|
-
appmesh/appmesh_client.py,sha256=7yy9c_ygJbqMekrUsHWmRObwHLK82qP65xQLXxfGd8U,339
|
6
|
-
appmesh/client_http.py,sha256=jUIZJHrnu2-r6N6Cd9lQNPeQcZefB7ENl6DUn8Mugck,57896
|
7
|
-
appmesh/client_tcp.py,sha256=hE0T_2Z0OQZdF5zi--iuvu2_-ka0DfSSQ4BP3oHlg44,11243
|
8
|
-
appmesh/server_http.py,sha256=zr9sfS8g_mtP2kuAfmz-qU7rLSFTVt3srbFXaCbl8Y0,4253
|
9
|
-
appmesh/server_tcp.py,sha256=biBFF5IGWFOw2ru831cfmzn1DVXcBm9e-W6CP2VkfzE,1444
|
10
|
-
appmesh/tcp_messages.py,sha256=4XRv5lSm_8ElMg_37SuyIRrfxI7XFNNP_7SdO7us3PA,1023
|
11
|
-
appmesh/tcp_transport.py,sha256=f28zfZNH46tUHfT8F1PrCM1wUXiSBIW7R3ipMsXJqIU,8946
|
12
|
-
appmesh-1.6.1.dist-info/METADATA,sha256=DvJTf5922gT6fc-iOWizqodwljYIkIlCCI1SeTd0juM,11847
|
13
|
-
appmesh-1.6.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
14
|
-
appmesh-1.6.1.dist-info/top_level.txt,sha256=-y0MNQOGJxUzLdHZ6E_Rfv5_LNCkV-GTmOBME_b6pg8,8
|
15
|
-
appmesh-1.6.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|