visionai-sdk-python 2.0.0__tar.gz → 2.1.1__tar.gz
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.
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/PKG-INFO +58 -1
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/README.md +57 -0
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/_base.py +1 -1
- visionai_sdk_python-2.1.1/src/visionai_sdk_python/constants.py +36 -0
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/vlm/__init__.py +14 -2
- visionai_sdk_python-2.1.1/src/visionai_sdk_python/vlm/preprocess.py +186 -0
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python.egg-info/PKG-INFO +58 -1
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python.egg-info/SOURCES.txt +3 -1
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python.egg-info/scm_file_list.json +26 -24
- visionai_sdk_python-2.1.1/src/visionai_sdk_python.egg-info/scm_version.json +8 -0
- visionai_sdk_python-2.1.1/tests/test__base.py +198 -0
- visionai_sdk_python-2.1.1/tests/test_vlm_preprocess.py +213 -0
- visionai_sdk_python-2.0.0/src/visionai_sdk_python/constants.py +0 -49
- visionai_sdk_python-2.0.0/src/visionai_sdk_python.egg-info/scm_version.json +0 -8
- visionai_sdk_python-2.0.0/tests/test__base.py +0 -102
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/.github/dependabot.yml +0 -0
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/.github/workflows/ci.yml +0 -0
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/.github/workflows/publish.yml +0 -0
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/.gitignore +0 -0
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/pyproject.toml +0 -0
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/setup.cfg +0 -0
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/__init__.py +0 -0
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/_jwt_verifier.py +0 -0
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/_request_attribution.py +0 -0
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/_source_header.py +0 -0
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/async_client.py +0 -0
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/auth/__init__.py +0 -0
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/auth/_mixin.py +0 -0
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/auth/async_resource.py +0 -0
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/auth/models.py +0 -0
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/auth/resource.py +0 -0
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/client.py +0 -0
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/endpoints.py +0 -0
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/exceptions.py +0 -0
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/instrumentation.py +0 -0
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/py.typed +0 -0
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/vlm/_mixin.py +0 -0
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/vlm/async_resource.py +0 -0
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/vlm/models.py +0 -0
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/vlm/resource.py +0 -0
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python.egg-info/dependency_links.txt +0 -0
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python.egg-info/requires.txt +0 -0
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python.egg-info/top_level.txt +0 -0
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/tests/__init__.py +0 -0
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/tests/constants.py +0 -0
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/tests/test_async_client.py +0 -0
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/tests/test_client.py +0 -0
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/tests/test_instrumentation.py +0 -0
- {visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: visionai-sdk-python
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.1
|
|
4
4
|
Summary: VisionAI SDK for Python
|
|
5
5
|
Author-email: Tony Yang <tonyyang@linkervision.com>
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -27,6 +27,7 @@ Python client library for VisionAI authentication and Vision Language Model (VLM
|
|
|
27
27
|
- **Auto Token Management**: Automatic token refresh before expiration
|
|
28
28
|
- **JWT Validation**: Built-in token signature and expiration verification
|
|
29
29
|
- **VLM Inference**: Submit and poll vision-language model tasks
|
|
30
|
+
- **Resize Planning**: Compute VLM input dimensions (smart / square resize) without any image dependency — the caller does the actual resize
|
|
30
31
|
- **Async Support**: Full async/await support with `AsyncClient`
|
|
31
32
|
- **Type Safe**: Full type hints with Pydantic validation
|
|
32
33
|
- **Service Source Attribution**: Auto-attach a service-source header to every outbound `requests`/`httpx`/`aiohttp` call with a single startup call
|
|
@@ -187,6 +188,43 @@ elif result.status in ("failed", "timeout"):
|
|
|
187
188
|
- `failed`: Error, check `error`
|
|
188
189
|
- `timeout`: Request timeout
|
|
189
190
|
|
|
191
|
+
### Resize Planning
|
|
192
|
+
|
|
193
|
+
Compute the target dimensions for a VLM input image. Pure math, no image
|
|
194
|
+
dependencies — resize with whatever library your service already uses,
|
|
195
|
+
applying `plan.interpolation`. `name`, `factor`, and `interpolation` come
|
|
196
|
+
from the model server's resize spec; `pixels` is the UI resize option:
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
from visionai_sdk_python.vlm import compute_resize
|
|
200
|
+
|
|
201
|
+
plan = compute_resize(
|
|
202
|
+
width=1920, height=1080, name="smart_resize", factor=32, pixels=768,
|
|
203
|
+
interpolation="bicubic",
|
|
204
|
+
)
|
|
205
|
+
# ResizePlan(width=1024, height=576, interpolation='bicubic')
|
|
206
|
+
# smart_resize: dimensions divisible by factor, area capped at pixels * pixels
|
|
207
|
+
|
|
208
|
+
compute_resize(width=w, height=h, name="square_resize", pixels=384, interpolation="lanczos")
|
|
209
|
+
# square_resize: exact pixels x pixels; factor is ignored, so a whole resize
|
|
210
|
+
# spec can be forwarded as-is
|
|
211
|
+
|
|
212
|
+
# No resize spec from the model server? Fall back to the default
|
|
213
|
+
# (smart_resize, factor 32, bicubic):
|
|
214
|
+
from visionai_sdk_python.vlm import DEFAULT_RESIZE_SPEC
|
|
215
|
+
plan = compute_resize(width=w, height=h, pixels=768, **DEFAULT_RESIZE_SPEC)
|
|
216
|
+
|
|
217
|
+
# Apply with your own imaging library (PIL shown; see module docstring for cv2)
|
|
218
|
+
interpolation = {"bicubic": Image.Resampling.BICUBIC,
|
|
219
|
+
"lanczos": Image.Resampling.LANCZOS}[plan.interpolation]
|
|
220
|
+
img = img.resize((plan.width, plan.height), interpolation)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Invalid input (non-positive dimensions, a non-integer or non-positive
|
|
224
|
+
`pixels`/`factor`, unknown `name` or `interpolation`, missing `factor` for
|
|
225
|
+
smart resize, `min_pixels` on a square resize or over the pixel budget, an
|
|
226
|
+
unsatisfiable budget) raises `ValueError`.
|
|
227
|
+
|
|
190
228
|
## Token Validation
|
|
191
229
|
|
|
192
230
|
```python
|
|
@@ -212,6 +250,25 @@ client = Client(
|
|
|
212
250
|
)
|
|
213
251
|
```
|
|
214
252
|
|
|
253
|
+
### Token issuers
|
|
254
|
+
|
|
255
|
+
`allowed_issuers` is optional. When you leave it out, the SDK derives the
|
|
256
|
+
trusted issuers from `auth_url` and accepts a token whose `iss` claim either:
|
|
257
|
+
|
|
258
|
+
- equals `{auth_url}/keycloak/realms/linker-platform`, the realm every
|
|
259
|
+
deployment hosts behind its own base URL; or
|
|
260
|
+
- equals an external issuer the site is known to still accept — today only
|
|
261
|
+
production, which takes its Auth0 tenant alongside Keycloak while it migrates.
|
|
262
|
+
|
|
263
|
+
That means `auth_url` has to be the site's **public base URL** — the same host
|
|
264
|
+
that appears in the token's `iss` claim. Pointing it at an in-cluster or proxied
|
|
265
|
+
address (for example a backend service URL) derives issuers that no token will
|
|
266
|
+
ever carry, and verification fails with `jwt.InvalidIssuerError`. Pass
|
|
267
|
+
`allowed_issuers` explicitly if you need to talk to a site through such an
|
|
268
|
+
address.
|
|
269
|
+
|
|
270
|
+
Passing `allowed_issuers` yourself replaces both rules with an exact-match list.
|
|
271
|
+
|
|
215
272
|
## Error Handling
|
|
216
273
|
|
|
217
274
|
```python
|
|
@@ -9,6 +9,7 @@ Python client library for VisionAI authentication and Vision Language Model (VLM
|
|
|
9
9
|
- **Auto Token Management**: Automatic token refresh before expiration
|
|
10
10
|
- **JWT Validation**: Built-in token signature and expiration verification
|
|
11
11
|
- **VLM Inference**: Submit and poll vision-language model tasks
|
|
12
|
+
- **Resize Planning**: Compute VLM input dimensions (smart / square resize) without any image dependency — the caller does the actual resize
|
|
12
13
|
- **Async Support**: Full async/await support with `AsyncClient`
|
|
13
14
|
- **Type Safe**: Full type hints with Pydantic validation
|
|
14
15
|
- **Service Source Attribution**: Auto-attach a service-source header to every outbound `requests`/`httpx`/`aiohttp` call with a single startup call
|
|
@@ -169,6 +170,43 @@ elif result.status in ("failed", "timeout"):
|
|
|
169
170
|
- `failed`: Error, check `error`
|
|
170
171
|
- `timeout`: Request timeout
|
|
171
172
|
|
|
173
|
+
### Resize Planning
|
|
174
|
+
|
|
175
|
+
Compute the target dimensions for a VLM input image. Pure math, no image
|
|
176
|
+
dependencies — resize with whatever library your service already uses,
|
|
177
|
+
applying `plan.interpolation`. `name`, `factor`, and `interpolation` come
|
|
178
|
+
from the model server's resize spec; `pixels` is the UI resize option:
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
from visionai_sdk_python.vlm import compute_resize
|
|
182
|
+
|
|
183
|
+
plan = compute_resize(
|
|
184
|
+
width=1920, height=1080, name="smart_resize", factor=32, pixels=768,
|
|
185
|
+
interpolation="bicubic",
|
|
186
|
+
)
|
|
187
|
+
# ResizePlan(width=1024, height=576, interpolation='bicubic')
|
|
188
|
+
# smart_resize: dimensions divisible by factor, area capped at pixels * pixels
|
|
189
|
+
|
|
190
|
+
compute_resize(width=w, height=h, name="square_resize", pixels=384, interpolation="lanczos")
|
|
191
|
+
# square_resize: exact pixels x pixels; factor is ignored, so a whole resize
|
|
192
|
+
# spec can be forwarded as-is
|
|
193
|
+
|
|
194
|
+
# No resize spec from the model server? Fall back to the default
|
|
195
|
+
# (smart_resize, factor 32, bicubic):
|
|
196
|
+
from visionai_sdk_python.vlm import DEFAULT_RESIZE_SPEC
|
|
197
|
+
plan = compute_resize(width=w, height=h, pixels=768, **DEFAULT_RESIZE_SPEC)
|
|
198
|
+
|
|
199
|
+
# Apply with your own imaging library (PIL shown; see module docstring for cv2)
|
|
200
|
+
interpolation = {"bicubic": Image.Resampling.BICUBIC,
|
|
201
|
+
"lanczos": Image.Resampling.LANCZOS}[plan.interpolation]
|
|
202
|
+
img = img.resize((plan.width, plan.height), interpolation)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Invalid input (non-positive dimensions, a non-integer or non-positive
|
|
206
|
+
`pixels`/`factor`, unknown `name` or `interpolation`, missing `factor` for
|
|
207
|
+
smart resize, `min_pixels` on a square resize or over the pixel budget, an
|
|
208
|
+
unsatisfiable budget) raises `ValueError`.
|
|
209
|
+
|
|
172
210
|
## Token Validation
|
|
173
211
|
|
|
174
212
|
```python
|
|
@@ -194,6 +232,25 @@ client = Client(
|
|
|
194
232
|
)
|
|
195
233
|
```
|
|
196
234
|
|
|
235
|
+
### Token issuers
|
|
236
|
+
|
|
237
|
+
`allowed_issuers` is optional. When you leave it out, the SDK derives the
|
|
238
|
+
trusted issuers from `auth_url` and accepts a token whose `iss` claim either:
|
|
239
|
+
|
|
240
|
+
- equals `{auth_url}/keycloak/realms/linker-platform`, the realm every
|
|
241
|
+
deployment hosts behind its own base URL; or
|
|
242
|
+
- equals an external issuer the site is known to still accept — today only
|
|
243
|
+
production, which takes its Auth0 tenant alongside Keycloak while it migrates.
|
|
244
|
+
|
|
245
|
+
That means `auth_url` has to be the site's **public base URL** — the same host
|
|
246
|
+
that appears in the token's `iss` claim. Pointing it at an in-cluster or proxied
|
|
247
|
+
address (for example a backend service URL) derives issuers that no token will
|
|
248
|
+
ever carry, and verification fails with `jwt.InvalidIssuerError`. Pass
|
|
249
|
+
`allowed_issuers` explicitly if you need to talk to a site through such an
|
|
250
|
+
address.
|
|
251
|
+
|
|
252
|
+
Passing `allowed_issuers` yourself replaces both rules with an exact-match list.
|
|
253
|
+
|
|
197
254
|
## Error Handling
|
|
198
255
|
|
|
199
256
|
```python
|
|
@@ -38,7 +38,7 @@ class _BaseClient:
|
|
|
38
38
|
vlm_url: Base URL for the VLM inference service.
|
|
39
39
|
allowed_issuers: Optional list of allowed JWT issuers. If provided, tokens
|
|
40
40
|
whose ``iss`` claim is not in this list will be rejected. If omitted,
|
|
41
|
-
|
|
41
|
+
the issuers are derived from ``auth_url``.
|
|
42
42
|
verify_ssl: Whether to verify TLS certificates.
|
|
43
43
|
timeout: Default request timeout in seconds.
|
|
44
44
|
max_connections: Maximum number of concurrent connections in the pool.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Resolves the trusted JWT issuers for a VisionAI server base URL.
|
|
2
|
+
#
|
|
3
|
+
# Every deployment hosts its own Keycloak behind the site's public base URL, so
|
|
4
|
+
# the realm issuer is derived from `auth_url` rather than kept in a table. Only
|
|
5
|
+
# sites that additionally accept tokens from an external IdP need an entry here.
|
|
6
|
+
#
|
|
7
|
+
# Long-term: replace this with dynamic discovery from the server's
|
|
8
|
+
# trusted-issuers endpoint (e.g. GET {auth_url}/api/v1/auth/trusted-issuers).
|
|
9
|
+
|
|
10
|
+
_KEYCLOAK_REALM_PATH = "/keycloak/realms/linker-platform"
|
|
11
|
+
|
|
12
|
+
# Sites whose tokens may still come from an external IdP, in addition to their
|
|
13
|
+
# own Keycloak realm. Production has not cut over yet; listing both issuers lets
|
|
14
|
+
# an installed SDK keep working across the migration without an upgrade.
|
|
15
|
+
#
|
|
16
|
+
# Drop the entry once production is confirmed migrated — a retired Auth0 tenant
|
|
17
|
+
# name can be re-registered by someone else, so it should not stay trusted
|
|
18
|
+
# indefinitely.
|
|
19
|
+
_EXTERNAL_IDP_ISSUERS: dict[str, list[str]] = {
|
|
20
|
+
"https://visionai.linkervision.com": [
|
|
21
|
+
"https://data-engine-prod.us.auth0.com",
|
|
22
|
+
],
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def resolve_allowed_issuers(auth_url: str) -> list[str]:
|
|
27
|
+
"""Return the trusted issuers for the given auth_url.
|
|
28
|
+
|
|
29
|
+
Always includes the site's own Keycloak realm issuer, plus any external IdP
|
|
30
|
+
issuer the site still accepts.
|
|
31
|
+
"""
|
|
32
|
+
normalized = auth_url.rstrip("/")
|
|
33
|
+
return [
|
|
34
|
+
f"{normalized}{_KEYCLOAK_REALM_PATH}",
|
|
35
|
+
*_EXTERNAL_IDP_ISSUERS.get(normalized, []),
|
|
36
|
+
]
|
{visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/vlm/__init__.py
RENAMED
|
@@ -2,12 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
from .async_resource import AsyncVLMResource
|
|
4
4
|
from .models import NIMRequestModel, ResponseErrorModel, ResponseNormalModel
|
|
5
|
+
from .preprocess import (
|
|
6
|
+
DEFAULT_RESIZE_SPEC,
|
|
7
|
+
Interpolation,
|
|
8
|
+
ResizeMode,
|
|
9
|
+
ResizePlan,
|
|
10
|
+
compute_resize,
|
|
11
|
+
)
|
|
5
12
|
from .resource import VLMResource
|
|
6
13
|
|
|
7
14
|
__all__ = [
|
|
15
|
+
"DEFAULT_RESIZE_SPEC",
|
|
8
16
|
"AsyncVLMResource",
|
|
9
|
-
"
|
|
17
|
+
"Interpolation",
|
|
10
18
|
"NIMRequestModel",
|
|
11
|
-
"
|
|
19
|
+
"ResizeMode",
|
|
20
|
+
"ResizePlan",
|
|
12
21
|
"ResponseErrorModel",
|
|
22
|
+
"ResponseNormalModel",
|
|
23
|
+
"VLMResource",
|
|
24
|
+
"compute_resize",
|
|
13
25
|
]
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Image-resize planning helpers for VLM inference.
|
|
2
|
+
|
|
3
|
+
This module only computes target dimensions (pure math, no image
|
|
4
|
+
dependencies). The mode (``name``), ``factor``, and ``interpolation`` come
|
|
5
|
+
from the model server's resize spec; ``pixels`` comes from the UI resize
|
|
6
|
+
option. The actual resize is done by the caller with whatever imaging
|
|
7
|
+
library its pipeline already uses, applying ``plan.interpolation``:
|
|
8
|
+
|
|
9
|
+
plan = compute_resize(
|
|
10
|
+
width=w, height=h, name="smart_resize", factor=32, pixels=768,
|
|
11
|
+
interpolation="bicubic",
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
# PIL client
|
|
15
|
+
PIL_INTERPOLATION = {
|
|
16
|
+
"bicubic": Image.Resampling.BICUBIC,
|
|
17
|
+
"lanczos": Image.Resampling.LANCZOS,
|
|
18
|
+
}
|
|
19
|
+
out = img.resize((plan.width, plan.height), PIL_INTERPOLATION[plan.interpolation])
|
|
20
|
+
|
|
21
|
+
# OpenCV client
|
|
22
|
+
CV2_INTERPOLATION = {
|
|
23
|
+
"bicubic": cv2.INTER_CUBIC,
|
|
24
|
+
"lanczos": cv2.INTER_LANCZOS4,
|
|
25
|
+
}
|
|
26
|
+
out = cv2.resize(
|
|
27
|
+
arr, (plan.width, plan.height), interpolation=CV2_INTERPOLATION[plan.interpolation]
|
|
28
|
+
)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
import math
|
|
32
|
+
from collections.abc import Mapping
|
|
33
|
+
from types import MappingProxyType
|
|
34
|
+
from typing import Any, Literal, NamedTuple, get_args
|
|
35
|
+
|
|
36
|
+
_MAX_ASPECT_RATIO = 200
|
|
37
|
+
|
|
38
|
+
ResizeMode = Literal["smart_resize", "square_resize"]
|
|
39
|
+
Interpolation = Literal["bicubic", "lanczos"]
|
|
40
|
+
|
|
41
|
+
# Fallback resize spec for when the model server does not provide one.
|
|
42
|
+
# Read-only; splat it into compute_resize alongside the UI pixels value:
|
|
43
|
+
# compute_resize(width=w, height=h, pixels=768, **DEFAULT_RESIZE_SPEC)
|
|
44
|
+
DEFAULT_RESIZE_SPEC: Mapping[str, Any] = MappingProxyType(
|
|
45
|
+
{"name": "smart_resize", "factor": 32, "interpolation": "bicubic"}
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ResizePlan(NamedTuple):
|
|
50
|
+
"""Target dimensions and interpolation method for a client-side resize."""
|
|
51
|
+
|
|
52
|
+
width: int
|
|
53
|
+
height: int
|
|
54
|
+
interpolation: Interpolation
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _smart_resize(
|
|
58
|
+
height: int,
|
|
59
|
+
width: int,
|
|
60
|
+
factor: int,
|
|
61
|
+
min_pixels: int,
|
|
62
|
+
max_pixels: int,
|
|
63
|
+
) -> tuple[int, int]:
|
|
64
|
+
"""Compute target (height, width) — note height-first, unlike ResizePlan.
|
|
65
|
+
|
|
66
|
+
1. Both dimensions are divisible by ``factor``.
|
|
67
|
+
2. The total number of pixels is within [``min_pixels``, ``max_pixels``].
|
|
68
|
+
3. The aspect ratio is maintained as closely as possible.
|
|
69
|
+
|
|
70
|
+
Raises ValueError when the aspect ratio exceeds 200 or the constraints
|
|
71
|
+
cannot all be satisfied. Argument validation is compute_resize's job.
|
|
72
|
+
"""
|
|
73
|
+
if max(height, width) / min(height, width) > _MAX_ASPECT_RATIO:
|
|
74
|
+
raise ValueError(
|
|
75
|
+
"absolute aspect ratio must be smaller than "
|
|
76
|
+
f"{_MAX_ASPECT_RATIO}, got {max(height, width) / min(height, width)}"
|
|
77
|
+
)
|
|
78
|
+
h_bar = max(factor, round(height / factor) * factor)
|
|
79
|
+
w_bar = max(factor, round(width / factor) * factor)
|
|
80
|
+
if h_bar * w_bar > max_pixels:
|
|
81
|
+
beta = math.sqrt((height * width) / max_pixels)
|
|
82
|
+
h_bar = max(factor, math.floor(height / beta / factor) * factor)
|
|
83
|
+
w_bar = max(factor, math.floor(width / beta / factor) * factor)
|
|
84
|
+
elif h_bar * w_bar < min_pixels:
|
|
85
|
+
beta = math.sqrt(min_pixels / (height * width))
|
|
86
|
+
h_bar = math.ceil(height * beta / factor) * factor
|
|
87
|
+
w_bar = math.ceil(width * beta / factor) * factor
|
|
88
|
+
if not min_pixels <= h_bar * w_bar <= max_pixels:
|
|
89
|
+
raise ValueError(
|
|
90
|
+
f"cannot satisfy min_pixels={min_pixels}, max_pixels={max_pixels} for a "
|
|
91
|
+
f"{width}x{height} image with factor={factor}"
|
|
92
|
+
)
|
|
93
|
+
return h_bar, w_bar
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def compute_resize(
|
|
97
|
+
*,
|
|
98
|
+
width: int,
|
|
99
|
+
height: int,
|
|
100
|
+
name: ResizeMode,
|
|
101
|
+
pixels: int,
|
|
102
|
+
interpolation: Interpolation,
|
|
103
|
+
factor: int | None = None,
|
|
104
|
+
min_pixels: int | None = None,
|
|
105
|
+
) -> ResizePlan:
|
|
106
|
+
"""Plan a VLM-input resize from the model server's resize spec.
|
|
107
|
+
|
|
108
|
+
All arguments are keyword-only — a transposed width/height cannot happen
|
|
109
|
+
silently. ``name``, ``factor``, and ``interpolation`` mirror the model
|
|
110
|
+
server's resize spec fields; ``pixels`` is the UI resize option value.
|
|
111
|
+
|
|
112
|
+
- ``name="smart_resize"`` — dimensions divisible by ``factor``, total
|
|
113
|
+
pixel count within [``min_pixels``, ``pixels * pixels``], aspect ratio
|
|
114
|
+
kept. ``factor`` is required (the model's patch size, e.g. 32 or 48);
|
|
115
|
+
``min_pixels`` defaults to ``4 * factor * factor``.
|
|
116
|
+
- ``name="square_resize"`` — exact ``pixels`` x ``pixels``; aspect ratio
|
|
117
|
+
not preserved. ``factor`` has no meaning and is ignored, so a whole
|
|
118
|
+
server resize spec can be forwarded as-is.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
width: Source image width in pixels.
|
|
122
|
+
height: Source image height in pixels.
|
|
123
|
+
name: Resize mode from the model server: "smart_resize" or
|
|
124
|
+
"square_resize".
|
|
125
|
+
pixels: UI pixel option — one number, but its meaning depends on
|
|
126
|
+
``name``:
|
|
127
|
+
|
|
128
|
+
- ``"smart_resize"``: an area budget, not a side length. The
|
|
129
|
+
output area is capped at ``pixels * pixels`` while keeping the
|
|
130
|
+
source aspect ratio, so neither output side is generally equal
|
|
131
|
+
to ``pixels`` — e.g. ``pixels=768`` on a 1920x1080 frame gives
|
|
132
|
+
1024x576 (= 589,824 px, the same area as 768x768).
|
|
133
|
+
- ``"square_resize"``: the exact side length. The output is
|
|
134
|
+
always ``pixels`` x ``pixels`` — e.g. ``pixels=384`` gives
|
|
135
|
+
384x384 regardless of the source shape.
|
|
136
|
+
interpolation: Interpolation the caller should resize with,
|
|
137
|
+
from the model server: "bicubic" or "lanczos". Passed through
|
|
138
|
+
into the returned plan.
|
|
139
|
+
factor: Smart resize only: round dimensions to multiples of this.
|
|
140
|
+
min_pixels: Smart resize only: lower bound on output pixel count.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
ResizePlan(width, height, interpolation) — note width-first,
|
|
144
|
+
matching the (width, height) order PIL and OpenCV resize calls expect.
|
|
145
|
+
|
|
146
|
+
Raises:
|
|
147
|
+
ValueError: If height/width are not positive, ``pixels`` or ``factor``
|
|
148
|
+
is not a positive integer, ``name`` or ``interpolation`` is not a
|
|
149
|
+
supported value, ``factor`` is missing (smart), ``min_pixels`` is
|
|
150
|
+
given for square resize or exceeds the pixel budget, or the
|
|
151
|
+
constraints cannot be satisfied (extreme aspect ratio).
|
|
152
|
+
"""
|
|
153
|
+
if height <= 0 or width <= 0:
|
|
154
|
+
raise ValueError(f"height and width must be positive, got {width}x{height}")
|
|
155
|
+
if name not in get_args(ResizeMode):
|
|
156
|
+
raise ValueError(f"name must be one of {get_args(ResizeMode)}, got {name!r}")
|
|
157
|
+
if interpolation not in get_args(Interpolation):
|
|
158
|
+
raise ValueError(
|
|
159
|
+
f"interpolation must be one of {get_args(Interpolation)}, "
|
|
160
|
+
f"got {interpolation!r}"
|
|
161
|
+
)
|
|
162
|
+
if isinstance(pixels, bool) or not isinstance(pixels, int) or pixels <= 0:
|
|
163
|
+
raise ValueError(f"pixels must be a positive integer, got {pixels!r}")
|
|
164
|
+
|
|
165
|
+
if name == "smart_resize":
|
|
166
|
+
if factor is None:
|
|
167
|
+
raise ValueError("smart_resize requires factor")
|
|
168
|
+
if isinstance(factor, bool) or not isinstance(factor, int) or factor <= 0:
|
|
169
|
+
raise ValueError(f"factor must be a positive integer, got {factor!r}")
|
|
170
|
+
max_pixels = pixels * pixels
|
|
171
|
+
if min_pixels is None:
|
|
172
|
+
min_pixels = 4 * factor * factor
|
|
173
|
+
if min_pixels <= 0 or min_pixels > max_pixels:
|
|
174
|
+
raise ValueError(
|
|
175
|
+
f"min_pixels must be in (0, pixels * pixels], got "
|
|
176
|
+
f"min_pixels={min_pixels}, pixels={pixels}"
|
|
177
|
+
)
|
|
178
|
+
h_bar, w_bar = _smart_resize(height, width, factor, min_pixels, max_pixels)
|
|
179
|
+
else:
|
|
180
|
+
# factor is part of the server resize spec, so a caller forwarding the
|
|
181
|
+
# whole spec may pass it; it has no meaning here, so ignore it.
|
|
182
|
+
if min_pixels is not None:
|
|
183
|
+
raise ValueError("min_pixels only applies to smart_resize")
|
|
184
|
+
w_bar = h_bar = pixels
|
|
185
|
+
|
|
186
|
+
return ResizePlan(width=w_bar, height=h_bar, interpolation=interpolation)
|
{visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: visionai-sdk-python
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.1
|
|
4
4
|
Summary: VisionAI SDK for Python
|
|
5
5
|
Author-email: Tony Yang <tonyyang@linkervision.com>
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -27,6 +27,7 @@ Python client library for VisionAI authentication and Vision Language Model (VLM
|
|
|
27
27
|
- **Auto Token Management**: Automatic token refresh before expiration
|
|
28
28
|
- **JWT Validation**: Built-in token signature and expiration verification
|
|
29
29
|
- **VLM Inference**: Submit and poll vision-language model tasks
|
|
30
|
+
- **Resize Planning**: Compute VLM input dimensions (smart / square resize) without any image dependency — the caller does the actual resize
|
|
30
31
|
- **Async Support**: Full async/await support with `AsyncClient`
|
|
31
32
|
- **Type Safe**: Full type hints with Pydantic validation
|
|
32
33
|
- **Service Source Attribution**: Auto-attach a service-source header to every outbound `requests`/`httpx`/`aiohttp` call with a single startup call
|
|
@@ -187,6 +188,43 @@ elif result.status in ("failed", "timeout"):
|
|
|
187
188
|
- `failed`: Error, check `error`
|
|
188
189
|
- `timeout`: Request timeout
|
|
189
190
|
|
|
191
|
+
### Resize Planning
|
|
192
|
+
|
|
193
|
+
Compute the target dimensions for a VLM input image. Pure math, no image
|
|
194
|
+
dependencies — resize with whatever library your service already uses,
|
|
195
|
+
applying `plan.interpolation`. `name`, `factor`, and `interpolation` come
|
|
196
|
+
from the model server's resize spec; `pixels` is the UI resize option:
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
from visionai_sdk_python.vlm import compute_resize
|
|
200
|
+
|
|
201
|
+
plan = compute_resize(
|
|
202
|
+
width=1920, height=1080, name="smart_resize", factor=32, pixels=768,
|
|
203
|
+
interpolation="bicubic",
|
|
204
|
+
)
|
|
205
|
+
# ResizePlan(width=1024, height=576, interpolation='bicubic')
|
|
206
|
+
# smart_resize: dimensions divisible by factor, area capped at pixels * pixels
|
|
207
|
+
|
|
208
|
+
compute_resize(width=w, height=h, name="square_resize", pixels=384, interpolation="lanczos")
|
|
209
|
+
# square_resize: exact pixels x pixels; factor is ignored, so a whole resize
|
|
210
|
+
# spec can be forwarded as-is
|
|
211
|
+
|
|
212
|
+
# No resize spec from the model server? Fall back to the default
|
|
213
|
+
# (smart_resize, factor 32, bicubic):
|
|
214
|
+
from visionai_sdk_python.vlm import DEFAULT_RESIZE_SPEC
|
|
215
|
+
plan = compute_resize(width=w, height=h, pixels=768, **DEFAULT_RESIZE_SPEC)
|
|
216
|
+
|
|
217
|
+
# Apply with your own imaging library (PIL shown; see module docstring for cv2)
|
|
218
|
+
interpolation = {"bicubic": Image.Resampling.BICUBIC,
|
|
219
|
+
"lanczos": Image.Resampling.LANCZOS}[plan.interpolation]
|
|
220
|
+
img = img.resize((plan.width, plan.height), interpolation)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Invalid input (non-positive dimensions, a non-integer or non-positive
|
|
224
|
+
`pixels`/`factor`, unknown `name` or `interpolation`, missing `factor` for
|
|
225
|
+
smart resize, `min_pixels` on a square resize or over the pixel budget, an
|
|
226
|
+
unsatisfiable budget) raises `ValueError`.
|
|
227
|
+
|
|
190
228
|
## Token Validation
|
|
191
229
|
|
|
192
230
|
```python
|
|
@@ -212,6 +250,25 @@ client = Client(
|
|
|
212
250
|
)
|
|
213
251
|
```
|
|
214
252
|
|
|
253
|
+
### Token issuers
|
|
254
|
+
|
|
255
|
+
`allowed_issuers` is optional. When you leave it out, the SDK derives the
|
|
256
|
+
trusted issuers from `auth_url` and accepts a token whose `iss` claim either:
|
|
257
|
+
|
|
258
|
+
- equals `{auth_url}/keycloak/realms/linker-platform`, the realm every
|
|
259
|
+
deployment hosts behind its own base URL; or
|
|
260
|
+
- equals an external issuer the site is known to still accept — today only
|
|
261
|
+
production, which takes its Auth0 tenant alongside Keycloak while it migrates.
|
|
262
|
+
|
|
263
|
+
That means `auth_url` has to be the site's **public base URL** — the same host
|
|
264
|
+
that appears in the token's `iss` claim. Pointing it at an in-cluster or proxied
|
|
265
|
+
address (for example a backend service URL) derives issuers that no token will
|
|
266
|
+
ever carry, and verification fails with `jwt.InvalidIssuerError`. Pass
|
|
267
|
+
`allowed_issuers` explicitly if you need to talk to a site through such an
|
|
268
|
+
address.
|
|
269
|
+
|
|
270
|
+
Passing `allowed_issuers` yourself replaces both rules with an exact-match list.
|
|
271
|
+
|
|
215
272
|
## Error Handling
|
|
216
273
|
|
|
217
274
|
```python
|
{visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python.egg-info/SOURCES.txt
RENAMED
|
@@ -33,10 +33,12 @@ src/visionai_sdk_python/vlm/__init__.py
|
|
|
33
33
|
src/visionai_sdk_python/vlm/_mixin.py
|
|
34
34
|
src/visionai_sdk_python/vlm/async_resource.py
|
|
35
35
|
src/visionai_sdk_python/vlm/models.py
|
|
36
|
+
src/visionai_sdk_python/vlm/preprocess.py
|
|
36
37
|
src/visionai_sdk_python/vlm/resource.py
|
|
37
38
|
tests/__init__.py
|
|
38
39
|
tests/constants.py
|
|
39
40
|
tests/test__base.py
|
|
40
41
|
tests/test_async_client.py
|
|
41
42
|
tests/test_client.py
|
|
42
|
-
tests/test_instrumentation.py
|
|
43
|
+
tests/test_instrumentation.py
|
|
44
|
+
tests/test_vlm_preprocess.py
|
|
@@ -1,39 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"files": [
|
|
3
|
-
"pyproject.toml",
|
|
4
|
-
"README.md",
|
|
5
|
-
"uv.lock",
|
|
6
|
-
".gitignore",
|
|
7
3
|
".github/dependabot.yml",
|
|
8
4
|
".github/workflows/ci.yml",
|
|
9
5
|
".github/workflows/publish.yml",
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"tests/test_client.py",
|
|
14
|
-
"tests/test__base.py",
|
|
15
|
-
"tests/constants.py",
|
|
16
|
-
"src/visionai_sdk_python/_base.py",
|
|
17
|
-
"src/visionai_sdk_python/async_client.py",
|
|
18
|
-
"src/visionai_sdk_python/endpoints.py",
|
|
19
|
-
"src/visionai_sdk_python/_source_header.py",
|
|
20
|
-
"src/visionai_sdk_python/_request_attribution.py",
|
|
21
|
-
"src/visionai_sdk_python/py.typed",
|
|
6
|
+
".gitignore",
|
|
7
|
+
"README.md",
|
|
8
|
+
"pyproject.toml",
|
|
22
9
|
"src/visionai_sdk_python/__init__.py",
|
|
10
|
+
"src/visionai_sdk_python/_base.py",
|
|
23
11
|
"src/visionai_sdk_python/_jwt_verifier.py",
|
|
24
|
-
"src/visionai_sdk_python/
|
|
25
|
-
"src/visionai_sdk_python/
|
|
26
|
-
"src/visionai_sdk_python/
|
|
27
|
-
"src/visionai_sdk_python/constants.py",
|
|
12
|
+
"src/visionai_sdk_python/_request_attribution.py",
|
|
13
|
+
"src/visionai_sdk_python/_source_header.py",
|
|
14
|
+
"src/visionai_sdk_python/async_client.py",
|
|
28
15
|
"src/visionai_sdk_python/auth/__init__.py",
|
|
16
|
+
"src/visionai_sdk_python/auth/_mixin.py",
|
|
29
17
|
"src/visionai_sdk_python/auth/async_resource.py",
|
|
30
|
-
"src/visionai_sdk_python/auth/resource.py",
|
|
31
18
|
"src/visionai_sdk_python/auth/models.py",
|
|
32
|
-
"src/visionai_sdk_python/auth/
|
|
19
|
+
"src/visionai_sdk_python/auth/resource.py",
|
|
20
|
+
"src/visionai_sdk_python/client.py",
|
|
21
|
+
"src/visionai_sdk_python/constants.py",
|
|
22
|
+
"src/visionai_sdk_python/endpoints.py",
|
|
23
|
+
"src/visionai_sdk_python/exceptions.py",
|
|
24
|
+
"src/visionai_sdk_python/instrumentation.py",
|
|
25
|
+
"src/visionai_sdk_python/py.typed",
|
|
33
26
|
"src/visionai_sdk_python/vlm/__init__.py",
|
|
27
|
+
"src/visionai_sdk_python/vlm/_mixin.py",
|
|
34
28
|
"src/visionai_sdk_python/vlm/async_resource.py",
|
|
35
|
-
"src/visionai_sdk_python/vlm/resource.py",
|
|
36
29
|
"src/visionai_sdk_python/vlm/models.py",
|
|
37
|
-
"src/visionai_sdk_python/vlm/
|
|
30
|
+
"src/visionai_sdk_python/vlm/preprocess.py",
|
|
31
|
+
"src/visionai_sdk_python/vlm/resource.py",
|
|
32
|
+
"tests/__init__.py",
|
|
33
|
+
"tests/constants.py",
|
|
34
|
+
"tests/test__base.py",
|
|
35
|
+
"tests/test_async_client.py",
|
|
36
|
+
"tests/test_client.py",
|
|
37
|
+
"tests/test_instrumentation.py",
|
|
38
|
+
"tests/test_vlm_preprocess.py",
|
|
39
|
+
"uv.lock"
|
|
38
40
|
]
|
|
39
41
|
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import jwt
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
from visionai_sdk_python._base import _BaseClient
|
|
5
|
+
from visionai_sdk_python.constants import resolve_allowed_issuers
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_init_stores_attributes() -> None:
|
|
9
|
+
client = _BaseClient(
|
|
10
|
+
auth_url="https://auth.example.com", vlm_url="https://vlm.example.com"
|
|
11
|
+
)
|
|
12
|
+
assert client.auth_url == "https://auth.example.com"
|
|
13
|
+
assert client.vlm_url == "https://vlm.example.com"
|
|
14
|
+
assert client.verify_ssl is True
|
|
15
|
+
assert client.timeout == 10.0
|
|
16
|
+
assert client.max_connections == 100
|
|
17
|
+
assert client.max_keepalive_connections == 20
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.mark.parametrize(
|
|
21
|
+
"base_url, path, expected",
|
|
22
|
+
[
|
|
23
|
+
# Standard case
|
|
24
|
+
("https://auth.example.com", "token", "https://auth.example.com/token"),
|
|
25
|
+
# Trailing slash on base
|
|
26
|
+
("https://auth.example.com/", "token", "https://auth.example.com/token"),
|
|
27
|
+
# Leading slash on path
|
|
28
|
+
("https://auth.example.com", "/token", "https://auth.example.com/token"),
|
|
29
|
+
# Both have slashes — should not double-slash
|
|
30
|
+
("https://auth.example.com/", "/token", "https://auth.example.com/token"),
|
|
31
|
+
# Nested path
|
|
32
|
+
("https://vlm.example.com", "v1/infer", "https://vlm.example.com/v1/infer"),
|
|
33
|
+
# Empty path — should not raise, returns base with trailing slash
|
|
34
|
+
("https://auth.example.com", "", "https://auth.example.com/"),
|
|
35
|
+
],
|
|
36
|
+
)
|
|
37
|
+
def test_slash_normalization(base_url: str, path: str, expected: str) -> None:
|
|
38
|
+
assert _BaseClient._build_url(base_url, path) == expected
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_auth_header() -> None:
|
|
42
|
+
headers = _BaseClient._build_auth_header(access_token="my-token")
|
|
43
|
+
assert headers["Authorization"] == "Bearer my-token"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@pytest.mark.parametrize("empty_url", ["", " "])
|
|
47
|
+
def test_init_raises_on_empty_auth_url(empty_url: str) -> None:
|
|
48
|
+
with pytest.raises(ValueError, match="auth_url"):
|
|
49
|
+
_BaseClient(auth_url=empty_url, vlm_url="https://vlm.example.com")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@pytest.mark.parametrize("empty_url", ["", " "])
|
|
53
|
+
def test_init_raises_on_empty_vlm_url(empty_url: str) -> None:
|
|
54
|
+
with pytest.raises(ValueError, match="vlm_url"):
|
|
55
|
+
_BaseClient(auth_url="https://auth.example.com", vlm_url=empty_url)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_auth_header_raises_on_empty_token() -> None:
|
|
59
|
+
with pytest.raises(ValueError, match="access_token"):
|
|
60
|
+
_BaseClient._build_auth_header(access_token="")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
_KEYCLOAK_SITES = [
|
|
64
|
+
"https://staging.visionai.linkervision.com",
|
|
65
|
+
"https://dev.visionai.linkervision.com",
|
|
66
|
+
"https://dev2.visionai.linkervision.com",
|
|
67
|
+
"https://dev3.visionai.linkervision.com",
|
|
68
|
+
"https://offline.visionai.linkervision.com",
|
|
69
|
+
"https://lighthouse.visionai.linkervision.ai",
|
|
70
|
+
"https://lighthouse-production.visionai.linkervision.ai",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@pytest.mark.parametrize("auth_url", _KEYCLOAK_SITES)
|
|
75
|
+
def test_resolve_allowed_issuers_for_keycloak_sites(auth_url: str) -> None:
|
|
76
|
+
"""Every site that has cut over to Keycloak resolves to its realm issuer."""
|
|
77
|
+
assert resolve_allowed_issuers(auth_url) == [
|
|
78
|
+
f"{auth_url}/keycloak/realms/linker-platform"
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_resolve_allowed_issuers_for_production() -> None:
|
|
83
|
+
"""Production accepts its Auth0 tenant and its Keycloak realm during the cutover."""
|
|
84
|
+
assert resolve_allowed_issuers("https://visionai.linkervision.com") == [
|
|
85
|
+
"https://visionai.linkervision.com/keycloak/realms/linker-platform",
|
|
86
|
+
"https://data-engine-prod.us.auth0.com",
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_resolve_allowed_issuers_falls_back_to_keycloak() -> None:
|
|
91
|
+
"""An unknown host is assumed to be a Keycloak deployment."""
|
|
92
|
+
assert resolve_allowed_issuers("https://somewhere.example.com") == [
|
|
93
|
+
"https://somewhere.example.com/keycloak/realms/linker-platform"
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@pytest.mark.parametrize(
|
|
98
|
+
"auth_url",
|
|
99
|
+
[
|
|
100
|
+
"https://dev.visionai.linkervision.com/",
|
|
101
|
+
"https://dev.visionai.linkervision.com///",
|
|
102
|
+
],
|
|
103
|
+
)
|
|
104
|
+
def test_resolve_allowed_issuers_ignores_trailing_slashes(auth_url: str) -> None:
|
|
105
|
+
assert resolve_allowed_issuers(auth_url) == [
|
|
106
|
+
"https://dev.visionai.linkervision.com/keycloak/realms/linker-platform"
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_client_defaults_to_resolved_issuers() -> None:
|
|
111
|
+
"""Omitting allowed_issuers pins the client to the site's own issuer."""
|
|
112
|
+
client = _BaseClient(
|
|
113
|
+
auth_url="https://dev.visionai.linkervision.com",
|
|
114
|
+
vlm_url="https://vlm.example.com",
|
|
115
|
+
)
|
|
116
|
+
client._jwt_verifier._validate_issuer(
|
|
117
|
+
"https://dev.visionai.linkervision.com/keycloak/realms/linker-platform"
|
|
118
|
+
)
|
|
119
|
+
with pytest.raises(jwt.InvalidIssuerError):
|
|
120
|
+
client._jwt_verifier._validate_issuer("https://data-engine-dev2.jp.auth0.com")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_production_accepts_both_issuers_across_the_migration() -> None:
|
|
124
|
+
"""Production keeps working whether the token comes from Auth0 or Keycloak."""
|
|
125
|
+
client = _BaseClient(
|
|
126
|
+
auth_url="https://visionai.linkervision.com",
|
|
127
|
+
vlm_url="https://vlm.example.com",
|
|
128
|
+
)
|
|
129
|
+
client._jwt_verifier._validate_issuer("https://data-engine-prod.us.auth0.com")
|
|
130
|
+
client._jwt_verifier._validate_issuer(
|
|
131
|
+
"https://visionai.linkervision.com/keycloak/realms/linker-platform"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@pytest.mark.parametrize(
|
|
136
|
+
"issuer",
|
|
137
|
+
[
|
|
138
|
+
# Same realm path, different host.
|
|
139
|
+
"https://evil.example.com/keycloak/realms/linker-platform",
|
|
140
|
+
# Site host as a substring of an attacker-controlled one.
|
|
141
|
+
"https://dev.visionai.linkervision.com.evil.example.com/keycloak/realms/x",
|
|
142
|
+
# Right host, wrong scheme.
|
|
143
|
+
"http://dev.visionai.linkervision.com/keycloak/realms/linker-platform",
|
|
144
|
+
# Right host, different realm.
|
|
145
|
+
"https://dev.visionai.linkervision.com/keycloak/realms/another-realm",
|
|
146
|
+
],
|
|
147
|
+
)
|
|
148
|
+
def test_default_issuers_reject_lookalikes(issuer: str) -> None:
|
|
149
|
+
"""The derived issuer is matched exactly — near misses are not accepted."""
|
|
150
|
+
client = _BaseClient(
|
|
151
|
+
auth_url="https://dev.visionai.linkervision.com",
|
|
152
|
+
vlm_url="https://vlm.example.com",
|
|
153
|
+
)
|
|
154
|
+
with pytest.raises(jwt.InvalidIssuerError):
|
|
155
|
+
client._jwt_verifier._validate_issuer(issuer)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
_ALLOWED_ISSUERS = [
|
|
159
|
+
"https://offline.visionai.linkervision.com/keycloak/realms/linker-platform",
|
|
160
|
+
"https://data-engine-prod.us.auth0.com",
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@pytest.mark.parametrize("issuer", _ALLOWED_ISSUERS)
|
|
165
|
+
def test_validate_issuer_passes_for_allowed(issuer: str) -> None:
|
|
166
|
+
client = _BaseClient(
|
|
167
|
+
auth_url="https://auth.example.com",
|
|
168
|
+
vlm_url="https://vlm.example.com",
|
|
169
|
+
allowed_issuers=_ALLOWED_ISSUERS,
|
|
170
|
+
)
|
|
171
|
+
client._jwt_verifier._validate_issuer(issuer) # should not raise
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@pytest.mark.parametrize(
|
|
175
|
+
"issuer",
|
|
176
|
+
[
|
|
177
|
+
"https://evil.com/keycloak/realms/linker-platform",
|
|
178
|
+
"https://attacker.auth0.com/",
|
|
179
|
+
"https://offline.visionai.linkervision.com/keycloak/realms/other-realm",
|
|
180
|
+
],
|
|
181
|
+
)
|
|
182
|
+
def test_validate_issuer_raises_for_unknown(issuer: str) -> None:
|
|
183
|
+
client = _BaseClient(
|
|
184
|
+
auth_url="https://auth.example.com",
|
|
185
|
+
vlm_url="https://vlm.example.com",
|
|
186
|
+
allowed_issuers=_ALLOWED_ISSUERS,
|
|
187
|
+
)
|
|
188
|
+
with pytest.raises(jwt.InvalidIssuerError):
|
|
189
|
+
client._jwt_verifier._validate_issuer(issuer)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def test_validate_issuer_raises_for_unknown_when_no_allowed_issuers() -> None:
|
|
193
|
+
client = _BaseClient(
|
|
194
|
+
auth_url="https://auth.example.com",
|
|
195
|
+
vlm_url="https://vlm.example.com",
|
|
196
|
+
)
|
|
197
|
+
with pytest.raises(jwt.InvalidIssuerError):
|
|
198
|
+
client._jwt_verifier._validate_issuer("https://anyone.com/") # should not raise
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Tests for VLM image-resize planning helpers."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from visionai_sdk_python.vlm import compute_resize
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def smart(width, height, **overrides):
|
|
9
|
+
kwargs = {
|
|
10
|
+
"width": width,
|
|
11
|
+
"height": height,
|
|
12
|
+
"name": "smart_resize",
|
|
13
|
+
"factor": 32,
|
|
14
|
+
"pixels": 768,
|
|
15
|
+
"interpolation": "bicubic",
|
|
16
|
+
}
|
|
17
|
+
kwargs.update(overrides)
|
|
18
|
+
return compute_resize(**kwargs)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.mark.parametrize("factor", [32, 48])
|
|
22
|
+
@pytest.mark.parametrize(
|
|
23
|
+
"width,height",
|
|
24
|
+
[
|
|
25
|
+
(1920, 1080), # downscale (over the pixel budget)
|
|
26
|
+
(32, 32), # tiny, upscale to min_pixels
|
|
27
|
+
(375, 500), # within bounds, snap to factor
|
|
28
|
+
(4000, 3000), # large downscale
|
|
29
|
+
],
|
|
30
|
+
)
|
|
31
|
+
def test_smart_constraints(width, height, factor):
|
|
32
|
+
plan = smart(width, height, factor=factor)
|
|
33
|
+
assert plan.width % factor == 0
|
|
34
|
+
assert plan.height % factor == 0
|
|
35
|
+
assert 4 * factor * factor <= plan.width * plan.height <= 768 * 768
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.mark.parametrize(
|
|
39
|
+
"width,height,expected",
|
|
40
|
+
[
|
|
41
|
+
(1920, 1080, (1024, 576)), # README example
|
|
42
|
+
(1280, 8, (1280, 32)), # thin strip: short side snaps up, long side kept
|
|
43
|
+
(1000, 17, (992, 32)), # ...and does not jump at the factor/2 boundary
|
|
44
|
+
(1000, 16, (992, 32)),
|
|
45
|
+
(224, 2, (224, 32)), # already inside the budget — no upscale
|
|
46
|
+
],
|
|
47
|
+
)
|
|
48
|
+
def test_smart_golden_dimensions(width, height, expected):
|
|
49
|
+
# Pins parity with the model server's smart_resize; the constraint-only
|
|
50
|
+
# tests above pass for wrong-but-legal plans.
|
|
51
|
+
plan = smart(width, height)
|
|
52
|
+
assert (plan.width, plan.height) == expected
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_smart_keeps_aspect_ratio():
|
|
56
|
+
plan = smart(1920, 1080)
|
|
57
|
+
assert plan.width / plan.height == pytest.approx(1920 / 1080, rel=0.1)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_smart_orientation_not_transposed():
|
|
61
|
+
# Landscape in, landscape out — catches any width/height swap regression.
|
|
62
|
+
plan = smart(1920, 1080)
|
|
63
|
+
assert plan.width > plan.height
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_smart_rejects_extreme_aspect_ratio():
|
|
67
|
+
with pytest.raises(ValueError, match="aspect ratio"):
|
|
68
|
+
smart(2010, 10)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_smart_rejects_unsatisfiable_max_pixels():
|
|
72
|
+
# Elongated strip: aspect ratio passes the <=200 check, but the factor
|
|
73
|
+
# floor cannot fit the pixel budget. Must raise, not silently overshoot.
|
|
74
|
+
with pytest.raises(ValueError, match="cannot satisfy"):
|
|
75
|
+
smart(40000, 200, factor=48, pixels=384)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_smart_rejects_unsatisfiable_min_pixels():
|
|
79
|
+
# Tight budget + extreme shape floors below min_pixels.
|
|
80
|
+
with pytest.raises(ValueError, match="cannot satisfy"):
|
|
81
|
+
smart(20000, 10000, pixels=64)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_smart_min_pixels_defaults_to_four_patches():
|
|
85
|
+
plan = smart(32, 32)
|
|
86
|
+
assert plan.width * plan.height >= 4 * 32 * 32
|
|
87
|
+
assert plan == smart(32, 32, min_pixels=4 * 32 * 32)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_smart_requires_factor():
|
|
91
|
+
with pytest.raises(ValueError, match="requires factor"):
|
|
92
|
+
compute_resize(
|
|
93
|
+
width=1920,
|
|
94
|
+
height=1080,
|
|
95
|
+
name="smart_resize",
|
|
96
|
+
pixels=768,
|
|
97
|
+
interpolation="bicubic",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_smart_rejects_min_pixels_over_budget():
|
|
102
|
+
with pytest.raises(ValueError, match="min_pixels"):
|
|
103
|
+
smart(1920, 1080, min_pixels=100000, pixels=64)
|
|
104
|
+
# Implicit default min_pixels (4 * 48 * 48 = 9216) exceeds 64 * 64.
|
|
105
|
+
with pytest.raises(ValueError, match="min_pixels"):
|
|
106
|
+
smart(1920, 1080, factor=48, pixels=64)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@pytest.mark.parametrize(
|
|
110
|
+
"kwargs",
|
|
111
|
+
[{"factor": 0}, {"factor": -32}, {"factor": 32.5}, {"min_pixels": 0}],
|
|
112
|
+
)
|
|
113
|
+
def test_smart_rejects_bad_params(kwargs):
|
|
114
|
+
with pytest.raises(ValueError):
|
|
115
|
+
smart(1920, 1080, **kwargs)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_square():
|
|
119
|
+
plan = compute_resize(
|
|
120
|
+
width=1920,
|
|
121
|
+
height=1080,
|
|
122
|
+
name="square_resize",
|
|
123
|
+
pixels=384,
|
|
124
|
+
interpolation="lanczos",
|
|
125
|
+
)
|
|
126
|
+
assert (plan.width, plan.height) == (384, 384)
|
|
127
|
+
assert plan.interpolation == "lanczos"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_square_ignores_factor():
|
|
131
|
+
# factor comes from the server resize spec, so forwarding the whole spec
|
|
132
|
+
# must not blow up on a square model.
|
|
133
|
+
plan = compute_resize(
|
|
134
|
+
width=1920,
|
|
135
|
+
height=1080,
|
|
136
|
+
name="square_resize",
|
|
137
|
+
pixels=384,
|
|
138
|
+
factor=32,
|
|
139
|
+
interpolation="bicubic",
|
|
140
|
+
)
|
|
141
|
+
assert (plan.width, plan.height) == (384, 384)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_square_rejects_min_pixels():
|
|
145
|
+
with pytest.raises(ValueError, match="only applies to smart_resize"):
|
|
146
|
+
compute_resize(
|
|
147
|
+
width=1920,
|
|
148
|
+
height=1080,
|
|
149
|
+
name="square_resize",
|
|
150
|
+
pixels=384,
|
|
151
|
+
min_pixels=100,
|
|
152
|
+
interpolation="bicubic",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_positional_arguments_rejected():
|
|
157
|
+
with pytest.raises(TypeError):
|
|
158
|
+
compute_resize(
|
|
159
|
+
1920, 1080, name="square_resize", pixels=384, interpolation="lanczos"
|
|
160
|
+
) # type: ignore[misc]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@pytest.mark.parametrize("width,height", [(0, 1080), (1920, 0), (0, 0), (-1, 100)])
|
|
164
|
+
def test_rejects_non_positive_dimensions(width, height):
|
|
165
|
+
with pytest.raises(ValueError, match="height and width"):
|
|
166
|
+
smart(width, height)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@pytest.mark.parametrize("pixels", [0, -384, 384.0, True, "384"])
|
|
170
|
+
def test_rejects_bad_pixels(pixels):
|
|
171
|
+
# 384.0 is what a JSON-decoded UI option looks like; no imaging library
|
|
172
|
+
# accepts float dimensions, so reject at the boundary, not downstream.
|
|
173
|
+
with pytest.raises(ValueError, match="pixels must be a positive integer"):
|
|
174
|
+
compute_resize(
|
|
175
|
+
width=1920,
|
|
176
|
+
height=1080,
|
|
177
|
+
name="square_resize",
|
|
178
|
+
pixels=pixels,
|
|
179
|
+
interpolation="bicubic",
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def test_rejects_unknown_name():
|
|
184
|
+
with pytest.raises(ValueError, match="name must be one of"):
|
|
185
|
+
compute_resize(
|
|
186
|
+
width=1920,
|
|
187
|
+
height=1080,
|
|
188
|
+
name="longest_edge",
|
|
189
|
+
pixels=768,
|
|
190
|
+
interpolation="bicubic",
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def test_rejects_unknown_interpolation():
|
|
195
|
+
# bilinear is no longer a supported value.
|
|
196
|
+
with pytest.raises(ValueError, match="interpolation must be one of"):
|
|
197
|
+
smart(1920, 1080, interpolation="bilinear")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def test_default_resize_spec():
|
|
201
|
+
from visionai_sdk_python.vlm import DEFAULT_RESIZE_SPEC
|
|
202
|
+
|
|
203
|
+
plan = compute_resize(width=1920, height=1080, pixels=768, **DEFAULT_RESIZE_SPEC)
|
|
204
|
+
assert plan == compute_resize(
|
|
205
|
+
width=1920,
|
|
206
|
+
height=1080,
|
|
207
|
+
pixels=768,
|
|
208
|
+
name="smart_resize",
|
|
209
|
+
factor=32,
|
|
210
|
+
interpolation="bicubic",
|
|
211
|
+
)
|
|
212
|
+
with pytest.raises(TypeError):
|
|
213
|
+
DEFAULT_RESIZE_SPEC["factor"] = 48 # type: ignore[index]
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
# Maps known VisionAI server base URLs to their trusted JWT issuers.
|
|
2
|
-
# Keys are normalized (no trailing slash).
|
|
3
|
-
# Values are exact issuer strings as they appear in the JWT `iss` claim.
|
|
4
|
-
#
|
|
5
|
-
# Long-term: replace this static table with dynamic discovery from the server's
|
|
6
|
-
# trusted-issuers endpoint (e.g. GET {auth_url}/api/v1/auth/trusted-issuers).
|
|
7
|
-
|
|
8
|
-
_KEYCLOAK_REALM_PATH = "/keycloak/realms/linker-platform"
|
|
9
|
-
|
|
10
|
-
# On-premise deployments backed by Keycloak (realm: linker-platform).
|
|
11
|
-
# Issuer = base_url + _KEYCLOAK_REALM_PATH for all entries.
|
|
12
|
-
_KEYCLOAK_BASE_URLS: list[str] = [
|
|
13
|
-
"https://offline.visionai.linkervision.com",
|
|
14
|
-
"https://lighthouse.visionai.linkervision.ai",
|
|
15
|
-
"https://lighthouse-production.visionai.linkervision.ai",
|
|
16
|
-
]
|
|
17
|
-
|
|
18
|
-
# Cloud deployments backed by Auth0 (one tenant per environment).
|
|
19
|
-
_AUTH0_URL_TO_ISSUERS: dict[str, list[str]] = {
|
|
20
|
-
"https://visionai.linkervision.com": [
|
|
21
|
-
"https://data-engine-prod.us.auth0.com",
|
|
22
|
-
],
|
|
23
|
-
"https://staging.visionai.linkervision.com": [
|
|
24
|
-
"https://data-engine-staging.jp.auth0.com",
|
|
25
|
-
],
|
|
26
|
-
"https://dev2.visionai.linkervision.com": [
|
|
27
|
-
"https://data-engine-dev2.jp.auth0.com",
|
|
28
|
-
],
|
|
29
|
-
"https://dev.visionai.linkervision.com": [
|
|
30
|
-
"https://dev-045acunea5v1mm3l.us.auth0.com",
|
|
31
|
-
],
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
_AUTH_URL_TO_ISSUERS: dict[str, list[str]] = {
|
|
35
|
-
**{url: [f"{url}{_KEYCLOAK_REALM_PATH}"] for url in _KEYCLOAK_BASE_URLS},
|
|
36
|
-
**_AUTH0_URL_TO_ISSUERS,
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def resolve_allowed_issuers(auth_url: str) -> list[str]:
|
|
41
|
-
"""Return the trusted issuers for the given auth_url.
|
|
42
|
-
|
|
43
|
-
If the URL is in the known table, return its configured issuers.
|
|
44
|
-
Otherwise, assume a Keycloak deployment and derive the issuer from auth_url.
|
|
45
|
-
"""
|
|
46
|
-
normalized = auth_url.rstrip("/")
|
|
47
|
-
if known := _AUTH_URL_TO_ISSUERS.get(normalized):
|
|
48
|
-
return known
|
|
49
|
-
return [f"{normalized}{_KEYCLOAK_REALM_PATH}"]
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import jwt
|
|
2
|
-
import pytest
|
|
3
|
-
|
|
4
|
-
from visionai_sdk_python._base import _BaseClient
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def test_init_stores_attributes() -> None:
|
|
8
|
-
client = _BaseClient(
|
|
9
|
-
auth_url="https://auth.example.com", vlm_url="https://vlm.example.com"
|
|
10
|
-
)
|
|
11
|
-
assert client.auth_url == "https://auth.example.com"
|
|
12
|
-
assert client.vlm_url == "https://vlm.example.com"
|
|
13
|
-
assert client.verify_ssl is True
|
|
14
|
-
assert client.timeout == 10.0
|
|
15
|
-
assert client.max_connections == 100
|
|
16
|
-
assert client.max_keepalive_connections == 20
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
@pytest.mark.parametrize(
|
|
20
|
-
"base_url, path, expected",
|
|
21
|
-
[
|
|
22
|
-
# Standard case
|
|
23
|
-
("https://auth.example.com", "token", "https://auth.example.com/token"),
|
|
24
|
-
# Trailing slash on base
|
|
25
|
-
("https://auth.example.com/", "token", "https://auth.example.com/token"),
|
|
26
|
-
# Leading slash on path
|
|
27
|
-
("https://auth.example.com", "/token", "https://auth.example.com/token"),
|
|
28
|
-
# Both have slashes — should not double-slash
|
|
29
|
-
("https://auth.example.com/", "/token", "https://auth.example.com/token"),
|
|
30
|
-
# Nested path
|
|
31
|
-
("https://vlm.example.com", "v1/infer", "https://vlm.example.com/v1/infer"),
|
|
32
|
-
# Empty path — should not raise, returns base with trailing slash
|
|
33
|
-
("https://auth.example.com", "", "https://auth.example.com/"),
|
|
34
|
-
],
|
|
35
|
-
)
|
|
36
|
-
def test_slash_normalization(base_url: str, path: str, expected: str) -> None:
|
|
37
|
-
assert _BaseClient._build_url(base_url, path) == expected
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def test_auth_header() -> None:
|
|
41
|
-
headers = _BaseClient._build_auth_header(access_token="my-token")
|
|
42
|
-
assert headers["Authorization"] == "Bearer my-token"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
@pytest.mark.parametrize("empty_url", ["", " "])
|
|
46
|
-
def test_init_raises_on_empty_auth_url(empty_url: str) -> None:
|
|
47
|
-
with pytest.raises(ValueError, match="auth_url"):
|
|
48
|
-
_BaseClient(auth_url=empty_url, vlm_url="https://vlm.example.com")
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
@pytest.mark.parametrize("empty_url", ["", " "])
|
|
52
|
-
def test_init_raises_on_empty_vlm_url(empty_url: str) -> None:
|
|
53
|
-
with pytest.raises(ValueError, match="vlm_url"):
|
|
54
|
-
_BaseClient(auth_url="https://auth.example.com", vlm_url=empty_url)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def test_auth_header_raises_on_empty_token() -> None:
|
|
58
|
-
with pytest.raises(ValueError, match="access_token"):
|
|
59
|
-
_BaseClient._build_auth_header(access_token="")
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
_ALLOWED_ISSUERS = [
|
|
63
|
-
"https://offline.visionai.linkervision.com/keycloak/realms/linker-platform",
|
|
64
|
-
"https://data-engine-staging.jp.auth0.com",
|
|
65
|
-
]
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
@pytest.mark.parametrize("issuer", _ALLOWED_ISSUERS)
|
|
69
|
-
def test_validate_issuer_passes_for_allowed(issuer: str) -> None:
|
|
70
|
-
client = _BaseClient(
|
|
71
|
-
auth_url="https://auth.example.com",
|
|
72
|
-
vlm_url="https://vlm.example.com",
|
|
73
|
-
allowed_issuers=_ALLOWED_ISSUERS,
|
|
74
|
-
)
|
|
75
|
-
client._jwt_verifier._validate_issuer(issuer) # should not raise
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
@pytest.mark.parametrize(
|
|
79
|
-
"issuer",
|
|
80
|
-
[
|
|
81
|
-
"https://evil.com/keycloak/realms/linker-platform",
|
|
82
|
-
"https://attacker.auth0.com/",
|
|
83
|
-
"https://offline.visionai.linkervision.com/keycloak/realms/other-realm",
|
|
84
|
-
],
|
|
85
|
-
)
|
|
86
|
-
def test_validate_issuer_raises_for_unknown(issuer: str) -> None:
|
|
87
|
-
client = _BaseClient(
|
|
88
|
-
auth_url="https://auth.example.com",
|
|
89
|
-
vlm_url="https://vlm.example.com",
|
|
90
|
-
allowed_issuers=_ALLOWED_ISSUERS,
|
|
91
|
-
)
|
|
92
|
-
with pytest.raises(jwt.InvalidIssuerError):
|
|
93
|
-
client._jwt_verifier._validate_issuer(issuer)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def test_validate_issuer_raises_for_unknown_when_no_allowed_issuers() -> None:
|
|
97
|
-
client = _BaseClient(
|
|
98
|
-
auth_url="https://auth.example.com",
|
|
99
|
-
vlm_url="https://vlm.example.com",
|
|
100
|
-
)
|
|
101
|
-
with pytest.raises(jwt.InvalidIssuerError):
|
|
102
|
-
client._jwt_verifier._validate_issuer("https://anyone.com/") # should not raise
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/_jwt_verifier.py
RENAMED
|
File without changes
|
|
File without changes
|
{visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/_source_header.py
RENAMED
|
File without changes
|
{visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/async_client.py
RENAMED
|
File without changes
|
{visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/auth/__init__.py
RENAMED
|
File without changes
|
{visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/auth/_mixin.py
RENAMED
|
File without changes
|
|
File without changes
|
{visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/auth/models.py
RENAMED
|
File without changes
|
{visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/auth/resource.py
RENAMED
|
File without changes
|
|
File without changes
|
{visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/endpoints.py
RENAMED
|
File without changes
|
{visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/exceptions.py
RENAMED
|
File without changes
|
{visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/instrumentation.py
RENAMED
|
File without changes
|
|
File without changes
|
{visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/vlm/_mixin.py
RENAMED
|
File without changes
|
|
File without changes
|
{visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/vlm/models.py
RENAMED
|
File without changes
|
{visionai_sdk_python-2.0.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/vlm/resource.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|