hyperbrowser 0.3.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 hyperbrowser might be problematic. Click here for more details.

@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 hyperbrowserai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,117 @@
1
+ Metadata-Version: 2.1
2
+ Name: hyperbrowser
3
+ Version: 0.3.0
4
+ Summary: Python SDK for hyperbrowser
5
+ Home-page: https://github.com/hyperbrowserai/python-sdk
6
+ License: MIT
7
+ Author: Nikhil Shahi
8
+ Author-email: nshahi1998@gmail.com
9
+ Requires-Python: >=3.9,<4.0
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Requires-Dist: httpx (>=0.28.0,<0.29.0)
18
+ Requires-Dist: pydantic (>=2.10.0,<3.0.0)
19
+ Project-URL: Repository, https://github.com/hyperbrowserai/python-sdk
20
+ Description-Content-Type: text/markdown
21
+
22
+ ## Installation
23
+
24
+ Currently Hyperbrowser supports creating a browser session in two ways:
25
+
26
+ - Async Client
27
+ - Sync Client
28
+
29
+ It can be installed from `pypi` by running :
30
+
31
+ ```shell
32
+ pip install hyperbrowser
33
+ ```
34
+
35
+ ## Configuration
36
+
37
+ Both the sync and async client follow similar configuration params
38
+
39
+ ### API Key
40
+ The API key can be configured either from the constructor arguments or environment variables using `HYPERBROWSER_API_KEY`
41
+
42
+ ## Usage
43
+
44
+ ### Async
45
+
46
+ ```python
47
+ import asyncio
48
+ from pyppeteer import connect
49
+ from hyperbrowser.client.async_client import AsyncHyperbrowser as Hyperbrowser
50
+
51
+ HYPERBROWSER_API_KEY = "test-key"
52
+
53
+ async def main():
54
+ async with Hyperbrowser(api_key=HYPERBROWSER_API_KEY) as client:
55
+ session = await client.create_session()
56
+
57
+ ws_endpoint = f"{session.websocket_url}&apiKey={HYPERBROWSER_API_KEY}"
58
+ browser = await connect(browserWSEndpoint=ws_endpoint, defaultViewport=None)
59
+
60
+ # Get pages
61
+ pages = await browser.pages()
62
+ if not pages:
63
+ raise Exception("No pages available")
64
+
65
+ page = pages[0]
66
+
67
+ # Navigate to a website
68
+ print("Navigating to Hacker News...")
69
+ await page.goto("https://news.ycombinator.com/")
70
+ page_title = await page.title()
71
+ print("Page title:", page_title)
72
+
73
+ await page.close()
74
+ await browser.disconnect()
75
+ print("Session completed!")
76
+
77
+ # Run the asyncio event loop
78
+ asyncio.get_event_loop().run_until_complete(main())
79
+ ```
80
+ ### Sync
81
+
82
+ ```python
83
+ from playwright.sync_api import sync_playwright
84
+ from hyperbrowser.client.sync import Hyperbrowser
85
+
86
+ HYPERBROWSER_API_KEY = "test-key"
87
+
88
+ def main():
89
+ client = Hyperbrowser(api_key=HYPERBROWSER_API_KEY)
90
+ session = client.create_session()
91
+
92
+ ws_endpoint = f"{session.websocket_url}&apiKey={HYPERBROWSER_API_KEY}"
93
+
94
+ # Launch Playwright and connect to the remote browser
95
+ with sync_playwright() as p:
96
+ browser = p.chromium.connect_over_cdp(ws_endpoint)
97
+ context = browser.new_context()
98
+
99
+ # Get the first page or create a new one
100
+ if len(context.pages) == 0:
101
+ page = context.new_page()
102
+ else:
103
+ page = context.pages[0]
104
+
105
+ # Navigate to a website
106
+ print("Navigating to Hacker News...")
107
+ page.goto("https://news.ycombinator.com/")
108
+ page_title = page.title()
109
+ print("Page title:", page_title)
110
+
111
+ page.close()
112
+ browser.close()
113
+ print("Session completed!")
114
+
115
+ # Run the asyncio event loop
116
+ main()
117
+ ```
@@ -0,0 +1,96 @@
1
+ ## Installation
2
+
3
+ Currently Hyperbrowser supports creating a browser session in two ways:
4
+
5
+ - Async Client
6
+ - Sync Client
7
+
8
+ It can be installed from `pypi` by running :
9
+
10
+ ```shell
11
+ pip install hyperbrowser
12
+ ```
13
+
14
+ ## Configuration
15
+
16
+ Both the sync and async client follow similar configuration params
17
+
18
+ ### API Key
19
+ The API key can be configured either from the constructor arguments or environment variables using `HYPERBROWSER_API_KEY`
20
+
21
+ ## Usage
22
+
23
+ ### Async
24
+
25
+ ```python
26
+ import asyncio
27
+ from pyppeteer import connect
28
+ from hyperbrowser.client.async_client import AsyncHyperbrowser as Hyperbrowser
29
+
30
+ HYPERBROWSER_API_KEY = "test-key"
31
+
32
+ async def main():
33
+ async with Hyperbrowser(api_key=HYPERBROWSER_API_KEY) as client:
34
+ session = await client.create_session()
35
+
36
+ ws_endpoint = f"{session.websocket_url}&apiKey={HYPERBROWSER_API_KEY}"
37
+ browser = await connect(browserWSEndpoint=ws_endpoint, defaultViewport=None)
38
+
39
+ # Get pages
40
+ pages = await browser.pages()
41
+ if not pages:
42
+ raise Exception("No pages available")
43
+
44
+ page = pages[0]
45
+
46
+ # Navigate to a website
47
+ print("Navigating to Hacker News...")
48
+ await page.goto("https://news.ycombinator.com/")
49
+ page_title = await page.title()
50
+ print("Page title:", page_title)
51
+
52
+ await page.close()
53
+ await browser.disconnect()
54
+ print("Session completed!")
55
+
56
+ # Run the asyncio event loop
57
+ asyncio.get_event_loop().run_until_complete(main())
58
+ ```
59
+ ### Sync
60
+
61
+ ```python
62
+ from playwright.sync_api import sync_playwright
63
+ from hyperbrowser.client.sync import Hyperbrowser
64
+
65
+ HYPERBROWSER_API_KEY = "test-key"
66
+
67
+ def main():
68
+ client = Hyperbrowser(api_key=HYPERBROWSER_API_KEY)
69
+ session = client.create_session()
70
+
71
+ ws_endpoint = f"{session.websocket_url}&apiKey={HYPERBROWSER_API_KEY}"
72
+
73
+ # Launch Playwright and connect to the remote browser
74
+ with sync_playwright() as p:
75
+ browser = p.chromium.connect_over_cdp(ws_endpoint)
76
+ context = browser.new_context()
77
+
78
+ # Get the first page or create a new one
79
+ if len(context.pages) == 0:
80
+ page = context.new_page()
81
+ else:
82
+ page = context.pages[0]
83
+
84
+ # Navigate to a website
85
+ print("Navigating to Hacker News...")
86
+ page.goto("https://news.ycombinator.com/")
87
+ page_title = page.title()
88
+ print("Page title:", page_title)
89
+
90
+ page.close()
91
+ browser.close()
92
+ print("Session completed!")
93
+
94
+ # Run the asyncio event loop
95
+ main()
96
+ ```
@@ -0,0 +1,5 @@
1
+ from .client.sync import Hyperbrowser
2
+ from .client.async_client import AsyncHyperbrowser
3
+ from .config import ClientConfig
4
+
5
+ __all__ = ["Hyperbrowser", "AsyncHyperbrowser", "ClientConfig"]
@@ -0,0 +1,51 @@
1
+ from typing import Optional
2
+ from ..transport.async_transport import AsyncTransport
3
+ from .base import HyperbrowserBase
4
+ from ..models.session import (
5
+ BasicResponse,
6
+ SessionDetail,
7
+ SessionListParams,
8
+ SessionListResponse,
9
+ )
10
+ from ..config import ClientConfig
11
+
12
+
13
+ class AsyncHyperbrowser(HyperbrowserBase):
14
+ """Asynchronous Hyperbrowser client"""
15
+
16
+ def __init__(
17
+ self,
18
+ config: Optional[ClientConfig] = None,
19
+ api_key: Optional[str] = None,
20
+ base_url: Optional[str] = None,
21
+ ):
22
+ super().__init__(AsyncTransport, config, api_key, base_url)
23
+
24
+ async def create_session(self) -> SessionDetail:
25
+ response = await self.transport.post(self._build_url("/session"))
26
+ return SessionDetail(**response.data)
27
+
28
+ async def get_session(self, id: str) -> SessionDetail:
29
+ response = await self.transport.get(self._build_url(f"/session/{id}"))
30
+ return SessionDetail(**response.data)
31
+
32
+ async def stop_session(self, id: str) -> BasicResponse:
33
+ response = await self.transport.put(self._build_url(f"/session/{id}/stop"))
34
+ return BasicResponse(**response.data)
35
+
36
+ async def get_session_list(
37
+ self, params: SessionListParams = SessionListParams()
38
+ ) -> SessionListResponse:
39
+ response = await self.transport.get(
40
+ self._build_url("/sessions"), params=params.__dict__
41
+ )
42
+ return SessionListResponse(**response.data)
43
+
44
+ async def close(self) -> None:
45
+ await self.transport.close()
46
+
47
+ async def __aenter__(self):
48
+ return self
49
+
50
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
51
+ await self.close()
@@ -0,0 +1,42 @@
1
+ from typing import Optional
2
+
3
+ from hyperbrowser.exceptions import HyperbrowserError
4
+ from ..config import ClientConfig
5
+ from ..transport.base import TransportStrategy
6
+ import os
7
+
8
+
9
+ class HyperbrowserBase:
10
+ """Base class with shared functionality for sync/async clients"""
11
+
12
+ def __init__(
13
+ self,
14
+ transport: TransportStrategy,
15
+ config: Optional[ClientConfig] = None,
16
+ api_key: Optional[str] = None,
17
+ base_url: Optional[str] = None,
18
+ ):
19
+ if config is None:
20
+ config = ClientConfig(
21
+ api_key=(
22
+ api_key
23
+ if api_key is not None
24
+ else os.environ.get("HYPERBROWSER_API_KEY", "")
25
+ ),
26
+ base_url=(
27
+ base_url
28
+ if base_url is not None
29
+ else os.environ.get(
30
+ "HYPERBROWSER_BASE_URL", "https://app.hyperbrowser.ai"
31
+ )
32
+ ),
33
+ )
34
+
35
+ if not config.api_key:
36
+ raise HyperbrowserError("API key must be provided")
37
+
38
+ self.config = config
39
+ self.transport = transport(config.api_key)
40
+
41
+ def _build_url(self, path: str) -> str:
42
+ return f"{self.config.base_url}/api{path}"
@@ -0,0 +1,43 @@
1
+ from typing import Optional
2
+ from ..transport.sync import SyncTransport
3
+ from .base import HyperbrowserBase
4
+ from ..models.session import (
5
+ BasicResponse,
6
+ SessionDetail,
7
+ SessionListParams,
8
+ SessionListResponse,
9
+ )
10
+ from ..config import ClientConfig
11
+
12
+
13
+ class Hyperbrowser(HyperbrowserBase):
14
+ """Synchronous Hyperbrowser client"""
15
+
16
+ def __init__(
17
+ self,
18
+ config: Optional[ClientConfig] = None,
19
+ api_key: Optional[str] = None,
20
+ base_url: Optional[str] = None,
21
+ ):
22
+ super().__init__(SyncTransport, config, api_key, base_url)
23
+
24
+ def create_session(self) -> SessionDetail:
25
+ response = self.transport.post(self._build_url("/session"))
26
+ return SessionDetail(**response.data)
27
+
28
+ def get_session(self, id: str) -> SessionDetail:
29
+ response = self.transport.get(self._build_url(f"/session/{id}"))
30
+ return SessionDetail(**response.data)
31
+
32
+ def stop_session(self, id: str) -> BasicResponse:
33
+ response = self.transport.put(self._build_url(f"/session/{id}/stop"))
34
+ return BasicResponse(**response.data)
35
+
36
+ def get_session_list(self, params: SessionListParams) -> SessionListResponse:
37
+ response = self.transport.get(
38
+ self._build_url("/sessions"), params=params.__dict__
39
+ )
40
+ return SessionListResponse(**response.data)
41
+
42
+ def close(self) -> None:
43
+ self.transport.close()
@@ -0,0 +1,22 @@
1
+ from dataclasses import dataclass
2
+ from typing import Optional
3
+ import os
4
+
5
+
6
+ @dataclass
7
+ class ClientConfig:
8
+ """Configuration for the Hyperbrowser client"""
9
+
10
+ api_key: str
11
+ base_url: str = "https://api.hyperbrowser.com"
12
+
13
+ @classmethod
14
+ def from_env(cls) -> "ClientConfig":
15
+ api_key = os.environ.get("HYPERBROWSER_API_KEY")
16
+ if api_key is None:
17
+ raise ValueError("HYPERBROWSER_API_KEY environment variable is required")
18
+
19
+ base_url = os.environ.get(
20
+ "HYPERBROWSER_BASE_URL", "https://api.hyperbrowser.com"
21
+ )
22
+ return cls(api_key=api_key, base_url=base_url)
@@ -0,0 +1,38 @@
1
+ # exceptions.py
2
+ from typing import Optional, Any
3
+
4
+
5
+ class HyperbrowserError(Exception):
6
+ """Base exception class for Hyperbrowser SDK errors"""
7
+
8
+ def __init__(
9
+ self,
10
+ message: str,
11
+ status_code: Optional[int] = None,
12
+ response: Optional[Any] = None,
13
+ original_error: Optional[Exception] = None,
14
+ ):
15
+ super().__init__(message)
16
+ self.status_code = status_code
17
+ self.response = response
18
+ self.original_error = original_error
19
+
20
+ def __str__(self) -> str:
21
+ """Custom string representation to show a cleaner error message"""
22
+ parts = [f"{self.args[0]}"]
23
+
24
+ if self.status_code:
25
+ parts.append(f"Status: {self.status_code}")
26
+
27
+ if self.original_error and not isinstance(
28
+ self.original_error, HyperbrowserError
29
+ ):
30
+ error_type = type(self.original_error).__name__
31
+ error_msg = str(self.original_error)
32
+ if error_msg and error_msg != str(self.args[0]):
33
+ parts.append(f"Caused by {error_type}: {error_msg}")
34
+
35
+ return " - ".join(parts)
36
+
37
+ def __repr__(self) -> str:
38
+ return self.__str__()
@@ -0,0 +1,89 @@
1
+ from typing import List, Literal, Optional, Union
2
+ from datetime import datetime
3
+ from pydantic import BaseModel, Field, ConfigDict, field_validator
4
+
5
+ SessionStatus = Literal["active", "closed", "error"]
6
+
7
+
8
+ class BasicResponse(BaseModel):
9
+ """
10
+ Represents a basic Hyperbrowser response.
11
+ """
12
+
13
+ success: bool
14
+
15
+
16
+ class Session(BaseModel):
17
+ """
18
+ Represents a basic session in the Hyperbrowser system.
19
+ """
20
+
21
+ model_config = ConfigDict(
22
+ populate_by_alias=True,
23
+ )
24
+
25
+ id: str
26
+ team_id: str = Field(alias="teamId")
27
+ status: SessionStatus
28
+ created_at: datetime = Field(alias="createdAt")
29
+ updated_at: datetime = Field(alias="updatedAt")
30
+ start_time: Optional[int] = Field(default=None, alias="startTime")
31
+ end_time: Optional[int] = Field(default=None, alias="endTime")
32
+ duration: Optional[int] = None
33
+ session_url: str = Field(alias="sessionUrl")
34
+
35
+ @field_validator("start_time", "end_time", mode="before")
36
+ @classmethod
37
+ def parse_timestamp(cls, value: Optional[Union[str, int]]) -> Optional[int]:
38
+ """Convert string timestamps to integers."""
39
+ if value is None:
40
+ return None
41
+ if isinstance(value, str):
42
+ return int(value)
43
+ return value
44
+
45
+
46
+ class SessionDetail(Session):
47
+ """
48
+ Detailed session information including websocket endpoint.
49
+ """
50
+
51
+ websocket_url: Optional[str] = Field(alias="wsEndpoint", default=None)
52
+
53
+
54
+ class SessionListParams(BaseModel):
55
+ """
56
+ Parameters for listing sessions.
57
+ """
58
+
59
+ model_config = ConfigDict(
60
+ populate_by_alias=True,
61
+ )
62
+
63
+ status: Optional[SessionStatus] = Field(default=None, exclude=None)
64
+ page: int = Field(default=1, ge=1)
65
+
66
+
67
+ class SessionListResponse(BaseModel):
68
+ """
69
+ Response containing a list of sessions with pagination information.
70
+ """
71
+
72
+ model_config = ConfigDict(
73
+ populate_by_alias=True,
74
+ )
75
+
76
+ sessions: List[Session]
77
+ total_count: int = Field(alias="totalCount")
78
+ page: int
79
+ per_page: int = Field(alias="perPage")
80
+
81
+ @property
82
+ def has_more(self) -> bool:
83
+ """Check if there are more pages available."""
84
+ return self.total_count > (self.page * self.per_page)
85
+
86
+ @property
87
+ def total_pages(self) -> int:
88
+ """Calculate the total number of pages."""
89
+ return -(-self.total_count // self.per_page)
@@ -0,0 +1,96 @@
1
+ import asyncio
2
+ import httpx
3
+ from typing import Optional
4
+
5
+ from hyperbrowser.exceptions import HyperbrowserError
6
+ from .base import TransportStrategy, APIResponse
7
+
8
+
9
+ class AsyncTransport(TransportStrategy):
10
+ """Asynchronous transport implementation using httpx"""
11
+
12
+ def __init__(self, api_key: str):
13
+ self.client = httpx.AsyncClient(headers={"x-api-key": api_key})
14
+ self._closed = False
15
+
16
+ async def close(self) -> None:
17
+ if not self._closed:
18
+ self._closed = True
19
+ await self.client.aclose()
20
+
21
+ async def __aenter__(self):
22
+ return self
23
+
24
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
25
+ await self.close()
26
+
27
+ def __del__(self):
28
+ if not self._closed:
29
+ try:
30
+ loop = asyncio.get_event_loop()
31
+ if loop.is_running():
32
+ loop.create_task(self.client.aclose())
33
+ else:
34
+ loop.run_until_complete(self.client.aclose())
35
+ except Exception:
36
+ pass
37
+
38
+ async def _handle_response(self, response: httpx.Response) -> APIResponse:
39
+ try:
40
+ response.raise_for_status()
41
+ try:
42
+ if not response.content:
43
+ return APIResponse.from_status(response.status_code)
44
+ return APIResponse(response.json())
45
+ except httpx.DecodingError as e:
46
+ if response.status_code >= 400:
47
+ raise HyperbrowserError(
48
+ response.text or "Unknown error occurred",
49
+ status_code=response.status_code,
50
+ response=response,
51
+ original_error=e,
52
+ )
53
+ return APIResponse.from_status(response.status_code)
54
+ except httpx.HTTPStatusError as e:
55
+ try:
56
+ error_data = response.json()
57
+ message = error_data.get("message") or error_data.get("error") or str(e)
58
+ except:
59
+ message = str(e)
60
+ raise HyperbrowserError(
61
+ message,
62
+ status_code=response.status_code,
63
+ response=response,
64
+ original_error=e,
65
+ )
66
+ except httpx.RequestError as e:
67
+ raise HyperbrowserError("Request failed", original_error=e)
68
+
69
+ async def post(self, url: str) -> APIResponse:
70
+ try:
71
+ response = await self.client.post(url)
72
+ return await self._handle_response(response)
73
+ except HyperbrowserError:
74
+ raise
75
+ except Exception as e:
76
+ raise HyperbrowserError("Post request failed", original_error=e)
77
+
78
+ async def get(self, url: str, params: Optional[dict] = None) -> APIResponse:
79
+ if params:
80
+ params = {k: v for k, v in params.items() if v is not None}
81
+ try:
82
+ response = await self.client.get(url, params=params)
83
+ return await self._handle_response(response)
84
+ except HyperbrowserError:
85
+ raise
86
+ except Exception as e:
87
+ raise HyperbrowserError("Get request failed", original_error=e)
88
+
89
+ async def put(self, url: str) -> APIResponse:
90
+ try:
91
+ response = await self.client.put(url)
92
+ return await self._handle_response(response)
93
+ except HyperbrowserError:
94
+ raise
95
+ except Exception as e:
96
+ raise HyperbrowserError("Put request failed", original_error=e)
@@ -0,0 +1,57 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Optional, TypeVar, Generic, Type, Union
3
+
4
+ from hyperbrowser.exceptions import HyperbrowserError
5
+
6
+ T = TypeVar("T")
7
+
8
+
9
+ class APIResponse(Generic[T]):
10
+ """
11
+ Wrapper for API responses to standardize sync/async handling.
12
+ """
13
+
14
+ def __init__(self, data: Optional[Union[dict, T]] = None, status_code: int = 200):
15
+ self.data = data
16
+ self.status_code = status_code
17
+
18
+ @classmethod
19
+ def from_json(cls, json_data: dict, model: Type[T]) -> "APIResponse[T]":
20
+ """Create an APIResponse from JSON data with a specific model."""
21
+ try:
22
+ return cls(data=model(**json_data))
23
+ except Exception as e:
24
+ raise HyperbrowserError("Failed to parse response data", original_error=e)
25
+
26
+ @classmethod
27
+ def from_status(cls, status_code: int) -> "APIResponse[None]":
28
+ """Create an APIResponse from just a status code."""
29
+ return cls(data=None, status_code=status_code)
30
+
31
+ def is_success(self) -> bool:
32
+ """Check if the response indicates success."""
33
+ return 200 <= self.status_code < 300
34
+
35
+
36
+ class TransportStrategy(ABC):
37
+ """Abstract base class for different transport implementations"""
38
+
39
+ @abstractmethod
40
+ def __init__(self, api_key: str):
41
+ pass
42
+
43
+ @abstractmethod
44
+ def close(self) -> None:
45
+ pass
46
+
47
+ @abstractmethod
48
+ def post(self, url: str) -> APIResponse:
49
+ pass
50
+
51
+ @abstractmethod
52
+ def get(self, url: str, params: Optional[dict] = None) -> APIResponse:
53
+ pass
54
+
55
+ @abstractmethod
56
+ def put(self, url: str) -> APIResponse:
57
+ pass
@@ -0,0 +1,75 @@
1
+ import httpx
2
+ from typing import Optional
3
+
4
+ from hyperbrowser.exceptions import HyperbrowserError
5
+ from .base import TransportStrategy, APIResponse
6
+
7
+
8
+ class SyncTransport(TransportStrategy):
9
+ """Synchronous transport implementation using httpx"""
10
+
11
+ def __init__(self, api_key: str):
12
+ self.client = httpx.Client(headers={"x-api-key": api_key})
13
+
14
+ def _handle_response(self, response: httpx.Response) -> APIResponse:
15
+ try:
16
+ response.raise_for_status()
17
+ try:
18
+ if not response.content:
19
+ return APIResponse.from_status(response.status_code)
20
+ return APIResponse(response.json())
21
+ except httpx.DecodingError as e:
22
+ if response.status_code >= 400:
23
+ raise HyperbrowserError(
24
+ response.text or "Unknown error occurred",
25
+ status_code=response.status_code,
26
+ response=response,
27
+ original_error=e,
28
+ )
29
+ return APIResponse.from_status(response.status_code)
30
+ except httpx.HTTPStatusError as e:
31
+ try:
32
+ error_data = response.json()
33
+ message = error_data.get("message") or error_data.get("error") or str(e)
34
+ except:
35
+ message = str(e)
36
+ raise HyperbrowserError(
37
+ message,
38
+ status_code=response.status_code,
39
+ response=response,
40
+ original_error=e,
41
+ )
42
+ except httpx.RequestError as e:
43
+ raise HyperbrowserError("Request failed", original_error=e)
44
+
45
+ def close(self) -> None:
46
+ self.client.close()
47
+
48
+ def post(self, url: str) -> APIResponse:
49
+ try:
50
+ response = self.client.post(url)
51
+ return self._handle_response(response)
52
+ except HyperbrowserError:
53
+ raise
54
+ except Exception as e:
55
+ raise HyperbrowserError("Post request failed", original_error=e)
56
+
57
+ def get(self, url: str, params: Optional[dict] = None) -> APIResponse:
58
+ if params:
59
+ params = {k: v for k, v in params.items() if v is not None}
60
+ try:
61
+ response = self.client.get(url, params=params)
62
+ return self._handle_response(response)
63
+ except HyperbrowserError:
64
+ raise
65
+ except Exception as e:
66
+ raise HyperbrowserError("Get request failed", original_error=e)
67
+
68
+ def put(self, url: str) -> APIResponse:
69
+ try:
70
+ response = self.client.put(url)
71
+ return self._handle_response(response)
72
+ except HyperbrowserError:
73
+ raise
74
+ except Exception as e:
75
+ raise HyperbrowserError("Put request failed", original_error=e)
@@ -0,0 +1,23 @@
1
+ [tool.poetry]
2
+ name = "hyperbrowser"
3
+ version = "0.3.0"
4
+ description = "Python SDK for hyperbrowser"
5
+ authors = ["Nikhil Shahi <nshahi1998@gmail.com>"]
6
+ license = "MIT"
7
+ readme = "README.md"
8
+ include = ["LICENSE"]
9
+ homepage = "https://github.com/hyperbrowserai/python-sdk"
10
+ repository = "https://github.com/hyperbrowserai/python-sdk"
11
+
12
+ [tool.poetry.dependencies]
13
+ python = "^3.9"
14
+ pydantic = "^2.10.0"
15
+ httpx = "^0.28.0"
16
+
17
+
18
+ [tool.poetry.group.dev.dependencies]
19
+ black = "^24.10.0"
20
+
21
+ [build-system]
22
+ requires = ["poetry-core"]
23
+ build-backend = "poetry.core.masonry.api"