Changelog¶
All notable changes to sci-etl-core are recorded here. The format follows Keep a Changelog, and versions follow Semantic Versioning. Until 1.0, a minor release may change behavior; each such change is listed under Changed.
[0.6.0] - 2026-09-27¶
This release changes the data contract: entity extractors can return typed
entities, exporters receive each record with its entities, the library logs
through the standard logging module, and a base install needs only
Pydantic. It adds claims with evidence and provenance. See "Upgrading to 0.6"
in MIGRATION.md.
Added¶
- Exporter lifecycle.
AsyncExporterhasopen(),write(record, entities),flush(), andaclose(), anddurable_writessays whetherwriteorflushmakes entities durable. The pipeline marks a record processed only once its entities are durable, so a crash can repeat a record but never lose one. AsyncCsvExporter, which writes one row per entity with therecord_idof its paper, keeps every other key in anextracolumn, and never merges, clips, or coerces a value. It renders the file once per run from an append-only journal, so export time grows linearly with the number of records. Cells a spreadsheet would run as a formula get a leading apostrophe; plain numbers such as-5.361are written unchanged.AsyncJsonlExporter, which appends one JSON line per record, andread_jsonl_export, which reads the last line of each record back.- Typed entities.
AsyncLLMEntityExtractor(schema=Model)validates every entity against a Pydantic model and returns model instances. It requests JSON-schema structured output through the newAsyncLLMClient.complete_structured, whichAsyncOpenAICompatibleClient(structured_output=True)andLLMConfig.structured_outputturn on for endpoints that support it.entity_list_schemabuilds the requested schema. - Rejection reasons.
RecordValidator.validatereturns aValidationResultofViolations naming the field and rule of each rejection. The bundled validators report their rules, andCompositeValidator.validatecollects the violations of every validator. Rejections are logged with their reasons. AsyncEntityExtractor.extract_record(record, text)andrequires_record, for extractors that need the record. The pipeline callsextract_record.AsyncLLMEntityExtractor.preparereturns the text the model is sent, andstampdescribes the model, prompt, schema, and library release behind its entities.rejections=keeps rejected entities in a rejection store.response_cache_key(schema=, variant=)andCachingLLMClient(variant=). The schema of a typed request joins the cache key.sci_etl_core.claims(provisional):Claim,ClaimDraft,EvidenceSpan,ExtractionStamp,locate_quote,AsyncLLMClaimExtractor, theInMemoryClaimStoreandAsyncSqliteClaimStoreclaim stores,AsyncClaimStoreExporter, and theInMemoryRejectionStoreandAsyncSqliteRejectionStorerejection stores.DeduplicationStep(source_column=)adds asourcescolumn listing the distinct sources, such as therecord_idof each paper, of every row merged into each output row.ExportError,ClaimError, andClaimStoreError.- The
config,arxiv,xml,html, andprocessorsextras. load_config(load_env=True)andload_config_async(load_env=True).
Changed¶
- Breaking: exporters take their destination when they are constructed,
and implement
write(record, entities)instead ofexport(data, destination). The pipeline no longer takesdestination. - Breaking: the pipeline writes every processed record to the exporter, including one with no entities, so an exporter can clear rows a re-extraction no longer finds.
- Breaking: the pipeline opens the exporter before the first listing
request, flushes it after every page, and flushes and closes it before
flushing state however the run ends. An
openfault aborts the run; aflushfault leaves the page's records unsettled without counting an attempt against them. - Breaking: the base install requires only
pydantic. Install theconfigextra forload_config,arxivforAsyncArxivExtractor,xmlfor the PubMed extractor and the JATS and DOCX parsers,htmlforHtmlTextParser, andprocessorsfor the pandas processors and table sinks. - Breaking:
load_configandload_config_asyncread a.envfile only when givenenv_pathorload_env=True. - Breaking: every module logs through
logging.getLogger(__name__)under thesci_etl_corelogger, atWARNINGfor failed and skipped records,ERRORfor sink and state faults, andINFOfor routine notes. The library configures no handlers. - Breaking:
AsyncEntityExtractorandAsyncExporterare generic in the entity type, and everyAsyncLLMEntityExtractorargument aftersystem_promptis keyword-only. AsyncLLMClient.invalidatetakesschema=for a typed request.- The
asyncextra no longer installsaiofiles, and thesqlextra no longer installsaiosqlite. AsyncArxivExtractorretries a406response instead of failing the request, because arXiv returns it intermittently for valid requests.
Removed¶
- The blocking
Extractor,StateManager,Exporter,LLMClient,RelevanceFilter, andEntityExtractorinterfaces and theSync*Adapterclasses. LegacyExtractorAdapter.AsyncExporter.exportand the pipeline'sdestinationargument.AsyncCsvUpsertExporter; useAsyncCsvExporter.AsyncSqlTableExporterandAsyncPlotly3DExporter; useSqlTableSinkandPlotly3DSink.ScatterPlotConfigis imported fromsci_etl_core.processors.- Every
logger=argument,configure_logging, andAsyncETLPipeline.log.
0.5.1 - 2026-09-26¶
This release stops LLM responses that carry no answer from settling a record, keeps the LLM cache from replaying them, and adds download and decompression limits. The first run after upgrading misses the LLM cache once. See "Upgrading to 0.5.1" in MIGRATION.md.
Added¶
AsyncLLMClient.invalidate(system_prompt, user_content)reports a rejected response, andAsyncLLMResponseCache.delete(key)removes one entry; both bundled caches implement it.- Size limits.
max_download_bytesonAsyncArxivExtractor,AsyncOpenAlexExtractor,AsyncPubMedExtractor, andAsyncSemanticScholarExtractorcaps each response body after decoding, andLatexTarballParser(max_tex_bytes=)caps the TeX decompressed from one e-print. An oversized listing page raisesExtractionError; an oversized full-text download or e-print is logged and passed over. Both default to no limit. AsyncArxivExtractor.from_config(full_text=)builds the extractor's rate limiter from thefull_textconfig section.AsyncOpenAICompatibleClientandCachingLLMClientexposebase_urlandtemperature, and everyAsyncLLMClientexposesresponse_format, which defaults to{"type": "json_object"}.response_cache_keyaccepts all three as keywords.
Changed¶
- An empty LLM completion now fails the record instead of settling it.
AsyncOpenAICompatibleClient.complete_jsonraisesLLMErrorwhere it returned{}, so the pipeline retries the record on the next run instead of marking it processed with nothing exported. AsyncLLMEntityExtractor.extractraisesLLMErrorwhen the response holds no entity list: it is empty, or it has several keys and none isresult_key. It returned[], and the record was marked processed.AsyncLLMRelevanceFilterandAsyncEmbeddingRelevanceFilterwithdefault_on_error=Falsefail closed: a failed call or an unclear verdict raises, and the record is retried on the next run. It was read as irrelevant, and the record was marked processed for good.- The LLM cache key now includes the endpoint's
base_url, the temperature, and the response format, so an answer cached for one provider, temperature, or format is no longer served for another. Responses cached by earlier releases are not found, and the first run after upgrading calls the LLM for every request. - A third-party
AsyncLLMResponseCachewithoutdeletekeeps serving the responses the library rejects. Implementdelete, or accept the replay. PdfPlumberParser.extract_textopens a PDF once for its text and tables, where it opened it twice.AsyncSqliteEmbeddingStore.queryis much faster when called repeatedly. The store keeps its vectors in memory and rereads them only after the file changes, scores them in one NumPy product, and reads text and metadata for the returned chunks only. A discovery graph over a 9,000-chunk memory builds about four times faster. The store now holds its vectors in memory between queries until it is closed.- Releases are no longer held until sci-etl-cli and udg-catalogue run on them; consumer readiness is reported in the release notes instead.
Fixed¶
AsyncArxivExtractorretries a408request timeout, as the other bundled extractors do. It now shares their retry code, so its retry and failure messages start witharXivand name the action, such asarXiv LaTeX fetch for '2401.00001v1' failed after 3 attempts.AsyncSqliteFts5Store.searchraisesSearchQueryErrornaming the SQLite version when SQLite fails to highlight a field-scopedNEARgroup insideORorNOT, as SQLite 3.50.4 does for some documents. It raisedSearchStoreErrorwith "database disk image is malformed", which reads as a corrupt index.CachingLLMClientno longer replays a response the entity extractor or the relevance filter rejected. The rejected response was cached, so every retry got the same answer until the record was quarantined, without the model being asked again.
0.5.0 - 2026-09-25¶
This release changes how extractors page through a listing, what run state is saved, and how the pipeline is constructed. State saved by 0.4 is upgraded automatically. See "Upgrading to 0.5" in MIGRATION.md for code changes.
Added¶
- Cursor paging. Extractors return a
ListingPagefromfetch_page, with the next page's cursor. Extractors that page by offset also implementOffsetListing, whichnewest_firstruns andrun(start_index=)require. - Quarantine for records that keep failing.
run(max_attempts=3)skips a record that has failed in 3 runs.RunMetrics.quarantinedcounts skipped records. Both bundled state managers store the attempts and last error of each record. - Result caps are reported.
RunMetrics.listing_truncated,PageFetched.truncated, andPipelineMetadata.truncatedshow when a source stopped at its result cap. Progress events also include the pagecursor. - Table sinks.
SqlTableSinkandPlotly3DSinkinsci_etl_core.processors.sinkswrite a post-processedDataFrame. - Schema versions. The SQLite state database, LLM cache, and embedding
store, and the file state, record a schema version. A file written by a
newer release is refused, with
StateStoreErrorfor state files. StaleCursorError, for a cursor the source no longer accepts.BaseAppConfig.strict_sections, to opt out of strict config validation.LegacyExtractorAdapter, which runs an extractor written for 0.4 until it is ported. It is already deprecated.
Changed¶
- Breaking:
AsyncExtractor.fetch_page(query, cursor, page_size)replacessearchandparse_listing. The pipeline now skips processed records itself. - Breaking:
PipelineMetadata.cursorreplaceslast_start_index. - Breaking: pipeline constructors take the five collaborators by position
or name and all other arguments by keyword.
run()takes every argument afterqueryby keyword. - Breaking:
RawRecord,PipelineMetadata,TokenUsage,RunMetrics, and the progress events must be constructed with keyword arguments. - Breaking: unknown keys in the library's config sections, such as
search.bm25.titel, fail validation instead of being ignored. - Breaking: Python 3.11 or newer is required.
- A run that reaches a source's result cap now completes, and the next run starts again from the first page instead of stopping at the cap.
- A cursor the source rejects restarts the listing from the first page once.
AsyncOpenAlexExtractorpages with OpenAlex cursors and is no longer limited to the first 10,000 results. It no longer supportsnewest_first.AsyncPubMedExtractorandAsyncSemanticScholarExtractorstop at the last page of results without an extra empty request.
Deprecated¶
These keep working in 0.5 and are removed in 0.6.0.
With a replacement available now (DeprecationWarning):
- the blocking
Extractor,StateManager,Exporter,LLMClient,RelevanceFilter, andEntityExtractorinterfaces and theSync*Adapterclasses; LegacyExtractorAdapter;AsyncSqlTableExporterandAsyncPlotly3DExporter, replaced bySqlTableSinkandPlotly3DSink.
With a replacement arriving in 0.6.0 (PendingDeprecationWarning, so nothing
needs to change yet):
logger=arguments andconfigure_logging;AsyncETLPipeline(destination=);AsyncExporter.exportandAsyncCsvUpsertExporter.
Removed¶
- The config keys
pipeline.max_recordsandpipeline.max_workers, the matchingPipelineConfigproperties, andrun(max_records=). Usetotal_limitandmax_concurrency. build_retrying_session, andrequestsfrom thefullextra.
Fixed¶
AsyncPubMedExtractorno longer requests results past the 9,999th, which PubMed rejects.
0.4.1 - 2026-09-25¶
Changed¶
HttpConfig.user_agentandbuild_async_clientdefault tosci-etl-core/<installed version>instead ofsci-etl-core/0.1.
Fixed¶
load_config_asyncandAsyncCsvUpsertExportercheck whether a file exists in a worker thread instead of blocking the event loop.- The
sqlandfullextras requiresqlalchemy[asyncio], so they installgreenlet. SQLAlchemy 2.1 no longer installs it by default, and without itAsyncSqlTableExportercould not be imported.
0.4.0 - 2026-09-16¶
Added¶
AsyncPubMedExtractor,AsyncSemanticScholarExtractor, andAsyncOpenAlexExtractor. Each fillsRawRecord.metadatawithauthorsandcategories, andpublishedandyearwhen the source has a date, and retries transport faults,408,429, and server errors, honoringRetry-After.AsyncOpenAlexExtractoralso stores the works a paper cites underreferences.DocxParserfor Word.docxfiles, andJatsXmlParserfor JATS XML, whoseparse_articlereturns aJatsArticlewith sections, authors, keywords, identifiers, and references.- LLM response caching:
CachingLLMClientwraps anyAsyncLLMClientand answers repeated requests from anAsyncLLMResponseCache, eitherInMemoryLLMResponseCacheorAsyncSqliteLLMResponseCache. A cache fault is logged and counted inCacheStats, raised asLLMCacheErrorby the stores, and never fails a completion. - Graceful shutdown:
AsyncETLPipeline(shutdown=)andETLPipeline(shutdown=)take aShutdownSignal, so SIGINT, SIGTERM, orrequest()lets in-flight records finish and raisesPipelineInterrupted, a subclass ofPipelineAborted. Every run now ends with the state manager'sflush(), however it ends. - Progress events and run metrics:
on_eventreceivesRunStarted,PageFetched,RecordFinished,PageFinished, andRunFinishedfromsci_etl_core.observability, andlast_run_metricsreturnsRunMetricswith counts, durations, the run's outcome, and the tokens theusage_sourcesused.TokenUsagesupports+and-. run(newest_first=True)picks up new submissions in a newest-first listing without rescanning from offset 0.PipelineMetadatagainshead_ids,head_offset, andtail_ids, which both state backends save.rate_limiteron every bundled extractor,AsyncOpenAICompatibleClient, andAsyncOpenAIEmbedder, andHostRateLimiterfor limits per host.- Components built from the config:
AsyncArxivExtractor.from_config,AsyncOpenAICompatibleClient.from_config,AsyncETLPipeline.from_config, andETLPipeline.from_config, plusHttpConfig.build_client(),RateLimitConfig.build_limiter(), andPipelineConfig.run_arguments(). Asearchconfig section buildsBM25Weights,FusionParams,HybridParams, andGraphParams.PipelineConfiggainsnewest_first, andAsyncOpenAICompatibleClientamodelproperty. AsyncLLMEntityExtractor(validator=, logger=, label_field=)drops and logs the entities aRecordValidatorrejects.ScatterPlotConfigtakeshover_data_columns,hover_template,color_continuous_scale,color_range,color_label,marker, andlayout.ValueClipStepclamps numeric columns during post-processing, andTableLayoutStepsorts rows and orders columns.NEAR(...)proximity queries in the query language, as theNearnode withNEAR_DISTANCE, supported by both text stores, andQueryChip.near.- Range filters:
RangeFilterkeeps records whose tags lie between integer or text bounds, such as years or ISO 8601 dates, in both text stores, hybrid search, andfilter_graph.AsyncTextSearchStore.range_countscounts the matches in each of several ranges.SearchFilternames either filter type. - Richer snippets:
TextHit.snippetsandFusedHit.snippetshold aSnippetfor every field with a highlighted match.passage_snippetandsnippet_windowbuild snippets of other text. backfill_text_indexbuilds a text index from the chunks in the vector memory, removing the words overlapping chunks share (merge_passages), and reports what it did in aBackfillReport.AsyncEmbeddingStore.iter_recordsyields each record's passages as aStoredRecordwithout loading vectors, implemented by both bundled stores.AsyncSimilarArticleFinder.find_best_chunksreturns each article's best chunk, text included.SlidingWindowChunkerexposeschunk_wordsandoverlap_words.
Changed¶
PipelineConfig.max_recordsis nowtotal_limit, andmax_workersismax_concurrency. The old YAML keys and attributes still work with aDeprecationWarninguntil 0.5.0, and loading a config that sets an old and a new key to different values raisesConfigurationError.run(max_records=)is deprecated the same way.build_retrying_sessionis deprecated and will be removed in 0.5.0, along withrequestsin thefullextra.- A hybrid search hit found only by the semantic leg now carries a snippet of
its best chunk in
snippet,highlights, andsnippets, where these used to be empty. Code that showed the abstract wheneversnippetwas empty should checklexical_rank is Noneinstead. ETLPipeline.runwaits for the background loop in short slices, so a signal handler on the calling thread runs promptly, and aKeyboardInterruptcancels the run on the background loop.
0.3.0 - 2026-09-15¶
Added¶
- Local Boolean search in
sci_etl_core.search, which needs only the standard library: - A query language with terms,
"phrases",prefix*terms,title:,abstract:, andbody:scopes,AND,OR, andNOT(also written&&,||,-, or, forAND, nothing), and parentheses.parse_queryreturns a normalized AST, and a malformed query raisesSearchQueryError, whosepositionandtokenlocate the fault.describeturns a query into chips for display. AsyncSqliteFts5Store, a durable text index on SQLite FTS5. It ranks by BM25 with per-fieldBM25Weights, returns plain-text snippets with highlight offsets, filters by metadata (MetadataFilter), counts facets over itsfacet_keys, and offersoptimize,rebuild_index,rebuild_tags, andintegrity_checkfor maintenance.fts5_available()reports whether the interpreter's SQLite includes FTS5.InMemoryTextSearchStore, which matches the same records as the FTS5 store.AsyncSearchIndexer, the text-index counterpart ofAsyncChunkIngestor.- Rank fusion with
reciprocal_rank_fusion, the default, ornormalized_score_fusion, configured byFusionParams. AsyncHybridSearcher, which runs a lexical, semantic, or hybrid search and reports inSearchOutcome.degradedandSearchOutcome.skippedwhich retrieval legs failed or had nothing to run.- Discovery graphs in
sci_etl_core.search.build_discovery_graphgrows the neighborhood of a seed record breadth-first from one or more edge sources, keeps only mutual nearest neighbors by default, and groups the records into communities by deterministic label propagation.GraphParamsbounds the depth, fanout, minimum edge weight, node count, and label-propagation passes, andDiscoveryGraph.communities_convergedreports whether the pass limit cut label propagation short.filter_graphnarrows a built graph to matched records and metadata filters without any I/O.label_communitiesandselect_edgesare public as well. - Edge sources behind a new
AsyncEdgeSourceinterface.EmbeddingEdgeSourcerelates records by cosine similarity in the vector memory, andMetadataEdgeSourceby the share of tags two records have in common, such as arXiv categories and authors. sci_etl_core.discovery, a read-model for user interfaces:FacetandDiscoveryResult, also exported fromsci_etl_core. Importing it loads no store and no optional dependency.AsyncCompositeIngestor, which sends each record to several memory backends at once, such as the vector memory and a text index, so that a memory fault in one does not stop the others.MemoryIngestor, the protocol amemory_ingestorsatisfies, andMEMORY_FAULTS, the exceptions the pipeline treats as memory faults.SearchError, with its subclassesSearchQueryErrorandSearchStoreError.- A
searchextra. It installs nothing, because search needs only the standard library; it lets a requirements file say why the package is there.
Changed¶
AsyncETLPipeline(memory_ingestor=)accepts anyMemoryIngestor. ASearchStoreErrorduring memory ingest is logged, and the record's entities are still exported, as for an embedding fault. ASearchQueryErroris not a memory fault and fails the record.RawRecord.metadatais no longer empty for arXiv records:AsyncArxivExtractorfills it withcategories,authors,published, andyear. Code that comparedmetadata == {}will notice. The chunk metadataAsyncChunkIngestorstores is unchanged.AsyncSqliteEmbeddingStoreruns on a shared internal SQLite runner. This is behavior-preserving: exception types, messages, transactions, and cancellation behavior are unchanged.
Fixed¶
AsyncSqliteStateManager: cancelling a task that is awaiting a state operation no longer releases the connection while its worker thread is still using it. The next operation waits for that thread to finish.
0.2.0 - 2026-09-14¶
Security¶
load_configandload_config_asyncno longer copy pydantic's error text intoConfigurationError. That text could include the raw settings, and with them an API key read from the environment. The message now lists each failing key and the reason without its value, and the validation error is no longer chained to it.
Added¶
validate_config(config_cls, raw, source)validates settings loaded by other means with the same secret-safe messages.Retry-Aftersupport.AsyncArxivExtractor,AsyncOpenAICompatibleClient, andAsyncOpenAIEmbedderwait as long as a throttled or failing response asks, throughRetry-Afteror theretry-after-msheader OpenAI-compatible APIs send, when that is longer than their backoff. The newmax_retry_afterargument caps the wait (default 60 seconds).- The arXiv extractor logs each retry and how long it waits.
- Token usage.
AsyncOpenAICompatibleClient.usageandAsyncOpenAIEmbedder.usagereturn aTokenUsagesnapshot withrequests,prompt_tokens,completion_tokens, andtotal_tokens. Theusageproperty onAsyncLLMClientandAsyncEmbedderreturnsNoneunless overridden. - ruff and mypy run in CI, and a
lintextra installs them locally. - This changelog.
Changed¶
- The OpenAI SDK's built-in retries are turned off in the chat and embedding
clients, so
max_retriesis now the total number of attempts. Before, the SDK could retry each of those attempts again on its own. - Invalid-settings messages start with
Invalid configuration in <file>:and put each problem on its own line. AsyncETLPipeline.__aexit__is annotated to returnNone, so type checkers knowasync with pipelinenever suppresses an exception.
0.1.2 - 2026-09-14¶
Fixed¶
max_concurrencyof 0 left every record waiting forever. Values below 1 now raiseValueError, as do apage_sizebelow 1 and a negativetotal_limit.- A single failed record on a page of otherwise irrelevant records aborted the whole run. A run now aborts only when a second page fails with nothing processed before any progress, or when the listing ends right after such a page.
- The pipeline waited
sleep_betweenonce more after reachingtotal_limit. - A
max_retriesbelow 1 made the arXiv extractor, LLM client, and embedder fail without a single attempt. It now raisesValueError. configure_loggingfailed when the log file's folder was missing, and ignored a different file or level on later calls.AsyncFileStateManagersilently stripped whitespace from record ids, so such records were processed again on every run. It now rejects them, and the pipeline skips blank ids.last_run_atis recorded in UTC with an offset instead of naive local time.
Added¶
PipelineConfig.page_sizeandPipelineConfig.search_delay.- Range validation for every config section.
Changed¶
- The release workflow uploads the built files to a GitHub release that already exists instead of failing.
0.1.1 - 2026-09-14¶
Added¶
- A tag-triggered release workflow that runs the CI suite, checks the tag against the project version, and publishes to PyPI with trusted publishing.
- Package metadata for PyPI: license, keywords, classifiers, and project URLs.
0.1.0 - 2026-09-13¶
First tagged release: the async pipeline and its blocking facade, the arXiv extractor, OpenAI-compatible chat and embedding clients, PDF, LaTeX, and HTML parsers, CSV, SQL, and Plotly exporters, dataframe processors and validators, file and SQLite state, semantic memory, and the udg-catalogue migration guide.