vswarm 0.1.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.
vswarm/__init__.py ADDED
@@ -0,0 +1,136 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ from vswarm.client import VSwarmClient
4
+ from vswarm.resources import (
5
+ ArtifactClient,
6
+ BrowserClient,
7
+ BrowserSessionHandle,
8
+ BrowserSessionManager,
9
+ EventClient,
10
+ FileClient,
11
+ RunClient,
12
+ SandboxHandle,
13
+ SandboxManager,
14
+ TemplateClient,
15
+ TraceClient,
16
+ TraceHandle,
17
+ TraceSession,
18
+ TraceStep,
19
+ )
20
+ from vswarm.models import (
21
+ Admission,
22
+ BrowserAction,
23
+ BrowserActionResult,
24
+ BrowserAgentObservation,
25
+ BrowserCapacity,
26
+ BrowserClick,
27
+ BrowserDiagnostics,
28
+ BrowserElement,
29
+ BrowserEvidenceCapture,
30
+ BrowserFill,
31
+ BrowserNavigate,
32
+ BrowserObservation,
33
+ BrowserObserveInclude,
34
+ BrowserPolicy,
35
+ BrowserSiteChallenge,
36
+ BrowserTargetResolution,
37
+ BrowserTiming,
38
+ CapacityError,
39
+ CleanupResult,
40
+ Metadata,
41
+ NetworkPolicy,
42
+ PolicyError,
43
+ RequestOptions,
44
+ ResourceLimits,
45
+ RetryPolicy,
46
+ RunResult,
47
+ Timeout,
48
+ TraceArtifact,
49
+ TraceEvent,
50
+ TraceExportArtifact,
51
+ TraceExportBundle,
52
+ TraceRun,
53
+ Viewport,
54
+ )
55
+ from vswarm.errors import (
56
+ VSwarmAPIError,
57
+ VSwarmAuthenticationError,
58
+ VSwarmAuthorizationError,
59
+ VSwarmCapacityError,
60
+ VSwarmConflictError,
61
+ VSwarmNotFoundError,
62
+ VSwarmPolicyError,
63
+ VSwarmQuotaError,
64
+ VSwarmTimeoutError,
65
+ VSwarmValidationError,
66
+ VSwarmVersionError,
67
+ )
68
+
69
+ try:
70
+ __version__ = version("vswarm")
71
+ except PackageNotFoundError:
72
+ # Support source-tree imports used by local conformance scripts.
73
+ __version__ = "0.0.0+unknown"
74
+
75
+ __all__ = [
76
+ "__version__",
77
+ "Admission",
78
+ "ArtifactClient",
79
+ "BrowserAction",
80
+ "BrowserActionResult",
81
+ "BrowserAgentObservation",
82
+ "BrowserCapacity",
83
+ "BrowserClick",
84
+ "BrowserClient",
85
+ "BrowserDiagnostics",
86
+ "BrowserElement",
87
+ "BrowserEvidenceCapture",
88
+ "BrowserFill",
89
+ "BrowserNavigate",
90
+ "BrowserObservation",
91
+ "BrowserObserveInclude",
92
+ "BrowserPolicy",
93
+ "BrowserSessionHandle",
94
+ "BrowserSessionManager",
95
+ "BrowserSiteChallenge",
96
+ "BrowserTargetResolution",
97
+ "BrowserTiming",
98
+ "CapacityError",
99
+ "CleanupResult",
100
+ "EventClient",
101
+ "FileClient",
102
+ "Metadata",
103
+ "NetworkPolicy",
104
+ "PolicyError",
105
+ "RequestOptions",
106
+ "ResourceLimits",
107
+ "RetryPolicy",
108
+ "RunClient",
109
+ "RunResult",
110
+ "SandboxHandle",
111
+ "SandboxManager",
112
+ "TemplateClient",
113
+ "Timeout",
114
+ "TraceArtifact",
115
+ "TraceClient",
116
+ "TraceEvent",
117
+ "TraceExportArtifact",
118
+ "TraceExportBundle",
119
+ "TraceHandle",
120
+ "TraceRun",
121
+ "TraceSession",
122
+ "TraceStep",
123
+ "VSwarmAPIError",
124
+ "VSwarmAuthenticationError",
125
+ "VSwarmAuthorizationError",
126
+ "VSwarmCapacityError",
127
+ "VSwarmConflictError",
128
+ "VSwarmClient",
129
+ "VSwarmNotFoundError",
130
+ "VSwarmPolicyError",
131
+ "VSwarmQuotaError",
132
+ "VSwarmTimeoutError",
133
+ "VSwarmValidationError",
134
+ "VSwarmVersionError",
135
+ "Viewport",
136
+ ]
vswarm/client.py ADDED
@@ -0,0 +1,193 @@
1
+ from __future__ import annotations
2
+
3
+ from contextvars import ContextVar, Token
4
+ from typing import Any, Mapping
5
+
6
+ from packaging.version import InvalidVersion, Version
7
+
8
+ from vswarm.errors import VSwarmVersionError
9
+ from vswarm.models import RequestOptions, to_api
10
+ from vswarm.resources import (
11
+ ArtifactClient,
12
+ BrowserClient,
13
+ EventClient,
14
+ PreviewAttachmentClient,
15
+ RunClient,
16
+ SandboxManager,
17
+ TemplateClient,
18
+ TraceClient,
19
+ WorkspaceManager,
20
+ )
21
+ from vswarm.transport import AsyncTransport, HTTPXTransport, decode_response
22
+
23
+
24
+ class VSwarmClient:
25
+ def __init__(
26
+ self,
27
+ *,
28
+ base_url: str = "http://127.0.0.1:8080",
29
+ token: str | None = None,
30
+ project: str | None = None,
31
+ user: str | None = None,
32
+ transport: AsyncTransport | None = None,
33
+ timeout: float = 60.0,
34
+ connect_timeout: float | None = None,
35
+ min_api_version: str | None = None,
36
+ compatibility: str = "off",
37
+ ):
38
+ self.base_url = base_url.rstrip("/")
39
+ self.token = token
40
+ self.project = project
41
+ self.user = user
42
+ self.timeout = timeout
43
+ self.connect_timeout = connect_timeout
44
+ self.min_api_version = min_api_version
45
+ self.compatibility = compatibility
46
+ self.transport = transport or HTTPXTransport(timeout=timeout, connect_timeout=connect_timeout)
47
+ self._trace_context: ContextVar[Any | None] = ContextVar("vswarm_trace_context", default=None)
48
+ self.templates = TemplateClient(self)
49
+ self.sandboxes = SandboxManager(self)
50
+ self.workspaces = WorkspaceManager(self)
51
+ self.artifacts = ArtifactClient(self)
52
+ self.browser = BrowserClient(self)
53
+ self.runs = RunClient(self)
54
+ self.events = EventClient(self)
55
+ self.preview_attachments = PreviewAttachmentClient(self)
56
+ self.traces = TraceClient(self)
57
+
58
+ async def close(self) -> None:
59
+ await self.transport.aclose()
60
+
61
+ async def __aenter__(self) -> "VSwarmClient":
62
+ if self.compatibility == "strict":
63
+ await self.check_compatibility()
64
+ return self
65
+
66
+ async def __aexit__(self, *_: object) -> None:
67
+ await self.close()
68
+
69
+ async def health(self) -> dict[str, Any]:
70
+ return await self._request("GET", "/v1/health")
71
+
72
+ async def server_info(self) -> dict[str, Any]:
73
+ return await self.health()
74
+
75
+ async def features(self) -> dict[str, Any]:
76
+ info = await self.server_info()
77
+ return dict(info.get("features", {}) if isinstance(info.get("features"), Mapping) else {})
78
+
79
+ async def check_compatibility(self) -> dict[str, Any]:
80
+ info = await self.server_info()
81
+ if self.min_api_version and info.get("api_version"):
82
+ current_version = _parse_api_version(str(info["api_version"]), "server")
83
+ minimum_version = _parse_api_version(self.min_api_version, "minimum")
84
+ if current_version < minimum_version:
85
+ raise VSwarmVersionError(
86
+ f"VSwarm API version {info['api_version']} is older than required {self.min_api_version}",
87
+ payload=info,
88
+ )
89
+ return info
90
+
91
+ async def browser_capacity(self) -> dict[str, Any]:
92
+ return await self._request("GET", "/v1/browser/capacity")
93
+
94
+ async def capacity(self) -> dict[str, Any]:
95
+ return await self._request("GET", "/v1/capacity")
96
+
97
+ async def reconcile_browser(self) -> dict[str, Any]:
98
+ return await self._request("POST", "/v1/browser/reconcile", json={})
99
+
100
+ def trace(self, **request: Any):
101
+ return self.traces.session(**request)
102
+
103
+ def browser_session(
104
+ self,
105
+ *,
106
+ template: str = "browser-chromium",
107
+ sandbox: Mapping[str, Any] | None = None,
108
+ **session: Any,
109
+ ):
110
+ return self.browser.session(template=template, sandbox=sandbox, **session)
111
+
112
+ async def request(
113
+ self,
114
+ method: str,
115
+ path: str,
116
+ *,
117
+ json: Any | None = None,
118
+ content: bytes | str | None = None,
119
+ headers: Mapping[str, str] | None = None,
120
+ options: RequestOptions | Mapping[str, Any] | None = None,
121
+ ) -> Any:
122
+ return await self._request(method, path, json=json, content=content, headers=headers, options=options)
123
+
124
+ async def _request(
125
+ self,
126
+ method: str,
127
+ path: str,
128
+ *,
129
+ json: Any | None = None,
130
+ content: bytes | str | None = None,
131
+ headers: Mapping[str, str] | None = None,
132
+ trace: Any | None = None,
133
+ options: RequestOptions | Mapping[str, Any] | None = None,
134
+ ) -> Any:
135
+ request_headers = self._headers(headers=headers, options=options, trace=trace)
136
+ response = await self.transport.request(
137
+ method,
138
+ self.base_url + path,
139
+ headers=request_headers,
140
+ json=to_api(json),
141
+ content=content,
142
+ )
143
+ return await decode_response(response)
144
+
145
+ def _headers(
146
+ self,
147
+ *,
148
+ headers: Mapping[str, str] | None = None,
149
+ options: RequestOptions | Mapping[str, Any] | None = None,
150
+ trace: Any | None = None,
151
+ ) -> dict[str, str]:
152
+ request_headers: dict[str, str] = {}
153
+ opts = _request_options(options)
154
+ if opts.headers:
155
+ request_headers.update(dict(opts.headers))
156
+ if headers:
157
+ request_headers.update(dict(headers))
158
+ if opts.request_id:
159
+ request_headers["X-VSwarm-Request-ID"] = opts.request_id
160
+ if self.token:
161
+ request_headers["Authorization"] = f"Bearer {self.token}"
162
+ if self.project:
163
+ request_headers["X-VSwarm-Project"] = self.project
164
+ if self.user:
165
+ request_headers["X-VSwarm-User"] = self.user
166
+ trace_context = trace or self._trace_context.get()
167
+ if trace_context is not None and hasattr(trace_context, "headers"):
168
+ request_headers.update(trace_context.headers())
169
+ return request_headers
170
+
171
+ def _push_trace(self, trace: Any) -> Token:
172
+ return self._trace_context.set(trace)
173
+
174
+ def _pop_trace(self, token: Token) -> None:
175
+ self._trace_context.reset(token)
176
+
177
+
178
+ def _request_options(options: RequestOptions | Mapping[str, Any] | None) -> RequestOptions:
179
+ if options is None:
180
+ return RequestOptions()
181
+ if isinstance(options, RequestOptions):
182
+ return options
183
+ return RequestOptions(
184
+ request_id=options.get("request_id"),
185
+ headers=options.get("headers"),
186
+ )
187
+
188
+
189
+ def _parse_api_version(value: str, source: str) -> Version:
190
+ try:
191
+ return Version(value)
192
+ except InvalidVersion as exc:
193
+ raise VSwarmVersionError(f"Invalid {source} VSwarm API version: {value}") from exc
vswarm/errors.py ADDED
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ class VSwarmAPIError(RuntimeError):
7
+ def __init__(
8
+ self,
9
+ message: str,
10
+ *,
11
+ status_code: int | None = None,
12
+ payload: object | None = None,
13
+ request_id: str | None = None,
14
+ retry_after_seconds: float | None = None,
15
+ capacity: object | None = None,
16
+ resource_id: str | None = None,
17
+ operation_id: str | None = None,
18
+ trace_id: str | None = None,
19
+ ):
20
+ super().__init__(message)
21
+ self.status_code = status_code
22
+ self.payload = payload
23
+ self.request_id = request_id
24
+ self.trace_id = trace_id
25
+ self.retry_after_seconds = retry_after_seconds
26
+ self.capacity = capacity
27
+ self.resource_id = resource_id
28
+ self.operation_id = operation_id
29
+
30
+
31
+ class VSwarmValidationError(VSwarmAPIError):
32
+ pass
33
+
34
+
35
+ class VSwarmAuthenticationError(VSwarmAPIError):
36
+ pass
37
+
38
+
39
+ class VSwarmAuthorizationError(VSwarmAPIError):
40
+ pass
41
+
42
+
43
+ class VSwarmNotFoundError(VSwarmAPIError):
44
+ pass
45
+
46
+
47
+ class VSwarmCapacityError(VSwarmAPIError):
48
+ pass
49
+
50
+
51
+ class VSwarmQuotaError(VSwarmAPIError):
52
+ pass
53
+
54
+
55
+ class VSwarmPolicyError(VSwarmAPIError):
56
+ pass
57
+
58
+
59
+ class VSwarmTimeoutError(VSwarmAPIError):
60
+ pass
61
+
62
+
63
+ class VSwarmConflictError(VSwarmAPIError):
64
+ pass
65
+
66
+
67
+ class VSwarmVersionError(VSwarmAPIError):
68
+ pass
69
+
70
+
71
+ def payload_field(payload: object, *names: str) -> Any | None:
72
+ if not isinstance(payload, dict):
73
+ return None
74
+ for name in names:
75
+ value = payload.get(name)
76
+ if value is not None:
77
+ return value
78
+ return None
@@ -0,0 +1,77 @@
1
+ from vswarm.models.artifacts import ArtifactRef
2
+ from vswarm.models.browser import (
3
+ BrowserAction,
4
+ BrowserActionResult,
5
+ BrowserAgentObservation,
6
+ BrowserCapacity,
7
+ BrowserClick,
8
+ BrowserDiagnostics,
9
+ BrowserElement,
10
+ BrowserEvidenceCapture,
11
+ BrowserFill,
12
+ BrowserNavigate,
13
+ BrowserObservation,
14
+ BrowserObserveInclude,
15
+ BrowserSiteChallenge,
16
+ BrowserTargetResolution,
17
+ BrowserTiming,
18
+ CapacityError,
19
+ PolicyError,
20
+ Viewport,
21
+ )
22
+ from vswarm.models.common import (
23
+ Admission,
24
+ CleanupResult,
25
+ Metadata,
26
+ NetworkPolicy,
27
+ RequestOptions,
28
+ ResourceLimits,
29
+ RetryPolicy,
30
+ Timeout,
31
+ clean_mapping,
32
+ encode_base64,
33
+ to_api,
34
+ )
35
+ from vswarm.models.policy import BrowserPolicy
36
+ from vswarm.models.runs import RunResult
37
+ from vswarm.models.traces import TraceArtifact, TraceEvent, TraceExportArtifact, TraceExportBundle, TraceRun
38
+
39
+ __all__ = [
40
+ "Admission",
41
+ "ArtifactRef",
42
+ "BrowserAction",
43
+ "BrowserActionResult",
44
+ "BrowserAgentObservation",
45
+ "BrowserCapacity",
46
+ "BrowserClick",
47
+ "BrowserDiagnostics",
48
+ "BrowserElement",
49
+ "BrowserEvidenceCapture",
50
+ "BrowserFill",
51
+ "BrowserNavigate",
52
+ "BrowserObservation",
53
+ "BrowserObserveInclude",
54
+ "BrowserPolicy",
55
+ "BrowserSiteChallenge",
56
+ "BrowserTargetResolution",
57
+ "BrowserTiming",
58
+ "CapacityError",
59
+ "CleanupResult",
60
+ "Metadata",
61
+ "NetworkPolicy",
62
+ "PolicyError",
63
+ "RequestOptions",
64
+ "ResourceLimits",
65
+ "RetryPolicy",
66
+ "RunResult",
67
+ "Timeout",
68
+ "TraceArtifact",
69
+ "TraceEvent",
70
+ "TraceExportArtifact",
71
+ "TraceExportBundle",
72
+ "TraceRun",
73
+ "Viewport",
74
+ "clean_mapping",
75
+ "encode_base64",
76
+ "to_api",
77
+ ]
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Mapping
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class ArtifactRef:
9
+ id: str
10
+ kind: str | None = None
11
+ path: str | None = None
12
+ content_type: str | None = None
13
+ size_bytes: int | None = None
14
+ raw: Mapping[str, Any] | None = None
15
+
16
+ @classmethod
17
+ def from_api(cls, data: Mapping[str, Any]) -> "ArtifactRef":
18
+ return cls(
19
+ id=str(data.get("id", "")),
20
+ kind=data.get("kind"),
21
+ path=data.get("path"),
22
+ content_type=data.get("content_type"),
23
+ size_bytes=data.get("size_bytes"),
24
+ raw=data,
25
+ )