public-source-extractor 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. public_source_extractor-0.1.0/LICENSE +22 -0
  2. public_source_extractor-0.1.0/PKG-INFO +82 -0
  3. public_source_extractor-0.1.0/README.md +237 -0
  4. public_source_extractor-0.1.0/docs/pypi-readme.md +58 -0
  5. public_source_extractor-0.1.0/pyproject.toml +49 -0
  6. public_source_extractor-0.1.0/setup.cfg +4 -0
  7. public_source_extractor-0.1.0/src/public_source_extractor/__init__.py +3 -0
  8. public_source_extractor-0.1.0/src/public_source_extractor/__main__.py +5 -0
  9. public_source_extractor-0.1.0/src/public_source_extractor/cli.py +61 -0
  10. public_source_extractor-0.1.0/src/public_source_extractor/errors.py +58 -0
  11. public_source_extractor-0.1.0/src/public_source_extractor/output.py +123 -0
  12. public_source_extractor-0.1.0/src/public_source_extractor/provider.py +154 -0
  13. public_source_extractor-0.1.0/src/public_source_extractor/schemas/__init__.py +0 -0
  14. public_source_extractor-0.1.0/src/public_source_extractor/schemas/output-v0.1.schema.json +90 -0
  15. public_source_extractor-0.1.0/src/public_source_extractor/url_policy.py +171 -0
  16. public_source_extractor-0.1.0/src/public_source_extractor.egg-info/PKG-INFO +82 -0
  17. public_source_extractor-0.1.0/src/public_source_extractor.egg-info/SOURCES.txt +27 -0
  18. public_source_extractor-0.1.0/src/public_source_extractor.egg-info/dependency_links.txt +1 -0
  19. public_source_extractor-0.1.0/src/public_source_extractor.egg-info/entry_points.txt +2 -0
  20. public_source_extractor-0.1.0/src/public_source_extractor.egg-info/requires.txt +5 -0
  21. public_source_extractor-0.1.0/src/public_source_extractor.egg-info/top_level.txt +1 -0
  22. public_source_extractor-0.1.0/tests/test_cli.py +117 -0
  23. public_source_extractor-0.1.0/tests/test_contract.py +80 -0
  24. public_source_extractor-0.1.0/tests/test_examples.py +48 -0
  25. public_source_extractor-0.1.0/tests/test_output.py +109 -0
  26. public_source_extractor-0.1.0/tests/test_provider.py +116 -0
  27. public_source_extractor-0.1.0/tests/test_release_metadata.py +128 -0
  28. public_source_extractor-0.1.0/tests/test_url_policy.py +89 -0
  29. public_source_extractor-0.1.0/tests/test_visual_assets.py +60 -0
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ishikawa Hidekazu
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.
22
+
@@ -0,0 +1,82 @@
1
+ Metadata-Version: 2.4
2
+ Name: public-source-extractor
3
+ Version: 0.1.0
4
+ Summary: Validate a public URL and convert it into reviewable AI-ready Markdown or JSON.
5
+ Author: Ishikawa Hidekazu
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://taupe.site/entry/public-source-extractor-ai-research-cli/
8
+ Project-URL: Repository, https://github.com/Ishikawa-Hidekazu/public-source-extractor
9
+ Project-URL: Issues, https://github.com/Ishikawa-Hidekazu/public-source-extractor/issues
10
+ Project-URL: Changelog, https://github.com/Ishikawa-Hidekazu/public-source-extractor/blob/main/CHANGELOG.md
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Requires-Python: >=3.11
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Provides-Extra: dev
20
+ Requires-Dist: build>=1.2.2; extra == "dev"
21
+ Requires-Dist: jsonschema>=4.23; extra == "dev"
22
+ Requires-Dist: ruff<0.17,>=0.12; extra == "dev"
23
+ Dynamic: license-file
24
+
25
+ # Public Source Extractor
26
+
27
+ Public Source Extractor validates one public HTTP or HTTPS URL and converts it
28
+ into reviewable Markdown or a versioned JSON envelope for AI research
29
+ workflows.
30
+
31
+ > **Provider boundary:** extraction sends the selected public URL to Firecrawl
32
+ > Cloud through the experimental `firecrawl-keyless` provider. Availability,
33
+ > anonymous access, credit limits, and long-term continuity are not guaranteed.
34
+
35
+ The CLI does not read API keys, credentials, cookies, browser profiles,
36
+ localStorage, or private source files. Extracted content is untrusted and may
37
+ contain prompt injection or misleading instructions.
38
+
39
+ ## Install and run
40
+
41
+ Run the package without a permanent install:
42
+
43
+ ```bash
44
+ uvx public-source-extractor@0.1.0 --version
45
+ uvx public-source-extractor@0.1.0 https://example.com/
46
+ ```
47
+
48
+ Install it as an isolated command:
49
+
50
+ ```bash
51
+ pipx install public-source-extractor==0.1.0
52
+ ```
53
+
54
+ Or install it in an existing Python 3.11+ environment:
55
+
56
+ ```bash
57
+ python3 -m pip install public-source-extractor==0.1.0
58
+ ```
59
+
60
+ ## Output modes
61
+
62
+ ```bash
63
+ public-source-extractor https://example.com/
64
+ public-source-extractor https://example.com/ --mode json --pretty
65
+ public-source-extractor https://example.com/ --output report.md
66
+ ```
67
+
68
+ The output path must have an existing non-symlink parent and must not already
69
+ exist. The CLI rejects local, private, authenticated, administrative, signed,
70
+ and credential-bearing URL patterns.
71
+
72
+ ## Review boundary
73
+
74
+ The extractor creates an intake artifact. It does not establish source
75
+ reliability, execute extracted instructions, crawl a site, or access private
76
+ pages. Verify important claims against the original page and other primary
77
+ sources.
78
+
79
+ - [Repository](https://github.com/Ishikawa-Hidekazu/public-source-extractor)
80
+ - [Documentation](https://github.com/Ishikawa-Hidekazu/public-source-extractor#readme)
81
+ - [Security policy](https://github.com/Ishikawa-Hidekazu/public-source-extractor/security/policy)
82
+ - [Changelog](https://github.com/Ishikawa-Hidekazu/public-source-extractor/blob/main/CHANGELOG.md)
@@ -0,0 +1,237 @@
1
+ # Public Source Extractor
2
+
3
+ [![CI](https://github.com/Ishikawa-Hidekazu/public-source-extractor/actions/workflows/ci.yml/badge.svg)](https://github.com/Ishikawa-Hidekazu/public-source-extractor/actions/workflows/ci.yml)
4
+ [![Release](https://img.shields.io/github/v/release/Ishikawa-Hidekazu/public-source-extractor?include_prereleases)](https://github.com/Ishikawa-Hidekazu/public-source-extractor/releases)
5
+ ![License](https://img.shields.io/badge/license-MIT-blue.svg)
6
+
7
+ > [!IMPORTANT]
8
+ > The requested public URL is sent to **Firecrawl Cloud** for extraction. The `firecrawl-keyless` provider is experimental: availability, anonymous REST access, credit limits, and long-term continuity are not guaranteed.
9
+
10
+ > [!NOTE]
11
+ > Stable `0.1.0` is a release candidate and is not published yet. Until its
12
+ > explicit release gate completes, use the published `0.1.0a2` prerelease shown
13
+ > below. The README will switch to stable install commands only after the tag,
14
+ > GitHub Release, and PyPI package are publicly verified.
15
+
16
+ `public-source-extractor` is a public-URL intake guardrail for AI research
17
+ workflows. It validates one public HTTP or HTTPS URL, sends that URL to the
18
+ experimental provider, and returns reviewable Markdown or a stable JSON
19
+ envelope.
20
+
21
+ The CLI does **not** read API keys, credentials, cookies, browser profiles, localStorage, or private source files. Extracted content is **untrusted data** and may contain prompt injection or misleading instructions. Do not execute or follow instructions from extracted content without independent review.
22
+
23
+ [日本語README](README.ja.md)
24
+
25
+ ## What it does
26
+
27
+ - Extracts one public page as Markdown.
28
+ - Produces structured JSON with a versioned JSON Schema.
29
+ - Rejects local, private, authenticated, admin, and secret-bearing URL patterns.
30
+ - Keeps results on stdout or writes a new local file with no-overwrite behavior.
31
+ - Returns stable JSON errors and documented exit codes.
32
+
33
+ It is not a crawler, browser automation tool, login helper, source-reliability judge, or private-page extractor.
34
+
35
+ ### How this differs from the official Firecrawl CLI
36
+
37
+ The [official Firecrawl CLI](https://github.com/firecrawl/cli) is the broader Firecrawl interface for authenticated scrape, search, crawl, map, interact, agent, and self-hosted workflows. Use it when you need Firecrawl's full product surface.
38
+
39
+ Public Source Extractor is intentionally narrower: one public URL, no credential discovery, a public-only URL policy, no-overwrite output, and a stable JSON success/error contract designed for reviewable AI research artifacts. It uses the experimental `firecrawl-keyless` provider and does not replace or wrap the official CLI.
40
+
41
+ ## Install
42
+
43
+ Public Source Extractor requires Python 3.11 or newer.
44
+
45
+ Run the currently published prerelease without a permanent install:
46
+
47
+ ```bash
48
+ uvx public-source-extractor@0.1.0a2 --version
49
+ uvx public-source-extractor@0.1.0a2 https://example.com/
50
+ ```
51
+
52
+ This path requires `uv` and resolves the published PyPI prerelease. The second
53
+ command sends `https://example.com/` to Firecrawl Cloud; the version command
54
+ does not perform extraction.
55
+
56
+ Pin the exact package version when reproducibility matters:
57
+
58
+ ```bash
59
+ uvx public-source-extractor@0.1.0a2 --version
60
+ ```
61
+
62
+ Install the published prerelease as an isolated command:
63
+
64
+ ```bash
65
+ pipx install public-source-extractor==0.1.0a2
66
+ ```
67
+
68
+ Or use pip in an existing Python environment:
69
+
70
+ ```bash
71
+ python3 -m pip install public-source-extractor==0.1.0a2
72
+ ```
73
+
74
+ The public Git tag remains an auditable fallback:
75
+
76
+ ```bash
77
+ uvx --from 'git+https://github.com/Ishikawa-Hidekazu/public-source-extractor.git@v0.1.0-alpha.2' public-source-extractor --version
78
+ ```
79
+
80
+ PyPI publication uses GitHub Actions Trusted Publishing with short-lived OIDC
81
+ credentials. No long-lived PyPI API token is stored in this repository.
82
+
83
+ ## Quick start
84
+
85
+ Markdown to stdout:
86
+
87
+ ```bash
88
+ public-source-extractor https://example.com/
89
+ ```
90
+
91
+ Markdown front matter includes `provider_credits_used` and
92
+ `provider_elapsed_ms`. The credits field is `null` when the experimental
93
+ provider does not report it; elapsed time is measured by the CLI in
94
+ milliseconds. These are metadata-only values. Raw provider responses and
95
+ request identifiers are not exposed.
96
+
97
+ JSON to stdout:
98
+
99
+ ```bash
100
+ public-source-extractor https://example.com/ --mode json --pretty
101
+ ```
102
+
103
+ Write a new file:
104
+
105
+ ```bash
106
+ public-source-extractor https://example.com/ --output report.md
107
+ ```
108
+
109
+ The output path must have an existing non-symlink parent and must not already exist.
110
+
111
+ ## Codex Agent Plugin preview
112
+
113
+ Codex CLI `0.147.0` or newer can load this repository as a portable Agent Plugin marketplace. This preview adds a safety-focused skill and starter prompts; it does not install a global Python package, add an MCP server, or store credentials.
114
+
115
+ ```bash
116
+ codex plugin marketplace add Ishikawa-Hidekazu/public-source-extractor --ref main
117
+ codex plugin add public-source-extractor@ishikawa-public-tools
118
+ ```
119
+
120
+ The skill uses an existing `public-source-extractor` command when available and
121
+ otherwise runs the latest verified published package through `uvx`. Until
122
+ stable `0.1.0` is public, that fallback remains `0.1.0a2`. Extraction still
123
+ sends the selected public URL to Firecrawl Cloud. Review the
124
+ [Safety boundary](#safety-boundary) before use.
125
+
126
+ <picture>
127
+ <source media="(max-width: 600px)" srcset="assets/source/terminal-example-mobile.svg">
128
+ <img src="assets/source/terminal-example.svg" alt="Fixture-only Public Source Extractor terminal example showing one public example.com URL converted to Markdown, followed by the stable provider_rate_limited JSON error contract.">
129
+ </picture>
130
+
131
+ [View the public-safe examples](examples/) ·
132
+ [View the reproducible visual sources](assets/source/README.md)
133
+
134
+ ## Real-world use
135
+
136
+ [The repository-hosted GitHub Changelog case study](docs/case-study-github-changelog.md)
137
+ shows a pinned command, a successful public-source extraction, the human review
138
+ step, and the failure boundary without using browser state or local credentials.
139
+
140
+ [This Japanese implementation log](https://taupe.site/entry/public-source-extractor-ai-research-cli/)
141
+ documents the broader daily workflow, Firecrawl Cloud boundary, untrusted
142
+ extracted content, and observed provider limits.
143
+
144
+ [Stable 0.1.0 criteria](docs/stable-0.1.0-criteria.md) separate the local CLI
145
+ contract from the experimental provider's availability.
146
+
147
+ ## CLI contract
148
+
149
+ ```text
150
+ public-source-extractor <url> [--mode markdown|json]
151
+ [--output <new-path>]
152
+ [--timeout 1..120]
153
+ [--provider firecrawl-keyless]
154
+ [--pretty]
155
+ ```
156
+
157
+ | Exit | Meaning |
158
+ |---:|---|
159
+ | `0` | Success |
160
+ | `2` | Usage error or rejected URL |
161
+ | `3` | Provider, network, rate-limit, or timeout failure |
162
+ | `4` | Invalid, incomplete, or unsafe provider response |
163
+ | `5` | Output path or write failure |
164
+
165
+ On failure, stdout is empty and stderr contains exactly one JSON error object. Provider response bodies, stack traces, request IDs, and local paths are not exposed.
166
+
167
+ ### Recovering from `provider_rate_limited`
168
+
169
+ The experimental provider can return HTTP 429 during short bursts or across an anonymous usage window. This is a provider availability condition, not by itself a failure of the local URL policy or parser.
170
+
171
+ When stderr reports `provider_rate_limited` with `retryable: true`:
172
+
173
+ ```json
174
+ {"schema_version":"0.1","ok":false,"error":{"code":"provider_rate_limited","message":"The experimental provider rate limit was exceeded.","retryable":true}}
175
+ ```
176
+
177
+ The process exits with code `3` and stdout remains empty.
178
+
179
+ 1. Stop the current burst instead of retrying repeatedly.
180
+ 2. Wait and retry later. This CLI does not promise an exact delay when no retry timing is safely exposed.
181
+ 3. Reduce request volume and process only selected public sources.
182
+ 4. Use the original public page directly when extraction is not required.
183
+
184
+ The CLI does not automatically retry, switch providers, discover credentials, or expose raw provider bodies. See [Issue #9](https://github.com/Ishikawa-Hidekazu/public-source-extractor/issues/9) for the observed condition and documentation scope.
185
+
186
+ ## Safety boundary
187
+
188
+ Before sending a URL, the CLI rejects:
189
+
190
+ - schemes other than HTTP or HTTPS;
191
+ - URL user information and fragments;
192
+ - localhost, local suffixes, and non-global literal IP addresses;
193
+ - ambiguous integer, octal, and hexadecimal IPv4 forms;
194
+ - Unicode or percent-encoded hostnames and IPv6 zone identifiers;
195
+ - non-default ports;
196
+ - login, admin, OAuth, and callback paths, including encoded forms;
197
+ - query parameter names that indicate tokens, secrets, passwords, credentials, signatures, sessions, cookies, authorization, or keys.
198
+
199
+ After extraction, provider redirect metadata is checked with the same public URL policy. If redirect metadata is unsafe, the result is discarded. If the provider does not supply redirect metadata, the output contains a warning.
200
+
201
+ These checks validate hostname syntax, literal IP policy, and provider-returned metadata. They cannot fully guarantee DNS rebinding behavior or the provider's actual fetch destination. Never put credentials, tokens, signed URL parameters, or other private values in a URL, even when its hostname looks public.
202
+
203
+ Firecrawl Cloud receives the requested URL and processes the page. Review [Firecrawl's terms](https://www.firecrawl.dev/terms-of-service), the target site's terms, robots policy, copyright, and privacy requirements before use.
204
+
205
+ ## JSON Schema and examples
206
+
207
+ - [Output Schema v0.1](schemas/output-v0.1.schema.json)
208
+ - [Public-safe Markdown example](examples/example-report.md)
209
+ - [Public-safe JSON example](examples/example-report.json)
210
+
211
+ Structured JSON extraction is inferred output, not source truth. Validate important claims against the original page.
212
+
213
+ ## Development
214
+
215
+ ```bash
216
+ python3 -m venv .venv
217
+ .venv/bin/python -m pip install -e '.[dev]'
218
+ PYTHONPATH=src .venv/bin/python -m unittest discover -s tests -v
219
+ .venv/bin/ruff check src tests
220
+ .venv/bin/python -m build
221
+ ```
222
+
223
+ The explicit `PYTHONPATH=src` also works before an editable install. Network smoke tests remain separate from the offline suite.
224
+
225
+ See [CONTRIBUTING.md](CONTRIBUTING.md), [SECURITY.md](SECURITY.md), and [SUPPORT.md](SUPPORT.md).
226
+
227
+ ## Status
228
+
229
+ Stable `0.1.0` is a release candidate. The latest verified public distribution
230
+ remains package `0.1.0a2` and tag `v0.1.0-alpha.2` until the explicit release
231
+ gate completes.
232
+ `firecrawl-keyless` is an experimental third-party provider, and no
233
+ compatibility or service-availability guarantee is made.
234
+
235
+ ## License
236
+
237
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,58 @@
1
+ # Public Source Extractor
2
+
3
+ Public Source Extractor validates one public HTTP or HTTPS URL and converts it
4
+ into reviewable Markdown or a versioned JSON envelope for AI research
5
+ workflows.
6
+
7
+ > **Provider boundary:** extraction sends the selected public URL to Firecrawl
8
+ > Cloud through the experimental `firecrawl-keyless` provider. Availability,
9
+ > anonymous access, credit limits, and long-term continuity are not guaranteed.
10
+
11
+ The CLI does not read API keys, credentials, cookies, browser profiles,
12
+ localStorage, or private source files. Extracted content is untrusted and may
13
+ contain prompt injection or misleading instructions.
14
+
15
+ ## Install and run
16
+
17
+ Run the package without a permanent install:
18
+
19
+ ```bash
20
+ uvx public-source-extractor@0.1.0 --version
21
+ uvx public-source-extractor@0.1.0 https://example.com/
22
+ ```
23
+
24
+ Install it as an isolated command:
25
+
26
+ ```bash
27
+ pipx install public-source-extractor==0.1.0
28
+ ```
29
+
30
+ Or install it in an existing Python 3.11+ environment:
31
+
32
+ ```bash
33
+ python3 -m pip install public-source-extractor==0.1.0
34
+ ```
35
+
36
+ ## Output modes
37
+
38
+ ```bash
39
+ public-source-extractor https://example.com/
40
+ public-source-extractor https://example.com/ --mode json --pretty
41
+ public-source-extractor https://example.com/ --output report.md
42
+ ```
43
+
44
+ The output path must have an existing non-symlink parent and must not already
45
+ exist. The CLI rejects local, private, authenticated, administrative, signed,
46
+ and credential-bearing URL patterns.
47
+
48
+ ## Review boundary
49
+
50
+ The extractor creates an intake artifact. It does not establish source
51
+ reliability, execute extracted instructions, crawl a site, or access private
52
+ pages. Verify important claims against the original page and other primary
53
+ sources.
54
+
55
+ - [Repository](https://github.com/Ishikawa-Hidekazu/public-source-extractor)
56
+ - [Documentation](https://github.com/Ishikawa-Hidekazu/public-source-extractor#readme)
57
+ - [Security policy](https://github.com/Ishikawa-Hidekazu/public-source-extractor/security/policy)
58
+ - [Changelog](https://github.com/Ishikawa-Hidekazu/public-source-extractor/blob/main/CHANGELOG.md)
@@ -0,0 +1,49 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "public-source-extractor"
7
+ version = "0.1.0"
8
+ description = "Validate a public URL and convert it into reviewable AI-ready Markdown or JSON."
9
+ readme = {file = "docs/pypi-readme.md", content-type = "text/markdown"}
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ authors = [{name = "Ishikawa Hidekazu"}]
13
+ dependencies = []
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ ]
21
+
22
+ [project.urls]
23
+ Homepage = "https://taupe.site/entry/public-source-extractor-ai-research-cli/"
24
+ Repository = "https://github.com/Ishikawa-Hidekazu/public-source-extractor"
25
+ Issues = "https://github.com/Ishikawa-Hidekazu/public-source-extractor/issues"
26
+ Changelog = "https://github.com/Ishikawa-Hidekazu/public-source-extractor/blob/main/CHANGELOG.md"
27
+
28
+ [project.optional-dependencies]
29
+ dev = [
30
+ "build>=1.2.2",
31
+ "jsonschema>=4.23",
32
+ "ruff>=0.12,<0.17",
33
+ ]
34
+
35
+ [project.scripts]
36
+ public-source-extractor = "public_source_extractor.cli:main"
37
+
38
+ [tool.setuptools.package-data]
39
+ public_source_extractor = ["schemas/*.json"]
40
+
41
+ [tool.setuptools]
42
+ include-package-data = true
43
+
44
+ [tool.ruff]
45
+ target-version = "py311"
46
+ line-length = 100
47
+
48
+ [tool.ruff.lint]
49
+ select = ["E4", "E7", "E9", "F"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """Public Source Extractor package."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ from .cli import main
2
+
3
+
4
+ raise SystemExit(main())
5
+
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Sequence
8
+
9
+ from . import __version__
10
+ from .errors import ExtractorError, InputRejected
11
+ from .output import build_success_envelope, render_json, render_markdown, write_new_file_atomic
12
+ from .provider import FirecrawlKeylessProvider
13
+ from .url_policy import validate_public_url
14
+
15
+
16
+ class StableArgumentParser(argparse.ArgumentParser):
17
+ def error(self, _message: str) -> None:
18
+ raise InputRejected()
19
+
20
+
21
+ def build_parser() -> argparse.ArgumentParser:
22
+ parser = StableArgumentParser(
23
+ prog="public-source-extractor",
24
+ description="Convert one public URL into Markdown or a stable JSON envelope.",
25
+ )
26
+ parser.add_argument("url", help="Public HTTP or HTTPS URL.")
27
+ parser.add_argument("--mode", choices=("markdown", "json"), default="markdown")
28
+ parser.add_argument("--output", type=Path, help="Write to a new file instead of stdout.")
29
+ parser.add_argument("--timeout", type=int, default=60, metavar="SECONDS")
30
+ parser.add_argument("--provider", choices=("firecrawl-keyless",), default="firecrawl-keyless")
31
+ parser.add_argument("--pretty", action="store_true", help="Pretty-print JSON output.")
32
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
33
+ return parser
34
+
35
+
36
+ def run(argv: Sequence[str] | None = None) -> int:
37
+ try:
38
+ args = build_parser().parse_args(argv)
39
+ if not 1 <= args.timeout <= 120:
40
+ raise InputRejected()
41
+ validate_public_url(args.url)
42
+ provider = FirecrawlKeylessProvider()
43
+ result = provider.extract(args.url, args.mode, args.timeout)
44
+ envelope = build_success_envelope(args.url, args.mode, result)
45
+ rendered = (
46
+ render_markdown(envelope)
47
+ if args.mode == "markdown"
48
+ else render_json(envelope, args.pretty)
49
+ )
50
+ if args.output is None:
51
+ sys.stdout.write(rendered)
52
+ else:
53
+ write_new_file_atomic(args.output, rendered)
54
+ return 0
55
+ except ExtractorError as exc:
56
+ sys.stderr.write(json.dumps(exc.envelope(), ensure_ascii=False) + "\n")
57
+ return exc.exit_code
58
+
59
+
60
+ def main() -> int:
61
+ return run()
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class ExtractorError(Exception):
5
+ code = "extractor_error"
6
+ exit_code = 1
7
+ retryable = False
8
+ public_message = "Extraction failed."
9
+
10
+ def __init__(self, message: str | None = None) -> None:
11
+ super().__init__(message or self.public_message)
12
+
13
+ def envelope(self) -> dict[str, object]:
14
+ return {
15
+ "schema_version": "0.1",
16
+ "ok": False,
17
+ "error": {
18
+ "code": self.code,
19
+ "message": self.public_message,
20
+ "retryable": self.retryable,
21
+ },
22
+ }
23
+
24
+
25
+ class InputRejected(ExtractorError):
26
+ code = "input_rejected"
27
+ exit_code = 2
28
+ public_message = "The URL was rejected by the public-source safety policy."
29
+
30
+
31
+ class ProviderFailure(ExtractorError):
32
+ code = "provider_failure"
33
+ exit_code = 3
34
+ retryable = True
35
+ public_message = "The extraction provider is unavailable or rejected the request."
36
+
37
+
38
+ class ProviderRateLimited(ProviderFailure):
39
+ code = "provider_rate_limited"
40
+ public_message = "The experimental provider rate limit was exceeded."
41
+
42
+
43
+ class ProviderTimeout(ProviderFailure):
44
+ code = "provider_timeout"
45
+ public_message = "The extraction provider timed out."
46
+
47
+
48
+ class InvalidProviderResponse(ExtractorError):
49
+ code = "invalid_provider_response"
50
+ exit_code = 4
51
+ public_message = "The extraction provider returned an invalid or unsafe response."
52
+
53
+
54
+ class OutputFailure(ExtractorError):
55
+ code = "output_failure"
56
+ exit_code = 5
57
+ public_message = "The output file could not be written safely."
58
+