meshagent-api 0.24.6__py3-none-any.whl → 0.25.0__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.
- meshagent/api/client.py +37 -9
- meshagent/api/helpers.py +7 -2
- meshagent/api/http.py +14 -0
- meshagent/api/participant_token.py +7 -0
- meshagent/api/port_forward.py +2 -1
- meshagent/api/room_server_client.py +359 -308
- meshagent/api/specs/service.py +126 -27
- meshagent/api/sql.py +223 -0
- meshagent/api/sql_test.py +38 -0
- meshagent/api/urls.py +1 -1
- meshagent/api/version.py +1 -1
- meshagent/api/websocket_protocol.py +18 -4
- {meshagent_api-0.24.6.dist-info → meshagent_api-0.25.0.dist-info}/METADATA +3 -2
- {meshagent_api-0.24.6.dist-info → meshagent_api-0.25.0.dist-info}/RECORD +17 -14
- {meshagent_api-0.24.6.dist-info → meshagent_api-0.25.0.dist-info}/WHEEL +0 -0
- {meshagent_api-0.24.6.dist-info → meshagent_api-0.25.0.dist-info}/licenses/LICENSE +0 -0
- {meshagent_api-0.24.6.dist-info → meshagent_api-0.25.0.dist-info}/top_level.txt +0 -0
meshagent/api/specs/service.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from pydantic import BaseModel, PositiveInt, ConfigDict, model_validator
|
|
1
|
+
from pydantic import BaseModel, PositiveInt, ConfigDict, Field, model_validator
|
|
2
2
|
from typing import Optional, Literal
|
|
3
3
|
from meshagent.api.participant_token import ApiScope
|
|
4
4
|
from meshagent.api.oauth import OAuthClientConfig
|
|
@@ -9,9 +9,18 @@ from yaml.loader import SafeLoader
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class TokenValue(BaseModel):
|
|
12
|
-
identity: str
|
|
13
|
-
api: Optional[ApiScope] =
|
|
14
|
-
|
|
12
|
+
identity: str = Field(..., description="the name to use in the participant token")
|
|
13
|
+
api: Optional[ApiScope] = Field(
|
|
14
|
+
None,
|
|
15
|
+
description=(
|
|
16
|
+
"the api permissions that should be granted to this token, set to null "
|
|
17
|
+
"or omit to use default permissions"
|
|
18
|
+
),
|
|
19
|
+
)
|
|
20
|
+
role: Optional[str] = Field(
|
|
21
|
+
None,
|
|
22
|
+
description="a role to use in the participant token, such as user, agent, or tool",
|
|
23
|
+
)
|
|
15
24
|
|
|
16
25
|
|
|
17
26
|
class EnvironmentVariable(BaseModel):
|
|
@@ -22,28 +31,51 @@ class EnvironmentVariable(BaseModel):
|
|
|
22
31
|
|
|
23
32
|
|
|
24
33
|
class RoomStorageMountSpec(BaseModel):
|
|
34
|
+
"""mounts room storage at the specified path using a FUSE mount"""
|
|
35
|
+
|
|
25
36
|
model_config = ConfigDict(extra="forbid")
|
|
26
|
-
path: str
|
|
27
|
-
|
|
37
|
+
path: str = Field(
|
|
38
|
+
...,
|
|
39
|
+
description="the path within the container for the room's storage to be mounted to",
|
|
40
|
+
)
|
|
41
|
+
subpath: Optional[str] = Field(
|
|
42
|
+
None, description="mount only a portion of the rooms storage"
|
|
43
|
+
)
|
|
28
44
|
read_only: bool = False
|
|
29
45
|
|
|
30
46
|
|
|
31
47
|
class ProjectStorageMountSpec(BaseModel):
|
|
48
|
+
"""mounts shared project storage at the specified path using a FUSE mount"""
|
|
49
|
+
|
|
32
50
|
model_config = ConfigDict(extra="forbid")
|
|
33
|
-
path: str
|
|
34
|
-
|
|
51
|
+
path: str = Field(
|
|
52
|
+
...,
|
|
53
|
+
description="the path within the container for the project storage to be mounted to",
|
|
54
|
+
)
|
|
55
|
+
subpath: Optional[str] = Field(
|
|
56
|
+
None, description="mount only a portion of the project's storage"
|
|
57
|
+
)
|
|
35
58
|
read_only: bool = True
|
|
36
59
|
|
|
37
60
|
|
|
38
61
|
class ImageStorageMountSpec(BaseModel):
|
|
62
|
+
"""mounts a the content of a Docker / OCI image at the specified path within the container"""
|
|
63
|
+
|
|
39
64
|
model_config = ConfigDict(extra="forbid")
|
|
40
|
-
image: str
|
|
41
|
-
path: str
|
|
42
|
-
|
|
65
|
+
image: str = Field(..., description="the tag of an image that will be mounted")
|
|
66
|
+
path: str = Field(
|
|
67
|
+
...,
|
|
68
|
+
description="the path within the container for the image volume to be mounted to",
|
|
69
|
+
)
|
|
70
|
+
subpath: Optional[str] = Field(
|
|
71
|
+
None, description="mount only a portion of the image volume"
|
|
72
|
+
)
|
|
43
73
|
read_only: bool = True
|
|
44
74
|
|
|
45
75
|
|
|
46
76
|
class FileStorageMountSpec(BaseModel):
|
|
77
|
+
"""mounts a static file into the container at the specified path"""
|
|
78
|
+
|
|
47
79
|
model_config = ConfigDict(extra="forbid")
|
|
48
80
|
path: str
|
|
49
81
|
text: str
|
|
@@ -113,13 +145,24 @@ class ServiceMetadata(BaseModel):
|
|
|
113
145
|
class ContainerSpec(BaseModel):
|
|
114
146
|
model_config = ConfigDict(extra="forbid")
|
|
115
147
|
image: str
|
|
148
|
+
|
|
116
149
|
command: Optional[str] = None
|
|
117
150
|
environment: Optional[list[EnvironmentVariable]] = None
|
|
118
|
-
secrets: Optional[list[str]] =
|
|
119
|
-
|
|
120
|
-
|
|
151
|
+
secrets: Optional[list[str]] = Field(
|
|
152
|
+
None,
|
|
153
|
+
description="ids of secrets that contains environment variables for this service to use",
|
|
154
|
+
)
|
|
155
|
+
pull_secret: Optional[str] = Field(
|
|
156
|
+
None,
|
|
157
|
+
description=(
|
|
158
|
+
"the id of a pull secret, can be used to pull private container images"
|
|
159
|
+
),
|
|
160
|
+
)
|
|
161
|
+
storage: Optional[ContainerMountSpec] = Field(
|
|
162
|
+
None, description="storage mounts that should be provided to this container"
|
|
163
|
+
)
|
|
121
164
|
api_key: Optional[ServiceApiKeySpec] = None
|
|
122
|
-
on_demand: Optional[bool] = None
|
|
165
|
+
on_demand: Optional[bool] = Field(None, description="an on demand service")
|
|
123
166
|
writable_root_fs: Optional[bool] = None
|
|
124
167
|
|
|
125
168
|
|
|
@@ -133,11 +176,26 @@ class ServiceSpec(BaseModel):
|
|
|
133
176
|
version: Literal["v1"]
|
|
134
177
|
kind: Literal["Service"]
|
|
135
178
|
id: Optional[str] = None
|
|
136
|
-
metadata: ServiceMetadata
|
|
137
|
-
agents: Optional[list[AgentSpec]] =
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
179
|
+
metadata: ServiceMetadata = Field(..., description="service metadata")
|
|
180
|
+
agents: Optional[list[AgentSpec]] = Field(
|
|
181
|
+
None, description="a list of agents that will be exposed by this service"
|
|
182
|
+
)
|
|
183
|
+
ports: Optional[list["PortSpec"]] = Field(
|
|
184
|
+
default_factory=list,
|
|
185
|
+
description="a list of ports that are exposed by this service",
|
|
186
|
+
)
|
|
187
|
+
container: Optional[ContainerSpec] = Field(
|
|
188
|
+
None,
|
|
189
|
+
description=(
|
|
190
|
+
"container based services run agents in sandboxed containers inside the room"
|
|
191
|
+
),
|
|
192
|
+
)
|
|
193
|
+
external: Optional[ExternalServiceSpec] = Field(
|
|
194
|
+
None,
|
|
195
|
+
description=(
|
|
196
|
+
"external services allow discovery of externally hosted agents, mcp servers, and tools"
|
|
197
|
+
),
|
|
198
|
+
)
|
|
141
199
|
|
|
142
200
|
@model_validator(mode="after")
|
|
143
201
|
def require_one_of(cls, m):
|
|
@@ -152,8 +210,17 @@ class ServiceSpec(BaseModel):
|
|
|
152
210
|
|
|
153
211
|
class MeshagentEndpointSpec(BaseModel):
|
|
154
212
|
model_config = ConfigDict(extra="forbid")
|
|
155
|
-
|
|
156
|
-
|
|
213
|
+
|
|
214
|
+
identity: str = Field(
|
|
215
|
+
...,
|
|
216
|
+
description="the name to use for the participant token provided to this endpoint",
|
|
217
|
+
)
|
|
218
|
+
api: Optional[ApiScope] = Field(
|
|
219
|
+
None,
|
|
220
|
+
description=(
|
|
221
|
+
"customize the permissions available to this endpoint, omit to use default agent permissions"
|
|
222
|
+
),
|
|
223
|
+
)
|
|
157
224
|
|
|
158
225
|
|
|
159
226
|
class AllowedMcpToolFilter(BaseModel):
|
|
@@ -175,8 +242,16 @@ class MCPEndpointSpec(BaseModel):
|
|
|
175
242
|
|
|
176
243
|
class EndpointSpec(BaseModel):
|
|
177
244
|
model_config = ConfigDict(extra="forbid")
|
|
178
|
-
path: str
|
|
179
|
-
|
|
245
|
+
path: str = Field(
|
|
246
|
+
...,
|
|
247
|
+
description="the path that should receive a webhook call when the service starts",
|
|
248
|
+
)
|
|
249
|
+
meshagent: Optional[MeshagentEndpointSpec] = Field(
|
|
250
|
+
None,
|
|
251
|
+
description=(
|
|
252
|
+
"meshagent endpoints will be automatically notified when the service starts in order to call an agent or tool into the room"
|
|
253
|
+
),
|
|
254
|
+
)
|
|
180
255
|
mcp: Optional[MCPEndpointSpec] = None
|
|
181
256
|
|
|
182
257
|
|
|
@@ -184,9 +259,33 @@ class PortSpec(BaseModel):
|
|
|
184
259
|
model_config = ConfigDict(extra="forbid")
|
|
185
260
|
num: Literal["*"] | PositiveInt = "*"
|
|
186
261
|
type: Optional[Literal["http", "tcp"]] = "http"
|
|
187
|
-
endpoints: list[EndpointSpec] =
|
|
188
|
-
|
|
189
|
-
|
|
262
|
+
endpoints: list[EndpointSpec] = Field(
|
|
263
|
+
default_factory=list, description="a list of endpoints exposed under this port"
|
|
264
|
+
)
|
|
265
|
+
liveness: Optional[str] = Field(
|
|
266
|
+
None,
|
|
267
|
+
description=(
|
|
268
|
+
"a path that will accept a HTTP request and should return 200 when the port is live"
|
|
269
|
+
),
|
|
270
|
+
)
|
|
271
|
+
host_port: Optional[PositiveInt] = Field(
|
|
272
|
+
None,
|
|
273
|
+
description=(
|
|
274
|
+
"expose a host port for this service, allows traffic to be tunneled to the container with port forwarding"
|
|
275
|
+
),
|
|
276
|
+
)
|
|
277
|
+
published: Optional[bool] = Field(
|
|
278
|
+
None,
|
|
279
|
+
description=(
|
|
280
|
+
"allow traffic to be routed directly to this container from the internet, useful for implementing patterns such as webhooks"
|
|
281
|
+
),
|
|
282
|
+
)
|
|
283
|
+
public: Optional[bool] = Field(
|
|
284
|
+
None,
|
|
285
|
+
description=(
|
|
286
|
+
"if a port is not public it will require a participant token to be passed as a Bearer token in the Authorization header"
|
|
287
|
+
),
|
|
288
|
+
)
|
|
190
289
|
|
|
191
290
|
|
|
192
291
|
class ServiceTemplateVariable(BaseModel):
|
meshagent/api/sql.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Iterable
|
|
5
|
+
|
|
6
|
+
from meshagent.api.room_server_client import (
|
|
7
|
+
BinaryDataType,
|
|
8
|
+
BoolDataType,
|
|
9
|
+
DataTypeUnion,
|
|
10
|
+
DateDataType,
|
|
11
|
+
FloatDataType,
|
|
12
|
+
IntDataType,
|
|
13
|
+
TextDataType,
|
|
14
|
+
TimestampDataType,
|
|
15
|
+
VectorDataType,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
TABLE_SCHEMA_GRAMMAR = """
|
|
19
|
+
TABLE_SCHEMA := COLUMN_DEF ("," COLUMN_DEF)*
|
|
20
|
+
COLUMN_DEF := IDENTIFIER TYPE_SPEC NULLABILITY?
|
|
21
|
+
TYPE_SPEC := SIMPLE_TYPE | VECTOR_TYPE
|
|
22
|
+
SIMPLE_TYPE := "int" | "bool" | "date" | "timestamp" | "float" | "text" | "binary"
|
|
23
|
+
VECTOR_TYPE := "vector" "(" INT ("," TYPE_SPEC)? ")"
|
|
24
|
+
NULLABILITY := "null" | "not" "null"
|
|
25
|
+
IDENTIFIER := /[A-Za-z_][A-Za-z0-9_]*/
|
|
26
|
+
INT := /[0-9]+/
|
|
27
|
+
""".strip()
|
|
28
|
+
|
|
29
|
+
ALLOWED_DATA_TYPES = (
|
|
30
|
+
"int",
|
|
31
|
+
"bool",
|
|
32
|
+
"date",
|
|
33
|
+
"timestamp",
|
|
34
|
+
"float",
|
|
35
|
+
"text",
|
|
36
|
+
"binary",
|
|
37
|
+
"vector",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class _Token:
|
|
43
|
+
kind: str
|
|
44
|
+
value: str
|
|
45
|
+
position: int
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class SchemaParseError(ValueError):
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def parse_table_schema(source: str) -> dict[str, DataTypeUnion]:
|
|
53
|
+
tokens = _tokenize(source)
|
|
54
|
+
parser = _SchemaParser(tokens)
|
|
55
|
+
schema = parser.parse_schema()
|
|
56
|
+
parser.ensure_end()
|
|
57
|
+
return schema
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class _SchemaParser:
|
|
61
|
+
def __init__(self, tokens: Iterable[_Token]):
|
|
62
|
+
self._tokens = list(tokens)
|
|
63
|
+
self._index = 0
|
|
64
|
+
|
|
65
|
+
def parse_schema(self) -> dict[str, DataTypeUnion]:
|
|
66
|
+
schema: dict[str, DataTypeUnion] = {}
|
|
67
|
+
while True:
|
|
68
|
+
name, data_type = self._parse_column_def()
|
|
69
|
+
if name in schema:
|
|
70
|
+
raise SchemaParseError(f"Duplicate column name: {name}")
|
|
71
|
+
schema[name] = data_type
|
|
72
|
+
if not self._accept("COMMA"):
|
|
73
|
+
break
|
|
74
|
+
return schema
|
|
75
|
+
|
|
76
|
+
def parse_type(self) -> DataTypeUnion:
|
|
77
|
+
token = self._peek()
|
|
78
|
+
if token is None or token.kind != "IDENT":
|
|
79
|
+
raise self._error("Expected data type")
|
|
80
|
+
type_name = token.value.casefold()
|
|
81
|
+
if type_name == "vector":
|
|
82
|
+
self._advance()
|
|
83
|
+
return self._parse_vector_type()
|
|
84
|
+
if type_name not in ALLOWED_DATA_TYPES:
|
|
85
|
+
raise self._error(
|
|
86
|
+
f"Unsupported data type '{token.value}'. Allowed: {', '.join(ALLOWED_DATA_TYPES)}"
|
|
87
|
+
)
|
|
88
|
+
self._advance()
|
|
89
|
+
return _simple_type(type_name)
|
|
90
|
+
|
|
91
|
+
def parse_schema_type(self) -> DataTypeUnion:
|
|
92
|
+
return self.parse_type()
|
|
93
|
+
|
|
94
|
+
def parse_schema_type_with_nullability(self) -> DataTypeUnion:
|
|
95
|
+
data_type = self.parse_schema_type()
|
|
96
|
+
if self._accept("IDENT", "null"):
|
|
97
|
+
data_type.nullable = True
|
|
98
|
+
elif self._accept("IDENT", "not"):
|
|
99
|
+
self._expect("IDENT", "null")
|
|
100
|
+
data_type.nullable = False
|
|
101
|
+
return data_type
|
|
102
|
+
|
|
103
|
+
def ensure_end(self) -> None:
|
|
104
|
+
if self._peek() is not None:
|
|
105
|
+
token = self._peek()
|
|
106
|
+
raise SchemaParseError(
|
|
107
|
+
f"Unexpected token '{token.value}' at position {token.position}"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def _parse_column_def(self) -> tuple[str, DataTypeUnion]:
|
|
111
|
+
name_token = self._expect("IDENT")
|
|
112
|
+
data_type = self.parse_schema_type_with_nullability()
|
|
113
|
+
return name_token.value, data_type
|
|
114
|
+
|
|
115
|
+
def _parse_vector_type(self) -> DataTypeUnion:
|
|
116
|
+
self._expect("LPAREN")
|
|
117
|
+
size_token = self._expect("INT")
|
|
118
|
+
size = int(size_token.value)
|
|
119
|
+
if size <= 0:
|
|
120
|
+
raise self._error("Vector size must be a positive integer")
|
|
121
|
+
element_type = FloatDataType()
|
|
122
|
+
if self._accept("COMMA"):
|
|
123
|
+
element_type = self.parse_schema_type()
|
|
124
|
+
self._expect("RPAREN")
|
|
125
|
+
return VectorDataType(size=size, element_type=element_type)
|
|
126
|
+
|
|
127
|
+
def _expect(self, kind: str, value: str | None = None) -> _Token:
|
|
128
|
+
token = self._peek()
|
|
129
|
+
if token is None:
|
|
130
|
+
raise self._error("Unexpected end of input")
|
|
131
|
+
if token.kind != kind or (
|
|
132
|
+
value is not None and token.value.casefold() != value.casefold()
|
|
133
|
+
):
|
|
134
|
+
expected = f"{kind}{' ' + value if value else ''}"
|
|
135
|
+
raise self._error(f"Expected {expected}")
|
|
136
|
+
self._index += 1
|
|
137
|
+
return token
|
|
138
|
+
|
|
139
|
+
def _accept(self, kind: str, value: str | None = None) -> bool:
|
|
140
|
+
token = self._peek()
|
|
141
|
+
if token is None:
|
|
142
|
+
return False
|
|
143
|
+
if token.kind != kind:
|
|
144
|
+
return False
|
|
145
|
+
if value is not None and token.value.casefold() != value.casefold():
|
|
146
|
+
return False
|
|
147
|
+
self._index += 1
|
|
148
|
+
return True
|
|
149
|
+
|
|
150
|
+
def _peek(self) -> _Token | None:
|
|
151
|
+
if self._index >= len(self._tokens):
|
|
152
|
+
return None
|
|
153
|
+
return self._tokens[self._index]
|
|
154
|
+
|
|
155
|
+
def _advance(self) -> _Token:
|
|
156
|
+
token = self._peek()
|
|
157
|
+
if token is None:
|
|
158
|
+
raise self._error("Unexpected end of input")
|
|
159
|
+
self._index += 1
|
|
160
|
+
return token
|
|
161
|
+
|
|
162
|
+
def _error(self, message: str) -> SchemaParseError:
|
|
163
|
+
token = self._peek()
|
|
164
|
+
if token is None:
|
|
165
|
+
return SchemaParseError(f"{message} at end of input")
|
|
166
|
+
return SchemaParseError(f"{message} at position {token.position}")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _tokenize(source: str) -> list[_Token]:
|
|
170
|
+
tokens: list[_Token] = []
|
|
171
|
+
idx = 0
|
|
172
|
+
length = len(source)
|
|
173
|
+
while idx < length:
|
|
174
|
+
char = source[idx]
|
|
175
|
+
if char.isspace():
|
|
176
|
+
idx += 1
|
|
177
|
+
continue
|
|
178
|
+
if char == ",":
|
|
179
|
+
tokens.append(_Token("COMMA", char, idx))
|
|
180
|
+
idx += 1
|
|
181
|
+
continue
|
|
182
|
+
if char == "(":
|
|
183
|
+
tokens.append(_Token("LPAREN", char, idx))
|
|
184
|
+
idx += 1
|
|
185
|
+
continue
|
|
186
|
+
if char == ")":
|
|
187
|
+
tokens.append(_Token("RPAREN", char, idx))
|
|
188
|
+
idx += 1
|
|
189
|
+
continue
|
|
190
|
+
if char.isdigit():
|
|
191
|
+
start = idx
|
|
192
|
+
while idx < length and source[idx].isdigit():
|
|
193
|
+
idx += 1
|
|
194
|
+
tokens.append(_Token("INT", source[start:idx], start))
|
|
195
|
+
continue
|
|
196
|
+
if char.isalpha() or char == "_":
|
|
197
|
+
start = idx
|
|
198
|
+
while idx < length and (source[idx].isalnum() or source[idx] == "_"):
|
|
199
|
+
idx += 1
|
|
200
|
+
tokens.append(_Token("IDENT", source[start:idx], start))
|
|
201
|
+
continue
|
|
202
|
+
raise SchemaParseError(f"Unexpected character '{char}' at position {idx}")
|
|
203
|
+
return tokens
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _simple_type(type_name: str) -> DataTypeUnion:
|
|
207
|
+
if type_name == "int":
|
|
208
|
+
return IntDataType()
|
|
209
|
+
if type_name == "bool":
|
|
210
|
+
return BoolDataType()
|
|
211
|
+
if type_name == "date":
|
|
212
|
+
return DateDataType()
|
|
213
|
+
if type_name == "timestamp":
|
|
214
|
+
return TimestampDataType()
|
|
215
|
+
if type_name == "float":
|
|
216
|
+
return FloatDataType()
|
|
217
|
+
if type_name == "text":
|
|
218
|
+
return TextDataType()
|
|
219
|
+
if type_name == "binary":
|
|
220
|
+
return BinaryDataType()
|
|
221
|
+
raise SchemaParseError(
|
|
222
|
+
f"Unsupported data type '{type_name}'. Allowed: {', '.join(ALLOWED_DATA_TYPES)}"
|
|
223
|
+
)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from meshagent.api.room_server_client import (
|
|
4
|
+
FloatDataType,
|
|
5
|
+
IntDataType,
|
|
6
|
+
TextDataType,
|
|
7
|
+
VectorDataType,
|
|
8
|
+
)
|
|
9
|
+
from meshagent.api.sql import SchemaParseError, parse_table_schema
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_parse_schema_case_insensitive():
|
|
13
|
+
schema = parse_table_schema("names VeCtOr(20) nUlL, test TeXT NoT NuLL, age INT")
|
|
14
|
+
|
|
15
|
+
assert isinstance(schema["names"], VectorDataType)
|
|
16
|
+
assert schema["names"].size == 20
|
|
17
|
+
assert schema["names"].nullable is True
|
|
18
|
+
assert isinstance(schema["names"].element_type, FloatDataType)
|
|
19
|
+
|
|
20
|
+
assert isinstance(schema["test"], TextDataType)
|
|
21
|
+
assert schema["test"].nullable is False
|
|
22
|
+
|
|
23
|
+
assert isinstance(schema["age"], IntDataType)
|
|
24
|
+
assert schema["age"].nullable is None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_parse_schema_vector_element_type_case_insensitive():
|
|
28
|
+
schema = parse_table_schema("embedding vector(3, FLOAT)")
|
|
29
|
+
column = schema["embedding"]
|
|
30
|
+
|
|
31
|
+
assert isinstance(column, VectorDataType)
|
|
32
|
+
assert column.size == 3
|
|
33
|
+
assert isinstance(column.element_type, FloatDataType)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_parse_schema_duplicate_columns():
|
|
37
|
+
with pytest.raises(SchemaParseError, match="Duplicate column name"):
|
|
38
|
+
parse_table_schema("id int, id text")
|
meshagent/api/urls.py
CHANGED
|
@@ -8,7 +8,7 @@ def meshagent_base_url(base_url: Optional[str] = None):
|
|
|
8
8
|
|
|
9
9
|
def websocket_room_url(*, room_name: str, base_url: Optional[str] = None) -> str:
|
|
10
10
|
if base_url is None:
|
|
11
|
-
api_url = os.getenv("MESHAGENT_API_URL")
|
|
11
|
+
api_url = os.getenv("MESHAGENT_ROOM_URL", os.getenv("MESHAGENT_API_URL"))
|
|
12
12
|
if api_url is None:
|
|
13
13
|
base_url = "wss://api.meshagent.com"
|
|
14
14
|
else:
|
meshagent/api/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "0.25.0"
|
|
@@ -4,6 +4,7 @@ import asyncio
|
|
|
4
4
|
import logging
|
|
5
5
|
import urllib
|
|
6
6
|
from meshagent.api.version import __version__
|
|
7
|
+
from meshagent.api.http import new_client_session
|
|
7
8
|
from typing import Optional
|
|
8
9
|
|
|
9
10
|
from meshagent.api.protocol import Protocol, ClientProtocol
|
|
@@ -12,19 +13,31 @@ logger = logging.getLogger("protocol.websocket")
|
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
class WebSocketClientProtocol(ClientProtocol):
|
|
15
|
-
def __init__(
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
*,
|
|
19
|
+
url: str,
|
|
20
|
+
token: str,
|
|
21
|
+
heartbeat: float = 30,
|
|
22
|
+
session: ClientSession | None = None,
|
|
23
|
+
):
|
|
16
24
|
super().__init__(token=token)
|
|
17
25
|
self._url = url
|
|
18
26
|
self._heartbeat = heartbeat
|
|
27
|
+
self._session = session
|
|
28
|
+
self._session_external = session is not None
|
|
19
29
|
|
|
20
30
|
@property
|
|
21
31
|
def url(self):
|
|
22
32
|
return self._url
|
|
23
33
|
|
|
24
34
|
async def __aenter__(self):
|
|
25
|
-
self._session
|
|
35
|
+
if self._session is None:
|
|
36
|
+
self._session = new_client_session()
|
|
37
|
+
self._session_external = False
|
|
26
38
|
|
|
27
|
-
|
|
39
|
+
if not self._session_external:
|
|
40
|
+
await self._session.__aenter__()
|
|
28
41
|
|
|
29
42
|
url_parts = urllib.parse.urlparse(self._url)
|
|
30
43
|
query_dict = urllib.parse.parse_qs(url_parts.query)
|
|
@@ -70,7 +83,8 @@ class WebSocketClientProtocol(ClientProtocol):
|
|
|
70
83
|
if not self._ws.closed:
|
|
71
84
|
await self._ws.close()
|
|
72
85
|
await self._ws_recv_task
|
|
73
|
-
|
|
86
|
+
if not self._session_external:
|
|
87
|
+
await self._session.__aexit__(exc_type, exc, tb)
|
|
74
88
|
await self._ws_ctx.__aexit__(exc_type, exc, tb)
|
|
75
89
|
await super().__aexit__(exc_type, exc, tb)
|
|
76
90
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meshagent-api
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.25.0
|
|
4
4
|
Summary: Python Server API for Meshagent
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Project-URL: Documentation, https://docs.meshagent.com
|
|
@@ -10,12 +10,13 @@ Requires-Python: >=3.13
|
|
|
10
10
|
Description-Content-Type: text/markdown
|
|
11
11
|
License-File: LICENSE
|
|
12
12
|
Requires-Dist: pyjwt~=2.10
|
|
13
|
-
Requires-Dist: aiohttp~=3.
|
|
13
|
+
Requires-Dist: aiohttp[speedups]~=3.13.0
|
|
14
14
|
Requires-Dist: jsonschema~=4.23
|
|
15
15
|
Requires-Dist: pycrdt~=0.12.26
|
|
16
16
|
Requires-Dist: opentelemetry-distro~=0.54b1
|
|
17
17
|
Requires-Dist: pydantic~=2.11.7
|
|
18
18
|
Requires-Dist: jinja2~=3.1.6
|
|
19
|
+
Requires-Dist: certifi~=2026.1.4
|
|
19
20
|
Provides-Extra: all
|
|
20
21
|
Provides-Extra: sync
|
|
21
22
|
Provides-Extra: stpyv8
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
meshagent/api/__init__.py,sha256=kzhnEnzNjm4yUztR4aY78h1mxZxkvXGeWIvxOA2rDxY,2037
|
|
2
2
|
meshagent/api/chan.py,sha256=PBIfVHj_znsmlZ0zxvmglCrX_4m7zzPLhxWRycfyEgc,4763
|
|
3
|
-
meshagent/api/client.py,sha256=
|
|
3
|
+
meshagent/api/client.py,sha256=4aHj3BI6y1UE0jAHPh4m6-X_F4iOe39IwrC2Y-esqFQ,76362
|
|
4
4
|
meshagent/api/crdt.py,sha256=1d_bjIR8iYfktHBW04dfaaiXWA6ZYCfgfxCmDHXivGk,18580
|
|
5
5
|
meshagent/api/entrypoint.js,sha256=82XGHgopru9fyzi3V5JFy6YoaV0OmlsczXE9OdHjYgw,85567
|
|
6
|
-
meshagent/api/helpers.py,sha256=
|
|
6
|
+
meshagent/api/helpers.py,sha256=5tU0k1DcWZMcZy2dlA3qJ5Ifs5Au4TEsBontKHa43y4,3365
|
|
7
|
+
meshagent/api/http.py,sha256=gooHy2PIpP4Upnbxn_TZ5D3si6dixsG5piPbFzdGXEI,413
|
|
7
8
|
meshagent/api/keys.py,sha256=VkRo0tH3uEMgCDnUg7640oDQkruGoE1AItb9g-Jjg9c,2950
|
|
8
9
|
meshagent/api/messaging.py,sha256=0vCmdxwtki9rHKrON427r2TTbXdyW9iEluQ3fAyjMYU,8844
|
|
9
10
|
meshagent/api/oauth.py,sha256=ezwtsdM76FWFGIZeUhJ5z02tOTh-ve7djHPe-PuzGVU,460
|
|
10
11
|
meshagent/api/participant.py,sha256=usyXHK42Swobb76SRNHZrfB3w4h0P8tGmzGRrOJCw-k,341
|
|
11
|
-
meshagent/api/participant_token.py,sha256=
|
|
12
|
+
meshagent/api/participant_token.py,sha256=i5trPgWtCxG380GIMz7xGvYbDiyLsO8jeuztBMmxeEs,13893
|
|
12
13
|
meshagent/api/participant_token_test.py,sha256=EeMuw47iR1N2mBxVZRAhhiWYtN9BvIT80fAoZAN-VqQ,6998
|
|
13
|
-
meshagent/api/port_forward.py,sha256=
|
|
14
|
+
meshagent/api/port_forward.py,sha256=WijWKs14kMabW97GjjBXTdB0dg7_mdlCWeNk7KW91e4,5097
|
|
14
15
|
meshagent/api/protocol.py,sha256=1N82ZQmSTg_XIf5yrjAo3QVEmW9yT29JctWTwzoVDzA,10571
|
|
15
16
|
meshagent/api/protocol_test.py,sha256=3ETAAAEzuqhFW26xYPxE7Pj1_Us6dHlifgp1W2QiwFs,1428
|
|
16
17
|
meshagent/api/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
18
|
meshagent/api/reasoning_schema.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
-
meshagent/api/room_server_client.py,sha256=
|
|
19
|
+
meshagent/api/room_server_client.py,sha256=vRpFealzUkRnQsMd8OPc-Rd738hUXJM9ukooHMrjsHM,105259
|
|
19
20
|
meshagent/api/runtime.py,sha256=H0pcZHefjDBFMZgZMeAW7dKUtYrrrKI7Fb8_vvI7o4s,11355
|
|
20
21
|
meshagent/api/runtime_test.py,sha256=hQSp2fz2CT94cGg3-yeyFMK5F7jrIXRDoEtn15A5h4k,10807
|
|
21
22
|
meshagent/api/schema.py,sha256=Y0A1QRk6_hG3zP81Qv7rFO01y2mbNqyvD1sKDyhEJfM,11673
|
|
@@ -26,14 +27,16 @@ meshagent/api/schema_test.py,sha256=lYi7AGm5LD8aVMKzGFdN_o219kifb3_laETXAvC7KJI,
|
|
|
26
27
|
meshagent/api/schema_util.py,sha256=-vtbWKfolYK0946HAGfD5xYmQ0qHJftcPcMBLPtzs1I,1371
|
|
27
28
|
meshagent/api/service_template_test.py,sha256=N348oZzlOiLR4GZNTiiLyEXXWq8Btapg6C3oymtVo8g,6145
|
|
28
29
|
meshagent/api/services.py,sha256=LPJqGuwN0zZgZWIiJqkf6Kh0lyKn9s1z-SEuX2oPw10,11365
|
|
30
|
+
meshagent/api/sql.py,sha256=b3q2xO0yn88S31vha-WZBVEYHUxPcrKR9OcY6dcKXcI,6886
|
|
31
|
+
meshagent/api/sql_test.py,sha256=sDX4vVNRez0EE09J0yPYMuKYJi8dG4eUSE21bBox0sg,1186
|
|
29
32
|
meshagent/api/token_test.py,sha256=KGa-lDMAsB3QST_6ORCPGSOHPnhm6WSTA4uj4ZYgrPY,3719
|
|
30
|
-
meshagent/api/urls.py,sha256=
|
|
31
|
-
meshagent/api/version.py,sha256=
|
|
33
|
+
meshagent/api/urls.py,sha256=g7w-1nzbcRzJVbxp2M8TiT8pz7NLVfSamrEkr0NDiKc,745
|
|
34
|
+
meshagent/api/version.py,sha256=Mu4JbSLl5nr-J2figk5hmW2mrw4skf_oeIzxbnpcgwY,23
|
|
32
35
|
meshagent/api/webhooks.py,sha256=6ZKUgc-uXzg6V2DBy-yAvOsb1dXxUVEPTqj36A9w_6Y,8352
|
|
33
|
-
meshagent/api/websocket_protocol.py,sha256=
|
|
34
|
-
meshagent/api/specs/service.py,sha256=
|
|
35
|
-
meshagent_api-0.
|
|
36
|
-
meshagent_api-0.
|
|
37
|
-
meshagent_api-0.
|
|
38
|
-
meshagent_api-0.
|
|
39
|
-
meshagent_api-0.
|
|
36
|
+
meshagent/api/websocket_protocol.py,sha256=f9IV6goyRvpbOTIsziOd5_aXujikHuSiTSXk2Q0dXTA,4160
|
|
37
|
+
meshagent/api/specs/service.py,sha256=XIBS9MFmG-k8aeY7JV-_IBFtu0TQUADn2418waGyqHc,15398
|
|
38
|
+
meshagent_api-0.25.0.dist-info/licenses/LICENSE,sha256=eTt0SPW-sVNdkZe9PS_S8WfCIyLjRXRl7sUBWdlteFg,10254
|
|
39
|
+
meshagent_api-0.25.0.dist-info/METADATA,sha256=XE_gStvbJHSuGbiuq1qoQG23c8aO3OSEezhL58inWa4,4585
|
|
40
|
+
meshagent_api-0.25.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
41
|
+
meshagent_api-0.25.0.dist-info/top_level.txt,sha256=GlcXnHtRP6m7zlG3Df04M35OsHtNXy_DY09oFwWrH74,10
|
|
42
|
+
meshagent_api-0.25.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|