shadey 0.1.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.
- shadey-0.1.0/PKG-INFO +165 -0
- shadey-0.1.0/README.md +140 -0
- shadey-0.1.0/setup.cfg +4 -0
- shadey-0.1.0/setup.py +25 -0
- shadey-0.1.0/shadey/__init__.py +5 -0
- shadey-0.1.0/shadey/client.py +85 -0
- shadey-0.1.0/shadey/session.py +116 -0
- shadey-0.1.0/shadey.egg-info/PKG-INFO +165 -0
- shadey-0.1.0/shadey.egg-info/SOURCES.txt +10 -0
- shadey-0.1.0/shadey.egg-info/dependency_links.txt +1 -0
- shadey-0.1.0/shadey.egg-info/requires.txt +4 -0
- shadey-0.1.0/shadey.egg-info/top_level.txt +1 -0
shadey-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: shadey
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Stealth browser sessions for AI agents
|
|
5
|
+
Home-page: https://shadey.dev
|
|
6
|
+
Author: Shadey
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: aiohttp>=3.8
|
|
14
|
+
Provides-Extra: playwright
|
|
15
|
+
Requires-Dist: playwright>=1.40; extra == "playwright"
|
|
16
|
+
Dynamic: author
|
|
17
|
+
Dynamic: classifier
|
|
18
|
+
Dynamic: description
|
|
19
|
+
Dynamic: description-content-type
|
|
20
|
+
Dynamic: home-page
|
|
21
|
+
Dynamic: provides-extra
|
|
22
|
+
Dynamic: requires-dist
|
|
23
|
+
Dynamic: requires-python
|
|
24
|
+
Dynamic: summary
|
|
25
|
+
|
|
26
|
+
# shadey
|
|
27
|
+
|
|
28
|
+
Stealth browser sessions for AI agents. Pass Cloudflare, DataDome, PerimeterX, and Kasada from any IP.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install shadey
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
import asyncio
|
|
40
|
+
from shadey import Shadey
|
|
41
|
+
|
|
42
|
+
async def main():
|
|
43
|
+
async with Shadey("sk_your_api_key") as client:
|
|
44
|
+
async with await client.session() as session:
|
|
45
|
+
# Navigate and extract — no CDP/Playwright needed
|
|
46
|
+
await session.goto("https://example.com")
|
|
47
|
+
title = await session.evaluate("document.title")
|
|
48
|
+
print(title["result"])
|
|
49
|
+
|
|
50
|
+
# Extract text from elements
|
|
51
|
+
headings = await session.extract("h1", all=True)
|
|
52
|
+
print(headings["data"])
|
|
53
|
+
|
|
54
|
+
# Get full page HTML
|
|
55
|
+
page = await session.content()
|
|
56
|
+
print(f"{page['length']} bytes")
|
|
57
|
+
|
|
58
|
+
# Screenshot
|
|
59
|
+
img = await session.screenshot() # base64 jpeg
|
|
60
|
+
|
|
61
|
+
asyncio.run(main())
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## With Behavioral Actions
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
async with Shadey("sk_your_api_key") as client:
|
|
68
|
+
async with await client.session() as session:
|
|
69
|
+
await session.goto("https://nowsecure.nl")
|
|
70
|
+
|
|
71
|
+
# Human-like interactions to build trust
|
|
72
|
+
await session.idle(duration_ms=3000)
|
|
73
|
+
await session.scroll(400)
|
|
74
|
+
await session.click(500, 300)
|
|
75
|
+
await session.type_text("hello world", context="search")
|
|
76
|
+
|
|
77
|
+
# Then extract what you need
|
|
78
|
+
result = await session.extract(".result-title", all=True)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## With Playwright (CDP)
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
async with Shadey("sk_your_api_key") as client:
|
|
85
|
+
async with await client.session() as session:
|
|
86
|
+
# Full browser control via CDP
|
|
87
|
+
from playwright.async_api import async_playwright
|
|
88
|
+
async with async_playwright() as p:
|
|
89
|
+
browser = await p.chromium.connect_over_cdp(session.playwright_url)
|
|
90
|
+
page = browser.contexts[0].pages[0]
|
|
91
|
+
await page.goto("https://nowsecure.nl")
|
|
92
|
+
print(await page.title())
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## API
|
|
96
|
+
|
|
97
|
+
### `Shadey(api_key, base_url="https://api.shadey.dev")`
|
|
98
|
+
|
|
99
|
+
Create a client. Use as async context manager or call `.close()` manually.
|
|
100
|
+
|
|
101
|
+
### `client.session(stealth="full", proxy=None, profile=None)`
|
|
102
|
+
|
|
103
|
+
Create a stealth browser session. Returns a `ShadeySession`.
|
|
104
|
+
|
|
105
|
+
### REST Actions
|
|
106
|
+
|
|
107
|
+
### `session.goto(url, wait_ms=3000)`
|
|
108
|
+
|
|
109
|
+
Navigate to a URL. Waits for page load + extra `wait_ms`.
|
|
110
|
+
|
|
111
|
+
### `session.screenshot()`
|
|
112
|
+
|
|
113
|
+
Returns a base64 JPEG data URI of the current page.
|
|
114
|
+
|
|
115
|
+
### `session.extract(selector, attribute="textContent", all=False)`
|
|
116
|
+
|
|
117
|
+
Extract data from the page. Set `all=True` to get all matching elements.
|
|
118
|
+
|
|
119
|
+
### `session.evaluate(expression)`
|
|
120
|
+
|
|
121
|
+
Run JavaScript and return the result.
|
|
122
|
+
|
|
123
|
+
### `session.content()`
|
|
124
|
+
|
|
125
|
+
Get the full page HTML.
|
|
126
|
+
|
|
127
|
+
### Behavioral Actions
|
|
128
|
+
|
|
129
|
+
### `session.click(x, y, width=50)`
|
|
130
|
+
|
|
131
|
+
Move mouse and click at coordinates with Fitts' law timing and corrective submovements.
|
|
132
|
+
|
|
133
|
+
### `session.type_text(text, context="form")`
|
|
134
|
+
|
|
135
|
+
Type text with QWERTY bigram-modeled keystroke timing. Context: `"form"`, `"search"`, `"url"`.
|
|
136
|
+
|
|
137
|
+
### `session.scroll(distance)`
|
|
138
|
+
|
|
139
|
+
Scroll with momentum and inertia. Positive = down, negative = up.
|
|
140
|
+
|
|
141
|
+
### `session.dwell(context="content")`
|
|
142
|
+
|
|
143
|
+
Wait a human-realistic amount of time. Context: `"content"`, `"search"`, `"form"`.
|
|
144
|
+
|
|
145
|
+
### `session.idle(duration_ms=5000)`
|
|
146
|
+
|
|
147
|
+
Simulate idle browsing — mouse movements, scrolls, pauses driven by HMM behavioral model.
|
|
148
|
+
|
|
149
|
+
### CDP Access
|
|
150
|
+
|
|
151
|
+
### `session.playwright_url` / `session.puppeteer_url`
|
|
152
|
+
|
|
153
|
+
CDP WebSocket URL for direct browser control.
|
|
154
|
+
|
|
155
|
+
### `session.close()`
|
|
156
|
+
|
|
157
|
+
Destroy the session.
|
|
158
|
+
|
|
159
|
+
### `client.usage()`
|
|
160
|
+
|
|
161
|
+
Get plan usage stats.
|
|
162
|
+
|
|
163
|
+
### `client.health()`
|
|
164
|
+
|
|
165
|
+
Check API health.
|
shadey-0.1.0/README.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# shadey
|
|
2
|
+
|
|
3
|
+
Stealth browser sessions for AI agents. Pass Cloudflare, DataDome, PerimeterX, and Kasada from any IP.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install shadey
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
import asyncio
|
|
15
|
+
from shadey import Shadey
|
|
16
|
+
|
|
17
|
+
async def main():
|
|
18
|
+
async with Shadey("sk_your_api_key") as client:
|
|
19
|
+
async with await client.session() as session:
|
|
20
|
+
# Navigate and extract — no CDP/Playwright needed
|
|
21
|
+
await session.goto("https://example.com")
|
|
22
|
+
title = await session.evaluate("document.title")
|
|
23
|
+
print(title["result"])
|
|
24
|
+
|
|
25
|
+
# Extract text from elements
|
|
26
|
+
headings = await session.extract("h1", all=True)
|
|
27
|
+
print(headings["data"])
|
|
28
|
+
|
|
29
|
+
# Get full page HTML
|
|
30
|
+
page = await session.content()
|
|
31
|
+
print(f"{page['length']} bytes")
|
|
32
|
+
|
|
33
|
+
# Screenshot
|
|
34
|
+
img = await session.screenshot() # base64 jpeg
|
|
35
|
+
|
|
36
|
+
asyncio.run(main())
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## With Behavioral Actions
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
async with Shadey("sk_your_api_key") as client:
|
|
43
|
+
async with await client.session() as session:
|
|
44
|
+
await session.goto("https://nowsecure.nl")
|
|
45
|
+
|
|
46
|
+
# Human-like interactions to build trust
|
|
47
|
+
await session.idle(duration_ms=3000)
|
|
48
|
+
await session.scroll(400)
|
|
49
|
+
await session.click(500, 300)
|
|
50
|
+
await session.type_text("hello world", context="search")
|
|
51
|
+
|
|
52
|
+
# Then extract what you need
|
|
53
|
+
result = await session.extract(".result-title", all=True)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## With Playwright (CDP)
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
async with Shadey("sk_your_api_key") as client:
|
|
60
|
+
async with await client.session() as session:
|
|
61
|
+
# Full browser control via CDP
|
|
62
|
+
from playwright.async_api import async_playwright
|
|
63
|
+
async with async_playwright() as p:
|
|
64
|
+
browser = await p.chromium.connect_over_cdp(session.playwright_url)
|
|
65
|
+
page = browser.contexts[0].pages[0]
|
|
66
|
+
await page.goto("https://nowsecure.nl")
|
|
67
|
+
print(await page.title())
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## API
|
|
71
|
+
|
|
72
|
+
### `Shadey(api_key, base_url="https://api.shadey.dev")`
|
|
73
|
+
|
|
74
|
+
Create a client. Use as async context manager or call `.close()` manually.
|
|
75
|
+
|
|
76
|
+
### `client.session(stealth="full", proxy=None, profile=None)`
|
|
77
|
+
|
|
78
|
+
Create a stealth browser session. Returns a `ShadeySession`.
|
|
79
|
+
|
|
80
|
+
### REST Actions
|
|
81
|
+
|
|
82
|
+
### `session.goto(url, wait_ms=3000)`
|
|
83
|
+
|
|
84
|
+
Navigate to a URL. Waits for page load + extra `wait_ms`.
|
|
85
|
+
|
|
86
|
+
### `session.screenshot()`
|
|
87
|
+
|
|
88
|
+
Returns a base64 JPEG data URI of the current page.
|
|
89
|
+
|
|
90
|
+
### `session.extract(selector, attribute="textContent", all=False)`
|
|
91
|
+
|
|
92
|
+
Extract data from the page. Set `all=True` to get all matching elements.
|
|
93
|
+
|
|
94
|
+
### `session.evaluate(expression)`
|
|
95
|
+
|
|
96
|
+
Run JavaScript and return the result.
|
|
97
|
+
|
|
98
|
+
### `session.content()`
|
|
99
|
+
|
|
100
|
+
Get the full page HTML.
|
|
101
|
+
|
|
102
|
+
### Behavioral Actions
|
|
103
|
+
|
|
104
|
+
### `session.click(x, y, width=50)`
|
|
105
|
+
|
|
106
|
+
Move mouse and click at coordinates with Fitts' law timing and corrective submovements.
|
|
107
|
+
|
|
108
|
+
### `session.type_text(text, context="form")`
|
|
109
|
+
|
|
110
|
+
Type text with QWERTY bigram-modeled keystroke timing. Context: `"form"`, `"search"`, `"url"`.
|
|
111
|
+
|
|
112
|
+
### `session.scroll(distance)`
|
|
113
|
+
|
|
114
|
+
Scroll with momentum and inertia. Positive = down, negative = up.
|
|
115
|
+
|
|
116
|
+
### `session.dwell(context="content")`
|
|
117
|
+
|
|
118
|
+
Wait a human-realistic amount of time. Context: `"content"`, `"search"`, `"form"`.
|
|
119
|
+
|
|
120
|
+
### `session.idle(duration_ms=5000)`
|
|
121
|
+
|
|
122
|
+
Simulate idle browsing — mouse movements, scrolls, pauses driven by HMM behavioral model.
|
|
123
|
+
|
|
124
|
+
### CDP Access
|
|
125
|
+
|
|
126
|
+
### `session.playwright_url` / `session.puppeteer_url`
|
|
127
|
+
|
|
128
|
+
CDP WebSocket URL for direct browser control.
|
|
129
|
+
|
|
130
|
+
### `session.close()`
|
|
131
|
+
|
|
132
|
+
Destroy the session.
|
|
133
|
+
|
|
134
|
+
### `client.usage()`
|
|
135
|
+
|
|
136
|
+
Get plan usage stats.
|
|
137
|
+
|
|
138
|
+
### `client.health()`
|
|
139
|
+
|
|
140
|
+
Check API health.
|
shadey-0.1.0/setup.cfg
ADDED
shadey-0.1.0/setup.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="shadey",
|
|
5
|
+
version="0.1.0",
|
|
6
|
+
description="Stealth browser sessions for AI agents",
|
|
7
|
+
long_description=open("README.md").read(),
|
|
8
|
+
long_description_content_type="text/markdown",
|
|
9
|
+
author="Shadey",
|
|
10
|
+
url="https://shadey.dev",
|
|
11
|
+
packages=find_packages(),
|
|
12
|
+
python_requires=">=3.8",
|
|
13
|
+
install_requires=[
|
|
14
|
+
"aiohttp>=3.8",
|
|
15
|
+
],
|
|
16
|
+
extras_require={
|
|
17
|
+
"playwright": ["playwright>=1.40"],
|
|
18
|
+
},
|
|
19
|
+
classifiers=[
|
|
20
|
+
"Development Status :: 3 - Alpha",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"License :: OSI Approved :: MIT License",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
],
|
|
25
|
+
)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import aiohttp
|
|
2
|
+
from typing import Optional, List
|
|
3
|
+
from .session import ShadeySession
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Shadey:
|
|
7
|
+
def __init__(self, api_key: str, base_url: str = "https://api.shadey.dev"):
|
|
8
|
+
self.api_key = api_key
|
|
9
|
+
self.base_url = base_url.rstrip("/")
|
|
10
|
+
self._http: Optional[aiohttp.ClientSession] = None
|
|
11
|
+
|
|
12
|
+
async def __aenter__(self):
|
|
13
|
+
self._http = aiohttp.ClientSession(
|
|
14
|
+
headers={"Authorization": f"Bearer {self.api_key}"}
|
|
15
|
+
)
|
|
16
|
+
return self
|
|
17
|
+
|
|
18
|
+
async def __aexit__(self, *args):
|
|
19
|
+
if self._http:
|
|
20
|
+
await self._http.close()
|
|
21
|
+
|
|
22
|
+
def _ensure_http(self):
|
|
23
|
+
if not self._http:
|
|
24
|
+
self._http = aiohttp.ClientSession(
|
|
25
|
+
headers={"Authorization": f"Bearer {self.api_key}"}
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
async def _request(self, method: str, path: str, **kwargs) -> dict:
|
|
29
|
+
self._ensure_http()
|
|
30
|
+
async with self._http.request(method, f"{self.base_url}{path}", **kwargs) as resp:
|
|
31
|
+
if resp.status == 204:
|
|
32
|
+
return {}
|
|
33
|
+
data = await resp.json()
|
|
34
|
+
if resp.status >= 400:
|
|
35
|
+
raise ShadeyError(resp.status, data.get("error", str(data)))
|
|
36
|
+
return data
|
|
37
|
+
|
|
38
|
+
async def session(
|
|
39
|
+
self,
|
|
40
|
+
stealth: str = "full",
|
|
41
|
+
proxy: Optional[str] = None,
|
|
42
|
+
profile: Optional[str] = None,
|
|
43
|
+
) -> "ShadeySession":
|
|
44
|
+
payload = {"stealth": stealth}
|
|
45
|
+
if proxy:
|
|
46
|
+
payload["proxy"] = proxy
|
|
47
|
+
if profile:
|
|
48
|
+
payload["profile_name"] = profile
|
|
49
|
+
|
|
50
|
+
data = await self._request("POST", "/v1/sessions", json=payload)
|
|
51
|
+
return ShadeySession(
|
|
52
|
+
id=data["id"],
|
|
53
|
+
cdp_url=data["cdp_url"],
|
|
54
|
+
client=self,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
async def sessions(self) -> List[dict]:
|
|
58
|
+
return await self._request("GET", "/v1/sessions")
|
|
59
|
+
|
|
60
|
+
async def health(self) -> dict:
|
|
61
|
+
self._ensure_http()
|
|
62
|
+
async with self._http.get(f"{self.base_url}/health") as resp:
|
|
63
|
+
return await resp.json()
|
|
64
|
+
|
|
65
|
+
async def usage(self) -> dict:
|
|
66
|
+
return await self._request("GET", "/v1/usage")
|
|
67
|
+
|
|
68
|
+
async def profiles(self) -> list:
|
|
69
|
+
data = await self._request("GET", "/v1/profiles")
|
|
70
|
+
return data.get("profiles", [])
|
|
71
|
+
|
|
72
|
+
async def delete_profile(self, name: str):
|
|
73
|
+
await self._request("DELETE", f"/v1/profiles/{name}")
|
|
74
|
+
|
|
75
|
+
async def close(self):
|
|
76
|
+
if self._http:
|
|
77
|
+
await self._http.close()
|
|
78
|
+
self._http = None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ShadeyError(Exception):
|
|
82
|
+
def __init__(self, status: int, message: str):
|
|
83
|
+
self.status = status
|
|
84
|
+
self.message = message
|
|
85
|
+
super().__init__(f"[{status}] {message}")
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Optional, TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from .client import Shadey
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ShadeySession:
|
|
9
|
+
def __init__(self, id: str, cdp_url: str, client: "Shadey"):
|
|
10
|
+
self.id = id
|
|
11
|
+
self.cdp_url = cdp_url
|
|
12
|
+
self._client = client
|
|
13
|
+
self._closed = False
|
|
14
|
+
|
|
15
|
+
async def close(self, save: bool = False):
|
|
16
|
+
if not self._closed:
|
|
17
|
+
path = f"/v1/sessions/{self.id}"
|
|
18
|
+
if save:
|
|
19
|
+
path += "?save=true"
|
|
20
|
+
await self._client._request("DELETE", path)
|
|
21
|
+
self._closed = True
|
|
22
|
+
|
|
23
|
+
async def __aenter__(self):
|
|
24
|
+
return self
|
|
25
|
+
|
|
26
|
+
async def __aexit__(self, *args):
|
|
27
|
+
await self.close()
|
|
28
|
+
|
|
29
|
+
# -- behavioral actions --
|
|
30
|
+
|
|
31
|
+
async def click(self, x: float, y: float, width: float = 50.0) -> dict:
|
|
32
|
+
return await self._behave({
|
|
33
|
+
"action": "move_and_click",
|
|
34
|
+
"target_x": x,
|
|
35
|
+
"target_y": y,
|
|
36
|
+
"target_width": width,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
async def type_text(self, text: str, context: str = "form") -> dict:
|
|
40
|
+
return await self._behave({
|
|
41
|
+
"action": "type",
|
|
42
|
+
"text": text,
|
|
43
|
+
"typing_context": context,
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
async def scroll(self, distance: float) -> dict:
|
|
47
|
+
return await self._behave({
|
|
48
|
+
"action": "scroll",
|
|
49
|
+
"distance": distance,
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
async def dwell(self, context: str = "content") -> dict:
|
|
53
|
+
return await self._behave({
|
|
54
|
+
"action": "dwell",
|
|
55
|
+
"page_context": context,
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
async def idle(self, duration_ms: int = 5000) -> dict:
|
|
59
|
+
return await self._behave({
|
|
60
|
+
"action": "idle_browse",
|
|
61
|
+
"duration_ms": duration_ms,
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
async def _behave(self, payload: dict) -> dict:
|
|
65
|
+
return await self._client._request(
|
|
66
|
+
"POST", f"/v1/sessions/{self.id}/behave", json=payload
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# -- REST actions --
|
|
70
|
+
|
|
71
|
+
async def goto(self, url: str, wait_ms: int = 3000) -> dict:
|
|
72
|
+
return await self._client._request(
|
|
73
|
+
"POST", f"/v1/sessions/{self.id}/navigate",
|
|
74
|
+
json={"url": url, "wait_ms": wait_ms},
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
async def screenshot(self) -> str:
|
|
78
|
+
data = await self._client._request(
|
|
79
|
+
"POST", f"/v1/sessions/{self.id}/screenshot"
|
|
80
|
+
)
|
|
81
|
+
return data["data"]
|
|
82
|
+
|
|
83
|
+
async def extract(self, selector: str, attribute: str = "textContent", all: bool = False) -> dict:
|
|
84
|
+
return await self._client._request(
|
|
85
|
+
"POST", f"/v1/sessions/{self.id}/extract",
|
|
86
|
+
json={"selector": selector, "attribute": attribute, "all": all},
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
async def evaluate(self, expression: str) -> dict:
|
|
90
|
+
return await self._client._request(
|
|
91
|
+
"POST", f"/v1/sessions/{self.id}/evaluate",
|
|
92
|
+
json={"expression": expression},
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
async def content(self) -> dict:
|
|
96
|
+
return await self._client._request(
|
|
97
|
+
"POST", f"/v1/sessions/{self.id}/content"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# -- CDP access --
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def playwright_url(self) -> str:
|
|
104
|
+
return self.cdp_url
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def puppeteer_url(self) -> str:
|
|
108
|
+
return self.cdp_url
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def is_active(self) -> bool:
|
|
112
|
+
return not self._closed
|
|
113
|
+
|
|
114
|
+
def __repr__(self):
|
|
115
|
+
status = "active" if not self._closed else "closed"
|
|
116
|
+
return f"ShadeySession(id={self.id!r}, status={status!r})"
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: shadey
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Stealth browser sessions for AI agents
|
|
5
|
+
Home-page: https://shadey.dev
|
|
6
|
+
Author: Shadey
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: aiohttp>=3.8
|
|
14
|
+
Provides-Extra: playwright
|
|
15
|
+
Requires-Dist: playwright>=1.40; extra == "playwright"
|
|
16
|
+
Dynamic: author
|
|
17
|
+
Dynamic: classifier
|
|
18
|
+
Dynamic: description
|
|
19
|
+
Dynamic: description-content-type
|
|
20
|
+
Dynamic: home-page
|
|
21
|
+
Dynamic: provides-extra
|
|
22
|
+
Dynamic: requires-dist
|
|
23
|
+
Dynamic: requires-python
|
|
24
|
+
Dynamic: summary
|
|
25
|
+
|
|
26
|
+
# shadey
|
|
27
|
+
|
|
28
|
+
Stealth browser sessions for AI agents. Pass Cloudflare, DataDome, PerimeterX, and Kasada from any IP.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install shadey
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
import asyncio
|
|
40
|
+
from shadey import Shadey
|
|
41
|
+
|
|
42
|
+
async def main():
|
|
43
|
+
async with Shadey("sk_your_api_key") as client:
|
|
44
|
+
async with await client.session() as session:
|
|
45
|
+
# Navigate and extract — no CDP/Playwright needed
|
|
46
|
+
await session.goto("https://example.com")
|
|
47
|
+
title = await session.evaluate("document.title")
|
|
48
|
+
print(title["result"])
|
|
49
|
+
|
|
50
|
+
# Extract text from elements
|
|
51
|
+
headings = await session.extract("h1", all=True)
|
|
52
|
+
print(headings["data"])
|
|
53
|
+
|
|
54
|
+
# Get full page HTML
|
|
55
|
+
page = await session.content()
|
|
56
|
+
print(f"{page['length']} bytes")
|
|
57
|
+
|
|
58
|
+
# Screenshot
|
|
59
|
+
img = await session.screenshot() # base64 jpeg
|
|
60
|
+
|
|
61
|
+
asyncio.run(main())
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## With Behavioral Actions
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
async with Shadey("sk_your_api_key") as client:
|
|
68
|
+
async with await client.session() as session:
|
|
69
|
+
await session.goto("https://nowsecure.nl")
|
|
70
|
+
|
|
71
|
+
# Human-like interactions to build trust
|
|
72
|
+
await session.idle(duration_ms=3000)
|
|
73
|
+
await session.scroll(400)
|
|
74
|
+
await session.click(500, 300)
|
|
75
|
+
await session.type_text("hello world", context="search")
|
|
76
|
+
|
|
77
|
+
# Then extract what you need
|
|
78
|
+
result = await session.extract(".result-title", all=True)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## With Playwright (CDP)
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
async with Shadey("sk_your_api_key") as client:
|
|
85
|
+
async with await client.session() as session:
|
|
86
|
+
# Full browser control via CDP
|
|
87
|
+
from playwright.async_api import async_playwright
|
|
88
|
+
async with async_playwright() as p:
|
|
89
|
+
browser = await p.chromium.connect_over_cdp(session.playwright_url)
|
|
90
|
+
page = browser.contexts[0].pages[0]
|
|
91
|
+
await page.goto("https://nowsecure.nl")
|
|
92
|
+
print(await page.title())
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## API
|
|
96
|
+
|
|
97
|
+
### `Shadey(api_key, base_url="https://api.shadey.dev")`
|
|
98
|
+
|
|
99
|
+
Create a client. Use as async context manager or call `.close()` manually.
|
|
100
|
+
|
|
101
|
+
### `client.session(stealth="full", proxy=None, profile=None)`
|
|
102
|
+
|
|
103
|
+
Create a stealth browser session. Returns a `ShadeySession`.
|
|
104
|
+
|
|
105
|
+
### REST Actions
|
|
106
|
+
|
|
107
|
+
### `session.goto(url, wait_ms=3000)`
|
|
108
|
+
|
|
109
|
+
Navigate to a URL. Waits for page load + extra `wait_ms`.
|
|
110
|
+
|
|
111
|
+
### `session.screenshot()`
|
|
112
|
+
|
|
113
|
+
Returns a base64 JPEG data URI of the current page.
|
|
114
|
+
|
|
115
|
+
### `session.extract(selector, attribute="textContent", all=False)`
|
|
116
|
+
|
|
117
|
+
Extract data from the page. Set `all=True` to get all matching elements.
|
|
118
|
+
|
|
119
|
+
### `session.evaluate(expression)`
|
|
120
|
+
|
|
121
|
+
Run JavaScript and return the result.
|
|
122
|
+
|
|
123
|
+
### `session.content()`
|
|
124
|
+
|
|
125
|
+
Get the full page HTML.
|
|
126
|
+
|
|
127
|
+
### Behavioral Actions
|
|
128
|
+
|
|
129
|
+
### `session.click(x, y, width=50)`
|
|
130
|
+
|
|
131
|
+
Move mouse and click at coordinates with Fitts' law timing and corrective submovements.
|
|
132
|
+
|
|
133
|
+
### `session.type_text(text, context="form")`
|
|
134
|
+
|
|
135
|
+
Type text with QWERTY bigram-modeled keystroke timing. Context: `"form"`, `"search"`, `"url"`.
|
|
136
|
+
|
|
137
|
+
### `session.scroll(distance)`
|
|
138
|
+
|
|
139
|
+
Scroll with momentum and inertia. Positive = down, negative = up.
|
|
140
|
+
|
|
141
|
+
### `session.dwell(context="content")`
|
|
142
|
+
|
|
143
|
+
Wait a human-realistic amount of time. Context: `"content"`, `"search"`, `"form"`.
|
|
144
|
+
|
|
145
|
+
### `session.idle(duration_ms=5000)`
|
|
146
|
+
|
|
147
|
+
Simulate idle browsing — mouse movements, scrolls, pauses driven by HMM behavioral model.
|
|
148
|
+
|
|
149
|
+
### CDP Access
|
|
150
|
+
|
|
151
|
+
### `session.playwright_url` / `session.puppeteer_url`
|
|
152
|
+
|
|
153
|
+
CDP WebSocket URL for direct browser control.
|
|
154
|
+
|
|
155
|
+
### `session.close()`
|
|
156
|
+
|
|
157
|
+
Destroy the session.
|
|
158
|
+
|
|
159
|
+
### `client.usage()`
|
|
160
|
+
|
|
161
|
+
Get plan usage stats.
|
|
162
|
+
|
|
163
|
+
### `client.health()`
|
|
164
|
+
|
|
165
|
+
Check API health.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
shadey
|