meshagent-api 0.41.9__tar.gz → 0.42.2__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.
- {meshagent_api-0.41.9/meshagent_api.egg-info → meshagent_api-0.42.2}/PKG-INFO +1 -1
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/client.py +11 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/client_test.py +71 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/room_server_client.py +67 -3
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/room_server_client_test.py +65 -1
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/specs/service.py +16 -0
- meshagent_api-0.42.2/meshagent/api/version.py +1 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2/meshagent_api.egg-info}/PKG-INFO +1 -1
- meshagent_api-0.41.9/meshagent/api/version.py +0 -1
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/LICENSE +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/MANIFEST.in +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/README.md +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/__init__.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/agent_content.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/aiohttp_redaction.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/chan.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/crdt.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/dataset_schema.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/entrypoint.js +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/error_codes.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/helpers.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/http.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/image_runtime.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/keys.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/managed_agents.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/managed_agents_test.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/messaging.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/messaging_test.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/oauth.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/oauth_scopes.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/oauth_scopes_test.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/participant.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/participant_token.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/participant_token_test.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/port_forward.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/port_forward_test.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/protocol.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/protocol_test.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/py.typed +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/reasoning_schema.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/registry_auth.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/room_ports.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/runtime.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/runtime_test.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/schema.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/schema_document.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/schema_document_test.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/schema_registry.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/schema_test.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/schema_util.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/service_spec_test.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/service_template_test.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/services.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/sql.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/sql_test.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/token_test.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/urls.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/webhooks.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/websocket_protocol.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent/api/websocket_protocol_test.py +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent_api.egg-info/SOURCES.txt +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent_api.egg-info/dependency_links.txt +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent_api.egg-info/requires.txt +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/meshagent_api.egg-info/top_level.txt +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/pyproject.toml +0 -0
- {meshagent_api-0.41.9 → meshagent_api-0.42.2}/setup.cfg +0 -0
|
@@ -1333,6 +1333,17 @@ class Meshagent:
|
|
|
1333
1333
|
await self._raise_for_status(resp)
|
|
1334
1334
|
return await resp.json()
|
|
1335
1335
|
|
|
1336
|
+
async def get_project_by_key(self, project_key: str) -> Dict[str, Any]:
|
|
1337
|
+
"""
|
|
1338
|
+
Corresponds to: GET /accounts/projects/by-key/{project_key}
|
|
1339
|
+
Returns a JSON dict with { "id", "owner_user_id", "name", "project_key" }.
|
|
1340
|
+
"""
|
|
1341
|
+
url = f"{self.base_url}/accounts/projects/by-key/{quote(project_key, safe='')}"
|
|
1342
|
+
|
|
1343
|
+
async with self._session.get(url, headers=self._get_headers()) as resp:
|
|
1344
|
+
await self._raise_for_status(resp)
|
|
1345
|
+
return await resp.json()
|
|
1346
|
+
|
|
1336
1347
|
async def get_project_info(self, project_id: str) -> ProjectInfo:
|
|
1337
1348
|
url = f"{self.base_url}/accounts/projects/{project_id}"
|
|
1338
1349
|
|
|
@@ -12,6 +12,11 @@ from meshagent.api.managed_agents import (
|
|
|
12
12
|
from meshagent.api.participant_token import ApiScope
|
|
13
13
|
from meshagent.api.client import ManagedAgentGrant, Meshagent
|
|
14
14
|
from meshagent.api.specs.service import (
|
|
15
|
+
RouteBackendSpec,
|
|
16
|
+
RouteMetadata,
|
|
17
|
+
RoutePathSpec,
|
|
18
|
+
RouteRoomBackendSpec,
|
|
19
|
+
RouteSpec,
|
|
15
20
|
ScheduledTaskQueueSpec,
|
|
16
21
|
ScheduledTaskSpec,
|
|
17
22
|
ServiceMetadata,
|
|
@@ -98,6 +103,72 @@ async def test_render_template_accepts_decoded_json_response():
|
|
|
98
103
|
]
|
|
99
104
|
|
|
100
105
|
|
|
106
|
+
@pytest.mark.asyncio
|
|
107
|
+
async def test_create_route_omits_default_strip_prefix_from_paths():
|
|
108
|
+
session = _FakeSession([_FakeResponse(status=200, payload={})])
|
|
109
|
+
client = Meshagent(base_url="http://example.test", token="token", session=session)
|
|
110
|
+
|
|
111
|
+
await client.create_route(
|
|
112
|
+
project_id="project-1",
|
|
113
|
+
spec=RouteSpec(
|
|
114
|
+
metadata=RouteMetadata(name="app.example.test"),
|
|
115
|
+
domain="app.example.test",
|
|
116
|
+
backend=RouteBackendSpec(room=RouteRoomBackendSpec(name="room-1")),
|
|
117
|
+
paths=[RoutePathSpec(path="/", targetPort=3000)],
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
assert session.calls == [
|
|
122
|
+
(
|
|
123
|
+
"post",
|
|
124
|
+
"http://example.test/accounts/projects/project-1/routes",
|
|
125
|
+
{
|
|
126
|
+
"spec": {
|
|
127
|
+
"version": "v1",
|
|
128
|
+
"kind": "Route",
|
|
129
|
+
"metadata": {"name": "app.example.test", "annotations": {}},
|
|
130
|
+
"domain": "app.example.test",
|
|
131
|
+
"backend": {"room": {"name": "room-1"}, "agent": None},
|
|
132
|
+
"paths": [
|
|
133
|
+
{
|
|
134
|
+
"path": "/",
|
|
135
|
+
"pathType": "prefix",
|
|
136
|
+
"targetPort": 3000,
|
|
137
|
+
}
|
|
138
|
+
],
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
)
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@pytest.mark.asyncio
|
|
146
|
+
async def test_create_route_serializes_true_strip_prefix():
|
|
147
|
+
session = _FakeSession([_FakeResponse(status=200, payload={})])
|
|
148
|
+
client = Meshagent(base_url="http://example.test", token="token", session=session)
|
|
149
|
+
|
|
150
|
+
await client.create_route(
|
|
151
|
+
project_id="project-1",
|
|
152
|
+
spec=RouteSpec(
|
|
153
|
+
metadata=RouteMetadata(name="app.example.test"),
|
|
154
|
+
domain="app.example.test",
|
|
155
|
+
backend=RouteBackendSpec(room=RouteRoomBackendSpec(name="room-1")),
|
|
156
|
+
paths=[
|
|
157
|
+
RoutePathSpec(path="/api", targetPort=3000, stripPrefix=True),
|
|
158
|
+
],
|
|
159
|
+
),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
assert session.calls[0][2]["spec"]["paths"] == [
|
|
163
|
+
{
|
|
164
|
+
"path": "/api",
|
|
165
|
+
"pathType": "prefix",
|
|
166
|
+
"stripPrefix": True,
|
|
167
|
+
"targetPort": 3000,
|
|
168
|
+
}
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
|
|
101
172
|
@pytest.mark.asyncio
|
|
102
173
|
async def test_exchange_oauth_token_posts_form_encoded_token_request():
|
|
103
174
|
session = _FakeSession(
|
|
@@ -1123,6 +1123,14 @@ class RoomClient:
|
|
|
1123
1123
|
with contextlib.suppress(Exception):
|
|
1124
1124
|
await protocol.__aexit__(None, None, None)
|
|
1125
1125
|
|
|
1126
|
+
async def _close_protocol_shielded(self, protocol: Protocol) -> None:
|
|
1127
|
+
close_task = asyncio.create_task(self._close_protocol(protocol))
|
|
1128
|
+
try:
|
|
1129
|
+
await asyncio.shield(close_task)
|
|
1130
|
+
except asyncio.CancelledError:
|
|
1131
|
+
await asyncio.shield(close_task)
|
|
1132
|
+
raise
|
|
1133
|
+
|
|
1126
1134
|
def _replace_protocol(self, next_protocol: Protocol) -> None:
|
|
1127
1135
|
current_protocol = self._protocol_instance
|
|
1128
1136
|
self.protocol._unbind(current_protocol)
|
|
@@ -1644,13 +1652,19 @@ class RoomClient:
|
|
|
1644
1652
|
self._close_watcher_task = None
|
|
1645
1653
|
if close_watcher is not None:
|
|
1646
1654
|
close_watcher.cancel()
|
|
1655
|
+
close_cancelled = False
|
|
1647
1656
|
try:
|
|
1648
|
-
|
|
1657
|
+
try:
|
|
1658
|
+
await self._close_protocol_shielded(self._protocol_instance)
|
|
1659
|
+
except asyncio.CancelledError:
|
|
1660
|
+
close_cancelled = True
|
|
1649
1661
|
lifecycle_task = self._lifecycle_task
|
|
1650
1662
|
if lifecycle_task is not None:
|
|
1651
1663
|
await asyncio.gather(lifecycle_task, return_exceptions=True)
|
|
1652
1664
|
await self.sync.stop()
|
|
1653
1665
|
await self.messaging.stop()
|
|
1666
|
+
if close_cancelled:
|
|
1667
|
+
raise asyncio.CancelledError
|
|
1654
1668
|
finally:
|
|
1655
1669
|
self._lifecycle_task = None
|
|
1656
1670
|
self._entered = False
|
|
@@ -7639,11 +7653,35 @@ class ImportedImage(BaseModel):
|
|
|
7639
7653
|
refs: List[str] = Field(default_factory=list)
|
|
7640
7654
|
|
|
7641
7655
|
|
|
7656
|
+
class PublishedImageLayer(BaseModel):
|
|
7657
|
+
digest: str
|
|
7658
|
+
size_bytes: int
|
|
7659
|
+
media_type: Optional[str] = None
|
|
7660
|
+
|
|
7661
|
+
|
|
7662
|
+
class PublishedImageStats(BaseModel):
|
|
7663
|
+
manifest_media_type: str
|
|
7664
|
+
layer_count: int
|
|
7665
|
+
total_layer_size_bytes: int
|
|
7666
|
+
total_size_bytes: int
|
|
7667
|
+
layers: List[PublishedImageLayer] = Field(default_factory=list)
|
|
7668
|
+
config_size_bytes: Optional[int] = None
|
|
7669
|
+
|
|
7670
|
+
|
|
7671
|
+
class PublishedBuildImage(BaseModel):
|
|
7672
|
+
tag: str
|
|
7673
|
+
digest: str
|
|
7674
|
+
resolved_ref: str
|
|
7675
|
+
optimized: bool
|
|
7676
|
+
stats: Optional[PublishedImageStats] = None
|
|
7677
|
+
|
|
7678
|
+
|
|
7642
7679
|
class BuildJob(BaseModel):
|
|
7643
7680
|
id: str
|
|
7644
7681
|
tag: str
|
|
7645
7682
|
status: Literal["queued", "running", "failed", "cancelled", "succeeded"]
|
|
7646
7683
|
exit_code: Optional[int] = None
|
|
7684
|
+
published_images: List[PublishedBuildImage] = Field(default_factory=list)
|
|
7647
7685
|
|
|
7648
7686
|
|
|
7649
7687
|
class ContainerStartedBy(BaseModel):
|
|
@@ -7651,16 +7689,38 @@ class ContainerStartedBy(BaseModel):
|
|
|
7651
7689
|
name: str
|
|
7652
7690
|
|
|
7653
7691
|
|
|
7692
|
+
class RoomContainerPort(BaseModel):
|
|
7693
|
+
container_port: int
|
|
7694
|
+
host_port: int
|
|
7695
|
+
|
|
7696
|
+
|
|
7697
|
+
class RoomContainerStats(BaseModel):
|
|
7698
|
+
cpu_usage_nano_cores: Optional[int] = None
|
|
7699
|
+
memory_usage_bytes: Optional[int] = None
|
|
7700
|
+
memory_working_set_bytes: Optional[int] = None
|
|
7701
|
+
timestamp_ns: Optional[int] = None
|
|
7702
|
+
|
|
7703
|
+
|
|
7704
|
+
class ContainerExitStatus(BaseModel):
|
|
7705
|
+
exit_code: int
|
|
7706
|
+
reason: Optional[str] = None
|
|
7707
|
+
message: Optional[str] = None
|
|
7708
|
+
oom_killed: Optional[bool] = None
|
|
7709
|
+
|
|
7710
|
+
|
|
7654
7711
|
class RoomContainer(BaseModel):
|
|
7655
7712
|
id: str
|
|
7656
7713
|
image: Optional[str] = None
|
|
7714
|
+
image_id: Optional[str] = None
|
|
7657
7715
|
status: Optional[str] = None
|
|
7658
7716
|
name: Optional[str] = None
|
|
7659
|
-
ports: list[
|
|
7717
|
+
ports: list[RoomContainerPort] = Field(default_factory=list)
|
|
7660
7718
|
started_by: ContainerStartedBy
|
|
7661
7719
|
state: Literal["CREATED", "RUNNING", "EXITED", "UNKNOWN"]
|
|
7662
7720
|
private: bool
|
|
7663
7721
|
service_id: Optional[str] = None
|
|
7722
|
+
stats: Optional[RoomContainerStats] = None
|
|
7723
|
+
exit_status: Optional[ContainerExitStatus] = None
|
|
7664
7724
|
|
|
7665
7725
|
# Accept arbitrary extras (names, created, state, etc.)
|
|
7666
7726
|
model_config = ConfigDict(extra="allow")
|
|
@@ -8601,6 +8661,10 @@ class ContainersClient:
|
|
|
8601
8661
|
)
|
|
8602
8662
|
|
|
8603
8663
|
async def wait_for_exit(self, *, container_id: str) -> int:
|
|
8664
|
+
status = await self.wait_for_exit_status(container_id=container_id)
|
|
8665
|
+
return status.exit_code
|
|
8666
|
+
|
|
8667
|
+
async def wait_for_exit_status(self, *, container_id: str) -> ContainerExitStatus:
|
|
8604
8668
|
response = await self.room.invoke(
|
|
8605
8669
|
toolkit="containers",
|
|
8606
8670
|
tool="wait_for_exit",
|
|
@@ -8611,7 +8675,7 @@ class ContainersClient:
|
|
|
8611
8675
|
exit_code = response.json.get("exit_code")
|
|
8612
8676
|
if isinstance(exit_code, bool) or not isinstance(exit_code, int):
|
|
8613
8677
|
raise self._unexpected_response_error(operation="wait_for_exit")
|
|
8614
|
-
return
|
|
8678
|
+
return ContainerExitStatus.model_validate(response.json)
|
|
8615
8679
|
|
|
8616
8680
|
async def delete(self, *, container_id: str) -> None:
|
|
8617
8681
|
await self.room.invoke(
|
|
@@ -220,6 +220,18 @@ class _CloseableProtocol(_FakeProtocol):
|
|
|
220
220
|
return self._close_reason
|
|
221
221
|
|
|
222
222
|
|
|
223
|
+
class _SlowExitProtocol(_FakeProtocol):
|
|
224
|
+
def __init__(self) -> None:
|
|
225
|
+
super().__init__()
|
|
226
|
+
self.exit_started = asyncio.Event()
|
|
227
|
+
self.allow_exit = asyncio.Event()
|
|
228
|
+
|
|
229
|
+
async def __aexit__(self, exc_type, exc, tb) -> None:
|
|
230
|
+
self.exit_started.set()
|
|
231
|
+
await self.allow_exit.wait()
|
|
232
|
+
await super().__aexit__(exc_type, exc, tb)
|
|
233
|
+
|
|
234
|
+
|
|
223
235
|
def test_room_exception_defaults_to_invalid_request_code() -> None:
|
|
224
236
|
ex = RoomException("boom")
|
|
225
237
|
assert ex.code == ErrorCode.INVALID_REQUEST
|
|
@@ -2051,6 +2063,26 @@ async def test_room_client_exit_fails_pending_requests_and_cancels_close_watcher
|
|
|
2051
2063
|
assert client._close_watcher_task is None
|
|
2052
2064
|
|
|
2053
2065
|
|
|
2066
|
+
@pytest.mark.asyncio
|
|
2067
|
+
async def test_room_client_exit_shields_protocol_close_from_cancellation() -> None:
|
|
2068
|
+
protocol = _SlowExitProtocol()
|
|
2069
|
+
client = RoomClient(protocol_factory=protocol.create_factory())
|
|
2070
|
+
|
|
2071
|
+
exit_task = asyncio.create_task(client.__aexit__(None, None, None))
|
|
2072
|
+
await asyncio.wait_for(protocol.exit_started.wait(), timeout=1)
|
|
2073
|
+
|
|
2074
|
+
exit_task.cancel()
|
|
2075
|
+
await asyncio.sleep(0)
|
|
2076
|
+
assert not exit_task.done()
|
|
2077
|
+
|
|
2078
|
+
protocol.allow_exit.set()
|
|
2079
|
+
with pytest.raises(asyncio.CancelledError):
|
|
2080
|
+
await asyncio.wait_for(exit_task, timeout=1)
|
|
2081
|
+
|
|
2082
|
+
assert protocol.exited is True
|
|
2083
|
+
assert client._close_watcher_task is None
|
|
2084
|
+
|
|
2085
|
+
|
|
2054
2086
|
@pytest.mark.asyncio
|
|
2055
2087
|
async def test_room_client_wait_for_close_ignores_unexpected_disconnects() -> None:
|
|
2056
2088
|
controller = _ReconnectRoomController(schema=_simple_thread_schema())
|
|
@@ -4049,6 +4081,32 @@ async def test_containers_client_uses_room_invoke_with_strict_payloads() -> None
|
|
|
4049
4081
|
"tag": "example:latest",
|
|
4050
4082
|
"status": "running",
|
|
4051
4083
|
"exit_code": None,
|
|
4084
|
+
"published_images": [
|
|
4085
|
+
{
|
|
4086
|
+
"tag": "example:latest",
|
|
4087
|
+
"digest": "sha256:digest",
|
|
4088
|
+
"resolved_ref": "example@sha256:digest",
|
|
4089
|
+
"optimized": True,
|
|
4090
|
+
"stats": {
|
|
4091
|
+
"manifest_media_type": (
|
|
4092
|
+
"application/vnd.oci.image.manifest.v1+json"
|
|
4093
|
+
),
|
|
4094
|
+
"layer_count": 1,
|
|
4095
|
+
"total_layer_size_bytes": 200,
|
|
4096
|
+
"total_size_bytes": 300,
|
|
4097
|
+
"config_size_bytes": 100,
|
|
4098
|
+
"layers": [
|
|
4099
|
+
{
|
|
4100
|
+
"digest": "sha256:layer",
|
|
4101
|
+
"size_bytes": 200,
|
|
4102
|
+
"media_type": (
|
|
4103
|
+
"application/vnd.oci.image.layer.v1.tar"
|
|
4104
|
+
),
|
|
4105
|
+
}
|
|
4106
|
+
],
|
|
4107
|
+
},
|
|
4108
|
+
}
|
|
4109
|
+
],
|
|
4052
4110
|
}
|
|
4053
4111
|
]
|
|
4054
4112
|
}
|
|
@@ -4070,6 +4128,7 @@ async def test_containers_client_uses_room_invoke_with_strict_payloads() -> None
|
|
|
4070
4128
|
configs=[ConfigMountSpec()],
|
|
4071
4129
|
empty_dirs=[EmptyDirMountSpec(path="/cache")],
|
|
4072
4130
|
),
|
|
4131
|
+
template="agent",
|
|
4073
4132
|
)
|
|
4074
4133
|
await client.build(
|
|
4075
4134
|
tags=["example:latest"],
|
|
@@ -4086,7 +4145,7 @@ async def test_containers_client_uses_room_invoke_with_strict_payloads() -> None
|
|
|
4086
4145
|
await client.run_service(service_id="svc-1", env={"A": "1"})
|
|
4087
4146
|
images = await client.list_images()
|
|
4088
4147
|
inspection = await client.inspect_image(image_id="img-1")
|
|
4089
|
-
await client.list_builds()
|
|
4148
|
+
builds = await client.list_builds()
|
|
4090
4149
|
await client.cancel_build(build_id="build-1")
|
|
4091
4150
|
await client.delete_build(build_id="build-1")
|
|
4092
4151
|
await client.list()
|
|
@@ -4119,6 +4178,10 @@ async def test_containers_client_uses_room_invoke_with_strict_payloads() -> None
|
|
|
4119
4178
|
assert inspection.image.references == ["demo:latest"]
|
|
4120
4179
|
assert inspection.target.digest == "sha256:target"
|
|
4121
4180
|
assert inspection.content_size == 235
|
|
4181
|
+
assert builds[0].published_images[0].resolved_ref == "example@sha256:digest"
|
|
4182
|
+
assert builds[0].published_images[0].stats is not None
|
|
4183
|
+
assert builds[0].published_images[0].stats.layer_count == 1
|
|
4184
|
+
assert builds[0].published_images[0].stats.total_size_bytes == 300
|
|
4122
4185
|
|
|
4123
4186
|
pull_input = room.requests[0]["input"]
|
|
4124
4187
|
assert isinstance(pull_input, dict)
|
|
@@ -4130,6 +4193,7 @@ async def test_containers_client_uses_room_invoke_with_strict_payloads() -> None
|
|
|
4130
4193
|
assert isinstance(run_input, dict)
|
|
4131
4194
|
assert run_input["env"] == [{"key": "KEY", "value": "VALUE"}]
|
|
4132
4195
|
assert run_input["ports"] == [{"container_port": 8080, "host_port": 80}]
|
|
4196
|
+
assert run_input["template"] == "agent"
|
|
4133
4197
|
assert isinstance(run_input["mounts"], dict)
|
|
4134
4198
|
assert run_input["mounts"]["configs"] == [{"path": "/var/run/meshagent"}]
|
|
4135
4199
|
assert run_input["mounts"]["empty_dirs"] == [{"path": "/cache", "read_only": False}]
|
|
@@ -4,6 +4,7 @@ from pydantic import (
|
|
|
4
4
|
ConfigDict,
|
|
5
5
|
Field,
|
|
6
6
|
field_validator,
|
|
7
|
+
model_serializer,
|
|
7
8
|
model_validator,
|
|
8
9
|
)
|
|
9
10
|
from typing import Any, Optional, Literal
|
|
@@ -461,8 +462,16 @@ class RoutePathSpec(BaseModel):
|
|
|
461
462
|
|
|
462
463
|
path: str = "/"
|
|
463
464
|
pathType: Literal["prefix", "exact"] = "prefix"
|
|
465
|
+
stripPrefix: bool = False
|
|
464
466
|
targetPort: str | int
|
|
465
467
|
|
|
468
|
+
@model_serializer(mode="wrap")
|
|
469
|
+
def serialize_route_path(self, handler: Any) -> dict[str, Any]:
|
|
470
|
+
data: dict[str, Any] = handler(self)
|
|
471
|
+
if not self.stripPrefix:
|
|
472
|
+
data.pop("stripPrefix", None)
|
|
473
|
+
return data
|
|
474
|
+
|
|
466
475
|
@field_validator("path")
|
|
467
476
|
@classmethod
|
|
468
477
|
def validate_path(cls, value: str) -> str:
|
|
@@ -633,6 +642,13 @@ class EndpointSpec(BaseModel):
|
|
|
633
642
|
class PortSpec(BaseModel):
|
|
634
643
|
model_config = ConfigDict(extra="forbid")
|
|
635
644
|
num: Literal["*"] | PositiveInt = "*"
|
|
645
|
+
host_port: Optional[PositiveInt] = Field(
|
|
646
|
+
None,
|
|
647
|
+
description=(
|
|
648
|
+
"optional room host port to publish this container port on; when set, "
|
|
649
|
+
"published defaults to true unless explicitly set"
|
|
650
|
+
),
|
|
651
|
+
)
|
|
636
652
|
type: Optional[Literal["http", "tcp"]] = "http"
|
|
637
653
|
endpoints: list[EndpointSpec] = Field(
|
|
638
654
|
default_factory=list, description="a list of endpoints exposed under this port"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.42.2"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.41.9"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|