visionai-sdk-python 2.2.0.dev2__py3-none-any.whl → 2.2.0.dev3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,178 @@
1
+ """Wire format between platform-relay and platform-edge-agent.
2
+
3
+ The spec is platform-service's ``docs/adr/0001-relay-protocol.md``; this module is
4
+ the one implementation both sides import. Frames are JSON objects told apart by
5
+ ``type``. Receivers ignore unknown fields, and the string-typed ``reason`` /
6
+ ``code`` fields accept values they do not know, so either side can be upgraded
7
+ first.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import re
14
+ from enum import IntEnum, StrEnum
15
+ from typing import Annotated, Literal
16
+
17
+ from pydantic import BaseModel, ConfigDict, Field, TypeAdapter, ValidationError
18
+
19
+ SUBPROTOCOL = "platform-relay.v1"
20
+ # The first frame after the upgrade must be `register`, this soon; else the relay closes 1008.
21
+ REGISTER_DEADLINE_S = 10.0
22
+ HEARTBEAT_INTERVAL_S = 30.0
23
+ # No frame for this long: the relay closes 4001 and forgets the connection.
24
+ STALE_AFTER_S = 90.0
25
+ # Both sides refuse to send a larger frame; the relay refuses a larger chat body (413).
26
+ MAX_FRAME_BYTES = 16 * 1024 * 1024
27
+ MAX_BODY_BYTES = 15 * 1024 * 1024
28
+ # Timeouts nest: the agent waits for VS, the relay waits a little longer for the agent.
29
+ AGENT_TIMEOUT_S = 100
30
+ RELAY_TIMEOUT_S = 110.0
31
+
32
+ # Platform workspace ids: lowercase hyphenated UUIDs.
33
+ UUID_RE = re.compile(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}")
34
+
35
+
36
+ def is_workspace_id(value: str) -> bool:
37
+ return UUID_RE.fullmatch(value) is not None
38
+
39
+
40
+ class CloseCode(IntEnum):
41
+ NORMAL = 1000
42
+ GOING_AWAY = 1001
43
+ POLICY_VIOLATION = 1008
44
+ TOO_BIG = 1009
45
+ REPLACED = 4000
46
+ STALE = 4001
47
+
48
+
49
+ class RejectReason(StrEnum):
50
+ INVALID_ID = "invalid_id"
51
+ CLUSTER_MISMATCH = "cluster_mismatch"
52
+ PLATFORM_UNAVAILABLE = "platform_unavailable"
53
+
54
+
55
+ class ErrorCode(StrEnum):
56
+ INVALID_MESSAGE = "invalid_message"
57
+ UNKNOWN_WORKSPACE = "unknown_workspace"
58
+ PATH_NOT_ALLOWED = "path_not_allowed"
59
+ UPSTREAM_UNREACHABLE = "upstream_unreachable"
60
+ UPSTREAM_TIMEOUT = "upstream_timeout"
61
+
62
+
63
+ class Frame(BaseModel):
64
+ # Receivers ignore unknown fields.
65
+ model_config = ConfigDict(extra="ignore")
66
+
67
+
68
+ class ModelEntry(Frame):
69
+ name: str
70
+ version: str
71
+ # Required by the ADR. A receiver counts None as 0 and logs it: the sender has a bug.
72
+ replicas: int | None = Field(default=None, ge=0)
73
+
74
+
75
+ class WorkspaceEntry(Frame):
76
+ id: str
77
+ models: list[ModelEntry] = Field(default_factory=list)
78
+
79
+
80
+ class Register(Frame):
81
+ type: Literal["register"] = "register"
82
+ agent_id: str = Field(min_length=1)
83
+ cluster_id: str = Field(min_length=1)
84
+ workspaces: list[WorkspaceEntry] = Field(default_factory=list)
85
+
86
+
87
+ class Rejected(Frame):
88
+ id: str
89
+ reason: str # a RejectReason; a plain str so an older agent accepts new reasons
90
+
91
+
92
+ class Registered(Frame):
93
+ type: Literal["registered"] = "registered"
94
+ accepted: list[str] = Field(default_factory=list)
95
+ rejected: list[Rejected] = Field(default_factory=list)
96
+
97
+
98
+ class Heartbeat(Frame):
99
+ type: Literal["heartbeat"] = "heartbeat"
100
+
101
+
102
+ class Request(Frame):
103
+ type: Literal["request"] = "request"
104
+ request_id: str
105
+ workspace_id: str
106
+ method: str
107
+ path: str
108
+ headers: dict[str, str] = Field(default_factory=dict)
109
+ body: dict | list | str | None = None
110
+ timeout_s: int = Field(gt=0)
111
+
112
+
113
+ class Response(Frame):
114
+ type: Literal["response"] = "response"
115
+ request_id: str
116
+ status: int
117
+ headers: dict[str, str] = Field(default_factory=dict)
118
+ body: dict | list | str | None = None
119
+
120
+
121
+ class Error(Frame):
122
+ type: Literal["error"] = "error"
123
+ request_id: str | None = None
124
+ code: str # an ErrorCode; a plain str so an older receiver accepts new codes
125
+ message: str
126
+
127
+
128
+ # What each side receives.
129
+ AgentFrame = Register | Heartbeat | Response | Error # agent -> relay
130
+ RelayFrame = Registered | Request | Error # relay -> agent
131
+ _agent_frame = TypeAdapter(Annotated[AgentFrame, Field(discriminator="type")])
132
+ _relay_frame = TypeAdapter(Annotated[RelayFrame, Field(discriminator="type")])
133
+
134
+
135
+ class InvalidFrame(ValueError):
136
+ """A frame the receiver cannot read; the text is safe to send back in an ``error``."""
137
+
138
+ def __init__(self, message: str, request_id: str | None = None) -> None:
139
+ super().__init__(message)
140
+ self.request_id = request_id
141
+
142
+
143
+ def parse_agent_frame(text: str) -> AgentFrame:
144
+ """A frame the relay received from an agent."""
145
+ return _parse(text, _agent_frame)
146
+
147
+
148
+ def parse_relay_frame(text: str) -> RelayFrame:
149
+ """A frame the agent received from the relay."""
150
+ return _parse(text, _relay_frame)
151
+
152
+
153
+ def _parse(text: str, adapter: TypeAdapter):
154
+ try:
155
+ data = json.loads(text)
156
+ except ValueError:
157
+ raise InvalidFrame("frame is not JSON") from None
158
+ if not isinstance(data, dict):
159
+ raise InvalidFrame("frame is not a JSON object")
160
+ kind = data.get("type")
161
+ if not isinstance(kind, str):
162
+ raise InvalidFrame("frame has no type")
163
+ request_id = data.get("request_id")
164
+ request_id = request_id if isinstance(request_id, str) else None
165
+ try:
166
+ return adapter.validate_python(data)
167
+ except ValidationError as exc:
168
+ if any(err["type"] == "union_tag_invalid" for err in exc.errors()):
169
+ raise InvalidFrame(f"unknown type {kind!r}", request_id) from None
170
+ fields = ", ".join(
171
+ ".".join(str(part) for part in err["loc"][1:]) or "body"
172
+ for err in exc.errors()
173
+ )
174
+ raise InvalidFrame(f"{kind} fails validation: {fields}", request_id) from None
175
+
176
+
177
+ def dump_frame(frame: BaseModel) -> str:
178
+ return frame.model_dump_json(exclude_none=True)
@@ -51,7 +51,9 @@ class AsyncVLMResource(VLMMixin):
51
51
  # I/O operation (async); the client adds the token and retries once on 401
52
52
  response = await self._sdk_client.request(
53
53
  "POST",
54
- self._sdk_client._build_url(self._sdk_client._vlm_base(), VLMEndpoint.CHAT),
54
+ self._sdk_client._build_url(
55
+ self._sdk_client._require_vlm_url(), VLMEndpoint.CHAT
56
+ ),
55
57
  json=body,
56
58
  )
57
59
 
@@ -82,7 +84,7 @@ class AsyncVLMResource(VLMMixin):
82
84
  response = await self._sdk_client.request(
83
85
  "GET",
84
86
  self._sdk_client._build_url(
85
- self._sdk_client._vlm_base(), f"{VLMEndpoint.CHAT}/{result_id}"
87
+ self._sdk_client._require_vlm_url(), f"{VLMEndpoint.CHAT}/{result_id}"
86
88
  ),
87
89
  )
88
90
 
@@ -51,7 +51,9 @@ class VLMResource(VLMMixin):
51
51
  # I/O operation (sync); the client adds the token and retries once on 401
52
52
  response = self._sdk_client.request(
53
53
  "POST",
54
- self._sdk_client._build_url(self._sdk_client._vlm_base(), VLMEndpoint.CHAT),
54
+ self._sdk_client._build_url(
55
+ self._sdk_client._require_vlm_url(), VLMEndpoint.CHAT
56
+ ),
55
57
  json=body,
56
58
  )
57
59
 
@@ -80,7 +82,7 @@ class VLMResource(VLMMixin):
80
82
  response = self._sdk_client.request(
81
83
  "GET",
82
84
  self._sdk_client._build_url(
83
- self._sdk_client._vlm_base(), f"{VLMEndpoint.CHAT}/{result_id}"
85
+ self._sdk_client._require_vlm_url(), f"{VLMEndpoint.CHAT}/{result_id}"
84
86
  ),
85
87
  )
86
88
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: visionai-sdk-python
3
- Version: 2.2.0.dev2
3
+ Version: 2.2.0.dev3
4
4
  Summary: VisionAI SDK for Python
5
5
  Author-email: Tony Yang <tonyyang@linkervision.com>
6
6
  Requires-Python: >=3.11
@@ -135,8 +135,8 @@ token = client.auth.login("user@example.com", "password")
135
135
  ### OAuth Client Credentials
136
136
 
137
137
  A `ClientCredentials` (or `AsyncClientCredentials`) holds the token for one Platform
138
- client: it is minted on first use, cached until five minutes before it expires, and
139
- one instance can serve any number of clients and your own requests.
138
+ client: it is minted on first use, cached until shortly before it expires, and one
139
+ instance can serve any number of clients and your own requests.
140
140
 
141
141
  ```python
142
142
  from visionai_sdk_python import Client, ClientCredentials
@@ -152,14 +152,26 @@ token = cred.get_token().token # for a WebSocket upgrade or another lib
152
152
  nodes = client.request("GET", "https://vs.example.com/mgmt/nodes").json()
153
153
  ```
154
154
 
155
- Platform mints at most 50 tokens per client per month, so the credential protects the
156
- quota: concurrent callers share one mint; a 401 on a cached token
157
- (`cred.invalidate(token)`) buys exactly one replacement, and a replacement that is
158
- rejected too is kept until its own refresh; after a 429 no mint is tried for an hour
159
- (`RateLimitedError`).
155
+ A mint is a Platform round trip and, on sites that cap client tokens, a scarce one:
156
+ concurrent callers share one mint, a refresh that fails keeps the current token while
157
+ Platform still accepts it, a 401 on a token minted more than 30 minutes ago buys a
158
+ replacement (`cred.invalidate(token)`; a younger one is kept, so a persistent 401
159
+ mints at most twice an hour), and after a 429 no mint is tried for an hour
160
+ (`RateLimitedError`). `get_token(force_refresh=True)` mints on demand.
160
161
 
161
162
  `client.auth.get_access_token(client_id, client_secret)` still works and now builds a
162
- `ClientCredentials` underneath. `vlm_url` is optional for clients that only authenticate.
163
+ `ClientCredentials` underneath; it returns the cached token unless `force_refresh=True`.
164
+ A later `login()` or `set_token()` replaces the credential: the last explicit
165
+ authentication wins. `vlm_url` is optional for clients that only authenticate.
166
+
167
+ ## Relay protocol (internal)
168
+
169
+ `visionai_sdk_python.relay` holds the wire format between platform-relay and
170
+ platform-edge-agent: the frame models, the constants and `parse_agent_frame()` /
171
+ `parse_relay_frame()` / `dump_frame()`. The spec is platform-service's
172
+ `docs/adr/0001-relay-protocol.md`; both sides import this module instead of keeping a
173
+ copy, and the ADR's JSON examples are this package's tests. It is internal to the
174
+ platform and not a public API.
163
175
 
164
176
  ## VLM Inference
165
177
 
@@ -0,0 +1,30 @@
1
+ visionai_sdk_python/__init__.py,sha256=EITFK0aQPAQN3BSnwTfrPAt1iu50pmuhXl0-jBSbiD4,774
2
+ visionai_sdk_python/_base.py,sha256=q8Ybwk9uEdaAFh85IRCHvsSpXEvYFosFYGhJAeQNIr4,9390
3
+ visionai_sdk_python/_jwt_verifier.py,sha256=h4CAK1iiDAJ-oKnXrLbCJw5rWcsVe94QNY12ne1Wzto,8442
4
+ visionai_sdk_python/_request_attribution.py,sha256=zQk_bSLsu1RTp1Fe3MGUUX8YRzq3iqfBTmhhjM7BuRU,1907
5
+ visionai_sdk_python/_source_header.py,sha256=DWiR-TGclET4UrqmAEO7bYPtf9XuLSE4FGQjZIgVWiY,5133
6
+ visionai_sdk_python/async_client.py,sha256=JHx7Kz3l84PdyHvgGPhpTKprRdSabXxjqazqUVj--rY,5542
7
+ visionai_sdk_python/client.py,sha256=0B3ut9iXsiZxveOIP8SEbTZwv3D0mDZ62D0ycIFOBC8,5352
8
+ visionai_sdk_python/constants.py,sha256=KWhoxZdlwqXwhUJDqERStq2mrms-MA8aRDqBwbIvNO0,1471
9
+ visionai_sdk_python/endpoints.py,sha256=CC-AoxXzg39sMAhqx1CTS3gRWKx_H7VOqBANRLVotoU,183
10
+ visionai_sdk_python/exceptions.py,sha256=BUpcoTgILc3z68lIQGyTYs7PCvu_BTSi228YMTx7ZWs,3020
11
+ visionai_sdk_python/instrumentation.py,sha256=tTTL5Op0fZULpmoOKW8dw8-2ReOQUEpMvHrf-R5C_yg,24512
12
+ visionai_sdk_python/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ visionai_sdk_python/auth/__init__.py,sha256=sWtanqUos5Q8P80IvjoyvOzXv6gYNkflop9qwDwsZk8,400
14
+ visionai_sdk_python/auth/_mixin.py,sha256=EDDhEB9jHc_7wEoLx_MprhToTVLekL5CyJuLTK8GhNM,5022
15
+ visionai_sdk_python/auth/async_resource.py,sha256=V6O51i7KyYSBkiQeKAbmw-EIKMVZIozO1gDexTX6tXw,5535
16
+ visionai_sdk_python/auth/credentials.py,sha256=nsZkavY8pDT39gZxFaKUcl-X_JfKKkc3Nkv9fSFFP4Y,10892
17
+ visionai_sdk_python/auth/models.py,sha256=u6MDFYaWMyf08XCrGZzwPHv43d6AElH9XO2QSjRsb3Q,449
18
+ visionai_sdk_python/auth/resource.py,sha256=3PgcxH8N6unKTs2jSQhMeJT_lg6bTce0hz0bzuID12U,5446
19
+ visionai_sdk_python/relay/__init__.py,sha256=N4DuhutN8P4EWZRFIaWTqvylNsLkzRSkHMRRF2pknFk,1296
20
+ visionai_sdk_python/relay/protocol.py,sha256=Hy7J4kgDxaKy0WOrtkmqD5xUFXgKAVXwTWX4NQ9yQHE,5561
21
+ visionai_sdk_python/vlm/__init__.py,sha256=EDd7ZG4k7inn8qqkyIhO3xI_4MPMWJT5Npsient-tTc,571
22
+ visionai_sdk_python/vlm/_mixin.py,sha256=TkqezNpW8QEe9pujsvx1APcylYQYr_PvHruEz4Le3JQ,1718
23
+ visionai_sdk_python/vlm/async_resource.py,sha256=meMUfBDa0g1hk7vFZq2fDuIQ1-QUpU0BUJtbvuG8UEY,3461
24
+ visionai_sdk_python/vlm/models.py,sha256=yDFjo4LfAxqXYBnl6ZW7YUJQoKKRkQWtqBJi2ruI6Jw,984
25
+ visionai_sdk_python/vlm/preprocess.py,sha256=ZpJJa13h8_YKwh_P7Pn8WIXV0x-r2xJi-ETgCFsGw-Y,7892
26
+ visionai_sdk_python/vlm/resource.py,sha256=4eMQ4ACcgd7nUfdRyADWmPd8yZ--uqQC4zwzBft68lg,3386
27
+ visionai_sdk_python-2.2.0.dev3.dist-info/METADATA,sha256=yaMgyrv9_edbxVHgdaOFTCc2EhV1YFjjZDaxa2D3Lt4,30403
28
+ visionai_sdk_python-2.2.0.dev3.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
29
+ visionai_sdk_python-2.2.0.dev3.dist-info/top_level.txt,sha256=79a5MCSLLv6Mbfun-8RS-G6O927hahBwDqJjeuWLdvM,20
30
+ visionai_sdk_python-2.2.0.dev3.dist-info/RECORD,,
@@ -1,28 +0,0 @@
1
- visionai_sdk_python/__init__.py,sha256=EITFK0aQPAQN3BSnwTfrPAt1iu50pmuhXl0-jBSbiD4,774
2
- visionai_sdk_python/_base.py,sha256=fnvnes3i5K5CObwIROs3NM6X-PHTLS-IyK6cK9hILeA,10397
3
- visionai_sdk_python/_jwt_verifier.py,sha256=h4CAK1iiDAJ-oKnXrLbCJw5rWcsVe94QNY12ne1Wzto,8442
4
- visionai_sdk_python/_request_attribution.py,sha256=zQk_bSLsu1RTp1Fe3MGUUX8YRzq3iqfBTmhhjM7BuRU,1907
5
- visionai_sdk_python/_source_header.py,sha256=DWiR-TGclET4UrqmAEO7bYPtf9XuLSE4FGQjZIgVWiY,5133
6
- visionai_sdk_python/async_client.py,sha256=b8hMe9e3eth8B5tOIhP4Wo7ja7JvxO7fe8xQk1lSDjQ,5837
7
- visionai_sdk_python/client.py,sha256=urkFrMtw7qrkyfzekhF5vsYZt5LlgeJXml-jI8uWSqI,5599
8
- visionai_sdk_python/constants.py,sha256=KWhoxZdlwqXwhUJDqERStq2mrms-MA8aRDqBwbIvNO0,1471
9
- visionai_sdk_python/endpoints.py,sha256=CC-AoxXzg39sMAhqx1CTS3gRWKx_H7VOqBANRLVotoU,183
10
- visionai_sdk_python/exceptions.py,sha256=UExz_lx7do154RyFobcYIaD_hDD-AExvMiOSDkKxe-0,1259
11
- visionai_sdk_python/instrumentation.py,sha256=tTTL5Op0fZULpmoOKW8dw8-2ReOQUEpMvHrf-R5C_yg,24512
12
- visionai_sdk_python/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
- visionai_sdk_python/auth/__init__.py,sha256=sWtanqUos5Q8P80IvjoyvOzXv6gYNkflop9qwDwsZk8,400
14
- visionai_sdk_python/auth/_mixin.py,sha256=qxn9wQxOtigcvbFGSmjv7Fu7-HCsIJVs1oqf-wfIN4w,5034
15
- visionai_sdk_python/auth/async_resource.py,sha256=FnL9c4K7VFsXXMkY2mMPESYMS5fh08WvsqlY9tuvPSI,5341
16
- visionai_sdk_python/auth/credentials.py,sha256=STuV_ygrvAZbqY6ElE_q612w65zRHAG8oXKBm8X0U1M,8471
17
- visionai_sdk_python/auth/models.py,sha256=u6MDFYaWMyf08XCrGZzwPHv43d6AElH9XO2QSjRsb3Q,449
18
- visionai_sdk_python/auth/resource.py,sha256=vFBf_27h2u6R1w3aXm5cDEpvS21RyiwgtDm0olZFpec,5238
19
- visionai_sdk_python/vlm/__init__.py,sha256=EDd7ZG4k7inn8qqkyIhO3xI_4MPMWJT5Npsient-tTc,571
20
- visionai_sdk_python/vlm/_mixin.py,sha256=TkqezNpW8QEe9pujsvx1APcylYQYr_PvHruEz4Le3JQ,1718
21
- visionai_sdk_python/vlm/async_resource.py,sha256=b878VPxVfO4xpjG-JH2XcimqywMFtEw-cyEfeSLVGRo,3417
22
- visionai_sdk_python/vlm/models.py,sha256=yDFjo4LfAxqXYBnl6ZW7YUJQoKKRkQWtqBJi2ruI6Jw,984
23
- visionai_sdk_python/vlm/preprocess.py,sha256=ZpJJa13h8_YKwh_P7Pn8WIXV0x-r2xJi-ETgCFsGw-Y,7892
24
- visionai_sdk_python/vlm/resource.py,sha256=qx03gOxVamtoDrLNadp1J9EtVuP95SntOuilFog_SSo,3342
25
- visionai_sdk_python-2.2.0.dev2.dist-info/METADATA,sha256=DWZr4KOvRRLb1IXTQtpvwSBxr4ozPzJCAm9xAlTe53Q,29654
26
- visionai_sdk_python-2.2.0.dev2.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
27
- visionai_sdk_python-2.2.0.dev2.dist-info/top_level.txt,sha256=79a5MCSLLv6Mbfun-8RS-G6O927hahBwDqJjeuWLdvM,20
28
- visionai_sdk_python-2.2.0.dev2.dist-info/RECORD,,