appmesh 1.4.6__py3-none-any.whl → 1.4.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- appmesh/http_client.py +68 -17
- appmesh/keycloak.py +232 -0
- {appmesh-1.4.6.dist-info → appmesh-1.4.7.dist-info}/METADATA +6 -3
- {appmesh-1.4.6.dist-info → appmesh-1.4.7.dist-info}/RECORD +6 -5
- {appmesh-1.4.6.dist-info → appmesh-1.4.7.dist-info}/WHEEL +1 -1
- {appmesh-1.4.6.dist-info → appmesh-1.4.7.dist-info}/top_level.txt +0 -0
appmesh/http_client.py
CHANGED
@@ -8,12 +8,13 @@ from datetime import datetime
|
|
8
8
|
from enum import Enum, unique
|
9
9
|
from http import HTTPStatus
|
10
10
|
from typing import Optional, Tuple, Union
|
11
|
+
from urllib import parse
|
11
12
|
import aniso8601
|
12
13
|
import requests
|
13
|
-
import urllib
|
14
14
|
from .app import App
|
15
15
|
from .app_run import AppRun
|
16
16
|
from .app_output import AppOutput
|
17
|
+
from .keycloak import KeycloakClient
|
17
18
|
|
18
19
|
|
19
20
|
class AppMeshClient(metaclass=abc.ABCMeta):
|
@@ -131,6 +132,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
131
132
|
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,
|
132
133
|
rest_timeout=(60, 300),
|
133
134
|
jwt_token=None,
|
135
|
+
oauth2_config=None,
|
134
136
|
):
|
135
137
|
"""Initialize an App Mesh HTTP client for interacting with the App Mesh server via secure HTTPS.
|
136
138
|
|
@@ -152,8 +154,14 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
152
154
|
|
153
155
|
jwt_token (str, optional): JWT token for API authentication, used in headers to authorize requests where required.
|
154
156
|
|
157
|
+
oauth2 (Dict[str, Any], optional): Keycloak configuration for oauth2 authentication:
|
158
|
+
- server_url: Keycloak server URL (e.g. "https://keycloak.example.com")
|
159
|
+
- realm: Keycloak realm
|
160
|
+
- client_id: Keycloak client ID
|
161
|
+
- client_secret: Keycloak client secret (optional)
|
155
162
|
"""
|
156
163
|
|
164
|
+
self.session = requests.Session()
|
157
165
|
self.server_url = rest_url
|
158
166
|
self._jwt_token = jwt_token
|
159
167
|
self.ssl_verify = rest_ssl_verify
|
@@ -161,6 +169,33 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
161
169
|
self.rest_timeout = rest_timeout
|
162
170
|
self._forward_to = None
|
163
171
|
|
172
|
+
# Keycloak integration
|
173
|
+
self._keycloak_client = None
|
174
|
+
if oauth2_config:
|
175
|
+
self._keycloak_client = KeycloakClient(
|
176
|
+
server_url=oauth2_config.get("server_url"),
|
177
|
+
realm=oauth2_config.get("realm"),
|
178
|
+
client_id=oauth2_config.get("client_id"),
|
179
|
+
client_secret=oauth2_config.get("client_secret"),
|
180
|
+
ssl_verify=oauth2_config.get("ssl_verify", self.ssl_verify),
|
181
|
+
timeout=oauth2_config.get("timeout", (30, 60)),
|
182
|
+
)
|
183
|
+
|
184
|
+
def close(self):
|
185
|
+
"""Close the session and release resources."""
|
186
|
+
if hasattr(self, 'session'):
|
187
|
+
self.session.close()
|
188
|
+
if self._keycloak_client and hasattr(self._keycloak_client, 'close'):
|
189
|
+
self._keycloak_client.close()
|
190
|
+
|
191
|
+
def __enter__(self):
|
192
|
+
"""Support for context manager protocol."""
|
193
|
+
return self
|
194
|
+
|
195
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
196
|
+
"""Support for context manager protocol, ensuring resources are released."""
|
197
|
+
self.close()
|
198
|
+
|
164
199
|
@property
|
165
200
|
def jwt_token(self) -> str:
|
166
201
|
"""Get the current JWT (JSON Web Token) used for authentication.
|
@@ -266,6 +301,12 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
266
301
|
Returns:
|
267
302
|
str: JWT token.
|
268
303
|
"""
|
304
|
+
# Keycloak authentication if configured
|
305
|
+
if self._keycloak_client:
|
306
|
+
self.jwt_token = self._keycloak_client.authenticate(user_name, user_pwd)
|
307
|
+
return self.jwt_token
|
308
|
+
|
309
|
+
# Standard App Mesh authentication
|
269
310
|
self.jwt_token = None
|
270
311
|
resp = self._request_http(
|
271
312
|
AppMeshClient.Method.POST,
|
@@ -293,7 +334,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
293
334
|
username (str): Username to validate
|
294
335
|
challenge (str): Challenge string from server
|
295
336
|
code (str): TOTP code to validate
|
296
|
-
timeout (Union[int, str], optional): Token expiry timeout.
|
337
|
+
timeout (Union[int, str], optional): Token expiry timeout.
|
297
338
|
Accepts ISO 8601 duration format (e.g., 'P1Y2M3DT4H5M6S', 'P1W') or seconds.
|
298
339
|
Defaults to DURATION_ONE_WEEK_ISO.
|
299
340
|
|
@@ -325,11 +366,11 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
325
366
|
Returns:
|
326
367
|
bool: logoff success or failure.
|
327
368
|
"""
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
return
|
369
|
+
if self.jwt_token:
|
370
|
+
resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/self/logoff")
|
371
|
+
self.jwt_token = None
|
372
|
+
return resp.status_code == HTTPStatus.OK
|
373
|
+
return True
|
333
374
|
|
334
375
|
def authentication(self, token: str, permission: Optional[str] = None, audience: Optional[str] = None) -> bool:
|
335
376
|
"""Deprecated: Use authenticate() instead."""
|
@@ -370,6 +411,12 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
370
411
|
Returns:
|
371
412
|
str: The new JWT token if renew success, the old token will be blocked.
|
372
413
|
"""
|
414
|
+
# Refresh the Keycloak token
|
415
|
+
if self._keycloak_client:
|
416
|
+
self.jwt_token = self._keycloak_client.refresh_tokens()
|
417
|
+
return self.jwt_token
|
418
|
+
|
419
|
+
# Refresh the App Mesh token
|
373
420
|
assert self.jwt_token
|
374
421
|
resp = self._request_http(
|
375
422
|
AppMeshClient.Method.POST,
|
@@ -445,13 +492,13 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
445
492
|
dict: eextract parameters
|
446
493
|
"""
|
447
494
|
parsed_info = {}
|
448
|
-
parsed_uri =
|
495
|
+
parsed_uri = parse.urlparse(totp_uri)
|
449
496
|
|
450
497
|
# Extract label from the path
|
451
498
|
parsed_info["label"] = parsed_uri.path[1:] # Remove the leading slash
|
452
499
|
|
453
500
|
# Extract parameters from the query string
|
454
|
-
query_params =
|
501
|
+
query_params = parse.parse_qs(parsed_uri.query)
|
455
502
|
for key, value in query_params.items():
|
456
503
|
parsed_info[key] = value[0]
|
457
504
|
return parsed_info
|
@@ -953,7 +1000,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
953
1000
|
|
954
1001
|
with open(file=local_file, mode="rb") as fp:
|
955
1002
|
encoder = MultipartEncoder(fields={"filename": os.path.basename(remote_file), "file": ("filename", fp, "application/octet-stream")})
|
956
|
-
header = {"File-Path":
|
1003
|
+
header = {"File-Path": parse.quote(remote_file), "Content-Type": encoder.content_type}
|
957
1004
|
|
958
1005
|
# Include file attributes (permissions, owner, group) if requested
|
959
1006
|
if apply_file_attributes:
|
@@ -1120,7 +1167,11 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1120
1167
|
Returns:
|
1121
1168
|
requests.Response: HTTP response
|
1122
1169
|
"""
|
1123
|
-
|
1170
|
+
# Try to refresh token via Keycloak if using Keycloak and token needs refreshing
|
1171
|
+
if self._keycloak_client and self.jwt_token and not self._keycloak_client.validate_token():
|
1172
|
+
self.jwt_token = self._keycloak_client.get_active_token()
|
1173
|
+
|
1174
|
+
rest_url = parse.urljoin(self.server_url, path)
|
1124
1175
|
|
1125
1176
|
header = {} if header is None else header
|
1126
1177
|
if self.jwt_token:
|
@@ -1129,20 +1180,20 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1129
1180
|
if ":" in self.forward_to:
|
1130
1181
|
header[self.HTTP_HEADER_KEY_X_TARGET_HOST] = self.forward_to
|
1131
1182
|
else:
|
1132
|
-
header[self.HTTP_HEADER_KEY_X_TARGET_HOST] = self.forward_to + ":" + str(
|
1183
|
+
header[self.HTTP_HEADER_KEY_X_TARGET_HOST] = self.forward_to + ":" + str(parse.urlsplit(self.server_url).port)
|
1133
1184
|
header[self.HTTP_HEADER_KEY_USER_AGENT] = self.HTTP_USER_AGENT
|
1134
1185
|
|
1135
1186
|
if method is AppMeshClient.Method.GET:
|
1136
|
-
return
|
1187
|
+
return self.session.get(url=rest_url, params=query, headers=header, cert=self.ssl_client_cert, verify=self.ssl_verify, timeout=self.rest_timeout)
|
1137
1188
|
elif method is AppMeshClient.Method.POST:
|
1138
|
-
return
|
1189
|
+
return self.session.post(
|
1139
1190
|
url=rest_url, params=query, headers=header, data=json.dumps(body) if type(body) in (dict, list) else body, cert=self.ssl_client_cert, verify=self.ssl_verify, timeout=self.rest_timeout
|
1140
1191
|
)
|
1141
1192
|
elif method is AppMeshClient.Method.POST_STREAM:
|
1142
|
-
return
|
1193
|
+
return 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)
|
1143
1194
|
elif method is AppMeshClient.Method.DELETE:
|
1144
|
-
return
|
1195
|
+
return self.session.delete(url=rest_url, headers=header, cert=self.ssl_client_cert, verify=self.ssl_verify, timeout=self.rest_timeout)
|
1145
1196
|
elif method is AppMeshClient.Method.PUT:
|
1146
|
-
return
|
1197
|
+
return self.session.put(url=rest_url, params=query, headers=header, json=body, cert=self.ssl_client_cert, verify=self.ssl_verify, timeout=self.rest_timeout)
|
1147
1198
|
else:
|
1148
1199
|
raise Exception("Invalid http method", method)
|
appmesh/keycloak.py
ADDED
@@ -0,0 +1,232 @@
|
|
1
|
+
# Keycloak authentication client for App Mesh Python SDK
|
2
|
+
|
3
|
+
import base64
|
4
|
+
import json
|
5
|
+
import time
|
6
|
+
from typing import Dict, Optional, Tuple, Any
|
7
|
+
import requests
|
8
|
+
|
9
|
+
|
10
|
+
class KeycloakClient:
|
11
|
+
"""
|
12
|
+
Client for authenticating with Keycloak and obtaining tokens for App Mesh.
|
13
|
+
|
14
|
+
This class handles the OAuth2/OIDC workflow with Keycloak, including:
|
15
|
+
- Direct authentication with username/password
|
16
|
+
- Token refresh
|
17
|
+
- Token validation
|
18
|
+
"""
|
19
|
+
|
20
|
+
def __init__(
|
21
|
+
self,
|
22
|
+
server_url: str,
|
23
|
+
realm: str,
|
24
|
+
client_id: str,
|
25
|
+
client_secret: Optional[str] = None,
|
26
|
+
ssl_verify: bool = True,
|
27
|
+
timeout: Tuple[int, int] = (10, 60),
|
28
|
+
token_refresh_threshold: int = 30,
|
29
|
+
):
|
30
|
+
"""Initialize Keycloak client.
|
31
|
+
|
32
|
+
Args:
|
33
|
+
server_url (str): Keycloak server URL (e.g. https://keycloak.example.com/auth)
|
34
|
+
realm (str): Keycloak realm name
|
35
|
+
client_id (str): Client ID registered in Keycloak
|
36
|
+
client_secret (Optional[str], optional): Client secret if using confidential client. Defaults to None.
|
37
|
+
ssl_verify (bool, optional): Verify SSL certificates. Defaults to True.
|
38
|
+
timeout (Tuple[int, int], optional): Connection and read timeouts. Defaults to (10, 60).
|
39
|
+
token_refresh_threshold (int, optional): Seconds before token expiry to trigger refresh. Defaults to 30.
|
40
|
+
"""
|
41
|
+
self.server_url = server_url.rstrip("/")
|
42
|
+
self.realm = realm
|
43
|
+
self.client_id = client_id
|
44
|
+
self.client_secret = client_secret
|
45
|
+
self.ssl_verify = ssl_verify
|
46
|
+
self.timeout = timeout
|
47
|
+
self.token_refresh_threshold = token_refresh_threshold
|
48
|
+
|
49
|
+
# Token storage
|
50
|
+
self.access_token = None
|
51
|
+
self.refresh_token = None
|
52
|
+
self.token_expires_at = 0
|
53
|
+
|
54
|
+
# Construct the token endpoint URL
|
55
|
+
self.token_endpoint = f"{self.server_url}/realms/{self.realm}/protocol/openid-connect/token"
|
56
|
+
self.userinfo_endpoint = f"{self.server_url}/realms/{self.realm}/protocol/openid-connect/userinfo"
|
57
|
+
|
58
|
+
# Create a session for connection pooling
|
59
|
+
self.session = requests.Session()
|
60
|
+
|
61
|
+
def authenticate(self, username: str, password: str) -> str:
|
62
|
+
"""Authenticate with username and password.
|
63
|
+
|
64
|
+
Args:
|
65
|
+
username (str): Username
|
66
|
+
password (str): Password
|
67
|
+
|
68
|
+
Returns:
|
69
|
+
str: Access token
|
70
|
+
|
71
|
+
Raises:
|
72
|
+
Exception: If authentication fails
|
73
|
+
"""
|
74
|
+
data = {
|
75
|
+
"client_id": self.client_id,
|
76
|
+
"grant_type": "password",
|
77
|
+
"username": username,
|
78
|
+
"password": password,
|
79
|
+
}
|
80
|
+
|
81
|
+
if self.client_secret:
|
82
|
+
data["client_secret"] = self.client_secret
|
83
|
+
|
84
|
+
try:
|
85
|
+
response = self.session.post(self.token_endpoint, data=data, verify=self.ssl_verify, timeout=self.timeout)
|
86
|
+
|
87
|
+
if response.status_code != 200:
|
88
|
+
raise Exception(f"Authentication failed: {response.text}")
|
89
|
+
|
90
|
+
token_data = response.json()
|
91
|
+
self._update_token_data(token_data)
|
92
|
+
return self.access_token
|
93
|
+
|
94
|
+
except requests.RequestException as e:
|
95
|
+
raise Exception(f"Failed to connect to Keycloak: {str(e)}")
|
96
|
+
|
97
|
+
def refresh_tokens(self) -> str:
|
98
|
+
"""Refresh the access token using the refresh token.
|
99
|
+
|
100
|
+
Returns:
|
101
|
+
str: New access token
|
102
|
+
|
103
|
+
Raises:
|
104
|
+
Exception: If refresh fails
|
105
|
+
"""
|
106
|
+
if not self.refresh_token:
|
107
|
+
raise Exception("No refresh token available")
|
108
|
+
|
109
|
+
data = {
|
110
|
+
"client_id": self.client_id,
|
111
|
+
"grant_type": "refresh_token",
|
112
|
+
"refresh_token": self.refresh_token,
|
113
|
+
}
|
114
|
+
|
115
|
+
if self.client_secret:
|
116
|
+
data["client_secret"] = self.client_secret
|
117
|
+
|
118
|
+
try:
|
119
|
+
response = self.session.post(self.token_endpoint, data=data, verify=self.ssl_verify, timeout=self.timeout)
|
120
|
+
|
121
|
+
if response.status_code != 200:
|
122
|
+
raise Exception(f"Token refresh failed: {response.text}")
|
123
|
+
|
124
|
+
token_data = response.json()
|
125
|
+
self._update_token_data(token_data)
|
126
|
+
return self.access_token
|
127
|
+
|
128
|
+
except requests.RequestException as e:
|
129
|
+
raise Exception(f"Failed to connect to Keycloak: {str(e)}")
|
130
|
+
|
131
|
+
def validate_token(self) -> bool:
|
132
|
+
"""Check if the current token is valid and not expired.
|
133
|
+
|
134
|
+
Returns:
|
135
|
+
bool: True if valid, False otherwise
|
136
|
+
"""
|
137
|
+
if not self.access_token:
|
138
|
+
return False
|
139
|
+
|
140
|
+
# Check if the token is expired based on our local expiry time
|
141
|
+
if time.time() > self.token_expires_at:
|
142
|
+
return False
|
143
|
+
|
144
|
+
return True
|
145
|
+
|
146
|
+
def get_active_token(self) -> str:
|
147
|
+
"""Get a valid access token, refreshing if necessary.
|
148
|
+
|
149
|
+
Returns:
|
150
|
+
str: Valid access token
|
151
|
+
|
152
|
+
Raises:
|
153
|
+
Exception: If no valid token can be obtained
|
154
|
+
"""
|
155
|
+
if self.validate_token():
|
156
|
+
return self.access_token
|
157
|
+
|
158
|
+
if self.refresh_token:
|
159
|
+
try:
|
160
|
+
return self.refresh_tokens()
|
161
|
+
except Exception:
|
162
|
+
pass
|
163
|
+
|
164
|
+
raise Exception("No valid token available and unable to refresh")
|
165
|
+
|
166
|
+
def get_user_info(self) -> Dict[str, Any]:
|
167
|
+
"""Get information about the authenticated user.
|
168
|
+
|
169
|
+
Returns:
|
170
|
+
Dict[str, Any]: User information
|
171
|
+
|
172
|
+
Raises:
|
173
|
+
Exception: If request fails
|
174
|
+
"""
|
175
|
+
if not self.access_token:
|
176
|
+
raise Exception("Not authenticated")
|
177
|
+
|
178
|
+
headers = {"Authorization": f"Bearer {self.access_token}"}
|
179
|
+
|
180
|
+
try:
|
181
|
+
response = self.session.get(self.userinfo_endpoint, headers=headers, verify=self.ssl_verify, timeout=self.timeout)
|
182
|
+
|
183
|
+
if response.status_code != 200:
|
184
|
+
raise Exception(f"Failed to get user info: {response.text}")
|
185
|
+
|
186
|
+
return response.json()
|
187
|
+
|
188
|
+
except requests.RequestException as e:
|
189
|
+
raise Exception(f"Failed to connect to Keycloak: {str(e)}")
|
190
|
+
|
191
|
+
def _update_token_data(self, token_data: Dict[str, Any]) -> None:
|
192
|
+
"""Update the stored token data.
|
193
|
+
|
194
|
+
Args:
|
195
|
+
token_data (Dict[str, Any]): Token data from Keycloak
|
196
|
+
"""
|
197
|
+
self.access_token = token_data["access_token"]
|
198
|
+
self.refresh_token = token_data.get("refresh_token")
|
199
|
+
|
200
|
+
# Calculate token expiration time with configurable buffer
|
201
|
+
expires_in = token_data.get("expires_in", 300)
|
202
|
+
self.token_expires_at = time.time() + expires_in - self.token_refresh_threshold
|
203
|
+
|
204
|
+
def close(self) -> None:
|
205
|
+
"""Close the session and release resources."""
|
206
|
+
if hasattr(self, 'session'):
|
207
|
+
self.session.close()
|
208
|
+
|
209
|
+
@staticmethod
|
210
|
+
def decode_token(token: str) -> Dict[str, Any]:
|
211
|
+
"""Decode a JWT token without verification.
|
212
|
+
|
213
|
+
Args:
|
214
|
+
token (str): JWT token
|
215
|
+
|
216
|
+
Returns:
|
217
|
+
Dict[str, Any]: Decoded token payload
|
218
|
+
"""
|
219
|
+
parts = token.split(".")
|
220
|
+
if len(parts) != 3:
|
221
|
+
raise Exception("Invalid token format")
|
222
|
+
|
223
|
+
# Decode the payload (second part)
|
224
|
+
payload = parts[1]
|
225
|
+
# Add padding if necessary
|
226
|
+
payload += "=" * (4 - len(payload) % 4) if len(payload) % 4 else ""
|
227
|
+
|
228
|
+
try:
|
229
|
+
decoded = base64.b64decode(payload)
|
230
|
+
return json.loads(decoded)
|
231
|
+
except Exception as e:
|
232
|
+
raise Exception(f"Failed to decode token: {str(e)}")
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: appmesh
|
3
|
-
Version: 1.4.
|
3
|
+
Version: 1.4.7
|
4
4
|
Summary: Client SDK for App Mesh
|
5
5
|
Home-page: https://github.com/laoshanxi/app-mesh
|
6
6
|
Author: laoshanxi
|
@@ -28,13 +28,14 @@ Dynamic: requires-dist
|
|
28
28
|
Dynamic: requires-python
|
29
29
|
Dynamic: summary
|
30
30
|
|
31
|
-
[![language.badge]][language.url] [![standard.badge]][standard.url] [![
|
31
|
+
[![language.badge]][language.url] [![standard.badge]][standard.url] [![unittest.badge]][unittest.url] [![docker.badge]][docker.url] [![cockpit.badge]][cockpit.url]
|
32
32
|
[](https://app-mesh.readthedocs.io/en/latest/?badge=latest) [](https://gitter.im/app-mesh/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
33
33
|
<a href="https://scan.coverity.com/projects/laoshanxi-app-mesh">
|
34
34
|
<img alt="Coverity Scan Build Status"
|
35
35
|
src="https://img.shields.io/coverity/scan/21528.svg"/>
|
36
36
|
</a>
|
37
37
|
[](https://api.securityscorecards.dev/projects/github.com/laoshanxi/app-mesh)
|
38
|
+
[![release.badge]][release.url] [![pypi.badge]][pypi.url] [![npm.badge]][npm.url]
|
38
39
|
|
39
40
|
# App Mesh: Advanced Application Management Platform
|
40
41
|
|
@@ -148,7 +149,7 @@ Refer to the [Installation doc](https://app-mesh.readthedocs.io/en/latest/Instal
|
|
148
149
|
[standard.url]: https://en.wikipedia.org/wiki/C%2B%2B#Standardization
|
149
150
|
[standard.badge]: https://img.shields.io/badge/C%2B%2B-11%2F14%2F17-blue.svg
|
150
151
|
[release.url]: https://github.com/laoshanxi/app-mesh/releases
|
151
|
-
[release.badge]: https://img.shields.io/github/v/release/laoshanxi/app-mesh
|
152
|
+
[release.badge]: https://img.shields.io/github/v/release/laoshanxi/app-mesh?label=Github%20package
|
152
153
|
[docker.url]: https://hub.docker.com/repository/docker/laoshanxi/appmesh
|
153
154
|
[docker.badge]: https://img.shields.io/docker/pulls/laoshanxi/appmesh.svg
|
154
155
|
[cockpit.url]: https://github.com/laoshanxi/app-mesh-ui
|
@@ -157,3 +158,5 @@ Refer to the [Installation doc](https://app-mesh.readthedocs.io/en/latest/Instal
|
|
157
158
|
[unittest.badge]: https://img.shields.io/badge/UnitTest-Catch2-blue?logo=appveyor
|
158
159
|
[pypi.badge]: https://img.shields.io/pypi/v/appmesh?label=PyPI%3Aappmesh
|
159
160
|
[pypi.url]: https://pypi.org/project/appmesh/
|
161
|
+
[npm.badge]: https://img.shields.io/npm/v/appmesh?label=npm%3Aappmesh
|
162
|
+
[npm.url]: https://www.npmjs.com/package/appmesh
|
@@ -3,11 +3,12 @@ appmesh/app.py,sha256=9Q-SOOej-MH13BU5Dv2iTa-p-sECCJQp6ZX9DjWWmwE,10526
|
|
3
3
|
appmesh/app_output.py,sha256=JK_TMKgjvaw4n_ys_vmN5S4MyWVZpmD7NlKz_UyMIM8,1015
|
4
4
|
appmesh/app_run.py,sha256=9ISKGZ3k3kkbQvSsPfRfkOLqD9xhbqNOM7ork9F4w9c,1712
|
5
5
|
appmesh/appmesh_client.py,sha256=0ltkqHZUq094gKneYmC0bEZCP0X9kHTp9fccKdWFWP0,339
|
6
|
-
appmesh/http_client.py,sha256=
|
6
|
+
appmesh/http_client.py,sha256=dtrZryrkJ-dmT_NQE5v0fZPE00-PCD9VB9xL1Wu8ORI,47403
|
7
|
+
appmesh/keycloak.py,sha256=siyifDaH3EASIti8s0DzxBo477m8eCRmzKwK6dqGRDY,7497
|
7
8
|
appmesh/tcp_client.py,sha256=RkHl5s8jE333BJOgxJqJ_fvjbdRQza7ciV49vLT6YO4,10923
|
8
9
|
appmesh/tcp_messages.py,sha256=w1Kehz_aX4X2CYAUsy9mFVJRhxnLQwwc6L58W4YkQqs,969
|
9
10
|
appmesh/tcp_transport.py,sha256=UMGby2oKV4k7lyXZUMSOe2Je34fb1w7nTkxEpatKLKg,7256
|
10
|
-
appmesh-1.4.
|
11
|
-
appmesh-1.4.
|
12
|
-
appmesh-1.4.
|
13
|
-
appmesh-1.4.
|
11
|
+
appmesh-1.4.7.dist-info/METADATA,sha256=Tinrv6mkVtpVQi-ZZm_rTpCWLu-NoTEUvL-dBAQVaZg,11663
|
12
|
+
appmesh-1.4.7.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
|
13
|
+
appmesh-1.4.7.dist-info/top_level.txt,sha256=-y0MNQOGJxUzLdHZ6E_Rfv5_LNCkV-GTmOBME_b6pg8,8
|
14
|
+
appmesh-1.4.7.dist-info/RECORD,,
|
File without changes
|