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.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.