visionai-sdk-python 2.1.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.1.0 → visionai_sdk_python-2.1.1}/PKG-INFO +20 -1
- {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/README.md +19 -0
- {visionai_sdk_python-2.1.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.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python.egg-info/PKG-INFO +20 -1
- 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.0/src/visionai_sdk_python/constants.py +0 -49
- visionai_sdk_python-2.1.0/src/visionai_sdk_python.egg-info/scm_version.json +0 -8
- visionai_sdk_python-2.1.0/tests/test__base.py +0 -102
- {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/.github/dependabot.yml +0 -0
- {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/.github/workflows/ci.yml +0 -0
- {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/.github/workflows/publish.yml +0 -0
- {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/.gitignore +0 -0
- {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/pyproject.toml +0 -0
- {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/setup.cfg +0 -0
- {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/__init__.py +0 -0
- {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/_jwt_verifier.py +0 -0
- {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/_request_attribution.py +0 -0
- {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/_source_header.py +0 -0
- {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/async_client.py +0 -0
- {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/auth/__init__.py +0 -0
- {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/auth/_mixin.py +0 -0
- {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/auth/async_resource.py +0 -0
- {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/auth/models.py +0 -0
- {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/auth/resource.py +0 -0
- {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/client.py +0 -0
- {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/endpoints.py +0 -0
- {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/exceptions.py +0 -0
- {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/instrumentation.py +0 -0
- {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/py.typed +0 -0
- {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/vlm/__init__.py +0 -0
- {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/vlm/_mixin.py +0 -0
- {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/vlm/async_resource.py +0 -0
- {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/vlm/models.py +0 -0
- {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/vlm/preprocess.py +0 -0
- {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/vlm/resource.py +0 -0
- {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python.egg-info/SOURCES.txt +0 -0
- {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python.egg-info/dependency_links.txt +0 -0
- {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python.egg-info/requires.txt +0 -0
- {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python.egg-info/scm_file_list.json +0 -0
- {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python.egg-info/top_level.txt +0 -0
- {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/tests/__init__.py +0 -0
- {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/tests/constants.py +0 -0
- {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/tests/test_async_client.py +0 -0
- {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/tests/test_client.py +0 -0
- {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/tests/test_instrumentation.py +0 -0
- {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/tests/test_vlm_preprocess.py +0 -0
- {visionai_sdk_python-2.1.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.1.
|
|
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
|
|
@@ -250,6 +250,25 @@ client = Client(
|
|
|
250
250
|
)
|
|
251
251
|
```
|
|
252
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
|
+
|
|
253
272
|
## Error Handling
|
|
254
273
|
|
|
255
274
|
```python
|
|
@@ -232,6 +232,25 @@ client = Client(
|
|
|
232
232
|
)
|
|
233
233
|
```
|
|
234
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
|
+
|
|
235
254
|
## Error Handling
|
|
236
255
|
|
|
237
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.1.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.1.
|
|
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
|
|
@@ -250,6 +250,25 @@ client = Client(
|
|
|
250
250
|
)
|
|
251
251
|
```
|
|
252
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
|
+
|
|
253
272
|
## Error Handling
|
|
254
273
|
|
|
255
274
|
```python
|
|
@@ -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
|
|
@@ -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.1.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.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/_source_header.py
RENAMED
|
File without changes
|
{visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/async_client.py
RENAMED
|
File without changes
|
{visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/auth/__init__.py
RENAMED
|
File without changes
|
{visionai_sdk_python-2.1.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.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/auth/models.py
RENAMED
|
File without changes
|
{visionai_sdk_python-2.1.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.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/endpoints.py
RENAMED
|
File without changes
|
{visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/exceptions.py
RENAMED
|
File without changes
|
{visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/instrumentation.py
RENAMED
|
File without changes
|
|
File without changes
|
{visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/vlm/__init__.py
RENAMED
|
File without changes
|
{visionai_sdk_python-2.1.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.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/vlm/models.py
RENAMED
|
File without changes
|
{visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/vlm/preprocess.py
RENAMED
|
File without changes
|
{visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/vlm/resource.py
RENAMED
|
File without changes
|
{visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python.egg-info/SOURCES.txt
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
|
|
File without changes
|
|
File without changes
|