fleet-python 0.1.1__tar.gz → 0.2.0__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.

Potentially problematic release.


This version of fleet-python might be problematic. Click here for more details.

Files changed (33) hide show
  1. {fleet_python-0.1.1/fleet_python.egg-info → fleet_python-0.2.0}/PKG-INFO +2 -1
  2. fleet_python-0.2.0/examples/browser_control_example.py +51 -0
  3. {fleet_python-0.1.1 → fleet_python-0.2.0}/fleet/__init__.py +3 -15
  4. fleet_python-0.2.0/fleet/base.py +74 -0
  5. fleet_python-0.2.0/fleet/client.py +154 -0
  6. fleet_python-0.2.0/fleet/env/__init__.py +8 -0
  7. fleet_python-0.2.0/fleet/env/base.py +60 -0
  8. fleet_python-0.2.0/fleet/env/client.py +241 -0
  9. fleet_python-0.2.0/fleet/env/models.py +127 -0
  10. fleet_python-0.2.0/fleet/models.py +109 -0
  11. fleet_python-0.2.0/fleet/resources/base.py +23 -0
  12. fleet_python-0.2.0/fleet/resources/browser.py +18 -0
  13. fleet_python-0.2.0/fleet/resources/sqlite.py +41 -0
  14. {fleet_python-0.1.1 → fleet_python-0.2.0/fleet_python.egg-info}/PKG-INFO +2 -1
  15. {fleet_python-0.1.1 → fleet_python-0.2.0}/fleet_python.egg-info/SOURCES.txt +8 -6
  16. {fleet_python-0.1.1 → fleet_python-0.2.0}/fleet_python.egg-info/requires.txt +1 -0
  17. {fleet_python-0.1.1 → fleet_python-0.2.0}/pyproject.toml +2 -1
  18. fleet_python-0.1.1/fleet/client.py +0 -318
  19. fleet_python-0.1.1/fleet/config.py +0 -125
  20. fleet_python-0.1.1/fleet/env/__init__.py +0 -20
  21. fleet_python-0.1.1/fleet/env/base.py +0 -361
  22. fleet_python-0.1.1/fleet/env/factory.py +0 -337
  23. fleet_python-0.1.1/fleet/facets/__init__.py +0 -7
  24. fleet_python-0.1.1/fleet/facets/base.py +0 -223
  25. fleet_python-0.1.1/fleet/facets/factory.py +0 -29
  26. fleet_python-0.1.1/fleet/manager_client.py +0 -177
  27. {fleet_python-0.1.1 → fleet_python-0.2.0}/LICENSE +0 -0
  28. {fleet_python-0.1.1 → fleet_python-0.2.0}/README.md +0 -0
  29. {fleet_python-0.1.1 → fleet_python-0.2.0}/examples/quickstart.py +0 -0
  30. {fleet_python-0.1.1 → fleet_python-0.2.0}/fleet/exceptions.py +0 -0
  31. {fleet_python-0.1.1 → fleet_python-0.2.0}/fleet_python.egg-info/dependency_links.txt +0 -0
  32. {fleet_python-0.1.1 → fleet_python-0.2.0}/fleet_python.egg-info/top_level.txt +0 -0
  33. {fleet_python-0.1.1 → fleet_python-0.2.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fleet-python
3
- Version: 0.1.1
3
+ Version: 0.2.0
4
4
  Summary: Python SDK for Fleet environments
5
5
  Author-email: Fleet AI <nic@fleet.so>
6
6
  License: Apache-2.0
@@ -22,6 +22,7 @@ Description-Content-Type: text/markdown
22
22
  License-File: LICENSE
23
23
  Requires-Dist: aiohttp>=3.8.0
24
24
  Requires-Dist: pydantic>=2.0.0
25
+ Requires-Dist: httpx>=0.27.0
25
26
  Requires-Dist: typing-extensions>=4.0.0
26
27
  Provides-Extra: dev
27
28
  Requires-Dist: pytest>=7.0.0; extra == "dev"
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env python3
2
+ """Example demonstrating browser control with Fleet Manager Client."""
3
+
4
+ import asyncio
5
+ import fleet as flt
6
+
7
+
8
+ async def main():
9
+ fleet = flt.AsyncFleet()
10
+
11
+ environments = await fleet.list_envs()
12
+ print("Environments:", len(environments))
13
+
14
+ instances = await fleet.instances(status="running")
15
+ print("Instances:", len(instances))
16
+
17
+ instance = await fleet.instance("16fdbc96")
18
+ print("Instance:", instance.instance_id)
19
+ print("Instance Environment:", instance.env_key)
20
+
21
+ environment = await fleet.environment(instance.env_key)
22
+ print("Environment Default Version:", environment.default_version)
23
+
24
+ response = await instance.env.reset()
25
+ print("Reset response:", response)
26
+
27
+ print(await instance.env.resources())
28
+
29
+ sqlite = instance.env.sqlite("current")
30
+ print("SQLite:", await sqlite.describe())
31
+
32
+ print("Query:", await sqlite.query("SELECT * FROM users"))
33
+
34
+ sqlite = await instance.env.state("sqlite://current").describe()
35
+ print("SQLite:", sqlite)
36
+
37
+ browser = await instance.env.browser("cdp").describe()
38
+ print("CDP URL:", browser.url)
39
+ print("CDP Devtools URL:", browser.devtools_url)
40
+
41
+ # Create a new instance
42
+ instance = await fleet.make(flt.InstanceRequest(env_key=instance.env_key))
43
+ print("New Instance:", instance.instance_id)
44
+
45
+ # Delete the instance
46
+ instance = await fleet.delete(instance.instance_id)
47
+ print("Instance deleted:", instance.terminated_at)
48
+
49
+
50
+ if __name__ == "__main__":
51
+ asyncio.run(main())
@@ -14,11 +14,8 @@
14
14
 
15
15
  """Fleet Python SDK - Environment-based AI agent interactions."""
16
16
 
17
- from . import env
18
17
  from .exceptions import FleetError, FleetAPIError, FleetTimeoutError, FleetConfigurationError
19
- from .config import get_config, FleetConfig
20
- from .client import FleetAPIClient, InstanceRequest, InstanceResponse, EnvDetails as APIEnvironment, HealthResponse, ManagerURLs, InstanceURLs
21
- from .manager_client import FleetManagerClient, ManagerHealthResponse, TimestampResponse
18
+ from .client import Fleet, AsyncFleet, InstanceRequest
22
19
 
23
20
  __version__ = "0.1.1"
24
21
  __all__ = [
@@ -27,16 +24,7 @@ __all__ = [
27
24
  "FleetAPIError",
28
25
  "FleetTimeoutError",
29
26
  "FleetConfigurationError",
30
- "get_config",
31
- "FleetConfig",
32
- "FleetAPIClient",
27
+ "Fleet",
28
+ "AsyncFleet",
33
29
  "InstanceRequest",
34
- "InstanceResponse",
35
- "APIEnvironment",
36
- "HealthResponse",
37
- "ManagerURLs",
38
- "InstanceURLs",
39
- "FleetManagerClient",
40
- "ManagerHealthResponse",
41
- "TimestampResponse",
42
30
  ]
@@ -0,0 +1,74 @@
1
+ import httpx
2
+ from typing import Dict, Any, Optional
3
+
4
+ from .models import InstanceResponse
5
+
6
+
7
+ class InstanceBase(InstanceResponse):
8
+ @property
9
+ def manager_url(self) -> str:
10
+ return f"{self.urls.manager.api}"
11
+
12
+
13
+ class BaseWrapper:
14
+ def __init__(self, *, api_key: Optional[str], base_url: Optional[str]):
15
+ if api_key is None:
16
+ raise ValueError("api_key is required")
17
+ self.api_key = api_key
18
+ if base_url is None:
19
+ base_url = "https://fleet.new"
20
+ self.base_url = base_url
21
+
22
+ def get_headers(self) -> Dict[str, str]:
23
+ headers: Dict[str, str] = {
24
+ "X-Fleet-SDK-Language": "Python",
25
+ "X-Fleet-SDK-Version": "1.0.0",
26
+ }
27
+ headers["Authorization"] = f"Bearer {self.api_key}"
28
+ return headers
29
+
30
+
31
+ class SyncWrapper(BaseWrapper):
32
+ def __init__(self, *, httpx_client: httpx.Client, **kwargs):
33
+ super().__init__(**kwargs)
34
+ self.httpx_client = httpx_client
35
+
36
+ def request(
37
+ self,
38
+ method: str,
39
+ url: str,
40
+ params: Optional[Dict[str, Any]] = None,
41
+ json: Optional[Any] = None,
42
+ **kwargs,
43
+ ) -> httpx.Response:
44
+ return self.httpx_client.request(
45
+ method,
46
+ f"{self.base_url}{url}",
47
+ headers=self.get_headers(),
48
+ params=params,
49
+ json=json,
50
+ **kwargs,
51
+ )
52
+
53
+
54
+ class AsyncWrapper(BaseWrapper):
55
+ def __init__(self, *, httpx_client: httpx.AsyncClient, **kwargs):
56
+ super().__init__(**kwargs)
57
+ self.httpx_client = httpx_client
58
+
59
+ async def request(
60
+ self,
61
+ method: str,
62
+ url: str,
63
+ params: Optional[Dict[str, Any]] = None,
64
+ json: Optional[Any] = None,
65
+ **kwargs,
66
+ ) -> httpx.Response:
67
+ return await self.httpx_client.request(
68
+ method,
69
+ f"{self.base_url}{url}",
70
+ headers=self.get_headers(),
71
+ params=params,
72
+ json=json,
73
+ **kwargs,
74
+ )
@@ -0,0 +1,154 @@
1
+ # Copyright 2025 Fleet AI
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Fleet API Client for making HTTP requests to Fleet services."""
16
+
17
+ import asyncio
18
+ import os
19
+ import httpx
20
+ import logging
21
+ from typing import Optional, List
22
+
23
+ from .base import InstanceBase, AsyncWrapper, SyncWrapper
24
+ from .models import InstanceRequest, InstanceRecord, Environment as EnvironmentModel
25
+
26
+ from .env import Environment, AsyncEnvironment
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ class Instance(InstanceBase):
32
+ def __init__(self, httpx_client: Optional[httpx.Client] = None, **kwargs):
33
+ super().__init__(**kwargs)
34
+ self._httpx_client = httpx_client or httpx.Client()
35
+ self._env: Optional[Environment] = None
36
+
37
+ @property
38
+ def env(self) -> Environment:
39
+ if self._env is None:
40
+ self._env = Environment(self.manager_url, self._httpx_client)
41
+ return self._env
42
+
43
+
44
+ class AsyncInstance(InstanceBase):
45
+ def __init__(self, httpx_client: Optional[httpx.AsyncClient] = None, **kwargs):
46
+ super().__init__(**kwargs)
47
+ self._httpx_client = httpx_client or httpx.AsyncClient()
48
+ self._env: Optional[AsyncEnvironment] = None
49
+
50
+ @property
51
+ def env(self) -> AsyncEnvironment:
52
+ if self._env is None:
53
+ self._env = AsyncEnvironment(self.manager_url, self._httpx_client)
54
+ return self._env
55
+
56
+
57
+ class Fleet:
58
+ def __init__(
59
+ self,
60
+ api_key: Optional[str] = os.getenv("FLEET_API_KEY"),
61
+ base_url: Optional[str] = None,
62
+ httpx_client: Optional[httpx.Client] = None,
63
+ ):
64
+ self._httpx_client = httpx_client or httpx.Client(timeout=60.0)
65
+ self.client = SyncWrapper(
66
+ api_key=api_key,
67
+ base_url=base_url,
68
+ httpx_client=self._httpx_client,
69
+ )
70
+
71
+ def environments(self) -> List[EnvironmentModel]:
72
+ response = self.client.request("GET", "/v1/env/")
73
+ return [EnvironmentModel(**env_data) for env_data in response.json()]
74
+
75
+ def environment(self, env_key: str) -> EnvironmentModel:
76
+ response = self.client.request("GET", f"/v1/env/{env_key}")
77
+ return EnvironmentModel(**response.json())
78
+
79
+ def make(self, request: InstanceRequest) -> Instance:
80
+ response = self.client.request(
81
+ "POST", "/v1/env/instances", json=request.model_dump()
82
+ )
83
+ return Instance(**response.json())
84
+
85
+ def instances(self, status: Optional[str] = None) -> List[Instance]:
86
+ params = {}
87
+ if status:
88
+ params["status"] = status
89
+
90
+ response = self.client.request("GET", "/v1/env/instances", params=params)
91
+ return [Instance(**instance_data) for instance_data in response.json()]
92
+
93
+ def instance(self, instance_id: str) -> Instance:
94
+ response = self.client.request("GET", f"/v1/env/instances/{instance_id}")
95
+ return Instance(**response.json())
96
+
97
+ def delete(self, instance_id: str) -> InstanceRecord:
98
+ response = self.client.request("DELETE", f"/v1/env/instances/{instance_id}")
99
+ return InstanceRecord(**response.json())
100
+
101
+
102
+ class AsyncFleet:
103
+ def __init__(
104
+ self,
105
+ api_key: Optional[str] = os.getenv("FLEET_API_KEY"),
106
+ base_url: Optional[str] = None,
107
+ httpx_client: Optional[httpx.AsyncClient] = None,
108
+ ):
109
+ self._httpx_client = httpx_client or httpx.AsyncClient(timeout=60.0)
110
+ self.client = AsyncWrapper(
111
+ api_key=api_key,
112
+ base_url=base_url,
113
+ httpx_client=self._httpx_client,
114
+ )
115
+
116
+ async def list_envs(self) -> List[EnvironmentModel]:
117
+ response = await self.client.request("GET", "/v1/env/")
118
+ return [EnvironmentModel(**env_data) for env_data in response.json()]
119
+
120
+ async def environment(self, env_key: str) -> EnvironmentModel:
121
+ response = await self.client.request("GET", f"/v1/env/{env_key}")
122
+ return EnvironmentModel(**response.json())
123
+
124
+ async def make(self, request: InstanceRequest) -> AsyncInstance:
125
+ response = await self.client.request(
126
+ "POST", "/v1/env/instances", json=request.model_dump()
127
+ )
128
+ instance = AsyncInstance(**response.json())
129
+ await instance.env.load()
130
+ return instance
131
+
132
+ async def instances(self, status: Optional[str] = None) -> List[AsyncInstance]:
133
+ params = {}
134
+ if status:
135
+ params["status"] = status
136
+
137
+ response = await self.client.request("GET", "/v1/env/instances", params=params)
138
+ instances = [
139
+ AsyncInstance(**instance_data) for instance_data in response.json()
140
+ ]
141
+ await asyncio.gather(*[instance.env.load() for instance in instances])
142
+ return instances
143
+
144
+ async def instance(self, instance_id: str) -> AsyncInstance:
145
+ response = await self.client.request("GET", f"/v1/env/instances/{instance_id}")
146
+ instance = AsyncInstance(**response.json())
147
+ await instance.env.load()
148
+ return instance
149
+
150
+ async def delete(self, instance_id: str) -> InstanceRecord:
151
+ response = await self.client.request(
152
+ "DELETE", f"/v1/env/instances/{instance_id}"
153
+ )
154
+ return InstanceRecord(**response.json())
@@ -0,0 +1,8 @@
1
+ """Fleet SDK Environment Module."""
2
+
3
+ from .client import Environment, AsyncEnvironment
4
+
5
+ __all__ = [
6
+ "Environment",
7
+ "AsyncEnvironment",
8
+ ]
@@ -0,0 +1,60 @@
1
+ import httpx
2
+ from typing import Dict, Any, Optional
3
+
4
+
5
+ class BaseWrapper:
6
+ def __init__(self, *, url: str):
7
+ self.url = url
8
+
9
+ def get_headers(self) -> Dict[str, str]:
10
+ headers: Dict[str, str] = {
11
+ "X-Fleet-SDK-Language": "Python",
12
+ "X-Fleet-SDK-Version": "1.0.0",
13
+ }
14
+ return headers
15
+
16
+
17
+ class SyncWrapper(BaseWrapper):
18
+ def __init__(self, *, httpx_client: httpx.Client, **kwargs):
19
+ super().__init__(**kwargs)
20
+ self.httpx_client = httpx_client
21
+
22
+ def request(
23
+ self,
24
+ method: str,
25
+ path: str,
26
+ params: Optional[Dict[str, Any]] = None,
27
+ json: Optional[Any] = None,
28
+ **kwargs,
29
+ ) -> httpx.Response:
30
+ return self.httpx_client.request(
31
+ method,
32
+ f"{self.url}{path}",
33
+ headers=self.get_headers(),
34
+ params=params,
35
+ json=json,
36
+ **kwargs,
37
+ )
38
+
39
+
40
+ class AsyncWrapper(BaseWrapper):
41
+ def __init__(self, *, httpx_client: httpx.AsyncClient, **kwargs):
42
+ super().__init__(**kwargs)
43
+ self.httpx_client = httpx_client
44
+
45
+ async def request(
46
+ self,
47
+ method: str,
48
+ path: str,
49
+ params: Optional[Dict[str, Any]] = None,
50
+ json: Optional[Any] = None,
51
+ **kwargs,
52
+ ) -> httpx.Response:
53
+ return await self.httpx_client.request(
54
+ method,
55
+ f"{self.url}{path}",
56
+ headers=self.get_headers(),
57
+ params=params,
58
+ json=json,
59
+ **kwargs,
60
+ )
@@ -0,0 +1,241 @@
1
+ """Fleet SDK Base Environment Classes."""
2
+
3
+ from typing import Any, Dict, List, Optional, Tuple
4
+ import asyncio
5
+ import httpx
6
+ import time
7
+ import logging
8
+ from urllib.parse import urlparse
9
+
10
+ from ..resources.sqlite import AsyncSQLiteResource
11
+ from ..resources.browser import AsyncBrowserResource
12
+ from ..resources.base import Resource
13
+
14
+ from ..exceptions import FleetEnvironmentError, FleetAPIError
15
+
16
+ from .base import SyncWrapper, AsyncWrapper
17
+ from .models import (
18
+ ResetResponse,
19
+ Resource as ResourceModel,
20
+ ResourceType,
21
+ HealthResponse,
22
+ )
23
+
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ RESOURCE_TYPES = {
29
+ ResourceType.sqlite: AsyncSQLiteResource,
30
+ ResourceType.cdp: AsyncBrowserResource,
31
+ }
32
+
33
+
34
+ class Environment:
35
+ def __init__(
36
+ self,
37
+ url: str,
38
+ httpx_client: Optional[httpx.Client] = None,
39
+ ):
40
+ self.base_url = url
41
+ self.client = SyncWrapper(
42
+ url=self.base_url, httpx_client=httpx_client or httpx.Client()
43
+ )
44
+ raise NotImplementedError("SyncManager is not implemented")
45
+
46
+ def reset(self) -> ResetResponse:
47
+ response = self.client.request("POST", "/reset")
48
+ return ResetResponse(**response.json())
49
+
50
+
51
+ class AsyncEnvironment:
52
+ def __init__(
53
+ self,
54
+ url: str,
55
+ httpx_client: Optional[httpx.AsyncClient] = None,
56
+ ):
57
+ self.base_url = url
58
+ self.client = AsyncWrapper(
59
+ url=self.base_url, httpx_client=httpx_client or httpx.AsyncClient()
60
+ )
61
+ self._resources: Optional[List[ResourceModel]] = None
62
+ self._resources_state: Dict[str, Dict[str, Resource]] = {
63
+ resource_type.value: {} for resource_type in ResourceType
64
+ }
65
+
66
+ async def load(self) -> None:
67
+ await self._load_resources()
68
+
69
+ async def reset(self) -> ResetResponse:
70
+ response = await self.client.request("POST", "/reset")
71
+ return ResetResponse(**response.json())
72
+
73
+ def state(self, uri: str) -> Resource:
74
+ url = urlparse(uri)
75
+ return self._resources_state[url.scheme][url.netloc]
76
+
77
+ def sqlite(self, name: str) -> AsyncSQLiteResource:
78
+ return AsyncSQLiteResource(
79
+ self._resources_state[ResourceType.sqlite.value][name], self.client
80
+ )
81
+
82
+ def browser(self, name: str) -> AsyncBrowserResource:
83
+ return AsyncBrowserResource(
84
+ self._resources_state[ResourceType.cdp.value][name], self.client
85
+ )
86
+
87
+ async def resources(self) -> List[ResourceModel]:
88
+ await self._load_resources()
89
+ return self._resources
90
+
91
+ async def _load_resources(self) -> None:
92
+ if self._resources is None:
93
+ response = await self.client.request("GET", "/resources")
94
+ if response.status_code != 200:
95
+ self._resources = []
96
+ return
97
+ data = response.json()
98
+ self._resources = [
99
+ ResourceModel(**resource) for resource in data["resources"]
100
+ ]
101
+ for resource in self._resources:
102
+ if resource.type not in self._resources_state:
103
+ self._resources_state[resource.type.value] = {}
104
+ self._resources_state[resource.type.value][resource.name] = (
105
+ RESOURCE_TYPES[resource.type](resource, self.client)
106
+ )
107
+
108
+ async def step(self, action: Dict[str, Any]) -> Tuple[Dict[str, Any], float, bool]:
109
+ """Execute one step in the environment."""
110
+ if not self._instance_id:
111
+ raise FleetEnvironmentError(
112
+ "Environment not initialized. Call reset() first."
113
+ )
114
+
115
+ try:
116
+ # Increment step count
117
+ self._increment_step()
118
+
119
+ # Execute action through instance manager API
120
+ # This is a placeholder - actual implementation depends on the manager API spec
121
+ state, reward, done = await self._execute_action(action)
122
+
123
+ return state, reward, done
124
+
125
+ except Exception as e:
126
+ raise FleetEnvironmentError(f"Failed to execute step: {e}")
127
+
128
+ async def close(self) -> None:
129
+ """Close the environment and clean up resources."""
130
+ try:
131
+ # Delete instance if it exists
132
+ if self._instance_id:
133
+ try:
134
+ await self._client.delete_instance(self._instance_id)
135
+ logger.info(f"Deleted instance: {self._instance_id}")
136
+ except FleetAPIError as e:
137
+ logger.warning(f"Failed to delete instance: {e}")
138
+ finally:
139
+ self._instance_id = None
140
+ self._instance_response = None
141
+
142
+ # Close manager client
143
+ if self._manager_client:
144
+ await self._manager_client.close()
145
+ self._manager_client = None
146
+
147
+ # Close API client
148
+ await self._client.close()
149
+
150
+ except Exception as e:
151
+ logger.error(f"Error closing environment: {e}")
152
+
153
+ async def manager_health_check(self) -> Optional[HealthResponse]:
154
+ response = await self.client.request("GET", "/health")
155
+ return HealthResponse(**response.json())
156
+
157
+ async def _wait_for_instance_ready(self, timeout: float = 300.0) -> None:
158
+ """Wait for instance to be ready.
159
+
160
+ Args:
161
+ timeout: Maximum time to wait in seconds
162
+ """
163
+ start_time = time.time()
164
+
165
+ while time.time() - start_time < timeout:
166
+ try:
167
+ instance = await self._client.get_instance(self._instance_id)
168
+ self._instance_response = instance
169
+
170
+ if instance.status == "running":
171
+ logger.info(f"Instance {self._instance_id} is ready")
172
+ return
173
+
174
+ elif instance.status == "error":
175
+ raise FleetEnvironmentError(
176
+ f"Instance {self._instance_id} failed to start"
177
+ )
178
+
179
+ # Wait before checking again
180
+ await asyncio.sleep(5)
181
+
182
+ except FleetAPIError as e:
183
+ if time.time() - start_time >= timeout:
184
+ raise FleetEnvironmentError(
185
+ f"Timeout waiting for instance to be ready: {e}"
186
+ )
187
+ await asyncio.sleep(5)
188
+
189
+ raise FleetEnvironmentError(
190
+ f"Timeout waiting for instance {self._instance_id} to be ready"
191
+ )
192
+
193
+ async def _execute_action(
194
+ self, action: Dict[str, Any]
195
+ ) -> Tuple[Dict[str, Any], float, bool]:
196
+ """Execute an action through the instance manager API.
197
+
198
+ This is a placeholder implementation that should be extended based on
199
+ the actual manager API specification.
200
+
201
+ Args:
202
+ action: The action to execute as a dictionary
203
+
204
+ Returns:
205
+ Tuple of (state, reward, done)
206
+ """
207
+ # Ensure manager client is available
208
+ await self._ensure_manager_client()
209
+
210
+ # TODO: In the future, this would use the manager API to execute actions
211
+ # For example: await self._manager_client.log_action(action)
212
+ # For now, return placeholder values
213
+
214
+ # Create a placeholder state
215
+ state = self._create_state_from_action(action)
216
+
217
+ # Create a placeholder reward
218
+ reward = 0.0
219
+
220
+ # Determine if episode is done (placeholder logic)
221
+ done = self._step_count >= 100 # Example: done after 100 steps
222
+
223
+ return state, reward, done
224
+
225
+ def _create_state_from_action(self, action: Dict[str, Any]) -> Dict[str, Any]:
226
+ """Create state based on executed action."""
227
+ return {
228
+ "instance_id": self._instance_id,
229
+ "step": self._step_count,
230
+ "last_action": action,
231
+ "timestamp": time.time(),
232
+ "status": "running",
233
+ }
234
+
235
+ async def __aenter__(self):
236
+ """Async context manager entry."""
237
+ return self
238
+
239
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
240
+ """Async context manager exit."""
241
+ await self.close()