visionai-sdk-python 2.2.0.dev1__py3-none-any.whl → 2.2.0.dev2__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.
@@ -85,11 +85,29 @@ class _BaseClient:
85
85
  self._credentials: dict | None = None
86
86
  self._credentials_type: Literal["login", "client"] | None = None
87
87
 
88
+ @property
89
+ def credential(self) -> "ClientCredentials | AsyncClientCredentials | None":
90
+ """The credential this client was built with, for callers that need the
91
+ raw token themselves (a WebSocket upgrade, another HTTP library)."""
92
+ return self._credential
93
+
88
94
  @staticmethod
89
95
  def _build_url(base_url: str, path: str) -> str:
90
96
  """Join a base URL and a path, normalizing slashes."""
91
97
  return f"{base_url.rstrip('/')}/{path.lstrip('/')}"
92
98
 
99
+ def _absolute(self, url: str) -> str:
100
+ """``url`` as given when absolute, else a path under ``auth_url``."""
101
+ return url if "://" in url else self._build_url(self.auth_url, url)
102
+
103
+ def _with_auth(self, kwargs: dict, token: str) -> dict:
104
+ """``kwargs`` for httpx with the bearer header; the client's token wins."""
105
+ headers = {
106
+ **dict(kwargs.get("headers") or {}),
107
+ **self._build_auth_header(token),
108
+ }
109
+ return {**kwargs, "headers": headers}
110
+
93
111
  def _vlm_base(self) -> str:
94
112
  if self.vlm_url is None:
95
113
  raise ValueError("vlm_url is required for VLM calls; pass it to the client")
@@ -19,7 +19,10 @@ class AsyncClient(_BaseClient):
19
19
  max_connections: int = 100,
20
20
  max_keepalive_connections: int = 20,
21
21
  credential: AsyncClientCredentials | None = None,
22
+ http_client: httpx.AsyncClient | None = None,
22
23
  ) -> None:
24
+ """See ``_BaseClient``; ``http_client`` is an httpx.AsyncClient to send through
25
+ (tests, shared pools); the client closes only the one it created."""
23
26
  super().__init__(
24
27
  auth_url=auth_url,
25
28
  vlm_url=vlm_url,
@@ -30,7 +33,7 @@ class AsyncClient(_BaseClient):
30
33
  max_keepalive_connections=max_keepalive_connections,
31
34
  credential=credential,
32
35
  )
33
- self._client = httpx.AsyncClient(
36
+ self._client = http_client or httpx.AsyncClient(
34
37
  verify=self.verify_ssl,
35
38
  timeout=self.timeout,
36
39
  limits=httpx.Limits(
@@ -38,6 +41,7 @@ class AsyncClient(_BaseClient):
38
41
  max_keepalive_connections=self.max_keepalive_connections,
39
42
  ),
40
43
  )
44
+ self._owns_http = http_client is None
41
45
  # Register the different function resource
42
46
  self.auth = AsyncAuthResource(self)
43
47
  self.vlm = AsyncVLMResource(self)
@@ -51,9 +55,46 @@ class AsyncClient(_BaseClient):
51
55
  raise self._map_transport_error(e) from e
52
56
  return self._handle_response(response)
53
57
 
58
+ async def request(self, method: str, url: str, **kwargs) -> httpx.Response:
59
+ """Send any request with this client's bearer token.
60
+
61
+ ``url`` is absolute, or a path under ``auth_url``; ``kwargs`` go to httpx.
62
+ A 401 buys one fresh token (from ``credential`` or the stored login) and
63
+ one retry, so a token revoked server-side costs one extra request.
64
+
65
+ Raises:
66
+ AuthenticationError: No token, or the fresh one was refused too.
67
+ RateLimitedError, NetworkError, ServerError, ClientError: As the
68
+ resource methods.
69
+ """
70
+ url = self._absolute(url)
71
+ await self._ensure_token()
72
+ token = self._access_token
73
+ try:
74
+ return await self._request(method, url, **self._with_auth(kwargs, token))
75
+ except AuthenticationError:
76
+ if not await self._renew_rejected(token):
77
+ raise
78
+ return await self._request(
79
+ method, url, **self._with_auth(kwargs, self._access_token)
80
+ )
81
+
82
+ async def _renew_rejected(self, token: str) -> bool:
83
+ """A 401 for ``token``: get a different one, once; False when none would help."""
84
+ if self._credential is not None:
85
+ if not self._credential.invalidate(token):
86
+ return False
87
+ self._access_token = (await self._credential.get_token()).token
88
+ return True
89
+ if self._credentials is None or self._access_token != token:
90
+ return False
91
+ await self._refresh_token()
92
+ return self._access_token != token
93
+
54
94
  async def close(self) -> None:
55
- """Close the HTTP client and release connections."""
56
- await self._client.aclose()
95
+ """Close the HTTP client this client created and release its connections."""
96
+ if self._owns_http:
97
+ await self._client.aclose()
57
98
 
58
99
  async def __aenter__(self) -> "AsyncClient":
59
100
  """Async context manager entry."""
@@ -19,7 +19,10 @@ class Client(_BaseClient):
19
19
  max_connections: int = 100,
20
20
  max_keepalive_connections: int = 20,
21
21
  credential: ClientCredentials | None = None,
22
+ http_client: httpx.Client | None = None,
22
23
  ) -> None:
