docpull 5.5.0__tar.gz → 5.5.1__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.
- {docpull-5.5.0/src/docpull.egg-info → docpull-5.5.1}/PKG-INFO +2 -2
- {docpull-5.5.0 → docpull-5.5.1}/README.md +1 -1
- {docpull-5.5.0 → docpull-5.5.1}/pyproject.toml +1 -1
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/__init__.py +1 -1
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/context_packs/search.py +24 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/context_packs/visuals.py +114 -4
- {docpull-5.5.0 → docpull-5.5.1/src/docpull.egg-info}/PKG-INFO +2 -2
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_ci_policy.py +26 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_context_packs.py +59 -0
- {docpull-5.5.0 → docpull-5.5.1}/LICENSE +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/setup.cfg +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/__main__.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/accounting.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/auth_cli.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/benchmark.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/cache/__init__.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/cache/frontier.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/cache/manager.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/cache/streaming_dedup.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/cli.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/context_packs/__init__.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/context_packs/brand.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/context_packs/cli.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/context_packs/common.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/context_packs/product.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/context_packs/schema_extract.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/context_packs/styleguide.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/conversion/__init__.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/conversion/chunking.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/conversion/extractor.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/conversion/filings.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/conversion/markdown.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/conversion/protocols.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/conversion/special_cases.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/conversion/trafilatura_extractor.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/core/__init__.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/core/fetcher.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/discovery/__init__.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/discovery/_fetch.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/discovery/composite.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/discovery/contracts.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/discovery/crawler.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/discovery/filters.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/discovery/link_extractors/__init__.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/discovery/link_extractors/enhanced.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/discovery/link_extractors/protocols.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/discovery/link_extractors/static.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/discovery/protocols.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/discovery/sitemap.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/discovery_cli.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/doctor.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/evidence_pack.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/exports.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/fixtures/__init__.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/fixtures/parallel-search-extract.json +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/graph.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/hosted.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/http/__init__.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/http/client.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/http/protocols.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/http/rate_limiter.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/judge.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/local_workflows.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/mcp/__init__.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/mcp/server.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/mcp/sources.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/mcp/tools.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/metadata_extractor.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/models/__init__.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/models/config.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/models/document.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/models/events.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/models/profiles.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/models/run.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/monitor.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/pack_reader.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/pack_tools.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/parallel_workflows.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/parity.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/parity_cli.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/passk.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/pipeline/__init__.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/pipeline/base.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/pipeline/manifest.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/pipeline/steps/__init__.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/pipeline/steps/chunk.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/pipeline/steps/convert.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/pipeline/steps/dedup.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/pipeline/steps/fetch.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/pipeline/steps/metadata.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/pipeline/steps/render.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/pipeline/steps/save.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/pipeline/steps/save_json.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/pipeline/steps/save_ndjson.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/pipeline/steps/save_okf.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/pipeline/steps/save_sqlite.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/pipeline/steps/validate.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/policy.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/policy_cli.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/project.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/provider_adapters.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/provider_capabilities.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/provider_cli.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/provider_keys.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/provider_probes.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/py.typed +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/rendering.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/scraper.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/security/__init__.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/security/download_policy.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/security/robots.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/security/url_validator.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/server.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/share.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/skill_export.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/source_scoring.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull/time_utils.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull.egg-info/SOURCES.txt +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull.egg-info/dependency_links.txt +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull.egg-info/entry_points.txt +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull.egg-info/requires.txt +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/src/docpull.egg-info/top_level.txt +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_accounting.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_auth_cli.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_benchmark.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_cache_conditional_get.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_chunking.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_cli.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_conversion.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_convert_step_new.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_discovery.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_discovery_contracts.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_doctor.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_document_record.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_evidence_pack.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_exports.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_frontier_resume.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_graph.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_hosted.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_integration.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_judge.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_link_extractors.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_local_workflows.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_mcp_server.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_mcp_tools.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_metadata_extractor.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_monitor.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_naming.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_outputs_e2e.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_pack_server.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_pack_tools.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_parallel_workflows.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_parity_workflows.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_passk.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_pipeline.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_policy.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_policy_cli.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_project.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_provider_adapters.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_provider_cli.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_provider_keys.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_provider_probes.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_rate_limiter.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_real_site_regressions.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_rendering.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_save_ndjson.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_save_sqlite.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_security_hardening.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_share.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_special_cases.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_surface_contract.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_time_utils.py +0 -0
- {docpull-5.5.0 → docpull-5.5.1}/tests/test_trafilatura_extractor.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: docpull
|
|
3
|
-
Version: 5.5.
|
|
3
|
+
Version: 5.5.1
|
|
4
4
|
Summary: Keep AI agents synced with changing public docs and web sources
|
|
5
5
|
Author-email: Zachary Roth <support@raintree.technology>
|
|
6
6
|
Maintainer-email: Raintree Technology <support@raintree.technology>
|
|
@@ -115,7 +115,7 @@ Dynamic: license-file
|
|
|
115
115
|
**Keep AI agents synced with changing public docs. Browser-free by default.**
|
|
116
116
|
|
|
117
117
|
[](https://www.python.org/downloads/)
|
|
118
|
-
[](https://pypi.org/project/docpull/)
|
|
119
119
|
[](https://pepy.tech/project/docpull)
|
|
120
120
|
[](https://github.com/raintree-technology/docpull/stargazers)
|
|
121
121
|
[](https://github.com/raintree-technology/docpull/blob/main/LICENSE)
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
**Keep AI agents synced with changing public docs. Browser-free by default.**
|
|
4
4
|
|
|
5
5
|
[](https://www.python.org/downloads/)
|
|
6
|
-
[](https://pypi.org/project/docpull/)
|
|
7
7
|
[](https://pepy.tech/project/docpull)
|
|
8
8
|
[](https://github.com/raintree-technology/docpull/stargazers)
|
|
9
9
|
[](https://github.com/raintree-technology/docpull/blob/main/LICENSE)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "docpull"
|
|
7
|
-
version = "5.5.
|
|
7
|
+
version = "5.5.1"
|
|
8
8
|
dynamic = []
|
|
9
9
|
description = "Keep AI agents synced with changing public docs and web sources"
|
|
10
10
|
readme = {file = "README.md", content-type = "text/markdown"}
|
|
@@ -95,6 +95,14 @@ def _build_local_search_pack(
|
|
|
95
95
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
96
96
|
local = search_local_pack(pack_dir, query, required_domains=required_domains, limit=max_results)
|
|
97
97
|
results = _normalize_local_results(local)
|
|
98
|
+
replay_config = {
|
|
99
|
+
"query": query,
|
|
100
|
+
"provider": "local",
|
|
101
|
+
"pack_dir": str(pack_dir),
|
|
102
|
+
"include_domains": required_domains,
|
|
103
|
+
"max_results": max_results,
|
|
104
|
+
"scrape": scrape,
|
|
105
|
+
}
|
|
98
106
|
result_path = output_dir / "search.result.json"
|
|
99
107
|
results_path = output_dir / "search.results.ndjson"
|
|
100
108
|
markdown_path = output_dir / "SEARCH.md"
|
|
@@ -120,6 +128,7 @@ def _build_local_search_pack(
|
|
|
120
128
|
"output_dir": str(output_dir),
|
|
121
129
|
"query": query,
|
|
122
130
|
"input": {"pack_dir": str(pack_dir), "scrape": scrape},
|
|
131
|
+
"replay_config": replay_config,
|
|
123
132
|
"summary": {
|
|
124
133
|
"result_count": len(results),
|
|
125
134
|
"max_results": max_results,
|
|
@@ -155,6 +164,7 @@ def _build_local_search_pack(
|
|
|
155
164
|
"query": query,
|
|
156
165
|
"summary": payload["summary"],
|
|
157
166
|
"request_options": payload["request_options"],
|
|
167
|
+
"replay_config": replay_config,
|
|
158
168
|
"artifacts": {**artifacts, "pack_metadata": artifact_ref(output_dir, pack_path)},
|
|
159
169
|
},
|
|
160
170
|
)
|
|
@@ -219,6 +229,17 @@ def _build_provider_search_pack(
|
|
|
219
229
|
"max_results": max_results,
|
|
220
230
|
"scrape": scrape,
|
|
221
231
|
}
|
|
232
|
+
replay_config = {
|
|
233
|
+
"query": query,
|
|
234
|
+
"provider": provider,
|
|
235
|
+
"include_domains": include_domains,
|
|
236
|
+
"exclude_domains": exclude_domains,
|
|
237
|
+
"max_results": max_results,
|
|
238
|
+
"scrape": scrape,
|
|
239
|
+
"dry_run": dry_run,
|
|
240
|
+
"budget": budget,
|
|
241
|
+
"max_estimated_cost": max_estimated_cost,
|
|
242
|
+
}
|
|
222
243
|
if dry_run or blocked:
|
|
223
244
|
result_path = output_dir / "search.result.json"
|
|
224
245
|
results_path = output_dir / "search.results.ndjson"
|
|
@@ -245,6 +266,7 @@ def _build_provider_search_pack(
|
|
|
245
266
|
"status": "dry_run" if dry_run and not blocked else "blocked_by_budget",
|
|
246
267
|
"output_dir": str(output_dir),
|
|
247
268
|
"query": query,
|
|
269
|
+
"replay_config": replay_config,
|
|
248
270
|
"summary": {
|
|
249
271
|
"result_count": 0,
|
|
250
272
|
"estimated_cost_usd": estimated,
|
|
@@ -283,6 +305,7 @@ def _build_provider_search_pack(
|
|
|
283
305
|
"query": query,
|
|
284
306
|
"summary": payload["summary"],
|
|
285
307
|
"request_options": request_options,
|
|
308
|
+
"replay_config": replay_config,
|
|
286
309
|
"artifacts": payload["artifacts"],
|
|
287
310
|
},
|
|
288
311
|
)
|
|
@@ -324,6 +347,7 @@ def _build_provider_search_pack(
|
|
|
324
347
|
"status": "completed",
|
|
325
348
|
"output_dir": str(output_dir),
|
|
326
349
|
"query": query,
|
|
350
|
+
"replay_config": replay_config,
|
|
327
351
|
"summary": {
|
|
328
352
|
"result_count": _parallel_result_count(pack_path),
|
|
329
353
|
"estimated_cost_usd": estimated,
|
|
@@ -375,11 +375,121 @@ def _run_screenshot_command(
|
|
|
375
375
|
text=True,
|
|
376
376
|
timeout=45,
|
|
377
377
|
)
|
|
378
|
+
legacy_error = ""
|
|
379
|
+
if result.returncode == 0:
|
|
380
|
+
try:
|
|
381
|
+
png = _png_from_agent_browser_stdout(result.stdout)
|
|
382
|
+
except ContextPackError as err:
|
|
383
|
+
legacy_error = str(err)
|
|
384
|
+
else:
|
|
385
|
+
output_path.write_bytes(png)
|
|
386
|
+
return _screenshot_payload(
|
|
387
|
+
url=url,
|
|
388
|
+
output_path=output_path,
|
|
389
|
+
viewport=viewport,
|
|
390
|
+
full_page=full_page,
|
|
391
|
+
png=png,
|
|
392
|
+
command=[part if part != url else public_url(url) for part in command],
|
|
393
|
+
)
|
|
394
|
+
else:
|
|
395
|
+
legacy_error = (result.stderr or result.stdout).strip()[:500]
|
|
396
|
+
try:
|
|
397
|
+
png, command = _run_agent_browser_batch_screenshot(
|
|
398
|
+
binary=binary,
|
|
399
|
+
url=url,
|
|
400
|
+
output_path=output_path,
|
|
401
|
+
viewport=viewport,
|
|
402
|
+
full_page=full_page,
|
|
403
|
+
wait_for=wait_for,
|
|
404
|
+
)
|
|
405
|
+
except ContextPackError as err:
|
|
406
|
+
detail = f"{legacy_error}; batch fallback: {err}" if legacy_error else str(err)
|
|
407
|
+
raise ContextPackError(
|
|
408
|
+
f"agent-browser screenshot failed with status {result.returncode}: {detail}"
|
|
409
|
+
) from err
|
|
410
|
+
return _screenshot_payload(
|
|
411
|
+
url=url,
|
|
412
|
+
output_path=output_path,
|
|
413
|
+
viewport=viewport,
|
|
414
|
+
full_page=full_page,
|
|
415
|
+
png=png,
|
|
416
|
+
command=command,
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _run_agent_browser_batch_screenshot(
|
|
421
|
+
*,
|
|
422
|
+
binary: str,
|
|
423
|
+
url: str,
|
|
424
|
+
output_path: Path,
|
|
425
|
+
viewport: str,
|
|
426
|
+
full_page: bool,
|
|
427
|
+
wait_for: str,
|
|
428
|
+
) -> tuple[bytes, list[str]]:
|
|
429
|
+
width, height = _viewport_dimensions(viewport)
|
|
430
|
+
wait_ms = {"domcontentloaded": "500", "load": "1000", "networkidle": "2000"}[wait_for]
|
|
431
|
+
screenshot_command = ["screenshot", str(output_path)]
|
|
432
|
+
if full_page:
|
|
433
|
+
screenshot_command.append("--full")
|
|
434
|
+
batch = [
|
|
435
|
+
["open", url],
|
|
436
|
+
["set", "viewport", str(width), str(height)],
|
|
437
|
+
["wait", wait_ms],
|
|
438
|
+
screenshot_command,
|
|
439
|
+
]
|
|
440
|
+
batch_json = json.dumps(batch, separators=(",", ":"))
|
|
441
|
+
command = [binary, "batch", "--bail", "--json"]
|
|
442
|
+
result = subprocess.run( # nosec B603
|
|
443
|
+
command,
|
|
444
|
+
input=batch_json,
|
|
445
|
+
check=False,
|
|
446
|
+
capture_output=True,
|
|
447
|
+
text=True,
|
|
448
|
+
timeout=45,
|
|
449
|
+
)
|
|
378
450
|
if result.returncode != 0:
|
|
379
451
|
detail = (result.stderr or result.stdout).strip()[:500]
|
|
380
|
-
raise ContextPackError(
|
|
381
|
-
|
|
382
|
-
|
|
452
|
+
raise ContextPackError(
|
|
453
|
+
f"agent-browser batch screenshot failed with status {result.returncode}: {detail}"
|
|
454
|
+
)
|
|
455
|
+
_validate_agent_browser_batch_stdout(result.stdout)
|
|
456
|
+
if not output_path.exists():
|
|
457
|
+
raise ContextPackError("agent-browser batch screenshot did not write the expected PNG path.")
|
|
458
|
+
png = output_path.read_bytes()
|
|
459
|
+
if not png.startswith(b"\x89PNG\r\n\x1a\n"):
|
|
460
|
+
raise ContextPackError("agent-browser batch screenshot did not write PNG data.")
|
|
461
|
+
public_batch = [[public_url(item) if item == url else item for item in step] for step in batch]
|
|
462
|
+
return png, [*command, json.dumps(public_batch, separators=(",", ":"))]
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _viewport_dimensions(viewport: str) -> tuple[int, int]:
|
|
466
|
+
match = VIEWPORT_RE.fullmatch(viewport)
|
|
467
|
+
if not match:
|
|
468
|
+
raise ContextPackError("viewport must use WIDTHxHEIGHT with positive integer dimensions.")
|
|
469
|
+
return int(match.group("width")), int(match.group("height"))
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def _validate_agent_browser_batch_stdout(stdout: str) -> None:
|
|
473
|
+
try:
|
|
474
|
+
payload = json.loads(stdout)
|
|
475
|
+
except json.JSONDecodeError as err:
|
|
476
|
+
raise ContextPackError("agent-browser batch screenshot did not return JSON output.") from err
|
|
477
|
+
if not isinstance(payload, list):
|
|
478
|
+
raise ContextPackError("agent-browser batch screenshot JSON was not a command result array.")
|
|
479
|
+
for item in payload:
|
|
480
|
+
if not isinstance(item, dict) or item.get("success") is not True:
|
|
481
|
+
raise ContextPackError("agent-browser batch screenshot reported a failed command.")
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _screenshot_payload(
|
|
485
|
+
*,
|
|
486
|
+
url: str,
|
|
487
|
+
output_path: Path,
|
|
488
|
+
viewport: str,
|
|
489
|
+
full_page: bool,
|
|
490
|
+
png: bytes,
|
|
491
|
+
command: list[str],
|
|
492
|
+
) -> dict[str, Any]:
|
|
383
493
|
return {
|
|
384
494
|
"schema_version": CONTEXT_PACK_SCHEMA_VERSION,
|
|
385
495
|
"url": url,
|
|
@@ -389,7 +499,7 @@ def _run_screenshot_command(
|
|
|
389
499
|
"bytes": len(png),
|
|
390
500
|
"sha256": hashlib.sha256(png).hexdigest(),
|
|
391
501
|
"content_type": "image/png",
|
|
392
|
-
"command":
|
|
502
|
+
"command": command,
|
|
393
503
|
}
|
|
394
504
|
|
|
395
505
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: docpull
|
|
3
|
-
Version: 5.5.
|
|
3
|
+
Version: 5.5.1
|
|
4
4
|
Summary: Keep AI agents synced with changing public docs and web sources
|
|
5
5
|
Author-email: Zachary Roth <support@raintree.technology>
|
|
6
6
|
Maintainer-email: Raintree Technology <support@raintree.technology>
|
|
@@ -115,7 +115,7 @@ Dynamic: license-file
|
|
|
115
115
|
**Keep AI agents synced with changing public docs. Browser-free by default.**
|
|
116
116
|
|
|
117
117
|
[](https://www.python.org/downloads/)
|
|
118
|
-
[](https://pypi.org/project/docpull/)
|
|
119
119
|
[](https://pepy.tech/project/docpull)
|
|
120
120
|
[](https://github.com/raintree-technology/docpull/stargazers)
|
|
121
121
|
[](https://github.com/raintree-technology/docpull/blob/main/LICENSE)
|
|
@@ -124,6 +124,32 @@ def test_publish_workflow_builds_artifact_before_unlocked_release_gates() -> Non
|
|
|
124
124
|
assert "needs: [build, release-gates]" in publish_section
|
|
125
125
|
|
|
126
126
|
|
|
127
|
+
def test_publish_workflow_creates_github_release_after_pypi_publish() -> None:
|
|
128
|
+
publish = (WORKFLOW_DIR / "publish.yml").read_text()
|
|
129
|
+
_, publish_section = publish.split("\n publish:\n", 1)
|
|
130
|
+
|
|
131
|
+
assert "contents: write" in publish_section
|
|
132
|
+
assert "id-token: write" in publish_section
|
|
133
|
+
assert "pypa/gh-action-pypi-publish@release/v1" in publish_section
|
|
134
|
+
assert "name: Create GitHub release" in publish_section
|
|
135
|
+
assert "if: github.event_name == 'push' && github.ref_type == 'tag'" in publish_section
|
|
136
|
+
assert "GH_TOKEN: ${{ github.token }}" in publish_section
|
|
137
|
+
assert 'NOTES_FILE="docs/release-post-v${MINOR_VERSION}.md"' in publish_section
|
|
138
|
+
assert (
|
|
139
|
+
'gh release edit "$TAG" --title "$RELEASE_TITLE" --latest --notes-file "$NOTES_FILE"'
|
|
140
|
+
in publish_section
|
|
141
|
+
)
|
|
142
|
+
assert 'gh release edit "$TAG" --title "$RELEASE_TITLE" --latest' in publish_section
|
|
143
|
+
assert (
|
|
144
|
+
'gh release create "$TAG" --title "$RELEASE_TITLE" --verify-tag --latest --notes-file "$NOTES_FILE"'
|
|
145
|
+
in publish_section
|
|
146
|
+
)
|
|
147
|
+
assert (
|
|
148
|
+
'gh release create "$TAG" --title "$RELEASE_TITLE" --verify-tag --latest --generate-notes'
|
|
149
|
+
in publish_section
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
127
153
|
def test_security_and_publish_bandit_scan_scripts() -> None:
|
|
128
154
|
security = (WORKFLOW_DIR / "security.yml").read_text()
|
|
129
155
|
publish = (WORKFLOW_DIR / "publish.yml").read_text()
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import base64
|
|
5
6
|
import json
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
from types import SimpleNamespace
|
|
@@ -173,8 +174,11 @@ def test_search_pack_local_searches_existing_pack(tmp_path: Path) -> None:
|
|
|
173
174
|
payload = build_search_pack("cited JSON", pack_dir=pack, output_dir=tmp_path / "search")
|
|
174
175
|
|
|
175
176
|
assert payload["provider"] == "local"
|
|
177
|
+
assert payload["replay_config"]["pack_dir"] == str(pack)
|
|
176
178
|
assert payload["summary"]["result_count"] >= 1
|
|
177
179
|
assert payload["artifacts"]["accounting"] == "run.accounting.json"
|
|
180
|
+
written = json.loads((tmp_path / "search" / "search.pack.json").read_text(encoding="utf-8"))
|
|
181
|
+
assert written["replay_config"]["provider"] == "local"
|
|
178
182
|
assert (tmp_path / "search" / "search.results.ndjson").exists()
|
|
179
183
|
assert (tmp_path / "search" / "run.accounting.json").exists()
|
|
180
184
|
|
|
@@ -189,7 +193,10 @@ def test_search_pack_provider_dry_run_writes_pack_artifacts(tmp_path: Path) -> N
|
|
|
189
193
|
|
|
190
194
|
assert payload["status"] == "dry_run"
|
|
191
195
|
assert payload["output_dir"] == str((tmp_path / "search").resolve())
|
|
196
|
+
assert payload["replay_config"]["provider"] == "tavily"
|
|
192
197
|
assert payload["artifacts"]["source_policy"] == "source_policy.json"
|
|
198
|
+
written = json.loads((tmp_path / "search" / "search.pack.json").read_text(encoding="utf-8"))
|
|
199
|
+
assert written["replay_config"]["dry_run"] is True
|
|
193
200
|
assert (tmp_path / "search" / "search.pack.json").exists()
|
|
194
201
|
assert (tmp_path / "search" / "SEARCH.md").exists()
|
|
195
202
|
assert (tmp_path / "search" / "run.accounting.json").exists()
|
|
@@ -203,6 +210,58 @@ def test_screenshot_pack_validates_options_before_renderer_gate(tmp_path: Path)
|
|
|
203
210
|
capture_screenshot_pack("https://acme.test", output_dir=tmp_path / "shot", wait_for="sleep")
|
|
204
211
|
|
|
205
212
|
|
|
213
|
+
def test_screenshot_pack_falls_back_to_agent_browser_batch(
|
|
214
|
+
tmp_path: Path,
|
|
215
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
216
|
+
) -> None:
|
|
217
|
+
from docpull.context_packs import visuals
|
|
218
|
+
|
|
219
|
+
png = base64.b64decode(
|
|
220
|
+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADUlEQVR4nGNgYGBgAAAABQABDQottAAAAABJRU5ErkJggg=="
|
|
221
|
+
)
|
|
222
|
+
calls: list[dict[str, object]] = []
|
|
223
|
+
|
|
224
|
+
def fake_run(command: list[str], **kwargs: object) -> SimpleNamespace:
|
|
225
|
+
calls.append({"command": command, "kwargs": kwargs})
|
|
226
|
+
if "--timeout" in command:
|
|
227
|
+
return SimpleNamespace(
|
|
228
|
+
returncode=1,
|
|
229
|
+
stdout='{"error":"Unknown command: --timeout","success":false}',
|
|
230
|
+
stderr="",
|
|
231
|
+
)
|
|
232
|
+
assert command == ["agent-browser", "batch", "--bail", "--json"]
|
|
233
|
+
batch = json.loads(str(kwargs["input"]))
|
|
234
|
+
assert batch[0] == ["open", "https://acme.test/"]
|
|
235
|
+
assert batch[1] == ["set", "viewport", "800", "600"]
|
|
236
|
+
screenshot_path = Path(batch[-1][1])
|
|
237
|
+
screenshot_path.write_bytes(png)
|
|
238
|
+
return SimpleNamespace(
|
|
239
|
+
returncode=0,
|
|
240
|
+
stdout=json.dumps(
|
|
241
|
+
[{"command": step, "success": True, "result": {}} for step in batch[:-1]]
|
|
242
|
+
+ [{"command": batch[-1], "success": True, "result": {"path": str(screenshot_path)}}]
|
|
243
|
+
),
|
|
244
|
+
stderr="",
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
monkeypatch.setenv("DOCPULL_RENDER_TRUSTED_BROWSER_TARGETS", "1")
|
|
248
|
+
monkeypatch.setattr(visuals.shutil, "which", lambda binary: f"/bin/{binary}")
|
|
249
|
+
monkeypatch.setattr(visuals.subprocess, "run", fake_run)
|
|
250
|
+
|
|
251
|
+
payload = capture_screenshot_pack(
|
|
252
|
+
"https://acme.test",
|
|
253
|
+
output_dir=tmp_path / "shot",
|
|
254
|
+
viewport="800x600",
|
|
255
|
+
agent_browser_binary="agent-browser",
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
assert payload["status"] == "completed"
|
|
259
|
+
assert payload["screenshots"][0]["bytes"] == len(png)
|
|
260
|
+
assert payload["screenshots"][0]["command"][1:4] == ["batch", "--bail", "--json"]
|
|
261
|
+
assert (tmp_path / "shot" / "screenshots" / "page.png").read_bytes() == png
|
|
262
|
+
assert len(calls) == 2
|
|
263
|
+
|
|
264
|
+
|
|
206
265
|
@pytest.mark.parametrize(
|
|
207
266
|
"argv",
|
|
208
267
|
[
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|