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.
Files changed (49) hide show
  1. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/PKG-INFO +20 -1
  2. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/README.md +19 -0
  3. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/_base.py +1 -1
  4. visionai_sdk_python-2.1.1/src/visionai_sdk_python/constants.py +36 -0
  5. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python.egg-info/PKG-INFO +20 -1
  6. visionai_sdk_python-2.1.1/src/visionai_sdk_python.egg-info/scm_version.json +8 -0
  7. visionai_sdk_python-2.1.1/tests/test__base.py +198 -0
  8. visionai_sdk_python-2.1.0/src/visionai_sdk_python/constants.py +0 -49
  9. visionai_sdk_python-2.1.0/src/visionai_sdk_python.egg-info/scm_version.json +0 -8
  10. visionai_sdk_python-2.1.0/tests/test__base.py +0 -102
  11. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/.github/dependabot.yml +0 -0
  12. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/.github/workflows/ci.yml +0 -0
  13. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/.github/workflows/publish.yml +0 -0
  14. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/.gitignore +0 -0
  15. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/pyproject.toml +0 -0
  16. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/setup.cfg +0 -0
  17. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/__init__.py +0 -0
  18. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/_jwt_verifier.py +0 -0
  19. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/_request_attribution.py +0 -0
  20. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/_source_header.py +0 -0
  21. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/async_client.py +0 -0
  22. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/auth/__init__.py +0 -0
  23. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/auth/_mixin.py +0 -0
  24. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/auth/async_resource.py +0 -0
  25. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/auth/models.py +0 -0
  26. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/auth/resource.py +0 -0
  27. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/client.py +0 -0
  28. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/endpoints.py +0 -0
  29. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/exceptions.py +0 -0
  30. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/instrumentation.py +0 -0
  31. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/py.typed +0 -0
  32. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/vlm/__init__.py +0 -0
  33. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/vlm/_mixin.py +0 -0
  34. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/vlm/async_resource.py +0 -0
  35. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/vlm/models.py +0 -0
  36. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/vlm/preprocess.py +0 -0
  37. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python/vlm/resource.py +0 -0
  38. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python.egg-info/SOURCES.txt +0 -0
  39. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python.egg-info/dependency_links.txt +0 -0
  40. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python.egg-info/requires.txt +0 -0
  41. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python.egg-info/scm_file_list.json +0 -0
  42. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/src/visionai_sdk_python.egg-info/top_level.txt +0 -0
  43. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/tests/__init__.py +0 -0
  44. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/tests/constants.py +0 -0
  45. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/tests/test_async_client.py +0 -0
  46. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/tests/test_client.py +0 -0
  47. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/tests/test_instrumentation.py +0 -0
  48. {visionai_sdk_python-2.1.0 → visionai_sdk_python-2.1.1}/tests/test_vlm_preprocess.py +0 -0
  49. {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.0
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
- issuer validation is skipped.
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
+ ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: visionai-sdk-python
3
- Version: 2.1.0
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,8 @@
1
+ {
2
+ "tag": "2.1.1",
3
+ "distance": 0,
4
+ "node": "gfb3ad463150fa3d6530e8390fee3865b9398d8c0",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-09-16"
8
+ }
@@ -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,8 +0,0 @@
1
- {
2
- "tag": "2.1.0",
3
- "distance": 0,
4
- "node": "g72bde18e5ad7c772f12e7d313f7396f24342ef92",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-09-14"
8
- }
@@ -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