olostep 0.9.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.
- olostep/.DS_Store +0 -0
- olostep/__init__.py +60 -0
- olostep/_log.py +238 -0
- olostep/backend/api_endpoints.py +323 -0
- olostep/backend/caller.py +394 -0
- olostep/backend/transport.py +211 -0
- olostep/backend/transport_protocol.py +45 -0
- olostep/clients/async_client.py +55 -0
- olostep/clients/sync_client.py +78 -0
- olostep/config.py +23 -0
- olostep/errors.py +411 -0
- olostep/frontend/_id_storage.py +74 -0
- olostep/frontend/batch_menu.py +187 -0
- olostep/frontend/client_state.py +1037 -0
- olostep/frontend/crawl_menu.py +200 -0
- olostep/frontend/input_coersion.py +36 -0
- olostep/frontend/retrieve_menu.py +89 -0
- olostep/frontend/scrape_menu.py +182 -0
- olostep/frontend/sitemap_menu.py +107 -0
- olostep/models/base.py +270 -0
- olostep/models/request.py +844 -0
- olostep/models/response.py +363 -0
- olostep-0.9.0.dist-info/METADATA +360 -0
- olostep-0.9.0.dist-info/RECORD +26 -0
- olostep-0.9.0.dist-info/WHEEL +4 -0
- olostep-0.9.0.dist-info/licenses/LICENSE +21 -0
olostep/.DS_Store
ADDED
|
Binary file
|
olostep/__init__.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Core clients
|
|
2
|
+
from .clients.async_client import OlostepClient
|
|
3
|
+
from .clients.sync_client import SyncOlostepClient
|
|
4
|
+
|
|
5
|
+
# Stateful result objects
|
|
6
|
+
from .frontend.client_state import ScrapeResult, BatchItem, CrawlPage, Crawl, CrawlInfo, Sitemap
|
|
7
|
+
|
|
8
|
+
# Type system
|
|
9
|
+
from .models.request import (
|
|
10
|
+
Format,
|
|
11
|
+
Country,
|
|
12
|
+
RetrieveFormat,
|
|
13
|
+
WaitAction,
|
|
14
|
+
ClickAction,
|
|
15
|
+
FillInputAction,
|
|
16
|
+
ScrollAction,
|
|
17
|
+
Parser,
|
|
18
|
+
LLMExtract,
|
|
19
|
+
LinksOnPage,
|
|
20
|
+
ScreenSize,
|
|
21
|
+
Transformer,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Error hierarchy
|
|
25
|
+
from . import errors
|
|
26
|
+
|
|
27
|
+
__version__ = "0.9.0"
|
|
28
|
+
|
|
29
|
+
# Get all error classes dynamically
|
|
30
|
+
_error_classes = [name for name in dir(errors) if not name.startswith('_') and name.endswith('Error')]
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
# Clients
|
|
34
|
+
"OlostepClient",
|
|
35
|
+
"SyncOlostepClient",
|
|
36
|
+
# Result objects
|
|
37
|
+
"ScrapeResult",
|
|
38
|
+
"BatchItem",
|
|
39
|
+
"CrawlPage",
|
|
40
|
+
"Crawl",
|
|
41
|
+
"CrawlInfo",
|
|
42
|
+
"Sitemap",
|
|
43
|
+
# Types
|
|
44
|
+
"Format",
|
|
45
|
+
"Country",
|
|
46
|
+
"RetrieveFormat",
|
|
47
|
+
"WaitAction",
|
|
48
|
+
"ClickAction",
|
|
49
|
+
"FillInputAction",
|
|
50
|
+
"ScrollAction",
|
|
51
|
+
"Parser",
|
|
52
|
+
"LLMExtract",
|
|
53
|
+
"LinksOnPage",
|
|
54
|
+
"ScreenSize",
|
|
55
|
+
"Transformer",
|
|
56
|
+
# Error classes
|
|
57
|
+
*_error_classes,
|
|
58
|
+
# Version
|
|
59
|
+
"__version__",
|
|
60
|
+
]
|
olostep/_log.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Modern logging configuration for Olostep SDK.
|
|
3
|
+
|
|
4
|
+
Provides library-safe logging with secret redaction and structured IO logging.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Iterable
|
|
15
|
+
from .config import IO_LOG_PATH
|
|
16
|
+
# Top-level package logger
|
|
17
|
+
logger = logging.getLogger("olostep")
|
|
18
|
+
logger.addHandler(logging.NullHandler()) # library-safe default
|
|
19
|
+
logger.propagate = True # let app handlers catch logs
|
|
20
|
+
|
|
21
|
+
# Sub-loggers (optional convenience)
|
|
22
|
+
io_logger = logging.getLogger("olostep.backend.io")
|
|
23
|
+
io_logger.propagate = True # it's the default, just to be explicit
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class RedactSecretsFilter(logging.Filter):
|
|
27
|
+
"""
|
|
28
|
+
Redacts common secrets in log records. Users may attach this to their handlers.
|
|
29
|
+
"""
|
|
30
|
+
SECRET_PATTERNS: Iterable[re.Pattern[str]] = (
|
|
31
|
+
re.compile(r"(Authorization:\s*Bearer\s+)[A-Za-z0-9\-\._~\+\/]+=*", re.I),
|
|
32
|
+
re.compile(r"(api[_-]?key=)[A-Za-z0-9]{10,}", re.I),
|
|
33
|
+
re.compile(r"(token=)[A-Za-z0-9\-_\.]{10,}", re.I),
|
|
34
|
+
re.compile(r"(\"authorization\":\s*\"Bearer\s+)[A-Za-z0-9\-\._~\+\/]+=*", re.I),
|
|
35
|
+
re.compile(r"(\"Authorization\":\s*\"Bearer\s+)[A-Za-z0-9\-\._~\+\/]+=*", re.I),
|
|
36
|
+
re.compile(r"(\"x-api-key\":\s*\")[A-Za-z0-9]{10,}", re.I),
|
|
37
|
+
re.compile(r"(\"x-auth-token\":\s*\")[A-Za-z0-9\-_\.]{10,}", re.I),
|
|
38
|
+
)
|
|
39
|
+
REPLACEMENT = r"\1[REDACTED]"
|
|
40
|
+
|
|
41
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
42
|
+
msg = record.getMessage()
|
|
43
|
+
for pat in self.SECRET_PATTERNS:
|
|
44
|
+
msg = pat.sub(self.REPLACEMENT, msg)
|
|
45
|
+
# stash sanitized message without mutating args (safe for %-formatting)
|
|
46
|
+
record.msg = msg
|
|
47
|
+
record.args = ()
|
|
48
|
+
return True
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class PerMessageIOFilter(logging.Filter):
|
|
52
|
+
"""
|
|
53
|
+
Filter that allows per-message control of IO logging.
|
|
54
|
+
Use the 'log_to_file' extra parameter to control whether a message gets logged to file.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
58
|
+
skip_file_logging = getattr(record, 'skip_file_logging', False)
|
|
59
|
+
return not skip_file_logging
|
|
60
|
+
|
|
61
|
+
#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
62
|
+
# functions to toggle logging behavior
|
|
63
|
+
#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
64
|
+
|
|
65
|
+
def _enable_stderr_debug(level: int = logging.DEBUG) -> None:
|
|
66
|
+
"""
|
|
67
|
+
Opt-in helper: attach a simple stderr handler to the package logger.
|
|
68
|
+
Safe for quickstarts; in real apps users should configure logging themselves.
|
|
69
|
+
"""
|
|
70
|
+
h = logging.StreamHandler()
|
|
71
|
+
fmt = "[%(levelname)s] %(name)s: %(message)s"
|
|
72
|
+
h.setFormatter(logging.Formatter(fmt))
|
|
73
|
+
h.addFilter(RedactSecretsFilter())
|
|
74
|
+
root = logging.getLogger("olostep")
|
|
75
|
+
root.setLevel(level)
|
|
76
|
+
root.addHandler(h)
|
|
77
|
+
|
|
78
|
+
# Setup IO logger with file handler if logging is enabled
|
|
79
|
+
def _setup_io_file_handler() -> None:
|
|
80
|
+
logger.warning("Setting up IO file logging. This is a lot of data.")
|
|
81
|
+
"""Setup file handler for IO logger if logging is enabled."""
|
|
82
|
+
# try:
|
|
83
|
+
# from .config import IO_LOG_PATH
|
|
84
|
+
# except ImportError:
|
|
85
|
+
# # Fallback when not imported as part of package
|
|
86
|
+
# IO_LOG_PATH = os.getenv("OLOSTEP_IO_LOG_PATH", "tests/runtime")
|
|
87
|
+
|
|
88
|
+
if not IO_LOG_PATH:
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
# Check if file handler already exists
|
|
92
|
+
if any(isinstance(h, logging.FileHandler) for h in io_logger.handlers):
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
# Create log directory
|
|
96
|
+
log_dir = Path(IO_LOG_PATH)
|
|
97
|
+
logger.info(f"Logging IO to: {log_dir.resolve()}")
|
|
98
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
99
|
+
|
|
100
|
+
# Create file handler with JSON formatter
|
|
101
|
+
file_handler = logging.FileHandler(log_dir / "io_logs.json", mode='a')
|
|
102
|
+
file_handler.setLevel(logging.DEBUG)
|
|
103
|
+
|
|
104
|
+
# Custom formatter for structured JSON logging
|
|
105
|
+
class IOJSONFormatter(logging.Formatter):
|
|
106
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
107
|
+
log_entry = {
|
|
108
|
+
"timestamp": datetime.now().isoformat(),
|
|
109
|
+
"level": record.levelname,
|
|
110
|
+
"logger": record.name,
|
|
111
|
+
"message": record.getMessage(),
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# Add i and o data if present, with redaction (handle both cases)
|
|
115
|
+
|
|
116
|
+
if hasattr(record, 'request_id'):
|
|
117
|
+
log_entry["request_id"] = record.request_id
|
|
118
|
+
|
|
119
|
+
if hasattr(record, 'response_time_ms'):
|
|
120
|
+
log_entry["response_time_ms"] = record.response_time_ms
|
|
121
|
+
|
|
122
|
+
if hasattr(record, 'i'):
|
|
123
|
+
log_entry["I"] = self._redact_data(record.i)
|
|
124
|
+
elif hasattr(record, 'I'):
|
|
125
|
+
log_entry["I"] = self._redact_data(record.I)
|
|
126
|
+
|
|
127
|
+
if hasattr(record, 'o'):
|
|
128
|
+
log_entry["O"] = self._redact_data(record.o)
|
|
129
|
+
elif hasattr(record, 'O'):
|
|
130
|
+
log_entry["O"] = self._redact_data(record.O)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
return json.dumps(log_entry, indent=2) + ","
|
|
134
|
+
|
|
135
|
+
def _redact_data(self, data: Any) -> Any:
|
|
136
|
+
"""Recursively redact sensitive data from dictionaries."""
|
|
137
|
+
if isinstance(data, dict):
|
|
138
|
+
redacted = {}
|
|
139
|
+
for key, value in data.items():
|
|
140
|
+
if key == "body" and isinstance(value, str):
|
|
141
|
+
# Always try to parse body as JSON first, then handle as text
|
|
142
|
+
try:
|
|
143
|
+
parsed_body = json.loads(value)
|
|
144
|
+
redacted[key] = self._redact_data(parsed_body)
|
|
145
|
+
except (json.JSONDecodeError, TypeError):
|
|
146
|
+
# If not valid JSON, keep as string but redact secrets
|
|
147
|
+
if self._contains_secret(value):
|
|
148
|
+
redacted[key] = self._redact_string(value)
|
|
149
|
+
else:
|
|
150
|
+
redacted[key] = value
|
|
151
|
+
elif isinstance(value, str) and self._contains_secret(value):
|
|
152
|
+
redacted[key] = self._redact_string(value)
|
|
153
|
+
elif isinstance(value, dict):
|
|
154
|
+
redacted[key] = self._redact_data(value)
|
|
155
|
+
else:
|
|
156
|
+
redacted[key] = value
|
|
157
|
+
return redacted
|
|
158
|
+
elif isinstance(data, str) and self._contains_secret(data):
|
|
159
|
+
return self._redact_string(data)
|
|
160
|
+
else:
|
|
161
|
+
return data
|
|
162
|
+
|
|
163
|
+
def _contains_secret(self, text: str) -> bool:
|
|
164
|
+
"""Check if text contains secrets."""
|
|
165
|
+
# Check for Bearer tokens in any case
|
|
166
|
+
if re.search(r'bearer\s+[a-za-z0-9\-\._~\+\/]+=*', text, re.I):
|
|
167
|
+
return True
|
|
168
|
+
# Check for API keys
|
|
169
|
+
if re.search(r'api[_-]?key[=:]\s*[a-za-z0-9]{10,}', text, re.I):
|
|
170
|
+
return True
|
|
171
|
+
# Check for tokens
|
|
172
|
+
if re.search(r'token[=:]\s*[a-za-z0-9\-_\.]{10,}', text, re.I):
|
|
173
|
+
return True
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
def _redact_string(self, text: str) -> str:
|
|
177
|
+
"""Redact secrets in a string."""
|
|
178
|
+
# Redact Bearer tokens
|
|
179
|
+
text = re.sub(r'(bearer\s+)[a-za-z0-9\-\._~\+\/]+=*', r'\1[REDACTED]', text, flags=re.I)
|
|
180
|
+
# Redact API keys
|
|
181
|
+
text = re.sub(r'(api[_-]?key[=:]\s*)[a-za-z0-9]{10,}', r'\1[REDACTED]', text, flags=re.I)
|
|
182
|
+
# Redact tokens
|
|
183
|
+
text = re.sub(r'(token[=:]\s*)[a-za-z0-9\-_\.]{10,}', r'\1[REDACTED]', text, flags=re.I)
|
|
184
|
+
return text
|
|
185
|
+
|
|
186
|
+
file_handler.setFormatter(IOJSONFormatter())
|
|
187
|
+
file_handler.addFilter(RedactSecretsFilter())
|
|
188
|
+
file_handler.addFilter(PerMessageIOFilter())
|
|
189
|
+
io_logger.addHandler(file_handler)
|
|
190
|
+
|
|
191
|
+
io_logger.setLevel(logging.DEBUG)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def get_logger(name: str | None = None) -> logging.Logger:
|
|
197
|
+
"""Library API for internal modules."""
|
|
198
|
+
return logging.getLogger("olostep" + ("" if not name else f".{name}"))
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def load_io_logs(log_file_path: str | Path) -> list[dict[str, Any]]:
|
|
202
|
+
"""
|
|
203
|
+
Load IO logs from a file as a list of dictionaries.
|
|
204
|
+
|
|
205
|
+
The log file contains JSON objects separated by commas.
|
|
206
|
+
This function wraps the content in square brackets to create a valid JSON array.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
log_file_path: Path to the IO logs JSON file
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
List of log entries as dictionaries
|
|
213
|
+
|
|
214
|
+
Raises:
|
|
215
|
+
FileNotFoundError: If the log file doesn't exist
|
|
216
|
+
json.JSONDecodeError: If the file contains invalid JSON
|
|
217
|
+
"""
|
|
218
|
+
log_path = Path(log_file_path)
|
|
219
|
+
|
|
220
|
+
if not log_path.exists():
|
|
221
|
+
raise FileNotFoundError(f"Log file not found: {log_path}")
|
|
222
|
+
|
|
223
|
+
with open(log_path, 'r') as f:
|
|
224
|
+
content = f.read().strip()
|
|
225
|
+
|
|
226
|
+
if not content:
|
|
227
|
+
return []
|
|
228
|
+
|
|
229
|
+
# Remove trailing comma if present
|
|
230
|
+
if content.endswith(','):
|
|
231
|
+
content = content[:-1]
|
|
232
|
+
|
|
233
|
+
# Wrap in square brackets to create a valid JSON array
|
|
234
|
+
json_array_content = f"[{content}]"
|
|
235
|
+
|
|
236
|
+
return json.loads(json_array_content)
|
|
237
|
+
|
|
238
|
+
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Olostep API Endpoints Documentation.
|
|
3
|
+
|
|
4
|
+
This module serves as comprehensive documentation in code for the Olostep API.
|
|
5
|
+
It contains all endpoint definitions, URL patterns, request/response formats,
|
|
6
|
+
and usage examples for each API endpoint.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import Any, Literal
|
|
14
|
+
from ..models.request import (
|
|
15
|
+
# Scrapes
|
|
16
|
+
ScrapeUrlRequest,
|
|
17
|
+
# Batches
|
|
18
|
+
BatchStartRequest,
|
|
19
|
+
BatchItemsRequest,
|
|
20
|
+
# Crawls
|
|
21
|
+
CrawlStartRequest,
|
|
22
|
+
CrawlPagesRequest,
|
|
23
|
+
# Maps
|
|
24
|
+
MapCreateRequest,
|
|
25
|
+
# Retrieve
|
|
26
|
+
RetrieveGetRequest,
|
|
27
|
+
)
|
|
28
|
+
from ..models.response import (
|
|
29
|
+
# Scrapes
|
|
30
|
+
CreateScrapeResponse,
|
|
31
|
+
GetScrapeResponse,
|
|
32
|
+
# Batches
|
|
33
|
+
BatchCreateResponse,
|
|
34
|
+
BatchInfoResponse,
|
|
35
|
+
BatchItemsResponse,
|
|
36
|
+
# Crawls
|
|
37
|
+
CreateCrawlResponse,
|
|
38
|
+
CrawlInfoResponse,
|
|
39
|
+
CrawlPagesResponse,
|
|
40
|
+
# Maps
|
|
41
|
+
MapResponse,
|
|
42
|
+
# Retrieve
|
|
43
|
+
RetrieveResponse,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
Method = Literal["GET", "POST", "PUT", "DELETE", "PATCH"]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class EndpointContract:
|
|
51
|
+
"""Single source of truth for an API endpoint.
|
|
52
|
+
|
|
53
|
+
Holds HTTP details and the concrete Pydantic v2 request/response models.
|
|
54
|
+
Frontends remain Pydantic-free and use these contracts only through the backend caller.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
# Stable registry key exposed to frontends as namespaces/methods
|
|
58
|
+
key: tuple[str, str]
|
|
59
|
+
|
|
60
|
+
# Human docs
|
|
61
|
+
name: str
|
|
62
|
+
description: str
|
|
63
|
+
|
|
64
|
+
# HTTP details
|
|
65
|
+
method: Method
|
|
66
|
+
path: str # e.g., "/scrapes" or "/scrapes/{scrape_id}"
|
|
67
|
+
|
|
68
|
+
# IO models (backend validation only)
|
|
69
|
+
request_model: type[Any] | None = None
|
|
70
|
+
response_model: type[Any] | None = None
|
|
71
|
+
|
|
72
|
+
# Optional examples for docs/SDK help
|
|
73
|
+
examples: list[dict[str, Any]] = field(default_factory=list)
|
|
74
|
+
|
|
75
|
+
def formatted_path(
|
|
76
|
+
self,
|
|
77
|
+
path_params: dict[str, Any] | None = None,
|
|
78
|
+
) -> str:
|
|
79
|
+
path_params = path_params or {}
|
|
80
|
+
formatted_path = self.path.format(**path_params)
|
|
81
|
+
formatted_path = formatted_path.rstrip('/')
|
|
82
|
+
formatted_path = formatted_path.lstrip('/')
|
|
83
|
+
return formatted_path
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def path_parameters(self) -> list[str]:
|
|
87
|
+
return re.findall(r"{([^}]+)}", self.path)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# =============================================================================
|
|
91
|
+
# SCRAPES
|
|
92
|
+
# =============================================================================
|
|
93
|
+
|
|
94
|
+
SCRAPE_URL = EndpointContract(
|
|
95
|
+
key=("scrape", "url"),
|
|
96
|
+
name="Create Scrape",
|
|
97
|
+
description="Create a new web scraping job to extract content from a URL",
|
|
98
|
+
method="POST",
|
|
99
|
+
path="/scrapes",
|
|
100
|
+
request_model=ScrapeUrlRequest,
|
|
101
|
+
response_model=CreateScrapeResponse,
|
|
102
|
+
examples=[
|
|
103
|
+
{
|
|
104
|
+
"description": "Basic HTML scrape",
|
|
105
|
+
"request": {"url_to_scrape": "https://example.com", "formats": ["html"]},
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"description": "Multi-format scrape with configuration",
|
|
109
|
+
"request": {
|
|
110
|
+
"url_to_scrape": "https://example.com",
|
|
111
|
+
"formats": ["html", "markdown", "text"],
|
|
112
|
+
"country": "US",
|
|
113
|
+
"remove_images": True,
|
|
114
|
+
"wait_before_scraping": 2000,
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
SCRAPE_GET = EndpointContract(
|
|
121
|
+
key=("scrape", "get"),
|
|
122
|
+
name="Get Scrape",
|
|
123
|
+
description="Retrieve the results of a completed scraping job",
|
|
124
|
+
method="GET",
|
|
125
|
+
path="/scrapes/{scrape_id}",
|
|
126
|
+
request_model=None,
|
|
127
|
+
response_model=GetScrapeResponse,
|
|
128
|
+
examples=[
|
|
129
|
+
{"description": "Retrieve scrape results", "path_params": {"scrape_id": "scrape_12345"}},
|
|
130
|
+
],
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# =============================================================================
|
|
135
|
+
# BATCHES
|
|
136
|
+
# =============================================================================
|
|
137
|
+
|
|
138
|
+
BATCH_START = EndpointContract(
|
|
139
|
+
key=("batch", "start"),
|
|
140
|
+
name="Create Batch",
|
|
141
|
+
description="Create a batch scraping job to process multiple URLs",
|
|
142
|
+
method="POST",
|
|
143
|
+
path="/batches",
|
|
144
|
+
request_model=BatchStartRequest,
|
|
145
|
+
response_model=BatchCreateResponse,
|
|
146
|
+
examples=[
|
|
147
|
+
{
|
|
148
|
+
"description": "Basic batch creation",
|
|
149
|
+
"request": {
|
|
150
|
+
"items": [
|
|
151
|
+
{"url": "https://example1.com", "custom_id": "item1"},
|
|
152
|
+
{"url": "https://example2.com", "custom_id": "item2"},
|
|
153
|
+
]
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"description": "Batch with configuration",
|
|
158
|
+
"request": {
|
|
159
|
+
"items": [{"url": "https://example.com", "custom_id": "test"}],
|
|
160
|
+
"country": "US",
|
|
161
|
+
"parser": {"id": "default"},
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
BATCH_INFO = EndpointContract(
|
|
168
|
+
key=("batch", "info"),
|
|
169
|
+
name="Get Batch Info",
|
|
170
|
+
description="Retrieve information about a batch scraping job",
|
|
171
|
+
method="GET",
|
|
172
|
+
path="/batches/{batch_id}",
|
|
173
|
+
request_model=None,
|
|
174
|
+
response_model=BatchInfoResponse,
|
|
175
|
+
examples=[
|
|
176
|
+
{"description": "Get batch status", "path_params": {"batch_id": "batch_12345"}},
|
|
177
|
+
],
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
BATCH_ITEMS = EndpointContract(
|
|
181
|
+
key=("batch", "items"),
|
|
182
|
+
name="Get Batch Items",
|
|
183
|
+
description="Retrieve the results of individual items in a batch",
|
|
184
|
+
method="GET",
|
|
185
|
+
path="/batches/{batch_id}/items",
|
|
186
|
+
request_model=BatchItemsRequest,
|
|
187
|
+
response_model=BatchItemsResponse,
|
|
188
|
+
examples=[
|
|
189
|
+
{
|
|
190
|
+
"description": "Get all completed items",
|
|
191
|
+
"path_params": {"batch_id": "batch_12345"},
|
|
192
|
+
"query_params": {"status": "completed"},
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"description": "Get items with pagination",
|
|
196
|
+
"path_params": {"batch_id": "batch_12345"},
|
|
197
|
+
"query_params": {"limit": 10, "cursor": 0},
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# =============================================================================
|
|
204
|
+
# CRAWLS
|
|
205
|
+
# =============================================================================
|
|
206
|
+
|
|
207
|
+
CRAWL_START = EndpointContract(
|
|
208
|
+
key=("crawl", "start"),
|
|
209
|
+
name="Create Crawl",
|
|
210
|
+
description="Create a web crawling job to discover and scrape linked pages",
|
|
211
|
+
method="POST",
|
|
212
|
+
path="/crawls",
|
|
213
|
+
request_model=CrawlStartRequest,
|
|
214
|
+
response_model=CreateCrawlResponse,
|
|
215
|
+
examples=[
|
|
216
|
+
{
|
|
217
|
+
"description": "Basic crawl creation",
|
|
218
|
+
"request": {"start_url": "https://example.com", "max_pages": 10, "follow_links": True},
|
|
219
|
+
}
|
|
220
|
+
],
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
CRAWL_INFO = EndpointContract(
|
|
224
|
+
key=("crawl", "info"),
|
|
225
|
+
name="Get Crawl Info",
|
|
226
|
+
description="Retrieve information about a web crawling job",
|
|
227
|
+
method="GET",
|
|
228
|
+
path="/crawls/{crawl_id}",
|
|
229
|
+
request_model=None,
|
|
230
|
+
response_model=CrawlInfoResponse,
|
|
231
|
+
examples=[
|
|
232
|
+
{"description": "Get crawl status", "path_params": {"crawl_id": "crawl_12345"}},
|
|
233
|
+
],
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
CRAWL_PAGES = EndpointContract(
|
|
237
|
+
key=("crawl", "pages"),
|
|
238
|
+
name="Get Crawl Pages",
|
|
239
|
+
description="Retrieve the pages discovered during a crawl",
|
|
240
|
+
method="GET",
|
|
241
|
+
path="/crawls/{crawl_id}/pages",
|
|
242
|
+
request_model=CrawlPagesRequest,
|
|
243
|
+
response_model=CrawlPagesResponse,
|
|
244
|
+
examples=[
|
|
245
|
+
{"description": "Get all crawled pages", "path_params": {"crawl_id": "crawl_12345"}},
|
|
246
|
+
{
|
|
247
|
+
"description": "Get pages with pagination",
|
|
248
|
+
"path_params": {"crawl_id": "crawl_12345"},
|
|
249
|
+
"query_params": {"limit": 20, "cursor": 0},
|
|
250
|
+
},
|
|
251
|
+
],
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
# =============================================================================
|
|
256
|
+
# MAPS
|
|
257
|
+
# =============================================================================
|
|
258
|
+
|
|
259
|
+
MAP_CREATE = EndpointContract(
|
|
260
|
+
key=("map", "create"),
|
|
261
|
+
name="Create Map",
|
|
262
|
+
description="Extract links from a website",
|
|
263
|
+
method="POST",
|
|
264
|
+
path="/maps",
|
|
265
|
+
request_model=MapCreateRequest,
|
|
266
|
+
response_model=MapResponse,
|
|
267
|
+
examples=[
|
|
268
|
+
{"description": "Basic link extraction", "request": {"url": "https://example.com"}},
|
|
269
|
+
{
|
|
270
|
+
"description": "Link extraction with filtering",
|
|
271
|
+
"request": {
|
|
272
|
+
"url": "https://example.com",
|
|
273
|
+
"search_query": "blog",
|
|
274
|
+
"top_n": 10,
|
|
275
|
+
"include_urls": ["/blog/**"],
|
|
276
|
+
"exclude_urls": ["/admin/**"],
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
# =============================================================================
|
|
284
|
+
# RETRIEVE
|
|
285
|
+
# =============================================================================
|
|
286
|
+
|
|
287
|
+
RETRIEVE_GET = EndpointContract(
|
|
288
|
+
key=("retrieve", "get"),
|
|
289
|
+
name="Retrieve Content",
|
|
290
|
+
description="Retrieve content by its unique identifier with specified formats",
|
|
291
|
+
method="GET",
|
|
292
|
+
path="/retrieve",
|
|
293
|
+
request_model=RetrieveGetRequest,
|
|
294
|
+
response_model=RetrieveResponse,
|
|
295
|
+
examples=[
|
|
296
|
+
{
|
|
297
|
+
"description": "Retrieve content by ID with formats",
|
|
298
|
+
"query_params": {"retrieve_id": "retrieve_12345", "formats": ["html", "markdown", "json"]},
|
|
299
|
+
}
|
|
300
|
+
],
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
# =============================================================================
|
|
305
|
+
# REGISTRY
|
|
306
|
+
# =============================================================================
|
|
307
|
+
|
|
308
|
+
CONTRACTS: dict[tuple[str, str], EndpointContract] = {
|
|
309
|
+
c.key: c
|
|
310
|
+
for c in [
|
|
311
|
+
SCRAPE_URL,
|
|
312
|
+
SCRAPE_GET,
|
|
313
|
+
BATCH_START,
|
|
314
|
+
BATCH_INFO,
|
|
315
|
+
BATCH_ITEMS,
|
|
316
|
+
CRAWL_START,
|
|
317
|
+
CRAWL_INFO,
|
|
318
|
+
CRAWL_PAGES,
|
|
319
|
+
MAP_CREATE,
|
|
320
|
+
RETRIEVE_GET,
|
|
321
|
+
]
|
|
322
|
+
}
|
|
323
|
+
|