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 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
- resp = self._request_http(AppMeshClient.Method.POST, path="/appmesh/self/logoff")
329
- if resp.status_code != HTTPStatus.OK:
330
- raise Exception(resp.text)
331
- self.jwt_token = None
332
- return resp.status_code == HTTPStatus.OK
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 = urllib.parse.urlparse(totp_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 = urllib.parse.parse_qs(parsed_uri.query)
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": urllib.parse.quote(remote_file), "Content-Type": encoder.content_type}
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
- rest_url = urllib.parse.urljoin(self.server_url, path)
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(urllib.parse.urlsplit(self.server_url).port)
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 requests.get(url=rest_url, params=query, headers=header, cert=self.ssl_client_cert, verify=self.ssl_verify, timeout=self.rest_timeout)
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 requests.post(
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 requests.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)
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 requests.delete(url=rest_url, headers=header, cert=self.ssl_client_cert, verify=self.ssl_verify, timeout=self.rest_timeout)
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 requests.put(url=rest_url, params=query, headers=header, json=body, cert=self.ssl_client_cert, verify=self.ssl_verify, timeout=self.rest_timeout)
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.6
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] [![release.badge]][release.url] [![pypi.badge]][pypi.url] [![unittest.badge]][unittest.url] [![docker.badge]][docker.url] [![cockpit.badge]][cockpit.url]
31
+ [![language.badge]][language.url] [![standard.badge]][standard.url] [![unittest.badge]][unittest.url] [![docker.badge]][docker.url] [![cockpit.badge]][cockpit.url]
32
32
  [![Documentation Status](https://readthedocs.org/projects/app-mesh/badge/?version=latest)](https://app-mesh.readthedocs.io/en/latest/?badge=latest) [![Join the chat at https://gitter.im/app-mesh/community](https://badges.gitter.im/app-mesh/community.svg)](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
  [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/laoshanxi/app-mesh/badge)](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.svg
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=qM0es3dM4PM95ymNqdTAuIvTD9-Xy6jPKhDW-Sn7ehg,45180
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.6.dist-info/METADATA,sha256=x8wWJAFfIPR3PU4nGmmZDezdmmeoCLFmkE-nrOM1lPY,11501
11
- appmesh-1.4.6.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
12
- appmesh-1.4.6.dist-info/top_level.txt,sha256=-y0MNQOGJxUzLdHZ6E_Rfv5_LNCkV-GTmOBME_b6pg8,8
13
- appmesh-1.4.6.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.0)
2
+ Generator: setuptools (75.8.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5