appmesh 1.4.5__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 +97 -27
- appmesh/keycloak.py +232 -0
- {appmesh-1.4.5.dist-info → appmesh-1.4.7.dist-info}/METADATA +6 -3
- {appmesh-1.4.5.dist-info → appmesh-1.4.7.dist-info}/RECORD +6 -5
- {appmesh-1.4.5.dist-info → appmesh-1.4.7.dist-info}/WHEEL +1 -1
- {appmesh-1.4.5.dist-info → appmesh-1.4.7.dist-info}/top_level.txt +0 -0
appmesh/http_client.py
CHANGED
@@ -14,6 +14,7 @@ import requests
|
|
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,
|
@@ -279,38 +320,57 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
279
320
|
if resp.status_code == HTTPStatus.OK:
|
280
321
|
if "Access-Token" in resp.json():
|
281
322
|
self.jwt_token = resp.json()["Access-Token"]
|
282
|
-
elif resp.status_code == HTTPStatus.
|
323
|
+
elif resp.status_code == HTTPStatus.PRECONDITION_REQUIRED and "Totp-Challenge" in resp.json():
|
283
324
|
challenge = resp.json()["Totp-Challenge"]
|
284
|
-
|
285
|
-
AppMeshClient.Method.POST,
|
286
|
-
path="/appmesh/totp/validate",
|
287
|
-
header={
|
288
|
-
"Username": base64.b64encode(user_name.encode()).decode(),
|
289
|
-
"Totp-Challenge": base64.b64encode(challenge.encode()).decode(),
|
290
|
-
"Totp": totp_code,
|
291
|
-
"Expire-Seconds": self._parse_duration(timeout_seconds),
|
292
|
-
},
|
293
|
-
)
|
294
|
-
if resp.status_code == HTTPStatus.OK:
|
295
|
-
if "Access-Token" in resp.json():
|
296
|
-
self.jwt_token = resp.json()["Access-Token"]
|
297
|
-
else:
|
298
|
-
raise Exception(resp.text)
|
325
|
+
self.validate_totp(user_name, challenge, totp_code, timeout_seconds)
|
299
326
|
else:
|
300
327
|
raise Exception(resp.text)
|
301
328
|
return self.jwt_token
|
302
329
|
|
330
|
+
def validate_totp(self, username: str, challenge: str, code: str, timeout: Union[int, str] = DURATION_ONE_WEEK_ISO) -> str:
|
331
|
+
"""Validate TOTP challenge and obtain a new JWT token.
|
332
|
+
|
333
|
+
Args:
|
334
|
+
username (str): Username to validate
|
335
|
+
challenge (str): Challenge string from server
|
336
|
+
code (str): TOTP code to validate
|
337
|
+
timeout (Union[int, str], optional): Token expiry timeout.
|
338
|
+
Accepts ISO 8601 duration format (e.g., 'P1Y2M3DT4H5M6S', 'P1W') or seconds.
|
339
|
+
Defaults to DURATION_ONE_WEEK_ISO.
|
340
|
+
|
341
|
+
Returns:
|
342
|
+
str: New JWT token if validation succeeds
|
343
|
+
|
344
|
+
Raises:
|
345
|
+
Exception: If validation fails or server returns error
|
346
|
+
"""
|
347
|
+
resp = self._request_http(
|
348
|
+
AppMeshClient.Method.POST,
|
349
|
+
path="/appmesh/totp/validate",
|
350
|
+
header={
|
351
|
+
"Username": base64.b64encode(username.encode()).decode(),
|
352
|
+
"Totp": code,
|
353
|
+
"Totp-Challenge": base64.b64encode(challenge.encode()).decode(),
|
354
|
+
"Expire-Seconds": self._parse_duration(timeout),
|
355
|
+
},
|
356
|
+
)
|
357
|
+
if resp.status_code == HTTPStatus.OK:
|
358
|
+
if "Access-Token" in resp.json():
|
359
|
+
self.jwt_token = resp.json()["Access-Token"]
|
360
|
+
return self.jwt_token
|
361
|
+
raise Exception(resp.text)
|
362
|
+
|
303
363
|
def logoff(self) -> bool:
|
304
364
|
"""Log out of the current session from the server.
|
305
365
|
|
306
366
|
Returns:
|
307
367
|
bool: logoff success or failure.
|
308
368
|
"""
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
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
|
314
374
|
|
315
375
|
def authentication(self, token: str, permission: Optional[str] = None, audience: Optional[str] = None) -> bool:
|
316
376
|
"""Deprecated: Use authenticate() instead."""
|
@@ -351,6 +411,12 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
351
411
|
Returns:
|
352
412
|
str: The new JWT token if renew success, the old token will be blocked.
|
353
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
|
354
420
|
assert self.jwt_token
|
355
421
|
resp = self._request_http(
|
356
422
|
AppMeshClient.Method.POST,
|
@@ -934,7 +1000,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
934
1000
|
|
935
1001
|
with open(file=local_file, mode="rb") as fp:
|
936
1002
|
encoder = MultipartEncoder(fields={"filename": os.path.basename(remote_file), "file": ("filename", fp, "application/octet-stream")})
|
937
|
-
header = {"File-Path": remote_file, "Content-Type": encoder.content_type}
|
1003
|
+
header = {"File-Path": parse.quote(remote_file), "Content-Type": encoder.content_type}
|
938
1004
|
|
939
1005
|
# Include file attributes (permissions, owner, group) if requested
|
940
1006
|
if apply_file_attributes:
|
@@ -1101,6 +1167,10 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1101
1167
|
Returns:
|
1102
1168
|
requests.Response: HTTP response
|
1103
1169
|
"""
|
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
|
+
|
1104
1174
|
rest_url = parse.urljoin(self.server_url, path)
|
1105
1175
|
|
1106
1176
|
header = {} if header is None else header
|
@@ -1114,16 +1184,16 @@ class AppMeshClient(metaclass=abc.ABCMeta):
|
|
1114
1184
|
header[self.HTTP_HEADER_KEY_USER_AGENT] = self.HTTP_USER_AGENT
|
1115
1185
|
|
1116
1186
|
if method is AppMeshClient.Method.GET:
|
1117
|
-
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)
|
1118
1188
|
elif method is AppMeshClient.Method.POST:
|
1119
|
-
return
|
1189
|
+
return self.session.post(
|
1120
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
|
1121
1191
|
)
|
1122
1192
|
elif method is AppMeshClient.Method.POST_STREAM:
|
1123
|
-
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)
|
1124
1194
|
elif method is AppMeshClient.Method.DELETE:
|
1125
|
-
return
|
1195
|
+
return self.session.delete(url=rest_url, headers=header, cert=self.ssl_client_cert, verify=self.ssl_verify, timeout=self.rest_timeout)
|
1126
1196
|
elif method is AppMeshClient.Method.PUT:
|
1127
|
-
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)
|
1128
1198
|
else:
|
1129
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
|