appmesh 1.6.8__py3-none-any.whl → 1.6.10__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 CHANGED
@@ -11,6 +11,7 @@ Example:
11
11
 
12
12
  import sys
13
13
  from types import ModuleType
14
+ from typing import TYPE_CHECKING
14
15
 
15
16
  __all__ = ["App", "AppMeshClient", "AppMeshClientTCP", "AppMeshClientOAuth", "AppMeshServer", "AppMeshServerTCP"]
16
17
 
@@ -24,6 +25,17 @@ _LAZY_IMPORTS = {
24
25
  }
25
26
 
26
27
 
28
+ if TYPE_CHECKING:
29
+ # Provide explicit imports for static analyzers and type checkers
30
+ # These imports are only executed during type checking and won't affect runtime.
31
+ from .app import App # noqa: F401
32
+ from .client_http import AppMeshClient # noqa: F401
33
+ from .client_tcp import AppMeshClientTCP # noqa: F401
34
+ from .client_http_oauth import AppMeshClientOAuth # noqa: F401
35
+ from .server_http import AppMeshServer # noqa: F401
36
+ from .server_tcp import AppMeshServerTCP # noqa: F401
37
+
38
+
27
39
  def _lazy_import(name):
28
40
  """Helper function for lazy importing."""
29
41
  if name in _LAZY_IMPORTS:
appmesh/client_http.py CHANGED
@@ -7,7 +7,10 @@ import locale
7
7
  import logging
8
8
  import os
9
9
  import sys
10
+ import threading
10
11
  import time
12
+ import requests
13
+ import http.cookiejar as cookiejar
11
14
  from datetime import datetime
12
15
  from enum import Enum, unique
13
16
  from http import HTTPStatus
@@ -16,7 +19,6 @@ from typing import Optional, Tuple, Union
16
19
  from urllib import parse
17
20
  import aniso8601
18
21
  import jwt
19
- import requests
20
22
  from .app import App
21
23
  from .app_run import AppRun
22
24
  from .app_output import AppOutput
@@ -74,7 +76,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
74
76
  - wait_for_async_run()
75
77
  - run_app_sync()
76
78
  - run_task()
77
- - cancle_task()
79
+ - cancel_task()
78
80
 
79
81
  # System Management
80
82
  - forward_to
@@ -127,6 +129,10 @@ class AppMeshClient(metaclass=abc.ABCMeta):
127
129
  HTTP_HEADER_KEY_USER_AGENT = "User-Agent"
128
130
  HTTP_HEADER_KEY_X_TARGET_HOST = "X-Target-Host"
129
131
  HTTP_HEADER_KEY_X_FILE_PATH = "X-File-Path"
132
+ HTTP_HEADER_JWT_set_cookie = "X-Set-Cookie"
133
+ HTTP_HEADER_NAME_CSRF_TOKEN = "X-CSRF-Token"
134
+ COOKIE_TOKEN = "appmesh_auth_token"
135
+ COOKIE_CSRF_TOKEN = "appmesh_csrf_token"
130
136
 
131
137
  @unique
132
138
  class Method(Enum):
@@ -191,7 +197,8 @@ class AppMeshClient(metaclass=abc.ABCMeta):
191
197
  rest_ssl_verify=DEFAULT_SSL_CA_CERT_PATH if os.path.exists(DEFAULT_SSL_CA_CERT_PATH) else False,
192
198
  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,
193
199
  rest_timeout=(60, 300),
194
- jwt_token=None,
200
+ jwt_token: Optional[str] = None,
201
+ rest_cookie_file: Optional[str] = None,
195
202
  auto_refresh_token=False,
196
203
  ):
197
204
  """Initialize an App Mesh HTTP client for interacting with the App Mesh server via secure HTTPS.
@@ -213,13 +220,14 @@ class AppMeshClient(metaclass=abc.ABCMeta):
213
220
  rest_timeout (tuple, optional): HTTP connection timeouts for API requests, as `(connect_timeout, read_timeout)`.
214
221
  The default is `(60, 300)`, where `60` seconds is the maximum time to establish a connection and `300` seconds for the maximum read duration.
215
222
 
223
+ rest_cookie_file (str, optional): Path to a file for storing session cookies. If provided, cookies will be saved to and loaded from this file to maintain session state across client instances.
224
+
216
225
  jwt_token (str, optional): JWT token for API authentication, used in headers to authorize requests where required.
217
226
  auto_refresh_token (bool, optional): Enable automatic token refresh before expiration.
218
227
  When enabled, a background timer will monitor token expiration and attempt to refresh
219
228
  the token before it expires. This works with both native App Mesh tokens and Keycloak tokens.
220
229
  """