24
+ """See ``_BaseClient``; ``http_client`` is an httpx.Client to send through
25
+ (tests, shared pools); the client closes only the one it created."""
23
26
  super().__init__(
24
27
  auth_url=auth_url,
25
28
  vlm_url=vlm_url,
@@ -30,7 +33,7 @@ class Client(_BaseClient):
30
33
  max_keepalive_connections=max_keepalive_connections,
31
34
  credential=credential,
32
35
  )
33
- self._client = httpx.Client(
36
+ self._client = http_client or httpx.Client(
34
37
  verify=self.verify_ssl,
35
38
  timeout=self.timeout,
36
39
  limits=httpx.Limits(
@@ -38,6 +41,7 @@ class Client(_BaseClient):
38
41
  max_keepalive_connections=self.max_keepalive_connections,
39
42
  ),
40
43
  )
44
+ self._owns_http = http_client is None
41
45
  # Register the different function resource
42
46
  self.auth = AuthResource(self)
43
47
  self.vlm = VLMResource(self)
@@ -51,9 +55,44 @@ class Client(_BaseClient):
51
55
  raise self._map_transport_error(e) from e
52
56
  return self._handle_response(response)
53
57
 
58
+ def request(self, method: str, url: str, **kwargs) -> httpx.Response:
59
+ """Send any request with this client's bearer token.
60
+
61
+ ``url`` is absolute, or a path under ``auth_url``; ``kwargs`` go to httpx.
62
+ A 401 buys one fresh token (from ``credential`` or the stored login) and
63
+ one retry, so a token revoked server-side costs one extra request.
64
+
65
+ Raises:
66
+ AuthenticationError: No token, or the fresh one was refused too.
67
+ RateLimitedError, NetworkError, ServerError, ClientError: As the
68
+ resource methods.
69
+ """
70
+ url = self._absolute(url)
71
+ self._ensure_token()
72
+ token = self._access_token
73
+ try:
74
+ return self._request(method, url, **self._with_auth(kwargs, token))
75
+ except AuthenticationError:
76
+ if not self._renew_rejected(token):
77
+ raise
78
+ return self._request(method, url, **self._with_auth(kwargs, self._access_token))
79
+
80
+ def _renew_rejected(self, token: str) -> bool:
81
+ """A 401 for ``token``: get a different one, once; False when none would help."""
82
+ if self._credential is not None:
83
+ if not self._credential.invalidate(token):
84
+ return False
85
+ self._access_token = self._credential.get_token().token
86
+ return True
87
+ if self._credentials is None or self._access_token != token:
88
+ return False
89
+ self._refresh_token()
90
+ return self._access_token != token
91
+
54
92
  def close(self) -> None:
55
- """Close the HTTP client and release connections."""
56
- self._client.close()
93
+ """Close the HTTP client this client created and release its connections."""
94
+ if self._owns_http:
95
+ self._client.close()
57
96
 
58
97
  def __enter__(self) -> "Client":
59
98
  """Context manager entry."""
@@ -28,8 +28,8 @@ class AsyncVLMResource(VLMMixin):
28
28
  ) -> ResponseNormalModel | ResponseErrorModel:
