mrscraper-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.
mrscraper/__init__.py ADDED
@@ -0,0 +1,31 @@
1
+ """
2
+ MrScraper Python SDK
3
+
4
+ A clean Python client for the `MrScraper <https://mrscraper.com>`_ API.
5
+ Supports async/await usage.
6
+
7
+ Quickstart::
8
+
9
+ import asyncio
10
+ from mrscraper import MrScraper
11
+
12
+ async def main():
13
+ client = MrScraper(token="atk_your_token_here")
14
+ result = await client.fetch_html("https://example.com")
15
+ print(result["data"])
16
+
17
+ asyncio.run(main())
18
+ """
19
+
20
+ from .mrscraper import MrScraper
21
+ from .exceptions import APIError, AuthenticationError, MrScraperError, NetworkError
22
+
23
+ __all__ = [
24
+ "MrScraper",
25
+ "MrScraperError",
26
+ "AuthenticationError",
27
+ "APIError",
28
+ "NetworkError",
29
+ ]
30
+
31
+ __version__ = "0.1.0"
@@ -0,0 +1,23 @@
1
+ """Custom exceptions for the MrScraper SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class MrScraperError(Exception):
7
+ """Base exception for all MrScraper SDK errors."""
8
+
9
+
10
+ class AuthenticationError(MrScraperError):
11
+ """Raised when the API token is invalid or missing."""
12
+
13
+
14
+ class APIError(MrScraperError):
15
+ """Raised when the MrScraper API returns an error response."""
16
+
17
+ def __init__(self, message: str, status_code: int | None = None) -> None:
18
+ super().__init__(message)
19
+ self.status_code = status_code
20
+
21
+
22
+ class NetworkError(MrScraperError):
23
+ """Raised when a network-level error occurs (timeout, connection failure, etc.)."""
mrscraper/mrscraper.py ADDED
@@ -0,0 +1,549 @@
1
+ """Async client for the MrScraper API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Literal
6
+ from urllib.parse import urlencode
7
+
8
+ import httpx
9
+
10
+ from .exceptions import APIError, AuthenticationError, NetworkError
11
+
12
+ _FETCH_BASE_URL = "https://api.mrscraper.com"
13
+ _API_BASE_URL = "https://api.app.mrscraper.com/api/v1"
14
+
15
+ _DEFAULT_TIMEOUT = 120
16
+
17
+
18
+ class MrScraper:
19
+ """
20
+ Async client for the MrScraper API.
21
+
22
+ All methods are coroutines and must be awaited.
23
+
24
+ Args:
25
+ token: Your MrScraper API token. Get yours at https://app.mrscraper.com.
26
+ http_client: Optional custom ``httpx.AsyncClient`` instance. If not
27
+ provided a new one is created per request.
28
+
29
+ Example::
30
+
31
+ import asyncio
32
+ from mrscraper import AsyncMrScraper
33
+
34
+ async def main():
35
+ client = AsyncMrScraper(token="atk_...")
36
+ html = await client.fetch_html("https://example.com")
37
+ print(html["data"])
38
+
39
+ asyncio.run(main())
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ token: str,
45
+ *,
46
+ http_client: httpx.AsyncClient | None = None,
47
+ ) -> None:
48
+ self._token = token
49
+ self._http_client = http_client
50
+
51
+ def _auth_headers(self) -> dict[str, str]:
52
+ return {
53
+ "Content-Type": "application/json",
54
+ "accept": "application/json",
55
+ "x-api-token": self._token,
56
+ }
57
+
58
+ def _client(self) -> httpx.AsyncClient:
59
+ if self._http_client is not None:
60
+ return self._http_client
61
+ return httpx.AsyncClient()
62
+
63
+ async def _get(
64
+ self,
65
+ url: str,
66
+ *,
67
+ headers: dict[str, str] | None = None,
68
+ timeout: float = float(_DEFAULT_TIMEOUT + 30),
69
+ ) -> dict[str, Any]:
70
+ async with self._client() as client:
71
+ try:
72
+ response = await client.get(url, headers=headers, timeout=timeout)
73
+ response.raise_for_status()
74
+ return self._parse(response)
75
+ except httpx.HTTPStatusError as exc:
76
+ self._raise_for_status(exc)
77
+ raise # unreachable; satisfies type checker
78
+ except httpx.HTTPError as exc:
79
+ raise NetworkError(str(exc)) from exc
80
+
81
+ async def _post(
82
+ self,
83
+ url: str,
84
+ *,
85
+ payload: dict[str, Any],
86
+ timeout: float = 600.0,
87
+ ) -> dict[str, Any]:
88
+ async with self._client() as client:
89
+ try:
90
+ response = await client.post(
91
+ url,
92
+ headers=self._auth_headers(),
93
+ json=payload,
94
+ timeout=timeout,
95
+ )
96
+ response.raise_for_status()
97
+ return self._parse(response)
98
+ except httpx.HTTPStatusError as exc:
99
+ self._raise_for_status(exc)
100
+ raise # unreachable; satisfies type checker
101
+ except httpx.HTTPError as exc:
102
+ raise NetworkError(str(exc)) from exc
103
+
104
+ @staticmethod
105
+ def _parse(response: httpx.Response) -> dict[str, Any]:
106
+ if response.status_code == 401:
107
+ raise AuthenticationError(
108
+ "Unauthorized or invalid token. "
109
+ "Please go to https://app.mrscraper.com to get your token."
110
+ )
111
+
112
+ content_type = response.headers.get("content-type", "").lower()
113
+ data: Any = response.json() if "application/json" in content_type else response.text
114
+ return {
115
+ "status_code": response.status_code,
116
+ "data": data,
117
+ "headers": dict(response.headers),
118
+ }
119
+
120
+ @staticmethod
121
+ def _raise_for_status(exc: httpx.HTTPStatusError) -> None:
122
+ status_code = exc.response.status_code
123
+ if status_code == 401:
124
+ raise AuthenticationError(
125
+ "Unauthorized or invalid token. "
126
+ "Please go to https://app.mrscraper.com to get your token."
127
+ ) from exc
128
+ raise APIError(str(exc), status_code=status_code) from exc
129
+
130
+ # ------------------------------------------------------------------
131
+ # Public API
132
+ # ------------------------------------------------------------------
133
+
134
+ async def fetch_html(
135
+ self,
136
+ url: str,
137
+ *,
138
+ timeout: int = _DEFAULT_TIMEOUT,
139
+ geo_code: str = "US",
140
+ block_resources: bool = False,
141
+ ) -> dict[str, Any]:
142
+ """
143
+ Fetch the rendered HTML of a page via the MrScraper stealth browser.
144
+
145
+ The service handles JavaScript rendering, bot detection evasion, and
146
+ optional geolocation proxying for you.
147
+
148
+ Args:
149
+ url: Target URL to scrape (e.g. ``"https://example.com/page"``).
150
+ timeout: Maximum seconds to wait for the page to load (default 120).
151
+ geo_code: ISO country code for proxy-based geolocation (default ``"US"``).
152
+ Examples: ``"US"``, ``"GB"``, ``"ID"``, ``"SG"``.
153
+ block_resources: When ``True``, images, CSS, and fonts are blocked to
154
+ speed up the request (default ``False``).
155
+
156
+ Returns:
157
+ A dict with keys ``status_code``, ``data`` (raw HTML or JSON), and
158
+ ``headers``.
159
+
160
+ Raises:
161
+ AuthenticationError: If the API token is invalid.
162
+ APIError: If the API returns a non-2xx error.
163
+ NetworkError: If a connection or timeout error occurs.
164
+
165
+ Example::
166
+
167
+ result = await client.fetch_html(
168
+ "https://stockx.com/air-jordan-1-retro-low-og-chicago-2025",
169
+ geo_code="US",
170
+ timeout=120,
171
+ )
172
+ print(result["data"]) # raw HTML string
173
+ """
174
+ params = {
175
+ "token": self._token,
176
+ "timeout": timeout,
177
+ "geoCode": geo_code,
178
+ "url": url,
179
+ "blockResources": str(block_resources).lower(),
180
+ }
181
+ full_url = f"{_FETCH_BASE_URL}?{urlencode(params)}"
182
+ return await self._get(full_url, timeout=float(timeout + 30))
183
+
184
+ async def create_scraper(
185
+ self,
186
+ url: str,
187
+ message: str,
188
+ *,
189
+ agent: Literal["general", "listing", "map"] = "general",
190
+ proxy_country: str | None = None,
191
+ max_depth: int = 2,
192
+ max_pages: int = 50,
193
+ limit: int = 1000,
194
+ include_patterns: str = "",
195
+ exclude_patterns: str = "",
196
+ ) -> dict[str, Any]:
197
+ """
198
+ Create an AI-powered scraper and run it immediately.
199
+
200
+ The scraper uses natural-language instructions to understand the page
201
+ structure and extract the requested data automatically — no CSS
202
+ selectors required.
203
+
204
+ Args:
205
+ url: Target URL to scrape.
206
+ message: Natural-language description of what to extract.
207
+ Examples: ``"Extract all product names and prices"``,
208
+ ``"Get article titles and publication dates"``.
209
+ agent: AI agent type (default ``"general"``).
210
+
211
+ * ``"general"`` — works on almost any page; best default.
212
+ * ``"listing"`` — optimised for listing/grid pages.
213
+ * ``"map"`` — crawls all sub-pages of a site.
214
+ proxy_country: ISO country code for proxy selection (optional).
215
+ Examples: ``"US"``, ``"GB"``, ``"SG"``.
216
+ max_depth: *(map agent only)* Crawl depth from the start URL
217
+ (default 2). 0 = start URL only.
218
+ max_pages: *(map agent only)* Maximum pages to process (default 50).
219
+ limit: *(map agent only)* Maximum records to extract (default 1000).
220
+ include_patterns: *(map agent only)* ``||``-separated URL regex
221
+ patterns to include when following links.
222
+ exclude_patterns: *(map agent only)* ``||``-separated URL regex
223
+ patterns to skip when following links.
224
+
225
+ Returns:
226
+ A dict with keys ``status_code``, ``data`` (scraper info including
227
+ the scraper ID), and ``headers``.
228
+
229
+ Raises:
230
+ AuthenticationError: If the API token is invalid.
231
+ APIError: If the API returns a non-2xx error.
232
+ NetworkError: If a connection or timeout error occurs.
233
+
234
+ Example::
235
+
236
+ result = await client.create_scraper(
237
+ "https://example.com/products",
238
+ "Extract all product names, prices, and ratings",
239
+ agent="listing",
240
+ proxy_country="US",
241
+ )
242
+ scraper_id = result["data"]["id"]
243
+ """
244
+ endpoint = f"{_API_BASE_URL}/scrapers-ai"
245
+
246
+ if agent in ("general", "listing"):
247
+ payload: dict[str, Any] = {
248
+ "url": url,
249
+ "message": message,
250
+ "agent": agent,
251
+ "proxyCountry": proxy_country,
252
+ }
253
+ else: # map
254
+ payload = {
255
+ "url": url,
256
+ "agent": agent,
257
+ "maxDepth": max_depth,
258
+ "maxPages": max_pages,
259
+ "limit": limit,
260
+ "includePatterns": include_patterns,
261
+ "excludePatterns": exclude_patterns,
262
+ }
263
+
264
+ return await self._post(endpoint, payload=payload)
265
+
266
+ async def rerun_scraper(
267
+ self,
268
+ scraper_id: str,
269
+ url: str,
270
+ *,
271
+ max_depth: int = 2,
272
+ max_pages: int = 50,
273
+ limit: int = 1000,
274
+ include_patterns: str = "",
275
+ exclude_patterns: str = "",
276
+ ) -> dict[str, Any]:
277
+ """
278
+ Rerun an existing AI scraper on a (new) URL.
279
+
280
+ This lets you reuse the extraction logic from a previously created
281
+ scraper on any compatible URL.
282
+
283
+ Args:
284
+ scraper_id: ID of the scraper to rerun (from :meth:`create_scraper`).
285
+ url: Target URL. Can be the original URL or a different page.
286
+ max_depth: *(map agent only)* Crawl depth (default 2).
287
+ max_pages: *(map agent only)* Maximum pages (default 50).
288
+ limit: *(map agent only)* Maximum records (default 1000).
289
+ include_patterns: *(map agent only)* ``||``-separated include patterns.
290
+ exclude_patterns: *(map agent only)* ``||``-separated exclude patterns.
291
+
292
+ Returns:
293
+ A dict with keys ``status_code``, ``data``, and ``headers``.
294
+
295
+ Raises:
296
+ AuthenticationError: If the API token is invalid.
297
+ APIError: If the API returns a non-2xx error.
298
+ NetworkError: If a connection or timeout error occurs.
299
+
300
+ Example::
301
+
302
+ result = await client.rerun_scraper(
303
+ scraper_id="scraper_12345",
304
+ url="https://example.com/category/electronics",
305
+ max_depth=3,
306
+ max_pages=100,
307
+ )
308
+ """
309
+ endpoint = f"{_API_BASE_URL}/scrapers-ai-rerun"
310
+ payload: dict[str, Any] = {
311
+ "scraperId": scraper_id,
312
+ "url": url,
313
+ "maxDepth": max_depth,
314
+ "maxPages": max_pages,
315
+ "limit": limit,
316
+ "includePatterns": include_patterns,
317
+ "excludePatterns": exclude_patterns,
318
+ }
319
+ return await self._post(endpoint, payload=payload)
320
+
321
+ async def bulk_rerun_scraper(
322
+ self,
323
+ scraper_id: str,
324
+ urls: list[str],
325
+ ) -> dict[str, Any]:
326
+ """
327
+ Rerun an existing AI scraper on multiple URLs in a single batch request.
328
+
329
+ More efficient than calling :meth:`rerun_scraper` in a loop because all
330
+ URLs are dispatched in parallel server-side.
331
+
332
+ Args:
333
+ scraper_id: ID of the scraper to rerun (from :meth:`create_scraper`).
334
+ urls: List of target URLs. Must contain at least one URL.
335
+
336
+ Returns:
337
+ A dict with keys ``status_code``, ``data``, and ``headers``.
338
+
339
+ Raises:
340
+ AuthenticationError: If the API token is invalid.
341
+ APIError: If the API returns a non-2xx error.
342
+ NetworkError: If a connection or timeout error occurs.
343
+
344
+ Example::
345
+
346
+ result = await client.bulk_rerun_scraper(
347
+ scraper_id="scraper_12345",
348
+ urls=[
349
+ "https://example.com/products/item1",
350
+ "https://example.com/products/item2",
351
+ ],
352
+ )
353
+ """
354
+ endpoint = f"{_API_BASE_URL}/scrapers-ai-rerun/bulk"
355
+ payload: dict[str, Any] = {
356
+ "scraperId": scraper_id,
357
+ "urls": urls,
358
+ }
359
+ return await self._post(endpoint, payload=payload)
360
+
361
+ async def rerun_manual_scraper(
362
+ self,
363
+ scraper_id: str,
364
+ url: str,
365
+ ) -> dict[str, Any]:
366
+ """
367
+ Rerun a manually configured scraper (created via the MrScraper dashboard)
368
+ on a new URL.
369
+
370
+ Use this for scrapers built with custom CSS selectors or XPath rules, not
371
+ for AI scrapers created with :meth:`create_scraper`.
372
+
373
+ Args:
374
+ scraper_id: ID of the manual scraper (found in the MrScraper dashboard).
375
+ url: Target URL. The page structure should match the original scraper's
376
+ target for selectors to work correctly.
377
+
378
+ Returns:
379
+ A dict with keys ``status_code``, ``data``, and ``headers``.
380
+
381
+ Raises:
382
+ AuthenticationError: If the API token is invalid.
383
+ APIError: If the API returns a non-2xx error.
384
+ NetworkError: If a connection or timeout error occurs.
385
+
386
+ Example::
387
+
388
+ result = await client.rerun_manual_scraper(
389
+ scraper_id="manual_scraper_67890",
390
+ url="https://example.com/products/new-item",
391
+ )
392
+ """
393
+ endpoint = f"{_API_BASE_URL}/scrapers-manual-rerun"
394
+ payload: dict[str, Any] = {
395
+ "scraperId": scraper_id,
396
+ "url": url,
397
+ }
398
+ return await self._post(endpoint, payload=payload)
399
+
400
+ async def bulk_rerun_manual_scraper(
401
+ self,
402
+ scraper_id: str,
403
+ urls: list[str],
404
+ ) -> dict[str, Any]:
405
+ """
406
+ Rerun a manually configured scraper on multiple URLs in a single batch.
407
+
408
+ More efficient than calling :meth:`rerun_manual_scraper` multiple times,
409
+ as all URLs are processed in parallel server-side. Ideal for scraping
410
+ multiple pages, products, or articles with the same extraction logic.
411
+
412
+ Args:
413
+ scraper_id: ID of the manual scraper (from the MrScraper dashboard).
414
+ Must be a scraper created manually via the web interface,
415
+ not an AI scraper. Find it at https://app.mrscraper.com.
416
+ urls: List of target URLs to scrape. Must contain at least one URL.
417
+ Each URL is processed independently using the scraper's logic.
418
+ Example: ``["https://example.com/page1", "https://example.com/page2"]``.
419
+
420
+ Returns:
421
+ A dict with keys ``status_code``, ``data`` (bulk job info including job ID,
422
+ status, metadata; use :meth:`get_all_results` or :meth:`get_result_by_id`
423
+ to fetch per-URL results), and ``headers``.
424
+
425
+ Raises:
426
+ AuthenticationError: If the API token is invalid.
427
+ APIError: If the API returns a non-2xx error.
428
+ NetworkError: If a connection or timeout error occurs.
429
+
430
+ Example::
431
+
432
+ result = await client.bulk_rerun_manual_scraper(
433
+ scraper_id="scraper_12345",
434
+ urls=[
435
+ "https://www.example.com/products/item1",
436
+ "https://www.example.com/products/item2",
437
+ "https://www.example.com/products/item3",
438
+ ],
439
+ )
440
+ """
441
+ endpoint = f"{_API_BASE_URL}/scrapers-manual-rerun/bulk"
442
+ payload: dict[str, Any] = {
443
+ "scraperId": scraper_id,
444
+ "urls": urls,
445
+ }
446
+ return await self._post(endpoint, payload=payload, timeout=600.0)
447
+
448
+ async def get_all_results(
449
+ self,
450
+ *,
451
+ sort_field: Literal[
452
+ "createdAt",
453
+ "updatedAt",
454
+ "id",
455
+ "type",
456
+ "url",
457
+ "status",
458
+ "error",
459
+ "tokenUsage",
460
+ "runtime",
461
+ ] = "updatedAt",
462
+ sort_order: Literal["ASC", "DESC"] = "DESC",
463
+ page_size: int = 10,
464
+ page: int = 1,
465
+ search: str | None = None,
466
+ date_range_column: str | None = None,
467
+ start_at: str | None = None,
468
+ end_at: str | None = None,
469
+ ) -> dict[str, Any]:
470
+ """
471
+ Retrieve a paginated, sortable, and filterable list of all scraping results.
472
+
473
+ Args:
474
+ sort_field: Field to sort by (default ``"updatedAt"``).
475
+ Options: ``"createdAt"``, ``"updatedAt"``, ``"id"``,
476
+ ``"type"``, ``"url"``, ``"status"``, ``"error"``,
477
+ ``"tokenUsage"``, ``"runtime"``.
478
+ sort_order: ``"ASC"`` or ``"DESC"`` (default ``"DESC"``).
479
+ page_size: Results per page (default 10).
480
+ page: Page number, 1-indexed (default 1).
481
+ search: Free-text search query across result fields (optional).
482
+ date_range_column: Column name to filter by date range (optional).
483
+ Common values: ``"updatedAt"``, ``"createdAt"``.
484
+ start_at: ISO-8601 start date for ``date_range_column`` filter (optional).
485
+ Example: ``"2024-01-01"`` or ``"2024-01-01T00:00:00"``.
486
+ end_at: ISO-8601 end date for ``date_range_column`` filter (optional).
487
+
488
+ Returns:
489
+ A dict with keys ``status_code``, ``data`` (paginated results and
490
+ pagination metadata), and ``headers``.
491
+
492
+ Raises:
493
+ AuthenticationError: If the API token is invalid.
494
+ APIError: If the API returns a non-2xx error.
495
+ NetworkError: If a connection or timeout error occurs.
496
+
497
+ Example::
498
+
499
+ page = await client.get_all_results(
500
+ sort_field="updatedAt",
501
+ sort_order="DESC",
502
+ page_size=20,
503
+ date_range_column="updatedAt",
504
+ start_at="2024-01-01",
505
+ end_at="2024-01-08",
506
+ )
507
+ """
508
+ params: dict[str, Any] = {
509
+ "sortField": sort_field,
510
+ "sortOrder": sort_order,
511
+ "pageSize": page_size,
512
+ "page": page,
513
+ }
514
+ if search is not None:
515
+ params["search"] = search
516
+ if date_range_column is not None:
517
+ params["dateRangeColumn"] = date_range_column
518
+ if start_at is not None:
519
+ params["startAt"] = start_at
520
+ if end_at is not None:
521
+ params["endAt"] = end_at
522
+
523
+ url = f"{_API_BASE_URL}/results?{urlencode(params)}"
524
+ return await self._get(url, headers=self._auth_headers(), timeout=600.0)
525
+
526
+ async def get_result_by_id(self, result_id: str) -> dict[str, Any]:
527
+ """
528
+ Retrieve full details of a specific scraping result by its ID.
529
+
530
+ Args:
531
+ result_id: Unique identifier of the result. Result IDs are returned
532
+ by scraper execution methods and by :meth:`get_all_results`.
533
+
534
+ Returns:
535
+ A dict with keys ``status_code``, ``data`` (complete result object),
536
+ and ``headers``.
537
+
538
+ Raises:
539
+ AuthenticationError: If the API token is invalid.
540
+ APIError: If the API returns a non-2xx error.
541
+ NetworkError: If a connection or timeout error occurs.
542
+
543
+ Example::
544
+
545
+ result = await client.get_result_by_id("result_12345")
546
+ print(result["data"])
547
+ """
548
+ url = f"{_API_BASE_URL}/results/{result_id}"
549
+ return await self._get(url, headers=self._auth_headers(), timeout=600.0)
@@ -0,0 +1,274 @@
1
+ Metadata-Version: 2.4
2
+ Name: mrscraper-sdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK for the MrScraper web-scraping API
5
+ Project-URL: Homepage, https://mrscraper.com
6
+ Project-URL: Documentation, https://docs.mrscraper.com
7
+ Project-URL: Repository, https://github.com/mrscraper/mrscraper-python
8
+ Project-URL: Bug Tracker, https://github.com/mrscraper/mrscraper-python/issues
9
+ Author: MrScraper
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: api,mrscraper,scraping,sdk,web scraping
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Internet :: WWW/HTTP
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.9
26
+ Requires-Dist: httpx>=0.27
27
+ Provides-Extra: dev
28
+ Requires-Dist: mypy>=1.10; extra == 'dev'
29
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
30
+ Requires-Dist: pytest>=8; extra == 'dev'
31
+ Requires-Dist: respx>=0.21; extra == 'dev'
32
+ Requires-Dist: ruff>=0.4; extra == 'dev'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # MrScraper Python SDK
36
+
37
+ A clean, typed Python client for the [MrScraper](https://mrscraper.com) web-scraping API.
38
+ Supports **async/await** usage.
39
+
40
+ ---
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ pip install mrscraper-sdk
46
+ ```
47
+
48
+ Requires Python 3.9+.
49
+
50
+ ---
51
+
52
+ ## Authentication
53
+
54
+ Every client is initialised with your MrScraper API token.
55
+ Get yours at <https://app.mrscraper.com>.
56
+
57
+ ```python
58
+ from mrscraper import MrScraper
59
+
60
+ client = MrScraper(token="atk_your_token_here")
61
+ ```
62
+
63
+ ---
64
+
65
+ ## Quick Start
66
+
67
+ ### Fetch raw HTML (stealth browser)
68
+
69
+ ```python
70
+ import asyncio
71
+ from mrscraper import MrScraper
72
+
73
+ async def main():
74
+ client = MrScraper(token="atk_your_token_here")
75
+
76
+ result = await client.fetch_html(
77
+ "https://stockx.com/air-jordan-1-retro-low-og-chicago-2025",
78
+ geo_code="US",
79
+ timeout=120,
80
+ block_resources=False,
81
+ )
82
+ print(result["data"]) # raw HTML string
83
+
84
+ asyncio.run(main())
85
+ ```
86
+
87
+ ### Create an AI scraper
88
+
89
+ ```python
90
+ result = await client.create_scraper(
91
+ url="https://example.com/products",
92
+ message="Extract all product names, prices, and ratings",
93
+ agent="listing", # "general" | "listing" | "map"
94
+ proxy_country="US",
95
+ )
96
+ scraper_id = result["data"]["data"]["id"]
97
+ print("Scraper ID:", scraper_id)
98
+ ```
99
+
100
+ ### Rerun a scraper on a new URL
101
+
102
+ ```python
103
+ result = await client.rerun_scraper(
104
+ scraper_id=scraper_id,
105
+ url="https://example.com/products?page=2",
106
+ )
107
+ ```
108
+
109
+ ### Bulk rerun on multiple URLs (AI scraper)
110
+
111
+ ```python
112
+ result = await client.bulk_rerun_scraper(
113
+ scraper_id=scraper_id,
114
+ urls=[
115
+ "https://example.com/products/item1",
116
+ "https://example.com/products/item2",
117
+ "https://example.com/products/item3",
118
+ ],
119
+ )
120
+ ```
121
+
122
+ ### Rerun a manually configured scraper
123
+
124
+ ```python
125
+ result = await client.rerun_manual_scraper(
126
+ scraper_id="manual_scraper_67890",
127
+ url="https://example.com/products/new-item",
128
+ )
129
+ ```
130
+
131
+ ### Bulk rerun manual scraper on multiple URLs
132
+
133
+ ```python
134
+ result = await client.bulk_rerun_manual_scraper(
135
+ scraper_id="scraper_12345",
136
+ urls=[
137
+ "https://www.example.com/products/item1",
138
+ "https://www.example.com/products/item2",
139
+ "https://www.example.com/products/item3",
140
+ ],
141
+ )
142
+ ```
143
+
144
+ ### Retrieve results
145
+
146
+ ```python
147
+ # All results (paginated)
148
+ page = await client.get_all_results(
149
+ sort_field="updatedAt",
150
+ sort_order="DESC",
151
+ page_size=20,
152
+ page=1,
153
+ search="product",
154
+ date_range_column="updatedAt",
155
+ start_at="2024-01-01",
156
+ end_at="2024-01-31",
157
+ )
158
+ print(page["data"])
159
+
160
+ # A specific result by ID
161
+ result = await client.get_result_by_id("result_12345")
162
+ print(result["data"])
163
+ ```
164
+
165
+ ---
166
+
167
+ ## API Reference
168
+
169
+ ### `MrScraper`
170
+
171
+ All methods are coroutines and must be awaited.
172
+
173
+ | Method | Description |
174
+ |--------|-------------|
175
+ | `fetch_html(url, *, timeout, geo_code, block_resources)` | Fetch rendered HTML via the MrScraper stealth browser |
176
+ | `create_scraper(url, message, *, agent, proxy_country, ...)` | Create & run an AI-powered scraper |
177
+ | `rerun_scraper(scraper_id, url, *, max_depth, max_pages, limit, ...)` | Rerun an AI scraper on a new URL |
178
+ | `bulk_rerun_scraper(scraper_id, urls)` | Rerun an AI scraper on multiple URLs in one batch |
179
+ | `rerun_manual_scraper(scraper_id, url)` | Rerun a manually configured scraper on a single URL |
180
+ | `bulk_rerun_manual_scraper(scraper_id, urls)` | Rerun a manual scraper on multiple URLs in one batch |
181
+ | `get_all_results(*, sort_field, sort_order, page_size, page, search, ...)` | List all results with filtering & pagination |
182
+ | `get_result_by_id(result_id)` | Fetch a single result by its ID |
183
+
184
+ All methods return a `dict` with the following keys:
185
+
186
+ | Key | Type | Description |
187
+ |-----|------|-------------|
188
+ | `status_code` | `int` | HTTP status code |
189
+ | `data` | `Any` | Parsed JSON body or raw text |
190
+ | `headers` | `dict` | Response headers |
191
+
192
+ ### `bulk_rerun_manual_scraper`
193
+
194
+ Reruns a manually configured scraper on multiple URLs simultaneously in a single batch operation. This is more efficient than calling `rerun_manual_scraper` multiple times, as it processes all URLs in parallel and returns consolidated results. Ideal for scraping multiple pages, products, or articles with the same extraction logic.
195
+
196
+ | Argument | Description |
197
+ |----------|-------------|
198
+ | `scraper_id` | The ID of the manual scraper to rerun (obtained from the MrScraper dashboard). Must be a scraper created manually through the web interface, not an AI scraper. Find it at https://app.mrscraper.com |
199
+ | `urls` | A list of target URLs to scrape (required, must contain at least one URL). Each URL will be processed independently using the scraper's extraction logic. Example: `["https://example.com/page1", "https://example.com/page2"]` |
200
+
201
+ **Returns:** A dict with `status_code`, `data` (bulk job info including job ID, status, metadata; use `get_all_results` or `get_result_by_id` to fetch per-URL results), and `headers`.
202
+
203
+ **Example:**
204
+
205
+ ```python
206
+ result = await client.bulk_rerun_manual_scraper(
207
+ scraper_id="scraper_12345",
208
+ urls=[
209
+ "https://www.example.com/products/item1",
210
+ "https://www.example.com/products/item2",
211
+ "https://www.example.com/products/item3",
212
+ ],
213
+ )
214
+ ```
215
+
216
+ ### `create_scraper` — agent types
217
+
218
+ | Agent | Best used for |
219
+ |-------|---------------|
220
+ | `"general"` | Default; handles almost any page |
221
+ | `"listing"` | Product listings, job boards, search results |
222
+ | `"map"` | Crawling all sub-pages / sitemaps of a site |
223
+
224
+ The `max_depth`, `max_pages`, `limit`, `include_patterns`, and `exclude_patterns`
225
+ parameters are only meaningful when `agent="map"`.
226
+
227
+ ---
228
+
229
+ ## Exceptions
230
+
231
+ | Exception | Raised when |
232
+ |-----------|-------------|
233
+ | `MrScraperError` | Base class for all SDK errors |
234
+ | `AuthenticationError` | API token is invalid or missing (HTTP 401) |
235
+ | `APIError` | API returned a non-2xx error; has `.status_code` attribute |
236
+ | `NetworkError` | Connection timeout or network-level failure |
237
+
238
+ ```python
239
+ from mrscraper.exceptions import AuthenticationError, APIError, NetworkError
240
+
241
+ try:
242
+ result = await client.fetch_html("https://example.com")
243
+ except AuthenticationError:
244
+ print("Check your API token at https://app.mrscraper.com")
245
+ except APIError as e:
246
+ print(f"API error {e.status_code}: {e}")
247
+ except NetworkError as e:
248
+ print(f"Network problem: {e}")
249
+ ```
250
+
251
+ ---
252
+
253
+ ## Development
254
+
255
+ ```bash
256
+ # Install with dev dependencies
257
+ pip install -e ".[dev]"
258
+
259
+ # Run tests
260
+ pytest
261
+
262
+ # Lint & format
263
+ ruff check .
264
+ ruff format .
265
+
266
+ # Type check
267
+ mypy src/mrscraper
268
+ ```
269
+
270
+ ---
271
+
272
+ ## License
273
+
274
+ MIT © MrScraper
@@ -0,0 +1,7 @@
1
+ mrscraper/__init__.py,sha256=oz--B3PhxkUAxfc_q0xPHMPLI0dKEuCGDsVP5c9BDLw,656
2
+ mrscraper/exceptions.py,sha256=oixafdD4I2hzounileLFnQqFWPoSrsEbJ189gllzdno,656
3
+ mrscraper/mrscraper.py,sha256=eOISFlyHSQXgYzGdLp-7dLAFc5bk_uq-oupXrukmG8A,19943
4
+ mrscraper_sdk-0.1.0.dist-info/METADATA,sha256=AqnPxfpAHTQc3nzIZ-d1DjW82m7Z0VuYO3PqKXiIpKI,7880
5
+ mrscraper_sdk-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
6
+ mrscraper_sdk-0.1.0.dist-info/licenses/LICENSE,sha256=PLsi4oNgNysc0BISmsiErxXUTKWrzKHMNv0whTadqJI,1066
7
+ mrscraper_sdk-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 MrScraper
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.