peep-sdk 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.
- peep/__init__.py +9 -0
- peep/client.py +188 -0
- peep_sdk-0.1.0.dist-info/METADATA +88 -0
- peep_sdk-0.1.0.dist-info/RECORD +6 -0
- peep_sdk-0.1.0.dist-info/WHEEL +4 -0
- peep_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
peep/__init__.py
ADDED
peep/client.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Peep API client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
_TERMINAL = ("completed", "failed", "cancelled", "canceled", "error")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PeepError(Exception):
|
|
14
|
+
"""Raised on any non-2xx response from the Peep API."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, status: int, code: str, message: str) -> None:
|
|
17
|
+
super().__init__(message)
|
|
18
|
+
self.status = status
|
|
19
|
+
self.code = code
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Peep:
|
|
23
|
+
"""Client for the Peep web scraping API.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
api_key: Your ``peep_live_*`` API key.
|
|
27
|
+
base_url: Override the API base URL.
|
|
28
|
+
|
|
29
|
+
Option keyword arguments are passed through verbatim, so use the API's
|
|
30
|
+
camelCase names — e.g. ``peep.scrape(url, onlyMainContent=True)``.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
api_key: str,
|
|
36
|
+
base_url: str = "https://peep.shownomore.com",
|
|
37
|
+
) -> None:
|
|
38
|
+
if not api_key:
|
|
39
|
+
raise ValueError("Peep: api_key is required.")
|
|
40
|
+
self.api_key = api_key
|
|
41
|
+
self.base_url = base_url.rstrip("/")
|
|
42
|
+
self.last_credits: Dict[str, Optional[int]] = {"used": None, "remaining": None}
|
|
43
|
+
self._session = requests.Session()
|
|
44
|
+
|
|
45
|
+
# ── Internals ───────────────────────────────────────────────
|
|
46
|
+
def _request(
|
|
47
|
+
self, method: str, path: str, body: Optional[Dict[str, Any]] = None
|
|
48
|
+
) -> Dict[str, Any]:
|
|
49
|
+
res = self._session.request(
|
|
50
|
+
method,
|
|
51
|
+
f"{self.base_url}{path}",
|
|
52
|
+
headers={"Authorization": f"Bearer {self.api_key}"},
|
|
53
|
+
json=body,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
used = res.headers.get("x-peep-credits-used")
|
|
57
|
+
remaining = res.headers.get("x-peep-credits-remaining")
|
|
58
|
+
self.last_credits = {
|
|
59
|
+
"used": int(used) if used is not None else None,
|
|
60
|
+
"remaining": int(remaining) if remaining is not None else None,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
data = res.json() if res.text else {}
|
|
64
|
+
if not res.ok:
|
|
65
|
+
err = data.get("error", {}) if isinstance(data, dict) else {}
|
|
66
|
+
raise PeepError(
|
|
67
|
+
res.status_code,
|
|
68
|
+
err.get("code", "UNKNOWN"),
|
|
69
|
+
err.get("message", f"Request failed with status {res.status_code}"),
|
|
70
|
+
)
|
|
71
|
+
return data
|
|
72
|
+
|
|
73
|
+
def _start_and_wait(
|
|
74
|
+
self,
|
|
75
|
+
start: Callable[[], Dict[str, Any]],
|
|
76
|
+
get_status: Callable[[str], Dict[str, Any]],
|
|
77
|
+
poll_interval: float,
|
|
78
|
+
timeout: float,
|
|
79
|
+
) -> Dict[str, Any]:
|
|
80
|
+
job = start()
|
|
81
|
+
job_id = job.get("jobId")
|
|
82
|
+
if not job_id:
|
|
83
|
+
return job
|
|
84
|
+
deadline = time.time() + timeout
|
|
85
|
+
last = get_status(job_id)
|
|
86
|
+
while time.time() < deadline:
|
|
87
|
+
if str(last.get("status", "")).lower() in _TERMINAL:
|
|
88
|
+
return last
|
|
89
|
+
time.sleep(poll_interval)
|
|
90
|
+
last = get_status(job_id)
|
|
91
|
+
return last # timed out — return the latest snapshot
|
|
92
|
+
|
|
93
|
+
# ── Sync endpoints ──────────────────────────────────────────
|
|
94
|
+
def scrape(self, url: str, **options: Any) -> Dict[str, Any]:
|
|
95
|
+
return self._request("POST", "/api/v1/scrape", {"url": url, **options})
|
|
96
|
+
|
|
97
|
+
def youtube(self, url: str) -> Dict[str, Any]:
|
|
98
|
+
return self.scrape(url, formats=["markdown"])
|
|
99
|
+
|
|
100
|
+
def map(self, url: str, **options: Any) -> Dict[str, Any]:
|
|
101
|
+
return self._request("POST", "/api/v1/map", {"url": url, **options})
|
|
102
|
+
|
|
103
|
+
def search(self, query: str, **options: Any) -> Dict[str, Any]:
|
|
104
|
+
return self._request("POST", "/api/v1/search", {"query": query, **options})
|
|
105
|
+
|
|
106
|
+
# ── Async endpoints ─────────────────────────────────────────
|
|
107
|
+
def crawl(self, url: str, **options: Any) -> Dict[str, Any]:
|
|
108
|
+
return self._request("POST", "/api/v1/crawl", {"url": url, **options})
|
|
109
|
+
|
|
110
|
+
def get_crawl(self, job_id: str) -> Dict[str, Any]:
|
|
111
|
+
return self._request("GET", f"/api/v1/crawl/{job_id}")
|
|
112
|
+
|
|
113
|
+
def crawl_and_wait(
|
|
114
|
+
self, url: str, poll_interval: float = 2.5, timeout: float = 300, **options: Any
|
|
115
|
+
) -> Dict[str, Any]:
|
|
116
|
+
return self._start_and_wait(
|
|
117
|
+
lambda: self.crawl(url, **options), self.get_crawl, poll_interval, timeout
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def batch_scrape(
|
|
121
|
+
self, urls: List[str], scrape_options: Optional[Dict[str, Any]] = None
|
|
122
|
+
) -> Dict[str, Any]:
|
|
123
|
+
return self._request(
|
|
124
|
+
"POST",
|
|
125
|
+
"/api/v1/batch/scrape",
|
|
126
|
+
{"urls": urls, "scrapeOptions": scrape_options or {}},
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def get_batch(self, job_id: str) -> Dict[str, Any]:
|
|
130
|
+
return self._request("GET", f"/api/v1/batch/scrape/{job_id}")
|
|
131
|
+
|
|
132
|
+
def batch_scrape_and_wait(
|
|
133
|
+
self,
|
|
134
|
+
urls: List[str],
|
|
135
|
+
scrape_options: Optional[Dict[str, Any]] = None,
|
|
136
|
+
poll_interval: float = 2.5,
|
|
137
|
+
timeout: float = 300,
|
|
138
|
+
) -> Dict[str, Any]:
|
|
139
|
+
return self._start_and_wait(
|
|
140
|
+
lambda: self.batch_scrape(urls, scrape_options),
|
|
141
|
+
self.get_batch,
|
|
142
|
+
poll_interval,
|
|
143
|
+
timeout,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def extract(self, urls: List[str], **options: Any) -> Dict[str, Any]:
|
|
147
|
+
return self._request("POST", "/api/v1/extract", {"urls": urls, **options})
|
|
148
|
+
|
|
149
|
+
def get_extract(self, job_id: str) -> Dict[str, Any]:
|
|
150
|
+
return self._request("GET", f"/api/v1/extract/{job_id}")
|
|
151
|
+
|
|
152
|
+
def extract_and_wait(
|
|
153
|
+
self,
|
|
154
|
+
urls: List[str],
|
|
155
|
+
poll_interval: float = 2.5,
|
|
156
|
+
timeout: float = 300,
|
|
157
|
+
**options: Any,
|
|
158
|
+
) -> Dict[str, Any]:
|
|
159
|
+
return self._start_and_wait(
|
|
160
|
+
lambda: self.extract(urls, **options),
|
|
161
|
+
self.get_extract,
|
|
162
|
+
poll_interval,
|
|
163
|
+
timeout,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def agent(self, prompt: str, **options: Any) -> Dict[str, Any]:
|
|
167
|
+
return self._request("POST", "/api/v1/agent", {"prompt": prompt, **options})
|
|
168
|
+
|
|
169
|
+
def get_agent(self, job_id: str) -> Dict[str, Any]:
|
|
170
|
+
return self._request("GET", f"/api/v1/agent/{job_id}")
|
|
171
|
+
|
|
172
|
+
def agent_and_wait(
|
|
173
|
+
self,
|
|
174
|
+
prompt: str,
|
|
175
|
+
poll_interval: float = 2.5,
|
|
176
|
+
timeout: float = 300,
|
|
177
|
+
**options: Any,
|
|
178
|
+
) -> Dict[str, Any]:
|
|
179
|
+
return self._start_and_wait(
|
|
180
|
+
lambda: self.agent(prompt, **options),
|
|
181
|
+
self.get_agent,
|
|
182
|
+
poll_interval,
|
|
183
|
+
timeout,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# ── Credits ─────────────────────────────────────────────────
|
|
187
|
+
def credits(self) -> Dict[str, Any]:
|
|
188
|
+
return self._request("GET", "/api/v1/credits")
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: peep-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for the Peep web scraping API — scrape, crawl, map, search, extract, and agent.
|
|
5
|
+
Project-URL: Homepage, https://peep.shownomore.com
|
|
6
|
+
Project-URL: Documentation, https://peep.shownomore.com/docs
|
|
7
|
+
License: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: api,crawler,peep,scraper,sdk,web-scraping
|
|
10
|
+
Requires-Python: >=3.8
|
|
11
|
+
Requires-Dist: requests>=2.25
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# peep-sdk (Python)
|
|
15
|
+
|
|
16
|
+
Official Python SDK for the [Peep](https://peep.shownomore.com) web scraping
|
|
17
|
+
API. Scrape, crawl, map, search, extract, and run autonomous agents. Every call
|
|
18
|
+
draws from your shared **Peep Card** credit balance.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
From PyPI with pip, Poetry, or uv:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install peep-sdk
|
|
26
|
+
poetry add peep-sdk
|
|
27
|
+
uv add peep-sdk
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
import os
|
|
34
|
+
from peep import Peep
|
|
35
|
+
|
|
36
|
+
peep = Peep(api_key=os.environ["PEEP_API_KEY"])
|
|
37
|
+
|
|
38
|
+
# Scrape a page to markdown
|
|
39
|
+
res = peep.scrape("https://example.com", formats=["markdown"])
|
|
40
|
+
print(res["data"]["markdown"])
|
|
41
|
+
print(peep.last_credits) # {"used": 1, "remaining": 499}
|
|
42
|
+
|
|
43
|
+
# Crawl a site and wait for completion
|
|
44
|
+
crawl = peep.crawl_and_wait("https://example.com", limit=50)
|
|
45
|
+
|
|
46
|
+
# Search the web
|
|
47
|
+
hits = peep.search("best web scraping api", limit=10)
|
|
48
|
+
|
|
49
|
+
# AI structured extraction
|
|
50
|
+
data = peep.extract_and_wait(
|
|
51
|
+
["https://example.com"],
|
|
52
|
+
prompt="Extract the company name and pricing tiers.",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Autonomous harvesting
|
|
56
|
+
leads = peep.agent_and_wait(
|
|
57
|
+
"Find flats for rent in Vivek Vihar with phone numbers and photos",
|
|
58
|
+
maxSources=10,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Check your balance
|
|
62
|
+
print(peep.credits()["balance"])
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
> Option keyword arguments are passed through verbatim — use the API's
|
|
66
|
+
> camelCase names, e.g. `peep.scrape(url, onlyMainContent=True)`.
|
|
67
|
+
|
|
68
|
+
## Methods
|
|
69
|
+
|
|
70
|
+
| Method | Endpoint | Notes |
|
|
71
|
+
|--------|----------|-------|
|
|
72
|
+
| `scrape(url, **options)` | `POST /scrape` | Sync. Pass a YouTube URL for video intelligence. |
|
|
73
|
+
| `youtube(url)` | `POST /scrape` | Sugar for YouTube videos. |
|
|
74
|
+
| `map(url, **options)` | `POST /map` | Sync URL discovery. |
|
|
75
|
+
| `search(query, **options)` | `POST /search` | Sync. |
|
|
76
|
+
| `crawl(...)` / `crawl_and_wait(...)` | `POST /crawl` | Async; `*_and_wait` polls to completion. |
|
|
77
|
+
| `batch_scrape(...)` / `batch_scrape_and_wait(...)` | `POST /batch/scrape` | Async. |
|
|
78
|
+
| `extract(...)` / `extract_and_wait(...)` | `POST /extract` | Async. |
|
|
79
|
+
| `agent(...)` / `agent_and_wait(...)` | `POST /agent` | Async. |
|
|
80
|
+
| `credits()` | `GET /credits` | Free. Balance + ledger. |
|
|
81
|
+
|
|
82
|
+
Non-2xx responses raise `PeepError` with `.status` and `.code`. After every
|
|
83
|
+
call, `peep.last_credits` holds `{"used", "remaining"}` from the response
|
|
84
|
+
headers.
|
|
85
|
+
|
|
86
|
+
## License
|
|
87
|
+
|
|
88
|
+
MIT
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
peep/__init__.py,sha256=U3bX1Vh32NXjuBVQx9kTPbxsML6aneNjFvQeyCFkuig,184
|
|
2
|
+
peep/client.py,sha256=zjiwNP0i-Iw-V0fSdghdwCl0Dc7TCkE8Qa26ae7yiag,6747
|
|
3
|
+
peep_sdk-0.1.0.dist-info/METADATA,sha256=V03JNULodDSuda_jzIrvzQlPwOZX1a91ohOAeQZmpnA,2667
|
|
4
|
+
peep_sdk-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
5
|
+
peep_sdk-0.1.0.dist-info/licenses/LICENSE,sha256=syLja01AQImdrcv-RbSrnpqP_jRBYpbj3MoL_kWYi_s,1067
|
|
6
|
+
peep_sdk-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ShowNoMore
|
|
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.
|