221
230
  self._ensure_logging_configured()
222
- self.session = requests.Session()
223
231
  self.auth_server_url = rest_url
224
232
  self._jwt_token = jwt_token
225
233
  self.ssl_verify = rest_ssl_verify
@@ -227,6 +235,11 @@ class AppMeshClient(metaclass=abc.ABCMeta):
227
235
  self.rest_timeout = rest_timeout
228
236
  self._forward_to = None
229
237
 
238
+ # Session and cookie management
239
+ self._lock = threading.Lock()
240
+ self.session = requests.Session()
241
+ self.cookie_file = self._load_cookies(rest_cookie_file)
242
+
230
243
  # Token auto-refresh
231
244
  self._token_refresh_timer = None
232
245
  self._auto_refresh_token = auto_refresh_token
@@ -241,6 +254,44 @@ class AppMeshClient(metaclass=abc.ABCMeta):
241
254
  def _get_access_token(self) -> str:
242
255
  return self.jwt_token
243
256
 
257
+ def _load_cookies(self, cookie_file: Optional[str]) -> str:
258
+ """Load cookies from the cookie file and return the file path ."""
259
+ if not cookie_file:
260
+ return ""
261
+
262
+ self.session.cookies = cookiejar.MozillaCookieJar(cookie_file)
263
+ if os.path.exists(cookie_file):
264
+ self.session.cookies.load(ignore_discard=True, ignore_expires=True)
265
+ else:
266
+ os.makedirs(os.path.dirname(cookie_file), exist_ok=True)
267
+ self.session.cookies.save(ignore_discard=True, ignore_expires=True)
268
+ if os.name == "posix":
269
+ os.chmod(cookie_file, 0o600) # User read/write only
270
+ return cookie_file
271
+
272
+ @staticmethod
273
+ def _get_cookie_value(cookies, name, check_expiry=True) -> Optional[str]:
274
+ """Get cookie value by name, checking expiry if requested."""
275
+ # If it's a RequestsCookieJar, use .get() but check expiry manually if requested
276
+ if hasattr(cookies, "get") and not isinstance(cookies, list):
277
+ cookie = cookies.get(name)
278
+ if cookie is None:
279
+ return None
280
+ if check_expiry and getattr(cookie, "expires", None):
281
+ if cookie.expires < time.time():
282
+ return None # expired
283
+ return cookie.value if hasattr(cookie, "value") else cookie
284
+
285
+ # Otherwise, assume it's a MozillaCookieJar — iterate manually
286
+ for c in cookies:
287
+ if c.name == name:
288
+ if check_expiry and getattr(c, "expires", None):
289
+ if c.expires < time.time():
290
+ return None # expired
291
+ return c.value
292
+
293
+ return None
294
+
244
295
  def _check_and_refresh_token(self):
245
296
  """Check and refresh token if needed, then schedule next check.
246
297
 
@@ -389,6 +440,8 @@ class AppMeshClient(metaclass=abc.ABCMeta):
389
440
  - Refresh tokens before expiration
390
441
  - Validate token format before setting
391
442
  """
443
+ if self._jwt_token == token:
444
+ return # No change
392
445
  self._jwt_token = token
393
446
 
394
447
  # handle refresh
@@ -398,6 +451,11 @@ class AppMeshClient(metaclass=abc.ABCMeta):
398
451
  self._token_refresh_timer.cancel()
399
452
  self._token_refresh_timer = None
400
453
 
454
+ # handle session
455
+ with self._lock:
456
+ if self.cookie_file:
457
+ self.session.cookies.save(ignore_discard=True, ignore_expires=True)
458
+
401
459
  @property
402
460
  def forward_to(self) -> str:
403
461
  """Get the target host address for request forwarding in a cluster setup.
@@ -444,44 +502,6 @@ class AppMeshClient(metaclass=abc.ABCMeta):
444
502
 
445
503
  self._forward_to = host
446
504
 
447
- def _normalize_totp_secret(self, secret: str) -> str:
448
- """
449
- Normalize a TOTP secret to a valid RFC 4648 Base32 format.
450
- This includes:
451
- - Removing all whitespace
452
- - Uppercasing
453
- - Replacing visually similar or Crockford-like digits:
454
- '1' -> 'I', '0' -> 'O', '8' -> 'B', '9' -> 'G'
455
- - Adding RFC 4648 padding
456
- """
457
- if not secret:
458
- raise ValueError("Empty TOTP secret")
459
-
460
- # Remove all whitespace and uppercase
461
- secret = "".join(secret.split()).upper()
462
-
463
- # Crockford/Base32-friendly replacements
464
- replacements = {
465
- "1": "I",
466
- "0": "O",
467
- "8": "B",
468
- "9": "G",
469
- }
470
-
471
- for old, new in replacements.items():
472
- secret = secret.replace(old, new)
473
-
474
- # Validate that only RFC 4648 Base32 characters remain
475
- valid_chars = set("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567")
476
- if not set(secret).issubset(valid_chars):
477
- raise ValueError("Invalid characters in TOTP secret")
478
-
479
- # Add padding to make length a multiple of 8 (RFC 4648)
480
- padding = (8 - (len(secret) % 8)) % 8
481
- secret += "=" * padding
482
-
483
- return secret
484
-
485
505
  ########################################
486
506
  # Security
487
507
  ########################################
@@ -514,6 +534,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
514
534
  self.HTTP_HEADER_KEY_AUTH: "Basic " + base64.b64encode(f"{user_name}:{user_pwd}".encode()).decode(),
515
535
  "X-Expire-Seconds": str(self._parse_duration(timeout_seconds)),
516
536
  **({"X-Audience": audience} if audience else {}),
537
+ **({self.HTTP_HEADER_JWT_set_cookie: "true"} if self.cookie_file else {}),
517
538
  # **({"X-Totp-Code": totp_code} if totp_code else {}),
518
539
  },
519
540
  )
@@ -648,20 +669,15 @@ class AppMeshClient(metaclass=abc.ABCMeta):
648
669
 
649
670
  def get_totp_secret(self) -> str:
650
671
  """
651
- Generate TOTP secret for the current user and return a normalized secret.
672
+ Generate TOTP secret for the current user and return a secret.
652
673
 
653
674
  Returns:
654
- str: Normalized TOTP secret string
675
+ str: TOTP secret string
655
676
  """
656
677
  resp = self._request_http(method=AppMeshClient.Method.POST, path="/appmesh/totp/secret")
657
678
  if resp.status_code == HTTPStatus.OK:
658
679
  totp_uri = base64.b64decode(resp.json()["mfa_uri"]).decode()
659
- secret = self._parse_totp_uri(totp_uri).get("secret")
660
- if not secret:
661
- raise ValueError("TOTP secret missing in response")
662
-
663
- # Normalize to standard Base32
664
- return self._normalize_totp_secret(secret)
680
+ return self._parse_totp_uri(totp_uri).get("secret")
665
681
 
666
682
  raise Exception(resp.text)
667
683
 
@@ -1282,7 +1298,7 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1282
1298
 
1283
1299
  return resp.text
1284
1300
 
1285
- def cancle_task(self, app_name: str) -> bool:
1301
+ def cancel_task(self, app_name: str) -> bool:
1286
1302
  """Client cancle a running task to a App Mesh application.
1287
1303
 
1288
1304
  Args:
@@ -1442,9 +1458,13 @@ class AppMeshClient(metaclass=abc.ABCMeta):
1442
1458
 
1443
1459
  # Prepare headers
1444
1460
  header = {} if header is None else header
1445
- token = self._get_access_token()
1446
- if token:
1447
- header[self.HTTP_HEADER_KEY_AUTH] = f"Bearer {token}"
1461
+
1462
+ # JWT or Cookie token
1463
+ if self.cookie_file and self._get_cookie_value(self.session.cookies, self.COOKIE_CSRF_TOKEN):
1464
+ header[self.HTTP_HEADER_NAME_CSRF_TOKEN] = self._get_cookie_value(self.session.cookies, self.COOKIE_CSRF_TOKEN)
1465
+ elif self._get_access_token():
1466
+ header[self.HTTP_HEADER_KEY_AUTH] = f"Bearer {self._get_access_token()}"
1467
+
1448
1468
  if self.forward_to and len(self.forward_to) > 0:
1449
1469
  if ":" in self.forward_to:
1450
1470
  header[self.HTTP_HEADER_KEY_X_TARGET_HOST] = self.forward_to
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: appmesh
3
- Version: 1.6.8
3
+ Version: 1.6.10
4
4
  Summary: Client SDK for App Mesh
5
5
  Home-page: https://github.com/laoshanxi/app-mesh
6
6
  Author: laoshanxi
@@ -145,7 +145,6 @@ Refer to the [Installation doc](https://app-mesh.readthedocs.io/en/latest/Instal
145
145
  - [log4cpp](http://log4cpp.sourceforge.net)
146
146
  - [Crypto++](https://www.cryptopp.com)
147
147
  - [ldap-cpp](https://github.com/AndreyBarmaley/ldap-cpp)
148
- - [OATH Toolkit](http://www.nongnu.org/oath-toolkit/liboath-api)
149
148
 
150
149
  [language.url]: https://isocpp.org/
151
150
  [language.badge]: https://img.shields.io/badge/language-C++-blue.svg
@@ -1,16 +1,16 @@
1
- appmesh/__init__.py,sha256=TY1y5B5cE57uhraEzCFOZRWuo9SY1R-fYNRan8hCZOM,1670
1
+ appmesh/__init__.py,sha256=uJ5LOadwZW2nOXkSuPOy2S3WH9Bx0BUn5wUvPBeFzoc,2217
2
2
  appmesh/app.py,sha256=crD4DRFZJuHtZMfSsz7C-EwvjPmGZbFXYXvA_wCdvdI,10734
3
3
  appmesh/app_output.py,sha256=vfn322AyixblI8DbXds08h6L_ybObiaRSifsA1-Xcoo,1035
4
4
  appmesh/app_run.py,sha256=aYq852a29OThIi32Xtx5s0sTXZ97T0lHD5WXH8yfPoc,2018
5
5
  appmesh/appmesh_client.py,sha256=ywB2222PtJUffdfdxZcBfdhZs1KYyc7JvzMxwuK2qyI,378
6
- appmesh/client_http.py,sha256=l8eUmJxXIDjowPvPKIppCLfwlk2kjztU-FXoaOac_tc,59003
6
+ appmesh/client_http.py,sha256=KD4AMcMbHqaLJWSrra0J03kMqWAiwYHiyusUc5kpr6o,60443
7
7
  appmesh/client_http_oauth.py,sha256=1d51o0JX_xtB8d2bEuM7_XJHcwMnhcjkbIq7GE1Zxm8,6120
8
8
  appmesh/client_tcp.py,sha256=aq6UUzytZA4ibE9WQMMWdo1uW8sHETEhJjsbM6IYSno,11457
9
9
  appmesh/server_http.py,sha256=rBIYO9rbR-r3x1Jcry440Sp--IM-OWKRaOhNpGdkxh8,4299
10
10
  appmesh/server_tcp.py,sha256=-CU5tw97WJmDcUNsNPWqpdZ0wxRzRD6kUP3XyNZUTHc,1444
11
11
  appmesh/tcp_messages.py,sha256=H9S_iCy0IuufY2v50_SUgRvcyQmJsySG65tBe_xb3Ko,1878
12
12
  appmesh/tcp_transport.py,sha256=0hRSp5fpL9wKB05JIyIRIuyBC8w1IdokryhMDHqtN4M,8946
13
- appmesh-1.6.8.dist-info/METADATA,sha256=bDxN-Vm4004kC7FdM52yMx-YcmNcFzexBDYBr3p6db4,11828
14
- appmesh-1.6.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
15
- appmesh-1.6.8.dist-info/top_level.txt,sha256=-y0MNQOGJxUzLdHZ6E_Rfv5_LNCkV-GTmOBME_b6pg8,8
16
- appmesh-1.6.8.dist-info/RECORD,,
13
+ appmesh-1.6.10.dist-info/METADATA,sha256=do80dYMa4qK9tg1qKDM4M0klzIgzfcEGlWo6w1OIBfg,11764
14
+ appmesh-1.6.10.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
15
+ appmesh-1.6.10.dist-info/top_level.txt,sha256=-y0MNQOGJxUzLdHZ6E_Rfv5_LNCkV-GTmOBME_b6pg8,8
16
+ appmesh-1.6.10.dist-info/RECORD,,