Changelog¶
0.48.1 - 2026-11-14¶
Fixed¶
- A batch showed each config's result only after the last config ended.
run_manywith more than one config now logs each result ondatabrew.progresswhen its config ends (#131). run_manylabeled the log records of a config given as a path with the file's stem, and the report with the config'sname. Both carry thenamenow, and so do the records ofrun, which carried-(#135).
0.48.0 - 2026-11-13¶
A crawl now walks its listing pages as a traversal that resumes across runs, and every listing
sighting is an observation. .state.db holds the queue, the traversals, and the failures.
Added¶
- Traversals: the first is exhaustive, later ones are incremental, and an interrupted one resumes
without repeating completed pages. A paused traversal keeps its start URLs, and a run with others
fails before it changes listing progress. Failed listing requests permit a new traversal and remain
visible as coverage gaps. Exhausted requests wait for
reactivateor--rescan. - A listing page that no fetch has expanded, or whose last attempt failed or was cut off, follows
its pagination in any traversal, so a page read after a failure, a
reactivate, or an empty fetch reaches the pages behind it. RunReport.traversal(pausedorcompleted) andRunReport.coverage_gaps, in.last_run.jsonand the summary line, andStatus.traversalwith its lines instatusandcheck.databrew.Traversal, the type ofStatus.traversal.
Changed¶
- Breaking: the top-level tables
[listing]and[item]replace[extract.listing]and[extract.item], and a config with[extract]fails to load. - Breaking:
[vars]and${name}substitution are removed, and a config with[vars]fails to load. A child ofextendssets its own values in place. - Breaking:
--rescanandRunOptions.rescanreplace--fresh,--full-crawl,fresh, andfull_crawl. A rescan begins an exhaustive traversal and keeps item URLs, their failures, and the stores. - Breaking:
.state.dbis the record of the queue, the traversals, the failures, and each item's last fetch and validators. A missing one beside an earlier crawl, an unreadable one, or one of another schema fails the run. More stored versions rebuild only the item index; fewer require restoring records and state together. Stores written by 0.47 are incompatible; keep them with that version and use a new storage path for a new crawl. databrew no longer reads the contents of_failed_urls.json. - Breaking: an observation is one card of one response, keyed
fetch_id:N, so each sighting is stored, on the same day and with unchanged values too. - Breaking:
[policy].stop_on_no_new_itemsis removed, and a config that sets it fails to load. An incremental traversal stops a branch at an expanded page whose items are all known, on a site without item pages too. - Breaking:
CrawlStats.record_dropsis removed,CrawlStats.record_successtakes no counts, andCrawlStats.record_listing_pagerequiresstart, which marks a start URL of the traversal. - A run fails for all-empty listing pages only when it fetched a start URL, so a resumed or
--retry-failedrun that reaches only an empty last page succeeds. - A failed listing page returns among listing pages in the order it was queued, ahead of revisits.
databrew initwrites no key at its default value and leaves[fetch],[storage], and[policy]commented out.- An expanded listing page in an incremental traversal continues for a new item or a card that might be new: one without a key, or one discarded during extraction. Watched changes, revisits, and changed content do not extend it. Novelty uses the traversal-start baseline across roots and resumes; detail deduplication uses all stored items.
- A listing page is charged a failed run in every run that it fails, before any success too, so
only item pages are
maybe_blocked. A run that fetches nothing while listing coverage gaps remain fails. --retry-failedqueues what a recovered page links to, and the revisits it finds, for the next run.
Fixed¶
--retry-failedskipped failed requests that a stop left queued or in flight.items_droppedomitted cards discarded for missing required fields or item postprocess failures.- A listing page that failed after queueing item links could lose its pagination once those items were stored before it was fetched again.
- A version count that differed from
items/lost the queue, the last unchanged check, and the validators. read_reportsreturnedurls_answered,listing_pages, andempty_listing_pagesas zero.- Paced requests could start in a burst after synchronous work stalled the event loop.
0.47.0 - 2026-11-10¶
databrew now keeps a site's records current instead of snapshotting them once. Every change to a
record is a version, a listing card is one observation a day, [revisit] fetches known records
again, and each crawl operation has a command and a library call that do the same thing. 0.47 does
not read a store that 0.46 wrote.
Added¶
[revisit]:after_daysandwatchqueue known item pages after new work, with conditional requests onhttpxandcurl_cffi.watchcompares a card with its values at the last fetch of its item page, and needs[extract.listing.items].keywhen[extract.item.items]has a key.databrew parseanddatabrew.parse, which extract one page as a run does, through the middleware'spre_fetch,post_fetch, andpost_extract, and write no crawl state.databrew.check,status,reactivate, andread_reports, with thePlanandStatustypes.-qonrun, and a progress line per config per minute by default (#128).[policy].max_requests_per_minute, an upper bound on the fetch rate.databrew.StoreErrorfor a store or_failed_urls.jsonthat cannot be read and for a store that refuses[storage], with itemstore's message and its error as the cause. The CLI prints it in one line and exits 1.
Changed¶
- Breaking: databrew requires extractkit 0.25.1, fetchkit 0.23.1, and itemstore 0.29. Each store
holds one schema:
key,item_key,url,fetched_at, anddataas one JSON column. - Breaking:
[extract.listing]and[extract.item]each hold one extractkit config and itstype, andurl_typesayslistingoritem. They replace[extract].type,[extract.items],[extract.links], anditems_from.[extract.item]needs[extract.item.items]. - Breaking: an item-page item is a version keyed
item_key:N, numbered per record and stored when its content differs from the latest version. A listing-page item is an observation keyeditem_key:YYYY-MM-DD, and also a version on a site without[extract.item], which then needs[extract.listing.items].key. - Breaking: an item page holds one item and takes the key of the card that linked it, else its own key, else a digest of its URL. A page that yields several items fails.
- Breaking:
.state.dbis a cache, rebuilt from the stores and_failed_urls.jsonwhen it is missing, is not a readable database, or has another schema, when that file differs from its last export, or whenitems/holds another count of versions than it recorded. Failures live in itsurlstable. - A full crawl lives in the queue. It fetches each listing page once, past the caught-up and
stop_on_no_new_itemsstops, goes on in the next run when one stops early, and goes on from a listing page that failed when a later run fetches it. A start URL new to the queue begins one, so a new store, a start URL added to a config, and the empty queue that--freshor a rebuilt.state.dbleaves are crawled in full.--full-crawlbegins one for every start URL. - A start URL that a run stopped short of is served ahead of the ones it fetched, so a budget smaller than the list of start URLs reaches each of them over a few runs.
- A reactivated URL is served behind fresh work, as every URL with a failure on record is.
- The summary of degraded extraction is a warning, so the default output of
runshows it. - Breaking:
[policy]has 15 keys.max_requestsnames the budget everywhere, with-n/--max-requestsandRunOptions.max_requests.max_retry_aftercaps every retry delay. A fixed table says which fetch failures retry, 408 among them, andadaptive_concurrencyreads its own window. - Breaking:
[storage]ispath,format,compression, andpart_size_mb. An existing store keeps the compression and the part size it recorded when the table names none. - Breaking:
.last_run.json,RunReport, and the summary carrynew_items,changed_items,observations, andtotal_items. A run is productive when it stored a new item or a change. - Breaking:
checktakes several configs and prints the plan thatrun --dry-runprinted. It checks each config whatever the one before it did.statusandcheckrebuild a missing or stale.state.dbbefore they count. - Breaking: the default output of
runleaves out per-page lines, which-vprints. -vprints databrew's debug lines and leaves out those of the libraries under it.- Breaking:
reactivatetakes--urland--dry-run, and startsfailed_runsover. - Breaking:
--freshclears the whole queue and the failures. max_failed_runscounts failed runs in a row, since a success starts the count over.- Breaking:
build_componentsno longer checks a supplied fetcher, whichrunchecks before it loads the config. - A config that
--deadlineor a signal keeps from starting reports as failed and writes no.last_run.json, so its store's record still describes the last run that started. - The extra for the curl_cffi backend is named
curl_cffi.
Removed¶
- Breaking:
[hooks], the--on-*flags,HookCallbacks,on_recover,replace_fetcher, andStop.HOOK_ABORT. Middleware is the extension point for recovery. - Breaking:
[observations],ObservationsMiddleware,.watch.db,revisions/,revisit_scheduled, andrequeue_url. - Breaking:
.failures.dbandFailureStore. - Breaking:
databrew.state.SchemaVersionError, since a.state.dbof another schema version is rebuilt. - Breaking:
run --dry-run,run --limit,RunOptions.limit,list --config-dir, and reactivate's--error,--status,--max-age, and--reset-failed-runs. - Breaking: the
[policy]keysmax_retry_delay,retryable_categories,min_requests_for_error_rate,stop_on_empty,stop_on_caught_up,caught_up_threshold,aimd_error_threshold,aimd_grow_step, anddelay, andStop.CAUGHT_UP. - Breaking:
[stamp],[storage].state_db_name, the[extract].base_urlalias, and[fetch].jitter. - Breaking:
replace_extract_result, sinceExtractResulttakesdataclasses.replace. - Breaking:
CrawlStats.summary,error_rate,urls_queued,urls_retried,last_success_at, andlinks_dropped. - Breaking:
StateStore.add_pagination_url,add_item_url,url_completed_count,url_permanently_failed_count,read_items, andget_failed_urls, and the single-URL adds ofUrlQueue, which only tests called. - Breaking: the
StateStoremethods that forwarded to its queue, such asmark_url_doneandurl_pending_count.StateStore.queueis theUrlQueue, whosecountandrestoretake a status. - Breaking: the
run_*hook methods ofMiddlewareChain, whichrun(hook, ctx)replaces, and thefull_crawlargument ofOrchestrator, which serves one run. - The matching on itemstore's error messages (#99).
Fixed¶
- An item page that answers 200 without an item stays out of the stop rules, as a 404 does (#130).
StateStorecloses its connection when its constructor raises.- A command that finds the store held by a running crawl exits with a one-line error, and
runleaves that store's.last_run.jsonto the crawl that holds it. examples/configs/ethiopiapropertycentre.tomlmatches the redesigned site (#129).run_manyraisesValueErrorforparallelbelow 1, which hung, and a--deadlinethat passed while the configs loaded starts none of them.- A
WebsiteConfigbuilt in code stores underdata/<name>, as one loaded from TOML does. initwith stdin at end of file reports the missing name, and the-chelp names no fixed default.
0.46.0 - 2026-10-07¶
Breaking changes¶
Each entry ends with the edit a config or a program makes.
- databrew requires fetchkit 0.21 and extractkit 0.22. The zendriver backend
always navigates and rejects the
modeandheadersoptions. Its results carry the document's status, so a 403 or 429 block page fails the URL. Deletemodeandheadersfrom the[fetch]table of a zendriver config. - The PyDoll backend and the
pydollextra are gone. Setbackend = "zendriver"and install thezendriverextra. Delete the PyDoll optionswait_for_network_idleandnetwork_idle_time, which zendriver rejects. - The
[policy]optionfetcher_close_timeoutis gone, since zendriver's close finishes its cleanup even when cancelled. Delete the option. load_config(path)builds no fetcher or extractor and raisesConfigErrorwith a one-line message.databrew checkand--dry-runbuild the components to check them.create_components,apply_cli_overrides,validate_fetcher,create_extractor, thevalidate_extractorandvalidate_fetcherarguments ofload_config, andrunner.run_crawlare gone. Replace a hand-builtOrchestratorwithdatabrew.run(config, options=RunOptions(...), middleware=[...], fetcher=...). Code that needs the parts callsbuild_components(config, fetcher)andopen_state(config, fresh=, retry_failed=).- A run reports why it stopped as a
Stopand how it went as anOutcome, both in aRunReport.Orchestrator.run()returns it,on_completereceives it, andCrawlPolicy.should_stopreturns theStopand its detail orNone.CrawlResult,StopClassification,CrawlPolicy.classify_stop_reason, the stop-reason constants indatabrew.core.policy, andwrite_last_run_recordare gone. Readreport.stopandreport.outcomewhere code matchedstopped_reasontext, and letdatabrew.runwrite.last_run.json. databrew.core.policy.CHALLENGE_CATEGORYis gone. Compare an error category withfetchkit.ErrorCategory.CHALLENGE, which equals"challenge".- Every run except
--retry-failedadds the start URLs, also when the queue holds pending work, so a crawl that stopped on a throttle still fetches page 1 of each category the next day (#120).StateStore.url_resumable_count()andUrlQueue.resumable_count()are gone. Delete code that seeds start URLs itself, or callStateStore.seed_start_urls(urls, fresh=...). - The built-in
LoggingMiddleware,HeaderMiddleware,UrlFilterMiddleware,RetryOnPatternMiddleware,ItemEnricherMiddleware,LinkFilterMiddleware, andCookieRefreshMiddleware, theMiddlewareChain.addmethod, theon_item,on_url_complete, andon_errorarguments ofOrchestrator, and the cookie-refresh example are gone. No config or known crawler used them. Copy a class you need into the project and list it in[[middleware]]. HookCallbacks,HookContext,run_hook,build_hook_callbacks,MiddlewareChain,UrlTask,CrawlComponents,Orchestrator, andStateStoreleavedatabrew.__all__, which lists the supported API. Import them from their modules, or usedatabrew.run.build_hook_callbackstakes(hooks, name, get_orchestrator, rebuild_fetcher),CrawlComponentsholdsfetcher,extractor, andmiddleware, andStorageConfig.itemstore_options()is gone. Code that wired these into anOrchestratorcallsdatabrew.run.- Extraction reads the fetch result directly. Middleware that replaces
ctx.responsesupplies an object withurlandtext, such as afetchkit.FetchResult. Orchestrator.pause(),Orchestrator.resume(),StateStore.url_failed_runs_sum(), andUrlQueue.failed_runs_sum()are gone. Delete the calls.Orchestrator.stop(reason)ends a run.reset_failed_itemsofStateStoreandUrlQueueisreset_failed_urls, which returns failed listing pages to the queue as well, and theirreset_failed_for_retry_onlyis gone. Callreset_failed_urls.- The
failures_db_nameargument ofStateStoreis gone. The failure database is.failures.db, the namedatabrew reactivatereads. Drop the argument. databrew status,check,reactivate, andrun --dry-runexit with status 2 and one line on stderr when a config cannot load, where they exited with 1. When any of its configs cannot load,databrew runruns the others and exits with 2, whatever--fail-whensays. A script that tells a bad config from a failed run checks for 2.${name}in a config string is a variable, and a name that[vars]does not set is a config error. Write a shell variable in a hook command as$NAME.[storage].state_db_nameand the observation store directories reject every name that databrew or itemstore uses at the storage root, which addsitems,.writer.lock.db, and.last_run.json. Rename a state database or directory that uses one.- The module
databrew.config.storageis gone, andITEMS_DIR_NAMEleavesdatabrew.config. ImportStorageConfigfromdatabrew.configandITEMS_DIR_NAMEfromdatabrew.itemstore_adapter. StateStore.stats()andUrlQueue.pagination_seen_count()are gone. Call the count methods ofStateStore, such asurl_pending_count()anditem_count().- A run that empties its queue ends with the
queue_emptystop, also when its last URLs reachedmax_consecutive_failuresor its last fetch reached--limit, and its failed URLs stay inurls_failed. It used to end withconsecutive_failures, which made a run without a new itemfailed, or withmax_requests. A run with work left stops on those rules as before. A script that read either stop as work left reads the queue withdatabrew status.
Added¶
databrew.run(config, *, options, middleware, fetcher)runs one crawl asdatabrew rundoes and returns itsRunReport.RunOptionsholds what thedatabrew runflags express, andrun_many(configs, *, options)runs several configs. The CLI is a thin caller ofrun_many, so a project that needs one middleware or fetcher keeps seeding, hooks, passes, the run record, and a clean stop at SIGINT or SIGTERM.runhandles a signal on the main thread when the program has neither set a handler for it nor ignored it, and gives the signal its handler back when it ends.runraisesConfigErrorwhen the config cannot load. When the setup of a loaded config raises, it returns a failed report with theerrorstop.databrew.hash_url(url)is the key of an item whose config sets no[extract.items].key.[[middleware]]tables run middleware from the config file (#46).usenames the class as"module:Class", from beside the config file when that module is there and from the installed packages otherwise, and the other keys go to its constructor. Loading checks the class and arguments. The config's directory is a package of its own: a module or package beside the config imports from it, two config directories can each hold a module of the same name, and a module imports its siblings relatively (from . import helpers). Entries run in file order after the observations middleware, a child's entries follow its parent's, and a middleware withclose()oraclose()is closed when the run ends.databrew run --parallel Nruns up to N configs at once, and--deadline SECONDSstops every run that long after process start, as a signal does. The deadline is a positive finite number.--fail-when any|allpicks the exit rule. Log lines name their config, the deadline's and a signal's warning included, and the run ends with one summary line per config. A config that cannot load, or whose setup raises, counts as a failed run, and the others run.run_many(configs, *, parallel, deadline, options)is the library form.[vars]sets variables, and${name}in any string of the config takes the value. A child's[vars]merges over its parent's throughextends, so a shared parent plus a few lines per country or site replaces a file of repeated settings. A[vars]value is a string or a number, and a boolean is a config error. An unknown${name}is a config error that names the key, and so is a${name}inside a[vars]value.extendstakes a literal path, since[vars]is known only once the files are merged.[stamp]adds constant fields, such ascountry = "${cc}", to every stored item, observation, and revision. An extracted field of the same name wins. Without the table, stored payloads stay as they are.databrew --versionprints the installed version.databrew report PATH...prints one line per store from its.last_run.json, and a totals line, in the form of the run summary. It writes nothing, reports a missing or unreadable record as failed, and reads records from databrew 0.45.--fail-when any|allpicks the exit rule, and--stale-after Nexits with 1 when any store has gone N runs without a new item, whatever--fail-whensays.Orchestrator.stop(reason)ends a run from outside the crawl loop.MiddlewareContext.stop_reasonlets middleware end the run with a reason, set on the context a hook returns, whether it is the one the hook received or a new one. The run takes no new URLs and ends once the batch in flight finishes..last_run.json,CrawlStats, and the run's log count the items and item links that extraction dropped, from extractkit's drop observations, asitems_droppedandlinks_dropped. A drop changes neither the outcome nordegraded_responses.StateStore.mark_url_maybe_blocked(url, error)gives a URL themaybe_blockedstatus, andStateStore.restore_maybe_blocked_urls()returns those URLs to pending. The first pass of the next run fetches them, a listing page after the other listing pages and an item URL after the other pending URLs.databrew statusprints them asURLs maybe blocked.Orchestrator.run(passes=...)runs the passes that--passesasks for.StateStore.mark_url_failed(count_run=False)records a failure without adding tofailed_runs.
Fixed¶
- A run without
observations/.watch.dbrebuilds the price-watch baseline from the stored observations before it starts. A runner that starts from a clean checkout records revisions instead of reading every listing as first seen. - A run writes a
failed.last_run.jsonwith theinterruptedstop when it starts, and replaces it when it ends. A run killed by a CI timeout, and a config a batch never starts, leave a failed record instead of the previous run's. So does a loaded config whose setup raises, with theerrorstop. - A run whose listing pages all come back without an item or an item link is
failed, andstopped_reasonsays so. A changed listing layout used to end as a successful run with zero items. A run whose listing pages link only to stored items stays a success. - A run in which every URL failed is
failedand exits with status 1, also when it made fewer thanmin_requests_for_error_raterequests. A run that stored new items before a throttle stop ispartialand exits with 0. So is a run that a deadline or a signal cut short after a URL succeeded. - An exception inside a run ends it with the
errorstop..last_run.jsonandon_completestill record it. An exception while one URL's failure is recorded, such asdatabase is locked, leaves that URLin_progressfor the next run, and the other URLs of its batch finish. Every URL of a batch finishes before an exception leaves it, so no fetch runs on after the store closes. - The store closes before the run writes
.last_run.jsonand runson_complete. A store that cannot write its items at close, such as a read-onlyitems/, ends the run with theerrorstop and keeps its stats, so the record, the summary line, anddatabrew reportagree. It used to raise while the record saidsuccess. - A middleware hook that returns something other than a
MiddlewareContextfails the URL withMiddleware <Class>.<hook> returned <type>. It used to fail with anAttributeErrorthat named no middleware. - A hook command that cannot be formatted, such as one with
{}, fails the hook as a nonzero exit does, and the log names the command. It used to end the run with theerrorstop. A literal brace is written twice, as{{}}. databrew statuson a.state.dbthat is not a database, or that a newer databrew wrote, prints one line on stderr and exits with 1, asrunrefuses the store. It used to print a traceback.- A completed item URL that also appears as a start URL or pagination link stays completed. Requeueing pagination resets pagination rows only.
- SIGINT and SIGTERM stop the run cleanly, also while it opens its state or
closes its fetcher and store. Fetches in flight are cancelled and return to
the queue, a running
on_startoron_failurehook command is killed,.last_run.jsonrecords"Interrupted by SIGINT"or"Interrupted by SIGTERM", items are flushed, and the process prints no traceback. The exit status follows the run's outcome, so a signal after a URL succeeded exits with 0. - A blocked crawl records nothing against the URLs it could not fetch. A
URL that fails for good before the run's first success is maybe blocked,
uncharged, and the next run fetches it again. A retry pass and a
--retry-failedrun leave a maybe-blocked URL alone. A URL that fails that way within a--retry-failedrun stays failed, uncharged, so the next--retry-failedtries it again. A 404, a 410, and a page that extraction cannot use are the site's answer about one URL, so any run charges them (#122). - A terminal 404 or 410 counts as a failed URL and stays out of
max_consecutive_failures,max_error_rate, and the adaptive-concurrency window, since it says nothing about a block. A 404 counts towardmax_failed_runs. HTTP defines a 410 as likely permanent, so a 410 shelves its URL in the run that sees it, whatevermax_failed_runsis. A block of dead seed pages, such as a category the site removed, used to end every run before the pages behind it.CrawlStats.record_failuretakesnot_found, andCrawlStats.urls_not_foundcounts these failures. - A rate-limited response's
Retry-Afterpauses every fetch until it expires. - A run retries the failed item URLs of earlier runs after its other pending URLs, and a listing page that failed or was maybe blocked after its other listing pages, the start URLs included. A group of item URLs that keeps failing used to be fetched first, and could end every run before it reached a listing page.
- A listing page that fails returns to the queue on the next run, as a failed
item URL does, and is shelved after
max_failed_runsruns in a row that fail it. A plain run used to retry failed item URLs only, so a listing page that failed after the run's first success was not fetched again unless it was a start URL, and the pages behind it were never crawled. A start URL is fetched in every run whatever its state, since seeding returns it to the queue at the start URLs' priority with itsfailed_runscount started over. databrew statusandrun --dry-runread.state.dbwithout writing to the storage directory, so they leave_failed_urls.jsonuntouched andstatusworks while a crawl holds the writer lock. They list item files instead of the stored item count, which itemstore reports only to its writer.- Caught-up detection works with
[extract.items].keyin HTML configs..state.dbindexes the source URL of each keyed item, so a plain item link counts as scraped. JSON item links carry their key and skip the index. The index fills itself from stored items when it is empty, so stores written by earlier versions need no migration. - An item URL whose page yields no item fails with an
extractionerror that names the extractor's reason, such as a missing required field. A layout change shows up as failed URLs, and a later run fetches them again. --passes autoends when a retry pass recovers no URL, so a pagination URL that always fails cannot keep it looping (#123).- The passes of one invocation form one run. Hooks fire once,
--limitcounts requests across all passes, a URL'sfailed_runsgrows by at most one, and a URL that fails in several passes is one failed URL. A short outage costs a URL one run of itsmax_failed_runsbudget.max_consecutive_failuresandmax_error_ratecount each retry pass from zero, and when one of them ends a retry pass the run keeps the stop of the pass before it.
Changed¶
- Every store databrew opens, including the observation and revision stores,
raises
IncompatibleStoreError, aRuntimeError, when its part files predate itemstore 0.24. The message says how to migrate them. databrew.runcloses a fetcher passed to it, also when setup fails. Anon_failurerecovery keeps that fetcher instead of rebuilding one from[fetch].--start-urlsskips#comment lines, asstart_urls = { file = ... }does, and a missing file or a line that is not an HTTP URL is a usage error, with or without--dry-run. So is--start-urlswith--retry-failed, which seeds no start URL.- databrew requires pydantic 2.12, the first release with Python 3.14 wheels.
HttpUrlis a class from 2.10 on, whichRunOptions.extra_start_urlsrelies on. .last_run.jsongainsstopandoutcome.stopped_reasonkeeps its text..last_run.jsongainsconsecutive_empty_runs: 0 when the run stored a new item, the previous record's value when the run fetched nothing, and otherwise one more. The run summary shows a streak above 0.- Pagination URLs added together are served in list order, also when they were queued before, so a reordered seed file takes effect (#124).
0.45.0 - 2026-07-28¶
Added¶
backend = "zendriver"selects fetchkit's second browser backend. It is registered inBROWSER_BACKENDS, so it gets the samemax_tabsdefault from[policy].concurrencyand the samepage_load_timeoutdry-run preview aspydoll. Install it with the newzendriverextra.
Changed¶
- Upgraded Fetchkit to 0.18.0 for the Zendriver backend, lazy tab pooling, and cancellation-safe browser startup and shutdown.
- CLI startup no longer pays for browser extras it does not use. Fetchkit 0.18.0
imports optional backends on first access, so
import fetchkitdrops from ~293 ms to ~57 ms with all three extras installed, anddatabrew --helpfrom ~440 ms to ~190 ms. - Boundary tests now cover every name in
BROWSER_BACKENDSrather thanpydollalone, and assert each one is a fetchkit backend that accepts themax_tabsandpage_load_timeoutoptions databrew supplies.
0.44.0 - 2026-07-28¶
Added¶
create_components(config, fetcher=...)accepts a caller-supplied fetcher and skips construction from[fetch], so a backend outside fetchkit's registry keeps databrew's resume, fresh, and retry-failed seeding without blankingfetch_configor reassigningCrawlComponents.fetcher(#118). The supplied object is checked forfetchandclosebefore any state is opened, so a class or factory fails there rather than after the queue is seeded. A fetcher databrew builds is still closed if component creation fails; a supplied one is left open for the caller.build_hook_callbacks(..., fetcher_factory=...)builds the replacement fetcher foron_recover. Without it, recovery rebuilds from[fetch], which swaps out a caller-supplied backend — or fails outright when fetchkit cannot build the configured name.
Changed¶
- The browser tab-pool default (
max_tabsfrompolicy.concurrency) and the dry-run page-load timeout lookup now key on theBROWSER_BACKENDSset indatabrew.config.fetchinstead of the literal backend namepydoll, so another fetchkit browser backend needs one name added in one place rather than silently losing the default.
0.43.0 - 2026-07-26¶
Changed¶
- Upgraded Fetchkit to 0.16.0 for browser lifecycle hardening and response-body access on failed HTTP responses.
- Upgraded ItemStore to 0.26.0 for bounded catalog rebuilds and stricter enforcement of its frozen storage format.
0.42.0 - 2026-07-20¶
Added¶
databrew reactivate <config>: first-class force-retry forfailedandpermanently_failedURLs, with--url,--error,--status,--max-age,--reset-failed-runs, and--dry-runfilters, plus a programmaticStateStore.reactivate(...)API (#76). Reactivation updates.state.dband.failures.dbtogether and re-exports_failed_urls.json; the snapshot merge now orders records by newest event (last_failed_atvs. a newreactivated_atstamp), so a committed reactivation survives merging against a stale runner DB with a higherfailed_runscount, while a failure recorded after the reactivation still wins back. Reactivated records reach the queue even when an item already exists in storage, and--dry-runpreviews against scratch copies so it writes no durable state.- Degraded-extraction visibility:
CrawlStatsgainsdegraded_responsesandextraction_diagnostics, surfaced insummary(),.last_run.json(including the multi-pass aggregate), a completion summary line when nonzero, and per-diagnostic DEBUG context under-v(#93). Middleware can transform successful extraction results through the newreplace_extract_resulthelper, which preserves diagnostics and any futureExtractResultfields (#94).
Changed¶
- Failure-store timestamps are now recorded as aware UTC so cross-machine event ordering is immune to timezone skew; existing local-time stamps are converted (assuming the machine's own timezone) on the next open.
max_requestsis documented as completed fetch attempts — retries and middleware-recovery attempts count toward the budget (#104).
Fixed¶
- Failure-classified policy stops (consecutive failures, error rate) now end
a multi-pass invocation immediately with a nonzero exit status instead of
starting another pass;
.last_run.jsonaggregates statistics across passes and recordspass_count. A deliberateon_startabort remains a successful stop (#105). - Errors reclassified as challenges during general error recovery are redispatched through the challenge chain once.
- Re-syncing failure state no longer re-stamps
last_failed_atto the sync time; records keep the actual failure time, which--max-agefiltering relies on.
0.41.0 - 2026-07-16¶
Fixed¶
- Delayed retries now remain live work in the active crawl instead of making the queue appear empty until a later run. Middleware-cleared errors use the same bounded retry budget, and challenge recovery runs before general error recovery.
- Incremental pagination now distinguishes item links that are already stored
from links that merely already exist in the queue. Pending links from a
resume or overlapping page no longer trigger a false
caught_upresult and truncate deeper pagination. --passes autocompares the complete queue-state snapshot instead of a collision-prone scalar progress sum. Promoting one URL topermanently_failedcan no longer hide progress on another failed URL and stop before its retry budget is exhausted.- Item URLs are marked durably as soon as they reach
max_failed_runs, remain permanently failed if.state.dbis lost, and are not resurrected by normal seeding. Failed pagination seeds are reactivated on later runs. - Terminal error-rate accounting excludes retryable attempts from both the numerator and denominator. Successful recovery starts a new policy epoch without erasing lifetime failure statistics.
- Crawl setup and teardown now close partially initialized resources even
when observation setup or fetcher shutdown fails. Fetcher shutdown is
bounded by
fetcher_close_timeouton every lifecycle path. - Malformed ports in extracted URLs no longer abort processing of the page's otherwise valid items and links.
Changed¶
- Exceptions from data-path middleware hooks (
pre_fetch,post_fetch, andpost_extract) now fail the URL as non-retryable middleware errors instead of continuing with partially processed data. Recovery-hook exceptions are logged while the remaining recovery chain continues. - Configuration loading now rejects unknown top-level and component fields,
invalid section shapes,
extendscycles, unsafe state filenames, and colliding or escaping observation paths before crawl state is opened. Config-relative files continue to resolve from the user-facing config path, including when it is a symlink. - CLI limits must be positive integers, policy overrides are revalidated, and
setup failures return a normal nonzero result. Shared config fragments whose
names start with
_are omitted fromdatabrew list. - Generated HTML and JSON starter configs are runnable list-page examples and safely encode TOML strings, including Unicode and control characters.
- Supported component lines are capped at
extractkit 0.21.x,fetchkit 0.13.x, anditemstore 0.24.x; Pydantic is capped below 3.0.
Migration¶
- Existing configs with misspelled, unknown, or previously ignored fields now fail validation and must be corrected. Parser modules and hook commands are trusted executable input and must come from trusted sources.
- Middleware that intentionally swallowed data-path exceptions must now return
an explicit valid context or move recovery behavior into
on_errororon_challenge. - The
max_error_ratethreshold now measures terminal URL outcomes only, so operators should review thresholds calibrated against retry attempts. - Shared example config
base.tomlis now_base.toml; update copied example configs that extend the old filename.
0.40.0 - 2026-07-16¶
Changed¶
- Upgraded itemstore from 0.23.2 to 0.24.0 and replaced the removed
StorageEngine/upstreamStorageConfigintegration with databrew-owned storage configuration plus the newItemStorefacade. URL hashing is now owned by databrew, preserving existing SHA-256 identities without importing an itemstore implementation helper. - Storage defaults now follow itemstore's periodic policy: flush at 500 pending
operations or the target-size threshold. The catalog is fixed at
.itemstore.db, item parts remain underitems/, andbackendis the storage backend TOML key. - Removed catalog layout and ephemeral-index settings. Configs using
ephemeral_index_db,index_db_ephemeral,index_db_name, oritems_dir_namenow fail with a migration error instead of being ignored.
Migration¶
- Itemstore 0.24 uses
{op, key, recorded_at, data}operation envelopes and does not read 0.23{v, key, stored_at, data}part files. Existing stores must be exported with itemstore 0.23 before switching dependencies, then reingested into a new 0.24 storage path. Starting a fresh path requires no migration; the old.index.dbis no longer used.
0.36.1 - 2026-04-28¶
Changed¶
cli.pysplit across three sibling modules; closes #53. The argparse + dispatch surface stays incli.py(905 → 171 LoC over the three slices); the command implementations move to dedicated modules where they can be tested and evolved independently.databrew.templatesnow owns thedatabrew initTOML templates:.toml.tmplpackage data loaded viaimportlib.resourcesandstring.Template. Newrender_init_config(kind, *, name, url)exposes the renderer;kindis typedLiteral["html", "json"]. Output is byte-identical to the prior version (#84).databrew.runnerowns crawl execution:setup_logging,_resolve_passes,_queue_counts,_run_single_crawl,_run_multi_pass, andrun_crawl. Tests for the multi-pass loop and arg validation also moved (tests/test_cli.py→tests/test_runner.py) (#85).databrew.displayowns read-only CLI commands:show_status,check_config,list_configs,show_dry_run_info. Both status and dry-run now use theStateStorecontext-manager API (#86).apply_cli_overrideslives indatabrew.config(alongsideload_config/create_store) and takes keyword-onlylimit/concurrency. The previousargparse.Namespacesignature kept the CLI layer entangled with the config module; the new shape lets non-CLI callers (tests, library use) apply the same overrides without constructing a Namespace.
0.36.0 - 2026-04-27¶
Fixed¶
adaptive_concurrencywas effectively a no-op under sustained 429 bursts. The AIMD signal counted only terminal failures, lagging the actual API health by the entire retry budget (~5-9 min per URL withmax_retries=5andRetry-After=60-120s), so AIMD never fired beforemax_consecutive_failurestripped and stopped the crawl. Outcome tracking is now split into two parallel windows:_outcome_windowstays terminal-only and continues to driveerror_rate/max_error_rate(transient bursts must not kill long-running crawls); new_aimd_outcome_windowrecords terminal + retryable failures + successes and drives the newaimd_error_rateproperty the AIMD branch reads. Operators who calibratedaimd_error_thresholdagainst the broken signal may see more aggressive halving and need to recalibrate (#74).
Added¶
- Retry decisions now surface at INFO so operators can observe retry
timing without dropping global level to DEBUG. The first retry per
URL logs at INFO with an explicit attempt counter
(
Will retry <url> in <delay>s (retry N/max_retries)); subsequent retries stay at DEBUG to avoid flooding long backoff chains. Terminal failures emit a new WARNING line (Permanently failed: <url> - <message> (after N attempt[s])) so the should_retry → False transition is explicit (#75).
0.35.0 - 2026-04-27¶
Fixed¶
- Server-instructed
Retry-Afterdelays were silently truncated bymax_retry_delay(default 60s), guaranteeing the next retry would fire before the server's cooldown elapsed and waste amax_retriesslot on a request the server already said would fail. The cap now splits into two fields with independent trust models:max_retry_delay(default 60s) caps locally-computed exponential backoff; newmax_retry_after(default 600s) caps server-instructedRetry-Aftervalues. Configs that previously usedmax_retry_delayto also throttleRetry-Afterwill see longer effective waits — setmax_retry_after = max_retry_delayto restore the prior behavior (#73).
0.34.0 - 2026-04-27¶
Added¶
- After every crawl, write a machine-readable
<storage_path>/.last_run.jsoncontaining the run's stats record (name,urls_processed,urls_succeeded,urls_failed,items_extracted,error_rate,elapsed,urls_per_sec,stopped_reason). Replaces fragile parsing of the human-formattedStats: {...}log line. Written atomically on both normal exits andon_startaborts; available to CLI, Python API, and CI consumers viajq .items_extracted data/<name>/.last_run.json(#45). CrawlStats.as_record()returns a numeric-only snapshot suitable for JSON serialization.summary()continues to return human-formatted strings for log output.Orchestrator(name=...)parameter labels the run in.last_run.json. Defaults to the storage directory's basename when not provided; the CLI now passesconfig.name.
0.33.0 - 2026-04-25¶
Removed¶
databrew.core.StrictModelis no longer re-exported fromdatabrew.core. The class is owned by extractkit; importing it viadatabrew.corewas a fragile public-API surface (an extractkit rename would silently break consumers). Import directly from extractkit instead:from extractkit import StrictModel(#58).
Changed¶
- Split
databrew/core/config.pyintocore/types.py(theUnitIntervalandItemsFromprimitives) andcore/hooks.py(HooksConfig). Public re-exports fromdatabrew.coreare unchanged —HooksConfig,ItemsFrom, andUnitIntervalstill import the same way. Internal callers reaching into the old module path (from databrew.core.config import ...) must update to the new paths (#58). load_confignow routes throughWebsiteConfig.model_validateinstead of constructing the model from pre-cooked typed kwargs. Anyfield_validatorormodel_validatordeclared onWebsiteConfig(or a subclass) now fires reliably during loading, includingmode='before'validators that need the full normalized payload. No observable change for the standard config path (#57).
0.32.0 - 2026-04-24¶
Fixed¶
RetryOnPatternMiddlewareno longer accumulates per-URL retry counts without bound. State is stored in an LRU capped by the newmax_trackedkwarg (default 10 000); evicted URLs start fresh on their next match. Previously the internal dict grew for every URL seen and persisted acrossOrchestrator.run()invocations — a URL that exhausted retries in one run was still atmax_retriesin the next one (#50).RetryOnPatternMiddleware's "retries exhausted" branch now emits a non-retryable error so the orchestrator stops cycling the URL throughpolicy.max_retriesafter the middleware has given up. Previously the exhausted error inheritedretryable=TruefromFetchError.challenge(...), so the orchestrator could keep re-running the middleware, which kept firing the same exhausted branch.
Added¶
Middleware.reset()lifecycle hook (default no-op) andMiddlewareChain.run_reset(). The orchestrator callsrun_reset()at the start of everyrun()so per-run state (retry counters, caches) can be cleared without recreating the chain. The base-class docstring now documents that middleware instances persist across runs.- New public
databrew.CrawlErrordataclass: canonical error shape carried onMiddlewareContext.error. ProvidesCrawlError.from_anyto normalize afetchkit.FetchError, anextractkit.CrawlError, or any exception-like object (#55).
Changed¶
- Refactored
Orchestrator._process_url(was 140 lines) by extracting_save_items,_collect_new_links,_build_pagination_flags, and_should_follow_paginationhelpers plus a newPaginationFlagsdataclass. The pagination-follow decision is now expressed as a documented priority cascade (retry_failed_only > caught_up > stop_on_no_new_items > stop_on_empty) instead of an anonymousif/elifchain. No behaviour change; covered by new unit tests intest_orchestrator_pagination.py(#54). - Breaking (middleware API):
MiddlewareContext.erroris now typedCrawlError | None(wasobject | None). Custom middleware that writes toctx.errormust set aCrawlError; previous code that stashed a rawFetchErrorshould switch toCrawlError.from_any(err)(or construct aCrawlErrordirectly). Built-inRetryOnPatternMiddlewarenow emits aCrawlError. The orchestrator no longer probes error objects withgetattr: it normalizes fetch/extract errors at the boundary and passes a concreteCrawlErrorthrough_handle_failure(#55).
0.31.2 - 2026-04-24¶
Fixed¶
on_recoverfetcher swap is no longer racy. The old path closed the fetcher before reassigningorchestrator.fetcher, so an in-flight_process_urlholding the old reference could hit a closed object. NewOrchestrator.replace_fetcher(new)installs the new fetcher before closing the old one via a shared_close_fetcherhelper — in-flight tasks finish against a live fetcher and new fetches see the replacement immediately.hooks.pyno longer reaches into the orchestrator's private module state; itscomponents.fetcherassignment is guarded byfinallyso a cancel during the swap can't desync it fromorch.fetcher.
Changed¶
- Breaking (minor surface):
CrawlPolicy.should_stopnow requireserror_rate: float. Thetotal_failuresargument and theNonefallback that recomputed the rate internally are both gone —CrawlStats.error_rateis the single owner of the computation (windowed whenerror_rate_windowis set, cumulative otherwise). Callers passingstats.error_rate(as the orchestrator already does) need no change; anyone callingshould_stopdirectly must droptotal_failuresand pass a concreteerror_rate. _FETCHER_CLOSE_TIMEOUT(previously a private module constant inorchestrator) is nowCrawlPolicy.fetcher_close_timeout(PositiveFloat, default 5.0 s). Users with slow-closing fetchers can tune it in TOML.
Added¶
- Schema versioning for both state-layer SQLite DBs.
StateStoreandFailureStorenow stampPRAGMA user_versionon a fresh DB and validate it on subsequent opens. A DB newer than the running code raisesdatabrew.state.SchemaVersionErrorinstead of silently corrupting state. Pre-versioning DBs (user_version = 0) upgrade in place on next open, so existing deployments require no manual step.STATE_SCHEMA_VERSION = 1/FAILURE_SCHEMA_VERSION = 1.
0.31.1 - 2026-04-24¶
Fixed¶
StateStore.get_next_urlis now atomic. The old SELECT followed by a separate UPDATE could let two writers sharing a.state.dbclaim the same URL; it now runs as a singleUPDATE … WHERE url_hash = (SELECT … LIMIT 1) RETURNING …. The dequeue index is retuned to(status, priority DESC, created_at ASC, scheduled_at)to match the query'sORDER BY. Oldidx_urls_statusis dropped on first open.reset_failed_itemsnow clearsscheduled_at = NULL, matching the behavior ofreset_failed_for_retry_only. A shared_permanently_fail_exhausted_itemshelper de-duplicates the two reset paths so they can't drift again.
Internal¶
- SQLite PRAGMA setup for
StateStoreandFailureStorenow routes through a sharedstate/_connection.configure_connectionhelper. Both stores pick upPRAGMA busy_timeout=5000so multi-process writers wait on the write lock instead of raisingOperationalError. Drift-detection test asserts identical PRAGMA values across the two stores.
0.31.0 - 2026-04-24¶
Removed¶
- Breaking:
databrew migrate-url-keyssubcommand and thedatabrew.migrationspackage. The one-shot rekey tooling introduced in v0.28 (and the olderscripts/migrate_to_0_22_1.py) are gone — all known caller stores are already on canonical URL-hash keys. Users still on an un-migrated store should pindatabrew<0.31to run the migration, then upgrade. docs/guide/migration.mdremoved from the docs nav alongside the subcommand.
0.30.1 - 2026-04-24¶
Fixed¶
normalize_urlnow produces the same hash for URLs that differ only in repeated-query-param order (?a=1&a=2vs?a=2&a=1) or in percent- encoding case (%2fvs%2F). Values within each query key are sorted, and%xxsequences in the path are canonicalized to uppercase per RFC 3986. Migration: existing URL-keyed item stores whose URLs contain lowercase%xxor repeated query params will see those rows rehash. Rundatabrew migrate-url-keys <config> --forceto rekey under the new normalization — otherwise affected items miss their dedup lookup on the next crawl and produce one duplicate row per URL.--limit 0and--concurrency 0are no longer silently ignored. The CLI override path used truthy checks; it now usesis not None, so an explicit zero is honored. The shared_apply_cli_overrideshelper replaces the duplicated override block in_run_single_crawlandshow_dry_run_info.- Crawl-failure log in
_run_single_crawluseslogger.exceptionso the traceback is preserved instead of collapsing to a single line.
Internal¶
_already_migratedloop control clarified (break after sample counter bump; no behavior change).- Renamed
error→error_infoinOrchestrator._handle_failureto avoid the rebind-shadow read confusion.
0.30.0 - 2026-04-24¶
Changed¶
- Behavior change:
consecutive_failurescounts only terminal failures. Retryable failures (e.g. 429, transient network errors) no longer advanceconsecutive_failures, so a concurrent burst at startup can't tripmax_consecutive_failuresbefore retries get a chance (#47). This also narrows the{failures}shell-hook template variable to terminal failures only; operators relying on the old number will see smaller values.
Fixed¶
--retry-failedruns now auto-restore deferred URLs back to the pending queue at the end of the run (including onKeyboardInterruptor stop conditions), fixing a silent-park bug where active URLs would stay indeferredstate after a retry pass (#47).
Added¶
- Bump
fetchkit>=0.10.0and plumb the server-providedRetry-Aftervalue (now available viaFetchError.retry_after) through the orchestrator's retry scheduling.CrawlPolicy.get_retry_delayaccepts aretry_afterargument that preempts exponential backoff when set (#47). CrawlPolicy.warmup_seconds: linearly ramp effective concurrency from 1 toconcurrencyat startup. Logs a single INFO line each time the effective cap changes. Defaults to 0 (no ramp) for backwards compatibility (#47).CrawlPolicy.error_rate_window: opt-in sliding-window error rate. When set,stats.error_rateandpolicy.should_stopevaluate the most-recent N terminal outcomes instead of lifetime cumulative. Retryable failures do not count toward the window — transient bursts heal out of the signal automatically (#47).CrawlPolicy.adaptive_concurrency(AIMD): halve effective concurrency when the windowed error rate exceedsaimd_error_threshold; grow it byaimd_grow_stepper batch when the rate is healthy AND the current cap is ≥ 90% saturated (utilization gate prevents grow-on-idle). Requireserror_rate_windowto be set (#47).Middleware.on_challengehook andMiddlewareChain.run_on_challenge: new lifecycle point for responding to CDN challenges (Cloudflare, bot checks) before the URL is marked failed.RetryOnPatternMiddlewarenow emitsFetchError.challenge(...)so CAPTCHA-detection flows can opt in via the new hook."challenge"is added to the defaultretryable_categories._handle_failureis now async (#47).Orchestrator.pause()/Orchestrator.resume(): freeze new task scheduling without aborting the run. In-flight tasks finish normally. Backed by anasyncio.Eventgate at the top of the run loop (#47).CookieRefreshMiddleware: built-in middleware that injects aCookie:header from an in-memory dict and, on challenge, calls a user-supplied async callback to fetch fresh cookies. Concurrent challenges are serialized by an internal lock + cooldown window. Sample end-to-end Cloudflare-clearance flow inexamples/middleware/refresh_cookies_middleware.py(#47).databrew run --passes <N|auto>: drive the full primary → resume → retry-failed sequence from one CLI invocation.autoterminates when both pending and failed queues are empty, or when two consecutive passes make no forward progress. Incompatible with--retry-failed(the loop handles that per-pass) (#47).
0.29.0 - 2026-04-13¶
Breaking: upgrade to extractkit v0.12.0 with renamed config fields. This release separates item identity (dedup key) from URL template variable (#44).
Config field renames (no backward-compat aliases):
- [extract.items].id → [extract.items].key
- [extract.links].items_id → [extract.links].items_url_field
- WebsiteConfig.id_field removed → use WebsiteConfig.key_field
New: natural-key mode. When [extract.items].key is set (e.g. key = "sku"),
items are stored and deduped by that field's value instead of URL hash. This
fixes the long-standing issue where has_item() dedup was a no-op in
URL-hash-keyed stores. Sites that don't set key continue using URL-hash mode.
StateStoreacceptskey_fieldparameter; branches storage init andsave_item()on natural-key vs URL-hash modemigrate-url-keysskips stores that use natural keys (not applicable)- extractkit
ItemLink.item_id→ItemLink.item_keyreflected in orchestrator dedup loop - All example configs, CLI template, and docs updated
0.28.1 - 2026-04-12¶
- Fix
migrate-url-keyscatch-22 on fresh checkout: migration no longer requires a pre-existing.index.db— it builds one from parquet files automatically (#42) - Fix false-positive "already migrated" detection: the check now validates
actual key data instead of trusting
store_config.key_fieldmetadata - Add
--forceflag tomigrate-url-keysto bypass the already-migrated check - Disable auto-compaction on the staging engine during migration to prevent
ConnectionExceptionon large stores (>70K rows)
0.28.0 - 2026-04-12¶
Breaking: id_field no longer affects storage keying. Items are now keyed
by hash(normalize_url(source_url)) via a single path. The in-memory
_url_to_key dict that silently broke incremental crawls after restart is
gone (#41).
Existing stores created with id_field must be migrated before their
first run under v0.28:
databrew migrate-url-keys path/to/site.toml
The migration is idempotent, keeps a .bak copy alongside the swapped store,
and recovers the source URL from each row's _source_url field (or url
fallback, stamped by extractkit). Configs that still declare
[extract.items].id = "..." keep loading; the field is ignored with a
deprecation warning pointing at databrew migrate-url-keys.
- Fix incremental crawls walking the entire pagination archive after restart
on sites with
id_fieldset (#41). Affected HTML-scraped sites (realethio,ethiopiapropertycentre,ethiopianproperties) now stop at the first caught-up page as intended. StateStore.save_itemstamps_source_urlon every row as a durable storage-layer invariant (separate from extractkit'surlconvenience). This decouples storage dedup from extractor conventions and makes future rekey/repair tools straightforward.Orchestratornow reads the per-item source URL fromitem.get("url")before falling back to the task URL, so JSONitems_urltemplates and HTML list configs withfields.url = ...selectors continue to get per-item dedup without relying onid_field.- New subcommand:
databrew migrate-url-keys <config>..., with--dry-run,--drop-missing-url, and--no-keep-backupflags.
0.27.1 - 2026-04-08¶
- Fix
has_item_for_url()returning true for completed URLs with no stored item (#40) - Fix
RetryOnPatternMiddlewaresilently accepting blocked pages after max retries (#40) - Fix
databrew listsilently hiding configs that fail to load (#40) - Bump itemstore to >=0.23.1 for
read_items()buffered item fix (#39)
0.25.0 - 2026-03-30¶
Breaking: requires Python >=3.14 (was >=3.11).
- Upgrade itemstore to v0.18.0 with envelope Parquet schema (#33, #35)
- Fix broken import path for
hash_keyafter itemstore refactor - Add
StateStore.read_items()for reading back stored payloads - Remove 10 tests that duplicated itemstore's own coverage
- Update docs for envelope schema,
export_store(),compact_storage()params
Migration: existing flat Parquet stores need conversion. See the migration guide.
0.24.1 - 2026-03-23¶
- Timeout
fetcher.close()to prevent store flush hang (#32) - Upgrade itemstore to v0.9.0
0.24.0 - 2026-03-23¶
- Batch URL seeding, relocate
items_from, addstop_on_no_new_items(#31)
0.23.1 - 2026-03-22¶
- Fix misleading log, store flush, and custom fetcher types (#27)
0.23.0 - 2026-03-14¶
- Fix generated config templates:
compression->backend_options - Overhaul documentation for accuracy, tone, and real examples
- Upgrade to extractkit 0.7.1
0.22.2 - 2026-03-14¶
- Fall back to URL-based key when
id_fielddoesn't resolve
0.22.1 - 2026-03-14¶
- Fix caught-up detection and
--freshto preserve incremental crawl state
0.22.0 - 2026-03-13¶
- Inject
_scraped_attimestamp into every saved item