diff --git a/pyproject.toml b/pyproject.toml index b772335e..941db703 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,9 @@ default-optional-dependency-keys = [ ] [project.optional-dependencies] +cli = [ + "rich>=14.3.2" +] mssql = [ "mssql-python>=1.0.0" ] diff --git a/src/databao_context_engine/build_sources/build_runner.py b/src/databao_context_engine/build_sources/build_runner.py index 0ddbc45d..f258dbfe 100644 --- a/src/databao_context_engine/build_sources/build_runner.py +++ b/src/databao_context_engine/build_sources/build_runner.py @@ -17,6 +17,7 @@ from databao_context_engine.datasources.types import DatasourceId from databao_context_engine.pluginlib.build_plugin import DatasourceType from databao_context_engine.plugins.plugin_loader import load_plugins +from databao_context_engine.progress.progress import DatasourceStatus, ProgressCallback, ProgressEmitter from databao_context_engine.project.layout import ProjectLayout logger = logging.getLogger(__name__) @@ -50,9 +51,7 @@ class IndexSummary: def build( - project_layout: ProjectLayout, - *, - build_service: BuildService, + project_layout: ProjectLayout, *, build_service: BuildService, progress: ProgressCallback | None = None ) -> list[BuildContextResult]: """Build the context for all datasources in the project. @@ -70,14 +69,22 @@ def build( datasource_ids = discover_datasources(project_layout) + emitter = ProgressEmitter(progress) + if not datasource_ids: logger.info("No sources discovered under %s", project_layout.src_dir) + emitter.task_started(total_datasources=0) + emitter.task_finished(ok=0, failed=0, skipped=0) return [] + emitter.task_started(total_datasources=len(datasource_ids)) + number_of_failed_builds = 0 + number_of_skipped_builds = 0 + build_result = [] reset_all_results(project_layout.output_dir) - for datasource_id in datasource_ids: + for datasource_index, datasource_id in enumerate(datasource_ids, start=1): try: prepared_source = prepare_source(project_layout, datasource_id) @@ -85,6 +92,11 @@ def build( f'Found datasource of type "{prepared_source.datasource_type.full_type}" with name {prepared_source.datasource_id.datasource_path}' ) + emitter.datasource_started( + datasource_id=str(datasource_id), + index=datasource_index, + total=len(datasource_ids), + ) plugin = plugins.get(prepared_source.datasource_type) if plugin is None: logger.warning( @@ -92,12 +104,19 @@ def build( prepared_source.datasource_type.full_type, prepared_source.datasource_id.relative_path_to_config_file(), ) - number_of_failed_builds += 1 + emitter.datasource_finished( + datasource_id=str(datasource_id), + index=datasource_index, + total=len(datasource_ids), + status=DatasourceStatus.SKIPPED, + ) + number_of_skipped_builds += 1 continue result = build_service.process_prepared_source( prepared_source=prepared_source, plugin=plugin, + progress=progress, ) output_dir = project_layout.output_dir @@ -113,23 +132,46 @@ def build( context_file_path=context_file_path, ) ) + emitter.datasource_finished( + datasource_id=str(datasource_id), + index=datasource_index, + total=len(datasource_ids), + status=DatasourceStatus.OK, + ) except Exception as e: logger.debug(str(e), exc_info=True, stack_info=True) logger.info(f"Failed to build source at ({datasource_id.relative_path_to_config_file()}): {str(e)}") - + emitter.datasource_finished( + datasource_id=str(datasource_id), + index=datasource_index, + total=len(datasource_ids), + status=DatasourceStatus.FAILED, + error=str(e), + ) number_of_failed_builds += 1 logger.debug( - "Successfully built %d datasources. %s", + "Successfully built %d datasources. %s %s", len(build_result), + f"Skipped {number_of_skipped_builds}." if number_of_skipped_builds > 0 else "", f"Failed to build {number_of_failed_builds}." if number_of_failed_builds > 0 else "", ) + emitter.task_finished( + ok=len(build_result), + failed=number_of_failed_builds, + skipped=number_of_skipped_builds, + ) + return build_result def run_indexing( - *, project_layout: ProjectLayout, build_service: BuildService, contexts: list[DatasourceContext] + *, + project_layout: ProjectLayout, + build_service: BuildService, + contexts: list[DatasourceContext], + progress: ProgressCallback | None = None, ) -> IndexSummary: """Index a list of built datasource contexts. @@ -144,10 +186,19 @@ def run_indexing( summary = IndexSummary(total=len(contexts), indexed=0, skipped=0, failed=0) - for context in contexts: + emitter = ProgressEmitter(progress) + emitter.task_started(total_datasources=len(contexts)) + + for datasource_index, context in enumerate(contexts, start=1): try: logger.info(f"Indexing datasource {context.datasource_id}") + emitter.datasource_started( + datasource_id=str(context.datasource_id), + index=datasource_index, + total=len(contexts), + ) + datasource_type = read_datasource_type_from_context(context) plugin = plugins.get(datasource_type) @@ -158,14 +209,34 @@ def run_indexing( context.datasource_id, ) summary.skipped += 1 + emitter.datasource_finished( + datasource_id=str(context.datasource_id), + index=datasource_index, + total=len(contexts), + status=DatasourceStatus.SKIPPED, + ) continue - build_service.index_built_context(context=context, plugin=plugin) + build_service.index_built_context(context=context, plugin=plugin, progress=progress) summary.indexed += 1 + + emitter.datasource_finished( + datasource_id=str(context.datasource_id), + index=datasource_index, + total=len(contexts), + status=DatasourceStatus.OK, + ) except Exception as e: logger.debug(str(e), exc_info=True, stack_info=True) logger.info(f"Failed to build source at ({context.datasource_id}): {str(e)}") summary.failed += 1 + emitter.datasource_finished( + datasource_id=str(context.datasource_id), + index=datasource_index, + total=len(contexts), + status=DatasourceStatus.FAILED, + error=str(e), + ) logger.debug( "Successfully indexed %d/%d datasource(s). %s", @@ -174,4 +245,5 @@ def run_indexing( f"Skipped {summary.skipped}. Failed {summary.failed}." if (summary.skipped or summary.failed) else "", ) + emitter.task_finished(ok=summary.indexed, failed=summary.failed, skipped=summary.skipped) return summary diff --git a/src/databao_context_engine/build_sources/build_service.py b/src/databao_context_engine/build_sources/build_service.py index b80384f4..e8148e9a 100644 --- a/src/databao_context_engine/build_sources/build_service.py +++ b/src/databao_context_engine/build_sources/build_service.py @@ -13,6 +13,7 @@ from databao_context_engine.pluginlib.build_plugin import ( BuildPlugin, ) +from databao_context_engine.progress.progress import ProgressCallback from databao_context_engine.project.layout import ProjectLayout from databao_context_engine.serialization.yaml import to_yaml_string from databao_context_engine.services.chunk_embedding_service import ChunkEmbeddingService @@ -35,6 +36,7 @@ def process_prepared_source( *, prepared_source: PreparedDatasource, plugin: BuildPlugin, + progress: ProgressCallback | None = None, ) -> BuiltDatasourceContext: """Process a single source to build its context. @@ -58,11 +60,14 @@ def process_prepared_source( result=to_yaml_string(result.context), full_type=prepared_source.datasource_type.full_type, datasource_id=result.datasource_id, + progress=progress, ) return result - def index_built_context(self, *, context: DatasourceContext, plugin: BuildPlugin) -> None: + def index_built_context( + self, *, context: DatasourceContext, plugin: BuildPlugin, progress: ProgressCallback | None = None + ) -> None: """Index a context file using the given plugin. 1) Parses the yaml context file contents @@ -85,6 +90,7 @@ def index_built_context(self, *, context: DatasourceContext, plugin: BuildPlugin full_type=built.datasource_type, datasource_id=built.datasource_id, override=True, + progress=progress, ) def _deserialize_built_context( diff --git a/src/databao_context_engine/build_sources/build_wiring.py b/src/databao_context_engine/build_sources/build_wiring.py index b65887ff..024ef2fa 100644 --- a/src/databao_context_engine/build_sources/build_wiring.py +++ b/src/databao_context_engine/build_sources/build_wiring.py @@ -12,6 +12,7 @@ create_ollama_embedding_provider, create_ollama_service, ) +from databao_context_engine.progress.progress import ProgressCallback from databao_context_engine.project.layout import ProjectLayout from databao_context_engine.services.chunk_embedding_service import ChunkEmbeddingMode from databao_context_engine.services.factories import create_chunk_embedding_service @@ -23,7 +24,10 @@ def build_all_datasources( - project_layout: ProjectLayout, chunk_embedding_mode: ChunkEmbeddingMode + project_layout: ProjectLayout, + chunk_embedding_mode: ChunkEmbeddingMode, + *, + progress: ProgressCallback | None = None, ) -> list[BuildContextResult]: """Build the context for all datasources in the project. @@ -62,6 +66,7 @@ def build_all_datasources( return build( project_layout=project_layout, build_service=build_service, + progress=progress, ) @@ -69,6 +74,8 @@ def index_built_contexts( project_layout: ProjectLayout, contexts: list[DatasourceContext], chunk_embedding_mode: ChunkEmbeddingMode, + *, + progress: ProgressCallback | None = None, ) -> IndexSummary: """Index the contexts into the database. @@ -101,7 +108,9 @@ def index_built_contexts( description_provider=description_provider, chunk_embedding_mode=chunk_embedding_mode, ) - return run_indexing(project_layout=project_layout, build_service=build_service, contexts=contexts) + return run_indexing( + project_layout=project_layout, build_service=build_service, contexts=contexts, progress=progress + ) def _create_build_service( diff --git a/src/databao_context_engine/cli/commands.py b/src/databao_context_engine/cli/commands.py index 7563fa30..b2fa369d 100644 --- a/src/databao_context_engine/cli/commands.py +++ b/src/databao_context_engine/cli/commands.py @@ -20,6 +20,7 @@ from databao_context_engine.cli.info import echo_info from databao_context_engine.config.logging import configure_logging from databao_context_engine.mcp.mcp_runner import McpTransport, run_mcp_server +from databao_context_engine.progress.rich_progress import rich_progress @click.group() @@ -151,9 +152,12 @@ def build( Internally, this indexes the context to be used by the MCP server and the "retrieve" command. """ - result = DatabaoContextProjectManager(project_dir=ctx.obj["project_dir"]).build_context( - datasource_ids=None, chunk_embedding_mode=ChunkEmbeddingMode(chunk_embedding_mode.upper()) - ) + with rich_progress() as progress_cb: + result = DatabaoContextProjectManager(project_dir=ctx.obj["project_dir"]).build_context( + datasource_ids=None, + chunk_embedding_mode=ChunkEmbeddingMode(chunk_embedding_mode.upper()), + progress=progress_cb, + ) click.echo(f"Build complete. Processed {len(result)} datasources.") @@ -175,9 +179,11 @@ def index(ctx: Context, datasources_config_files: tuple[str, ...]) -> None: [DatasourceId.from_string_repr(p) for p in datasources_config_files] if datasources_config_files else None ) - summary = DatabaoContextProjectManager(project_dir=ctx.obj["project_dir"]).index_built_contexts( - datasource_ids=datasource_ids - ) + with rich_progress() as progress_cb: + summary = DatabaoContextProjectManager(project_dir=ctx.obj["project_dir"]).index_built_contexts( + datasource_ids=datasource_ids, + progress=progress_cb, + ) suffix = [] if summary.skipped: diff --git a/src/databao_context_engine/databao_context_project_manager.py b/src/databao_context_engine/databao_context_project_manager.py index 4755e4bf..702d2594 100644 --- a/src/databao_context_engine/databao_context_project_manager.py +++ b/src/databao_context_engine/databao_context_project_manager.py @@ -16,6 +16,7 @@ from databao_context_engine.datasources.datasource_discovery import get_datasource_list from databao_context_engine.datasources.types import Datasource, DatasourceId from databao_context_engine.pluginlib.build_plugin import DatasourceType +from databao_context_engine.progress.progress import ProgressCallback from databao_context_engine.project.layout import ( ProjectLayout, ensure_project_dir, @@ -77,6 +78,8 @@ def build_context( self, datasource_ids: list[DatasourceId] | None = None, chunk_embedding_mode: ChunkEmbeddingMode = ChunkEmbeddingMode.EMBEDDABLE_TEXT_ONLY, + *, + progress: ProgressCallback | None = None, ) -> list[BuildContextResult]: """Build the context for datasources in the project. @@ -85,17 +88,24 @@ def build_context( Args: datasource_ids: The list of datasource ids to build. If None, all datasources will be built. chunk_embedding_mode: The mode to use for chunk embedding. + progress: Optional callback that receives progress events during execution. Returns: The list of all built results. """ # TODO: Filter which datasources to build by datasource_ids - return build_all_datasources(project_layout=self._project_layout, chunk_embedding_mode=chunk_embedding_mode) + return build_all_datasources( + project_layout=self._project_layout, + chunk_embedding_mode=chunk_embedding_mode, + progress=progress, + ) def index_built_contexts( self, datasource_ids: list[DatasourceId] | None = None, chunk_embedding_mode: ChunkEmbeddingMode = ChunkEmbeddingMode.EMBEDDABLE_TEXT_ONLY, + *, + progress: ProgressCallback | None = None, ) -> IndexSummary: """Index built datasource contexts into the embeddings database. @@ -105,6 +115,7 @@ def index_built_contexts( Args: datasource_ids: The list of datsource ids to index. If None, all datsources will be indexed. chunk_embedding_mode: The mode to use for chunk embedding. + progress: Optional callback that receives progress events during execution. Returns: The summary of the index operation. @@ -117,7 +128,10 @@ def index_built_contexts( contexts = [c for c in contexts if c.datasource_id.datasource_path in wanted_paths] return index_built_contexts( - project_layout=self._project_layout, contexts=contexts, chunk_embedding_mode=chunk_embedding_mode + project_layout=self._project_layout, + contexts=contexts, + chunk_embedding_mode=chunk_embedding_mode, + progress=progress, ) def check_datasource_connection( diff --git a/src/databao_context_engine/progress/__init__.py b/src/databao_context_engine/progress/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/databao_context_engine/progress/progress.py b/src/databao_context_engine/progress/progress.py new file mode 100644 index 00000000..efc4050c --- /dev/null +++ b/src/databao_context_engine/progress/progress.py @@ -0,0 +1,126 @@ +from __future__ import annotations + +from dataclasses import dataclass +from enum import Enum +from typing import Callable + +EMIT_EVERY = 10 + + +class ProgressKind(str, Enum): + TASK_STARTED = "task_started" + TASK_FINISHED = "task_finished" + DATASOURCE_STARTED = "datasource_started" + DATASOURCE_FINISHED = "datasource_finished" + DATASOURCE_PROGRESS = "datasource_progress" + + +class DatasourceStatus(str, Enum): + OK = "ok" + SKIPPED = "skipped" + FAILED = "failed" + + +@dataclass(frozen=True, slots=True) +class ProgressEvent: + kind: ProgressKind + datasource_id: str | None = None + datasource_index: int | None = None + datasource_total: int | None = None + percent: int | None = None + status: DatasourceStatus | None = None + error: str | None = None + message: str = "" + + +ProgressCallback = Callable[[ProgressEvent], None] + + +class ProgressEmitter: + def __init__(self, cb: ProgressCallback | None): + self._cb = cb + + def emit(self, event: ProgressEvent) -> None: + if self._cb is not None: + self._cb(event) + + def task_started(self, *, total_datasources: int | None) -> None: + self.emit(ProgressEvent(kind=ProgressKind.TASK_STARTED, datasource_total=total_datasources)) + + def task_finished(self, *, ok: int, failed: int, skipped: int) -> None: + self.emit( + ProgressEvent( + kind=ProgressKind.TASK_FINISHED, + message=f"Finished (ok={ok}, failed={failed}, skipped={skipped})", + ) + ) + + def datasource_started( + self, + *, + datasource_id: str, + index: int, + total: int, + ) -> None: + self.emit( + ProgressEvent( + kind=ProgressKind.DATASOURCE_STARTED, + datasource_id=datasource_id, + datasource_index=index, + datasource_total=total, + message=f"Starting {datasource_id}", + ) + ) + + def datasource_progress_units( + self, + *, + datasource_id: str, + completed_units: int, + total_units: int, + message: str = "", + ) -> None: + if total_units <= 0: + self.datasource_progress(datasource_id=datasource_id, percent=100, message=message) + return + + completed_units = max(0, min(completed_units, total_units)) + percent = round((completed_units / total_units) * 100) + self.datasource_progress(datasource_id=datasource_id, percent=percent, message=message) + + def datasource_progress(self, *, datasource_id: str, percent: int, message: str = "") -> None: + if percent < 0: + percent = 0 + if percent > 100: + percent = 100 + self.emit( + ProgressEvent( + kind=ProgressKind.DATASOURCE_PROGRESS, + datasource_id=datasource_id, + percent=percent, + message=message, + ) + ) + + def datasource_finished( + self, + *, + datasource_id: str, + index: int, + total: int, + status: DatasourceStatus, + error: str | None = None, + ) -> None: + self.emit( + ProgressEvent( + kind=ProgressKind.DATASOURCE_FINISHED, + datasource_id=datasource_id, + datasource_index=index, + datasource_total=total, + status=status, + error=error, + message=( + f"Finished {datasource_id}" if status == DatasourceStatus.OK else f"{status.value}: {datasource_id}" + ), + ) + ) diff --git a/src/databao_context_engine/progress/rich_progress.py b/src/databao_context_engine/progress/rich_progress.py new file mode 100644 index 00000000..b5e87319 --- /dev/null +++ b/src/databao_context_engine/progress/rich_progress.py @@ -0,0 +1,195 @@ +from __future__ import annotations + +import logging +import sys +from contextlib import contextmanager +from typing import Callable, Iterator, Optional, TypedDict + +from rich.logging import RichHandler + +from databao_context_engine.progress.progress import ( + ProgressCallback, + ProgressEvent, + ProgressKind, +) + +_DESCRIPTION_COL_WIDTH = 50 + + +def _datasource_label(ds_id: str | None) -> str: + return ds_id or "datasource" + + +def _noop_progress_cb(_: ProgressEvent) -> None: + return + + +class _UIState(TypedDict): + datasource_index: int | None + datasource_total: int | None + last_percent: int + + +@contextmanager +def rich_progress() -> Iterator[ProgressCallback]: + try: + from rich.console import Console + from rich.progress import ( + BarColumn, + Progress, + ProgressColumn, + SpinnerColumn, + TaskID, + TaskProgressColumn, + TextColumn, + TimeRemainingColumn, + ) + from rich.table import Column + from rich.text import Text + except ImportError: + yield _noop_progress_cb + return + + interactive = sys.stderr.isatty() + if not interactive: + yield _noop_progress_cb + return + + class _EtaExceptOverallColumn(ProgressColumn): + def __init__(self, overall_task_id_getter: Callable[[], Optional[TaskID]]): + super().__init__() + self._overall_task_id_getter = overall_task_id_getter + self._eta = TimeRemainingColumn() + + def render(self, task) -> Text: + overall_id = self._overall_task_id_getter() + if overall_id is not None and task.id == overall_id: + return Text("") + return self._eta.render(task) + + console = Console(stderr=True) + + @contextmanager + def _use_rich_console_logging() -> Iterator[None]: + app_logger = logging.getLogger("databao_context_engine") + + prev_handlers = list(app_logger.handlers) + prev_propagate = app_logger.propagate + + def _is_console_handler(h: logging.Handler) -> bool: + return isinstance(h, logging.StreamHandler) and getattr(h, "stream", None) in (sys.stderr, sys.stdout) + + kept_handlers = [h for h in prev_handlers if not _is_console_handler(h)] + + rich_handler = RichHandler( + console=console, + show_time=False, + show_level=True, + show_path=False, + rich_tracebacks=False, + ) + + try: + app_logger.handlers = kept_handlers + [rich_handler] + app_logger.propagate = False + yield + finally: + app_logger.handlers = prev_handlers + app_logger.propagate = prev_propagate + + + tasks: dict[str, TaskID] = {} + ui_state: _UIState = { + "datasource_index": None, + "datasource_total": None, + "last_percent": 0, + } + + progress = Progress( + SpinnerColumn(), + TextColumn( + "[progress.description]{task.description}", + table_column=Column(width=_DESCRIPTION_COL_WIDTH, overflow="ellipsis", no_wrap=True), + ), + BarColumn(), + TaskProgressColumn(), + _EtaExceptOverallColumn(lambda: tasks.get("overall")), + transient=True, + console=console, + redirect_stdout=True, + redirect_stderr=True, + ) + + def _get_or_create_overall_task(total: int | None) -> TaskID: + if "overall" not in tasks: + tasks["overall"] = progress.add_task("Datasources", total=total) + else: + if total is not None: + progress.update(tasks["overall"], total=total) + return tasks["overall"] + + def _get_or_create_datasource_task() -> TaskID: + if "datasource" not in tasks: + tasks["datasource"] = progress.add_task("Datasource", total=100.0) + return tasks["datasource"] + + def _set_datasource_percent(percent: float) -> None: + task_id = _get_or_create_datasource_task() + clamped = max(0.0, min(100.0, percent)) + progress.update(task_id, completed=clamped) + + def _update_overall_description() -> None: + if "overall" not in tasks: + return + idx = ui_state["datasource_index"] + tot = ui_state["datasource_total"] + + if idx is not None and tot is not None: + progress.update(tasks["overall"], description=f"Datasources {idx}/{tot}") + + def on_event(ev: ProgressEvent) -> None: + match ev.kind: + case ProgressKind.TASK_STARTED: + _get_or_create_overall_task(ev.datasource_total) + return + case ProgressKind.TASK_FINISHED: + if ev.message: + progress.console.print(f"{ev.message}") + return + case ProgressKind.DATASOURCE_STARTED: + ui_state["datasource_index"] = ev.datasource_index + ui_state["datasource_total"] = ev.datasource_total + ui_state["last_percent"] = 0 + _get_or_create_overall_task(ev.datasource_total) + _update_overall_description() + + ds = _datasource_label(ev.datasource_id) + + task_id = _get_or_create_datasource_task() + progress.reset(task_id, completed=0, total=100.0, description=f"{ds}") + return + case ProgressKind.DATASOURCE_FINISHED: + idx = ev.datasource_index or 0 + tot = ev.datasource_total or 0 + ds = ev.datasource_id or "(unknown datasource)" + status = ev.status.value if ev.status else "done" + progress.console.print(f"{status.upper()} {idx}/{tot}: {ds}") + + _set_datasource_percent(100.0) + + _get_or_create_overall_task(ev.datasource_total) + progress.advance(tasks["overall"], 1) + + _update_overall_description() + return + case ProgressKind.DATASOURCE_PROGRESS: + if ev.percent is not None: + pct = int(ev.percent) + pct = max(ui_state["last_percent"], pct) + ui_state["last_percent"] = pct + _set_datasource_percent(float(pct)) + return + + with _use_rich_console_logging(): + with progress: + yield on_event \ No newline at end of file diff --git a/src/databao_context_engine/services/chunk_embedding_service.py b/src/databao_context_engine/services/chunk_embedding_service.py index 8d84a841..594936b1 100644 --- a/src/databao_context_engine/services/chunk_embedding_service.py +++ b/src/databao_context_engine/services/chunk_embedding_service.py @@ -5,6 +5,7 @@ from databao_context_engine.llm.descriptions.provider import DescriptionProvider from databao_context_engine.llm.embeddings.provider import EmbeddingProvider from databao_context_engine.pluginlib.build_plugin import EmbeddableChunk +from databao_context_engine.progress.progress import EMIT_EVERY, ProgressCallback, ProgressEmitter from databao_context_engine.serialization.yaml import to_yaml_string from databao_context_engine.services.embedding_shard_resolver import EmbeddingShardResolver from databao_context_engine.services.models import ChunkEmbedding @@ -58,7 +59,14 @@ def __init__( raise ValueError("A DescriptionProvider must be provided when generating descriptions") def embed_chunks( - self, *, chunks: list[EmbeddableChunk], result: str, full_type: str, datasource_id: str, override: bool = False + self, + *, + chunks: list[EmbeddableChunk], + result: str, + full_type: str, + datasource_id: str, + override: bool = False, + progress: ProgressCallback | None = None, ) -> None: """Turn plugin chunks into persisted chunks and embeddings. @@ -70,12 +78,14 @@ def embed_chunks( if not chunks: return + emitter = ProgressEmitter(progress) + logger.debug( f"Embedding {len(chunks)} chunks for datasource {datasource_id}, with chunk_embedding_mode={self._chunk_embedding_mode}" ) enriched_embeddings: list[ChunkEmbedding] = [] - for chunk in chunks: + for i, chunk in enumerate(chunks, start=1): chunk_display_text = chunk.content if isinstance(chunk.content, str) else to_yaml_string(chunk.content) generated_description = "" @@ -103,6 +113,13 @@ def embed_chunks( generated_description=generated_description, ) ) + if i % EMIT_EVERY == 0 or i == len(chunks): + total_units = len(chunks) * 2 + emitter.datasource_progress_units( + datasource_id=datasource_id, + completed_units=i, + total_units=total_units, + ) table_name = self._shard_resolver.resolve_or_create( embedder=self._embedding_provider.embedder, @@ -116,4 +133,5 @@ def embed_chunks( full_type=full_type, datasource_id=datasource_id, override=override, + progress=progress, ) diff --git a/src/databao_context_engine/services/persistence_service.py b/src/databao_context_engine/services/persistence_service.py index ffe0b832..596a3c2e 100644 --- a/src/databao_context_engine/services/persistence_service.py +++ b/src/databao_context_engine/services/persistence_service.py @@ -2,6 +2,7 @@ import duckdb +from databao_context_engine.progress.progress import EMIT_EVERY, ProgressCallback, ProgressEmitter from databao_context_engine.services.models import ChunkEmbedding from databao_context_engine.storage.models import ChunkDTO from databao_context_engine.storage.repositories.chunk_repository import ChunkRepository @@ -31,6 +32,7 @@ def write_chunks_and_embeddings( full_type: str, datasource_id: str, override: bool = False, + progress: ProgressCallback | None = None, ): """Atomically persist chunks and their vectors. @@ -43,6 +45,9 @@ def write_chunks_and_embeddings( if not chunk_embeddings: raise ValueError("chunk_embeddings must be a non-empty list") + emitter = ProgressEmitter(progress) + total_items = len(chunk_embeddings) + # Outside the transaction due to duckdb limitations. # DuckDB FK checks can behave unexpectedly across multiple statements in the same transaction when deleting # and re-inserting related rows. It also does not support on delete cascade yet. @@ -52,7 +57,7 @@ def write_chunks_and_embeddings( self._chunk_repo.delete_by_datasource_id(datasource_id=datasource_id) with transaction(self._conn): - for chunk_embedding in chunk_embeddings: + for i, chunk_embedding in enumerate(chunk_embeddings, start=1): chunk_dto = self.create_chunk( full_type=full_type, datasource_id=datasource_id, @@ -61,6 +66,19 @@ def write_chunks_and_embeddings( ) self.create_embedding(table_name=table_name, chunk_id=chunk_dto.chunk_id, vec=chunk_embedding.vec) + if i % EMIT_EVERY == 0 or i == total_items: + total_units = total_items * 2 + emitter.datasource_progress_units( + datasource_id=datasource_id, + completed_units=total_items + i, + total_units=total_units, + ) + emitter.datasource_progress_units( + datasource_id=datasource_id, + completed_units=total_items * 2, + total_units=total_items * 2, + ) + def create_chunk(self, *, full_type: str, datasource_id: str, embeddable_text: str, display_text: str) -> ChunkDTO: return self._chunk_repo.create( full_type=full_type, diff --git a/tests/build_sources/test_build_runner.py b/tests/build_sources/test_build_runner.py index af84e5d0..4c47be20 100644 --- a/tests/build_sources/test_build_runner.py +++ b/tests/build_sources/test_build_runner.py @@ -149,7 +149,7 @@ def test_run_indexing_indexes_when_plugin_exists(mocker, mock_build_service, pro build_runner.run_indexing(project_layout=project_layout, build_service=mock_build_service, contexts=[ctx]) - mock_build_service.index_built_context.assert_called_once_with(context=ctx, plugin=plugin) + mock_build_service.index_built_context.assert_called_once_with(context=ctx, plugin=plugin, progress=None) def test_run_indexing_skips_when_plugin_missing(mocker, mock_build_service, project_layout, caplog): @@ -183,5 +183,5 @@ def test_run_indexing_continues_on_exception(mocker, mock_build_service, project build_runner.run_indexing(project_layout=project_layout, build_service=mock_build_service, contexts=[c1, c2]) assert mock_build_service.index_built_context.call_count == 2 - mock_build_service.index_built_context.assert_any_call(context=c1, plugin=plugin) - mock_build_service.index_built_context.assert_any_call(context=c2, plugin=plugin) + mock_build_service.index_built_context.assert_any_call(context=c1, plugin=plugin, progress=None) + mock_build_service.index_built_context.assert_any_call(context=c2, plugin=plugin, progress=None) diff --git a/tests/build_sources/test_build_service.py b/tests/build_sources/test_build_service.py index 384f7c13..be677452 100644 --- a/tests/build_sources/test_build_service.py +++ b/tests/build_sources/test_build_service.py @@ -73,6 +73,7 @@ def test_process_prepared_source_happy_path_creates_row_and_embeds(svc, chunk_em result=f"context: ok{os.linesep}", datasource_id="files/two.md", full_type="files/md", + progress=None, ) assert out is result @@ -135,6 +136,7 @@ def test_index_built_context_happy_path_embeds(svc, chunk_embed_svc, mocker): full_type="files/md", datasource_id="files/two.md", override=True, + progress=None, ) diff --git a/tests/test_databao_context_project_manager.py b/tests/test_databao_context_project_manager.py index 13dcd3f1..2495b031 100644 --- a/tests/test_databao_context_project_manager.py +++ b/tests/test_databao_context_project_manager.py @@ -132,6 +132,7 @@ def test_databao_context_project_manager__index_built_contexts_indexes_all_when_ project_layout=pm._project_layout, contexts=[c1, c2], chunk_embedding_mode=ChunkEmbeddingMode.EMBEDDABLE_TEXT_ONLY, + progress=None, ) @@ -164,6 +165,7 @@ def test_databao_context_project_manager__index_built_contexts_filters_by_dataso project_layout=pm._project_layout, contexts=[c1, c3], chunk_embedding_mode=ChunkEmbeddingMode.EMBEDDABLE_TEXT_ONLY, + progress=None, ) diff --git a/uv.lock b/uv.lock index b7858a05..9bc6ea53 100644 --- a/uv.lock +++ b/uv.lock @@ -174,30 +174,30 @@ wheels = [ [[package]] name = "boto3" -version = "1.42.40" +version = "1.42.45" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d1/5a/bc190b2e8f2f625cbe50e1226fd7ce6b3719159add6d8d3c01a60defb7c4/boto3-1.42.40.tar.gz", hash = "sha256:e9e08059ae1bd47de411d361e9bfaaa6f35c8f996d68025deefff2b4dda79318", size = 112843, upload-time = "2026-02-02T20:35:04.404Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/e6/8fdd78825de6d8086aa3097955f83d8db3c5a3868b73da233c49977a7444/boto3-1.42.45.tar.gz", hash = "sha256:4db50b8b39321fab87ff7f40ab407887d436d004c1f2b0dfdf56e42b4884709b", size = 112846, upload-time = "2026-02-09T21:50:14.925Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/e7/e0d57bcb17aa0ef1d234c9685b67d26e92ee5d2031415a3ca051803ebc6c/boto3-1.42.40-py3-none-any.whl", hash = "sha256:91d776b8b68006c1aca204d384be191883c2a36443f4a90561165986dae17b74", size = 140604, upload-time = "2026-02-02T20:35:01.913Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e0/d59a178799412cfe38c2757d6e49c337a5e71b18cdc3641dd6d9daf52151/boto3-1.42.45-py3-none-any.whl", hash = "sha256:5074e074a718a6f3c2b519cbb9ceab258f17b331a143d23351d487984f2a412f", size = 140604, upload-time = "2026-02-09T21:50:13.113Z" }, ] [[package]] name = "botocore" -version = "1.42.40" +version = "1.42.45" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/ea/912061bde3c36b951d7e56e53f48445f8ecf769c222ef46f604ecf8f574f/botocore-1.42.40.tar.gz", hash = "sha256:6cfa07cf35ad477daef4920324f6d81b8d3a10a35baeafaa5fca22fb3ad225e2", size = 14916776, upload-time = "2026-02-02T20:34:52.409Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7a/b1/c36ad705d67bb935eac3085052b5dc03ec22d5ac12e7aedf514f3d76cac8/botocore-1.42.45.tar.gz", hash = "sha256:40b577d07b91a0ed26879da9e4658d82d3a400382446af1014d6ad3957497545", size = 14941217, upload-time = "2026-02-09T21:50:01.966Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ac/6a/a8b4fa14774b576bd7410c32a63d06a3a55287457d67c6bf11ebfef22aeb/botocore-1.42.40-py3-none-any.whl", hash = "sha256:b115cdfece8162cb30f387fdff2ee4693713744c97ebb4b89742e53675dc521c", size = 14592684, upload-time = "2026-02-02T20:34:48.087Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ec/6681b8e4884f8663d7650220e702c503e4ba6bd09a5b91d44803b0b1d0a8/botocore-1.42.45-py3-none-any.whl", hash = "sha256:a5ea5d1b7c46c2d5d113879e45b21eaf7d60dc865f4bcb46dfcf0703fe3429f4", size = 14615557, upload-time = "2026-02-09T21:49:57.066Z" }, ] [[package]] @@ -498,6 +498,9 @@ dependencies = [ athena = [ { name = "pyathena" }, ] +cli = [ + { name = "rich" }, +] clickhouse = [ { name = "clickhouse-connect" }, ] @@ -546,9 +549,10 @@ requires-dist = [ { name = "pymysql", marker = "extra == 'mysql'", specifier = ">=1.1.2" }, { name = "pyyaml", specifier = ">=6.0.3" }, { name = "requests", specifier = ">=2.32.5" }, + { name = "rich", marker = "extra == 'cli'", specifier = ">=14.3.2" }, { name = "snowflake-connector-python", marker = "extra == 'snowflake'", specifier = ">=4.2.0" }, ] -provides-extras = ["mssql", "clickhouse", "athena", "snowflake", "mysql", "postgresql", "pdf"] +provides-extras = ["cli", "mssql", "clickhouse", "athena", "snowflake", "mysql", "postgresql", "pdf"] [package.metadata.requires-dev] dev = [ @@ -637,7 +641,7 @@ wheels = [ [[package]] name = "docling-core" -version = "2.63.0" +version = "2.64.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jsonref" }, @@ -651,9 +655,9 @@ dependencies = [ { name = "typer" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d3/76/f6a1333c0ce4c20e60358185ff8b7fa92e1e1561a43a6788e7c8aaa9898e/docling_core-2.63.0.tar.gz", hash = "sha256:946cf97f27cb81a2c6507121045a356be91e40b5a06bbaf028ca7036df78b2f1", size = 251016, upload-time = "2026-02-03T14:41:07.158Z" } +sdist = { url = "https://files.pythonhosted.org/packages/23/f2/692de80893b0e0b22e4a5faa03f970b65de6d274a84d375c0ef92b54700b/docling_core-2.64.0.tar.gz", hash = "sha256:5ceb993d1ad743a882fe9bbae63a6b91ae5475e9d90cdf7451e9a1a7c2c2589f", size = 251646, upload-time = "2026-02-09T12:04:51.565Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/c4/0c825b46412f088828dd2730d231c745d1ff4b5537eed292e827103eff37/docling_core-2.63.0-py3-none-any.whl", hash = "sha256:8f39167bf17da13225c8a67d23df98c87a74e2ab39762dbf51fab93d9b90de25", size = 238637, upload-time = "2026-02-03T14:41:05.55Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/9f75957f7ac2e048c0cfad56631267e9d5a23b0d5708b7639598072f5253/docling_core-2.64.0-py3-none-any.whl", hash = "sha256:38dd5d8c60eba8b76a0f37c6f3510c0e53d3d53e0e2585329755b1886887b714", size = 239264, upload-time = "2026-02-09T12:04:50.256Z" }, ] [package.optional-dependencies] @@ -758,14 +762,14 @@ wheels = [ [[package]] name = "faker" -version = "40.1.2" +version = "40.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tzdata", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/77/1c3ff07b6739b9a1d23ca01ec0a90a309a33b78e345a3eb52f9ce9240e36/faker-40.1.2.tar.gz", hash = "sha256:b76a68163aa5f171d260fc24827a8349bc1db672f6a665359e8d0095e8135d30", size = 1949802, upload-time = "2026-01-13T20:51:49.917Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/7e/dccb7013c9f3d66f2e379383600629fec75e4da2698548bdbf2041ea4b51/faker-40.4.0.tar.gz", hash = "sha256:76f8e74a3df28c3e2ec2caafa956e19e37a132fdc7ea067bc41783affcfee364", size = 1952221, upload-time = "2026-02-06T23:30:15.515Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/46/ec/91a434c8a53d40c3598966621dea9c50512bec6ce8e76fa1751015e74cef/faker-40.1.2-py3-none-any.whl", hash = "sha256:93503165c165d330260e4379fd6dc07c94da90c611ed3191a0174d2ab9966a42", size = 1985633, upload-time = "2026-01-13T20:51:47.982Z" }, + { url = "https://files.pythonhosted.org/packages/ac/63/58efa67c10fb27810d34351b7a10f85f109a7f7e2a07dc3773952459c47b/faker-40.4.0-py3-none-any.whl", hash = "sha256:486d43c67ebbb136bc932406418744f9a0bdf2c07f77703ea78b58b77e9aa443", size = 1987060, upload-time = "2026-02-06T23:30:13.44Z" }, ] [[package]] @@ -788,11 +792,11 @@ wheels = [ [[package]] name = "fsspec" -version = "2026.1.0" +version = "2026.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d5/7d/5df2650c57d47c57232af5ef4b4fdbff182070421e405e0d62c6cdbfaa87/fsspec-2026.1.0.tar.gz", hash = "sha256:e987cb0496a0d81bba3a9d1cee62922fb395e7d4c3b575e57f547953334fe07b", size = 310496, upload-time = "2026-01-09T15:21:35.562Z" } +sdist = { url = "https://files.pythonhosted.org/packages/51/7c/f60c259dcbf4f0c47cc4ddb8f7720d2dcdc8888c8e5ad84c73ea4531cc5b/fsspec-2026.2.0.tar.gz", hash = "sha256:6544e34b16869f5aacd5b90bdf1a71acb37792ea3ddf6125ee69a22a53fb8bff", size = 313441, upload-time = "2026-02-05T21:50:53.743Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/01/c9/97cc5aae1648dcb851958a3ddf73ccd7dbe5650d95203ecb4d7720b4cdbf/fsspec-2026.1.0-py3-none-any.whl", hash = "sha256:cb76aa913c2285a3b49bdd5fc55b1d7c708d7208126b60f2eb8194fe1b4cbdcc", size = 201838, upload-time = "2026-01-09T15:21:34.041Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" }, ] [[package]] @@ -872,7 +876,7 @@ wheels = [ [[package]] name = "huggingface-hub" -version = "0.36.1" +version = "0.36.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, @@ -884,9 +888,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/45/54/096903f02ca14eb2670a4d11729da44a026c0bababec8c15f160441124c5/huggingface_hub-0.36.1.tar.gz", hash = "sha256:5a3b8bf87e182ad6f1692c196bb9ec9ade7755311d5d5e792dc45045f77283ad", size = 649681, upload-time = "2026-02-02T10:46:58.287Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/b7/8cb61d2eece5fb05a83271da168186721c450eb74e3c31f7ef3169fa475b/huggingface_hub-0.36.2.tar.gz", hash = "sha256:1934304d2fb224f8afa3b87007d58501acfda9215b334eed53072dd5e815ff7a", size = 649782, upload-time = "2026-02-06T09:24:13.098Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/cb/8f5141b3c21d1ecdf87852506eb583fec497c7e9803a168fe4aec64252bb/huggingface_hub-0.36.1-py3-none-any.whl", hash = "sha256:c6fa8a8f7b8559bc624ebb7e218fb72171b30f6049ebe08f8bfc2a44b38ece50", size = 566283, upload-time = "2026-02-02T10:46:56.459Z" }, + { url = "https://files.pythonhosted.org/packages/a8/af/48ac8483240de756d2438c380746e7130d1c6f75802ef22f3c6d49982787/huggingface_hub-0.36.2-py3-none-any.whl", hash = "sha256:48f0c8eac16145dfce371e9d2d7772854a4f591bcb56c9cf548accf531d54270", size = 566395, upload-time = "2026-02-06T09:24:11.133Z" }, ] [[package]] @@ -1670,20 +1674,20 @@ wheels = [ [[package]] name = "opencv-python" -version = "4.13.0.90" +version = "4.13.0.92" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/77/d7/133d5756aef78090f4d8dd4895793aed24942dec6064a15375cfac9175fc/opencv_python-4.13.0.90-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:58803f8b05b51d8a785e2306d83b44173b32536f980342f3bc76d8c122b5938d", size = 46020278, upload-time = "2026-01-18T08:57:42.539Z" }, - { url = "https://files.pythonhosted.org/packages/7b/65/3b8cdbe13fa2436695d00e1d8c1ddf5edb4050a93436f34ed867233d1960/opencv_python-4.13.0.90-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:a5354e8b161409fce7710ba4c1cfe88b7bb460d97f705dc4e714a1636616f87d", size = 32568376, upload-time = "2026-01-18T08:58:47.19Z" }, - { url = "https://files.pythonhosted.org/packages/34/ff/e4d7c165e678563f49505d3d2811fcc16011e929cd00bc4b0070c7ee82b0/opencv_python-4.13.0.90-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d557cbf0c7818081c9acf56585b68e781af4f00638971f75eaa3de70904a6314", size = 47685110, upload-time = "2026-01-18T08:59:58.045Z" }, - { url = "https://files.pythonhosted.org/packages/cf/02/d9b73dbce28712204e85ae4c1e179505e9a771f95b33743a97e170caedde/opencv_python-4.13.0.90-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9911581e37b24169e4842069ff01d6645ea2bc4af7e10a022d9ebe340fd035ec", size = 70460479, upload-time = "2026-01-18T09:01:16.377Z" }, - { url = "https://files.pythonhosted.org/packages/fc/1c/87fa71968beb71481ed359e21772061ceff7c9b45a61b3e7daa71e5b0b66/opencv_python-4.13.0.90-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:1150b8f1947761b848bbfa9c96ceba8877743ffef157c08a04af6f7717ddd709", size = 46707819, upload-time = "2026-01-18T09:02:48.049Z" }, - { url = "https://files.pythonhosted.org/packages/af/16/915a94e5b537c328fa3e96b769c7d4eed3b67d1be978e0af658a3d3faed8/opencv_python-4.13.0.90-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:d6716f16149b04eea52f953b8ca983d60dd9cd4872c1fd5113f6e2fcebb90e93", size = 72926629, upload-time = "2026-01-18T09:04:29.23Z" }, - { url = "https://files.pythonhosted.org/packages/bf/84/9c63c84be013943dd4c5fff36157f1ec0ec894b69a2fc3026fd4e3c9280a/opencv_python-4.13.0.90-cp37-abi3-win32.whl", hash = "sha256:458a00f2ba47a877eca385be3e7bcc45e6d30a4361d107ce73c1800f516dab09", size = 30932151, upload-time = "2026-01-18T09:05:22.181Z" }, - { url = "https://files.pythonhosted.org/packages/13/de/291cbb17f44242ed6bfd3450fc2535d6bd298115c0ccd6f01cd51d4a11d7/opencv_python-4.13.0.90-cp37-abi3-win_amd64.whl", hash = "sha256:526bde4c33a86808a751e2bb57bf4921beb49794621810971926c472897f6433", size = 40211706, upload-time = "2026-01-18T09:06:06.749Z" }, + { url = "https://files.pythonhosted.org/packages/fc/6f/5a28fef4c4a382be06afe3938c64cc168223016fa520c5abaf37e8862aa5/opencv_python-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:caf60c071ec391ba51ed00a4a920f996d0b64e3e46068aac1f646b5de0326a19", size = 46247052, upload-time = "2026-02-05T07:01:25.046Z" }, + { url = "https://files.pythonhosted.org/packages/08/ac/6c98c44c650b8114a0fb901691351cfb3956d502e8e9b5cd27f4ee7fbf2f/opencv_python-4.13.0.92-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:5868a8c028a0b37561579bfb8ac1875babdc69546d236249fff296a8c010ccf9", size = 32568781, upload-time = "2026-02-05T07:01:41.379Z" }, + { url = "https://files.pythonhosted.org/packages/3e/51/82fed528b45173bf629fa44effb76dff8bc9f4eeaee759038362dfa60237/opencv_python-4.13.0.92-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bc2596e68f972ca452d80f444bc404e08807d021fbba40df26b61b18e01838a", size = 47685527, upload-time = "2026-02-05T06:59:11.24Z" }, + { url = "https://files.pythonhosted.org/packages/db/07/90b34a8e2cf9c50fe8ed25cac9011cde0676b4d9d9c973751ac7616223a2/opencv_python-4.13.0.92-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:402033cddf9d294693094de5ef532339f14ce821da3ad7df7c9f6e8316da32cf", size = 70460872, upload-time = "2026-02-05T06:59:19.162Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/7a9cc719b3eaf4377b9c2e3edeb7ed3a81de41f96421510c0a169ca3cfd4/opencv_python-4.13.0.92-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:bccaabf9eb7f897ca61880ce2869dcd9b25b72129c28478e7f2a5e8dee945616", size = 46708208, upload-time = "2026-02-05T06:59:15.419Z" }, + { url = "https://files.pythonhosted.org/packages/fd/55/b3b49a1b97aabcfbbd6c7326df9cb0b6fa0c0aefa8e89d500939e04aa229/opencv_python-4.13.0.92-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:620d602b8f7d8b8dab5f4b99c6eb353e78d3fb8b0f53db1bd258bb1aa001c1d5", size = 72927042, upload-time = "2026-02-05T06:59:23.389Z" }, + { url = "https://files.pythonhosted.org/packages/fb/17/de5458312bcb07ddf434d7bfcb24bb52c59635ad58c6e7c751b48949b009/opencv_python-4.13.0.92-cp37-abi3-win32.whl", hash = "sha256:372fe164a3148ac1ca51e5f3ad0541a4a276452273f503441d718fab9c5e5f59", size = 30932638, upload-time = "2026-02-05T07:02:14.98Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a5/1be1516390333ff9be3a9cb648c9f33df79d5096e5884b5df71a588af463/opencv_python-4.13.0.92-cp37-abi3-win_amd64.whl", hash = "sha256:423d934c9fafb91aad38edf26efb46da91ffbc05f3f59c4b0c72e699720706f5", size = 40212062, upload-time = "2026-02-05T07:02:12.724Z" }, ] [[package]] @@ -1906,7 +1910,7 @@ wheels = [ [[package]] name = "pyathena" -version = "3.26.0" +version = "3.27.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "boto3" }, @@ -1915,9 +1919,9 @@ dependencies = [ { name = "python-dateutil" }, { name = "tenacity" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6d/6f/175b48abd18c2a135c0ad2c093d340fdf75cb16cc25441a965c2fa04a727/pyathena-3.26.0.tar.gz", hash = "sha256:902380f52b953b73a10d6182b7145c4f739f2d0fcc050b408b4eb1a345b48016", size = 114457, upload-time = "2026-01-31T11:39:16.654Z" } +sdist = { url = "https://files.pythonhosted.org/packages/62/21/d1d5b9c8c81ffc755a79e29a4d8448b7ce13eda970da1b9882f360c04269/pyathena-3.27.1.tar.gz", hash = "sha256:482572e624560b6f238c2fcaf467d372602952315bc5b471e818d008f19c93f5", size = 114550, upload-time = "2026-02-08T13:46:26.583Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/d5/543aabe44fe99cc22def83f3da6722625e3efd4cde86c19bd082d65ad224/pyathena-3.26.0-py3-none-any.whl", hash = "sha256:c679e5a90d8fe0bf5871e03df4af9bfe9a896c5fed3799fb95be93c664e45aa5", size = 153534, upload-time = "2026-01-31T11:39:14.607Z" }, + { url = "https://files.pythonhosted.org/packages/70/9c/023fb3b7744ffcc9c23670f9114260c5cbc87d3e4553240bc0690c5553ca/pyathena-3.27.1-py3-none-any.whl", hash = "sha256:7bb0e1d3d692f71387ec75fe62ae6912acb32db5500233db07a06189783409d2", size = 153572, upload-time = "2026-02-08T13:46:25.2Z" }, ] [[package]] @@ -2194,31 +2198,31 @@ wheels = [ [[package]] name = "pypdfium2" -version = "5.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/83/173dab58beb6c7e772b838199014c173a2436018dd7cfde9bbf4a3be15da/pypdfium2-5.3.0.tar.gz", hash = "sha256:2873ffc95fcb01f329257ebc64a5fdce44b36447b6b171fe62f7db5dc3269885", size = 268742, upload-time = "2026-01-05T16:29:03.02Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/a4/6bb5b5918c7fc236ec426be8a0205a984fe0a26ae23d5e4dd497398a6571/pypdfium2-5.3.0-py3-none-android_23_arm64_v8a.whl", hash = "sha256:885df6c78d41600cb086dc0c76b912d165b5bd6931ca08138329ea5a991b3540", size = 2763287, upload-time = "2026-01-05T16:28:24.21Z" }, - { url = "https://files.pythonhosted.org/packages/3e/64/24b41b906006bf07099b095f0420ee1f01a3a83a899f3e3731e4da99c06a/pypdfium2-5.3.0-py3-none-android_23_armeabi_v7a.whl", hash = "sha256:6e53dee6b333ee77582499eff800300fb5aa0c7eb8f52f95ccb5ca35ebc86d48", size = 2303285, upload-time = "2026-01-05T16:28:26.274Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c0/3ec73f4ded83ba6c02acf6e9d228501759d5d74fe57f1b93849ab92dcc20/pypdfium2-5.3.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ce4466bdd62119fe25a5f74d107acc9db8652062bf217057630c6ff0bb419523", size = 2816066, upload-time = "2026-01-05T16:28:28.099Z" }, - { url = "https://files.pythonhosted.org/packages/62/ca/e553b3b8b5c2cdc3d955cc313493ac27bbe63fc22624769d56ded585dd5e/pypdfium2-5.3.0-py3-none-macosx_11_0_x86_64.whl", hash = "sha256:cc2647fd03db42b8a56a8835e8bc7899e604e2042cd6fedeea53483185612907", size = 2945545, upload-time = "2026-01-05T16:28:29.489Z" }, - { url = "https://files.pythonhosted.org/packages/a1/56/615b776071e95c8570d579038256d0c77969ff2ff381e427be4ab8967f44/pypdfium2-5.3.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35e205f537ddb4069e4b4e22af7ffe84fcf2d686c3fee5e5349f73268a0ef1ca", size = 2979892, upload-time = "2026-01-05T16:28:31.088Z" }, - { url = "https://files.pythonhosted.org/packages/df/10/27114199b765bdb7d19a9514c07036ad2fc3a579b910e7823ba167ead6de/pypdfium2-5.3.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5795298f44050797ac030994fc2525ea35d2d714efe70058e0ee22e5f613f27", size = 2765738, upload-time = "2026-01-05T16:28:33.18Z" }, - { url = "https://files.pythonhosted.org/packages/b4/d7/2a3afa35e6c205a4f6264c33b8d2f659707989f93c30b336aa58575f66fa/pypdfium2-5.3.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7cd43dfceb77137e69e74c933d41506da1dddaff70f3a794fb0ad0d73e90d75", size = 3064338, upload-time = "2026-01-05T16:28:34.731Z" }, - { url = "https://files.pythonhosted.org/packages/a2/f1/6658755cf6e369bb51d0bccb81c51c300404fbe67c2f894c90000b6442dd/pypdfium2-5.3.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5956867558fd3a793e58691cf169718864610becb765bfe74dd83f05cbf1ae3", size = 3415059, upload-time = "2026-01-05T16:28:37.313Z" }, - { url = "https://files.pythonhosted.org/packages/f5/34/f86482134fa641deb1f524c45ec7ebd6fc8d404df40c5657ddfce528593e/pypdfium2-5.3.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3ff1071e9a782625822658dfe6e29e3a644a66960f8713bb17819f5a0ac5987", size = 2998517, upload-time = "2026-01-05T16:28:38.873Z" }, - { url = "https://files.pythonhosted.org/packages/09/34/40ab99425dcf503c172885904c5dc356c052bfdbd085f9f3cc920e0b8b25/pypdfium2-5.3.0-py3-none-manylinux_2_27_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f319c46ead49d289ab8c1ed2ea63c91e684f35bdc4cf4dc52191c441182ac481", size = 3673154, upload-time = "2026-01-05T16:28:40.347Z" }, - { url = "https://files.pythonhosted.org/packages/a5/67/0f7532f80825a7728a5cbff3f1104857f8f9fe49ebfd6cb25582a89ae8e1/pypdfium2-5.3.0-py3-none-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6dc67a186da0962294321cace6ccc0a4d212dbc5e9522c640d35725a812324b8", size = 2965002, upload-time = "2026-01-05T16:28:42.143Z" }, - { url = "https://files.pythonhosted.org/packages/ce/6c/c03d2a3d6621b77aac9604bce1c060de2af94950448787298501eac6c6a2/pypdfium2-5.3.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0ad0afd3d2b5b54d86287266fd6ae3fef0e0a1a3df9d2c4984b3e3f8f70e6330", size = 4130530, upload-time = "2026-01-05T16:28:44.264Z" }, - { url = "https://files.pythonhosted.org/packages/af/39/9ad1f958cbe35d4693ae87c09ebafda4bb3e4709c7ccaec86c1a829163a3/pypdfium2-5.3.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1afe35230dc3951b3e79b934c0c35a2e79e2372d06503fce6cf1926d2a816f47", size = 3746568, upload-time = "2026-01-05T16:28:45.897Z" }, - { url = "https://files.pythonhosted.org/packages/2a/e2/4d32310166c2d6955d924737df8b0a3e3efc8d133344a98b10f96320157d/pypdfium2-5.3.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:00385793030cadce08469085cd21b168fd8ff981b009685fef3103bdc5fc4686", size = 4336683, upload-time = "2026-01-05T16:28:47.584Z" }, - { url = "https://files.pythonhosted.org/packages/14/ea/38c337ff12a8cec4b00fd4fdb0a63a70597a344581e20b02addbd301ab56/pypdfium2-5.3.0-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:d911e82676398949697fef80b7f412078df14d725a91c10e383b727051530285", size = 4375030, upload-time = "2026-01-05T16:28:49.5Z" }, - { url = "https://files.pythonhosted.org/packages/a1/77/9d8de90c35d2fc383be8819bcde52f5821dacbd7404a0225e4010b99d080/pypdfium2-5.3.0-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:ca1dc625ed347fac3d9002a3ed33d521d5803409bd572e7b3f823c12ab2ef58f", size = 3928914, upload-time = "2026-01-05T16:28:51.433Z" }, - { url = "https://files.pythonhosted.org/packages/a5/39/9d4a6fbd78fcb6803b0ea5e4952a31d6182a0aaa2609cfcd0eb88446fdb8/pypdfium2-5.3.0-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:ea4f9db2d3575f22cd41f4c7a855240ded842f135e59a961b5b1351a65ce2b6e", size = 4997777, upload-time = "2026-01-05T16:28:53.589Z" }, - { url = "https://files.pythonhosted.org/packages/9d/38/cdd4ed085c264234a59ad32df1dfe432c77a7403da2381e0fcc1ba60b74e/pypdfium2-5.3.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0ea24409613df350223c6afc50911c99dca0d43ddaf2616c5a1ebdffa3e1bcb5", size = 4179895, upload-time = "2026-01-05T16:28:55.322Z" }, - { url = "https://files.pythonhosted.org/packages/93/4c/d2f40145c9012482699664f615d7ae540a346c84f68a8179449e69dcc4d8/pypdfium2-5.3.0-py3-none-win32.whl", hash = "sha256:5bf695d603f9eb8fdd7c1786add5cf420d57fbc81df142ed63c029ce29614df9", size = 2993570, upload-time = "2026-01-05T16:28:58.37Z" }, - { url = "https://files.pythonhosted.org/packages/2c/dc/1388ea650020c26ef3f68856b9227e7f153dcaf445e7e4674a0b8f26891e/pypdfium2-5.3.0-py3-none-win_amd64.whl", hash = "sha256:8365af22a39d4373c265f8e90e561cd64d4ddeaf5e6a66546a8caed216ab9574", size = 3102340, upload-time = "2026-01-05T16:28:59.933Z" }, - { url = "https://files.pythonhosted.org/packages/c8/71/a433668d33999b3aeb2c2dda18aaf24948e862ea2ee148078a35daac6c1c/pypdfium2-5.3.0-py3-none-win_arm64.whl", hash = "sha256:0b2c6bf825e084d91d34456be54921da31e9199d9530b05435d69d1a80501a12", size = 2940987, upload-time = "2026-01-05T16:29:01.511Z" }, +version = "5.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/99/23/b3979a1d4f536fabce02e3d9f332e8aeeed064d9df9391f2a77160f4ab36/pypdfium2-5.4.0.tar.gz", hash = "sha256:7219e55048fb3999fc8adcaea467088507207df4676ff9e521a3ae15a67d99c4", size = 269136, upload-time = "2026-02-08T16:54:08.383Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/c0/3d707bff5e973272b5412556d19e8c6889ce859a235465f0049cc8d35bc3/pypdfium2-5.4.0-py3-none-android_23_arm64_v8a.whl", hash = "sha256:8bc51a12a8c8eabbdbd7499d3e5ec47bcf56ba18e07b52bdd07d321cc1252c90", size = 2759769, upload-time = "2026-02-08T16:53:32.985Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6b/306cafcb0b18d5fab41687d9ed76eabea86a9ff78bc568bee1bfa34e526d/pypdfium2-5.4.0-py3-none-android_23_armeabi_v7a.whl", hash = "sha256:a414ef5b685824cc6c7acbe19b7dbc735de2023cf473321a8ebfe8d7f5d8a41f", size = 2301913, upload-time = "2026-02-08T16:53:35.026Z" }, + { url = "https://files.pythonhosted.org/packages/7a/37/3d737c7eb84fb22939ab0a643aa0183dbc0745c309e962b4d61eeff8211b/pypdfium2-5.4.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:0e83657db8da5971434ff5683bf3faa007ee1f3a56b61f245b8aa5b60442c23a", size = 2814181, upload-time = "2026-02-08T16:53:36.481Z" }, + { url = "https://files.pythonhosted.org/packages/96/d7/0895737ec3d95ad607ade42e98fa8868b91e35b1170ec39b8c1b5fdb124c/pypdfium2-5.4.0-py3-none-macosx_11_0_x86_64.whl", hash = "sha256:e42b1d14db642e96bb3a57167f620b4247e9c843d22b9fb569b16a7c35a18f47", size = 2943476, upload-time = "2026-02-08T16:53:37.992Z" }, + { url = "https://files.pythonhosted.org/packages/9a/53/f8ab449997d3efa52737b8e6c494f1c3f09dc0642161fadc934f16a57cf0/pypdfium2-5.4.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0698c9a002f839127e74ec0185147e08b64e47a1e6caeaee95df434c05b26e8c", size = 2976675, upload-time = "2026-02-08T16:53:39.923Z" }, + { url = "https://files.pythonhosted.org/packages/c6/28/b8a4d4c1557019101bb722c88ba532ec9c14640117ab1c272c80774d83d7/pypdfium2-5.4.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:22e9d4c73fc48b18b022977ea6fe78df43adf95440e1135020ed35fea9595017", size = 2762396, upload-time = "2026-02-08T16:53:41.958Z" }, + { url = "https://files.pythonhosted.org/packages/0b/4a/6c765f6e0b69d792e2d4c7ef2359301896c82df265d60f9a56e87618ec50/pypdfium2-5.4.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f0619f8a8ae3eb71b2cdc1fbd2a8f5d43f0fc6bee66d1b3aac2c9c23e44a3bf", size = 3068559, upload-time = "2026-02-08T16:53:43.974Z" }, + { url = "https://files.pythonhosted.org/packages/1c/17/4464e4ab6dd98ac3783c10eb799d8da49cb551a769c987eb9c6ba72a5ccf/pypdfium2-5.4.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50124415d815c41de8ce7e21cee5450f74f6f1240a140573bb71ccac804d5e5f", size = 3419384, upload-time = "2026-02-08T16:53:46.041Z" }, + { url = "https://files.pythonhosted.org/packages/92/08/fa315a2ab353b41501b7088be72dc6cf8ad2bd4f1ebdfdb90c41b7f29155/pypdfium2-5.4.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce482d76e5447e745d761307401eaa366616ca44032b86cf7fbe6be918ade64e", size = 2998123, upload-time = "2026-02-08T16:53:47.705Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/a171d313d54a028d9437dea2c5d07fc9e1592f4daf5c39cbf514fca75242/pypdfium2-5.4.0-py3-none-manylinux_2_27_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16b9c6b07f3dbe7eda209bf7aaf131ca9614e1dae527e9764180dd58bcbaf411", size = 3673594, upload-time = "2026-02-08T16:53:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/0b/c0/60416f011f7e5a4ca29f40ae94907f34975239f3c6dd7fcb51f99e110f3b/pypdfium2-5.4.0-py3-none-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3b08d48b7cca3b51aefaad7855bc0e9e251432a6eef1356d532ff438be84855e", size = 2965025, upload-time = "2026-02-08T16:53:50.553Z" }, + { url = "https://files.pythonhosted.org/packages/75/e2/8e36144b5e933c707b6aeab7dc6638eee8208697925b48b5b78ef68fb52a/pypdfium2-5.4.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0a1526e2a2bde7f2f13bec0f471d9fd475f7bbac2c0c860d48c35af8394d5931", size = 4130551, upload-time = "2026-02-08T16:53:52.71Z" }, + { url = "https://files.pythonhosted.org/packages/a0/64/8cda96259a8fdecd457f5d14a9d650315d7bdf496f96055d1d55900b3881/pypdfium2-5.4.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:40cea0bceb1e60a71b3855e2b04d175d2199b7da06212bb80f0c78067d065810", size = 3746587, upload-time = "2026-02-08T16:53:54.219Z" }, + { url = "https://files.pythonhosted.org/packages/33/6b/7764491269f188a922bd6b254359d718899fc3092c90f0f68c2f6e451921/pypdfium2-5.4.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7a116f8fbeae7aa3a18ff2d1fa331ac647831cc16b589d4fbbbb66d64ecc8793", size = 4336703, upload-time = "2026-02-08T16:53:56.18Z" }, + { url = "https://files.pythonhosted.org/packages/87/b0/2484bd3c20ead51ecea2082deaf94a3e91bad709fa14f049ca7fb598dc9a/pypdfium2-5.4.0-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:55c7fc894718db5fa2981d46dee45fe3a4fcd60d26f5095ad8f7779600fa8b6f", size = 4375051, upload-time = "2026-02-08T16:53:57.804Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ac/5f0536be885c3cadc09422de0324a193a21c165488a574029d9d2db92ecb/pypdfium2-5.4.0-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:dfc1c0c7e6e7ba258ebb338aaf664eb933bff1854cda76e4ee530886ea39b31a", size = 3928935, upload-time = "2026-02-08T16:53:59.265Z" }, + { url = "https://files.pythonhosted.org/packages/13/b9/693b665df0939555491bece0777cafda1270e208734e925006de313abb5b/pypdfium2-5.4.0-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:4c0a48ede7180f804c029c509c2b6ea0c66813a3fde9eb9afc390183f947164d", size = 4997642, upload-time = "2026-02-08T16:54:00.809Z" }, + { url = "https://files.pythonhosted.org/packages/fb/ea/ba585acdfbefe309ee2fe5ebfeb097e36abe1d33c2a5108828c493c070bb/pypdfium2-5.4.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:dea22d15c44a275702fd95ad664ba6eaa3c493d53d58b4d69272a04bdfb0df70", size = 4179914, upload-time = "2026-02-08T16:54:02.264Z" }, + { url = "https://files.pythonhosted.org/packages/97/47/238383e89081a0ed1ca2bf4ef44f7e512fa0c72ffc51adc7df83bfcfd9b9/pypdfium2-5.4.0-py3-none-win32.whl", hash = "sha256:35c643827ed0f4dae9cedf3caf836f94cba5b31bd2c115b80a7c85f004636de9", size = 2995844, upload-time = "2026-02-08T16:54:03.692Z" }, + { url = "https://files.pythonhosted.org/packages/08/37/f1338a0600c6c6e31759f8f80d7ab20aa0bc43b11594da67091300e051d4/pypdfium2-5.4.0-py3-none-win_amd64.whl", hash = "sha256:f9d9ce3c6901294d6984004d4a797dea110f8248b1bde33a823d25b45d3c2685", size = 3104198, upload-time = "2026-02-08T16:54:05.304Z" }, + { url = "https://files.pythonhosted.org/packages/65/17/18ad82f070da18ab970928f730fbd44d9b05aafcb52a2ebb6470eaae53f9/pypdfium2-5.4.0-py3-none-win_arm64.whl", hash = "sha256:2b78ea216fb92e7709b61c46241ebf2cc0c60cf18ad2fb4633af665d7b4e21e6", size = 2938727, upload-time = "2026-02-08T16:54:06.814Z" }, ] [[package]] @@ -2640,28 +2644,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.14.14" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2e/06/f71e3a86b2df0dfa2d2f72195941cd09b44f87711cb7fa5193732cb9a5fc/ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b", size = 4515732, upload-time = "2026-01-22T22:30:17.527Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/89/20a12e97bc6b9f9f68343952da08a8099c57237aef953a56b82711d55edd/ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed", size = 10467650, upload-time = "2026-01-22T22:30:08.578Z" }, - { url = "https://files.pythonhosted.org/packages/a3/b1/c5de3fd2d5a831fcae21beda5e3589c0ba67eec8202e992388e4b17a6040/ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c", size = 10883245, upload-time = "2026-01-22T22:30:04.155Z" }, - { url = "https://files.pythonhosted.org/packages/b8/7c/3c1db59a10e7490f8f6f8559d1db8636cbb13dccebf18686f4e3c9d7c772/ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de", size = 10231273, upload-time = "2026-01-22T22:30:34.642Z" }, - { url = "https://files.pythonhosted.org/packages/a1/6e/5e0e0d9674be0f8581d1f5e0f0a04761203affce3232c1a1189d0e3b4dad/ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e", size = 10585753, upload-time = "2026-01-22T22:30:31.781Z" }, - { url = "https://files.pythonhosted.org/packages/23/09/754ab09f46ff1884d422dc26d59ba18b4e5d355be147721bb2518aa2a014/ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8", size = 10286052, upload-time = "2026-01-22T22:30:24.827Z" }, - { url = "https://files.pythonhosted.org/packages/c8/cc/e71f88dd2a12afb5f50733851729d6b571a7c3a35bfdb16c3035132675a0/ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906", size = 11043637, upload-time = "2026-01-22T22:30:13.239Z" }, - { url = "https://files.pythonhosted.org/packages/67/b2/397245026352494497dac935d7f00f1468c03a23a0c5db6ad8fc49ca3fb2/ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480", size = 12194761, upload-time = "2026-01-22T22:30:22.542Z" }, - { url = "https://files.pythonhosted.org/packages/5b/06/06ef271459f778323112c51b7587ce85230785cd64e91772034ddb88f200/ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df", size = 12005701, upload-time = "2026-01-22T22:30:20.499Z" }, - { url = "https://files.pythonhosted.org/packages/41/d6/99364514541cf811ccc5ac44362f88df66373e9fec1b9d1c4cc830593fe7/ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b", size = 11282455, upload-time = "2026-01-22T22:29:59.679Z" }, - { url = "https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974", size = 11215882, upload-time = "2026-01-22T22:29:57.032Z" }, - { url = "https://files.pythonhosted.org/packages/2c/10/a31f86169ec91c0705e618443ee74ede0bdd94da0a57b28e72db68b2dbac/ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66", size = 11180549, upload-time = "2026-01-22T22:30:27.175Z" }, - { url = "https://files.pythonhosted.org/packages/fd/1e/c723f20536b5163adf79bdd10c5f093414293cdf567eed9bdb7b83940f3f/ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13", size = 10543416, upload-time = "2026-01-22T22:30:01.964Z" }, - { url = "https://files.pythonhosted.org/packages/3e/34/8a84cea7e42c2d94ba5bde1d7a4fae164d6318f13f933d92da6d7c2041ff/ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412", size = 10285491, upload-time = "2026-01-22T22:30:29.51Z" }, - { url = "https://files.pythonhosted.org/packages/55/ef/b7c5ea0be82518906c978e365e56a77f8de7678c8bb6651ccfbdc178c29f/ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3", size = 10733525, upload-time = "2026-01-22T22:30:06.499Z" }, - { url = "https://files.pythonhosted.org/packages/6a/5b/aaf1dfbcc53a2811f6cc0a1759de24e4b03e02ba8762daabd9b6bd8c59e3/ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b", size = 11315626, upload-time = "2026-01-22T22:30:36.848Z" }, - { url = "https://files.pythonhosted.org/packages/2c/aa/9f89c719c467dfaf8ad799b9bae0df494513fb21d31a6059cb5870e57e74/ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167", size = 10502442, upload-time = "2026-01-22T22:30:38.93Z" }, - { url = "https://files.pythonhosted.org/packages/87/44/90fa543014c45560cae1fffc63ea059fb3575ee6e1cb654562197e5d16fb/ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd", size = 11630486, upload-time = "2026-01-22T22:30:10.852Z" }, - { url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" }, +version = "0.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c8/39/5cee96809fbca590abea6b46c6d1c586b49663d1d2830a751cc8fc42c666/ruff-0.15.0.tar.gz", hash = "sha256:6bdea47cdbea30d40f8f8d7d69c0854ba7c15420ec75a26f463290949d7f7e9a", size = 4524893, upload-time = "2026-02-03T17:53:35.357Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/88/3fd1b0aa4b6330d6aaa63a285bc96c9f71970351579152d231ed90914586/ruff-0.15.0-py3-none-linux_armv6l.whl", hash = "sha256:aac4ebaa612a82b23d45964586f24ae9bc23ca101919f5590bdb368d74ad5455", size = 10354332, upload-time = "2026-02-03T17:52:54.892Z" }, + { url = "https://files.pythonhosted.org/packages/72/f6/62e173fbb7eb75cc29fe2576a1e20f0a46f671a2587b5f604bfb0eaf5f6f/ruff-0.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dcd4be7cc75cfbbca24a98d04d0b9b36a270d0833241f776b788d59f4142b14d", size = 10767189, upload-time = "2026-02-03T17:53:19.778Z" }, + { url = "https://files.pythonhosted.org/packages/99/e4/968ae17b676d1d2ff101d56dc69cf333e3a4c985e1ec23803df84fc7bf9e/ruff-0.15.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d747e3319b2bce179c7c1eaad3d884dc0a199b5f4d5187620530adf9105268ce", size = 10075384, upload-time = "2026-02-03T17:53:29.241Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bf/9843c6044ab9e20af879c751487e61333ca79a2c8c3058b15722386b8cae/ruff-0.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:650bd9c56ae03102c51a5e4b554d74d825ff3abe4db22b90fd32d816c2e90621", size = 10481363, upload-time = "2026-02-03T17:52:43.332Z" }, + { url = "https://files.pythonhosted.org/packages/55/d9/4ada5ccf4cd1f532db1c8d44b6f664f2208d3d93acbeec18f82315e15193/ruff-0.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6664b7eac559e3048223a2da77769c2f92b43a6dfd4720cef42654299a599c9", size = 10187736, upload-time = "2026-02-03T17:53:00.522Z" }, + { url = "https://files.pythonhosted.org/packages/86/e2/f25eaecd446af7bb132af0a1d5b135a62971a41f5366ff41d06d25e77a91/ruff-0.15.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f811f97b0f092b35320d1556f3353bf238763420ade5d9e62ebd2b73f2ff179", size = 10968415, upload-time = "2026-02-03T17:53:15.705Z" }, + { url = "https://files.pythonhosted.org/packages/e7/dc/f06a8558d06333bf79b497d29a50c3a673d9251214e0d7ec78f90b30aa79/ruff-0.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761ec0a66680fab6454236635a39abaf14198818c8cdf691e036f4bc0f406b2d", size = 11809643, upload-time = "2026-02-03T17:53:23.031Z" }, + { url = "https://files.pythonhosted.org/packages/dd/45/0ece8db2c474ad7df13af3a6d50f76e22a09d078af63078f005057ca59eb/ruff-0.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:940f11c2604d317e797b289f4f9f3fa5555ffe4fb574b55ed006c3d9b6f0eb78", size = 11234787, upload-time = "2026-02-03T17:52:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/8a/d9/0e3a81467a120fd265658d127db648e4d3acfe3e4f6f5d4ea79fac47e587/ruff-0.15.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcbca3d40558789126da91d7ef9a7c87772ee107033db7191edefa34e2c7f1b4", size = 11112797, upload-time = "2026-02-03T17:52:49.274Z" }, + { url = "https://files.pythonhosted.org/packages/b2/cb/8c0b3b0c692683f8ff31351dfb6241047fa873a4481a76df4335a8bff716/ruff-0.15.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9a121a96db1d75fa3eb39c4539e607f628920dd72ff1f7c5ee4f1b768ac62d6e", size = 11033133, upload-time = "2026-02-03T17:53:33.105Z" }, + { url = "https://files.pythonhosted.org/packages/f8/5e/23b87370cf0f9081a8c89a753e69a4e8778805b8802ccfe175cc410e50b9/ruff-0.15.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5298d518e493061f2eabd4abd067c7e4fb89e2f63291c94332e35631c07c3662", size = 10442646, upload-time = "2026-02-03T17:53:06.278Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9a/3c94de5ce642830167e6d00b5c75aacd73e6347b4c7fc6828699b150a5ee/ruff-0.15.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afb6e603d6375ff0d6b0cee563fa21ab570fd15e65c852cb24922cef25050cf1", size = 10195750, upload-time = "2026-02-03T17:53:26.084Z" }, + { url = "https://files.pythonhosted.org/packages/30/15/e396325080d600b436acc970848d69df9c13977942fb62bb8722d729bee8/ruff-0.15.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:77e515f6b15f828b94dc17d2b4ace334c9ddb7d9468c54b2f9ed2b9c1593ef16", size = 10676120, upload-time = "2026-02-03T17:53:09.363Z" }, + { url = "https://files.pythonhosted.org/packages/8d/c9/229a23d52a2983de1ad0fb0ee37d36e0257e6f28bfd6b498ee2c76361874/ruff-0.15.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6f6e80850a01eb13b3e42ee0ebdf6e4497151b48c35051aab51c101266d187a3", size = 11201636, upload-time = "2026-02-03T17:52:57.281Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b0/69adf22f4e24f3677208adb715c578266842e6e6a3cc77483f48dd999ede/ruff-0.15.0-py3-none-win32.whl", hash = "sha256:238a717ef803e501b6d51e0bdd0d2c6e8513fe9eec14002445134d3907cd46c3", size = 10465945, upload-time = "2026-02-03T17:53:12.591Z" }, + { url = "https://files.pythonhosted.org/packages/51/ad/f813b6e2c97e9b4598be25e94a9147b9af7e60523b0cb5d94d307c15229d/ruff-0.15.0-py3-none-win_amd64.whl", hash = "sha256:dd5e4d3301dc01de614da3cdffc33d4b1b96fb89e45721f1598e5532ccf78b18", size = 11564657, upload-time = "2026-02-03T17:52:51.893Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b0/2d823f6e77ebe560f4e397d078487e8d52c1516b331e3521bc75db4272ca/ruff-0.15.0-py3-none-win_arm64.whl", hash = "sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a", size = 10865753, upload-time = "2026-02-03T17:53:03.014Z" }, ] [[package]] @@ -2781,11 +2784,11 @@ wheels = [ [[package]] name = "setuptools" -version = "80.10.2" +version = "82.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/95/faf61eb8363f26aa7e1d762267a8d602a1b26d4f3a1e758e92cb3cb8b054/setuptools-80.10.2.tar.gz", hash = "sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70", size = 1200343, upload-time = "2026-01-25T22:38:17.252Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/f3/748f4d6f65d1756b9ae577f329c951cda23fb900e4de9f70900ced962085/setuptools-82.0.0.tar.gz", hash = "sha256:22e0a2d69474c6ae4feb01951cb69d515ed23728cf96d05513d36e42b62b37cb", size = 1144893, upload-time = "2026-02-08T15:08:40.206Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/b8/f1f62a5e3c0ad2ff1d189590bfa4c46b4f3b6e49cef6f26c6ee4e575394d/setuptools-80.10.2-py3-none-any.whl", hash = "sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173", size = 1064234, upload-time = "2026-01-25T22:38:15.216Z" }, + { url = "https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl", hash = "sha256:70b18734b607bd1da571d097d236cfcfacaf01de45717d59e6e04b96877532e0", size = 1003468, upload-time = "2026-02-08T15:08:38.723Z" }, ] [[package]] @@ -2962,11 +2965,11 @@ wheels = [ [[package]] name = "tenacity" -version = "9.1.2" +version = "9.1.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036, upload-time = "2025-04-02T08:25:09.966Z" } +sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, + { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, ] [[package]] @@ -3109,14 +3112,14 @@ wheels = [ [[package]] name = "tqdm" -version = "4.67.2" +version = "4.67.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/27/89/4b0001b2dab8df0a5ee2787dcbe771de75ded01f18f1f8d53dedeea2882b/tqdm-4.67.2.tar.gz", hash = "sha256:649aac53964b2cb8dec76a14b405a4c0d13612cb8933aae547dd144eacc99653", size = 169514, upload-time = "2026-01-30T23:12:06.555Z" } +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/e2/31eac96de2915cf20ccaed0225035db149dfb9165a9ed28d4b252ef3f7f7/tqdm-4.67.2-py3-none-any.whl", hash = "sha256:9a12abcbbff58b6036b2167d9d3853042b9d436fe7330f06ae047867f2f8e0a7", size = 78354, upload-time = "2026-01-30T23:12:04.368Z" }, + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, ] [[package]]