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.
- {fleet_python-0.1.1/fleet_python.egg-info → fleet_python-0.2.0}/PKG-INFO +2 -1
- fleet_python-0.2.0/examples/browser_control_example.py +51 -0
- {fleet_python-0.1.1 → fleet_python-0.2.0}/fleet/__init__.py +3 -15
- fleet_python-0.2.0/fleet/base.py +74 -0
- fleet_python-0.2.0/fleet/client.py +154 -0
- fleet_python-0.2.0/fleet/env/__init__.py +8 -0
- fleet_python-0.2.0/fleet/env/base.py +60 -0
- fleet_python-0.2.0/fleet/env/client.py +241 -0
- fleet_python-0.2.0/fleet/env/models.py +127 -0
- fleet_python-0.2.0/fleet/models.py +109 -0
- fleet_python-0.2.0/fleet/resources/base.py +23 -0
- fleet_python-0.2.0/fleet/resources/browser.py +18 -0
- fleet_python-0.2.0/fleet/resources/sqlite.py +41 -0
- {fleet_python-0.1.1 → fleet_python-0.2.0/fleet_python.egg-info}/PKG-INFO +2 -1
- {fleet_python-0.1.1 → fleet_python-0.2.0}/fleet_python.egg-info/SOURCES.txt +8 -6
- {fleet_python-0.1.1 → fleet_python-0.2.0}/fleet_python.egg-info/requires.txt +1 -0
- {fleet_python-0.1.1 → fleet_python-0.2.0}/pyproject.toml +2 -1
- fleet_python-0.1.1/fleet/client.py +0 -318
- fleet_python-0.1.1/fleet/config.py +0 -125
- fleet_python-0.1.1/fleet/env/__init__.py +0 -20
- fleet_python-0.1.1/fleet/env/base.py +0 -361
- fleet_python-0.1.1/fleet/env/factory.py +0 -337
- fleet_python-0.1.1/fleet/facets/__init__.py +0 -7
- fleet_python-0.1.1/fleet/facets/base.py +0 -223
- fleet_python-0.1.1/fleet/facets/factory.py +0 -29
- fleet_python-0.1.1/fleet/manager_client.py +0 -177
- {fleet_python-0.1.1 → fleet_python-0.2.0}/LICENSE +0 -0
- {fleet_python-0.1.1 → fleet_python-0.2.0}/README.md +0 -0
- {fleet_python-0.1.1 → fleet_python-0.2.0}/examples/quickstart.py +0 -0
- {fleet_python-0.1.1 → fleet_python-0.2.0}/fleet/exceptions.py +0 -0
- {fleet_python-0.1.1 → fleet_python-0.2.0}/fleet_python.egg-info/dependency_links.txt +0 -0
- {fleet_python-0.1.1 → fleet_python-0.2.0}/fleet_python.egg-info/top_level.txt +0 -0
- {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.
|
|
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 .
|
|
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
|
-
"
|
|
31
|
-
"
|
|
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,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()
|