hopx-ai 0.1.10__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.
Potentially problematic release.
This version of hopx-ai might be problematic. Click here for more details.
- hopx_ai/__init__.py +114 -0
- hopx_ai/_agent_client.py +373 -0
- hopx_ai/_async_client.py +230 -0
- hopx_ai/_client.py +230 -0
- hopx_ai/_generated/__init__.py +22 -0
- hopx_ai/_generated/models.py +502 -0
- hopx_ai/_utils.py +9 -0
- hopx_ai/_ws_client.py +141 -0
- hopx_ai/async_sandbox.py +427 -0
- hopx_ai/cache.py +97 -0
- hopx_ai/commands.py +174 -0
- hopx_ai/desktop.py +1227 -0
- hopx_ai/env_vars.py +242 -0
- hopx_ai/errors.py +249 -0
- hopx_ai/files.py +489 -0
- hopx_ai/models.py +274 -0
- hopx_ai/models_updated.py +270 -0
- hopx_ai/sandbox.py +1439 -0
- hopx_ai/template/__init__.py +47 -0
- hopx_ai/template/build_flow.py +540 -0
- hopx_ai/template/builder.py +300 -0
- hopx_ai/template/file_hasher.py +81 -0
- hopx_ai/template/ready_checks.py +106 -0
- hopx_ai/template/tar_creator.py +122 -0
- hopx_ai/template/types.py +199 -0
- hopx_ai/terminal.py +164 -0
- hopx_ai-0.1.10.dist-info/METADATA +460 -0
- hopx_ai-0.1.10.dist-info/RECORD +29 -0
- hopx_ai-0.1.10.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Template Building Types
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import List, Dict, Optional, Callable, Any
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class StepType(str, Enum):
|
|
12
|
+
"""Step types for template building"""
|
|
13
|
+
FROM = "FROM"
|
|
14
|
+
COPY = "COPY"
|
|
15
|
+
RUN = "RUN"
|
|
16
|
+
ENV = "ENV"
|
|
17
|
+
WORKDIR = "WORKDIR"
|
|
18
|
+
USER = "USER"
|
|
19
|
+
CMD = "CMD"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class RegistryAuth:
|
|
24
|
+
"""Basic registry authentication"""
|
|
25
|
+
username: str
|
|
26
|
+
password: str
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class GCPRegistryAuth:
|
|
31
|
+
"""GCP Container Registry authentication"""
|
|
32
|
+
service_account_json: Any # str (file path) or dict
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class AWSRegistryAuth:
|
|
37
|
+
"""AWS ECR authentication"""
|
|
38
|
+
access_key_id: str
|
|
39
|
+
secret_access_key: str
|
|
40
|
+
region: str
|
|
41
|
+
session_token: Optional[str] = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class Step:
|
|
46
|
+
"""Represents a build step"""
|
|
47
|
+
type: StepType
|
|
48
|
+
args: List[str]
|
|
49
|
+
files_hash: Optional[str] = None
|
|
50
|
+
skip_cache: bool = False
|
|
51
|
+
registry_auth: Optional[RegistryAuth] = None
|
|
52
|
+
gcp_auth: Optional[GCPRegistryAuth] = None
|
|
53
|
+
aws_auth: Optional[AWSRegistryAuth] = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class CopyOptions:
|
|
58
|
+
"""Options for COPY steps"""
|
|
59
|
+
owner: Optional[str] = None
|
|
60
|
+
permissions: Optional[str] = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ReadyCheckType(str, Enum):
|
|
64
|
+
"""Types of ready checks"""
|
|
65
|
+
PORT = "port"
|
|
66
|
+
URL = "url"
|
|
67
|
+
FILE = "file"
|
|
68
|
+
PROCESS = "process"
|
|
69
|
+
COMMAND = "command"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class ReadyCheck:
|
|
74
|
+
"""Ready check configuration"""
|
|
75
|
+
type: ReadyCheckType
|
|
76
|
+
port: Optional[int] = None
|
|
77
|
+
url: Optional[str] = None
|
|
78
|
+
path: Optional[str] = None
|
|
79
|
+
process_name: Optional[str] = None
|
|
80
|
+
command: Optional[str] = None
|
|
81
|
+
timeout: int = 30000
|
|
82
|
+
interval: int = 2000
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass
|
|
86
|
+
class BuildOptions:
|
|
87
|
+
"""Options for building a template"""
|
|
88
|
+
alias: str
|
|
89
|
+
api_key: str
|
|
90
|
+
base_url: str = "https://api.your-domain.com"
|
|
91
|
+
cpu: int = 2
|
|
92
|
+
memory: int = 2048
|
|
93
|
+
disk_gb: int = 10
|
|
94
|
+
skip_cache: bool = False
|
|
95
|
+
context_path: Optional[str] = None
|
|
96
|
+
on_log: Optional[Callable[[Dict[str, Any]], None]] = None
|
|
97
|
+
on_progress: Optional[Callable[[int], None]] = None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass
|
|
101
|
+
class LogEntry:
|
|
102
|
+
"""Log entry from build"""
|
|
103
|
+
timestamp: str
|
|
104
|
+
level: str
|
|
105
|
+
message: str
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass
|
|
109
|
+
class StatusUpdate:
|
|
110
|
+
"""Status update from build"""
|
|
111
|
+
status: str
|
|
112
|
+
progress: int
|
|
113
|
+
current_step: Optional[str] = None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass
|
|
117
|
+
class CreateVMOptions:
|
|
118
|
+
"""Options for creating a VM from template"""
|
|
119
|
+
alias: Optional[str] = None
|
|
120
|
+
cpu: Optional[int] = None
|
|
121
|
+
memory: Optional[int] = None
|
|
122
|
+
disk_gb: Optional[int] = None
|
|
123
|
+
env_vars: Optional[Dict[str, str]] = None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass
|
|
127
|
+
class VM:
|
|
128
|
+
"""Represents a VM instance"""
|
|
129
|
+
vm_id: str
|
|
130
|
+
template_id: str
|
|
131
|
+
status: str
|
|
132
|
+
ip: str
|
|
133
|
+
agent_url: str
|
|
134
|
+
started_at: str
|
|
135
|
+
_delete_func: Optional[Callable[[], None]] = None
|
|
136
|
+
|
|
137
|
+
async def delete(self):
|
|
138
|
+
"""Delete this VM"""
|
|
139
|
+
if self._delete_func:
|
|
140
|
+
await self._delete_func()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@dataclass
|
|
144
|
+
class BuildResult:
|
|
145
|
+
"""Result of a template build"""
|
|
146
|
+
build_id: str
|
|
147
|
+
template_id: str
|
|
148
|
+
duration: int
|
|
149
|
+
_create_vm_func: Optional[Callable[[CreateVMOptions], Any]] = None
|
|
150
|
+
|
|
151
|
+
async def create_vm(self, options: CreateVMOptions = None) -> VM:
|
|
152
|
+
"""Create a VM from this template"""
|
|
153
|
+
if self._create_vm_func:
|
|
154
|
+
return await self._create_vm_func(options or CreateVMOptions())
|
|
155
|
+
raise RuntimeError("create_vm function not available")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@dataclass
|
|
159
|
+
class UploadLinkResponse:
|
|
160
|
+
"""Response from upload link request"""
|
|
161
|
+
present: bool
|
|
162
|
+
upload_url: Optional[str] = None
|
|
163
|
+
expires_at: Optional[str] = None
|
|
164
|
+
request_id: Optional[str] = None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@dataclass
|
|
168
|
+
class BuildResponse:
|
|
169
|
+
"""Response from build trigger"""
|
|
170
|
+
build_id: str
|
|
171
|
+
template_id: str
|
|
172
|
+
status: str
|
|
173
|
+
logs_url: str
|
|
174
|
+
request_id: Optional[str] = None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclass
|
|
178
|
+
class BuildStatusResponse:
|
|
179
|
+
"""Response from build status check"""
|
|
180
|
+
build_id: str
|
|
181
|
+
template_id: str
|
|
182
|
+
status: str
|
|
183
|
+
progress: int
|
|
184
|
+
started_at: str
|
|
185
|
+
current_step: Optional[str] = None
|
|
186
|
+
estimated_completion: Optional[str] = None
|
|
187
|
+
error: Optional[str] = None
|
|
188
|
+
request_id: Optional[str] = None
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@dataclass
|
|
192
|
+
class LogsResponse:
|
|
193
|
+
"""Response from build logs polling"""
|
|
194
|
+
logs: str
|
|
195
|
+
offset: int
|
|
196
|
+
status: str
|
|
197
|
+
complete: bool
|
|
198
|
+
request_id: Optional[str] = None
|
|
199
|
+
|
hopx_ai/terminal.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Interactive terminal access via WebSocket."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Optional, AsyncIterator, Dict, Any
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
from websockets.client import WebSocketClientProtocol
|
|
8
|
+
WEBSOCKETS_AVAILABLE = True
|
|
9
|
+
except ImportError:
|
|
10
|
+
WEBSOCKETS_AVAILABLE = False
|
|
11
|
+
WebSocketClientProtocol = Any # type: ignore
|
|
12
|
+
|
|
13
|
+
from ._ws_client import WebSocketClient
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Terminal:
|
|
19
|
+
"""
|
|
20
|
+
Interactive terminal resource with PTY support via WebSocket.
|
|
21
|
+
|
|
22
|
+
Provides real-time terminal access to the sandbox for interactive commands.
|
|
23
|
+
|
|
24
|
+
Features:
|
|
25
|
+
- Full PTY support
|
|
26
|
+
- Real-time output streaming
|
|
27
|
+
- Terminal resize support
|
|
28
|
+
- Process exit notifications
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
>>> import asyncio
|
|
32
|
+
>>>
|
|
33
|
+
>>> async def interactive_terminal():
|
|
34
|
+
... sandbox = Sandbox.create(template="code-interpreter")
|
|
35
|
+
...
|
|
36
|
+
... # Connect to terminal
|
|
37
|
+
... async with await sandbox.terminal.connect() as ws:
|
|
38
|
+
... # Send command
|
|
39
|
+
... await sandbox.terminal.send_input(ws, "ls -la\\n")
|
|
40
|
+
...
|
|
41
|
+
... # Receive output
|
|
42
|
+
... async for message in sandbox.terminal.iter_output(ws):
|
|
43
|
+
... if message['type'] == 'output':
|
|
44
|
+
... print(message['data'], end='')
|
|
45
|
+
... elif message['type'] == 'exit':
|
|
46
|
+
... print(f"\\nProcess exited: {message['code']}")
|
|
47
|
+
... break
|
|
48
|
+
>>>
|
|
49
|
+
>>> asyncio.run(interactive_terminal())
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, ws_client: WebSocketClient):
|
|
53
|
+
"""
|
|
54
|
+
Initialize Terminal resource.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
ws_client: WebSocket client
|
|
58
|
+
"""
|
|
59
|
+
if not WEBSOCKETS_AVAILABLE:
|
|
60
|
+
raise ImportError(
|
|
61
|
+
"websockets library is required for terminal features. "
|
|
62
|
+
"Install with: pip install websockets"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
self._ws_client = ws_client
|
|
66
|
+
logger.debug("Terminal resource initialized")
|
|
67
|
+
|
|
68
|
+
async def connect(
|
|
69
|
+
self,
|
|
70
|
+
*,
|
|
71
|
+
timeout: Optional[int] = 30
|
|
72
|
+
) -> WebSocketClientProtocol:
|
|
73
|
+
"""
|
|
74
|
+
Connect to interactive terminal.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
timeout: Connection timeout in seconds
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
WebSocket connection (use with async context manager)
|
|
81
|
+
|
|
82
|
+
Example:
|
|
83
|
+
>>> async with await sandbox.terminal.connect() as ws:
|
|
84
|
+
... await sandbox.terminal.send_input(ws, "echo 'Hello'\\n")
|
|
85
|
+
... async for msg in sandbox.terminal.iter_output(ws):
|
|
86
|
+
... print(msg['data'], end='')
|
|
87
|
+
... if msg['type'] == 'exit':
|
|
88
|
+
... break
|
|
89
|
+
"""
|
|
90
|
+
return await self._ws_client.connect("/terminal", timeout=timeout)
|
|
91
|
+
|
|
92
|
+
async def send_input(
|
|
93
|
+
self,
|
|
94
|
+
ws: WebSocketClientProtocol,
|
|
95
|
+
data: str
|
|
96
|
+
) -> None:
|
|
97
|
+
"""
|
|
98
|
+
Send input to terminal.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
ws: WebSocket connection
|
|
102
|
+
data: Input data (include \\n for commands)
|
|
103
|
+
|
|
104
|
+
Example:
|
|
105
|
+
>>> await terminal.send_input(ws, "ls -la\\n")
|
|
106
|
+
>>> await terminal.send_input(ws, "cd /workspace\\n")
|
|
107
|
+
"""
|
|
108
|
+
await self._ws_client.send_message(ws, {
|
|
109
|
+
"type": "input",
|
|
110
|
+
"data": data
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
async def resize(
|
|
114
|
+
self,
|
|
115
|
+
ws: WebSocketClientProtocol,
|
|
116
|
+
cols: int,
|
|
117
|
+
rows: int
|
|
118
|
+
) -> None:
|
|
119
|
+
"""
|
|
120
|
+
Resize terminal window.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
ws: WebSocket connection
|
|
124
|
+
cols: Number of columns
|
|
125
|
+
rows: Number of rows
|
|
126
|
+
|
|
127
|
+
Example:
|
|
128
|
+
>>> await terminal.resize(ws, cols=120, rows=40)
|
|
129
|
+
"""
|
|
130
|
+
await self._ws_client.send_message(ws, {
|
|
131
|
+
"type": "resize",
|
|
132
|
+
"cols": cols,
|
|
133
|
+
"rows": rows
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
async def iter_output(
|
|
137
|
+
self,
|
|
138
|
+
ws: WebSocketClientProtocol
|
|
139
|
+
) -> AsyncIterator[Dict[str, Any]]:
|
|
140
|
+
"""
|
|
141
|
+
Iterate over terminal output messages.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
ws: WebSocket connection
|
|
145
|
+
|
|
146
|
+
Yields:
|
|
147
|
+
Message dictionaries:
|
|
148
|
+
- {"type": "output", "data": "..."}
|
|
149
|
+
- {"type": "exit", "code": 0}
|
|
150
|
+
|
|
151
|
+
Example:
|
|
152
|
+
>>> async for message in terminal.iter_output(ws):
|
|
153
|
+
... if message['type'] == 'output':
|
|
154
|
+
... print(message['data'], end='')
|
|
155
|
+
... elif message['type'] == 'exit':
|
|
156
|
+
... print(f"Exit code: {message['code']}")
|
|
157
|
+
... break
|
|
158
|
+
"""
|
|
159
|
+
async for message in self._ws_client.iter_messages(ws):
|
|
160
|
+
yield message
|
|
161
|
+
|
|
162
|
+
def __repr__(self) -> str:
|
|
163
|
+
return f"<Terminal ws_client={self._ws_client}>"
|
|
164
|
+
|