vecshift 0.2.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {vecshift-0.2.0 → vecshift-0.3.0}/CHANGELOG.md +14 -1
- {vecshift-0.2.0 → vecshift-0.3.0}/PKG-INFO +3 -1
- {vecshift-0.2.0 → vecshift-0.3.0}/pyproject.toml +2 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/__init__.py +1 -1
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/cli.py +36 -2
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/cli_apply.py +158 -2
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/cli_bench.py +65 -10
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/cli_cutover.py +47 -26
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/cli_eval.py +166 -3
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/cli_init.py +84 -99
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/cli_plan.py +83 -26
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/cli_status.py +102 -1
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/cli_style.py +5 -29
- vecshift-0.3.0/src/vecshift/ui.py +432 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/tests/test_security.py +8 -3
- vecshift-0.3.0/tests/test_ui.py +139 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/.github/workflows/ci.yml +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/.github/workflows/release.yml +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/.gitignore +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/Dockerfile +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/LICENSE +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/NOTICE +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/README.md +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/RELEASING.md +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/SECURITY.md +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/demo/compose.yaml +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/demo/out/.gitignore +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/demo/run.sh +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/demo/seed.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/docs/architecture.md +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/docs/bench.md +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/docs/connectors/pgvector.md +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/docs/demo.md +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/docs/eval.md +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/docs/images/doctor-report.png +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/docs/images/eval-report.png +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/docs/images/logo/vecshift-dark.svg +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/docs/images/logo/vecshift-light.svg +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/docs/images/logo/vecshift-mark-dark.svg +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/docs/images/logo/vecshift-mark-light.svg +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/docs/images/logo/vecshift-mark.svg +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/docs/images/social-preview.png +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/docs/migrations.md +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/docs/prior-art.md +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/docs/roadmap.md +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/docs/security.md +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/scripts/release_notes.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/assets/bench.css +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/assets/eval.css +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/assets/report.css +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/assets/report.js +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/bench/__init__.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/bench/corpus.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/bench/generate.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/bench/html.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/bench/metrics.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/bench/runner.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/connectors/__init__.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/connectors/pgvector/__init__.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/connectors/pgvector/connection.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/connectors/pgvector/documents.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/connectors/pgvector/inspect.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/connectors/pgvector/search.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/connectors/pgvector/switch.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/connectors/pgvector/target.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/connectors/pgvector/writer.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/core/__init__.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/core/capabilities.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/core/contracts.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/core/fingerprint.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/core/record.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/doctor/__init__.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/doctor/checks.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/doctor/findings.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/doctor/html.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/doctor/profile.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/embeddings/__init__.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/embeddings/cache.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/embeddings/providers.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/embeddings/spec.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/envfile.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/eval/__init__.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/eval/html.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/eval/metrics.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/eval/queries.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/eval/runner.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/html_kit.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/jobs/__init__.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/jobs/spec.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/migrate/__init__.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/migrate/engine.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/migrate/state.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/planning/__init__.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/planning/plan.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/planning/planner.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/src/vecshift/py.typed +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/tests/__init__.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/tests/integration/__init__.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/tests/integration/conftest.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/tests/integration/test_pgvector_apply.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/tests/integration/test_pgvector_bench.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/tests/integration/test_pgvector_cutover.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/tests/integration/test_pgvector_doctor.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/tests/integration/test_pgvector_eval.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/tests/integration/test_pgvector_init_status.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/tests/integration/test_pgvector_plan.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/tests/test_bench.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/tests/test_cli.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/tests/test_contracts.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/tests/test_doctor_checks.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/tests/test_doctor_html.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/tests/test_embeddings.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/tests/test_envfile.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/tests/test_eval.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/tests/test_eval_html.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/tests/test_fingerprint.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/tests/test_jobs.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/tests/test_migrate.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/tests/test_pgvector_connection.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/tests/test_planner.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/tests/test_record.py +0 -0
- {vecshift-0.2.0 → vecshift-0.3.0}/tests/test_release.py +0 -0
|
@@ -7,6 +7,18 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0] - 2026-10-10
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- A new look at the terminal, across every command: a gradient block wordmark, numbered
|
|
15
|
+
steps and arrow-key menus in `vecshift init` (with hidden input for secrets), a panel
|
|
16
|
+
with the migration's progress in `vecshift status`, a live progress bar in `apply`,
|
|
17
|
+
highlighted SQL in `plan`, tables in `eval` and `bench`, and consistent headers,
|
|
18
|
+
findings, verdicts, and next steps everywhere. Off a terminal, output stays plain and
|
|
19
|
+
prompts fall back to typed answers, as before.
|
|
20
|
+
- New dependency: `questionary`, for the arrow-key menus.
|
|
21
|
+
|
|
10
22
|
## [0.2.0] - 2026-10-10
|
|
11
23
|
|
|
12
24
|
### Added
|
|
@@ -149,6 +161,7 @@ The first release.
|
|
|
149
161
|
- `Capability` flags and plugin contracts for sources, targets, and embedding providers.
|
|
150
162
|
- `vecshift fingerprint` and `vecshift --version` commands.
|
|
151
163
|
|
|
152
|
-
[Unreleased]: https://github.com/Osamamu64/vecshift/compare/v0.
|
|
164
|
+
[Unreleased]: https://github.com/Osamamu64/vecshift/compare/v0.3.0...HEAD
|
|
165
|
+
[0.3.0]: https://github.com/Osamamu64/vecshift/compare/v0.2.0...v0.3.0
|
|
153
166
|
[0.2.0]: https://github.com/Osamamu64/vecshift/compare/v0.1.0...v0.2.0
|
|
154
167
|
[0.1.0]: https://github.com/Osamamu64/vecshift/releases/tag/v0.1.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: vecshift
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Zero-downtime embedding model migrations for pgvector and Supabase: plan, re-embed, evaluate, and cut over safely.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Osamamu64/vecshift
|
|
6
6
|
Project-URL: Repository, https://github.com/Osamamu64/vecshift
|
|
@@ -31,6 +31,8 @@ Requires-Dist: numpy>=1.26
|
|
|
31
31
|
Requires-Dist: psycopg[binary]>=3.2
|
|
32
32
|
Requires-Dist: pydantic>=2.6
|
|
33
33
|
Requires-Dist: pyyaml>=6
|
|
34
|
+
Requires-Dist: questionary>=2.0
|
|
35
|
+
Requires-Dist: rich>=13
|
|
34
36
|
Requires-Dist: typer>=0.12
|
|
35
37
|
Description-Content-Type: text/markdown
|
|
36
38
|
|
|
@@ -11,7 +11,7 @@ import typer
|
|
|
11
11
|
|
|
12
12
|
from vecshift import __version__
|
|
13
13
|
from vecshift.cli_style import STYLE as _STYLE
|
|
14
|
-
from vecshift.cli_style import
|
|
14
|
+
from vecshift.cli_style import warn_if_password_on_command_line
|
|
15
15
|
from vecshift.cli_style import wrap as _wrap
|
|
16
16
|
from vecshift.core.fingerprint import EmbeddingFingerprint
|
|
17
17
|
from vecshift.doctor import Report, Severity, run_checks
|
|
@@ -80,7 +80,9 @@ def main(
|
|
|
80
80
|
"""Safe, observable embedding migrations for any vector store."""
|
|
81
81
|
_load_env_file(None if no_env_file else env_file)
|
|
82
82
|
if ctx.invoked_subcommand is None:
|
|
83
|
-
|
|
83
|
+
from vecshift import ui
|
|
84
|
+
|
|
85
|
+
ui.banner(__version__, TAGLINE)
|
|
84
86
|
typer.echo(ctx.get_help())
|
|
85
87
|
|
|
86
88
|
|
|
@@ -119,7 +121,39 @@ class FailOn(StrEnum):
|
|
|
119
121
|
ERROR = "error"
|
|
120
122
|
|
|
121
123
|
|
|
124
|
+
def _render_fancy(report: Report, connection: str) -> None:
|
|
125
|
+
from rich.text import Text
|
|
126
|
+
|
|
127
|
+
from vecshift import ui
|
|
128
|
+
|
|
129
|
+
dims = f"({report.declared_dimensions})" if report.declared_dimensions else ""
|
|
130
|
+
rows = f"~{report.estimated_rows:,}" if report.estimated_rows is not None else "unknown"
|
|
131
|
+
ui.title("doctor", f"{report.store} via {connection}")
|
|
132
|
+
ui.kv(
|
|
133
|
+
[
|
|
134
|
+
("Target", Text.assemble(report.target, (f" {report.vector_type}{dims}", "dim")), ""),
|
|
135
|
+
("Rows", rows, f"inspected {report.sample_rows:,}, {report.sample_method}"),
|
|
136
|
+
]
|
|
137
|
+
)
|
|
138
|
+
ui.section("Findings")
|
|
139
|
+
ui.findings(report.sorted_findings())
|
|
140
|
+
summary = Text(" ")
|
|
141
|
+
for severity, (_, _, name) in _STYLE.items():
|
|
142
|
+
n = report.count(severity)
|
|
143
|
+
icon, colour, _ = ui.SEVERITY_STYLE[severity.value]
|
|
144
|
+
plural = "s" if n != 1 and name in ("error", "warning") else ""
|
|
145
|
+
summary.append(f"{icon} {n} {name}{plural} ", style=colour if n else "dim")
|
|
146
|
+
ui.console.print()
|
|
147
|
+
ui.console.print(summary)
|
|
148
|
+
ui.console.print()
|
|
149
|
+
|
|
150
|
+
|
|
122
151
|
def _render(report: Report, connection: str) -> None:
|
|
152
|
+
from vecshift import ui
|
|
153
|
+
|
|
154
|
+
if ui.fancy():
|
|
155
|
+
_render_fancy(report, connection)
|
|
156
|
+
return
|
|
123
157
|
dims = f"({report.declared_dimensions})" if report.declared_dimensions else ""
|
|
124
158
|
rows = f"~{report.estimated_rows:,}" if report.estimated_rows is not None else "unknown"
|
|
125
159
|
typer.secho(f"vecshift doctor · {report.store} via {connection}", bold=True)
|
|
@@ -83,6 +83,82 @@ class _Progress:
|
|
|
83
83
|
self.last_print = time.monotonic()
|
|
84
84
|
|
|
85
85
|
|
|
86
|
+
class _FancyProgress:
|
|
87
|
+
"""A live progress bar and spinners, for a person at a colour terminal."""
|
|
88
|
+
|
|
89
|
+
def __init__(self) -> None:
|
|
90
|
+
from rich.progress import (
|
|
91
|
+
BarColumn,
|
|
92
|
+
MofNCompleteColumn,
|
|
93
|
+
Progress,
|
|
94
|
+
SpinnerColumn,
|
|
95
|
+
TextColumn,
|
|
96
|
+
TimeRemainingColumn,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
from vecshift import ui
|
|
100
|
+
|
|
101
|
+
self.ui = ui
|
|
102
|
+
self.started = time.monotonic()
|
|
103
|
+
self.progress = Progress(
|
|
104
|
+
SpinnerColumn(style=ui.ACCENT),
|
|
105
|
+
TextColumn("{task.description}"),
|
|
106
|
+
BarColumn(complete_style=ui.ACCENT, finished_style="green", bar_width=32),
|
|
107
|
+
MofNCompleteColumn(),
|
|
108
|
+
TextColumn("[dim]{task.fields[rate]}"),
|
|
109
|
+
TextColumn("[dim]{task.fields[cost]}"),
|
|
110
|
+
TimeRemainingColumn(compact=True),
|
|
111
|
+
console=ui.console,
|
|
112
|
+
transient=False,
|
|
113
|
+
)
|
|
114
|
+
self.task: Any = None
|
|
115
|
+
self.status: Any = None
|
|
116
|
+
self.done = 0
|
|
117
|
+
|
|
118
|
+
def __call__(self, event: Event) -> None:
|
|
119
|
+
ui, d = self.ui, event.data
|
|
120
|
+
if event.kind == "column" and d["added"]:
|
|
121
|
+
ui.success("Added the new column")
|
|
122
|
+
elif event.kind == "trigger" and d["added"]:
|
|
123
|
+
ui.success("Added the sync trigger")
|
|
124
|
+
elif event.kind == "start":
|
|
125
|
+
ui.note(f" {d['pending']:,} rows need a new vector")
|
|
126
|
+
self.progress.start()
|
|
127
|
+
self.task = self.progress.add_task("Embedding", total=d["pending"], rate="", cost="")
|
|
128
|
+
elif event.kind == "batch" and self.task is not None:
|
|
129
|
+
self.done = d["rows_written"]
|
|
130
|
+
rate = self.done / max(time.monotonic() - self.started, 1e-6)
|
|
131
|
+
cost = _money(d["spent_usd"]) if d["spent_usd"] is not None else ""
|
|
132
|
+
self.progress.update(
|
|
133
|
+
self.task, completed=self.done, rate=f"{rate:,.0f} rows/s", cost=cost
|
|
134
|
+
)
|
|
135
|
+
elif event.kind == "pass" and d["number"] > 1 and self.task is not None:
|
|
136
|
+
self.progress.update(
|
|
137
|
+
self.task, description="Catching up", total=self.done + d["remaining"]
|
|
138
|
+
)
|
|
139
|
+
elif event.kind == "index":
|
|
140
|
+
self.close()
|
|
141
|
+
if d["state"] == "building":
|
|
142
|
+
self.status = ui.console.status(
|
|
143
|
+
f"Building the {d['method']} index concurrently. Writes continue…",
|
|
144
|
+
spinner="dots",
|
|
145
|
+
spinner_style=ui.ACCENT,
|
|
146
|
+
)
|
|
147
|
+
self.status.start()
|
|
148
|
+
else:
|
|
149
|
+
ui.success(f"Index {d['state']}")
|
|
150
|
+
elif event.kind == "done":
|
|
151
|
+
self.close()
|
|
152
|
+
|
|
153
|
+
def close(self) -> None:
|
|
154
|
+
if self.task is not None:
|
|
155
|
+
self.progress.stop()
|
|
156
|
+
self.task = None
|
|
157
|
+
if self.status is not None:
|
|
158
|
+
self.status.stop()
|
|
159
|
+
self.status = None
|
|
160
|
+
|
|
161
|
+
|
|
86
162
|
def _eta(seconds: float) -> str:
|
|
87
163
|
if seconds < 90:
|
|
88
164
|
return f"{max(1, round(seconds))} s"
|
|
@@ -91,7 +167,44 @@ def _eta(seconds: float) -> str:
|
|
|
91
167
|
return f"{seconds / 3600:.1f} h"
|
|
92
168
|
|
|
93
169
|
|
|
170
|
+
def _summary_fancy(result: ApplyResult) -> None:
|
|
171
|
+
from vecshift import ui
|
|
172
|
+
|
|
173
|
+
kind = {"complete": "ok", "failed": "error"}.get(result.status, "warn")
|
|
174
|
+
ui.verdict(f"Apply {result.status}.", kind)
|
|
175
|
+
if result.message:
|
|
176
|
+
ui.detail(result.message)
|
|
177
|
+
spent = _money(result.spent_usd) if result.spent_usd is not None else "unknown"
|
|
178
|
+
total = _money(result.total_spent_usd) if result.total_spent_usd is not None else ""
|
|
179
|
+
ui.console.print()
|
|
180
|
+
ui.kv(
|
|
181
|
+
[
|
|
182
|
+
("Rows written", f"{result.rows_written:,}", ""),
|
|
183
|
+
("Tokens", f"{result.tokens:,}", ""),
|
|
184
|
+
("Spent", spent, f"{total} across runs" if total else ""),
|
|
185
|
+
("Time", f"{result.seconds:,.0f} s", ""),
|
|
186
|
+
]
|
|
187
|
+
+ (
|
|
188
|
+
[("Rejected", f"{result.rows_failed:,} rows", "retried on the next run")]
|
|
189
|
+
if result.rows_failed
|
|
190
|
+
else []
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
if result.status == "complete":
|
|
194
|
+
ui.next_step(
|
|
195
|
+
"check it's safe to switch with vecshift cutover --check, then run vecshift "
|
|
196
|
+
"cutover. It embeds rows added or edited since this run first."
|
|
197
|
+
)
|
|
198
|
+
elif result.status in {"stopped", "budget"}:
|
|
199
|
+
ui.next_step("run vecshift apply again to continue where it stopped.")
|
|
200
|
+
|
|
201
|
+
|
|
94
202
|
def _summary(result: ApplyResult) -> None:
|
|
203
|
+
from vecshift import ui
|
|
204
|
+
|
|
205
|
+
if ui.fancy():
|
|
206
|
+
_summary_fancy(result)
|
|
207
|
+
return
|
|
95
208
|
color = {"complete": typer.colors.GREEN, "failed": typer.colors.RED}.get(
|
|
96
209
|
result.status, typer.colors.YELLOW
|
|
97
210
|
)
|
|
@@ -169,7 +282,39 @@ def apply(
|
|
|
169
282
|
state = JobState.for_job(job_file, job.name)
|
|
170
283
|
est = plan.estimates
|
|
171
284
|
warnings = [f for f in plan.findings if f.severity.rank == 2]
|
|
172
|
-
|
|
285
|
+
from vecshift import ui
|
|
286
|
+
|
|
287
|
+
fancy = ui.fancy() and not output_json
|
|
288
|
+
if fancy:
|
|
289
|
+
from rich.text import Text
|
|
290
|
+
|
|
291
|
+
from vecshift.cli_plan import CHANGE_MARKS
|
|
292
|
+
|
|
293
|
+
ui.title("apply", job.name)
|
|
294
|
+
for change in plan.changes:
|
|
295
|
+
if change.kind != "cutover":
|
|
296
|
+
mark, colour = CHANGE_MARKS.get(change.kind, ("·", "dim"))
|
|
297
|
+
ui.console.print(Text.assemble((f" {mark} ", f"bold {colour}"), change.summary))
|
|
298
|
+
ui.console.print()
|
|
299
|
+
rows: list[tuple[str, str, str]] = [
|
|
300
|
+
("Estimated cost", _money(est.cost_usd), ""),
|
|
301
|
+
("New column", _size(est.new_bytes), ""),
|
|
302
|
+
]
|
|
303
|
+
if job.limits.budget_usd:
|
|
304
|
+
rows.append(("Budget", f"${job.limits.budget_usd:,.2f}", "apply stops there"))
|
|
305
|
+
if state.runs:
|
|
306
|
+
rows.append(
|
|
307
|
+
(
|
|
308
|
+
"Resuming",
|
|
309
|
+
f"{state.rows_written:,} rows",
|
|
310
|
+
f"written in earlier runs, {_money(state.spent_usd)} spent",
|
|
311
|
+
)
|
|
312
|
+
)
|
|
313
|
+
ui.kv(rows)
|
|
314
|
+
if warnings:
|
|
315
|
+
ui.console.print()
|
|
316
|
+
ui.findings(warnings)
|
|
317
|
+
elif not output_json:
|
|
173
318
|
typer.secho(f"vecshift apply · {job.name}", bold=True)
|
|
174
319
|
for change in plan.changes:
|
|
175
320
|
if change.kind != "cutover":
|
|
@@ -184,7 +329,14 @@ def apply(
|
|
|
184
329
|
)
|
|
185
330
|
if warnings:
|
|
186
331
|
finding_lines(warnings)
|
|
187
|
-
if not yes:
|
|
332
|
+
if not yes and fancy:
|
|
333
|
+
where = "your machine" if spec.is_local else spec.url
|
|
334
|
+
ui.console.print()
|
|
335
|
+
ui.note(f" This changes {plan.source.split()[0]} and sends row text to {where}.")
|
|
336
|
+
if not ui.confirm("Apply?", default=False):
|
|
337
|
+
raise typer.Exit(1)
|
|
338
|
+
ui.console.print()
|
|
339
|
+
elif not yes:
|
|
188
340
|
where = "your machine" if spec.is_local else spec.url
|
|
189
341
|
typer.echo(
|
|
190
342
|
f"\nThis changes {plan.source.split()[0]} and sends row text to {where}.",
|
|
@@ -221,6 +373,8 @@ def apply(
|
|
|
221
373
|
def on_event(event: Event) -> None:
|
|
222
374
|
typer.echo(json.dumps({"event": event.kind, **event.data}))
|
|
223
375
|
|
|
376
|
+
elif fancy:
|
|
377
|
+
on_event = _FancyProgress()
|
|
224
378
|
else:
|
|
225
379
|
on_event = _Progress(live=sys.stdout.isatty())
|
|
226
380
|
try:
|
|
@@ -255,6 +409,8 @@ def apply(
|
|
|
255
409
|
)
|
|
256
410
|
raise _fail(f"The database reported an error: {detail}", hint) from exc
|
|
257
411
|
finally:
|
|
412
|
+
if isinstance(on_event, _FancyProgress):
|
|
413
|
+
on_event.close()
|
|
258
414
|
signal.signal(signal.SIGINT, previous)
|
|
259
415
|
conn.close()
|
|
260
416
|
|
|
@@ -6,7 +6,7 @@ import asyncio
|
|
|
6
6
|
import json
|
|
7
7
|
import sys
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from typing import TYPE_CHECKING, Annotated
|
|
9
|
+
from typing import TYPE_CHECKING, Annotated, Any
|
|
10
10
|
|
|
11
11
|
import typer
|
|
12
12
|
|
|
@@ -28,7 +28,12 @@ def _fail(message: str, hint: str | None = None) -> typer.Exit:
|
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
def _note(message: str) -> None:
|
|
31
|
-
|
|
31
|
+
from vecshift import ui
|
|
32
|
+
|
|
33
|
+
if ui.fancy():
|
|
34
|
+
ui.note(f" · {message}")
|
|
35
|
+
else:
|
|
36
|
+
typer.secho(message, err=True, dim=True)
|
|
32
37
|
|
|
33
38
|
|
|
34
39
|
def money(value: float | None) -> str:
|
|
@@ -48,8 +53,30 @@ def _ms(value: float | None) -> str:
|
|
|
48
53
|
|
|
49
54
|
|
|
50
55
|
def _confirm(items: list[PlanItem], generator: ModelSpec | None, n_docs: int, yes: bool) -> None:
|
|
56
|
+
from vecshift import ui
|
|
57
|
+
|
|
51
58
|
remote = [i for i in items if not i.spec.is_local]
|
|
52
59
|
remote_gen = generator is not None and not generator.is_local
|
|
60
|
+
if ui.fancy():
|
|
61
|
+
ui.title("bench")
|
|
62
|
+
rows: list[list[str | Any]] = [
|
|
63
|
+
[
|
|
64
|
+
item.spec.name,
|
|
65
|
+
f"~{item.est_tokens:,}",
|
|
66
|
+
money(item.est_cost) if item.est_cost is not None else "unknown price",
|
|
67
|
+
"local" if item.spec.is_local else (item.spec.url or ""),
|
|
68
|
+
]
|
|
69
|
+
for item in items
|
|
70
|
+
]
|
|
71
|
+
if generator is not None:
|
|
72
|
+
rows.append([generator.name, "generates queries", "", generator.url or ""])
|
|
73
|
+
ui.table(["Model", "Tokens", "Cost", "Where"], rows, right=frozenset({1, 2}))
|
|
74
|
+
if not remote and not remote_gen:
|
|
75
|
+
return
|
|
76
|
+
ui.warn(f" This sends the text of {n_docs:,} sampled documents to the services above.")
|
|
77
|
+
if not yes and not ui.confirm("Continue?", default=False):
|
|
78
|
+
raise typer.Exit(1)
|
|
79
|
+
return
|
|
53
80
|
typer.echo("Plan", err=True)
|
|
54
81
|
for item in items:
|
|
55
82
|
where = "local" if item.spec.is_local else (item.spec.url or "")
|
|
@@ -74,13 +101,17 @@ def _confirm(items: list[PlanItem], generator: ModelSpec | None, n_docs: int, ye
|
|
|
74
101
|
|
|
75
102
|
|
|
76
103
|
def _render(result: BenchResult) -> None:
|
|
104
|
+
from vecshift import ui
|
|
77
105
|
from vecshift.bench.runner import ModelResult
|
|
78
106
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
107
|
+
fancy = ui.fancy()
|
|
108
|
+
|
|
109
|
+
if not fancy:
|
|
110
|
+
typer.secho(
|
|
111
|
+
f"vecshift bench · {result.documents:,} documents, {result.queries:,} queries "
|
|
112
|
+
f"({result.query_source}) from {result.source}",
|
|
113
|
+
bold=True,
|
|
114
|
+
)
|
|
84
115
|
rows: list[tuple[str, ...]] = []
|
|
85
116
|
ranked = result.ranked()
|
|
86
117
|
for rank, m in enumerate(ranked, 1):
|
|
@@ -115,7 +146,23 @@ def _render(result: BenchResult) -> None:
|
|
|
115
146
|
"Per 1M docs",
|
|
116
147
|
"Per 1M vecs",
|
|
117
148
|
)
|
|
118
|
-
if rows:
|
|
149
|
+
if rows and fancy:
|
|
150
|
+
from rich.text import Text
|
|
151
|
+
|
|
152
|
+
ui.section(
|
|
153
|
+
f"Results · {result.documents:,} documents, {result.queries:,} queries "
|
|
154
|
+
f"({result.query_source}) from {result.source}"
|
|
155
|
+
)
|
|
156
|
+
styled: list[list[str | Text]] = [
|
|
157
|
+
[
|
|
158
|
+
Text(r[0], style="bold green" if r[0] == "1" else "dim"),
|
|
159
|
+
Text(r[1], style="bold"),
|
|
160
|
+
*r[2:],
|
|
161
|
+
]
|
|
162
|
+
for r in rows
|
|
163
|
+
]
|
|
164
|
+
ui.table(header, styled, right=frozenset({0, 2, 3, 4, 5, 6, 7, 8}))
|
|
165
|
+
elif rows:
|
|
119
166
|
widths = [max(len(r[i]) for r in [header, *rows]) for i in range(len(header))]
|
|
120
167
|
right = {0, 2, 3, 4, 5, 6, 7, 8}
|
|
121
168
|
|
|
@@ -132,9 +179,17 @@ def _render(result: BenchResult) -> None:
|
|
|
132
179
|
|
|
133
180
|
failed: list[ModelResult] = [m for m in ranked if m.error]
|
|
134
181
|
for m in failed:
|
|
135
|
-
|
|
182
|
+
if fancy:
|
|
183
|
+
ui.error(f"{m.name} failed: {m.error}")
|
|
184
|
+
else:
|
|
185
|
+
typer.secho(f"\n✖ {m.name} failed: {m.error}", fg=typer.colors.RED)
|
|
136
186
|
for note in result.notes:
|
|
137
|
-
|
|
187
|
+
if fancy:
|
|
188
|
+
ui.note(f" {note}")
|
|
189
|
+
else:
|
|
190
|
+
typer.secho(f"\n{note}", dim=True)
|
|
191
|
+
if fancy:
|
|
192
|
+
ui.console.print()
|
|
138
193
|
|
|
139
194
|
|
|
140
195
|
def bench(
|
|
@@ -12,6 +12,7 @@ from typing import TYPE_CHECKING, Annotated, Any
|
|
|
12
12
|
|
|
13
13
|
import typer
|
|
14
14
|
|
|
15
|
+
from vecshift import ui
|
|
15
16
|
from vecshift.cli_plan import DEFAULT_JOB, _fail, load
|
|
16
17
|
from vecshift.cli_style import finding_lines
|
|
17
18
|
|
|
@@ -85,8 +86,16 @@ def _locate(job_file: Path) -> _Located:
|
|
|
85
86
|
|
|
86
87
|
|
|
87
88
|
def _confirm(message: str, yes: bool) -> None:
|
|
89
|
+
from vecshift import ui
|
|
90
|
+
|
|
88
91
|
if yes:
|
|
89
92
|
return
|
|
93
|
+
if ui.fancy():
|
|
94
|
+
ui.console.print()
|
|
95
|
+
ui.note(" " + " ".join(message.split()))
|
|
96
|
+
if not ui.confirm("Continue?", default=False):
|
|
97
|
+
raise typer.Exit(1)
|
|
98
|
+
return
|
|
90
99
|
typer.echo(message, err=True)
|
|
91
100
|
if not sys.stdin.isatty():
|
|
92
101
|
raise _fail("Not changing the database without confirmation.", "Pass --yes to proceed.")
|
|
@@ -219,7 +228,7 @@ def cutover(
|
|
|
219
228
|
if check or blocked:
|
|
220
229
|
return {"status": "ready" if ready.ok else "blocked", "readiness": ready}
|
|
221
230
|
if not output_json:
|
|
222
|
-
|
|
231
|
+
ui.title("cutover", found.job.name)
|
|
223
232
|
_show([f for f in ready.findings if f.severity.rank >= 1], output_json)
|
|
224
233
|
where = "your machine" if found.spec.is_local else found.spec.url
|
|
225
234
|
sends = (
|
|
@@ -284,24 +293,24 @@ def cutover(
|
|
|
284
293
|
)
|
|
285
294
|
)
|
|
286
295
|
elif outcome["status"] in {"ready", "blocked"}:
|
|
287
|
-
|
|
296
|
+
ui.title("cutover", found.job.name)
|
|
288
297
|
finding_lines(ready.findings)
|
|
289
|
-
|
|
290
|
-
("Ready to cut over."
|
|
291
|
-
if
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
298
|
+
if ready.ok:
|
|
299
|
+
ui.verdict("Ready to cut over.")
|
|
300
|
+
if ui.fancy():
|
|
301
|
+
ui.next_step("vecshift cutover")
|
|
302
|
+
else:
|
|
303
|
+
ui.verdict("Not ready to cut over.", "error")
|
|
295
304
|
else:
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
f"
|
|
305
|
+
ui.verdict("Cut over.")
|
|
306
|
+
ui.detail(
|
|
307
|
+
f"{lay.table}.{lay.live} now holds {found.spec.name} vectors; the old ones are "
|
|
299
308
|
f"kept in {lay.previous}."
|
|
300
309
|
)
|
|
301
310
|
if outcome.get("caught_up"):
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
f"
|
|
311
|
+
ui.detail(f"Embedded {outcome['caught_up']:,} late rows first.")
|
|
312
|
+
ui.next_step(
|
|
313
|
+
f"make sure your application embeds queries and new rows with "
|
|
305
314
|
f"{found.spec.name}.\nUndo with: vecshift rollback. When you're sure, drop "
|
|
306
315
|
f"{lay.previous} to free its space."
|
|
307
316
|
)
|
|
@@ -363,16 +372,20 @@ def rollback(
|
|
|
363
372
|
)
|
|
364
373
|
)
|
|
365
374
|
return
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
375
|
+
if ui.fancy():
|
|
376
|
+
ui.title("rollback", found.job.name)
|
|
377
|
+
ui.verdict("Rolled back.")
|
|
378
|
+
ui.detail(
|
|
379
|
+
f"{lay.table}.{lay.live} holds the old vectors again; the new ones are back in "
|
|
369
380
|
f"{lay.target}, kept in sync for another cutover."
|
|
370
381
|
)
|
|
371
382
|
if missing:
|
|
372
|
-
|
|
373
|
-
f"
|
|
383
|
+
ui.detail(
|
|
384
|
+
f"{missing:,} rows were added or edited after cutover and have no old-model "
|
|
374
385
|
"vector. Your application needs to embed them with the old model."
|
|
375
386
|
)
|
|
387
|
+
if ui.fancy():
|
|
388
|
+
ui.console.print()
|
|
376
389
|
|
|
377
390
|
|
|
378
391
|
def cleanup(
|
|
@@ -406,14 +419,18 @@ def cleanup(
|
|
|
406
419
|
"Drop or change them first.",
|
|
407
420
|
)
|
|
408
421
|
if not yes:
|
|
409
|
-
|
|
422
|
+
warning = (
|
|
410
423
|
f"Cleanup drops {lay.schema}.{lay.table}.{lay.previous} and its index. The "
|
|
411
|
-
"old vectors are gone for good, and rollback is no longer possible."
|
|
412
|
-
err=True,
|
|
424
|
+
"old vectors are gone for good, and rollback is no longer possible."
|
|
413
425
|
)
|
|
414
|
-
if
|
|
415
|
-
|
|
416
|
-
|
|
426
|
+
if ui.fancy():
|
|
427
|
+
ui.warn(warning)
|
|
428
|
+
typed = ui.text(f"Type {lay.previous} to confirm")
|
|
429
|
+
else:
|
|
430
|
+
typer.echo(warning, err=True)
|
|
431
|
+
if not sys.stdin.isatty():
|
|
432
|
+
raise _fail("Not dropping anything without confirmation.", "Pass --yes.")
|
|
433
|
+
typed = typer.prompt(f"Type {lay.previous} to confirm", err=True, default="")
|
|
417
434
|
if typed != lay.previous:
|
|
418
435
|
raise _fail("That didn't match, so nothing was dropped.")
|
|
419
436
|
switch.cleanup()
|
|
@@ -428,4 +445,8 @@ def cleanup(
|
|
|
428
445
|
if output_json:
|
|
429
446
|
typer.echo(json.dumps({"status": "cleaned_up", "dropped": lay.previous}))
|
|
430
447
|
else:
|
|
431
|
-
|
|
448
|
+
if ui.fancy():
|
|
449
|
+
ui.title("cleanup", found.job.name)
|
|
450
|
+
ui.verdict(f"Dropped {lay.previous}. The migration is complete.")
|
|
451
|
+
if ui.fancy():
|
|
452
|
+
ui.console.print()
|