29
29
  """Submit an inference request to the VLM service.
30
30
 
31
- Uses the internally stored access token obtained from login() or get_access_token().
32
- If the token is expiring soon, it will be automatically refreshed.
31
+ Sent with the client's token (``credential``, ``login()`` or ``get_access_token()``);
32
+ a 401 gets one fresh token and one retry.
33
33
 
34
34
  Args:
35
35
  payload: Inference parameters as a NIMRequestModel instance or a dict
@@ -45,17 +45,13 @@ class AsyncVLMResource(VLMMixin):
45
45
  NetworkError: If the request times out or a network error occurs.
46
46
  VisionaiSDKError: If the request fails for any other reason.
47
47
  """
48
- # Ensure token is valid
49
- await self._sdk_client._ensure_token()
50
-
51
48
  # Prepare request (from Mixin)
52
49
  body = self._prepare_chat_request(payload)
53
50
 
54
- # I/O operation (async)
55
- response = await self._sdk_client._request(
51
+ # I/O operation (async); the client adds the token and retries once on 401
52
+ response = await self._sdk_client.request(
56
53
  "POST",
57
54
  self._sdk_client._build_url(self._sdk_client._vlm_base(), VLMEndpoint.CHAT),
58
- headers=self._sdk_client._build_auth_header(self._sdk_client._access_token),
59
55
  json=body,
60
56
  )
61
57
 
@@ -67,8 +63,8 @@ class AsyncVLMResource(VLMMixin):
67
63
  ) -> ResponseNormalModel | ResponseErrorModel:
68
64
  """Poll the result of a previously submitted inference request.
69
65
 
70
- Uses the internally stored access token obtained from login() or get_access_token().
71
- If the token is expiring soon, it will be automatically refreshed.
66
+ Sent with the client's token (``credential``, ``login()`` or ``get_access_token()``);
67
+ a 401 gets one fresh token and one retry.
72
68
 
73
69
  Args:
74
70
  result_id: Chat result ID returned from a prior chat() call.
@@ -82,16 +78,12 @@ class AsyncVLMResource(VLMMixin):
82
78
  NetworkError: If the request times out or a network error occurs.
83
79
  VisionaiSDKError: If the request fails for any other reason.
84
80
  """
85
- # Ensure token is valid
86
- await self._sdk_client._ensure_token()
87
-
88
- # I/O operation (async)
89
- response = await self._sdk_client._request(
81
+ # I/O operation (async); the client adds the token and retries once on 401
82
+ response = await self._sdk_client.request(
90
83
  "GET",
91
84
  self._sdk_client._build_url(
92
85
  self._sdk_client._vlm_base(), f"{VLMEndpoint.CHAT}/{result_id}"
93
86
  ),
94
- headers=self._sdk_client._build_auth_header(self._sdk_client._access_token),
95
87
  )
96
88
 
97
89
  # Parse response (from Mixin)
@@ -28,8 +28,8 @@ class VLMResource(VLMMixin):
28
28
  ) -> ResponseNormalModel | ResponseErrorModel:
29
29
  """Submit an inference request to the VLM service.
30
30
 
31
- Uses the internally stored access token obtained from login() or get_access_token().
32
- If the token is expiring soon, it will be automatically refreshed.
31
+ Sent with the client's token (``credential``, ``login()`` or ``get_access_token()``);
32
+ a 401 gets one fresh token and one retry.
33
33
 
34
34
  Args:
35
35
  payload: Inference parameters as a NIMRequestModel instance or a dict
@@ -45,17 +45,13 @@ class VLMResource(VLMMixin):
45
45
  NetworkError: If the request times out or a network error occurs.
46
46
  VisionaiSDKError: If the request fails for any other reason.
47
47
  """
48
- # Ensure token is valid
49
- self._sdk_client._ensure_token()
50
-
51
48
  # Prepare request (from Mixin)
