webextrator-cli 2026.7.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.
@@ -0,0 +1,3 @@
1
+ """WebExtrator CLI package."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,6 @@
1
+ """Entry point for python -m webextrator_cli."""
2
+
3
+ from webextrator_cli.main import cli
4
+
5
+ if __name__ == "__main__":
6
+ cli()
@@ -0,0 +1 @@
1
+ """WebExtrator CLI commands package."""
@@ -0,0 +1,25 @@
1
+ """Info and utility commands."""
2
+
3
+ import click
4
+
5
+ from webextrator_cli.core.config import settings
6
+ from webextrator_cli.core.output import console
7
+
8
+
9
+ @click.command()
10
+ def config() -> None:
11
+ """Show current configuration."""
12
+ from rich.table import Table
13
+
14
+ table = Table(title="WebExtrator CLI Configuration")
15
+ table.add_column("Setting", style="bold cyan")
16
+ table.add_column("Value")
17
+
18
+ table.add_row("API Base URL", settings.api_base_url)
19
+ table.add_row(
20
+ "API Token",
21
+ f"{settings.api_token[:8]}..." if settings.api_token else "[red]Not set[/red]",
22
+ )
23
+ table.add_row("Request Timeout", f"{settings.request_timeout}s")
24
+
25
+ console.print(table)
@@ -0,0 +1,101 @@
1
+ """Task management commands for WebExtrator."""
2
+
3
+ import click
4
+
5
+ from webextrator_cli.core.client import get_client
6
+ from webextrator_cli.core.exceptions import WebExtratorError
7
+ from webextrator_cli.core.output import print_error, print_json, print_task_result
8
+
9
+
10
+ @click.group()
11
+ def tasks() -> None:
12
+ """Query previously created render/extract tasks.
13
+
14
+ \b
15
+ Examples:
16
+ webextrator tasks retrieve --id 550e8400-e29b-41d4-a716-446655440000
17
+ webextrator tasks retrieve --trace-id 550e8400-e29b-41d4-a716-446655440001
18
+ webextrator tasks batch --ids id1 id2
19
+ """
20
+
21
+
22
+ @tasks.command()
23
+ @click.option("--id", "task_id", default=None, help="Task UUID to retrieve.")
24
+ @click.option("--trace-id", default=None, help="Trace UUID to retrieve.")
25
+ @click.option("--json", "output_json", is_flag=True, help="Output raw JSON.")
26
+ @click.pass_context
27
+ def retrieve(
28
+ ctx: click.Context,
29
+ task_id: str | None,
30
+ trace_id: str | None,
31
+ output_json: bool,
32
+ ) -> None:
33
+ """Retrieve a single render/extract task by ID or trace ID.
34
+
35
+ \b
36
+ Examples:
37
+ webextrator tasks retrieve --id 550e8400-e29b-41d4-a716-446655440000
38
+ webextrator tasks retrieve --trace-id 550e8400-e29b-41d4-a716-446655440001
39
+ """
40
+ if not task_id and not trace_id:
41
+ raise click.UsageError("Provide at least one of --id or --trace-id.")
42
+
43
+ client = get_client(ctx.obj.get("token"))
44
+ payload: dict[str, object] = {
45
+ "action": "retrieve",
46
+ "id": task_id,
47
+ "trace_id": trace_id,
48
+ }
49
+
50
+ try:
51
+ result = client.tasks(**payload) # type: ignore[arg-type]
52
+ if output_json:
53
+ print_json(result)
54
+ else:
55
+ print_task_result(result)
56
+ except WebExtratorError as e:
57
+ print_error(e.message)
58
+ raise SystemExit(1) from e
59
+
60
+
61
+ @tasks.command()
62
+ @click.option("--ids", multiple=True, help="Task UUIDs to retrieve (repeatable).")
63
+ @click.option("--trace-ids", multiple=True, help="Trace UUIDs to retrieve (repeatable).")
64
+ @click.option("--offset", default=None, type=int, help="Pagination offset (default 0).")
65
+ @click.option("--limit", default=None, type=int, help="Page size (default 12).")
66
+ @click.option("--json", "output_json", is_flag=True, help="Output raw JSON.")
67
+ @click.pass_context
68
+ def batch(
69
+ ctx: click.Context,
70
+ ids: tuple[str, ...],
71
+ trace_ids: tuple[str, ...],
72
+ offset: int | None,
73
+ limit: int | None,
74
+ output_json: bool,
75
+ ) -> None:
76
+ """Retrieve multiple render/extract tasks at once.
77
+
78
+ \b
79
+ Examples:
80
+ webextrator tasks batch --ids id1 id2
81
+ webextrator tasks batch --trace-ids trace-001 trace-002
82
+ webextrator tasks batch --limit 5
83
+ """
84
+ client = get_client(ctx.obj.get("token"))
85
+ payload: dict[str, object] = {
86
+ "action": "retrieve_batch",
87
+ "ids": list(ids) if ids else None,
88
+ "trace_ids": list(trace_ids) if trace_ids else None,
89
+ "offset": offset,
90
+ "limit": limit,
91
+ }
92
+
93
+ try:
94
+ result = client.tasks(**payload) # type: ignore[arg-type]
95
+ if output_json:
96
+ print_json(result)
97
+ else:
98
+ print_task_result(result)
99
+ except WebExtratorError as e:
100
+ print_error(e.message)
101
+ raise SystemExit(1) from e
@@ -0,0 +1,252 @@
1
+ """Web extraction and rendering commands."""
2
+
3
+ import json
4
+
5
+ import click
6
+
7
+ from webextrator_cli.core.client import get_client
8
+ from webextrator_cli.core.exceptions import WebExtratorError
9
+ from webextrator_cli.core.output import (
10
+ WAIT_UNTIL_OPTIONS,
11
+ print_error,
12
+ print_extract_result,
13
+ print_json,
14
+ print_render_result,
15
+ )
16
+
17
+
18
+ def _parse_headers(headers: str | None) -> dict[str, str] | None:
19
+ """Parse --headers JSON option."""
20
+ if headers is None:
21
+ return None
22
+ try:
23
+ parsed = json.loads(headers)
24
+ except json.JSONDecodeError as exc:
25
+ raise click.BadParameter("--headers must be a valid JSON object.") from exc
26
+ if not isinstance(parsed, dict) or not all(
27
+ isinstance(k, str) and isinstance(v, str) for k, v in parsed.items()
28
+ ):
29
+ raise click.BadParameter("--headers must be a JSON object of string key/value pairs.")
30
+ return parsed
31
+
32
+
33
+ @click.command()
34
+ @click.argument("url")
35
+ @click.option(
36
+ "--expected-type",
37
+ type=click.Choice(["product", "article", "general"]),
38
+ default=None,
39
+ help="Hint about the expected page type to optimize extraction.",
40
+ )
41
+ @click.option(
42
+ "--enable-llm",
43
+ is_flag=True,
44
+ default=False,
45
+ help="Enable LLM-based semantic normalization as a final extraction step.",
46
+ )
47
+ @click.option(
48
+ "--wait-until",
49
+ type=click.Choice(WAIT_UNTIL_OPTIONS),
50
+ default=None,
51
+ help="Page load wait condition (default: networkidle).",
52
+ )
53
+ @click.option(
54
+ "--timeout",
55
+ default=None,
56
+ type=float,
57
+ help="Total timeout in seconds for the extract operation (default: 30).",
58
+ )
59
+ @click.option(
60
+ "--delay",
61
+ default=None,
62
+ type=float,
63
+ help="Extra delay in seconds after the page is loaded, before extraction.",
64
+ )
65
+ @click.option(
66
+ "--wait-for-selector",
67
+ default=None,
68
+ help="CSS selector to wait for before starting extraction.",
69
+ )
70
+ @click.option(
71
+ "--block-resources/--no-block-resources",
72
+ default=None,
73
+ help="Block non-essential resources (images/fonts/media) during page load.",
74
+ )
75
+ @click.option(
76
+ "--headers",
77
+ default=None,
78
+ help='Custom request headers as JSON, e.g. \'{"Accept-Language":"en-US"}\'.',
79
+ )
80
+ @click.option(
81
+ "--user-agent",
82
+ default=None,
83
+ help="Override the User-Agent header.",
84
+ )
85
+ @click.option(
86
+ "--callback-url",
87
+ default=None,
88
+ help="Callback URL for async processing.",
89
+ )
90
+ @click.option(
91
+ "--async",
92
+ "async_mode",
93
+ is_flag=True,
94
+ default=False,
95
+ help="Submit asynchronously; returns a task_id to poll instead of waiting.",
96
+ )
97
+ @click.option("--json", "output_json", is_flag=True, help="Output raw JSON.")
98
+ @click.pass_context
99
+ def extract(
100
+ ctx: click.Context,
101
+ url: str,
102
+ expected_type: str | None,
103
+ enable_llm: bool,
104
+ wait_until: str | None,
105
+ timeout: float | None,
106
+ delay: float | None,
107
+ wait_for_selector: str | None,
108
+ block_resources: bool | None,
109
+ headers: str | None,
110
+ user_agent: str | None,
111
+ callback_url: str | None,
112
+ async_mode: bool,
113
+ output_json: bool,
114
+ ) -> None:
115
+ """Extract structured content from a web page.
116
+
117
+ URL is the address of the web page to extract content from.
118
+
119
+ \b
120
+ Examples:
121
+ webextrator extract https://www.amazon.com/dp/B0C1234567
122
+ webextrator extract https://example.com/article --expected-type article
123
+ webextrator extract https://shop.example.com --enable-llm
124
+ """
125
+ client = get_client(ctx.obj.get("token"))
126
+ payload: dict[str, object] = {
127
+ "url": url,
128
+ "expected_type": expected_type,
129
+ "enable_llm": enable_llm if enable_llm else None,
130
+ "wait_until": wait_until,
131
+ "timeout": timeout,
132
+ "delay": delay,
133
+ "wait_for_selector": wait_for_selector,
134
+ "block_resources": block_resources,
135
+ "headers": _parse_headers(headers),
136
+ "user_agent": user_agent,
137
+ "callback_url": callback_url,
138
+ "async": async_mode,
139
+ }
140
+
141
+ try:
142
+ result = client.extract(**payload) # type: ignore[arg-type]
143
+ if output_json:
144
+ print_json(result)
145
+ else:
146
+ print_extract_result(result)
147
+ except WebExtratorError as e:
148
+ print_error(e.message)
149
+ raise SystemExit(1) from e
150
+
151
+
152
+ @click.command()
153
+ @click.argument("url")
154
+ @click.option(
155
+ "--wait-until",
156
+ type=click.Choice(WAIT_UNTIL_OPTIONS),
157
+ default=None,
158
+ help="Page load wait condition (default: networkidle).",
159
+ )
160
+ @click.option(
161
+ "--timeout",
162
+ default=None,
163
+ type=float,
164
+ help="Total timeout in seconds for the render operation (default: 30).",
165
+ )
166
+ @click.option(
167
+ "--delay",
168
+ default=None,
169
+ type=float,
170
+ help="Extra delay in seconds after the page is loaded, before HTML is captured.",
171
+ )
172
+ @click.option(
173
+ "--wait-for-selector",
174
+ default=None,
175
+ help="CSS selector to wait for before capturing HTML.",
176
+ )
177
+ @click.option(
178
+ "--block-resources/--no-block-resources",
179
+ default=None,
180
+ help="Block non-essential resources (images/fonts/media) during page load.",
181
+ )
182
+ @click.option(
183
+ "--headers",
184
+ default=None,
185
+ help='Custom request headers as JSON, e.g. \'{"Accept-Language":"en-US"}\'.',
186
+ )
187
+ @click.option(
188
+ "--user-agent",
189
+ default=None,
190
+ help="Override the User-Agent header.",
191
+ )
192
+ @click.option(
193
+ "--callback-url",
194
+ default=None,
195
+ help="Callback URL for async processing.",
196
+ )
197
+ @click.option(
198
+ "--async",
199
+ "async_mode",
200
+ is_flag=True,
201
+ default=False,
202
+ help="Submit asynchronously; returns a task_id to poll instead of waiting.",
203
+ )
204
+ @click.option("--json", "output_json", is_flag=True, help="Output raw JSON.")
205
+ @click.pass_context
206
+ def render(
207
+ ctx: click.Context,
208
+ url: str,
209
+ wait_until: str | None,
210
+ timeout: float | None,
211
+ delay: float | None,
212
+ wait_for_selector: str | None,
213
+ block_resources: bool | None,
214
+ headers: str | None,
215
+ user_agent: str | None,
216
+ callback_url: str | None,
217
+ async_mode: bool,
218
+ output_json: bool,
219
+ ) -> None:
220
+ """Render a web page and return the rendered HTML.
221
+
222
+ URL is the address of the web page to render.
223
+
224
+ \b
225
+ Examples:
226
+ webextrator render https://example.com
227
+ webextrator render https://example.com --wait-until load
228
+ webextrator render https://spa.example.com --delay 2 --wait-for-selector "#app"
229
+ """
230
+ client = get_client(ctx.obj.get("token"))
231
+ payload: dict[str, object] = {
232
+ "url": url,
233
+ "wait_until": wait_until,
234
+ "timeout": timeout,
235
+ "delay": delay,
236
+ "wait_for_selector": wait_for_selector,
237
+ "block_resources": block_resources,
238
+ "headers": _parse_headers(headers),
239
+ "user_agent": user_agent,
240
+ "callback_url": callback_url,
241
+ "async": async_mode,
242
+ }
243
+
244
+ try:
245
+ result = client.render(**payload) # type: ignore[arg-type]
246
+ if output_json:
247
+ print_json(result)
248
+ else:
249
+ print_render_result(result)
250
+ except WebExtratorError as e:
251
+ print_error(e.message)
252
+ raise SystemExit(1) from e
@@ -0,0 +1 @@
1
+ """WebExtrator CLI core package."""
@@ -0,0 +1,103 @@
1
+ """HTTP client for WebExtrator API."""
2
+
3
+ from typing import Any
4
+
5
+ import httpx
6
+
7
+ from webextrator_cli.core.config import settings
8
+ from webextrator_cli.core.exceptions import (
9
+ WebExtratorAPIError,
10
+ WebExtratorAuthError,
11
+ WebExtratorTimeoutError,
12
+ )
13
+
14
+
15
+ class WebExtratorClient:
16
+ """HTTP client for AceDataCloud WebExtrator API."""
17
+
18
+ def __init__(self, api_token: str | None = None, base_url: str | None = None):
19
+ self.api_token = api_token if api_token is not None else settings.api_token
20
+ self.base_url = base_url or settings.api_base_url
21
+ self.timeout = settings.request_timeout
22
+
23
+ def _get_headers(self) -> dict[str, str]:
24
+ """Get request headers with authentication."""
25
+ if not self.api_token:
26
+ raise WebExtratorAuthError(
27
+ "API token not configured. Set ACEDATACLOUD_API_TOKEN or use --token option."
28
+ )
29
+ return {
30
+ "accept": "application/json",
31
+ "authorization": f"Bearer {self.api_token}",
32
+ "content-type": "application/json",
33
+ }
34
+
35
+ def request(
36
+ self,
37
+ endpoint: str,
38
+ payload: dict[str, Any],
39
+ timeout: float | None = None,
40
+ ) -> dict[str, Any]:
41
+ """Make a POST request to the WebExtrator API."""
42
+ url = f"{self.base_url}{endpoint}"
43
+ request_timeout = timeout or self.timeout
44
+
45
+ # Remove None values from payload
46
+ payload = {k: v for k, v in payload.items() if v is not None}
47
+
48
+ with httpx.Client() as http_client:
49
+ try:
50
+ response = http_client.post(
51
+ url,
52
+ json=payload,
53
+ headers=self._get_headers(),
54
+ timeout=request_timeout,
55
+ )
56
+
57
+ if response.status_code == 401:
58
+ raise WebExtratorAuthError("Invalid API token")
59
+
60
+ if response.status_code == 403:
61
+ raise WebExtratorAuthError("Access denied. Check your API permissions.")
62
+
63
+ response.raise_for_status()
64
+ return response.json() # type: ignore[no-any-return]
65
+
66
+ except httpx.TimeoutException as e:
67
+ raise WebExtratorTimeoutError(
68
+ f"Request to {endpoint} timed out after {request_timeout}s"
69
+ ) from e
70
+
71
+ except WebExtratorAuthError:
72
+ raise
73
+
74
+ except httpx.HTTPStatusError as e:
75
+ raise WebExtratorAPIError(
76
+ message=e.response.text,
77
+ code=f"http_{e.response.status_code}",
78
+ status_code=e.response.status_code,
79
+ ) from e
80
+
81
+ except Exception as e:
82
+ if isinstance(e, WebExtratorAPIError | WebExtratorTimeoutError):
83
+ raise
84
+ raise WebExtratorAPIError(message=str(e)) from e
85
+
86
+ def extract(self, **kwargs: Any) -> dict[str, Any]:
87
+ """Extract structured content from a web page."""
88
+ return self.request("/webextrator/extract", kwargs)
89
+
90
+ def render(self, **kwargs: Any) -> dict[str, Any]:
91
+ """Render a web page and return the HTML."""
92
+ return self.request("/webextrator/render", kwargs)
93
+
94
+ def tasks(self, **kwargs: Any) -> dict[str, Any]:
95
+ """Query previously created render/extract tasks."""
96
+ return self.request("/webextrator/tasks", kwargs)
97
+
98
+
99
+ def get_client(token: str | None = None) -> WebExtratorClient:
100
+ """Get a WebExtratorClient instance, optionally overriding the token."""
101
+ if token:
102
+ return WebExtratorClient(api_token=token)
103
+ return WebExtratorClient()
@@ -0,0 +1,39 @@
1
+ """Configuration management for WebExtrator CLI."""
2
+
3
+ import os
4
+ from dataclasses import dataclass, field
5
+
6
+ from dotenv import load_dotenv
7
+
8
+ load_dotenv()
9
+
10
+
11
+ @dataclass
12
+ class Settings:
13
+ """Application settings loaded from environment variables."""
14
+
15
+ api_base_url: str = field(
16
+ default_factory=lambda: os.environ.get(
17
+ "ACEDATACLOUD_API_BASE_URL", "https://api.acedata.cloud"
18
+ )
19
+ )
20
+ api_token: str = field(default_factory=lambda: os.environ.get("ACEDATACLOUD_API_TOKEN", ""))
21
+ request_timeout: float = field(
22
+ default_factory=lambda: float(os.environ.get("WEBEXTRATOR_REQUEST_TIMEOUT", "60"))
23
+ )
24
+
25
+ @property
26
+ def is_configured(self) -> bool:
27
+ """Check if the API token is configured."""
28
+ return bool(self.api_token)
29
+
30
+ def validate(self) -> None:
31
+ """Validate configuration. Raises ValueError if API token is missing."""
32
+ if not self.api_token:
33
+ raise ValueError(
34
+ "API token not configured. "
35
+ "Set ACEDATACLOUD_API_TOKEN environment variable or use --token option."
36
+ )
37
+
38
+
39
+ settings = Settings()
@@ -0,0 +1,37 @@
1
+ """Custom exceptions for WebExtrator CLI."""
2
+
3
+
4
+ class WebExtratorError(Exception):
5
+ """Base exception for WebExtrator CLI."""
6
+
7
+ def __init__(self, message: str, code: str = "unknown"):
8
+ self.message = message
9
+ self.code = code
10
+ super().__init__(message)
11
+
12
+
13
+ class WebExtratorAuthError(WebExtratorError):
14
+ """Authentication error."""
15
+
16
+ def __init__(self, message: str = "Authentication failed"):
17
+ super().__init__(message, code="auth_error")
18
+
19
+
20
+ class WebExtratorAPIError(WebExtratorError):
21
+ """API error with HTTP status code."""
22
+
23
+ def __init__(
24
+ self,
25
+ message: str = "API request failed",
26
+ code: str = "api_error",
27
+ status_code: int | None = None,
28
+ ):
29
+ self.status_code = status_code
30
+ super().__init__(message, code)
31
+
32
+
33
+ class WebExtratorTimeoutError(WebExtratorError):
34
+ """Request timeout error."""
35
+
36
+ def __init__(self, message: str = "Request timed out"):
37
+ super().__init__(message, code="timeout_error")
@@ -0,0 +1,114 @@
1
+ """Rich terminal output formatting for WebExtrator CLI."""
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+ import click
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+ from rich.table import Table
10
+
11
+ console = Console()
12
+
13
+ # Wait-until options
14
+ WAIT_UNTIL_OPTIONS = ["load", "domcontentloaded", "networkidle", "commit"]
15
+
16
+ # Block resource types
17
+ BLOCK_RESOURCE_TYPES = ["image", "font", "media", "stylesheet", "xhr", "fetch"]
18
+
19
+
20
+ def print_json(data: Any) -> None:
21
+ """Print data as formatted JSON."""
22
+ click.echo(json.dumps(data, indent=2, ensure_ascii=False))
23
+
24
+
25
+ def print_error(message: str) -> None:
26
+ """Print an error message."""
27
+ console.print(f"[bold red]Error:[/bold red] {message}")
28
+
29
+
30
+ def print_success(message: str) -> None:
31
+ """Print a success message."""
32
+ console.print(f"[bold green]\u2713[/bold green] {message}")
33
+
34
+
35
+ def print_extract_result(data: dict[str, Any]) -> None:
36
+ """Print an extract result in a rich format."""
37
+ task_id = data.get("task_id", "N/A")
38
+ trace_id = data.get("trace_id", "N/A")
39
+ elapsed = data.get("elapsed", "N/A")
40
+ result_data = data.get("data", {})
41
+
42
+ console.print(
43
+ Panel(
44
+ f"[bold]Task ID:[/bold] {task_id}\n"
45
+ f"[bold]Trace ID:[/bold] {trace_id}\n"
46
+ f"[bold]Elapsed:[/bold] {elapsed}s",
47
+ title="[bold green]Extract Result[/bold green]",
48
+ border_style="green",
49
+ )
50
+ )
51
+
52
+ if result_data and isinstance(result_data, dict):
53
+ table = Table(show_header=False, box=None, padding=(0, 2))
54
+ table.add_column("Field", style="bold cyan", width=15)
55
+ table.add_column("Value")
56
+ for key in ["url", "contentType", "title", "description", "siteName"]:
57
+ if result_data.get(key):
58
+ table.add_row(key, str(result_data[key]))
59
+ console.print(table)
60
+
61
+
62
+ def print_render_result(data: dict[str, Any]) -> None:
63
+ """Print a render result in a rich format."""
64
+ task_id = data.get("task_id", "N/A")
65
+ trace_id = data.get("trace_id", "N/A")
66
+ elapsed = data.get("elapsed", "N/A")
67
+ result_data = data.get("data", {})
68
+
69
+ console.print(
70
+ Panel(
71
+ f"[bold]Task ID:[/bold] {task_id}\n"
72
+ f"[bold]Trace ID:[/bold] {trace_id}\n"
73
+ f"[bold]Elapsed:[/bold] {elapsed}s",
74
+ title="[bold green]Render Result[/bold green]",
75
+ border_style="green",
76
+ )
77
+ )
78
+
79
+ if result_data and isinstance(result_data, dict):
80
+ table = Table(show_header=False, box=None, padding=(0, 2))
81
+ table.add_column("Field", style="bold cyan", width=15)
82
+ table.add_column("Value")
83
+ for key in ["url", "finalUrl", "title", "status"]:
84
+ if result_data.get(key) is not None:
85
+ table.add_row(key, str(result_data[key]))
86
+ html = result_data.get("html", "")
87
+ if html:
88
+ table.add_row("HTML length", f"{len(html)} chars")
89
+ console.print(table)
90
+
91
+
92
+ def print_task_result(data: dict[str, Any]) -> None:
93
+ """Print task query result in a rich format."""
94
+ items = data.get("items")
95
+ if items is not None:
96
+ # Batch result
97
+ count = data.get("count", len(items))
98
+ console.print(f"[bold]Tasks found:[/bold] {count}")
99
+ for item in items:
100
+ _print_single_task(item)
101
+ else:
102
+ _print_single_task(data)
103
+
104
+
105
+ def _print_single_task(task_data: dict[str, Any]) -> None:
106
+ """Print a single task row."""
107
+ table = Table(show_header=False, box=None, padding=(0, 2))
108
+ table.add_column("Field", style="bold cyan", width=15)
109
+ table.add_column("Value")
110
+ for key in ["id", "task_id", "trace_id", "type", "started_at", "finished_at", "elapsed"]:
111
+ if task_data.get(key) is not None:
112
+ table.add_row(key.replace("_", " ").title(), str(task_data[key]))
113
+ console.print(table)
114
+ console.print()
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ WebExtrator CLI - Web Render & Extract via AceDataCloud.
4
+
5
+ A command-line tool for extracting structured content and rendering web pages
6
+ powered by AceDataCloud.
7
+ """
8
+
9
+ from importlib import metadata
10
+
11
+ import click
12
+ from dotenv import load_dotenv
13
+
14
+ from webextrator_cli.commands.info import config
15
+ from webextrator_cli.commands.task import tasks
16
+ from webextrator_cli.commands.web import extract, render
17
+
18
+ load_dotenv()
19
+
20
+
21
+ def get_version() -> str:
22
+ """Get the package version."""
23
+ try:
24
+ return metadata.version("webextrator-cli")
25
+ except metadata.PackageNotFoundError:
26
+ return "dev"
27
+
28
+
29
+ @click.group()
30
+ @click.version_option(version=get_version(), prog_name="webextrator-cli")
31
+ @click.option(
32
+ "--token",
33
+ envvar="ACEDATACLOUD_API_TOKEN",
34
+ help="API token (or set ACEDATACLOUD_API_TOKEN env var).",
35
+ )
36
+ @click.pass_context
37
+ def cli(ctx: click.Context, token: str | None) -> None:
38
+ """WebExtrator CLI - Web Render & Extract via AceDataCloud.
39
+
40
+ Extract structured data and render web pages from the command line.
41
+
42
+ Get your API token at https://platform.acedata.cloud
43
+
44
+ \b
45
+ Examples:
46
+ webextrator extract https://www.amazon.com/dp/B0C1234567
47
+ webextrator render https://example.com
48
+ webextrator tasks retrieve --id <task-id>
49
+
50
+ Set your token:
51
+ export ACEDATACLOUD_API_TOKEN=your_token
52
+ """
53
+ ctx.ensure_object(dict)
54
+ ctx.obj["token"] = token
55
+
56
+
57
+ # Register commands
58
+ cli.add_command(extract)
59
+ cli.add_command(render)
60
+ cli.add_command(tasks)
61
+ cli.add_command(config)
62
+
63
+
64
+ if __name__ == "__main__":
65
+ cli()
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.1
2
+ Name: webextrator-cli
3
+ Version: 2026.7.1.0
4
+ Summary: CLI tool for WebExtrator Web Render & Extract via AceDataCloud
5
+ Project-URL: Homepage, https://github.com/AceDataCloud/Clis
6
+ Project-URL: Repository, https://github.com/AceDataCloud/Clis
7
+ Project-URL: Issues, https://github.com/AceDataCloud/Clis/issues
8
+ Project-URL: Changelog, https://github.com/AceDataCloud/Clis/blob/main/webextrator/CHANGELOG.md
9
+ Author-email: AceDataCloud <support@acedata.cloud>
10
+ Maintainer-email: AceDataCloud <support@acedata.cloud>
11
+ License: MIT
12
+ License-File: LICENSE
13
+ Keywords: acedata,cli,command-line,web-rendering,web-scraping,webextrator
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Environment :: Console
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: License :: OSI Approved :: MIT License
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Topic :: Internet :: WWW/HTTP
24
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
25
+ Requires-Python: >=3.10
26
+ Requires-Dist: click>=8.1.0
27
+ Requires-Dist: httpx>=0.27.0
28
+ Requires-Dist: pydantic>=2.0.0
29
+ Requires-Dist: python-dotenv>=1.0.0
30
+ Requires-Dist: rich>=13.0.0
31
+ Provides-Extra: all
32
+ Requires-Dist: webextrator-cli[dev,release,test]; extra == 'all'
33
+ Provides-Extra: dev
34
+ Requires-Dist: mypy>=1.10.0; extra == 'dev'
35
+ Requires-Dist: pre-commit>=3.7.0; extra == 'dev'
36
+ Requires-Dist: ruff>=0.4.0; extra == 'dev'
37
+ Provides-Extra: release
38
+ Requires-Dist: build>=1.2.0; extra == 'release'
39
+ Requires-Dist: twine>=6.1.0; extra == 'release'
40
+ Provides-Extra: test
41
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'test'
42
+ Requires-Dist: pytest-cov>=5.0.0; extra == 'test'
43
+ Requires-Dist: pytest>=8.0.0; extra == 'test'
44
+ Requires-Dist: respx>=0.21.0; extra == 'test'
45
+ Description-Content-Type: text/markdown
46
+
47
+ # WebExtrator CLI
48
+
49
+ A command-line tool for web rendering and extraction via [AceDataCloud](https://platform.acedata.cloud).
50
+
51
+ ## Installation
52
+
53
+ ```bash
54
+ pip install webextrator-cli
55
+ ```
56
+
57
+ ## Quick Start
58
+
59
+ ```bash
60
+ export ACEDATACLOUD_API_TOKEN=your_token
61
+
62
+ webextrator extract https://www.amazon.com/dp/B0C1234567
63
+ webextrator render https://example.com
64
+ webextrator tasks retrieve --id <task-id>
65
+ ```
66
+
67
+ ## Commands
68
+
69
+ | Command | Description |
70
+ |---------|-------------|
71
+ | `webextrator extract <url>` | Extract structured content from a web page |
72
+ | `webextrator render <url>` | Render a web page and return the HTML |
73
+ | `webextrator tasks retrieve` | Retrieve a single task by ID or trace ID |
74
+ | `webextrator tasks batch` | Retrieve multiple tasks at once |
75
+ | `webextrator config` | Show current configuration |
76
+
77
+ ## Global Options
78
+
79
+ ```
80
+ --token TEXT API token (or set ACEDATACLOUD_API_TOKEN env var)
81
+ --version Show version
82
+ --help Show help message
83
+ ```
84
+
85
+ ## Configuration
86
+
87
+ Set environment variables or use a `.env` file:
88
+
89
+ ```bash
90
+ ACEDATACLOUD_API_TOKEN=your_token
91
+ ACEDATACLOUD_API_BASE_URL=https://api.acedata.cloud
92
+ WEBEXTRATOR_REQUEST_TIMEOUT=60
93
+ ```
94
+
95
+ ## License
96
+
97
+ MIT
@@ -0,0 +1,17 @@
1
+ webextrator_cli/__init__.py,sha256=hnkmABdt_nUDcRh7s_qnlbryMOmL9WOK5vdlfGONikY,54
2
+ webextrator_cli/__main__.py,sha256=UwsREVewLZPy5FMKrTsVjYcLdG--PNDesfI4h3roFZM,125
3
+ webextrator_cli/main.py,sha256=01F3HwlNU1S3PJ5yaYLaZkUq63y65KGh_WqceblhvB8,1587
4
+ webextrator_cli/commands/__init__.py,sha256=yPsetRwqRP3ugV3tdPAGSdUHNN9hsHIN4qAUNqyHDk4,40
5
+ webextrator_cli/commands/info.py,sha256=nf05whSwk0oC9HvbEXqGcjjwtJCY5zPENlnw-F9xiaQ,683
6
+ webextrator_cli/commands/task.py,sha256=UzY_ahz-7j_6fUkzzAVcX5UODwo4dYNeHQp7cSZTz4I,3218
7
+ webextrator_cli/commands/web.py,sha256=EWK_lT4XRsZoUXWwzdLRPo_gghDlMhcJ62QI1iYfTc0,6958
8
+ webextrator_cli/core/__init__.py,sha256=keeJu-Y-FrpTbLSJjVHUGywB_xtvn2ZJledaoNpVWmI,36
9
+ webextrator_cli/core/client.py,sha256=2J5R1IL5jq-DWR-PJQbYxf5szkeo69RZ0SxKodz909I,3637
10
+ webextrator_cli/core/config.py,sha256=fy2RHL4OdkFdy4Xz-Oi9f9RtfwrZDulruTinNKt7UH0,1128
11
+ webextrator_cli/core/exceptions.py,sha256=FtI9U97z24kKRiaxe8E5CMkCb8YPkOKLgCNpm1FSC0o,1006
12
+ webextrator_cli/core/output.py,sha256=NNBIvWjZBZPRoW2t4auuR309hwvBs1pdzwN_NSmO2i0,3839
13
+ webextrator_cli-2026.7.1.0.dist-info/METADATA,sha256=yUnaD2J8a9ShQcTqVh6w-NM3BnIUhCvezfLL_bhIWQk,3134
14
+ webextrator_cli-2026.7.1.0.dist-info/WHEEL,sha256=TJPnKdtrSue7xZ_AVGkp9YXcvDrobsjBds1du3Nx6dc,87
15
+ webextrator_cli-2026.7.1.0.dist-info/entry_points.txt,sha256=dI54Jr_ZMP_WLzmKdpOsK2zV9uEPD9ubC-IQKiOscyk,100
16
+ webextrator_cli-2026.7.1.0.dist-info/licenses/LICENSE,sha256=pi1I0LFwS_WQBwOlBks6BkKATEJxYi54t8Q22FQ7ufo,1069
17
+ webextrator_cli-2026.7.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.21.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ webextrator = webextrator_cli.main:cli
3
+ webextrator-cli = webextrator_cli.main:cli
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 AceDataCloud
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.