Repository navigation
feat(crawl): add ZenRowsCrawlClient for the Crawl API - #31
Draft
StefanoGuerrini wants to merge 6 commits into
Draft
StefanoGuerrini wants to merge 6 commits into
StefanoGuerrini wants to merge 6 commits into
Conversation
Add a `zenrows.crawl` package with a typed client for the Crawl API: create, get, list_crawls / iter_crawls, iter_results, stop, get_content, iter_download / download, and a wait helper. Responses parse into pydantic v2 models with open enums; errors raise CrawlAPIError (403 REQS008 reads "Crawl is not enabled for this account"). The shared HTTP transport now takes the error mapping and logger as arguments and gains raw and streaming request helpers. Adds unit tests, an opt-in e2e test (`make test-e2e`, deselected from `make test`), a README section, examples/11_crawl.py and DEVELOPMENT.md notes.
Mark Crawl as Beta in the README and the module and client docstrings. iter_results no longer polls. It returns at the first empty page. iter_download and download return the X-Crawl-Status status with the lines or the saved path. Download lines accept object content. CrawlAPIError.code is None when the body has no code. The client takes the base URL only from the constructor. create never retries 429 too_many_crawls. Batch retries do not change. wait defaults to 600 s. The README lists every Crawl error code. The example lets --include replace the default pattern. Add a py.typed marker so type checkers use the package types.
Rename the methods to the names that all Crawl clients share: list_crawls is now list, iter_results is now results (with limit for the page size), get_content is now content, and iter_download is now download. download returns a CrawlDownload with the status and the parsed lines. Remove iter_crawls, the download-to-path helper with CrawlDownloadFile, and the wait poll interval options. The 2 s, x1.5, 15 s backoff stays internal. wait now returns the crawl when the timeout (600 s by default) runs out. The crawl is still running. It no longer raises WaiterTimeout, so the crawl package no longer exports it.
- content() accepts a result, its content_url, or a content id. - CrawlDownload exposes its parsed lines as the lines property. - Rename CRAWL_NOT_ENABLED to CRAWL_NOT_ENABLED_CODE and remove CrawlAPIError.not_enabled. - Remove CrawlResult.content_id. - Remove the runtime check on output_format; the type hint stays. - Document the client options base_url, retries and timeout.
Wait checks the status against running directly. The default base URL stays internal to the client.
Customer-facing Crawl text now says New. The README Crawl section and the package doc carry one still-evolving line.
gonzalo-zenrows
added this pull request to stack #33
October 9, 2026 19:51
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR replaces #30. The code is the same. The history drops the AI co-author trailer. The review of the first version is here.
What this adds
A
zenrows.crawlpackage withZenRowsCrawlClient, also exported from the top level (from zenrows import ZenRowsCrawlClient). Crawl is New: the README and the client docstrings say "(new)", and the README and the package docstring add one line that Crawl is still evolving. There is no runtime warning.ZenRowsCrawlClient(api_key, *, base_url=None, timeout=30.0, retries=3, verify_ssl=True, user_agent=..., httpx_args=None). The default base URL ishttps://api.zenrows.com/v1. Override it withbase_url=only; the client reads no environment variable.create(url, *, depth, max_items=None, max_pages=None, include_patterns=None, exclude_patterns=None, output_format: Literal["html"] | None = None, idempotency_key=None) -> Crawlsends only the fields you set.output_formatis checked by the type hint only; the API decides. Withoutoutput_format, a crawl returns URLs only. A crawl stays on the start URL's registrable domain; subdomains count.get(crawl_id, *, cursor=None, limit=None) -> CrawlWithResultslist(*, cursor=None, limit=None) -> CrawlListreturns one page of crawls, newest first.next_cursoris None on the last page.results(crawl_id, *, limit=None)followsnext_cursor,limitURLs per request, and returns at the first empty page or a null cursor. It does not poll: on a running crawl it yields only what the crawl has kept so far. Callwaitfirst to read every result.stop(crawl_id) -> CrawlStopis idempotent.content(crawl_id, content) -> strreturns one kept URL's HTML.contentis aCrawlResult, itscontent_url, or a content id.download(crawl_id) -> CrawlDownloadopens the NDJSON file. Iterate itslinesproperty for parsedDownloadLines, and read.status(fromX-Crawl-Status). It is also a context manager. On a running crawl,statusisrunningand the file holds only what the crawl has kept so far.DownloadLine.contentisstr | dict | None, so a line with object content parses.wait(crawl_id, *, timeout=600.0) -> Crawlpolls with backoff (2 s, x1.5, up to 15 s) until the crawl ends ortimeoutseconds pass. It never raises for the crawl's state. On timeout it returns the crawl, stillrunning; callwaitagain orstop. Afailedcrawl is returned; readcrawl.error.codeandcrawl.error.detail.UNKNOWNand keeps its raw value.CrawlAPIError, withstatus_code,code,detail,problemandretry_after.codeis None when the body has no code. The README lists every code:invalid_request/unknown_parameter/invalid_cursor.REQS008reads "Crawl is not enabled for this account". Compareexc.codewith the exportedCRAWL_NOT_ENABLED_CODE.crawl_not_found/content_not_found.idempotency_request_in_flight: retry after the first request finishes.invalid_parameter/invalid_start_url/domain_not_allowed, andidempotency_key_reused(use a new key or no key).too_many_crawls, withretry_after: the account has reached its limit of active jobs (3 by default), shared with its Batch jobs.createnever retries it, with or without an idempotency key.py.typedmarker (in the wheel and the sdist), so type checkers use its types.The SDK's shared HTTP transport now takes the error mapping and logger as constructor arguments, gains
request()andstream()helpers, and takes the retryable statuses per request.ProblemDetail.codeis None when the body has no code;BatchAPIErrorkeeps its"internal"fallback. Existing clients behave as before.Tests
make checkpasses (ty, ruff check, ruff format --check).make test: 205 passed, 1 deselected (the e2e test).waitreturning a running crawl on timeout and a failed crawl,listpaging with a cursor, andresultssendinglimit.tests/e2e/test_crawl_e2e.py:e2eand deselected frommake test.ZENROWS_API_KEY,ZENROWS_CRAWL_BASE_URLandZENROWS_E2E_CRAWL_URLare set. It passesZENROWS_CRAWL_BASE_URLasbase_url=.ZENROWS_E2E_CRAWL_INCLUDEis optional.make test-e2e. DEVELOPMENT.md has a "Running the Crawl e2e test" section.make test-e2eagainst a live deployment before the method renames: 1 passed. I did not run it again after them.Docs: a Crawl API (new) quickstart section in the README (with the client options
base_url,retriesandtimeout),examples/11_crawl.py, and DEVELOPMENT.md notes.Release: this PR does not change the version. Do a minor bump (1.6.0) in the release commit.