52
49
  body = self._prepare_chat_request(payload)
53
50
 
54
- # I/O operation (sync)
55
- response = self._sdk_client._request(
51
+ # I/O operation (sync); the client adds the token and retries once on 401
52
+ response = self._sdk_client.request(
56
53
  "POST",
57
54
  self._sdk_client._build_url(self._sdk_client._vlm_base(), VLMEndpoint.CHAT),
58
- headers=self._sdk_client._build_auth_header(self._sdk_client._access_token),
59
55
  json=body,
60
56
  )
61
57
 
@@ -65,8 +61,8 @@ class VLMResource(VLMMixin):
65
61
  def get_chat(self, result_id: str) -> ResponseNormalModel | ResponseErrorModel:
66
62
  """Poll the result of a previously submitted inference request.
67
63
 
68
- Uses the internally stored access token obtained from login() or get_access_token().
69
- If the token is expiring soon, it will be automatically refreshed.
64
+ Sent with the client's token (``credential``, ``login()`` or ``get_access_token()``);
65
+ a 401 gets one fresh token and one retry.
70
66
 
71
67
  Args:
72
68
  result_id: Chat result ID returned from a prior chat() call.
@@ -80,16 +76,12 @@ class VLMResource(VLMMixin):
80
76
  NetworkError: If the request times out or a network error occurs.
81
77
  VisionaiSDKError: If the request fails for any other reason.
82
78
  """
83
- # Ensure token is valid
84
- self._sdk_client._ensure_token()
85
-
86
- # I/O operation (sync)
87
- response = self._sdk_client._request(
79
+ # I/O operation (sync); the client adds the token and retries once on 401
80
+ response = self._sdk_client.request(
88
81
  "GET",
89
82
  self._sdk_client._build_url(
90
83
  self._sdk_client._vlm_base(), f"{VLMEndpoint.CHAT}/{result_id}"
91
84
  ),
92
- headers=self._sdk_client._build_auth_header(self._sdk_client._access_token),
93
85
  )
94
86
 
95
87
  # Parse response (from Mixin)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: visionai-sdk-python
3
- Version: 2.2.0.dev1
3
+ Version: 2.2.0.dev2
4
4
  Summary: VisionAI SDK for Python
5
5
  Author-email: Tony Yang <tonyyang@linkervision.com>
6
6
  Requires-Python: >=3.11
@@ -146,7 +146,10 @@ cred = ClientCredentials(
146
146
  )
147
147
  client = Client(auth_url="...", vlm_url="...", credential=cred) # vlm.chat() uses it
148
148
 
149
- token = cred.get_token().token # for requests the SDK does not make itself
149
+ token = cred.get_token().token # for a WebSocket upgrade or another library
150
+
151
+ # Any other HTTP call with the same token; a 401 gets one fresh token and one retry
152
+ nodes = client.request("GET", "https://vs.example.com/mgmt/nodes").json()
150
153
  ```
151
154
 
152
155
  Platform mints at most 50 tokens per client per month, so the credential protects the
@@ -1,10 +1,10 @@
1
1
  visionai_sdk_python/__init__.py,sha256=EITFK0aQPAQN3BSnwTfrPAt1iu50pmuhXl0-jBSbiD4,774
2
- visionai_sdk_python/_base.py,sha256=73Jq7OTNgY6naybpkdAIOyt-CHgEdtyD_TL5XBgERtU,9599
2
+ visionai_sdk_python/_base.py,sha256=fnvnes3i5K5CObwIROs3NM6X-PHTLS-IyK6cK9hILeA,10397
3
3
  visionai_sdk_python/_jwt_verifier.py,sha256=h4CAK1iiDAJ-oKnXrLbCJw5rWcsVe94QNY12ne1Wzto,8442
4
4
  visionai_sdk_python/_request_attribution.py,sha256=zQk_bSLsu1RTp1Fe3MGUUX8YRzq3iqfBTmhhjM7BuRU,1907
5
5
  visionai_sdk_python/_source_header.py,sha256=DWiR-TGclET4UrqmAEO7bYPtf9XuLSE4FGQjZIgVWiY,5133
6
- visionai_sdk_python/async_client.py,sha256=NbfWjAQcQ1xh53Q_Q9XQpQQSWGXhEge79cHHT6V2lnM,3924
7
- visionai_sdk_python/client.py,sha256=5iyqh3jVX7iWG52dN0zzdFL05UJYs8yQAE9fOqkt55s,3768
6
+ visionai_sdk_python/async_client.py,sha256=b8hMe9e3eth8B5tOIhP4Wo7ja7JvxO7fe8xQk1lSDjQ,5837
7
+ visionai_sdk_python/client.py,sha256=urkFrMtw7qrkyfzekhF5vsYZt5LlgeJXml-jI8uWSqI,5599
8
8
  visionai_sdk_python/constants.py,sha256=KWhoxZdlwqXwhUJDqERStq2mrms-MA8aRDqBwbIvNO0,1471
9
9
  visionai_sdk_python/endpoints.py,sha256=CC-AoxXzg39sMAhqx1CTS3gRWKx_H7VOqBANRLVotoU,183
10
10
  visionai_sdk_python/exceptions.py,sha256=UExz_lx7do154RyFobcYIaD_hDD-AExvMiOSDkKxe-0,1259
@@ -18,11 +18,11 @@ visionai_sdk_python/auth/models.py,sha256=u6MDFYaWMyf08XCrGZzwPHv43d6AElH9XO2QSj
18
18
  visionai_sdk_python/auth/resource.py,sha256=vFBf_27h2u6R1w3aXm5cDEpvS21RyiwgtDm0olZFpec,5238
19
19
  visionai_sdk_python/vlm/__init__.py,sha256=EDd7ZG4k7inn8qqkyIhO3xI_4MPMWJT5Npsient-tTc,571
20
20
  visionai_sdk_python/vlm/_mixin.py,sha256=TkqezNpW8QEe9pujsvx1APcylYQYr_PvHruEz4Le3JQ,1718
21
- visionai_sdk_python/vlm/async_resource.py,sha256=2TlLFiyiiQBvGR1BQHZVH_swCdKLANPZTOOD4fRIa0Y,3703
21
+ visionai_sdk_python/vlm/async_resource.py,sha256=b878VPxVfO4xpjG-JH2XcimqywMFtEw-cyEfeSLVGRo,3417
22
22
  visionai_sdk_python/vlm/models.py,sha256=yDFjo4LfAxqXYBnl6ZW7YUJQoKKRkQWtqBJi2ruI6Jw,984
23
23
  visionai_sdk_python/vlm/preprocess.py,sha256=ZpJJa13h8_YKwh_P7Pn8WIXV0x-r2xJi-ETgCFsGw-Y,7892
24
- visionai_sdk_python/vlm/resource.py,sha256=UK_FNkQbnXlIkq0MTVav4vRRNE-p6QnWg-RL_ACGZhc,3616
25
- visionai_sdk_python-2.2.0.dev1.dist-info/METADATA,sha256=0_CPJjTYkmwvxCw5n3_V0vt0UzwhO2KdwrAmgNONtOk,29494
26
- visionai_sdk_python-2.2.0.dev1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
27
- visionai_sdk_python-2.2.0.dev1.dist-info/top_level.txt,sha256=79a5MCSLLv6Mbfun-8RS-G6O927hahBwDqJjeuWLdvM,20
28
- visionai_sdk_python-2.2.0.dev1.dist-info/RECORD,,
24
+ visionai_sdk_python/vlm/resource.py,sha256=qx03gOxVamtoDrLNadp1J9EtVuP95SntOuilFog_SSo,3342
25
+ visionai_sdk_python-2.2.0.dev2.dist-info/METADATA,sha256=DWZr4KOvRRLb1IXTQtpvwSBxr4ozPzJCAm9xAlTe53Q,29654
26
+ visionai_sdk_python-2.2.0.dev2.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
27
+ visionai_sdk_python-2.2.0.dev2.dist-info/top_level.txt,sha256=79a5MCSLLv6Mbfun-8RS-G6O927hahBwDqJjeuWLdvM,20
28
+ visionai_sdk_python-2.2.0.dev2.dist-info/RECORD,,