agentrun-sdk 0.1.2__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 agentrun-sdk might be problematic. Click here for more details.
- agentrun_operation_sdk/cli/__init__.py +1 -0
- agentrun_operation_sdk/cli/cli.py +19 -0
- agentrun_operation_sdk/cli/common.py +21 -0
- agentrun_operation_sdk/cli/runtime/__init__.py +1 -0
- agentrun_operation_sdk/cli/runtime/commands.py +203 -0
- agentrun_operation_sdk/client/client.py +75 -0
- agentrun_operation_sdk/operations/runtime/__init__.py +8 -0
- agentrun_operation_sdk/operations/runtime/configure.py +101 -0
- agentrun_operation_sdk/operations/runtime/launch.py +82 -0
- agentrun_operation_sdk/operations/runtime/models.py +31 -0
- agentrun_operation_sdk/services/runtime.py +152 -0
- agentrun_operation_sdk/utils/logging_config.py +72 -0
- agentrun_operation_sdk/utils/runtime/config.py +94 -0
- agentrun_operation_sdk/utils/runtime/container.py +280 -0
- agentrun_operation_sdk/utils/runtime/entrypoint.py +203 -0
- agentrun_operation_sdk/utils/runtime/schema.py +56 -0
- agentrun_sdk/__init__.py +7 -0
- agentrun_sdk/agent/__init__.py +25 -0
- agentrun_sdk/agent/agent.py +696 -0
- agentrun_sdk/agent/agent_result.py +46 -0
- agentrun_sdk/agent/conversation_manager/__init__.py +26 -0
- agentrun_sdk/agent/conversation_manager/conversation_manager.py +88 -0
- agentrun_sdk/agent/conversation_manager/null_conversation_manager.py +46 -0
- agentrun_sdk/agent/conversation_manager/sliding_window_conversation_manager.py +179 -0
- agentrun_sdk/agent/conversation_manager/summarizing_conversation_manager.py +252 -0
- agentrun_sdk/agent/state.py +97 -0
- agentrun_sdk/event_loop/__init__.py +9 -0
- agentrun_sdk/event_loop/event_loop.py +499 -0
- agentrun_sdk/event_loop/streaming.py +319 -0
- agentrun_sdk/experimental/__init__.py +4 -0
- agentrun_sdk/experimental/hooks/__init__.py +15 -0
- agentrun_sdk/experimental/hooks/events.py +123 -0
- agentrun_sdk/handlers/__init__.py +10 -0
- agentrun_sdk/handlers/callback_handler.py +70 -0
- agentrun_sdk/hooks/__init__.py +49 -0
- agentrun_sdk/hooks/events.py +80 -0
- agentrun_sdk/hooks/registry.py +247 -0
- agentrun_sdk/models/__init__.py +10 -0
- agentrun_sdk/models/anthropic.py +432 -0
- agentrun_sdk/models/bedrock.py +649 -0
- agentrun_sdk/models/litellm.py +225 -0
- agentrun_sdk/models/llamaapi.py +438 -0
- agentrun_sdk/models/mistral.py +539 -0
- agentrun_sdk/models/model.py +95 -0
- agentrun_sdk/models/ollama.py +357 -0
- agentrun_sdk/models/openai.py +436 -0
- agentrun_sdk/models/sagemaker.py +598 -0
- agentrun_sdk/models/writer.py +449 -0
- agentrun_sdk/multiagent/__init__.py +22 -0
- agentrun_sdk/multiagent/a2a/__init__.py +15 -0
- agentrun_sdk/multiagent/a2a/executor.py +148 -0
- agentrun_sdk/multiagent/a2a/server.py +252 -0
- agentrun_sdk/multiagent/base.py +92 -0
- agentrun_sdk/multiagent/graph.py +555 -0
- agentrun_sdk/multiagent/swarm.py +656 -0
- agentrun_sdk/py.typed +1 -0
- agentrun_sdk/session/__init__.py +18 -0
- agentrun_sdk/session/file_session_manager.py +216 -0
- agentrun_sdk/session/repository_session_manager.py +152 -0
- agentrun_sdk/session/s3_session_manager.py +272 -0
- agentrun_sdk/session/session_manager.py +73 -0
- agentrun_sdk/session/session_repository.py +51 -0
- agentrun_sdk/telemetry/__init__.py +21 -0
- agentrun_sdk/telemetry/config.py +194 -0
- agentrun_sdk/telemetry/metrics.py +476 -0
- agentrun_sdk/telemetry/metrics_constants.py +15 -0
- agentrun_sdk/telemetry/tracer.py +563 -0
- agentrun_sdk/tools/__init__.py +17 -0
- agentrun_sdk/tools/decorator.py +569 -0
- agentrun_sdk/tools/executor.py +137 -0
- agentrun_sdk/tools/loader.py +152 -0
- agentrun_sdk/tools/mcp/__init__.py +13 -0
- agentrun_sdk/tools/mcp/mcp_agent_tool.py +99 -0
- agentrun_sdk/tools/mcp/mcp_client.py +423 -0
- agentrun_sdk/tools/mcp/mcp_instrumentation.py +322 -0
- agentrun_sdk/tools/mcp/mcp_types.py +63 -0
- agentrun_sdk/tools/registry.py +607 -0
- agentrun_sdk/tools/structured_output.py +421 -0
- agentrun_sdk/tools/tools.py +217 -0
- agentrun_sdk/tools/watcher.py +136 -0
- agentrun_sdk/types/__init__.py +5 -0
- agentrun_sdk/types/collections.py +23 -0
- agentrun_sdk/types/content.py +188 -0
- agentrun_sdk/types/event_loop.py +48 -0
- agentrun_sdk/types/exceptions.py +81 -0
- agentrun_sdk/types/guardrails.py +254 -0
- agentrun_sdk/types/media.py +89 -0
- agentrun_sdk/types/session.py +152 -0
- agentrun_sdk/types/streaming.py +201 -0
- agentrun_sdk/types/tools.py +258 -0
- agentrun_sdk/types/traces.py +5 -0
- agentrun_sdk-0.1.2.dist-info/METADATA +51 -0
- agentrun_sdk-0.1.2.dist-info/RECORD +115 -0
- agentrun_sdk-0.1.2.dist-info/WHEEL +5 -0
- agentrun_sdk-0.1.2.dist-info/entry_points.txt +2 -0
- agentrun_sdk-0.1.2.dist-info/top_level.txt +3 -0
- agentrun_wrapper/__init__.py +11 -0
- agentrun_wrapper/_utils/__init__.py +6 -0
- agentrun_wrapper/_utils/endpoints.py +16 -0
- agentrun_wrapper/identity/__init__.py +5 -0
- agentrun_wrapper/identity/auth.py +211 -0
- agentrun_wrapper/memory/__init__.py +6 -0
- agentrun_wrapper/memory/client.py +1697 -0
- agentrun_wrapper/memory/constants.py +103 -0
- agentrun_wrapper/memory/controlplane.py +626 -0
- agentrun_wrapper/py.typed +1 -0
- agentrun_wrapper/runtime/__init__.py +13 -0
- agentrun_wrapper/runtime/app.py +473 -0
- agentrun_wrapper/runtime/context.py +34 -0
- agentrun_wrapper/runtime/models.py +25 -0
- agentrun_wrapper/services/__init__.py +1 -0
- agentrun_wrapper/services/identity.py +192 -0
- agentrun_wrapper/tools/__init__.py +6 -0
- agentrun_wrapper/tools/browser_client.py +325 -0
- agentrun_wrapper/tools/code_interpreter_client.py +186 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""The main high-level client for the Bedrock AgentCore Identity service."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import time
|
|
6
|
+
import uuid
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from typing import Any, Callable, Dict, List, Literal, Optional
|
|
9
|
+
|
|
10
|
+
import boto3
|
|
11
|
+
|
|
12
|
+
from bedrock_agentcore._utils.endpoints import get_control_plane_endpoint, get_data_plane_endpoint
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TokenPoller(ABC):
|
|
16
|
+
"""Abstract base class for token polling implementations."""
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
async def poll_for_token(self) -> str:
|
|
20
|
+
"""Poll for a token and return it when available."""
|
|
21
|
+
raise NotImplementedError
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Default configuration for the polling mechanism
|
|
25
|
+
DEFAULT_POLLING_INTERVAL_SECONDS = 5
|
|
26
|
+
DEFAULT_POLLING_TIMEOUT_SECONDS = 600
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _DefaultApiTokenPoller(TokenPoller):
|
|
30
|
+
"""Default implementation of token polling."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, auth_url: str, func: Callable[[], str | None]):
|
|
33
|
+
"""Initialize the token poller with auth URL and polling function."""
|
|
34
|
+
self.auth_url = auth_url
|
|
35
|
+
self.polling_func = func
|
|
36
|
+
self.logger = logging.getLogger("bedrock_agentcore.default_token_poller")
|
|
37
|
+
self.logger.setLevel("INFO")
|
|
38
|
+
if not self.logger.handlers:
|
|
39
|
+
self.logger.addHandler(logging.StreamHandler())
|
|
40
|
+
|
|
41
|
+
async def poll_for_token(self) -> str:
|
|
42
|
+
"""Poll for a token until it becomes available or timeout occurs."""
|
|
43
|
+
start_time = time.time()
|
|
44
|
+
while time.time() - start_time < DEFAULT_POLLING_TIMEOUT_SECONDS:
|
|
45
|
+
await asyncio.sleep(DEFAULT_POLLING_INTERVAL_SECONDS)
|
|
46
|
+
|
|
47
|
+
self.logger.info("Polling for token for authorization url: %s", self.auth_url)
|
|
48
|
+
resp = self.polling_func()
|
|
49
|
+
if resp is not None:
|
|
50
|
+
self.logger.info("Token is ready")
|
|
51
|
+
return resp
|
|
52
|
+
|
|
53
|
+
raise asyncio.TimeoutError(
|
|
54
|
+
f"Polling timed out after {DEFAULT_POLLING_TIMEOUT_SECONDS} seconds. "
|
|
55
|
+
+ "User may not have completed authorization."
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class IdentityClient:
|
|
60
|
+
"""A high-level client for Bedrock AgentCore Identity."""
|
|
61
|
+
|
|
62
|
+
def __init__(self, region: str):
|
|
63
|
+
"""Initialize the identity client with the specified region."""
|
|
64
|
+
self.region = region
|
|
65
|
+
self.cp_client = boto3.client(
|
|
66
|
+
"bedrock-agentcore-control", region_name=region, endpoint_url=get_control_plane_endpoint(region)
|
|
67
|
+
)
|
|
68
|
+
self.identity_client = boto3.client(
|
|
69
|
+
"bedrock-agentcore-control", region_name=region, endpoint_url=get_data_plane_endpoint(region)
|
|
70
|
+
)
|
|
71
|
+
self.dp_client = boto3.client(
|
|
72
|
+
"bedrock-agentcore", region_name=region, endpoint_url=get_data_plane_endpoint(region)
|
|
73
|
+
)
|
|
74
|
+
self.logger = logging.getLogger("bedrock_agentcore.identity_client")
|
|
75
|
+
|
|
76
|
+
def create_oauth2_credential_provider(self, req):
|
|
77
|
+
"""Create an OAuth2 credential provider."""
|
|
78
|
+
self.logger.info("Creating OAuth2 credential provider...")
|
|
79
|
+
return self.cp_client.create_oauth2_credential_provider(**req)
|
|
80
|
+
|
|
81
|
+
def create_api_key_credential_provider(self, req):
|
|
82
|
+
"""Create an API key credential provider."""
|
|
83
|
+
self.logger.info("Creating API key credential provider...")
|
|
84
|
+
return self.cp_client.create_api_key_credential_provider(**req)
|
|
85
|
+
|
|
86
|
+
def get_workload_access_token(
|
|
87
|
+
self, workload_name: str, user_token: Optional[str] = None, user_id: Optional[str] = None
|
|
88
|
+
) -> Dict:
|
|
89
|
+
"""Get a workload access token using workload name and optionally user token."""
|
|
90
|
+
if user_token:
|
|
91
|
+
if user_id is not None:
|
|
92
|
+
self.logger.warning("Both user token and user id are supplied, using user token")
|
|
93
|
+
self.logger.info("Getting workload access token for JWT...")
|
|
94
|
+
resp = self.dp_client.get_workload_access_token_for_jwt(workloadName=workload_name, userToken=user_token)
|
|
95
|
+
elif user_id:
|
|
96
|
+
self.logger.info("Getting workload access token for user id...")
|
|
97
|
+
resp = self.dp_client.get_workload_access_token_for_user_id(workloadName=workload_name, userId=user_id)
|
|
98
|
+
else:
|
|
99
|
+
self.logger.info("Getting workload access token...")
|
|
100
|
+
resp = self.dp_client.get_workload_access_token(workloadName=workload_name)
|
|
101
|
+
|
|
102
|
+
self.logger.info("Successfully retrieved workload access token")
|
|
103
|
+
return resp
|
|
104
|
+
|
|
105
|
+
def create_workload_identity(self, name: Optional[str] = None) -> Dict:
|
|
106
|
+
"""Create workload identity with optional name."""
|
|
107
|
+
self.logger.info("Creating workload identity...")
|
|
108
|
+
if not name:
|
|
109
|
+
name = f"workload-{uuid.uuid4().hex[:8]}"
|
|
110
|
+
return self.identity_client.create_workload_identity(name=name)
|
|
111
|
+
|
|
112
|
+
async def get_token(
|
|
113
|
+
self,
|
|
114
|
+
*,
|
|
115
|
+
provider_name: str,
|
|
116
|
+
scopes: Optional[List[str]] = None,
|
|
117
|
+
agent_identity_token: str,
|
|
118
|
+
on_auth_url: Optional[Callable[[str], Any]] = None,
|
|
119
|
+
auth_flow: Literal["M2M", "USER_FEDERATION"],
|
|
120
|
+
callback_url: Optional[str] = None,
|
|
121
|
+
force_authentication: bool = False,
|
|
122
|
+
token_poller: Optional[TokenPoller] = None,
|
|
123
|
+
) -> str:
|
|
124
|
+
"""Get an OAuth2 access token for the specified provider.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
provider_name: The credential provider name
|
|
128
|
+
scopes: Optional list of OAuth2 scopes to request
|
|
129
|
+
agent_identity_token: Agent identity token for authentication
|
|
130
|
+
on_auth_url: Callback for handling authorization URLs
|
|
131
|
+
auth_flow: Authentication flow type ("M2M" or "USER_FEDERATION")
|
|
132
|
+
callback_url: OAuth2 callback URL (must be pre-registered)
|
|
133
|
+
force_authentication: Force re-authentication even if token exists in the token vault
|
|
134
|
+
token_poller: Custom token poller implementation
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
The access token string
|
|
138
|
+
|
|
139
|
+
Raises:
|
|
140
|
+
RequiresUserConsentException: When user consent is needed
|
|
141
|
+
Various other exceptions for error conditions
|
|
142
|
+
"""
|
|
143
|
+
self.logger.info("Getting OAuth2 token...")
|
|
144
|
+
|
|
145
|
+
# Build parameters
|
|
146
|
+
req = {
|
|
147
|
+
"resourceCredentialProviderName": provider_name,
|
|
148
|
+
"scopes": scopes,
|
|
149
|
+
"oauth2Flow": auth_flow,
|
|
150
|
+
"workloadIdentityToken": agent_identity_token,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
# Add optional parameters
|
|
154
|
+
if callback_url:
|
|
155
|
+
req["resourceOauth2ReturnUrl"] = callback_url
|
|
156
|
+
if force_authentication:
|
|
157
|
+
req["forceAuthentication"] = force_authentication
|
|
158
|
+
|
|
159
|
+
response = self.dp_client.get_resource_oauth2_token(**req)
|
|
160
|
+
|
|
161
|
+
# If we got a token directly, return it
|
|
162
|
+
if "accessToken" in response:
|
|
163
|
+
return response["accessToken"]
|
|
164
|
+
|
|
165
|
+
# If we got an authorization URL, handle the OAuth flow
|
|
166
|
+
if "authorizationUrl" in response:
|
|
167
|
+
auth_url = response["authorizationUrl"]
|
|
168
|
+
# Notify about the auth URL if callback provided
|
|
169
|
+
if on_auth_url:
|
|
170
|
+
if asyncio.iscoroutinefunction(on_auth_url):
|
|
171
|
+
await on_auth_url(auth_url)
|
|
172
|
+
else:
|
|
173
|
+
on_auth_url(auth_url)
|
|
174
|
+
|
|
175
|
+
# only the initial request should have force authentication
|
|
176
|
+
if force_authentication:
|
|
177
|
+
req["forceAuthentication"] = False
|
|
178
|
+
|
|
179
|
+
# Poll for the token
|
|
180
|
+
active_poller = token_poller or _DefaultApiTokenPoller(
|
|
181
|
+
auth_url, lambda: self.dp_client.get_resource_oauth2_token(**req).get("accessToken", None)
|
|
182
|
+
)
|
|
183
|
+
return await active_poller.poll_for_token()
|
|
184
|
+
|
|
185
|
+
raise RuntimeError("Identity service did not return a token or an authorization URL.")
|
|
186
|
+
|
|
187
|
+
async def get_api_key(self, *, provider_name: str, agent_identity_token: str) -> str:
|
|
188
|
+
"""Programmatically retrieves an API key from the Identity service."""
|
|
189
|
+
self.logger.info("Getting API key...")
|
|
190
|
+
req = {"resourceCredentialProviderName": provider_name, "workloadIdentityToken": agent_identity_token}
|
|
191
|
+
|
|
192
|
+
return self.dp_client.get_resource_api_key(**req)["apiKey"]
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"""Client for interacting with the Browser sandbox service.
|
|
2
|
+
|
|
3
|
+
This module provides a client for the AWS Browser sandbox, allowing
|
|
4
|
+
applications to start, stop, and automate browser interactions in a managed
|
|
5
|
+
sandbox environment using Playwright.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import base64
|
|
9
|
+
import datetime
|
|
10
|
+
import logging
|
|
11
|
+
import secrets
|
|
12
|
+
import uuid
|
|
13
|
+
from contextlib import contextmanager
|
|
14
|
+
from typing import Dict, Generator, Optional, Tuple
|
|
15
|
+
from urllib.parse import urlparse
|
|
16
|
+
|
|
17
|
+
import boto3
|
|
18
|
+
from botocore.auth import SigV4Auth, SigV4QueryAuth
|
|
19
|
+
from botocore.awsrequest import AWSRequest
|
|
20
|
+
|
|
21
|
+
from .._utils.endpoints import (
|
|
22
|
+
get_data_plane_endpoint,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
DEFAULT_IDENTIFIER = "aws.browser.v1"
|
|
26
|
+
DEFAULT_SESSION_TIMEOUT = 3600
|
|
27
|
+
DEFAULT_LIVE_VIEW_PRESIGNED_URL_TIMEOUT = 300
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class BrowserClient:
|
|
31
|
+
"""Client for interacting with the AWS Browser sandbox service.
|
|
32
|
+
|
|
33
|
+
This client handles the session lifecycle and browser automation for
|
|
34
|
+
Browser sandboxes, providing an interface to perform web automation
|
|
35
|
+
tasks in a secure, managed environment.
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
region (str): The AWS region being used.
|
|
39
|
+
data_plane_service_name (str): AWS service name for the data plane.
|
|
40
|
+
client: The boto3 client for interacting with the service.
|
|
41
|
+
identifier (str, optional): The browser identifier.
|
|
42
|
+
session_id (str, optional): The active session ID.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, region: str) -> None:
|
|
46
|
+
"""Initialize a Browser client for the specified AWS region.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
region (str): The AWS region to use for the Browser service.
|
|
50
|
+
"""
|
|
51
|
+
self.region = region
|
|
52
|
+
self.data_plane_service_name = "bedrock-agentcore"
|
|
53
|
+
self.client = boto3.client(
|
|
54
|
+
self.data_plane_service_name, region_name=region, endpoint_url=get_data_plane_endpoint(region)
|
|
55
|
+
)
|
|
56
|
+
self._identifier = None
|
|
57
|
+
self._session_id = None
|
|
58
|
+
self.logger = logging.getLogger(__name__)
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def identifier(self) -> Optional[str]:
|
|
62
|
+
"""Get the current browser identifier.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Optional[str]: The current identifier or None if not set.
|
|
66
|
+
"""
|
|
67
|
+
return self._identifier
|
|
68
|
+
|
|
69
|
+
@identifier.setter
|
|
70
|
+
def identifier(self, value: Optional[str]):
|
|
71
|
+
"""Set the browser identifier.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
value (Optional[str]): The identifier to set.
|
|
75
|
+
"""
|
|
76
|
+
self._identifier = value
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def session_id(self) -> Optional[str]:
|
|
80
|
+
"""Get the current session ID.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Optional[str]: The current session ID or None if not set.
|
|
84
|
+
"""
|
|
85
|
+
return self._session_id
|
|
86
|
+
|
|
87
|
+
@session_id.setter
|
|
88
|
+
def session_id(self, value: Optional[str]):
|
|
89
|
+
"""Set the session ID.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
value (Optional[str]): The session ID to set.
|
|
93
|
+
"""
|
|
94
|
+
self._session_id = value
|
|
95
|
+
|
|
96
|
+
def start(
|
|
97
|
+
self,
|
|
98
|
+
identifier: Optional[str] = DEFAULT_IDENTIFIER,
|
|
99
|
+
name: Optional[str] = None,
|
|
100
|
+
session_timeout_seconds: Optional[int] = DEFAULT_SESSION_TIMEOUT,
|
|
101
|
+
) -> str:
|
|
102
|
+
"""Start a browser sandbox session.
|
|
103
|
+
|
|
104
|
+
This method initializes a new browser session with the provided parameters.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
identifier (Optional[str]): The browser sandbox identifier to use.
|
|
108
|
+
Defaults to DEFAULT_IDENTIFIER.
|
|
109
|
+
name (Optional[str]): A name for this session. If not provided, a name
|
|
110
|
+
will be generated using a UUID.
|
|
111
|
+
session_timeout_seconds (Optional[int]): The timeout for the session in seconds.
|
|
112
|
+
Defaults to DEFAULT_TIMEOUT.
|
|
113
|
+
description (Optional[str]): A description for this session.
|
|
114
|
+
Defaults to an empty string.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
str: The session ID of the newly created session.
|
|
118
|
+
"""
|
|
119
|
+
self.logger.info("Starting browser session...")
|
|
120
|
+
|
|
121
|
+
response = self.client.start_browser_session(
|
|
122
|
+
browserIdentifier=identifier,
|
|
123
|
+
name=name or f"browser-session-{uuid.uuid4().hex[:8]}",
|
|
124
|
+
sessionTimeoutSeconds=session_timeout_seconds,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
self.identifier = response["browserIdentifier"]
|
|
128
|
+
self.session_id = response["sessionId"]
|
|
129
|
+
|
|
130
|
+
return self.session_id
|
|
131
|
+
|
|
132
|
+
def stop(self):
|
|
133
|
+
"""Stop the current browser session if one is active.
|
|
134
|
+
|
|
135
|
+
This method stops any active session and clears the session state.
|
|
136
|
+
If no session is active, this method does nothing.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
bool: True if no session was active or the session was successfully stopped.
|
|
140
|
+
"""
|
|
141
|
+
self.logger.info("Stopping browser session...")
|
|
142
|
+
|
|
143
|
+
if not self.session_id or not self.identifier:
|
|
144
|
+
return True
|
|
145
|
+
|
|
146
|
+
self.client.stop_browser_session(**{"browserIdentifier": self.identifier, "sessionId": self.session_id})
|
|
147
|
+
|
|
148
|
+
self.identifier = None
|
|
149
|
+
self.session_id = None
|
|
150
|
+
|
|
151
|
+
def generate_ws_headers(self) -> Tuple[str, Dict[str, str]]:
|
|
152
|
+
"""Generate the WebSocket headers needed for connecting to the browser sandbox.
|
|
153
|
+
|
|
154
|
+
This method creates properly signed WebSocket headers for connecting to
|
|
155
|
+
the browser automation endpoint.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Tuple[str, Dict[str, str]]: A tuple containing the WebSocket URL and
|
|
159
|
+
the headers dictionary.
|
|
160
|
+
|
|
161
|
+
Raises:
|
|
162
|
+
RuntimeError: If no AWS credentials are found.
|
|
163
|
+
"""
|
|
164
|
+
self.logger.info("Generating websocket headers...")
|
|
165
|
+
|
|
166
|
+
if not self.identifier or not self.session_id:
|
|
167
|
+
self.start()
|
|
168
|
+
|
|
169
|
+
host = get_data_plane_endpoint(self.region).replace("https://", "")
|
|
170
|
+
path = f"/browser-streams/{self.identifier}/sessions/{self.session_id}/automation"
|
|
171
|
+
ws_url = f"wss://{host}{path}"
|
|
172
|
+
|
|
173
|
+
boto_session = boto3.Session()
|
|
174
|
+
credentials = boto_session.get_credentials()
|
|
175
|
+
if not credentials:
|
|
176
|
+
raise RuntimeError("No AWS credentials found")
|
|
177
|
+
|
|
178
|
+
frozen_credentials = credentials.get_frozen_credentials()
|
|
179
|
+
|
|
180
|
+
request = AWSRequest(
|
|
181
|
+
method="GET",
|
|
182
|
+
url=f"https://{host}{path}",
|
|
183
|
+
headers={
|
|
184
|
+
"host": host,
|
|
185
|
+
"x-amz-date": datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%dT%H%M%SZ"),
|
|
186
|
+
},
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
auth = SigV4Auth(frozen_credentials, self.data_plane_service_name, self.region)
|
|
190
|
+
auth.add_auth(request)
|
|
191
|
+
|
|
192
|
+
headers = {
|
|
193
|
+
"Host": host,
|
|
194
|
+
"X-Amz-Date": request.headers["x-amz-date"],
|
|
195
|
+
"Authorization": request.headers["Authorization"],
|
|
196
|
+
"Upgrade": "websocket",
|
|
197
|
+
"Connection": "Upgrade",
|
|
198
|
+
"Sec-WebSocket-Version": "13",
|
|
199
|
+
"Sec-WebSocket-Key": base64.b64encode(secrets.token_bytes(16)).decode(),
|
|
200
|
+
"User-Agent": f"BrowserSandbox-Client/1.0 (Session: {self.session_id})",
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if frozen_credentials.token:
|
|
204
|
+
headers["X-Amz-Security-Token"] = frozen_credentials.token
|
|
205
|
+
|
|
206
|
+
return ws_url, headers
|
|
207
|
+
|
|
208
|
+
def generate_live_view_url(self, expires: int = DEFAULT_LIVE_VIEW_PRESIGNED_URL_TIMEOUT) -> str:
|
|
209
|
+
"""Generate a pre-signed URL for viewing the browser session.
|
|
210
|
+
|
|
211
|
+
Creates a pre-signed URL that can be used to view the current browser session.
|
|
212
|
+
If no session is active, a new session will be started.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
expires (int, optional): The number of seconds until the pre-signed URL expires.
|
|
216
|
+
Defaults to DEFAULT_LIVE_VIEW_PRESIGNED_URL_TIMEOUT (300 seconds).
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
str: The pre-signed URL for viewing the browser session.
|
|
220
|
+
|
|
221
|
+
Raises:
|
|
222
|
+
RuntimeError: If the URL generation fails.
|
|
223
|
+
"""
|
|
224
|
+
self.logger.info("Generating live view url...")
|
|
225
|
+
|
|
226
|
+
if not self.identifier or not self.session_id:
|
|
227
|
+
self.start()
|
|
228
|
+
|
|
229
|
+
url = urlparse(
|
|
230
|
+
f"{get_data_plane_endpoint(self.region)}/browser-streams/{self.identifier}/sessions/{self.session_id}/live-view"
|
|
231
|
+
)
|
|
232
|
+
boto_session = boto3.Session()
|
|
233
|
+
credentials = boto_session.get_credentials().get_frozen_credentials()
|
|
234
|
+
request = AWSRequest(method="GET", url=url.geturl(), headers={"host": url.hostname})
|
|
235
|
+
signer = SigV4QueryAuth(
|
|
236
|
+
credentials=credentials, service_name=self.data_plane_service_name, region_name=self.region, expires=expires
|
|
237
|
+
)
|
|
238
|
+
signer.add_auth(request)
|
|
239
|
+
|
|
240
|
+
if not request.url:
|
|
241
|
+
raise RuntimeError("Failed to generate live view url")
|
|
242
|
+
|
|
243
|
+
return request.url
|
|
244
|
+
|
|
245
|
+
def take_control(self):
|
|
246
|
+
"""Take control of the browser session by disabling the automation stream.
|
|
247
|
+
|
|
248
|
+
This method disables external automation capabilities of the browser session,
|
|
249
|
+
giving this client exclusive control. If no session is active, a new session
|
|
250
|
+
will be started.
|
|
251
|
+
|
|
252
|
+
Raises:
|
|
253
|
+
RuntimeError: If a session could not be found or started.
|
|
254
|
+
"""
|
|
255
|
+
self.logger.info("Taking control of browser session...")
|
|
256
|
+
|
|
257
|
+
if not self.identifier or not self.session_id:
|
|
258
|
+
self.start()
|
|
259
|
+
|
|
260
|
+
if not self.identifier or not self.session_id:
|
|
261
|
+
raise RuntimeError("Could not find or start a browser session")
|
|
262
|
+
|
|
263
|
+
self._update_browser_stream(self.identifier, self.session_id, "DISABLED")
|
|
264
|
+
|
|
265
|
+
def release_control(self):
|
|
266
|
+
"""Release control of the browser session by enabling the automation stream.
|
|
267
|
+
|
|
268
|
+
This method enables external automation capabilities of the browser session,
|
|
269
|
+
relinquishing exclusive control. If no session exists, a warning is logged
|
|
270
|
+
and the method returns without taking action.
|
|
271
|
+
"""
|
|
272
|
+
self.logger.info("Releasing control of browser session...")
|
|
273
|
+
|
|
274
|
+
if not self.identifier or not self.session_id:
|
|
275
|
+
self.logger.warning("Could not find a browser session when releasing control")
|
|
276
|
+
return
|
|
277
|
+
|
|
278
|
+
self._update_browser_stream(self.identifier, self.session_id, "ENABLED")
|
|
279
|
+
|
|
280
|
+
def _update_browser_stream(self, identifier: str, session_id: str, stream_status: str) -> None:
|
|
281
|
+
"""Update the browser stream status.
|
|
282
|
+
|
|
283
|
+
This private helper method updates the status of the browser automation stream.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
identifier (str): The browser identifier.
|
|
287
|
+
session_id (str): The session ID.
|
|
288
|
+
stream_status (str): The status to set for the automation stream.
|
|
289
|
+
Valid values are "ENABLED" or "DISABLED".
|
|
290
|
+
"""
|
|
291
|
+
self.client.update_browser_stream(
|
|
292
|
+
**{
|
|
293
|
+
"browserIdentifier": identifier,
|
|
294
|
+
"sessionId": session_id,
|
|
295
|
+
"streamUpdate": {"automationStreamUpdate": {"streamStatus": stream_status}},
|
|
296
|
+
}
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
@contextmanager
|
|
301
|
+
def browser_session(region: str) -> Generator[BrowserClient, None, None]:
|
|
302
|
+
"""Context manager for creating and managing a browser sandbox session.
|
|
303
|
+
|
|
304
|
+
This context manager handles creating a client, starting a session, and
|
|
305
|
+
ensuring the session is properly cleaned up when the context exits.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
region (str): The AWS region to use for the Browser service.
|
|
309
|
+
|
|
310
|
+
Yields:
|
|
311
|
+
BrowserClient: An initialized and started browser client.
|
|
312
|
+
|
|
313
|
+
Example:
|
|
314
|
+
>>> with browser_session('us-west-2') as client:
|
|
315
|
+
... browser = client.get_browser_obj()
|
|
316
|
+
... page = browser.new_page()
|
|
317
|
+
... page.goto('https://example.com')
|
|
318
|
+
"""
|
|
319
|
+
client = BrowserClient(region)
|
|
320
|
+
client.start()
|
|
321
|
+
|
|
322
|
+
try:
|
|
323
|
+
yield client
|
|
324
|
+
finally:
|
|
325
|
+
client.stop()
|